fix error with tags; add callable for related bindings; comments in PostListView; ...
This commit is contained in:
		@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 '',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -6,25 +6,24 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block pre_title %}
 | 
			
		||||
<div class="pre_title metadata">
 | 
			
		||||
{% if object.thread %}
 | 
			
		||||
<div class="threads">
 | 
			
		||||
    {{ object|threads:' > '|safe }}
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
<div class="pre_title meta">
 | 
			
		||||
    {% if object.thread %}
 | 
			
		||||
    <div class="threads">
 | 
			
		||||
        {{ object|threads:' > '|safe }}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
<time datetime="{{ object.date }}">
 | 
			
		||||
    {{ object.date|date:'l d F Y' }},
 | 
			
		||||
    {{ object.date|time:'H\hi' }}
 | 
			
		||||
</time>
 | 
			
		||||
 | 
			
		||||
{% if object.tags %}
 | 
			
		||||
{# TODO: url to the tags #}
 | 
			
		||||
<div class="tags">
 | 
			
		||||
    {{ object.tags.all|join:', ' }}
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
    <time datetime="{{ object.date }}">
 | 
			
		||||
        {{ object.date|date:'l d F Y' }},
 | 
			
		||||
        {{ object.date|time:'H\hi' }}
 | 
			
		||||
    </time>
 | 
			
		||||
 | 
			
		||||
    {% if object.tags.all %}
 | 
			
		||||
    {# TODO: url to the tags #}
 | 
			
		||||
    <div class="tags">
 | 
			
		||||
        {{ object.tags.all|join:', ' }}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
{% 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
 | 
			
		||||
    * 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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user