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 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

View File

@ -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)
if save:
super().save(*args, **kwargs)

View File

@ -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 '',
}

View File

@ -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:

View File

@ -6,7 +6,7 @@
{% endblock %}
{% block pre_title %}
<div class="pre_title metadata">
<div class="pre_title meta">
{% if object.thread %}
<div class="threads">
{{ object|threads:' > '|safe }}
@ -18,13 +18,12 @@
{{ object.date|time:'H\hi' }}
</time>
{% if object.tags %}
{% if object.tags.all %}
{# TODO: url to the tags #}
<div class="tags">
{{ object.tags.all|join:', ' }}
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -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

View File

@ -18,6 +18,7 @@ class Website:
# user interaction
allow_comments = True
auto_publish_comments = False
comments_routes = True
# components
urls = []
@ -36,12 +37,33 @@ class Website:
for menu in menus:
self.set_menu(menu)
if self.comments_routes:
self.register_comments_routes()
def name_of_model(self, model):
for name, _model in self.registry.items():
if model is _model:
return name
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.