switch to multi-table inheritance for posts; remove qcombine that is no more needed; add and integrate post.downcast + as template filter
This commit is contained in:
		@ -54,7 +54,7 @@ class Exposure:
 | 
			
		||||
                    'exp': cl._exposure,
 | 
			
		||||
                })
 | 
			
		||||
                res = render_to_string(exp.template_name,
 | 
			
		||||
                                       v, request = request)
 | 
			
		||||
                                       ctx, request = request)
 | 
			
		||||
            return HttpResponse(res or '')
 | 
			
		||||
 | 
			
		||||
        # id = str(uuid.uuid1())
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ from django.db import models
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.contenttypes.fields import GenericForeignKey
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ class Comment(models.Model, Routable):
 | 
			
		||||
    )
 | 
			
		||||
    date = models.DateTimeField(
 | 
			
		||||
        _('date'),
 | 
			
		||||
        default = timezone.datetime.now
 | 
			
		||||
        auto_now_add = True,
 | 
			
		||||
    )
 | 
			
		||||
    content = models.TextField (
 | 
			
		||||
        _('comment'),
 | 
			
		||||
@ -107,10 +107,18 @@ class Post (models.Model, Routable):
 | 
			
		||||
    You can declare an extra property "info" that can be used to append
 | 
			
		||||
    info in lists rendering.
 | 
			
		||||
    """
 | 
			
		||||
    # used for inherited children
 | 
			
		||||
    real_type = models.CharField(
 | 
			
		||||
        max_length=32,
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # metadata
 | 
			
		||||
    # FIXME: on_delete
 | 
			
		||||
    thread_type = models.ForeignKey(
 | 
			
		||||
        ContentType,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        related_name = 'thread_type',
 | 
			
		||||
        blank = True, null = True
 | 
			
		||||
    )
 | 
			
		||||
    thread_id = models.PositiveIntegerField(
 | 
			
		||||
@ -135,7 +143,7 @@ class Post (models.Model, Routable):
 | 
			
		||||
    )
 | 
			
		||||
    date = models.DateTimeField(
 | 
			
		||||
        _('date'),
 | 
			
		||||
        default = timezone.datetime.now
 | 
			
		||||
        default = tz.datetime.now
 | 
			
		||||
    )
 | 
			
		||||
    title = models.CharField (
 | 
			
		||||
        _('title'),
 | 
			
		||||
@ -154,6 +162,11 @@ class Post (models.Model, Routable):
 | 
			
		||||
        blank = True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    info = ''
 | 
			
		||||
    """
 | 
			
		||||
    Used to be extended: used in template to render contextual information about
 | 
			
		||||
    a sub-post item.
 | 
			
		||||
    """
 | 
			
		||||
    search_fields = [ 'title', 'content', 'tags__name' ]
 | 
			
		||||
    """
 | 
			
		||||
    Fields on which routes.SearchRoute must run the search
 | 
			
		||||
@ -196,7 +209,7 @@ class Post (models.Model, Routable):
 | 
			
		||||
            self.content = self.thread.content
 | 
			
		||||
        if not self.image:
 | 
			
		||||
            self.image = self.thread.image
 | 
			
		||||
        if not self.tags and self.pk:
 | 
			
		||||
        if self.pk and not self.tags:
 | 
			
		||||
            self.tags = self.thread.tags
 | 
			
		||||
 | 
			
		||||
    def get_object_list(self, request, object, **kwargs):
 | 
			
		||||
@ -228,17 +241,24 @@ class Post (models.Model, Routable):
 | 
			
		||||
                for tag in self.tags.all()
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
    def downcast(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return a downcasted version of the post if it is from another
 | 
			
		||||
        model, or itself
 | 
			
		||||
        """
 | 
			
		||||
        if not self.real_type or type(self) != Post:
 | 
			
		||||
            return self
 | 
			
		||||
        return getattr(self, self.real_type)
 | 
			
		||||
 | 
			
		||||
    def save(self, make_safe = True, *args, **kwargs):
 | 
			
		||||
        if type(self) != Post and not self.real_type:
 | 
			
		||||
            self.real_type = type(self).__name__.lower()
 | 
			
		||||
        if self.date and tz.is_naive(self.date):
 | 
			
		||||
            self.date = tz.make_aware(self.date)
 | 
			
		||||
        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:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RelatedMeta (models.base.ModelBase):
 | 
			
		||||
    """
 | 
			
		||||
@ -499,6 +519,8 @@ class RelatedPost (Post, metaclass = RelatedMeta):
 | 
			
		||||
                continue
 | 
			
		||||
            value = rel_attr(self, self.related) if callable(rel_attr) else \
 | 
			
		||||
                    getattr(self.related, rel_attr)
 | 
			
		||||
            if type(value) == tz.datetime and tz.is_naive(value):
 | 
			
		||||
                value = tz.make_aware(value)
 | 
			
		||||
            set_attr(attr, value)
 | 
			
		||||
 | 
			
		||||
        if rel.thread_model:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										153
									
								
								cms/qcombine.py
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								cms/qcombine.py
									
									
									
									
									
								
							@ -1,153 +0,0 @@
 | 
			
		||||
import operator
 | 
			
		||||
import itertools
 | 
			
		||||
import heapq
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
from django.db.models.query import QuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QCombine:
 | 
			
		||||
    """
 | 
			
		||||
    This class helps to combine querysets of different models and lists of
 | 
			
		||||
    object, and to iter over it.
 | 
			
		||||
 | 
			
		||||
    Notes:
 | 
			
		||||
    - when working on fields, we assume that they exists on all of them;
 | 
			
		||||
    - for efficiency, there is no possibility to change order per field;
 | 
			
		||||
      to do so, do it directly on the querysets
 | 
			
		||||
    - we dont clone the combinator in order to avoid overhead
 | 
			
		||||
    """
 | 
			
		||||
    order_fields = None
 | 
			
		||||
    lists = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *lists):
 | 
			
		||||
        """
 | 
			
		||||
        lists: list of querysets that are used to initialize the stuff.
 | 
			
		||||
        """
 | 
			
		||||
        self.lists = list(lists) or []
 | 
			
		||||
 | 
			
		||||
    def map(self, qs_func, non_qs = None):
 | 
			
		||||
        """
 | 
			
		||||
        Map results of qs_func for QuerySet instance and of non_qs for
 | 
			
		||||
        the others (if given), because QuerySet always clones itself.
 | 
			
		||||
        """
 | 
			
		||||
        for i, qs in enumerate(self.lists):
 | 
			
		||||
            if issubclass(type(qs), QuerySet):
 | 
			
		||||
                self.lists[i] = qs_func(qs)
 | 
			
		||||
            elif non_qs:
 | 
			
		||||
                self.lists[i] = non_qs(qs)
 | 
			
		||||
 | 
			
		||||
    def all(self):
 | 
			
		||||
        self.map(lambda qs: qs.all())
 | 
			
		||||
 | 
			
		||||
    def filter(self, **kwargs):
 | 
			
		||||
        self.map(lambda qs: qs.filter(**kwargs))
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def exclude(self, **kwargs):
 | 
			
		||||
        self.map(lambda qs: qs.exclude(**kwargs))
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def distinct(self, **kwargs):
 | 
			
		||||
        self.map(lambda qs: qs.distinct())
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def get(self, **kwargs):
 | 
			
		||||
        self.filter(**kwargs)
 | 
			
		||||
        it = iter(self)
 | 
			
		||||
        return next(it)
 | 
			
		||||
 | 
			
		||||
    def order_by(self, *fields, reverse = False):
 | 
			
		||||
        """
 | 
			
		||||
        Order using these fields. For compatibility, if there is
 | 
			
		||||
        at least one fields whose name starts with '-', reverse
 | 
			
		||||
        the order
 | 
			
		||||
        """
 | 
			
		||||
        for i, field in enumerate(fields):
 | 
			
		||||
            if field[0] == '-':
 | 
			
		||||
                reverse = True
 | 
			
		||||
                fields[i] = field[1:]
 | 
			
		||||
 | 
			
		||||
        self.order_reverse = reverse
 | 
			
		||||
        self.order_fields = fields
 | 
			
		||||
        self.map(
 | 
			
		||||
            lambda qs: qs.order_by(*fields),
 | 
			
		||||
            lambda qs: sorted(
 | 
			
		||||
                qs,
 | 
			
		||||
                qs.sort(
 | 
			
		||||
                    key = operator.attrgetter(*fields),
 | 
			
		||||
                    reverse = reverse
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def clone(self):
 | 
			
		||||
        """
 | 
			
		||||
        Make a clone of the class. Not that lists are copied, non-deeply
 | 
			
		||||
        """
 | 
			
		||||
        return QCombine(*[
 | 
			
		||||
            qs.all() if issubclass(type(qs), QuerySet) else qs.copy()
 | 
			
		||||
            for qs in self.lists
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
    def __len__(self):
 | 
			
		||||
        return sum([len(qs) for qs in self.lists])
 | 
			
		||||
 | 
			
		||||
    def __iter__(self):
 | 
			
		||||
        if not self.order_fields:
 | 
			
		||||
            return itertools.chain(self.lists)
 | 
			
		||||
 | 
			
		||||
        # FIXME: need it lazy?
 | 
			
		||||
        return heapq.merge(
 | 
			
		||||
            *self.lists,
 | 
			
		||||
            key = operator.attrgetter(*self.order_fields),
 | 
			
		||||
            reverse = self.order_reverse
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, k):
 | 
			
		||||
        if type(k) == slice:
 | 
			
		||||
            it = itertools.islice(iter(self), k.start, k.stop, k.step)
 | 
			
		||||
        else:
 | 
			
		||||
            it = itertools.islice(iter(self), k)
 | 
			
		||||
        return list(it)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Manager(type):
 | 
			
		||||
    """
 | 
			
		||||
    Metaclass used to generate the GenericModel.objects property
 | 
			
		||||
    """
 | 
			
		||||
    models = []
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def objects(self):
 | 
			
		||||
        qs = QCombine(*[model.objects.all() for model in self.models])
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GenericModel(metaclass=Manager):
 | 
			
		||||
    """
 | 
			
		||||
    This class is used to register a route for multiple models to a website.
 | 
			
		||||
    A QCombine is created with qs for all given models when objects
 | 
			
		||||
    property is retrieved.
 | 
			
		||||
 | 
			
		||||
    Note: there no other use-case.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('publication')
 | 
			
		||||
        verbose_name_plural = _('publications')
 | 
			
		||||
 | 
			
		||||
    _meta = Meta()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self.__dict__.update(kwargs)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def reverse(cl, route, use_default = True, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Reverse a url using a given route for the model - simple wrapper
 | 
			
		||||
        around cl._website.reverse
 | 
			
		||||
        """
 | 
			
		||||
        return cl._website.reverse(cl, route, use_default, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
from taggit.models import Tag
 | 
			
		||||
 | 
			
		||||
import aircox.cms.qcombine as qcombine
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Route:
 | 
			
		||||
    """
 | 
			
		||||
@ -189,7 +187,8 @@ class SearchRoute(Route):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def __search(cl, model, q):
 | 
			
		||||
    def get_queryset(cl, model, request, q = None, **kwargs):
 | 
			
		||||
        q = request.GET.get('q') or q or ''
 | 
			
		||||
        qs = None
 | 
			
		||||
        for search_field in model.search_fields or []:
 | 
			
		||||
            r = models.Q(**{ search_field + '__icontains': q })
 | 
			
		||||
@ -197,17 +196,6 @@ class SearchRoute(Route):
 | 
			
		||||
            else: qs = r
 | 
			
		||||
        return model.objects.filter(qs).distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_queryset(cl, model, request, q = None, **kwargs):
 | 
			
		||||
        q = request.GET.get('q') or q or ''
 | 
			
		||||
        if issubclass(model, qcombine.GenericModel):
 | 
			
		||||
            models = model.models
 | 
			
		||||
            return qcombine.QCombine(
 | 
			
		||||
                *(cl.__search(model, q) for model in models)
 | 
			
		||||
            )
 | 
			
		||||
        return cl.__search(model, q)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_title(cl, model, request, q = None, **kwargs):
 | 
			
		||||
        return _('Search <i>%(search)s</i> in %(model)s') % {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load thumbnail %}
 | 
			
		||||
{% load aircox_cms %}
 | 
			
		||||
 | 
			
		||||
{% with object|downcast as object %}
 | 
			
		||||
<li {% if object.css_class %}class="{{ object.css_class }}"{% endif %}
 | 
			
		||||
      {% for k, v in object.attrs.items %}
 | 
			
		||||
      {{ k }} = "{{ v|addslashes }}"
 | 
			
		||||
@ -61,5 +62,5 @@
 | 
			
		||||
  </a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
</li>
 | 
			
		||||
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,17 @@ import aircox.cms.utils as utils
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
@register.filter(name='downcast')
 | 
			
		||||
def downcast(post):
 | 
			
		||||
    """
 | 
			
		||||
    Downcast an object if it has a downcast function, or just return the
 | 
			
		||||
    post.
 | 
			
		||||
    """
 | 
			
		||||
    if hasattr(post, 'downcast') and callable(post.downcast):
 | 
			
		||||
        return post.downcast
 | 
			
		||||
    return post
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(name='post_tags')
 | 
			
		||||
def post_tags(post, sep = ' - '):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
import aircox.programs.models as programs
 | 
			
		||||
import aircox.cms.models as cms
 | 
			
		||||
import aircox.cms.qcombine as qcombine
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Article (cms.Post):
 | 
			
		||||
@ -129,11 +128,3 @@ class Sound (cms.RelatedPost):
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Publications (qcombine.GenericModel):
 | 
			
		||||
    """
 | 
			
		||||
    Combine views
 | 
			
		||||
    """
 | 
			
		||||
    models = [ Article, Program, Diffusion, Sound ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user