diff --git a/cms/models.py b/cms/models.py
index a90c59f..76c5358 100644
--- a/cms/models.py
+++ b/cms/models.py
@@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import ugettext as _, ugettext_lazy
-from django.core.urlresolvers import reverse
from django.db.models.signals import Signal, post_save, pre_save
from django.dispatch import receiver
@@ -38,11 +37,12 @@ class Routable:
)
@classmethod
- def route_url(cl, route, **kwargs):
- name = cl._website.name_of_model(cl)
- name = route.get_view_name(name)
- r = reverse(name, kwargs = kwargs)
- return r
+ 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)
class Comment(models.Model, Routable):
@@ -154,7 +154,10 @@ class Post (models.Model, Routable):
blank = True,
)
- search_fields = [ 'title', 'content' ]
+ search_fields = [ 'title', 'content', 'tags__name' ]
+ """
+ Fields on which routes.SearchRoute must run the search
+ """
def get_comments(self):
"""
@@ -171,12 +174,33 @@ class Post (models.Model, Routable):
"""
Return an url to the post detail view.
"""
- return self.route_url(
+ return self.reverse(
routes.DetailRoute,
pk = self.pk, slug = slugify(self.title)
)
+ def fill_empty(self):
+ """
+ Fill empty values using parent thread. Can be used before saving or
+ at loading
+ """
+ if not self.thread:
+ return
+
+ if not self.title:
+ self.title = _('{name} // {date}').format(
+ name = self.thread.title,
+ date = self.date
+ )
+ if not self.content:
+ self.content = self.thread.content
+ if not self.image:
+ self.image = self.thread.image
+ if not self.tags and self.pk:
+ self.tags = self.thread.tags
+
def get_object_list(self, request, object, **kwargs):
+ # FIXME: wtf
type = ContentType.objects.get_for_model(object)
qs = Comment.objects.filter(
thread_id = object.pk,
@@ -185,6 +209,9 @@ class Post (models.Model, Routable):
return qs
def make_safe(self):
+ """
+ Ensure that data of the publication are safe from code injection.
+ """
self.title = bleach.clean(
self.title,
tags=settings.AIRCOX_CMS_BLEACH_TITLE_TAGS,
@@ -293,6 +320,7 @@ class RelatedMeta (models.base.ModelBase):
else:
return
post.rel_to_post()
+ post.fill_empty()
post.save(avoid_sync = True)
post_save.connect(handler_rel, model._relation.model, False)
diff --git a/cms/qcombine.py b/cms/qcombine.py
index 9e4a227..8409116 100644
--- a/cms/qcombine.py
+++ b/cms/qcombine.py
@@ -2,8 +2,12 @@ import operator
import itertools
import heapq
+from django.utils.translation import ugettext as _, ugettext_lazy
from django.db.models.query import QuerySet
+from aircox.cms.models import Routable
+
+
class QCombine:
"""
This class helps to combine querysets of different models and lists of
@@ -29,9 +33,9 @@ class QCombine:
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 self.lists:
- if issubclass(type(qs, QuerySet):
- self.lists[i] = func(qs)
+ 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)
@@ -47,7 +51,7 @@ class QCombine:
return self
def distinct(self, **kwargs):
- self.map(qs.distinct())
+ self.map(lambda qs: qs.distinct())
return self
def get(self, **kwargs):
@@ -68,13 +72,12 @@ class QCombine:
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),
+ key = operator.attrgetter(*fields),
reverse = reverse
)
)
@@ -112,7 +115,18 @@ class QCombine:
return list(it)
-class QCombined:
+
+
+class Manager(type):
+ models = []
+
+ @property
+ def objects(self):
+ qs = QCombine(*[model.objects.all() for model in self.models])
+ return qs
+
+
+class FakeModel(Routable,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
@@ -120,21 +134,12 @@ class QCombined:
Note: there no other use-case.
"""
- def __init__(*models):
- self.models = models
- self._meta = self.Meta()
-
class Meta:
verbose_name = _('publication')
verbose_name_plural = _('publications')
- @property
- def objects(self):
- """
- The QCombine that is returned actually holds the models' managers,
- in order to simulate the same behaviour than a regular model.
- """
- qs = QCombine([model.objects for model in self.models])
- return qs
+ _meta = Meta()
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
diff --git a/cms/routes.py b/cms/routes.py
index b213241..3c262db 100644
--- a/cms/routes.py
+++ b/cms/routes.py
@@ -5,6 +5,9 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_lazy
+import aircox.cms.qcombine as qcombine
+
+
class Route:
"""
Base class for routing. Given a model, we generate url specific for each
@@ -41,7 +44,7 @@ class Route:
return ''
@classmethod
- def get_view_name(cl, name):
+ def make_view_name(cl, name):
return name + '.' + cl.name
@classmethod
@@ -65,7 +68,7 @@ class Route:
kwargs.update(view_kwargs)
return url(pattern, view, kwargs = kwargs,
- name = cl.get_view_name(name))
+ name = cl.make_view_name(name))
class DetailRoute(Route):
@@ -174,21 +177,28 @@ class SearchRoute(Route):
name = 'search'
@classmethod
- def get_queryset(cl, model, request, **kwargs):
- q = request.GET.get('q') or ''
+ def __search(cl, model, q):
qs = None
-
- ## TODO: by tag
for search_field in model.search_fields or []:
r = models.Q(**{ search_field + '__icontains': q })
if qs: qs = qs | r
else: qs = r
-
return model.objects.filter(qs).distinct()
+
+ @classmethod
+ def get_queryset(cl, model, request, **kwargs):
+ q = request.GET.get('q') or ''
+ if issubclass(model, qcombine.FakeModel):
+ 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, **kwargs):
- return _('Search "%(search)s" in %(model)s') % {
+ return _('Search %(search)s in %(model)s') % {
'model': model._meta.verbose_name_plural,
'search': request.GET.get('q') or '',
}
@@ -207,14 +217,14 @@ class TagsRoute(Route):
@classmethod
def get_queryset(cl, model, request, tags, **kwargs):
tags = tags.split('+')
- return model.objects.filter(tags__name__in=tags)
+ return model.objects.filter(tags__slug__in=tags).distinct()
@classmethod
def get_title(cl, model, request, tags, **kwargs):
- return _('Tagged %(model)s with %(tags)s') % {
+ # FIXME: get tag name instead of tag slug
+ return _('%(model)s tagged with %(tags)s') % {
'model': model._meta.verbose_name_plural,
- 'tags': tags.replace('+', ', ')
+ 'tags': model.tags_to_html(model, tags = tags.split('+'))
+ if '+' in tags else tags
}
-
-
diff --git a/cms/sections.py b/cms/sections.py
index 3cd06b9..05aeff7 100644
--- a/cms/sections.py
+++ b/cms/sections.py
@@ -2,6 +2,7 @@
Define different Section css_class that can be used by views.Sections;
"""
import re
+from random import shuffle
from django.template.loader import render_to_string
from django.views.generic.base import View
@@ -329,13 +330,22 @@ class Similar(List):
List of models allowed in the resulting list. If not set, all models
are available.
"""
+ shuffle = 20
+ """
+ Shuffle results in the self.shuffle most recents articles. If 0 or
+ None, do not shuffle.
+ """
+ # FIXME: limit in a date range
def get_object_list(self):
if not self.object:
return
qs = self.object.tags.similar_objects()
qs.sort(key = lambda post: post.date, reverse=True)
+ if self.shuffle:
+ qs = qs[:self.shuffle]
+ shuffle(qs)
return qs
@@ -371,11 +381,9 @@ class Comments(List):
import aircox.cms.models as models
import aircox.cms.routes as routes
if self.object:
- return models.Comment.route_url(routes.ThreadRoute, {
+ return models.Comment.reverse(routes.ThreadRoute, {
'pk': self.object.id,
- 'thread_model': self.object._website.name_of_model(
- self.object.__class__
- ),
+ 'thread_model': self.object._registration.name
})
return ''
diff --git a/cms/templatetags/aircox_cms.py b/cms/templatetags/aircox_cms.py
index d5975a6..aaf847b 100644
--- a/cms/templatetags/aircox_cms.py
+++ b/cms/templatetags/aircox_cms.py
@@ -1,27 +1,16 @@
from django import template
from django.core.urlresolvers import reverse
-import aircox.cms.routes as routes
-
+import aircox.cms.utils as utils
register = template.Library()
@register.filter(name='post_tags')
-def post_tags(post, sep = '-'):
+def post_tags(post, sep = ' - '):
"""
- print the list of all the tags of the given post, with url if available
+ return the result of post.tags_url
"""
- tags = post.tags.all()
- r = []
- for tag in tags:
- try:
- r.append('{name}'.format(
- url = post.route_url(routes.TagsRoute, tags = tag),
- name = tag,
- ))
- except:
- r.push(tag)
- return sep.join(r)
+ return utils.tags_to_html(type(post), post.tags.all(), sep)
@register.filter(name='threads')
@@ -37,7 +26,7 @@ def threads(post, sep = '/'):
return sep.join([
'{}'.format(post.url(), post.title)
- for post in posts if post.published
+ for post in posts[:-1] if post.published
])
@register.filter(name='around')
diff --git a/cms/website.py b/cms/website.py
index 2565cc9..20ff557 100644
--- a/cms/website.py
+++ b/cms/website.py
@@ -1,4 +1,7 @@
+from collections import namedtuple
+
from django.utils.text import slugify
+from django.core.urlresolvers import reverse
from django.conf.urls import include, url
import aircox.cms.routes as routes
@@ -33,6 +36,10 @@ class Website:
"""register list routes for the Comment model"""
## components
+ Registration = namedtuple('Registration',
+ 'name model routes as_default'
+ )
+
urls = []
"""list of urls generated thourgh registrations"""
exposures = []
@@ -57,44 +64,26 @@ class Website:
if self.comments_routes:
self.register_comments()
- def name_of_model(self, model):
+ def register_model(self, name, model, as_default):
"""
- Return the registered name for a given model if found.
- """
- for name, _model in self.registry.items():
- if model is _model:
- return name
+ Register a model and update model's fields with few data:
+ - _website: back ref to self
+ - _registration: ref to the registration object
- def register_comments(self):
- """
- Register routes for comments, for the moment, only
- ThreadRoute
- """
- self.register(
- 'comment',
- view = views.PostListView,
- routes = [routes.ThreadRoute],
- model = models.Comment,
- 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.
"""
if name in self.registry:
- if self.registry[name] is model:
- return name
+ reg = self.registry[name]
+ if reg.model is model:
+ return reg
raise ValueError('A model has yet been registered under "{}"'
.format(name))
- self.registry[name] = model
+
+ reg = self.Registration(name, model, [], as_default)
+ self.registry[name] = reg
+ model._registration = reg
model._website = self
- return name
+ return reg
def register_exposures(self, sections):
"""
@@ -112,7 +101,8 @@ class Website:
]
def register(self, name, routes = [], view = views.PageView,
- model = None, sections = None, **view_kwargs):
+ model = None, sections = None,
+ as_default = False, **view_kwargs):
"""
Register a view using given name and routes. If model is given,
register the views for it.
@@ -120,11 +110,21 @@ class Website:
* name is used to register the routes as urls and the model if given
* routes: can be a path or a route used to generate urls for the view.
Can be a one item or a list of items.
+ * view: route that is registered for the given routes
+ * model: model being registrated. If given, register it in the website
+ under the given name, and make it available to the view.
+ * as_default: make the view available as a default view.
"""
+ if type(routes) not in (tuple, list):
+ routes = [ routes ]
+
+ # model registration
if model:
- name = self.__register_model(name, model)
+ reg = self.register_model(name, model, as_default)
+ reg.routes.extend(routes)
view_kwargs['model'] = model
+ # init view
if not view_kwargs.get('menus'):
view_kwargs['menus'] = self.menus
@@ -137,9 +137,7 @@ class Website:
**view_kwargs
)
- if type(routes) not in (tuple, list):
- routes = [ routes ]
-
+ # url gen
self.urls += [
route.as_url(name, view)
if type(route) == type and issubclass(route, routes_.Route)
@@ -148,24 +146,51 @@ class Website:
for route in routes
]
- def register_post(self, name, model, sections = None, routes = None,
- list_view = views.PostListView,
- detail_view = views.PostDetailView,
- list_kwargs = {}, detail_kwargs = {}):
+ def register_dl(self, name, model, sections = None, routes = None,
+ list_view = views.PostListView,
+ detail_view = views.PostDetailView,
+ list_kwargs = {}, detail_kwargs = {},
+ as_default = False):
"""
Register a detail and list view for a given model, using
- routes. Just a wrapper around register.
+ routes.
+
+ Just a wrapper around `register`.
"""
if sections:
self.register(name, [ routes_.DetailRoute ], view = detail_view,
- model = model, sections = sections, **detail_kwargs)
+ model = model, sections = sections,
+ as_default = as_default,
+ **detail_kwargs)
if routes:
self.register(name, routes, view = list_view,
- model = model, **list_kwargs)
+ model = model, as_default = as_default,
+ **list_kwargs)
+
+ def register_comments(self):
+ """
+ Register routes for comments, for the moment, only
+ ThreadRoute.
+
+ Just a wrapper around `register`.
+ """
+ self.register(
+ 'comment',
+ view = views.PostListView,
+ routes = [routes.ThreadRoute],
+ model = models.Comment,
+ css_class = 'comments',
+ list = sections.Comments(
+ truncate = 30,
+ fields = ['content','author','date','time'],
+ )
+ )
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.
+ Also update the menu's tag depending on its position, in order
+ to have a semantic HTML5 on the web 2.0 (lol).
"""
if menu.position in ('footer','header'):
menu.tag = menu.position
@@ -174,10 +199,40 @@ class Website:
self.menus[menu.position] = menu
self.register_exposures(menu.sections)
- def get_menu(self, position):
+ def find_default(self, route):
"""
- Get an enabled menu by its position
+ Return a registration that can be used as default for the
+ given route.
"""
- return self.menus.get(position)
+ for r in self.registry.values():
+ if r.as_default and route in r.routes:
+ return r
+
+ def reverse(self, model, route, use_default = True, **kwargs):
+ """
+ Reverse a url using the given model and route. If the reverse does
+ not function and use_default is True, use a model that have been
+ registered as a default view and that have the given road.
+
+ If no model is given reverse with default.
+ """
+ if model and route in model._registration.routes:
+ try:
+ name = route.make_view_name(model._registration.name)
+ return reverse(name, kwargs = kwargs)
+ except:
+ pass
+
+ if model and not use_default:
+ return ''
+
+ for r in self.registry.values():
+ if r.as_default and route in r.routes:
+ try:
+ name = route.make_view_name(r.name)
+ return reverse(name, kwargs = kwargs)
+ except:
+ pass
+ return ''
diff --git a/notes.md b/notes.md
index 6290fae..2d36b43 100644
--- a/notes.md
+++ b/notes.md
@@ -21,11 +21,12 @@
- empty content -> empty string
- update documentation:
- cms.views
- - cms.parts
+ - cms.exposure
- cms.script
- cms.qcombine
- routes
- - integrate QCombine
+ - tag name instead of tag slug for the title
+ - optional url args
- admin cms
- content management -> do we use a markup language?
- sections:
diff --git a/website/models.py b/website/models.py
index bfce0f5..02121f3 100644
--- a/website/models.py
+++ b/website/models.py
@@ -7,11 +7,12 @@ logger = logging.getLogger('aircox')
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
-from aircox.cms.models import Post, RelatedPost
import aircox.programs.models as programs
+import aircox.cms.models as cms
+import aircox.cms.qcombine as qcombine
-class Article (Post):
+class Article (cms.Post):
"""
Represent an article or a static page on the website.
"""
@@ -29,7 +30,7 @@ class Article (Post):
verbose_name_plural = _('Articles')
-class Program (RelatedPost):
+class Program (cms.RelatedPost):
website = models.URLField(
_('website'),
blank=True, null=True
@@ -49,7 +50,7 @@ class Program (RelatedPost):
auto_create = True
-class Diffusion (RelatedPost):
+class Diffusion (cms.RelatedPost):
class Relation:
model = programs.Diffusion
bindings = {
@@ -68,18 +69,7 @@ class Diffusion (RelatedPost):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- if self.thread:
- if not self.title:
- self.title = _('{name} // {first_diff}').format(
- name = self.related.program.name,
- first_diff = self.related.start.strftime('%A %d %B')
- )
- if not self.content:
- self.content = self.thread.content
- if not self.image:
- self.image = self.thread.image
- if not self.tags and self.pk:
- self.tags = self.thread.tags
+ self.fill_empty()
@property
def info(self):
@@ -90,7 +80,7 @@ class Diffusion (RelatedPost):
}
-class Sound (RelatedPost):
+class Sound (cms.RelatedPost):
"""
Publication concerning sound. In order to manage access of sound
files in the filesystem, we use permissions -- it is up to the
@@ -138,3 +128,12 @@ class Sound (RelatedPost):
)
)
+
+class Publications (qcombine.FakeModel):
+ """
+ Combine views
+ """
+ models = [ Article, Program, Diffusion, Sound ]
+
+
+
diff --git a/website/sections.py b/website/sections.py
index de38cf6..3aeeb31 100644
--- a/website/sections.py
+++ b/website/sections.py
@@ -110,7 +110,9 @@ class Diffusions(sections.List):
continue
name = post.related.program.name
if name not in post.title:
- post.title = '{}: {}'.format(name, post.title)
+ post.title = ': ' + post.title if post.title else \
+ ' // ' + post.related.start.strftime('%A %d %B')
+ post.title = name + post.title
return object_list
def get_object_list(self):
diff --git a/website/templates/aircox/website/player.html b/website/templates/aircox/website/player.html
index 958aa2d..ce42efe 100644
--- a/website/templates/aircox/website/player.html
+++ b/website/templates/aircox/website/player.html
@@ -524,8 +524,10 @@ player = {
/// Select the next track in the current playlist, eventually play it
next: function(play = true) {
var playlist = this.playlist;
+ if(playlist == this.live)
+ return
+
var index = this.playlist.items.indexOf(this.item);
- console.log(index, this.item, this.playlist.items)
if(index == -1)
return;