work on cms; add templatetags, and few work on templates

This commit is contained in:
bkfox 2016-05-22 22:50:24 +02:00
parent 7b49bcc4bc
commit 14e9994a79
16 changed files with 219 additions and 187 deletions

View File

@ -24,10 +24,10 @@ class Post (models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
blank = True, null = True blank = True, null = True
) )
thread_pk = models.PositiveIntegerField( thread_id = models.PositiveIntegerField(
blank = True, null = True blank = True, null = True
) )
thread = GenericForeignKey('thread_type', 'thread_pk') thread = GenericForeignKey('thread_type', 'thread_id')
published = models.BooleanField( published = models.BooleanField(
verbose_name = _('public'), verbose_name = _('public'),
@ -64,9 +64,21 @@ class Post (models.Model):
blank = True, blank = True,
) )
@classmethod
def children_of(cl, thread, queryset = None):
"""
Return children of the given thread of the cl's type. If queryset
is not given, use cl.objects as starting queryset.
"""
if not queryset:
queryset = cl.objects
thread_type = ContentType.objects.get_for_model(thread)
qs = queryset.filter(thread_id = thread.pk,
thread_type__pk = thread_type.id)
return qs
def detail_url (self): def detail_url (self):
return reverse(self._meta.verbose_name.lower() + '_detail', return reverse(self._website.name_of_model(self.__class__) + '_detail',
kwargs = { 'pk': self.pk, kwargs = { 'pk': self.pk,
'slug': slugify(self.title) }) 'slug': slugify(self.title) })
@ -145,6 +157,7 @@ class RelatedPostBase (models.base.ModelBase):
def __new__ (cl, name, bases, attrs): def __new__ (cl, name, bases, attrs):
# TODO: allow proxy models and better inheritance # TODO: allow proxy models and better inheritance
# TODO: check bindings
if name == 'RelatedPost': if name == 'RelatedPost':
return super().__new__(cl, name, bases, attrs) return super().__new__(cl, name, bases, attrs)
@ -267,18 +280,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
self.related.save() self.related.save()
@classmethod
def sync_from_rel(cl, rel, save = True):
"""
Update a rel_to_post from a given rel object. Return -1 if there is no
related post to update
"""
self = cl.objects.filter(related = rel)
if not self or not self.count():
return -1
self[0].rel_to_post(save)
def rel_to_post(self, save = True): def rel_to_post(self, save = True):
""" """
Change the post using the related object bound values. Save the Change the post using the related object bound values. Save the
@ -286,25 +287,30 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
Note: does not check if Relation.post_to_rel is True Note: does not check if Relation.post_to_rel is True
""" """
rel = self._relation rel = self._relation
if rel.bindings: if not rel.bindings:
return return
has_changed = False
def set_attr(attr, value):
if getattr(self, attr) != value:
has_changed = True
setattr(self, attr, value)
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 self.set_rel_attr
value = getattr(self.related, attr) \ value = getattr(self.related, rel_attr)
if hasattr(self.related, attr) else None set_attr(attr, value)
setattr(self, attr, value)
if rel.thread_model: if rel.thread_model:
thread = self.get_rel_attr('thread') thread = self.get_rel_attr('thread')
thread = rel.thread_model.objects.filter(related = thread) \ thread = rel.thread_model.objects.filter(related = thread) \
if thread else None if thread else None
thread = thread[0] if thread else None thread = thread[0] if thread else None
self.thread = thread set_attr('thread', thread)
if save: if has_changed and save:
self.save() self.save()
def __init__ (self, *kargs, **kwargs): def __init__ (self, *kargs, **kwargs):
@ -329,10 +335,10 @@ class Comment(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
blank = True, null = True blank = True, null = True
) )
thread_pk = models.PositiveIntegerField( thread_id = models.PositiveIntegerField(
blank = True, null = True blank = True, null = True
) )
thread = GenericForeignKey('thread_type', 'thread_pk') thread = GenericForeignKey('thread_type', 'thread_id')
author = models.TextField( author = models.TextField(
verbose_name = _('author'), verbose_name = _('author'),

View File

@ -3,37 +3,20 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from website.models import *
from website.views import *
class Router:
registry = []
def register (self, route):
if not route in self.registry:
self.registry.append(route)
def register_set (self, view_set):
for url in view_set.urls:
self.register(url)
def unregister (self, route):
self.registry.remove(route)
def get_urlpatterns (self):
return [ url for url in self.registry ]
class Route: class Route:
""" """
Base class for routing. Given a model, we generate url specific for each Base class for routing. Given a model, we generate url specific for each
route type. The generated url takes this form: type of route.
model_name + '/' + route_name + '/' + '/'.join(route_url_args)
Where model_name by default is the given model's verbose_name (uses plural if The generated url takes this form:
Route is for a list). name + '/' + route_name + '/' + '/'.join(route_url_args)
The given view is considered as a django class view, and has view_ And their name (to use for reverse:
name + '_' + route_name
By default name is the verbose name of the model. It is always in
singular form.
""" """
name = None # route name name = None # route name
url_args = [] # arguments passed from the url [ (name : regex),... ] url_args = [] # arguments passed from the url [ (name : regex),... ]
@ -61,7 +44,7 @@ class Route:
return name + '_' + cl.name return name + '_' + cl.name
@classmethod @classmethod
def as_url (cl, name, model, view, view_kwargs = None): def as_url(cl, name, view, view_kwargs = None):
pattern = '^{}/{}'.format(name, cl.name) pattern = '^{}/{}'.format(name, cl.name)
if cl.url_args: if cl.url_args:
url_args = '/'.join([ url_args = '/'.join([

View File

@ -1,14 +1,31 @@
{% extends embed|yesno:"aircox/cms/base_content.html,aircox/cms/base_site.html" %} {% extends embed|yesno:"aircox/cms/base_content.html,aircox/cms/base_site.html" %}
{% load aircox_cms %}
{% block title %} {% block title %}
{{ object.title }} {{ object.title }}
{% endblock %} {% endblock %}
{% block pre_title %} {% block pre_title %}
<div class="pre_title">
{% if object.thread %}
<div class="threads">
{{ object|threads:' > '|safe }}
</div>
{% 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 %}
{# TODO: url to the tags #}
<div class="tags">
{{ object.tags.all|join:', ' }}
</div>
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -5,7 +5,7 @@
{% block section_content %} {% block section_content %}
<ul style="padding:0; margin:0"> <ul style="padding:0; margin:0">
{% for item in object_list %} {% for item in object_list %}
<li> <li class="{{item.css}}">
{% if item.url %} {% if item.url %}
<a href="{{item.url}}"> <a href="{{item.url}}">
{% endif %} {% endif %}

View File

View File

@ -0,0 +1,23 @@
from django import template
from django.core.urlresolvers import reverse
register = template.Library()
@register.filter(name='threads')
def threads (post, sep = '/'):
"""
print a list of all parents, from top to bottom
"""
posts = [post]
while posts[0].thread:
post = posts[0].thread
if post not in posts:
posts.insert(0, post)
return sep.join([
'<a href="{}">{}</a>'.format(post.detail_url(), post.title)
for post in posts if post.published
])

View File

@ -323,11 +323,13 @@ class Sections:
title = None title = None
text = None text = None
url = None url = None
css = None
def __init__ (self, icon, title = None, text = None, url = None): def __init__(self, icon, title = None, text = None, url = None, css = None):
self.icon = icon self.icon = icon
self.title = title self.title = title
self.text = text self.text = text
self.css = css
hide_empty = False # hides the section if the list is empty hide_empty = False # hides the section if the list is empty
use_icons = True # print icons use_icons = True # print icons
@ -341,7 +343,7 @@ class Sections:
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
object_list = self.get_object_list() object_list = self.get_object_list()
self.visibility = True self.visibility = True
if not object_list and hide_empty: if not object_list and self.hide_empty:
self.visibility = False self.visibility = False
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

View File

@ -35,6 +35,7 @@ class Website:
raise ValueError('A model has yet been registered under "{}"' raise ValueError('A model has yet been registered under "{}"'
.format(name)) .format(name))
self.registry[name] = model self.registry[name] = model
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,
@ -48,7 +49,7 @@ class Website:
model = model, model = model,
**view_kwargs, **view_kwargs,
) )
self.urls.append(routes.DetailRoute.as_url(name, model, 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,
@ -62,7 +63,7 @@ class Website:
model = model, model = model,
**view_kwargs **view_kwargs
) )
self.urls += [ route.as_url(name, model, 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,

View File

@ -97,7 +97,9 @@ class Monitor:
args = {'start__gt': prev_diff.start } if prev_diff else {} args = {'start__gt': prev_diff.start } if prev_diff else {}
next_diff = programs.Diffusion \ next_diff = programs.Diffusion \
.get(controller.station, now, now = True, .get(controller.station, now, now = True,
sounds__isnull = False, **args) \ type = programs.Diffusion.Type.normal,
sounds__isnull = False,
**args) \
.prefetch_related('sounds') .prefetch_related('sounds')
if next_diff: if next_diff:
next_diff = next_diff[0] next_diff = next_diff[0]

View File

@ -91,7 +91,7 @@ class DiffusionAdmin (admin.ModelAdmin):
return ', '.join(sounds) if sounds else '' return ', '.join(sounds) if sounds else ''
def conflicts (self, obj): def conflicts (self, obj):
if obj.type == Diffusion.Type['unconfirmed']: if obj.type == Diffusion.Type.unconfirmed:
return ', '.join([ str(d) for d in obj.get_conflicts()]) return ', '.join([ str(d) for d in obj.get_conflicts()])
return '' return ''
@ -115,9 +115,9 @@ class DiffusionAdmin (admin.ModelAdmin):
qs = super(DiffusionAdmin, self).get_queryset(request) qs = super(DiffusionAdmin, self).get_queryset(request)
if '_changelist_filters' in request.GET or \ if '_changelist_filters' in request.GET or \
'type__exact' in request.GET and \ 'type__exact' in request.GET and \
str(Diffusion.Type['unconfirmed']) in request.GET['type__exact']: str(Diffusion.Type.unconfirmed) in request.GET['type__exact']:
return qs return qs
return qs.exclude(type = Diffusion.Type['unconfirmed']) return qs.exclude(type = Diffusion.Type.unconfirmed)
@admin.register(Log) @admin.register(Log)

View File

@ -46,15 +46,15 @@ class Actions:
continue continue
if conflict.pk in saved_items and \ if conflict.pk in saved_items and \
conflict.type != Diffusion.Type['unconfirmed']: conflict.type != Diffusion.Type.unconfirmed:
conflict.type = Diffusion.Type['unconfirmed'] conflict.type = Diffusion.Type.unconfirmed
conflict.save() conflict.save()
if not conflicts: if not conflicts:
item.type = Diffusion.Type['normal'] item.type = Diffusion.Type.normal
return 0 return 0
item.type = Diffusion.Type['unconfirmed'] item.type = Diffusion.Type.unconfirmed
return len(conflicts) return len(conflicts)
@classmethod @classmethod
@ -93,14 +93,14 @@ class Actions:
@staticmethod @staticmethod
def clean (date): def clean (date):
qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'], qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
start__lt = date) start__lt = date)
logger.info('[clean] %d diffusions will be removed', qs.count()) logger.info('[clean] %d diffusions will be removed', qs.count())
qs.delete() qs.delete()
@staticmethod @staticmethod
def check (date): def check (date):
qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'], qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
start__gt = date) start__gt = date)
items = [] items = []
for diffusion in qs: for diffusion in qs:

View File

@ -162,9 +162,9 @@ class MonitorHandler (PatternMatchingEventHandler):
""" """
self.subdir = subdir self.subdir = subdir
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR: if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
self.sound_kwargs = { 'type': Sound.Type['archive'] } self.sound_kwargs = { 'type': Sound.Type.archive }
else: else:
self.sound_kwargs = { 'type': Sound.Type['excerpt'] } self.sound_kwargs = { 'type': Sound.Type.excerpt }
patterns = ['*/{}/*{}'.format(self.subdir, ext) patterns = ['*/{}/*{}'.format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT ] for ext in settings.AIRCOX_SOUND_FILE_EXT ]
@ -264,11 +264,11 @@ class Command (BaseCommand):
logger.info('#%d %s', program.id, program.name) logger.info('#%d %s', program.id, program.name)
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'], type = Sound.Type.archive,
) )
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
type = Sound.Type['excerpt'], type = Sound.Type.excerpt,
) )
def scan_for_program (self, program, subdir, **sound_kwargs): def scan_for_program (self, program, subdir, **sound_kwargs):

View File

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
import logging import logging
from enum import Enum, IntEnum
from django.db import models from django.db import models
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
@ -95,17 +96,14 @@ class Sound (Nameable):
The podcasting and public access permissions of a Sound are managed through The podcasting and public access permissions of a Sound are managed through
the related program info. the related program info.
""" """
Type = { class Type(IntEnum):
'other': 0x00, other = 0x00,
'archive': 0x01, archive = 0x01,
'excerpt': 0x02, excerpt = 0x02,
}
for key, value in Type.items():
ugettext_lazy(key)
type = models.SmallIntegerField( type = models.SmallIntegerField(
verbose_name = _('type'), verbose_name = _('type'),
choices = [ (y, x) for x,y in Type.items() ], choices = [ (y, _(x)) for x,y in Type.__members__.items() ],
blank = True, null = True blank = True, null = True
) )
path = models.FilePathField( path = models.FilePathField(
@ -394,7 +392,7 @@ class Schedule (models.Model):
else None else None
diffusions.append(Diffusion( diffusions.append(Diffusion(
program = self.program, program = self.program,
type = Diffusion.Type['unconfirmed'], type = Diffusion.Type.unconfirmed,
initial = first_diffusion if self.initial else None, initial = first_diffusion if self.initial else None,
start = date, start = date,
end = date + duration, end = date + duration,
@ -555,13 +553,10 @@ class Diffusion (models.Model):
- cancel: the diffusion has been canceled - cancel: the diffusion has been canceled
- stop: the diffusion has been manually stopped - stop: the diffusion has been manually stopped
""" """
Type = { class Type(IntEnum):
'normal': 0x00, # diffusion is planified normal = 0x00
'unconfirmed': 0x01, # scheduled by the generator but not confirmed for diffusion unconfirmed = 0x01
'cancel': 0x02, # diffusion canceled canceled = 0x02
}
for key, value in Type.items():
ugettext_lazy(key)
# common # common
program = models.ForeignKey ( program = models.ForeignKey (
@ -576,7 +571,7 @@ class Diffusion (models.Model):
# specific # specific
type = models.SmallIntegerField( type = models.SmallIntegerField(
verbose_name = _('type'), verbose_name = _('type'),
choices = [ (y, x) for x,y in Type.items() ], choices = [ (y, _(x)) for x,y in Type.__members__.items() ],
) )
initial = models.ForeignKey ( initial = models.ForeignKey (
'self', 'self',
@ -611,7 +606,7 @@ class Diffusion (models.Model):
""" """
sounds = self.initial.sounds if self.initial else self.sounds sounds = self.initial.sounds if self.initial else self.sounds
r = [ sound.duration r = [ sound.duration
for sound in sounds.filter(type = Sound.Type['archive']) for sound in sounds.filter(type = Sound.Type.archive)
if sound.duration ] if sound.duration ]
return utils.time_sum(r) return utils.time_sum(r)
@ -621,12 +616,13 @@ class Diffusion (models.Model):
""" """
sounds = self.initial.sounds if self.initial else self.sounds sounds = self.initial.sounds if self.initial else self.sounds
r = [ sound for sound in sounds.all().order_by('path') r = [ sound for sound in sounds.all().order_by('path')
if sound.type == Sound.Type['archive'] ] if sound.type == Sound.Type.archive ]
return r return r
@classmethod @classmethod
def get(cl, station = None, date = None, def get(cl, station = None, date = None,
now = False, next = False, prev = False, now = False, next = False, prev = False,
queryset = None,
**filter_args): **filter_args):
""" """
Return a queryset of diffusions, depending on value of now/next/prev Return a queryset of diffusions, depending on value of now/next/prev
@ -634,6 +630,8 @@ class Diffusion (models.Model):
- next: that start after date - next: that start after date
- prev: that end before date - prev: that end before date
If queryset is not given, use self.objects.all
Diffusions are ordered by +start for now and next; -start for prev Diffusions are ordered by +start for now and next; -start for prev
""" """
#FIXME: conflicts? ( + calling functions) #FIXME: conflicts? ( + calling functions)

View File

@ -10,7 +10,7 @@ class Programs (TestCase):
def setUp (self): def setUp (self):
stream = Stream.objects.get_or_create( stream = Stream.objects.get_or_create(
name = 'diffusions', name = 'diffusions',
defaults = { 'type': Stream.Type['schedule'] } defaults = { 'type': Stream.Type.schedule }
)[0] )[0]
Program.objects.create(name = 'source', stream = stream) Program.objects.create(name = 'source', stream = stream)
Program.objects.create(name = 'microouvert', stream = stream) Program.objects.create(name = 'microouvert', stream = stream)