fix error with tags; add callable for related bindings; comments in PostListView; ...
This commit is contained in:
parent
ad58d3c332
commit
392d48ac0c
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
21
cms/views.py
21
cms/views.py
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user