diff --git a/cms/decorators.py b/cms/decorators.py new file mode 100644 index 0000000..d7440d3 --- /dev/null +++ b/cms/decorators.py @@ -0,0 +1,96 @@ +from django.template.loader import render_to_string +from django.conf.urls import url +from django.core.urlresolvers import reverse +from django.utils.text import slugify + + +def __part_normalize(value, default): + value = value if value else default + return slugify(value.lower()) + + +def parts(cls, name = None, pattern = None): + """ + the decorated class is a parts class, and contains part + functions. Look `part` decorator doc for more info. + """ + name = __part_normalize(name, cls.__name__) + pattern = __part_normalize(pattern, cls.__name__) + + cls._parts = [] + for part in cls.__dict__.values(): + if not hasattr(part, 'is_part'): + continue + + part.name = name + '_' + part.name + part.pattern = pattern + '/' + part.pattern + part = url(part.pattern, name = part.name, + view = part, kwargs = {'cl': cls}) + cls._parts.append(part) + return cls + + +def part(view, name = None, pattern = None): + """ + A part function is a view that is used to retrieve data dynamically, + e.g. from Javascript with XMLHttpRequest. A part function is a classmethod + that returns a string and has the following signature: + + `part(cl, request, parent, *args, **kwargs)` + + When a section with parts is added to the website, the parts' urls + are added to the website's one and make them available. + + A part function can have the following parameters: + * name: part.name or part.__name__ + * pattern: part.pattern or part.__name__ + + An extra method `url` is added to the part function to return the adequate + url. + + Theses are combined with the containing parts class params such as: + * name: parts.name + '_' + part.name + * pattern: parts.pattern + '/' + part.pattern + + The parts class will have an attribute '_parts' as list of generated + urls. + """ + if hasattr(view, 'is_part'): + return view + + def view_(request, as_str = False, cl = None, *args, **kwargs): + v = view(cl, request, *args, **kwargs) + if as_str: + return v + return HttpResponse(v) + + def url(*args, **kwargs): + return reverse(view_.name, *args, **kwargs) + + view_.name = __part_normalize(name, view.__name__) + view_.pattern = __part_normalize(pattern, view.__name__) + view_.is_part = True + view_.url = url + return view_ + +def template(template_name): + """ + the decorated function returns a context that is used to + render a template value. + + * template_name: name of the template to use + * hide_empty: an empty context returns an empty string + """ + def wrapper(func): + def view_(cl, request, *args, **kwargs): + context = func(cl, request, *args, **kwargs) + if not context and hide_empty: + return '' + context['embed'] = True + return render_to_string(template_name, context, request=request) + view_.__name__ = func.__name__ + return view_ + return wrapper + + + diff --git a/cms/models.py b/cms/models.py index 5436bc6..915bdc0 100644 --- a/cms/models.py +++ b/cms/models.py @@ -212,7 +212,7 @@ class Post (models.Model, Routable): abstract = True -class RelatedPostBase (models.base.ModelBase): +class RelatedMeta (models.base.ModelBase): """ Metaclass for RelatedPost children. """ @@ -310,7 +310,7 @@ class RelatedPostBase (models.base.ModelBase): return model -class RelatedPost (Post, metaclass = RelatedPostBase): +class RelatedPost (Post, metaclass = RelatedMeta): """ Post linked to an object of other model. This object is accessible through the field "related". @@ -346,29 +346,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase): """ Relation descriptor used to generate and manage the related object. - * model: model of the related object - * bindings: values that are bound between the post and the related - object. When the post is saved, these fields are updated on it. - It is a dict of { post_attr: rel_attr } - - If there is a post_attr "thread", the corresponding rel_attr is used - to update the post thread to the correct Post model (in order to - establish a parent-child relation between two models) - - When a callable is set as bound value, it will be called to retrieve - the value, as: callable_func(post, related) - - Note: bound values can be any value, not only Django field. - * post_to_rel: auto update related object when post is updated - * rel_to_post: auto update the post when related object is updated - * thread_model: generated by the metaclass, points to the RelatedPost - model generated for the bindings.thread object. - * field_args: dict of arguments to pass to the ForeignKey constructor, - such as: ForeignKey(related_model, **field_args) - * auto_create: automatically create a RelatedPost for each new item of - the related object and init it with bounded values. Use 'post_save' - signal. If auto_create is callable, use `auto_create(related_object)`. - Be careful with post_to_rel! * There is no check of permissions when related object is synchronised from the post, so be careful when enabling post_to_rel. @@ -376,12 +353,48 @@ class RelatedPost (Post, metaclass = RelatedPostBase): (sub-)class thread_model, the related parent is set to None """ model = None - bindings = None # values to map { post_attr: rel_attr } + """ + model of the related object + """ + bindings = None + """ + dict of `post_attr: rel_attr` that represent bindings of values + between the post and the related object. Field are updated according + to `post_to_rel` and `rel_to_post`. + + If there is a post_attr "thread", the corresponding rel_attr is used + to update the post thread to the correct Post model (in order to + establish a parent-child relation between two models) + + When a callable is set as `rel_attr`, it will be called to retrieve + the value, as `rel_attr(post, related)` + + note: bound values can be any value, not only Django field. + """ post_to_rel = False + """ + update related object when the post is saved, using bindings + """ rel_to_post = False + """ + update the post when related object is updated, using bindings + """ thread_model = None + """ + generated by the metaclass, points to the RelatedPost model + generated for the bindings.thread object. + """ field_args = None + """ + dict of arguments to pass to the ForeignKey constructor, such as + `ForeignKey(related_model, **field_args)` + """ auto_create = False + """ + automatically create a RelatedPost for each new item of the related + object and init it with bounded values. Use 'post_save' signal. If + auto_create is callable, use `auto_create(related_object)`. + """ def get_rel_attr(self, attr): attr = self._relation.bindings.get(attr) diff --git a/cms/sections.py b/cms/sections.py index 604a64c..49642e0 100644 --- a/cms/sections.py +++ b/cms/sections.py @@ -3,9 +3,10 @@ Define different Section css_class that can be used by views.Sections; """ import re -from django.templatetags.static import static from django.template.loader import render_to_string from django.views.generic.base import View +from django.templatetags.static import static +from django.http import HttpResponse from django.contrib import messages from django.utils.html import escape from django.utils.translation import ugettext as _, ugettext_lazy @@ -75,9 +76,6 @@ class Section(Viewable, View): * title: title of the section * header: header of the section * footer: footer of the section - - * force_object: (can be persistent) related object - """ template_name = 'aircox/cms/website.html' @@ -88,7 +86,6 @@ class Section(Viewable, View): title = '' header = '' footer = '' - force_object = None request = None object = None diff --git a/cms/static/aircox/cms/scripts.js b/cms/static/aircox/cms/scripts.js new file mode 100644 index 0000000..9901c44 --- /dev/null +++ b/cms/static/aircox/cms/scripts.js @@ -0,0 +1,163 @@ + +function Part(url = '', params = '') { + return new Part_(url, params); +} + +// Small utility used to make XMLHttpRequests, and map results to other +// objects +function Part_(url = '', params = '') { + this.url = url; + this.params = params; + this.selectors = []; + this.actions = []; +} + +Part_.prototype = { + // XMLHttpRequest object used to retrieve data + xhr: null, + + // delayed actions that have been registered + actions: null, + + // registered selectors + selectors: null, + + /// parse request result and save in this.stanza + __parse_dom: function() { + var doc = document.implementation.createHTMLDocument('xhr').documentElement; + doc.innerHTML = this.xhr.responseText; + this.stanza = doc; + }, + + // make an xhr request, and call callback(err, xhr) if given + get: function() { + var self = this; + + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if(xhr.readyState != 4) + return + + // TODO: error handling + var err = self.xhr.status != 200 && self.xhr.status; + if(err) + return; + + for(var i = 0; i < self.actions.length; i++) + self.actions[i].apply(self); + } + + if(this.params) + xhr.open('GET', this.url + '?' + this.params, true); + else + xhr.open('GET', this.url, true); + this.xhr = xhr; + return this; + }, + + // send request + send: function() { + this.xhr.send(); + return this; + }, + + // set selectors. if callback is set, call this callback + // once data are retrieved with an object of + // `selector_name: select_result` + select: function(selectors, callback = undefined) { + for(var i in selectors) { + selector = selectors[i]; + if(!selector.sort) + selector = [selector] + + selector = new Part_.Selector(i, selector[0], selector[1], selector[2]) + this.selectors.push(selector) + } + + if(callback) { + var self = this; + this.actions.push(function() { + var r = {} + for(var i in self.selectors) + r[i] = self.selectors[i].select(self.stanza); + callback(r); + }); + } + return this; + }, + + // map data using this.selectors on xhr result *and* dest + map: function(dest) { + var self = this; + this.actions.push(function() { + if(!self.stanza) + self.__parse_dom(); + + for(var i = 0; i < self.selectors.length; i++) { + selector = self.selectors[i] + selector.map(self.stanza, dest); + } + }); + return this; + }, + + // add an action to the list of actions + on: function(callback) { + this.actions.push(callback) + return this; + }, +}; + + +Part_.Selector = function(name, selector, attribute = null, all = false) { + this.name = name; + this.selector = selector; + this.attribute = attribute; + this.all = all; +} + +Part_.Selector.prototype = { + select: function(obj, use_attr = true) { + if(!this.all) { + obj = obj.querySelectorAll(this.selector) + if(obj) + obj = obj[0]; + return (this.attribute && use_attr && obj) ? obj[this.attribute] : obj; + } + + obj = obj.querySelectorAll(this.selector); + if(!obj) + return; + + r = [] + for(var i = 0; i < obj.length; i++) { + r.push(this.attribute && use_attr ? obj[i][this.attribute] : obj[i]) + } + return r; + }, + + map: function(src, dst) { + src_qs = this.select(src, false); + dst_qs = this.select(dst, false); + if(!src_qs || !dst_qs) + return + + if(!this.all) { + src_qs = [ src_qs ]; + dst_qs = [ dst_qs ]; + } + + var size = Math.min(src_qs.length, dst_qs.length); + for(var i = 0; i < size; i++) { + var src = src_qs[i]; + var dst = dst_qs[i]; + + if(this.attribute) + dst[this.attribute] = src[this.attribute]; + else + dst.parentNode.replaceChild(src, dst); + } + }, +} + + diff --git a/cms/templates/aircox/cms/list.html b/cms/templates/aircox/cms/list.html index f62975b..8ce2d98 100644 --- a/cms/templates/aircox/cms/list.html +++ b/cms/templates/aircox/cms/list.html @@ -8,65 +8,7 @@ {% block content %}