work on cms; add templatetags, and few work on templates
This commit is contained in:
		@ -24,10 +24,10 @@ class Post (models.Model):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        blank = True, null = True
 | 
			
		||||
    )
 | 
			
		||||
    thread_pk = models.PositiveIntegerField(
 | 
			
		||||
    thread_id = models.PositiveIntegerField(
 | 
			
		||||
        blank = True, null = True
 | 
			
		||||
    )
 | 
			
		||||
    thread = GenericForeignKey('thread_type', 'thread_pk')
 | 
			
		||||
    thread = GenericForeignKey('thread_type', 'thread_id')
 | 
			
		||||
 | 
			
		||||
    published = models.BooleanField(
 | 
			
		||||
        verbose_name = _('public'),
 | 
			
		||||
@ -64,9 +64,21 @@ class Post (models.Model):
 | 
			
		||||
        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):
 | 
			
		||||
        return reverse(self._meta.verbose_name.lower() + '_detail',
 | 
			
		||||
        return reverse(self._website.name_of_model(self.__class__) + '_detail',
 | 
			
		||||
                       kwargs = { 'pk': self.pk,
 | 
			
		||||
                                  'slug': slugify(self.title) })
 | 
			
		||||
 | 
			
		||||
@ -145,6 +157,7 @@ class RelatedPostBase (models.base.ModelBase):
 | 
			
		||||
 | 
			
		||||
    def __new__ (cl, name, bases, attrs):
 | 
			
		||||
        # TODO: allow proxy models and better inheritance
 | 
			
		||||
        # TODO: check bindings
 | 
			
		||||
        if name == 'RelatedPost':
 | 
			
		||||
            return super().__new__(cl, name, bases, attrs)
 | 
			
		||||
 | 
			
		||||
@ -267,18 +280,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
 | 
			
		||||
            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):
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        """
 | 
			
		||||
        rel = self._relation
 | 
			
		||||
        if rel.bindings:
 | 
			
		||||
        if not rel.bindings:
 | 
			
		||||
            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():
 | 
			
		||||
            if attr == 'thread':
 | 
			
		||||
                continue
 | 
			
		||||
            self.set_rel_attr
 | 
			
		||||
            value = getattr(self.related, attr) \
 | 
			
		||||
                    if hasattr(self.related, attr) else None
 | 
			
		||||
            setattr(self, attr, value)
 | 
			
		||||
            value = getattr(self.related, rel_attr)
 | 
			
		||||
            set_attr(attr, value)
 | 
			
		||||
 | 
			
		||||
        if rel.thread_model:
 | 
			
		||||
            thread = self.get_rel_attr('thread')
 | 
			
		||||
            thread = rel.thread_model.objects.filter(related = thread) \
 | 
			
		||||
                        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()
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, *kargs, **kwargs):
 | 
			
		||||
@ -329,10 +335,10 @@ class Comment(models.Model):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        blank = True, null = True
 | 
			
		||||
    )
 | 
			
		||||
    thread_pk = models.PositiveIntegerField(
 | 
			
		||||
    thread_id = models.PositiveIntegerField(
 | 
			
		||||
        blank = True, null = True
 | 
			
		||||
    )
 | 
			
		||||
    thread = GenericForeignKey('thread_type', 'thread_pk')
 | 
			
		||||
    thread = GenericForeignKey('thread_type', 'thread_id')
 | 
			
		||||
 | 
			
		||||
    author = models.TextField(
 | 
			
		||||
        verbose_name = _('author'),
 | 
			
		||||
 | 
			
		||||
@ -3,65 +3,48 @@ from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
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:
 | 
			
		||||
    """
 | 
			
		||||
    Base class for routing. Given a model, we generate url specific for each
 | 
			
		||||
    route type. The generated url takes this form:
 | 
			
		||||
        model_name + '/' + route_name + '/' + '/'.join(route_url_args)
 | 
			
		||||
    type of route.
 | 
			
		||||
 | 
			
		||||
    Where model_name by default is the given model's verbose_name (uses plural if
 | 
			
		||||
    Route is for a list).
 | 
			
		||||
    The generated url takes this form:
 | 
			
		||||
        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
 | 
			
		||||
    url_args = []       # arguments passed from the url [ (name : regex),... ]
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_title (cl, model, request, **kwargs):
 | 
			
		||||
    def get_title(cl, model, request, **kwargs):
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_view_name (cl, name):
 | 
			
		||||
    def get_view_name(cl, name):
 | 
			
		||||
        return name + '_' + cl.name
 | 
			
		||||
 | 
			
		||||
    @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)
 | 
			
		||||
        if cl.url_args:
 | 
			
		||||
            url_args = '/'.join([
 | 
			
		||||
@ -84,7 +67,7 @@ class Route:
 | 
			
		||||
                   name = cl.get_view_name(name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DetailRoute (Route):
 | 
			
		||||
class DetailRoute(Route):
 | 
			
		||||
    name = 'detail'
 | 
			
		||||
    url_args = [
 | 
			
		||||
        ('pk', '[0-9]+'),
 | 
			
		||||
@ -92,25 +75,25 @@ class DetailRoute (Route):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    @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))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllRoute (Route):
 | 
			
		||||
class AllRoute(Route):
 | 
			
		||||
    name = 'all'
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_queryset (cl, website, model, request, **kwargs):
 | 
			
		||||
    def get_queryset(cl, website, model, request, **kwargs):
 | 
			
		||||
        return model.objects.all()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_title (cl, model, request, **kwargs):
 | 
			
		||||
    def get_title(cl, model, request, **kwargs):
 | 
			
		||||
        return _('All %(model)s') % {
 | 
			
		||||
            'model': model._meta.verbose_name_plural
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ThreadRoute (Route):
 | 
			
		||||
class ThreadRoute(Route):
 | 
			
		||||
    """
 | 
			
		||||
    Select posts using by their assigned thread.
 | 
			
		||||
 | 
			
		||||
@ -125,7 +108,7 @@ class ThreadRoute (Route):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    @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:
 | 
			
		||||
            thread_model = website.registry.get(thread_model)
 | 
			
		||||
 | 
			
		||||
@ -139,7 +122,7 @@ class ThreadRoute (Route):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DateRoute (Route):
 | 
			
		||||
class DateRoute(Route):
 | 
			
		||||
    name = 'date'
 | 
			
		||||
    url_args = [
 | 
			
		||||
        ('year', '[0-9]{4}'),
 | 
			
		||||
@ -148,7 +131,7 @@ class DateRoute (Route):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    @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(
 | 
			
		||||
            date__year = int(year),
 | 
			
		||||
            date__month = int(month),
 | 
			
		||||
@ -156,11 +139,11 @@ class DateRoute (Route):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchRoute (Route):
 | 
			
		||||
class SearchRoute(Route):
 | 
			
		||||
    name = 'search'
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_queryset (cl, website, model, request, q, **kwargs):
 | 
			
		||||
    def get_queryset(cl, website, model, request, q, **kwargs):
 | 
			
		||||
        qs = model.objects
 | 
			
		||||
        for search_field in model.search_fields or []:
 | 
			
		||||
            r = model.objects.filter(**{ search_field + '__icontains': q })
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,31 @@
 | 
			
		||||
{% extends embed|yesno:"aircox/cms/base_content.html,aircox/cms/base_site.html" %}
 | 
			
		||||
{% load aircox_cms %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{{ object.title }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block pre_title %}
 | 
			
		||||
<div class="pre_title">
 | 
			
		||||
{% if object.thread %}
 | 
			
		||||
<div class="threads">
 | 
			
		||||
    {{ object|threads:' > '|safe }}
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<time datetime="{{ object.date }}">
 | 
			
		||||
    {{ object.date|date:'l d F Y' }},
 | 
			
		||||
    {{ object.date|time:'H\hi' }}
 | 
			
		||||
</time>
 | 
			
		||||
 | 
			
		||||
{% if object.tags %}
 | 
			
		||||
{# TODO: url to the tags #}
 | 
			
		||||
<div class="tags">
 | 
			
		||||
    {{ object.tags.all|join:', ' }}
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
{% block section_content %}
 | 
			
		||||
<ul style="padding:0; margin:0">
 | 
			
		||||
    {% for item in object_list %}
 | 
			
		||||
    <li>
 | 
			
		||||
    <li class="{{item.css}}">
 | 
			
		||||
        {% if item.url %}
 | 
			
		||||
        <a href="{{item.url}}">
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								cms/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cms/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										23
									
								
								cms/templatetags/aircox_cms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								cms/templatetags/aircox_cms.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
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)
 | 
			
		||||
    if not name:
 | 
			
		||||
        return
 | 
			
		||||
@ -10,7 +10,7 @@ def get_url (website, route, model, 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__)
 | 
			
		||||
    return qs.filter(
 | 
			
		||||
        thread_pk = object.pk,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										88
									
								
								cms/views.py
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								cms/views.py
									
									
									
									
									
								
							@ -20,7 +20,7 @@ class PostBaseView:
 | 
			
		||||
    embed = False   # page is embed (if True, only post content is printed
 | 
			
		||||
    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
 | 
			
		||||
        to self.
 | 
			
		||||
@ -44,7 +44,7 @@ class PostBaseView:
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PostListView (PostBaseView, ListView):
 | 
			
		||||
class PostListView(PostBaseView, ListView):
 | 
			
		||||
    """
 | 
			
		||||
    List view for posts and children
 | 
			
		||||
    """
 | 
			
		||||
@ -59,11 +59,11 @@ class PostListView (PostBaseView, ListView):
 | 
			
		||||
        page = 1        # page number
 | 
			
		||||
        q = None        # query search
 | 
			
		||||
 | 
			
		||||
        def __init__ (self, query):
 | 
			
		||||
        def __init__(self, query):
 | 
			
		||||
            if query:
 | 
			
		||||
                self.update(query)
 | 
			
		||||
 | 
			
		||||
        def update (self, query):
 | 
			
		||||
        def update(self, query):
 | 
			
		||||
            my_class = self.__class__
 | 
			
		||||
            if type(query) is my_class:
 | 
			
		||||
                self.__dict__.update(query.__dict__)
 | 
			
		||||
@ -80,15 +80,15 @@ class PostListView (PostBaseView, ListView):
 | 
			
		||||
    fields = [ 'date', 'time', 'image', 'title', 'content' ]
 | 
			
		||||
    icon_size = '64x64'
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, *args, **kwargs):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        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
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_queryset (self):
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        if self.route:
 | 
			
		||||
            qs = self.route.get_queryset(self.website, self.model, self.request,
 | 
			
		||||
                                         **self.kwargs)
 | 
			
		||||
@ -116,7 +116,7 @@ class PostListView (PostBaseView, ListView):
 | 
			
		||||
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
    def get_context_data (self, **kwargs):
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context.update(self.get_base_context(**kwargs))
 | 
			
		||||
        context.update({
 | 
			
		||||
@ -124,7 +124,7 @@ class PostListView (PostBaseView, ListView):
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get_title (self):
 | 
			
		||||
    def get_title(self):
 | 
			
		||||
        if self.title:
 | 
			
		||||
            return self.title
 | 
			
		||||
 | 
			
		||||
@ -132,14 +132,14 @@ class PostListView (PostBaseView, ListView):
 | 
			
		||||
                                                    **self.kwargs)
 | 
			
		||||
        return title
 | 
			
		||||
 | 
			
		||||
    def get_url (self):
 | 
			
		||||
    def get_url(self):
 | 
			
		||||
        if self.route:
 | 
			
		||||
            return utils.get_urls(self.website, self.route,
 | 
			
		||||
                                  self.model, self.kwargs)
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PostDetailView (DetailView, PostBaseView):
 | 
			
		||||
class PostDetailView(DetailView, PostBaseView):
 | 
			
		||||
    """
 | 
			
		||||
    Detail view for posts and children
 | 
			
		||||
    """
 | 
			
		||||
@ -147,11 +147,11 @@ class PostDetailView (DetailView, PostBaseView):
 | 
			
		||||
 | 
			
		||||
    sections = []
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, sections = None, *args, **kwargs):
 | 
			
		||||
    def __init__(self, sections = None, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.sections = sections or []
 | 
			
		||||
 | 
			
		||||
    def get_queryset (self):
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        if self.request.GET.get('embed'):
 | 
			
		||||
            self.embed = True
 | 
			
		||||
 | 
			
		||||
@ -159,14 +159,14 @@ class PostDetailView (DetailView, PostBaseView):
 | 
			
		||||
            return super().get_queryset().filter(published = True)
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
    def get_object (self, **kwargs):
 | 
			
		||||
    def get_object(self, **kwargs):
 | 
			
		||||
        if self.model:
 | 
			
		||||
            object = super().get_object(**kwargs)
 | 
			
		||||
            if object.published:
 | 
			
		||||
                return object
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_context_data (self, **kwargs):
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context.update(self.get_base_context())
 | 
			
		||||
        context.update({
 | 
			
		||||
@ -178,7 +178,7 @@ class PostDetailView (DetailView, PostBaseView):
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Menu (View):
 | 
			
		||||
class Menu(View):
 | 
			
		||||
    template_name = 'aircox/cms/menu.html'
 | 
			
		||||
 | 
			
		||||
    name = ''
 | 
			
		||||
@ -188,11 +188,11 @@ class Menu (View):
 | 
			
		||||
    position = ''   # top, left, bottom, right, header, footer, page_top, page_bottom
 | 
			
		||||
    sections = None
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, *args, **kwargs):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.name = self.name or ('menu_' + self.position)
 | 
			
		||||
 | 
			
		||||
    def get_context_data (self, **kwargs):
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        return {
 | 
			
		||||
            'name': self.name,
 | 
			
		||||
            '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.website = website
 | 
			
		||||
        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
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_context_data (self):
 | 
			
		||||
    def get_context_data(self):
 | 
			
		||||
        return {
 | 
			
		||||
            'view': self,
 | 
			
		||||
            'tag': self.tag,
 | 
			
		||||
@ -237,7 +237,7 @@ class BaseSection (View):
 | 
			
		||||
            'content': self.content,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def get (self, request, website, **kwargs):
 | 
			
		||||
    def get(self, request, website, **kwargs):
 | 
			
		||||
        self.request = request
 | 
			
		||||
        self.website = website
 | 
			
		||||
        self.kwargs = kwargs
 | 
			
		||||
@ -249,7 +249,7 @@ class BaseSection (View):
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Section (BaseSection):
 | 
			
		||||
class Section(BaseSection):
 | 
			
		||||
    """
 | 
			
		||||
    A Section that can be related to an object.
 | 
			
		||||
    """
 | 
			
		||||
@ -260,7 +260,7 @@ class Section (BaseSection):
 | 
			
		||||
    header = ''
 | 
			
		||||
    footer = ''
 | 
			
		||||
 | 
			
		||||
    def get_context_data (self):
 | 
			
		||||
    def get_context_data(self):
 | 
			
		||||
        context = super().get_context_data()
 | 
			
		||||
        context.update({
 | 
			
		||||
            'title': self.title,
 | 
			
		||||
@ -269,7 +269,7 @@ class Section (BaseSection):
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get (self, request, object = None, **kwargs):
 | 
			
		||||
    def get(self, request, object = None, **kwargs):
 | 
			
		||||
        self.object = object or self.object
 | 
			
		||||
        if self.object_required and not self.object:
 | 
			
		||||
            raise ValueError('object is required by this Section but not given')
 | 
			
		||||
@ -277,43 +277,43 @@ class Section (BaseSection):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sections:
 | 
			
		||||
    class Image (BaseSection):
 | 
			
		||||
    class Image(BaseSection):
 | 
			
		||||
        """
 | 
			
		||||
        Render an image with the given relative url.
 | 
			
		||||
        """
 | 
			
		||||
        url = None
 | 
			
		||||
 | 
			
		||||
        @property
 | 
			
		||||
        def content (self):
 | 
			
		||||
        def content(self):
 | 
			
		||||
            return '<img src="{}">'.format(
 | 
			
		||||
                        static(self.url),
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    class PostContent (Section):
 | 
			
		||||
    class PostContent(Section):
 | 
			
		||||
        """
 | 
			
		||||
        Render the content of the Post (format the text a bit and escape HTML
 | 
			
		||||
        tags).
 | 
			
		||||
        """
 | 
			
		||||
        @property
 | 
			
		||||
        def content (self):
 | 
			
		||||
        def content(self):
 | 
			
		||||
            content = escape(self.object.content)
 | 
			
		||||
            content = re.sub(r'(^|\n\n)((\n?[^\n])+)', r'<p>\2</p>', content)
 | 
			
		||||
            content = re.sub(r'\n', r'<br>', content)
 | 
			
		||||
            return content
 | 
			
		||||
 | 
			
		||||
    class PostImage (Section):
 | 
			
		||||
    class PostImage(Section):
 | 
			
		||||
        """
 | 
			
		||||
        Render the image of the Post
 | 
			
		||||
        """
 | 
			
		||||
        @property
 | 
			
		||||
        def content (self):
 | 
			
		||||
        def content(self):
 | 
			
		||||
            if not self.object.image:
 | 
			
		||||
                return ''
 | 
			
		||||
            return '<img src="{}" class="post_image">'.format(
 | 
			
		||||
                        self.object.image.url
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    class List (Section):
 | 
			
		||||
    class List(Section):
 | 
			
		||||
        """
 | 
			
		||||
        Section to render list. The context item 'object_list' is used as list of
 | 
			
		||||
        items to render.
 | 
			
		||||
@ -323,11 +323,13 @@ class Sections:
 | 
			
		||||
            title = None
 | 
			
		||||
            text = 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.title = title
 | 
			
		||||
                self.text = text
 | 
			
		||||
                self.css = css
 | 
			
		||||
 | 
			
		||||
        hide_empty = False      # hides the section if the list is empty
 | 
			
		||||
        use_icons = True        # print icons
 | 
			
		||||
@ -335,13 +337,13 @@ class Sections:
 | 
			
		||||
        icon_size = '32x32'     # icons size
 | 
			
		||||
        template_name = 'aircox/cms/section_list.html'
 | 
			
		||||
 | 
			
		||||
        def get_object_list (self):
 | 
			
		||||
        def get_object_list(self):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        def get_context_data (self, **kwargs):
 | 
			
		||||
        def get_context_data(self, **kwargs):
 | 
			
		||||
            object_list = self.get_object_list()
 | 
			
		||||
            self.visibility = True
 | 
			
		||||
            if not object_list and hide_empty:
 | 
			
		||||
            if not object_list and self.hide_empty:
 | 
			
		||||
                self.visibility = False
 | 
			
		||||
 | 
			
		||||
            context = super().get_context_data(**kwargs)
 | 
			
		||||
@ -353,14 +355,14 @@ class Sections:
 | 
			
		||||
            })
 | 
			
		||||
            return context
 | 
			
		||||
 | 
			
		||||
    class Urls (List):
 | 
			
		||||
    class Urls(List):
 | 
			
		||||
        """
 | 
			
		||||
        Render a list of urls of targets that are Posts
 | 
			
		||||
        """
 | 
			
		||||
        classes = 'section_urls'
 | 
			
		||||
        targets = None
 | 
			
		||||
 | 
			
		||||
        def get_object_list (self):
 | 
			
		||||
        def get_object_list(self):
 | 
			
		||||
            return [
 | 
			
		||||
                List.Item(
 | 
			
		||||
                    target.image or None,
 | 
			
		||||
@ -370,7 +372,7 @@ class Sections:
 | 
			
		||||
                for target in self.targets
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
    class Posts (PostBaseView, Section):
 | 
			
		||||
    class Posts(PostBaseView, Section):
 | 
			
		||||
        """
 | 
			
		||||
        Render a list using PostListView's template.
 | 
			
		||||
        """
 | 
			
		||||
@ -379,13 +381,13 @@ class Sections:
 | 
			
		||||
        icon_size = '64x64'
 | 
			
		||||
        fields = [ 'date', 'time', 'image', 'title', 'content' ]
 | 
			
		||||
 | 
			
		||||
        def get_url (self):
 | 
			
		||||
        def get_url(self):
 | 
			
		||||
            return ''
 | 
			
		||||
 | 
			
		||||
        def get_object_list (self):
 | 
			
		||||
        def get_object_list(self):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        def render_list (self):
 | 
			
		||||
        def render_list(self):
 | 
			
		||||
            self.embed = True
 | 
			
		||||
            context = self.get_base_context(**self.kwargs)
 | 
			
		||||
            context.update({
 | 
			
		||||
@ -395,7 +397,7 @@ class Sections:
 | 
			
		||||
            })
 | 
			
		||||
            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['content'] = self.render_list()
 | 
			
		||||
            return context
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ class Website:
 | 
			
		||||
            raise ValueError('A model has yet been registered under "{}"'
 | 
			
		||||
                             .format(name))
 | 
			
		||||
        self.registry[name] = model
 | 
			
		||||
        model._website = self
 | 
			
		||||
        return name
 | 
			
		||||
 | 
			
		||||
    def register_detail (self, name, model, view = views.PostDetailView,
 | 
			
		||||
@ -48,7 +49,7 @@ class Website:
 | 
			
		||||
            model = model,
 | 
			
		||||
            **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
 | 
			
		||||
 | 
			
		||||
    def register_list (self, name, model, view = views.PostListView,
 | 
			
		||||
@ -62,7 +63,7 @@ class Website:
 | 
			
		||||
            model = model,
 | 
			
		||||
            **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
 | 
			
		||||
 | 
			
		||||
    def register (self, name, model, sections = None, routes = None,
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,9 @@ class Monitor:
 | 
			
		||||
        args = {'start__gt': prev_diff.start } if prev_diff else {}
 | 
			
		||||
        next_diff = programs.Diffusion \
 | 
			
		||||
                        .get(controller.station, now, now = True,
 | 
			
		||||
                             sounds__isnull = False, **args) \
 | 
			
		||||
                             type = programs.Diffusion.Type.normal,
 | 
			
		||||
                             sounds__isnull = False,
 | 
			
		||||
                             **args) \
 | 
			
		||||
                        .prefetch_related('sounds')
 | 
			
		||||
        if next_diff:
 | 
			
		||||
            next_diff = next_diff[0]
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,7 @@ class DiffusionAdmin (admin.ModelAdmin):
 | 
			
		||||
        return ', '.join(sounds) if sounds else ''
 | 
			
		||||
 | 
			
		||||
    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 ''
 | 
			
		||||
 | 
			
		||||
@ -115,9 +115,9 @@ class DiffusionAdmin (admin.ModelAdmin):
 | 
			
		||||
        qs = super(DiffusionAdmin, self).get_queryset(request)
 | 
			
		||||
        if '_changelist_filters' in request.GET or \
 | 
			
		||||
            '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.exclude(type = Diffusion.Type['unconfirmed'])
 | 
			
		||||
        return qs.exclude(type = Diffusion.Type.unconfirmed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Log)
 | 
			
		||||
 | 
			
		||||
@ -46,15 +46,15 @@ class Actions:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            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()
 | 
			
		||||
 | 
			
		||||
        if not conflicts:
 | 
			
		||||
            item.type = Diffusion.Type['normal']
 | 
			
		||||
            item.type = Diffusion.Type.normal
 | 
			
		||||
            return 0
 | 
			
		||||
 | 
			
		||||
        item.type = Diffusion.Type['unconfirmed']
 | 
			
		||||
        item.type = Diffusion.Type.unconfirmed
 | 
			
		||||
        return len(conflicts)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@ -93,14 +93,14 @@ class Actions:
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def clean (date):
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'],
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__lt = date)
 | 
			
		||||
        logger.info('[clean] %d diffusions will be removed', qs.count())
 | 
			
		||||
        qs.delete()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def check (date):
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type['unconfirmed'],
 | 
			
		||||
        qs = Diffusion.objects.filter(type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                      start__gt = date)
 | 
			
		||||
        items = []
 | 
			
		||||
        for diffusion in qs:
 | 
			
		||||
 | 
			
		||||
@ -162,9 +162,9 @@ class MonitorHandler (PatternMatchingEventHandler):
 | 
			
		||||
        """
 | 
			
		||||
        self.subdir = subdir
 | 
			
		||||
        if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type['archive'] }
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type.archive }
 | 
			
		||||
        else:
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type['excerpt'] }
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type.excerpt }
 | 
			
		||||
 | 
			
		||||
        patterns = ['*/{}/*{}'.format(self.subdir, ext)
 | 
			
		||||
                    for ext in settings.AIRCOX_SOUND_FILE_EXT ]
 | 
			
		||||
@ -264,11 +264,11 @@ class Command (BaseCommand):
 | 
			
		||||
            logger.info('#%d %s', program.id, program.name)
 | 
			
		||||
            self.scan_for_program(
 | 
			
		||||
                program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
 | 
			
		||||
                type = Sound.Type['archive'],
 | 
			
		||||
                type = Sound.Type.archive,
 | 
			
		||||
            )
 | 
			
		||||
            self.scan_for_program(
 | 
			
		||||
                program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
 | 
			
		||||
                type = Sound.Type['excerpt'],
 | 
			
		||||
                type = Sound.Type.excerpt,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def scan_for_program (self, program, subdir, **sound_kwargs):
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import logging
 | 
			
		||||
from enum import Enum, IntEnum
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.template.defaultfilters import slugify
 | 
			
		||||
@ -19,7 +20,7 @@ import aircox.programs.settings as settings
 | 
			
		||||
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
 | 
			
		||||
    if date_only is True
 | 
			
		||||
@ -32,20 +33,20 @@ def date_or_default (date, date_only = False):
 | 
			
		||||
    return date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Nameable (models.Model):
 | 
			
		||||
class Nameable(models.Model):
 | 
			
		||||
    name = models.CharField (
 | 
			
		||||
        _('name'),
 | 
			
		||||
        max_length = 128,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def slug (self):
 | 
			
		||||
    def slug(self):
 | 
			
		||||
        """
 | 
			
		||||
        Slug based on the name. We replace '-' by '_'
 | 
			
		||||
        """
 | 
			
		||||
        return slugify(self.name).replace('-', '_')
 | 
			
		||||
 | 
			
		||||
    def __str__ (self):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        #if self.pk:
 | 
			
		||||
        #    return '#{} {}'.format(self.pk, self.name)
 | 
			
		||||
        return '{}'.format(self.name)
 | 
			
		||||
@ -54,7 +55,7 @@ class Nameable (models.Model):
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Track (Nameable):
 | 
			
		||||
class Track(Nameable):
 | 
			
		||||
    """
 | 
			
		||||
    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.
 | 
			
		||||
@ -87,7 +88,7 @@ class Track (Nameable):
 | 
			
		||||
        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
 | 
			
		||||
    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 related program info.
 | 
			
		||||
    """
 | 
			
		||||
    Type = {
 | 
			
		||||
        'other': 0x00,
 | 
			
		||||
        'archive': 0x01,
 | 
			
		||||
        'excerpt': 0x02,
 | 
			
		||||
    }
 | 
			
		||||
    for key, value in Type.items():
 | 
			
		||||
        ugettext_lazy(key)
 | 
			
		||||
    class Type(IntEnum):
 | 
			
		||||
        other = 0x00,
 | 
			
		||||
        archive = 0x01,
 | 
			
		||||
        excerpt = 0x02,
 | 
			
		||||
 | 
			
		||||
    type = models.SmallIntegerField(
 | 
			
		||||
        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
 | 
			
		||||
    )
 | 
			
		||||
    path = models.FilePathField(
 | 
			
		||||
@ -148,7 +146,7 @@ class Sound (Nameable):
 | 
			
		||||
        help_text = _('sound\'s is accessible through the website')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_mtime (self):
 | 
			
		||||
    def get_mtime(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get the last modification date from file
 | 
			
		||||
        """
 | 
			
		||||
@ -158,13 +156,13 @@ class Sound (Nameable):
 | 
			
		||||
        mtime = mtime.replace(microsecond = 0)
 | 
			
		||||
        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 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
 | 
			
		||||
        needed (do not save). Return True if there was changes.
 | 
			
		||||
@ -188,7 +186,7 @@ class Sound (Nameable):
 | 
			
		||||
            return True
 | 
			
		||||
        return old_removed != self.removed
 | 
			
		||||
 | 
			
		||||
    def save (self, check = True, *args, **kwargs):
 | 
			
		||||
    def save(self, check = True, *args, **kwargs):
 | 
			
		||||
        if check:
 | 
			
		||||
            self.check_on_file()
 | 
			
		||||
 | 
			
		||||
@ -198,7 +196,7 @@ class Sound (Nameable):
 | 
			
		||||
            self.name = self.name.replace('_', ' ')
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__ (self):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '/'.join(self.path.split('/')[-3:])
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@ -206,7 +204,7 @@ class Sound (Nameable):
 | 
			
		||||
        verbose_name_plural = _('Sounds')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Stream (models.Model):
 | 
			
		||||
class Stream(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    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,
 | 
			
		||||
@ -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
 | 
			
		||||
    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',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        """
 | 
			
		||||
@ -292,7 +290,7 @@ class Schedule (models.Model):
 | 
			
		||||
            return self.date.time() == date.time() if check_time else True
 | 
			
		||||
        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
 | 
			
		||||
        otherwise.
 | 
			
		||||
@ -313,13 +311,13 @@ class Schedule (models.Model):
 | 
			
		||||
            return self.frequency == 0b1111
 | 
			
		||||
        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
 | 
			
		||||
        """
 | 
			
		||||
        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)
 | 
			
		||||
        """
 | 
			
		||||
@ -361,7 +359,7 @@ class Schedule (models.Model):
 | 
			
		||||
                date += tz.timedelta(days = 7)
 | 
			
		||||
        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
 | 
			
		||||
        can be not in the database.
 | 
			
		||||
@ -394,19 +392,19 @@ class Schedule (models.Model):
 | 
			
		||||
                              else None
 | 
			
		||||
            diffusions.append(Diffusion(
 | 
			
		||||
                                 program = self.program,
 | 
			
		||||
                                 type = Diffusion.Type['unconfirmed'],
 | 
			
		||||
                                 type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                 initial = first_diffusion if self.initial else None,
 | 
			
		||||
                                 start = date,
 | 
			
		||||
                                 end = date + duration,
 | 
			
		||||
                             ))
 | 
			
		||||
        return diffusions
 | 
			
		||||
 | 
			
		||||
    def __str__ (self):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return ' | '.join([ '#' + str(self.id), self.program.name,
 | 
			
		||||
                            self.get_frequency_display(),
 | 
			
		||||
                            self.date.strftime('%a %H:%M') ])
 | 
			
		||||
 | 
			
		||||
    def save (self, *args, **kwargs):
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.initial:
 | 
			
		||||
            self.program = self.initial.program
 | 
			
		||||
            self.duration = self.initial.duration
 | 
			
		||||
@ -418,7 +416,7 @@ class Schedule (models.Model):
 | 
			
		||||
        verbose_name_plural = _('Schedules')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Station (Nameable):
 | 
			
		||||
class Station(Nameable):
 | 
			
		||||
    """
 | 
			
		||||
    A Station regroup one or more programs (stream and normal), and is the top
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
@ -473,14 +471,14 @@ class Program (Nameable):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def path (self):
 | 
			
		||||
    def path(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return the path to the programs directory
 | 
			
		||||
        """
 | 
			
		||||
        return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
 | 
			
		||||
                            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
 | 
			
		||||
        if the dir (or subdir) exists.
 | 
			
		||||
@ -490,7 +488,7 @@ class Program (Nameable):
 | 
			
		||||
        os.makedirs(path, exist_ok = True)
 | 
			
		||||
        return os.path.exists(path)
 | 
			
		||||
 | 
			
		||||
    def find_schedule (self, date):
 | 
			
		||||
    def find_schedule(self, date):
 | 
			
		||||
        """
 | 
			
		||||
        Return the first schedule that matches a given date.
 | 
			
		||||
        """
 | 
			
		||||
@ -499,12 +497,12 @@ class Program (Nameable):
 | 
			
		||||
            if schedule.match(date, check_time = False):
 | 
			
		||||
                return schedule
 | 
			
		||||
 | 
			
		||||
    def __init__ (self, *kargs, **kwargs):
 | 
			
		||||
    def __init__(self, *kargs, **kwargs):
 | 
			
		||||
        super().__init__(*kargs, **kwargs)
 | 
			
		||||
        if self.name:
 | 
			
		||||
            self.__original_path = self.path
 | 
			
		||||
 | 
			
		||||
    def save (self, *kargs, **kwargs):
 | 
			
		||||
    def save(self, *kargs, **kwargs):
 | 
			
		||||
        super().save(*kargs, **kwargs)
 | 
			
		||||
        if hasattr(self, '__original_path') and \
 | 
			
		||||
                self.__original_path != self.path and \
 | 
			
		||||
@ -520,7 +518,7 @@ class Program (Nameable):
 | 
			
		||||
                sound.save()
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Diffusion (models.Model):
 | 
			
		||||
class Diffusion(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    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
 | 
			
		||||
@ -555,13 +553,10 @@ class Diffusion (models.Model):
 | 
			
		||||
    - cancel: the diffusion has been canceled
 | 
			
		||||
    - stop: the diffusion has been manually stopped
 | 
			
		||||
    """
 | 
			
		||||
    Type = {
 | 
			
		||||
        'normal':       0x00,   # diffusion is planified
 | 
			
		||||
        'unconfirmed':  0x01,   # scheduled by the generator but not confirmed for diffusion
 | 
			
		||||
        'cancel':       0x02,   # diffusion canceled
 | 
			
		||||
    }
 | 
			
		||||
    for key, value in Type.items():
 | 
			
		||||
        ugettext_lazy(key)
 | 
			
		||||
    class Type(IntEnum):
 | 
			
		||||
        normal = 0x00
 | 
			
		||||
        unconfirmed = 0x01
 | 
			
		||||
        canceled = 0x02
 | 
			
		||||
 | 
			
		||||
    # common
 | 
			
		||||
    program = models.ForeignKey (
 | 
			
		||||
@ -576,7 +571,7 @@ class Diffusion (models.Model):
 | 
			
		||||
    # specific
 | 
			
		||||
    type = models.SmallIntegerField(
 | 
			
		||||
        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 (
 | 
			
		||||
        'self',
 | 
			
		||||
@ -588,11 +583,11 @@ class Diffusion (models.Model):
 | 
			
		||||
    end = models.DateTimeField( _('end of the diffusion') )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def duration (self):
 | 
			
		||||
    def duration(self):
 | 
			
		||||
        return self.end - self.start
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def date (self):
 | 
			
		||||
    def date(self):
 | 
			
		||||
        return self.start
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -604,29 +599,30 @@ class Diffusion (models.Model):
 | 
			
		||||
        playlist.sort()
 | 
			
		||||
        return playlist
 | 
			
		||||
 | 
			
		||||
    def archives_duration (self):
 | 
			
		||||
    def archives_duration(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get total duration of the archives. May differ from the schedule
 | 
			
		||||
        duration.
 | 
			
		||||
        """
 | 
			
		||||
        sounds = self.initial.sounds if self.initial else self.sounds
 | 
			
		||||
        r = [ sound.duration
 | 
			
		||||
                for sound in sounds.filter(type = Sound.Type['archive'])
 | 
			
		||||
                for sound in sounds.filter(type = Sound.Type.archive)
 | 
			
		||||
                if sound.duration ]
 | 
			
		||||
        return utils.time_sum(r)
 | 
			
		||||
 | 
			
		||||
    def get_archives (self):
 | 
			
		||||
    def get_archives(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return an ordered list of archives sounds for the given episode.
 | 
			
		||||
        """
 | 
			
		||||
        sounds = self.initial.sounds if self.initial else self.sounds
 | 
			
		||||
        r = [ sound for sound in sounds.all().order_by('path')
 | 
			
		||||
              if sound.type == Sound.Type['archive'] ]
 | 
			
		||||
              if sound.type == Sound.Type.archive ]
 | 
			
		||||
        return r
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get (cl, station = None, date = None,
 | 
			
		||||
    def get(cl, station = None, date = None,
 | 
			
		||||
             now = False, next = False, prev = False,
 | 
			
		||||
             queryset = None,
 | 
			
		||||
             **filter_args):
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        - 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
 | 
			
		||||
        """
 | 
			
		||||
        #FIXME: conflicts? ( + calling functions)
 | 
			
		||||
@ -669,7 +667,7 @@ class Diffusion (models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        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.
 | 
			
		||||
        """
 | 
			
		||||
@ -681,7 +679,7 @@ class Diffusion (models.Model):
 | 
			
		||||
        )
 | 
			
		||||
        return r
 | 
			
		||||
 | 
			
		||||
    def save (self, *args, **kwargs):
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.initial:
 | 
			
		||||
            # force link to the top initial diffusion
 | 
			
		||||
            if self.initial.initial:
 | 
			
		||||
@ -689,7 +687,7 @@ class Diffusion (models.Model):
 | 
			
		||||
            self.program = self.initial.program
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__ (self):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{self.program.name} {date} #{self.pk}'.format(
 | 
			
		||||
            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')),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
class Log (models.Model):
 | 
			
		||||
class Log(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    Log a played sound start and stop, or a single message
 | 
			
		||||
    """
 | 
			
		||||
@ -733,14 +731,14 @@ class Log (models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @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 cl.objects.filter(related_type__pk =
 | 
			
		||||
                                    ContentType.objects.get_for_model(model).id)
 | 
			
		||||
 | 
			
		||||
    def print (self):
 | 
			
		||||
    def print(self):
 | 
			
		||||
        logger.info('log #%s: %s%s',
 | 
			
		||||
            str(self),
 | 
			
		||||
            self.comment or '',
 | 
			
		||||
@ -748,7 +746,7 @@ class Log (models.Model):
 | 
			
		||||
                if self.related_object else ''
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __str__ (self):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '#{} ({}, {})'.format(
 | 
			
		||||
                self.id, self.date.strftime('%Y-%m-%d %H:%M'), self.source
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ class Programs (TestCase):
 | 
			
		||||
    def setUp (self):
 | 
			
		||||
        stream = Stream.objects.get_or_create(
 | 
			
		||||
            name = 'diffusions',
 | 
			
		||||
            defaults = { 'type': Stream.Type['schedule'] }
 | 
			
		||||
            defaults = { 'type': Stream.Type.schedule }
 | 
			
		||||
        )[0]
 | 
			
		||||
        Program.objects.create(name = 'source', stream = stream)
 | 
			
		||||
        Program.objects.create(name = 'microouvert', stream = stream)
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user