Skip to content

Commit

Permalink
Merge pull request #1 from ksiazkowicz/next
Browse files Browse the repository at this point in the history
Next
  • Loading branch information
ksiazkowicz committed Feb 18, 2018
2 parents 22c3a98 + 195e1a5 commit 1a95160
Show file tree
Hide file tree
Showing 20 changed files with 733 additions and 22 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ ENV/

# mypy
.mypy_cache/


.vscode
.idea
1 change: 1 addition & 0 deletions django_decadence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .helpers import update # noqa
34 changes: 34 additions & 0 deletions django_decadence/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from channels import Group
from channels.generic.websockets import JsonWebsocketConsumer


class UpdateConsumer(JsonWebsocketConsumer):
"""
Update Consumer
Handles logic for adding user sessions to specific Groups. Handles
subscribe/unsubscribe requests.
"""
http_user = True

def connect(self, message, **kwargs):
message.reply_channel.send({'accept': True})

def receive(self, content, **kwargs):
messages = content.get("batch", [content, ])

for message in messages:
subscribe = message.get("subscribe", False)
group = message.get("group", None)

if not group:
continue

if subscribe:
Group(group).add(self.message.reply_channel)
else:
Group(group).remove(self.message.reply_channel)

def disconnect(self, message, **kwargs):
pass
37 changes: 37 additions & 0 deletions django_decadence/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import os
import json
from django.conf import settings
from channels import Group



DECADENCE_DIR = getattr(settings, "DECADENCE_DIR", os.path.join("includes", "decadence"))
Expand All @@ -16,3 +19,37 @@ def check_template_path(path):

if not normalized.startswith(DECADENCE_DIR):
raise Exception("Template path '%s' is outside '%s'" % normalized, DECADENCE_DIR)


def update(type_name="update_value", path="", value="", classname="",
attribute_name=""):
"""
Pushes out content updates to user through our update channel.
:param type_name: type of content update:
- toggle_class - adds/remove given class from element, if value is
true, it's added
- update_attribute - replaces html attribute value with given value
- update_value - updates content of html element with given value
:param path: name of update group
:param value: new value
:param classname: name of class to be added/removed to element (optional)
:param attribute_name: name of attribute which value will be replaced
(optional)
"""
# default data
data = {
"type": type_name,
"value": value,
"path": path,
}

# add optional arguments for different types
if type_name == "toggle_class":
data["class"] = classname
if type_name == "update_attribute":
data["attribute_name"] = attribute_name

Group(path).send({"text": json.dumps(data)})
117 changes: 99 additions & 18 deletions django_decadence/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.db import models
from datetime import datetime, date, time
from django.template import Context, Template
import json
from django.db import models
from django.conf import settings
from django.core.serializers import serialize
from .helpers import update


class SerializableQuerySet(models.QuerySet):
Expand All @@ -22,26 +23,91 @@ class DecadenceModel(models.Model):
Implements a generic model that supports Decadence-specific features like
serialization.
"""
objects = DecadenceManager()
updates_excluded = []
"""list of fields excluded from updates"""

objects = DecadenceManager()

class Meta:
abstract = True

def get_update_path(self, field_name):
return "%(namespace)s-%(pk)d-%(field)s" % {
"namespace": str(self._meta),
"pk": self.pk,
"field": field_name
}


def push_update(self, original_data={}):
"""
Compares changes between old serialization data and new, then pushes out updates through Updates API.
"""
if not original_data:
return False

# try to serialize first
new_data = self.serialize()

# compare all the fields!
for key, value in original_data.items():
# check if key is excluded first
if key in self.updates_excluded:
continue

def serialize(self, user=None):
# get value and compare
new_value = new_data.get(key)

if new_value != value:
# first check if custom update for this field exists in case we need to override it
# (for example, href attribute)
try:
logic = getattr(self, "updates_%s" % key)

# this field might also be a callable for some reason
if callable(logic):
logic = logic()
except AttributeError:
logic = [{"type_name": "update_value", }, ]

# handle each operation
for operation in logic:
# copy operation dictionary
options = operation.copy()

# "value" field is optional
if not "value" in options.keys():
options["value"] = new_value

# add "path" to options
options["path"] = self.get_update_path(operation.get("field", key))

# pop "field" value from "options" if provided
try:
options.pop("field")
except KeyError:
pass

update(**options)


def serialize(self, user=None, fields=None):
"""
Attempts to generate a JSON serializable dictionary
based on current model
"""
# you might want to define a list of fields to be serialized
try:
fields = self.decadence_fields
except:
fields = [f.name for f in self._meta.get_fields()]
if not fields:
if hasattr(self, "decadence_fields"):
fields = self.decadence_fields
else:
fields = [f.name for f in self._meta.get_fields()]

# use Django's built-in model serialization
serialized = json.loads(serialize('json', [self], fields=fields))[0]["fields"]
serialized["id"] = self.pk

# begin serialization
serialized = {}
for field in fields:
value = ""

Expand All @@ -54,22 +120,37 @@ def serialize(self, user=None):

# check if is callable and call it
if callable(original_value):
original_value = original_value()
original_value = original_value(user)

# choose method based on field type
# run custom serialization
if type(original_value) in [str, bool, int, ]:
# nothing to do here
value = original_value
if type(original_value) in [datetime, date, time, ]:
# use Django template engine for cool verbose date string
value = Template("{{ date }}").render(Context({"date": self.date}))
elif isinstance(original_value, DecadenceModel):
# nested Decadence serialization
value = original_value.serialized()
elif isinstance(original_value, models.Model):
# check for fields overrides
overrides = []
if hasattr(settings, "DECADENCE_FIELD_OVERRIDES"):
overrides = settings.DECADENCE_FIELD_OVERRIDES.get(str(original_value._meta), [])

# use overrides if provided and use Django serializer
if len(overrides) > 0:
fields = json.loads(serialize('json', [original_value], fields=overrides))
else:
fields = json.loads(serialize('json', [original_value]))
fields = fields[0]["fields"]

# add pk to fields
fields["id"] = original_value.pk
value = fields
else:
# try to use Python's JSON serialization as fallback
# probably will NEVER EVER work but shhhhh...
value = json.loads(json.dumps(original_value))
continue

# save serialized value
serialized[field] = value

serialized["update_namespace"] = str(self._meta)
return serialized

3 changes: 3 additions & 0 deletions django_decadence/templates/includes/decadence/updatable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% if element %}<{{ element }} {% endif %}data-update-group='{{ path }}'{% if element %}{% if attrs %} {{ attrs }}{% endif %}>
{{ value }}
</{{ element}}>{% endif %}
55 changes: 55 additions & 0 deletions django_decadence/templatetags/decadence_tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
from django import template
from django.utils.dateparse import parse_datetime
from django_decadence.helpers import check_template_path

register = template.Library()

@register.filter()
def iso_date(value):
"""
Serialization returns an ISO date by default, this tags
allows converting it back for displaying it in template
"""
return parse_datetime(value)


@register.filter()
def serialize(value, user):
"""
Serialization returns an ISO date by default, this tags
allows converting it back for displaying it in template
"""
return value.serialize(user)


@register.simple_tag(takes_context=True)
def decadence_render(context, template_name, data, **kwargs):
"""
Expand Down Expand Up @@ -36,6 +55,42 @@ def decadence_render(context, template_name, data, **kwargs):
return template_obj.render(data)


@register.inclusion_tag("includes/decadence/updatable.html", takes_context=True)
def decadence_updatable(context, path, attrs="", element="span"):
"""
Generates data-update-group for given path in
Decadence templates
"""
update_path = "%(namespace)s-%(obj_id)s-%(path)s" % {
"namespace": context["update_namespace"],
"obj_id": context["id"],
"path": path
}
return {
"path": update_path,
"value": context[path.split(".")[0]], # ignore users context
"attrs": attrs,
"element": element
}


@register.inclusion_tag("includes/decadence/updatable.html", takes_context=True)
def updatable(context, obj, path, attrs="", element="span"):
"""
Generates data-update-group for given path in
Decadence templates
"""
user = context.request.user
field = path.split(".")[0] # ignore users context
value = obj.serialize(user, fields=[field])[field]
return {
"path": obj.get_update_path(path),
"value": value,
"attrs": attrs,
"element": element
}


@register.simple_tag
def value_by_key(obj, key):
return obj.get(key, "")
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = django-decadence
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

0 comments on commit 1a95160

Please sign in to comment.