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,65 +3,48 @@ 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),... ]
@classmethod @classmethod
def get_queryset (cl, website, model, request, **kwargs): def get_queryset(cl, website, model, request, **kwargs):
""" """
Called by the view to get the queryset when it is needed Called by the view to get the queryset when it is needed
""" """
pass pass
@classmethod @classmethod
def get_object (cl, website, model, request, **kwargs): def get_object(cl, website, model, request, **kwargs):
""" """
Called by the view to get the object when it is needed Called by the view to get the object when it is needed
""" """
pass pass
@classmethod @classmethod
def get_title (cl, model, request, **kwargs): def get_title(cl, model, request, **kwargs):
return '' return ''
@classmethod @classmethod
def get_view_name (cl, name): def get_view_name(cl, name):
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([
@ -84,7 +67,7 @@ class Route:
name = cl.get_view_name(name)) name = cl.get_view_name(name))
class DetailRoute (Route): class DetailRoute(Route):
name = 'detail' name = 'detail'
url_args = [ url_args = [
('pk', '[0-9]+'), ('pk', '[0-9]+'),
@ -92,25 +75,25 @@ class DetailRoute (Route):
] ]
@classmethod @classmethod
def get_object (cl, website, model, request, pk, **kwargs): def get_object(cl, website, model, request, pk, **kwargs):
return model.objects.get(pk = int(pk)) return model.objects.get(pk = int(pk))
class AllRoute (Route): class AllRoute(Route):
name = 'all' name = 'all'
@classmethod @classmethod
def get_queryset (cl, website, model, request, **kwargs): def get_queryset(cl, website, model, request, **kwargs):
return model.objects.all() return model.objects.all()
@classmethod @classmethod
def get_title (cl, model, request, **kwargs): def get_title(cl, model, request, **kwargs):
return _('All %(model)s') % { return _('All %(model)s') % {
'model': model._meta.verbose_name_plural 'model': model._meta.verbose_name_plural
} }
class ThreadRoute (Route): class ThreadRoute(Route):
""" """
Select posts using by their assigned thread. Select posts using by their assigned thread.
@ -125,7 +108,7 @@ class ThreadRoute (Route):
] ]
@classmethod @classmethod
def get_queryset (cl, website, model, request, thread_model, pk, **kwargs): def get_queryset(cl, website, model, request, thread_model, pk, **kwargs):
if type(thread_model) is str: if type(thread_model) is str:
thread_model = website.registry.get(thread_model) thread_model = website.registry.get(thread_model)
@ -139,7 +122,7 @@ class ThreadRoute (Route):
) )
class DateRoute (Route): class DateRoute(Route):
name = 'date' name = 'date'
url_args = [ url_args = [
('year', '[0-9]{4}'), ('year', '[0-9]{4}'),
@ -148,7 +131,7 @@ class DateRoute (Route):
] ]
@classmethod @classmethod
def get_queryset (cl, website, model, request, year, month, day, **kwargs): def get_queryset(cl, website, model, request, year, month, day, **kwargs):
return model.objects.filter( return model.objects.filter(
date__year = int(year), date__year = int(year),
date__month = int(month), date__month = int(month),
@ -156,11 +139,11 @@ class DateRoute (Route):
) )
class SearchRoute (Route): class SearchRoute(Route):
name = 'search' name = 'search'
@classmethod @classmethod
def get_queryset (cl, website, model, request, q, **kwargs): def get_queryset(cl, website, model, request, q, **kwargs):
qs = model.objects qs = model.objects
for search_field in model.search_fields or []: for search_field in model.search_fields or []:
r = model.objects.filter(**{ search_field + '__icontains': q }) r = model.objects.filter(**{ search_field + '__icontains': q })

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

@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
def get_url (website, route, model, kwargs): def get_url(website, route, model, kwargs):
name = website.name_of_model(model) name = website.name_of_model(model)
if not name: if not name:
return return
@ -10,7 +10,7 @@ def get_url (website, route, model, kwargs):
return reverse(name, kwargs = kwargs) return reverse(name, kwargs = kwargs)
def filter_thread (qs, object): def filter_thread(qs, object):
model_type = ContentType.objects.get_for_model(object.__class__) model_type = ContentType.objects.get_for_model(object.__class__)
return qs.filter( return qs.filter(
thread_pk = object.pk, thread_pk = object.pk,

View File

@ -20,7 +20,7 @@ class PostBaseView:
embed = False # page is embed (if True, only post content is printed embed = False # page is embed (if True, only post content is printed
classes = '' # extra classes for the content classes = '' # extra classes for the content
def get_base_context (self, **kwargs): def get_base_context(self, **kwargs):
""" """
Return a context with all attributes of this classe plus 'view' set Return a context with all attributes of this classe plus 'view' set
to self. to self.
@ -44,7 +44,7 @@ class PostBaseView:
return context return context
class PostListView (PostBaseView, ListView): class PostListView(PostBaseView, ListView):
""" """
List view for posts and children List view for posts and children
""" """
@ -59,11 +59,11 @@ class PostListView (PostBaseView, ListView):
page = 1 # page number page = 1 # page number
q = None # query search q = None # query search
def __init__ (self, query): def __init__(self, query):
if query: if query:
self.update(query) self.update(query)
def update (self, query): def update(self, query):
my_class = self.__class__ my_class = self.__class__
if type(query) is my_class: if type(query) is my_class:
self.__dict__.update(query.__dict__) self.__dict__.update(query.__dict__)
@ -80,15 +80,15 @@ class PostListView (PostBaseView, ListView):
fields = [ 'date', 'time', 'image', 'title', 'content' ] fields = [ 'date', 'time', 'image', 'title', 'content' ]
icon_size = '64x64' icon_size = '64x64'
def __init__ (self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.query = PostListView.Query(self.query) self.query = PostListView.Query(self.query)
def dispatch (self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.route = self.kwargs.get('route') or self.route self.route = self.kwargs.get('route') or self.route
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_queryset (self): def get_queryset(self):
if self.route: if self.route:
qs = self.route.get_queryset(self.website, self.model, self.request, qs = self.route.get_queryset(self.website, self.model, self.request,
**self.kwargs) **self.kwargs)
@ -116,7 +116,7 @@ class PostListView (PostBaseView, ListView):
return qs return qs
def get_context_data (self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update(self.get_base_context(**kwargs)) context.update(self.get_base_context(**kwargs))
context.update({ context.update({
@ -124,7 +124,7 @@ class PostListView (PostBaseView, ListView):
}) })
return context return context
def get_title (self): def get_title(self):
if self.title: if self.title:
return self.title return self.title
@ -132,14 +132,14 @@ class PostListView (PostBaseView, ListView):
**self.kwargs) **self.kwargs)
return title return title
def get_url (self): def get_url(self):
if self.route: if self.route:
return utils.get_urls(self.website, self.route, return utils.get_urls(self.website, self.route,
self.model, self.kwargs) self.model, self.kwargs)
return '' return ''
class PostDetailView (DetailView, PostBaseView): class PostDetailView(DetailView, PostBaseView):
""" """
Detail view for posts and children Detail view for posts and children
""" """
@ -147,11 +147,11 @@ class PostDetailView (DetailView, PostBaseView):
sections = [] sections = []
def __init__ (self, sections = None, *args, **kwargs): def __init__(self, sections = None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.sections = sections or [] self.sections = sections or []
def get_queryset (self): def get_queryset(self):
if self.request.GET.get('embed'): if self.request.GET.get('embed'):
self.embed = True self.embed = True
@ -159,14 +159,14 @@ class PostDetailView (DetailView, PostBaseView):
return super().get_queryset().filter(published = True) return super().get_queryset().filter(published = True)
return [] return []
def get_object (self, **kwargs): def get_object(self, **kwargs):
if self.model: if self.model:
object = super().get_object(**kwargs) object = super().get_object(**kwargs)
if object.published: if object.published:
return object return object
return None return None
def get_context_data (self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update(self.get_base_context()) context.update(self.get_base_context())
context.update({ context.update({
@ -178,7 +178,7 @@ class PostDetailView (DetailView, PostBaseView):
return context return context
class Menu (View): class Menu(View):
template_name = 'aircox/cms/menu.html' template_name = 'aircox/cms/menu.html'
name = '' name = ''
@ -188,11 +188,11 @@ class Menu (View):
position = '' # top, left, bottom, right, header, footer, page_top, page_bottom position = '' # top, left, bottom, right, header, footer, page_top, page_bottom
sections = None sections = None
def __init__ (self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.name = self.name or ('menu_' + self.position) self.name = self.name or ('menu_' + self.position)
def get_context_data (self, **kwargs): def get_context_data(self, **kwargs):
return { return {
'name': self.name, 'name': self.name,
'tag': self.tag, 'tag': self.tag,
@ -205,7 +205,7 @@ class Menu (View):
] ]
} }
def get (self, request, website, **kwargs): def get(self, request, website, **kwargs):
self.request = request self.request = request
self.website = website self.website = website
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
@ -213,7 +213,7 @@ class Menu (View):
class BaseSection (View): class BaseSection(View):
""" """
Base class for sections. Sections are view that can be used in detail view Base class for sections. Sections are view that can be used in detail view
in order to have extra content about a post, or in menus. in order to have extra content about a post, or in menus.
@ -227,7 +227,7 @@ class BaseSection (View):
visible = True # if false renders an empty string visible = True # if false renders an empty string
def get_context_data (self): def get_context_data(self):
return { return {
'view': self, 'view': self,
'tag': self.tag, 'tag': self.tag,
@ -237,7 +237,7 @@ class BaseSection (View):
'content': self.content, 'content': self.content,
} }
def get (self, request, website, **kwargs): def get(self, request, website, **kwargs):
self.request = request self.request = request
self.website = website self.website = website
self.kwargs = kwargs self.kwargs = kwargs
@ -249,7 +249,7 @@ class BaseSection (View):
return '' return ''
class Section (BaseSection): class Section(BaseSection):
""" """
A Section that can be related to an object. A Section that can be related to an object.
""" """
@ -260,7 +260,7 @@ class Section (BaseSection):
header = '' header = ''
footer = '' footer = ''
def get_context_data (self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
context.update({ context.update({
'title': self.title, 'title': self.title,
@ -269,7 +269,7 @@ class Section (BaseSection):
}) })
return context return context
def get (self, request, object = None, **kwargs): def get(self, request, object = None, **kwargs):
self.object = object or self.object self.object = object or self.object
if self.object_required and not self.object: if self.object_required and not self.object:
raise ValueError('object is required by this Section but not given') raise ValueError('object is required by this Section but not given')
@ -277,43 +277,43 @@ class Section (BaseSection):
class Sections: class Sections:
class Image (BaseSection): class Image(BaseSection):
""" """
Render an image with the given relative url. Render an image with the given relative url.
""" """
url = None url = None
@property @property
def content (self): def content(self):
return '<img src="{}">'.format( return '<img src="{}">'.format(
static(self.url), static(self.url),
) )
class PostContent (Section): class PostContent(Section):
""" """
Render the content of the Post (format the text a bit and escape HTML Render the content of the Post (format the text a bit and escape HTML
tags). tags).
""" """
@property @property
def content (self): def content(self):
content = escape(self.object.content) content = escape(self.object.content)
content = re.sub(r'(^|\n\n)((\n?[^\n])+)', r'<p>\2</p>', content) content = re.sub(r'(^|\n\n)((\n?[^\n])+)', r'<p>\2</p>', content)
content = re.sub(r'\n', r'<br>', content) content = re.sub(r'\n', r'<br>', content)
return content return content
class PostImage (Section): class PostImage(Section):
""" """
Render the image of the Post Render the image of the Post
""" """
@property @property
def content (self): def content(self):
if not self.object.image: if not self.object.image:
return '' return ''
return '<img src="{}" class="post_image">'.format( return '<img src="{}" class="post_image">'.format(
self.object.image.url self.object.image.url
) )
class List (Section): class List(Section):
""" """
Section to render list. The context item 'object_list' is used as list of Section to render list. The context item 'object_list' is used as list of
items to render. items to render.
@ -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
@ -335,13 +337,13 @@ class Sections:
icon_size = '32x32' # icons size icon_size = '32x32' # icons size
template_name = 'aircox/cms/section_list.html' template_name = 'aircox/cms/section_list.html'
def get_object_list (self): def get_object_list(self):
return [] return []
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)
@ -353,14 +355,14 @@ class Sections:
}) })
return context return context
class Urls (List): class Urls(List):
""" """
Render a list of urls of targets that are Posts Render a list of urls of targets that are Posts
""" """
classes = 'section_urls' classes = 'section_urls'
targets = None targets = None
def get_object_list (self): def get_object_list(self):
return [ return [
List.Item( List.Item(
target.image or None, target.image or None,
@ -370,7 +372,7 @@ class Sections:
for target in self.targets for target in self.targets
] ]
class Posts (PostBaseView, Section): class Posts(PostBaseView, Section):
""" """
Render a list using PostListView's template. Render a list using PostListView's template.
""" """
@ -379,13 +381,13 @@ class Sections:
icon_size = '64x64' icon_size = '64x64'
fields = [ 'date', 'time', 'image', 'title', 'content' ] fields = [ 'date', 'time', 'image', 'title', 'content' ]
def get_url (self): def get_url(self):
return '' return ''
def get_object_list (self): def get_object_list(self):
return [] return []
def render_list (self): def render_list(self):
self.embed = True self.embed = True
context = self.get_base_context(**self.kwargs) context = self.get_base_context(**self.kwargs)
context.update({ context.update({
@ -395,7 +397,7 @@ class Sections:
}) })
return render_to_string(PostListView.template_name, context) return render_to_string(PostListView.template_name, context)
def get_context_data (self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['content'] = self.render_list() context['content'] = self.render_list()
return context return context

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
@ -19,7 +20,7 @@ import aircox.programs.settings as settings
logger = logging.getLogger('aircox.core') logger = logging.getLogger('aircox.core')
def date_or_default (date, date_only = False): def date_or_default(date, date_only = False):
""" """
Return date or default value (now) if not defined, and remove time info Return date or default value (now) if not defined, and remove time info
if date_only is True if date_only is True
@ -32,20 +33,20 @@ def date_or_default (date, date_only = False):
return date return date
class Nameable (models.Model): class Nameable(models.Model):
name = models.CharField ( name = models.CharField (
_('name'), _('name'),
max_length = 128, max_length = 128,
) )
@property @property
def slug (self): def slug(self):
""" """
Slug based on the name. We replace '-' by '_' Slug based on the name. We replace '-' by '_'
""" """
return slugify(self.name).replace('-', '_') return slugify(self.name).replace('-', '_')
def __str__ (self): def __str__(self):
#if self.pk: #if self.pk:
# return '#{} {}'.format(self.pk, self.name) # return '#{} {}'.format(self.pk, self.name)
return '{}'.format(self.name) return '{}'.format(self.name)
@ -54,7 +55,7 @@ class Nameable (models.Model):
abstract = True abstract = True
class Track (Nameable): class Track(Nameable):
""" """
Track of a playlist of a diffusion. The position can either be expressed Track of a playlist of a diffusion. The position can either be expressed
as the position in the playlist or as the moment in seconds it started. as the position in the playlist or as the moment in seconds it started.
@ -87,7 +88,7 @@ class Track (Nameable):
verbose_name_plural = _('Tracks') verbose_name_plural = _('Tracks')
class Sound (Nameable): class Sound(Nameable):
""" """
A Sound is the representation of a sound file that can be either an excerpt A Sound is the representation of a sound file that can be either an excerpt
or a complete archive of the related diffusion. or a complete archive of the related diffusion.
@ -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(
@ -148,7 +146,7 @@ class Sound (Nameable):
help_text = _('sound\'s is accessible through the website') help_text = _('sound\'s is accessible through the website')
) )
def get_mtime (self): def get_mtime(self):
""" """
Get the last modification date from file Get the last modification date from file
""" """
@ -158,13 +156,13 @@ class Sound (Nameable):
mtime = mtime.replace(microsecond = 0) mtime = mtime.replace(microsecond = 0)
return tz.make_aware(mtime, tz.get_current_timezone()) return tz.make_aware(mtime, tz.get_current_timezone())
def file_exists (self): def file_exists(self):
""" """
Return true if the file still exists Return true if the file still exists
""" """
return os.path.exists(self.path) return os.path.exists(self.path)
def check_on_file (self): def check_on_file(self):
""" """
Check sound file info again'st self, and update informations if Check sound file info again'st self, and update informations if
needed (do not save). Return True if there was changes. needed (do not save). Return True if there was changes.
@ -188,7 +186,7 @@ class Sound (Nameable):
return True return True
return old_removed != self.removed return old_removed != self.removed
def save (self, check = True, *args, **kwargs): def save(self, check = True, *args, **kwargs):
if check: if check:
self.check_on_file() self.check_on_file()
@ -198,7 +196,7 @@ class Sound (Nameable):
self.name = self.name.replace('_', ' ') self.name = self.name.replace('_', ' ')
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__ (self): def __str__(self):
return '/'.join(self.path.split('/')[-3:]) return '/'.join(self.path.split('/')[-3:])
class Meta: class Meta:
@ -206,7 +204,7 @@ class Sound (Nameable):
verbose_name_plural = _('Sounds') verbose_name_plural = _('Sounds')
class Stream (models.Model): class Stream(models.Model):
""" """
When there are no program scheduled, it is possible to play sounds When there are no program scheduled, it is possible to play sounds
in order to avoid blanks. A Stream is a Program that plays this role, in order to avoid blanks. A Stream is a Program that plays this role,
@ -238,7 +236,7 @@ class Stream (models.Model):
) )
class Schedule (models.Model): class Schedule(models.Model):
""" """
A Schedule defines time slots of programs' diffusions. It can be an initial A Schedule defines time slots of programs' diffusions. It can be an initial
run or a rerun (in such case it is linked to the related schedule). run or a rerun (in such case it is linked to the related schedule).
@ -282,7 +280,7 @@ class Schedule (models.Model):
help_text = 'this schedule is a rerun of this one', help_text = 'this schedule is a rerun of this one',
) )
def match (self, date = None, check_time = True): def match(self, date = None, check_time = True):
""" """
Return True if the given datetime matches the schedule Return True if the given datetime matches the schedule
""" """
@ -292,7 +290,7 @@ class Schedule (models.Model):
return self.date.time() == date.time() if check_time else True return self.date.time() == date.time() if check_time else True
return False return False
def match_week (self, date = None): def match_week(self, date = None):
""" """
Return True if the given week number matches the schedule, False Return True if the given week number matches the schedule, False
otherwise. otherwise.
@ -313,13 +311,13 @@ class Schedule (models.Model):
return self.frequency == 0b1111 return self.frequency == 0b1111
return (self.frequency & (0b0001 << week) > 0) return (self.frequency & (0b0001 << week) > 0)
def normalize (self, date): def normalize(self, date):
""" """
Set the time of a datetime to the schedule's one Set the time of a datetime to the schedule's one
""" """
return date.replace(hour = self.date.hour, minute = self.date.minute) return date.replace(hour = self.date.hour, minute = self.date.minute)
def dates_of_month (self, date = None): def dates_of_month(self, date = None):
""" """
Return a list with all matching dates of date.month (=today) Return a list with all matching dates of date.month (=today)
""" """
@ -361,7 +359,7 @@ class Schedule (models.Model):
date += tz.timedelta(days = 7) date += tz.timedelta(days = 7)
return [self.normalize(date) for date in dates] return [self.normalize(date) for date in dates]
def diffusions_of_month (self, date, exclude_saved = False): def diffusions_of_month(self, date, exclude_saved = False):
""" """
Return a list of Diffusion instances, from month of the given date, that Return a list of Diffusion instances, from month of the given date, that
can be not in the database. can be not in the database.
@ -394,19 +392,19 @@ 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,
)) ))
return diffusions return diffusions
def __str__ (self): def __str__(self):
return ' | '.join([ '#' + str(self.id), self.program.name, return ' | '.join([ '#' + str(self.id), self.program.name,
self.get_frequency_display(), self.get_frequency_display(),
self.date.strftime('%a %H:%M') ]) self.date.strftime('%a %H:%M') ])
def save (self, *args, **kwargs): def save(self, *args, **kwargs):
if self.initial: if self.initial:
self.program = self.initial.program self.program = self.initial.program
self.duration = self.initial.duration self.duration = self.initial.duration
@ -418,7 +416,7 @@ class Schedule (models.Model):
verbose_name_plural = _('Schedules') verbose_name_plural = _('Schedules')
class Station (Nameable): class Station(Nameable):
""" """
A Station regroup one or more programs (stream and normal), and is the top A Station regroup one or more programs (stream and normal), and is the top
element used to generate streams outputs and configuration. element used to generate streams outputs and configuration.
@ -444,7 +442,7 @@ class Station (Nameable):
) )
class Program (Nameable): class Program(Nameable):
""" """
A Program can either be a Streamed or a Scheduled program. A Program can either be a Streamed or a Scheduled program.
@ -473,14 +471,14 @@ class Program (Nameable):
) )
@property @property
def path (self): def path(self):
""" """
Return the path to the programs directory Return the path to the programs directory
""" """
return os.path.join(settings.AIRCOX_PROGRAMS_DIR, return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
self.slug + '_' + str(self.id) ) self.slug + '_' + str(self.id) )
def ensure_dir (self, subdir = None): def ensure_dir(self, subdir = None):
""" """
Make sur the program's dir exists (and optionally subdir). Return True Make sur the program's dir exists (and optionally subdir). Return True
if the dir (or subdir) exists. if the dir (or subdir) exists.
@ -490,7 +488,7 @@ class Program (Nameable):
os.makedirs(path, exist_ok = True) os.makedirs(path, exist_ok = True)
return os.path.exists(path) return os.path.exists(path)
def find_schedule (self, date): def find_schedule(self, date):
""" """
Return the first schedule that matches a given date. Return the first schedule that matches a given date.
""" """
@ -499,12 +497,12 @@ class Program (Nameable):
if schedule.match(date, check_time = False): if schedule.match(date, check_time = False):
return schedule return schedule
def __init__ (self, *kargs, **kwargs): def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
if self.name: if self.name:
self.__original_path = self.path self.__original_path = self.path
def save (self, *kargs, **kwargs): def save(self, *kargs, **kwargs):
super().save(*kargs, **kwargs) super().save(*kargs, **kwargs)
if hasattr(self, '__original_path') and \ if hasattr(self, '__original_path') and \
self.__original_path != self.path and \ self.__original_path != self.path and \
@ -520,7 +518,7 @@ class Program (Nameable):
sound.save() sound.save()
@classmethod @classmethod
def get_from_path (cl, path): def get_from_path(cl, path):
""" """
Return a Program from the given path. We assume the path has been Return a Program from the given path. We assume the path has been
given in a previous time by this model (Program.path getter). given in a previous time by this model (Program.path getter).
@ -537,7 +535,7 @@ class Program (Nameable):
return qs[0] if qs else None return qs[0] if qs else None
class Diffusion (models.Model): class Diffusion(models.Model):
""" """
A Diffusion is an occurrence of a Program that is scheduled on the A Diffusion is an occurrence of a Program that is scheduled on the
station's timetable. It can be a rerun of a previous diffusion. In such station's timetable. It can be a rerun of a previous diffusion. In such
@ -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',
@ -588,11 +583,11 @@ class Diffusion (models.Model):
end = models.DateTimeField( _('end of the diffusion') ) end = models.DateTimeField( _('end of the diffusion') )
@property @property
def duration (self): def duration(self):
return self.end - self.start return self.end - self.start
@property @property
def date (self): def date(self):
return self.start return self.start
@property @property
@ -604,29 +599,30 @@ class Diffusion (models.Model):
playlist.sort() playlist.sort()
return playlist return playlist
def archives_duration (self): def archives_duration(self):
""" """
Get total duration of the archives. May differ from the schedule Get total duration of the archives. May differ from the schedule
duration. duration.
""" """
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)
def get_archives (self): def get_archives(self):
""" """
Return an ordered list of archives sounds for the given episode. Return an ordered list of archives sounds for the given episode.
""" """
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)
@ -669,7 +667,7 @@ class Diffusion (models.Model):
""" """
return self.start < date_or_default(date) < self.end return self.start < date_or_default(date) < self.end
def get_conflicts (self): def get_conflicts(self):
""" """
Return a list of conflictual diffusions, based on the scheduled duration. Return a list of conflictual diffusions, based on the scheduled duration.
""" """
@ -681,7 +679,7 @@ class Diffusion (models.Model):
) )
return r return r
def save (self, *args, **kwargs): def save(self, *args, **kwargs):
if self.initial: if self.initial:
# force link to the top initial diffusion # force link to the top initial diffusion
if self.initial.initial: if self.initial.initial:
@ -689,7 +687,7 @@ class Diffusion (models.Model):
self.program = self.initial.program self.program = self.initial.program
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__ (self): def __str__(self):
return '{self.program.name} {date} #{self.pk}'.format( return '{self.program.name} {date} #{self.pk}'.format(
self=self, date=self.date.strftime('%Y-%m-%d %H:%M') self=self, date=self.date.strftime('%Y-%m-%d %H:%M')
) )
@ -702,7 +700,7 @@ class Diffusion (models.Model):
('programming', _('edit the diffusion\'s planification')), ('programming', _('edit the diffusion\'s planification')),
) )
class Log (models.Model): class Log(models.Model):
""" """
Log a played sound start and stop, or a single message Log a played sound start and stop, or a single message
""" """
@ -733,14 +731,14 @@ class Log (models.Model):
@classmethod @classmethod
def get_for_related_model (cl, model): def get_for_related_model(cl, model):
""" """
Return a queryset that filter related_type to the given one. Return a queryset that filter related_type to the given one.
""" """
return cl.objects.filter(related_type__pk = return cl.objects.filter(related_type__pk =
ContentType.objects.get_for_model(model).id) ContentType.objects.get_for_model(model).id)
def print (self): def print(self):
logger.info('log #%s: %s%s', logger.info('log #%s: %s%s',
str(self), str(self),
self.comment or '', self.comment or '',
@ -748,7 +746,7 @@ class Log (models.Model):
if self.related_object else '' if self.related_object else ''
) )
def __str__ (self): def __str__(self):
return '#{} ({}, {})'.format( return '#{} ({}, {})'.format(
self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source
) )

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)