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:
bkfox 2016-07-06 16:27:30 +02:00
parent cfce035527
commit ff02258d8b
7 changed files with 49 additions and 189 deletions

View File

@ -54,7 +54,7 @@ class Exposure:
'exp': cl._exposure, 'exp': cl._exposure,
}) })
res = render_to_string(exp.template_name, res = render_to_string(exp.template_name,
v, request = request) ctx, request = request)
return HttpResponse(res or '') return HttpResponse(res or '')
# id = str(uuid.uuid1()) # id = str(uuid.uuid1())

View File

@ -2,7 +2,7 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType 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.text import slugify
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
@ -75,7 +75,7 @@ class Comment(models.Model, Routable):
) )
date = models.DateTimeField( date = models.DateTimeField(
_('date'), _('date'),
default = timezone.datetime.now auto_now_add = True,
) )
content = models.TextField ( content = models.TextField (
_('comment'), _('comment'),
@ -107,10 +107,18 @@ class Post (models.Model, Routable):
You can declare an extra property "info" that can be used to append You can declare an extra property "info" that can be used to append
info in lists rendering. info in lists rendering.
""" """
# used for inherited children
real_type = models.CharField(
max_length=32,
blank = True, null = True,
)
# metadata # metadata
# FIXME: on_delete
thread_type = models.ForeignKey( thread_type = models.ForeignKey(
ContentType, ContentType,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name = 'thread_type',
blank = True, null = True blank = True, null = True
) )
thread_id = models.PositiveIntegerField( thread_id = models.PositiveIntegerField(
@ -135,7 +143,7 @@ class Post (models.Model, Routable):
) )
date = models.DateTimeField( date = models.DateTimeField(
_('date'), _('date'),
default = timezone.datetime.now default = tz.datetime.now
) )
title = models.CharField ( title = models.CharField (
_('title'), _('title'),
@ -154,6 +162,11 @@ class Post (models.Model, Routable):
blank = True, 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' ] search_fields = [ 'title', 'content', 'tags__name' ]
""" """
Fields on which routes.SearchRoute must run the search Fields on which routes.SearchRoute must run the search
@ -196,7 +209,7 @@ class Post (models.Model, Routable):
self.content = self.thread.content self.content = self.thread.content
if not self.image: if not self.image:
self.image = self.thread.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 self.tags = self.thread.tags
def get_object_list(self, request, object, **kwargs): def get_object_list(self, request, object, **kwargs):
@ -228,17 +241,24 @@ class Post (models.Model, Routable):
for tag in self.tags.all() 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): 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: 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:
abstract = True
class RelatedMeta (models.base.ModelBase): class RelatedMeta (models.base.ModelBase):
""" """
@ -499,6 +519,8 @@ class RelatedPost (Post, metaclass = RelatedMeta):
continue continue
value = rel_attr(self, self.related) if callable(rel_attr) else \ value = rel_attr(self, self.related) if callable(rel_attr) else \
getattr(self.related, rel_attr) getattr(self.related, rel_attr)
if type(value) == tz.datetime and tz.is_naive(value):
value = tz.make_aware(value)
set_attr(attr, value) set_attr(attr, value)
if rel.thread_model: if rel.thread_model:

View File

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

View File

@ -6,8 +6,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from taggit.models import Tag from taggit.models import Tag
import aircox.cms.qcombine as qcombine
class Route: class Route:
""" """
@ -189,7 +187,8 @@ class SearchRoute(Route):
] ]
@classmethod @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 qs = None
for search_field in model.search_fields or []: for search_field in model.search_fields or []:
r = models.Q(**{ search_field + '__icontains': q }) r = models.Q(**{ search_field + '__icontains': q })
@ -197,17 +196,6 @@ class SearchRoute(Route):
else: qs = r else: qs = r
return model.objects.filter(qs).distinct() 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 @classmethod
def get_title(cl, model, request, q = None, **kwargs): def get_title(cl, model, request, q = None, **kwargs):
return _('Search <i>%(search)s</i> in %(model)s') % { return _('Search <i>%(search)s</i> in %(model)s') % {

View File

@ -1,7 +1,8 @@
{% load i18n %} {% load i18n %}
{% load thumbnail %} {% load thumbnail %}
{% load aircox_cms %}
{% with object|downcast as object %}
<li {% if object.css_class %}class="{{ object.css_class }}"{% endif %} <li {% if object.css_class %}class="{{ object.css_class }}"{% endif %}
{% for k, v in object.attrs.items %} {% for k, v in object.attrs.items %}
{{ k }} = "{{ v|addslashes }}" {{ k }} = "{{ v|addslashes }}"
@ -61,5 +62,5 @@
</a> </a>
{% endif %} {% endif %}
</li> </li>
{% endwith %}

View File

@ -5,6 +5,17 @@ import aircox.cms.utils as utils
register = template.Library() 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') @register.filter(name='post_tags')
def post_tags(post, sep = ' - '): def post_tags(post, sep = ' - '):
""" """

View File

@ -9,7 +9,6 @@ from django.utils.translation import ugettext as _, ugettext_lazy
import aircox.programs.models as programs import aircox.programs.models as programs
import aircox.cms.models as cms import aircox.cms.models as cms
import aircox.cms.qcombine as qcombine
class Article (cms.Post): class Article (cms.Post):
@ -129,11 +128,3 @@ class Sound (cms.RelatedPost):
) )
class Publications (qcombine.GenericModel):
"""
Combine views
"""
models = [ Article, Program, Diffusion, Sound ]