fix error with tags; add callable for related bindings; comments in PostListView; ...

This commit is contained in:
bkfox 2016-06-02 01:05:38 +02:00
parent ad58d3c332
commit 392d48ac0c
7 changed files with 134 additions and 74 deletions

View File

@ -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
Routes are used to generate the URLs of the website. We provide some of the 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 common routes: for the detail view of course, but also to select all posts or

View File

@ -17,7 +17,33 @@ from aircox.cms import routes
from aircox.cms import settings 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( thread_type = models.ForeignKey(
ContentType, ContentType,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -71,7 +97,7 @@ class Comment(models.Model):
return super().save(*args, **kwargs) 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 Base model that can be used as is if wanted. Represent a generic
publication on the website. publication on the website.
@ -115,34 +141,15 @@ class Post (models.Model):
default = '', default = '',
) )
image = models.ImageField( image = models.ImageField(
blank = True, null = True blank = True, null = True,
) )
tags = TaggableManager( tags = TaggableManager(
_('tags'), verbose_name = _('tags'),
blank = True, blank = True,
) )
search_fields = [ 'title', 'content' ] 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): def get_comments(self):
""" """
Return comments pointing to this post Return comments pointing to this post
@ -166,12 +173,6 @@ class Post (models.Model):
) )
return qs 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): def make_safe(self):
self.title = bleach.clean( self.title = bleach.clean(
self.title, self.title,
@ -183,11 +184,18 @@ class Post (models.Model):
tags=settings.AIRCOX_CMS_BLEACH_CONTENT_TAGS, tags=settings.AIRCOX_CMS_BLEACH_CONTENT_TAGS,
attributes=settings.AIRCOX_CMS_BLEACH_CONTENT_ATTRS 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): def save(self, make_safe = True, *args, **kwargs):
if make_safe: if make_safe:
self.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) return super().save(*args, **kwargs)
class Meta: class Meta:
@ -353,6 +361,9 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
to update the post thread to the correct Post model (in order to to update the post thread to the correct Post model (in order to
establish a parent-child relation between two models) 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. Note: bound values can be any value, not only Django field.
* post_to_rel: auto update related object when post is updated * post_to_rel: auto update related object when post is updated
* rel_to_post: auto update the post when related object 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): def get_rel_attr(self, attr):
attr = self._relation.bindings.get(attr) attr = self._relation.bindings.get(attr)
if callable(attr):
return attr(self, self.related)
return getattr(self.related, attr) if attr else None return getattr(self.related, attr) if attr else None
def set_rel_attr(self, attr, value): def set_rel_attr(self, attr, value):
@ -432,8 +445,8 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
for attr, rel_attr in rel.bindings.items(): for attr, rel_attr in rel.bindings.items():
if attr == 'thread': if attr == 'thread':
continue continue
self.set_rel_attr value = rel_attr(self, self.related) if callable(rel_attr) else \
value = getattr(self.related, rel_attr) getattr(self.related, rel_attr)
set_attr(attr, value) set_attr(attr, value)
if rel.thread_model: if rel.thread_model:
@ -453,14 +466,16 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
if self.pk and self._relation.rel_to_post: if self.pk and self._relation.rel_to_post:
self.rel_to_post(False) 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 avoid_sync:
if not self.pk and self._relation.rel_to_post: if not self.pk and self._relation.rel_to_post:
self.rel_to_post(False) self.rel_to_post(False)
if self._relation.post_to_rel: if self._relation.post_to_rel:
self.post_to_rel(True) self.post_to_rel(True)
if save:
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -108,7 +108,6 @@ class ThreadRoute(Route):
('pk', '[0-9]+'), ('pk', '[0-9]+'),
] ]
@classmethod @classmethod
def get_thread(cl, model, thread_model, pk=None): def get_thread(cl, model, thread_model, pk=None):
""" """
@ -183,3 +182,4 @@ class SearchRoute(Route):
'search': request.GET.get('q') or '', 'search': request.GET.get('q') or '',
} }

View File

@ -24,6 +24,8 @@ class Section(View):
to Section configuration/rendering. However, some data are considered to Section configuration/rendering. However, some data are considered
as temporary, and are reset at each rendering, using given arguments. 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 ! Important Note: values given for rendering are considered as safe
HTML in templates. HTML in templates.
@ -90,6 +92,8 @@ class Section(View):
self.kwargs = kwargs self.kwargs = kwargs
context = self.get_context_data() context = self.get_context_data()
if not context:
return ''
return render_to_string(self.template_name, context, request=request) return render_to_string(self.template_name, context, request=request)
@ -207,11 +211,15 @@ class List(Section):
return self.object_list return self.object_list
def get_context_data(self): 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 = super().get_context_data()
context.update({ context.update({
'base_template': 'aircox/cms/section.html', 'base_template': 'aircox/cms/section.html',
'list': self, 'list': self,
'object_list': self.object_list or self.get_object_list(), 'object_list': object_list,
}) })
return context return context
@ -223,9 +231,9 @@ class Comments(List):
""" """
title=_('Comments') title=_('Comments')
css_class='comments' css_class='comments'
paginate_by = 0
truncate = 0 truncate = 0
fields = [ 'date', 'time', 'author', 'content' ] fields = [ 'date', 'time', 'author', 'content' ]
message_empty = _('no comment yet')
comment_form = None comment_form = None
success_message = ( _('Your message is awaiting for approval'), success_message = ( _('Your message is awaiting for approval'),
@ -240,6 +248,19 @@ class Comments(List):
attrs={ 'id': comment.id }) attrs={ 'id': comment.id })
for comment in qs ] 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): def get_context_data(self):
post = self.object post = self.object
if hasattr(post, 'allow_comments') and post.allow_comments: if hasattr(post, 'allow_comments') and post.allow_comments:

View File

@ -6,25 +6,24 @@
{% endblock %} {% endblock %}
{% block pre_title %} {% block pre_title %}
<div class="pre_title metadata"> <div class="pre_title meta">
{% if object.thread %} {% if object.thread %}
<div class="threads"> <div class="threads">
{{ object|threads:' > '|safe }} {{ object|threads:' > '|safe }}
</div> </div>
{% endif %} {% endif %}
<time datetime="{{ object.date }}"> <time datetime="{{ object.date }}">
{{ object.date|date:'l d F Y' }}, {{ object.date|date:'l d F Y' }},
{{ object.date|time:'H\hi' }} {{ object.date|time:'H\hi' }}
</time> </time>
{% if object.tags %} {% if object.tags.all %}
{# TODO: url to the tags #} {# TODO: url to the tags #}
<div class="tags"> <div class="tags">
{{ object.tags.all|join:', ' }} {{ object.tags.all|join:', ' }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -44,20 +44,26 @@ class PostListView(PostBaseView, ListView):
* embed: view is embedded, render only the list * embed: view is embedded, render only the list
* exclude: exclude item of the given id * exclude: exclude item of the given id
* order: 'desc' or 'asc' * order: 'desc' or 'asc'
* fields: fields to render
* page: page number * page: page number
""" """
template_name = 'aircox/cms/list.html' template_name = 'aircox/cms/list.html'
allow_empty = True allow_empty = True
paginate_by = 25 paginate_by = 30
model = None model = None
route = None route = None
list = None list = None
css_class = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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): def dispatch(self, request, *args, **kwargs):
self.route = self.kwargs.get('route') or self.route self.route = self.kwargs.get('route') or self.route
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -104,15 +110,8 @@ class PostListView(PostBaseView, ListView):
context['title'] = title context['title'] = title
context['base_template'] = 'aircox/cms/website.html' context['base_template'] = 'aircox/cms/website.html'
context['css_class'] = 'list' context['css_class'] = 'list' if not self.css_class else \
'list ' + self.css_class
if not self.list:
import aircox.cms.sections as sections
self.list = sections.List(
truncate = 32,
fields = [ 'date', 'time', 'image', 'title', 'content' ],
)
context['list'] = self.list context['list'] = self.list
# FIXME: list.url = if self.route: self.model(self.route, self.kwargs) else '' # FIXME: list.url = if self.route: self.model(self.route, self.kwargs) else ''
return context return context

View File

@ -18,12 +18,13 @@ class Website:
# user interaction # user interaction
allow_comments = True allow_comments = True
auto_publish_comments = False auto_publish_comments = False
comments_routes = True
# components # components
urls = [] urls = []
registry = {} registry = {}
def __init__ (self, menus = None, **kwargs): def __init__(self, menus = None, **kwargs):
""" """
* menus: a list of menus to add to the website * menus: a list of menus to add to the website
""" """
@ -36,13 +37,34 @@ class Website:
for menu in menus: for menu in menus:
self.set_menu(menu) 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(): for name, _model in self.registry.items():
if model is _model: if model is _model:
return name 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. Register a model and return the name under which it is registered.
Raise a ValueError if another model is yet associated under this name. Raise a ValueError if another model is yet associated under this name.
@ -54,7 +76,7 @@ class Website:
model._website = self model._website = self
return name return name
def register_detail (self, name, model, view = views.PostDetailView, def register_detail(self, name, model, view = views.PostDetailView,
**view_kwargs): **view_kwargs):
""" """
Register a model and the detail view Register a model and the detail view
@ -68,7 +90,7 @@ class Website:
self.urls.append(routes.DetailRoute.as_url(name, view)) self.urls.append(routes.DetailRoute.as_url(name, view))
self.registry[name] = model 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): routes = [], **view_kwargs):
""" """
Register a model and the given list view using the given routes 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.urls += [ route.as_url(name, view) for route in routes ]
self.registry[name] = model 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, list_view = views.PostListView,
detail_view = views.PostDetailView, detail_view = views.PostDetailView,
list_kwargs = {}, detail_kwargs = {}): list_kwargs = {}, detail_kwargs = {}):
@ -104,7 +126,7 @@ class Website:
**list_kwargs **list_kwargs
) )
def set_menu (self, menu): def set_menu(self, menu):
""" """
Set a menu, and remove any previous menu at the same position Set a menu, and remove any previous menu at the same position
""" """
@ -114,7 +136,7 @@ class Website:
menu.tag = 'side' menu.tag = 'side'
self.menus[menu.position] = menu self.menus[menu.position] = menu
def get_menu (self, position): def get_menu(self, position):
""" """
Get an enabled menu by its position Get an enabled menu by its position
""" """