From 392d48ac0c4f6cde7a22545b6bfc5c7486fc2779 Mon Sep 17 00:00:00 2001 From: bkfox Date: Thu, 2 Jun 2016 01:05:38 +0200 Subject: [PATCH] fix error with tags; add callable for related bindings; comments in PostListView; ... --- cms/README.md | 4 ++ cms/models.py | 85 ++++++++++++++++------------ cms/routes.py | 2 +- cms/sections.py | 25 +++++++- cms/templates/aircox/cms/detail.html | 33 ++++++----- cms/views.py | 21 ++++--- cms/website.py | 38 ++++++++++--- 7 files changed, 134 insertions(+), 74 deletions(-) diff --git a/cms/README.md b/cms/README.md index 5f3d89c..e18d852 100644 --- a/cms/README.md +++ b/cms/README.md @@ -40,6 +40,10 @@ class MyModelPost(RelatedPost): } ``` +Note: it is possible to assign a function as a bounded value; in such case, the +function will be called using arguments **(post, related_object)**. + + ## Routes Routes are used to generate the URLs of the website. We provide some of the common routes: for the detail view of course, but also to select all posts or diff --git a/cms/models.py b/cms/models.py index 3f51579..b3a8fe1 100644 --- a/cms/models.py +++ b/cms/models.py @@ -17,7 +17,33 @@ from aircox.cms import routes from aircox.cms import settings -class Comment(models.Model): +class Routable: + @classmethod + def get_with_thread(cl, thread = None, queryset = None, + thread_model = None, thread_id = None): + """ + Return posts of the cl's type that are children of the given thread. + """ + if not queryset: + queryset = cl.objects + + if thread: + thread_model = type(thread) + thread_id = thread.id + + thread_model = ContentType.objects.get_for_model(thread_model) + return queryset.filter( + thread_id = thread_id, + thread_type__pk = thread_model.id + ) + + @classmethod + def route_url(cl, route, kwargs = None): + name = cl._website.name_of_model(cl) + name = route.get_view_name(name) + return reverse(name, kwargs = kwargs) + +class Comment(models.Model, Routable): thread_type = models.ForeignKey( ContentType, on_delete=models.SET_NULL, @@ -71,7 +97,7 @@ class Comment(models.Model): return super().save(*args, **kwargs) -class Post (models.Model): +class Post (models.Model, Routable): """ Base model that can be used as is if wanted. Represent a generic publication on the website. @@ -115,34 +141,15 @@ class Post (models.Model): default = '', ) image = models.ImageField( - blank = True, null = True + blank = True, null = True, ) tags = TaggableManager( - _('tags'), + verbose_name = _('tags'), blank = True, ) search_fields = [ 'title', 'content' ] - @classmethod - def get_with_thread(cl, thread = None, queryset = None, - thread_model = None, thread_id = None): - """ - Return posts of the cl's type that are children of the given thread. - """ - if not queryset: - queryset = cl.objects - - if thread: - thread_model = type(thread) - thread_id = thread.id - - thread_model = ContentType.objects.get_for_model(thread_model) - return queryset.filter( - thread_id = thread_id, - thread_type__pk = thread_model.id - ) - def get_comments(self): """ Return comments pointing to this post @@ -166,12 +173,6 @@ class Post (models.Model): ) return qs - @classmethod - def route_url(cl, route, kwargs = None): - name = cl._website.name_of_model(cl) - name = route.get_view_name(name) - return reverse(name, kwargs = kwargs) - def make_safe(self): self.title = bleach.clean( self.title, @@ -183,11 +184,18 @@ class Post (models.Model): tags=settings.AIRCOX_CMS_BLEACH_CONTENT_TAGS, attributes=settings.AIRCOX_CMS_BLEACH_CONTENT_ATTRS ) - self.tags = [ bleach.clean(tag, tags=[]) for tag in self.tags.all() ] + if self.pk: + self.tags.set(*[ + bleach.clean(tag, tags=[]) + for tag in self.tags.all() + ]) def save(self, make_safe = True, *args, **kwargs): if make_safe: self.make_safe() + if self.date and self.date.tzinfo is None or \ + self.date.tzinfo.utcoffset(self.date) is None: + timezone.make_aware(self.date) return super().save(*args, **kwargs) class Meta: @@ -353,6 +361,9 @@ class RelatedPost (Post, metaclass = RelatedPostBase): 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 @@ -380,6 +391,8 @@ class RelatedPost (Post, metaclass = RelatedPostBase): def get_rel_attr(self, attr): attr = self._relation.bindings.get(attr) + if callable(attr): + return attr(self, self.related) return getattr(self.related, attr) if attr else None def set_rel_attr(self, attr, value): @@ -432,8 +445,8 @@ class RelatedPost (Post, metaclass = RelatedPostBase): for attr, rel_attr in rel.bindings.items(): if attr == 'thread': continue - self.set_rel_attr - value = getattr(self.related, rel_attr) + value = rel_attr(self, self.related) if callable(rel_attr) else \ + getattr(self.related, rel_attr) set_attr(attr, value) if rel.thread_model: @@ -453,14 +466,16 @@ class RelatedPost (Post, metaclass = RelatedPostBase): if self.pk and self._relation.rel_to_post: self.rel_to_post(False) - def save (self, avoid_sync = False, *args, **kwargs): + def save (self, avoid_sync = False, save = True, *args, **kwargs): """ - If avoid_relation, do not synchronise the post/related object. + * avoid_sync: do not synchronise the post/related object; + * save: if False, does not call parent save functions """ if not avoid_sync: if not self.pk and self._relation.rel_to_post: self.rel_to_post(False) if self._relation.post_to_rel: self.post_to_rel(True) - super().save(*args, **kwargs) + if save: + super().save(*args, **kwargs) diff --git a/cms/routes.py b/cms/routes.py index fb79976..84ef7a5 100644 --- a/cms/routes.py +++ b/cms/routes.py @@ -108,7 +108,6 @@ class ThreadRoute(Route): ('pk', '[0-9]+'), ] - @classmethod def get_thread(cl, model, thread_model, pk=None): """ @@ -183,3 +182,4 @@ class SearchRoute(Route): 'search': request.GET.get('q') or '', } + diff --git a/cms/sections.py b/cms/sections.py index 6577fa3..ef028c8 100644 --- a/cms/sections.py +++ b/cms/sections.py @@ -24,6 +24,8 @@ class Section(View): to Section configuration/rendering. However, some data are considered as temporary, and are reset at each rendering, using given arguments. + When get_context_data returns None, returns an empty string + ! Important Note: values given for rendering are considered as safe HTML in templates. @@ -90,6 +92,8 @@ class Section(View): self.kwargs = kwargs context = self.get_context_data() + if not context: + return '' return render_to_string(self.template_name, context, request=request) @@ -207,11 +211,15 @@ class List(Section): return self.object_list def get_context_data(self): + object_list = self.object_list or self.get_object_list() + if not object_list and not self.message_empty: + return + context = super().get_context_data() context.update({ 'base_template': 'aircox/cms/section.html', 'list': self, - 'object_list': self.object_list or self.get_object_list(), + 'object_list': object_list, }) return context @@ -223,9 +231,9 @@ class Comments(List): """ title=_('Comments') css_class='comments' - paginate_by = 0 truncate = 0 fields = [ 'date', 'time', 'author', 'content' ] + message_empty = _('no comment yet') comment_form = None success_message = ( _('Your message is awaiting for approval'), @@ -240,6 +248,19 @@ class Comments(List): attrs={ 'id': comment.id }) for comment in qs ] + @property + def url(self): + import aircox.cms.models as models + import aircox.cms.routes as routes + if self.object: + return models.Comment.route_url(routes.ThreadRoute, { + 'pk': self.object.id, + 'thread_model': self.object._website.name_of_model( + self.object.__class__ + ), + }) + return '' + def get_context_data(self): post = self.object if hasattr(post, 'allow_comments') and post.allow_comments: diff --git a/cms/templates/aircox/cms/detail.html b/cms/templates/aircox/cms/detail.html index 7e28e6b..ca6eacf 100644 --- a/cms/templates/aircox/cms/detail.html +++ b/cms/templates/aircox/cms/detail.html @@ -6,25 +6,24 @@ {% endblock %} {% block pre_title %} -
-{% if object.thread %} -
- {{ object|threads:' > '|safe }} -
-{% endif %} +
+ {% if object.thread %} +
+ {{ object|threads:' > '|safe }} +
+ {% endif %} - - -{% if object.tags %} -{# TODO: url to the tags #} -
- {{ object.tags.all|join:', ' }} -
-{% endif %} + + {% if object.tags.all %} + {# TODO: url to the tags #} +
+ {{ object.tags.all|join:', ' }} +
+ {% endif %}
{% endblock %} diff --git a/cms/views.py b/cms/views.py index d5f1a43..1295e7f 100644 --- a/cms/views.py +++ b/cms/views.py @@ -44,20 +44,26 @@ class PostListView(PostBaseView, ListView): * embed: view is embedded, render only the list * exclude: exclude item of the given id * order: 'desc' or 'asc' - * fields: fields to render * page: page number """ template_name = 'aircox/cms/list.html' allow_empty = True - paginate_by = 25 + paginate_by = 30 model = None route = None list = None + css_class = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if not self.list: + self.list = sections.List( + truncate = 32, + fields = [ 'date', 'time', 'image', 'title', 'content' ], + ) + def dispatch(self, request, *args, **kwargs): self.route = self.kwargs.get('route') or self.route return super().dispatch(request, *args, **kwargs) @@ -104,15 +110,8 @@ class PostListView(PostBaseView, ListView): context['title'] = title context['base_template'] = 'aircox/cms/website.html' - context['css_class'] = 'list' - - if not self.list: - import aircox.cms.sections as sections - self.list = sections.List( - truncate = 32, - fields = [ 'date', 'time', 'image', 'title', 'content' ], - ) - + context['css_class'] = 'list' if not self.css_class else \ + 'list ' + self.css_class context['list'] = self.list # FIXME: list.url = if self.route: self.model(self.route, self.kwargs) else '' return context diff --git a/cms/website.py b/cms/website.py index ece4722..93a4f01 100644 --- a/cms/website.py +++ b/cms/website.py @@ -18,12 +18,13 @@ class Website: # user interaction allow_comments = True auto_publish_comments = False + comments_routes = True # components urls = [] registry = {} - def __init__ (self, menus = None, **kwargs): + def __init__(self, menus = None, **kwargs): """ * menus: a list of menus to add to the website """ @@ -36,13 +37,34 @@ class Website: for menu in menus: self.set_menu(menu) + if self.comments_routes: + self.register_comments_routes() - def name_of_model (self, model): + + def name_of_model(self, model): for name, _model in self.registry.items(): if model is _model: return name - def register_model (self, name, model): + def register_comments_routes(self): + """ + Register routes for comments, for the moment, only: + * ThreadRoute + """ + import aircox.cms.models as models + import aircox.cms.sections as sections + + self.register_list( + 'comment', models.Comment, + routes = [routes.ThreadRoute], + css_class = 'comments', + list = sections.Comments( + truncate = 30, + fields = ['content','author','date','time'], + ) + ) + + def register_model(self, name, model): """ Register a model and return the name under which it is registered. Raise a ValueError if another model is yet associated under this name. @@ -54,7 +76,7 @@ class Website: model._website = self return name - def register_detail (self, name, model, view = views.PostDetailView, + def register_detail(self, name, model, view = views.PostDetailView, **view_kwargs): """ Register a model and the detail view @@ -68,7 +90,7 @@ class Website: self.urls.append(routes.DetailRoute.as_url(name, view)) self.registry[name] = model - def register_list (self, name, model, view = views.PostListView, + def register_list(self, name, model, view = views.PostListView, routes = [], **view_kwargs): """ Register a model and the given list view using the given routes @@ -82,7 +104,7 @@ class Website: self.urls += [ route.as_url(name, view) for route in routes ] self.registry[name] = model - def register (self, name, model, sections = None, routes = None, + def register(self, name, model, sections = None, routes = None, list_view = views.PostListView, detail_view = views.PostDetailView, list_kwargs = {}, detail_kwargs = {}): @@ -104,7 +126,7 @@ class Website: **list_kwargs ) - def set_menu (self, menu): + def set_menu(self, menu): """ Set a menu, and remove any previous menu at the same position """ @@ -114,7 +136,7 @@ class Website: menu.tag = 'side' self.menus[menu.position] = menu - def get_menu (self, position): + def get_menu(self, position): """ Get an enabled menu by its position """