From c3ae0e012cfe64347cda18936f897d151e65ef29 Mon Sep 17 00:00:00 2001 From: bkfox Date: Sun, 12 Jun 2016 21:34:31 +0200 Subject: [PATCH] remove Station model (to much trouble for few advantages); start new player; rename Post.detail_url to Post.url, same for ListItem; move Article into website app; add website.Sound post; work on lists;... --- cms/README.md | 3 + cms/admin.py | 1 - cms/models.py | 24 ++---- cms/sections.py | 25 ++++-- cms/templates/admin/base_site.html | 51 ------------ cms/templates/aircox/cms/list.html | 6 +- cms/templatetags/aircox_cms.py | 2 +- cms/views.py | 3 +- cms/website.py | 5 +- liquidsoap/admin.py | 2 +- liquidsoap/management/commands/liquidsoap.py | 27 ++----- liquidsoap/models.py | 19 ++--- liquidsoap/utils.py | 32 ++------ programs/admin.py | 15 ++-- programs/models.py | 40 +--------- website/admin.py | 2 +- website/models.py | 81 +++++++++++++++++++- website/sections.py | 38 ++++++++- 18 files changed, 180 insertions(+), 196 deletions(-) delete mode 100644 cms/templates/admin/base_site.html diff --git a/cms/README.md b/cms/README.md index 0a6751e..62e8955 100644 --- a/cms/README.md +++ b/cms/README.md @@ -50,6 +50,9 @@ class MyModelPost(RelatedPost): Note: it is possible to assign a function as a bounded value; in such case, the function will be called using arguments **(post, related_object)**. +At rendering, the property *info* can be retrieved from the Post. It is however +not a field. + ## Routes Routes are used to generate the URLs of the website. We provide some of the common routes: for the detail view of course, but also to select all posts or diff --git a/cms/admin.py b/cms/admin.py index 2cc6419..1a6a9fe 100644 --- a/cms/admin.py +++ b/cms/admin.py @@ -91,7 +91,6 @@ def inject_inline(model, inline, prepend = False): registry[model].inlines = inlines -admin.site.register(models.Article, PostAdmin) admin.site.register(models.Comment, CommentAdmin) diff --git a/cms/models.py b/cms/models.py index 41696b3..5436bc6 100644 --- a/cms/models.py +++ b/cms/models.py @@ -166,7 +166,10 @@ class Post (models.Model, Routable): ) return qs - def detail_url(self): + def url(self): + """ + Return an url to the post detail view. + """ return self.route_url( routes.DetailRoute, pk = self.pk, slug = slugify(self.title) @@ -209,24 +212,6 @@ class Post (models.Model, Routable): abstract = True -class Article (Post): - """ - Represent an article or a static page on the website. - """ - static_page = models.BooleanField( - _('static page'), - default = False, - ) - focus = models.BooleanField( - _('article is focus'), - default = False, - ) - - class Meta: - verbose_name = _('Article') - verbose_name_plural = _('Articles') - - class RelatedPostBase (models.base.ModelBase): """ Metaclass for RelatedPost children. @@ -356,6 +341,7 @@ class RelatedPost (Post, metaclass = RelatedPostBase): class Meta: abstract = True + # FIXME: declare a binding only for init class Relation: """ Relation descriptor used to generate and manage the related object. diff --git a/cms/sections.py b/cms/sections.py index 699347e..bd44526 100644 --- a/cms/sections.py +++ b/cms/sections.py @@ -203,7 +203,7 @@ class ListItem: date = None image = None info = None - detail_url = None + url = None css_class = None attrs = None @@ -222,8 +222,8 @@ class ListItem: continue if hasattr(post, i) and not getattr(self, i): setattr(self, i, getattr(post, i)) - if not self.detail_url and hasattr(post, 'detail_url'): - self.detail_url = post.detail_url() + if not self.url and hasattr(post, 'url'): + self.url = post.url() class List(Section): @@ -243,6 +243,7 @@ class List(Section): object_list = None url = None message_empty = _('nothing') + paginate_by = 4 fields = [ 'date', 'time', 'image', 'title', 'content', 'info' ] image_size = '64x64' @@ -264,20 +265,32 @@ class List(Section): def get_object_list(self): return self.object_list - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_context_data(self, request, object=None, *args, **kwargs): + if request: self.request = request + if object: self.object = object + if kwargs: self.kwargs = kwargs object_list = self.object_list or self.get_object_list() if not object_list and not self.message_empty: return + self.object_list = object_list + context = super().get_context_data(request, object, *args, **kwargs) context.update({ 'base_template': 'aircox/cms/section.html', 'list': self, - 'object_list': object_list, + 'object_list': object_list[:self.paginate_by] + if object_list and self.paginate_by else + object_list, }) return context + def need_url(self): + """ + Return True if there should be a pagination url + """ + return self.paginate_by and self.paginate_by < len(self.object_list) + class Comments(List): """ diff --git a/cms/templates/admin/base_site.html b/cms/templates/admin/base_site.html deleted file mode 100644 index 1e8b0e8..0000000 --- a/cms/templates/admin/base_site.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "admin/base.html" %} - -{% block extrahead %} -{% include 'autocomplete_light/static.html' %} - - -{% endblock %} - diff --git a/cms/templates/aircox/cms/list.html b/cms/templates/aircox/cms/list.html index 257808b..c60f5f1 100644 --- a/cms/templates/aircox/cms/list.html +++ b/cms/templates/aircox/cms/list.html @@ -13,8 +13,8 @@ {% for k, v in item.attrs.items %} {{ k }} = "{{ v|addslashes }}" {% endfor %} > - {% if item.detail_url %} - + {% if item.url %} + {% endif %} {% if 'image' in list.fields and item.image %} @@ -65,7 +65,7 @@ - {% if item.detail_url %} + {% if item.url %} {% endif %} {% empty %} diff --git a/cms/templatetags/aircox_cms.py b/cms/templatetags/aircox_cms.py index 2860c61..9e250e6 100644 --- a/cms/templatetags/aircox_cms.py +++ b/cms/templatetags/aircox_cms.py @@ -16,7 +16,7 @@ def threads(post, sep = '/'): posts.insert(0, post) return sep.join([ - '{}'.format(post.detail_url(), post.title) + '{}'.format(post.url(), post.title) for post in posts if post.published ]) diff --git a/cms/views.py b/cms/views.py index 425c725..b6db8d6 100644 --- a/cms/views.py +++ b/cms/views.py @@ -118,10 +118,11 @@ class PostListView(PostBaseView, ListView): if not self.list: self.list = sections.List( truncate = 32, + paginate_by = 0, fields = ['date', 'time', 'image', 'title', 'content'], ) else: - self.list = self.list() + self.list = self.list(paginate_by = 0) self.template_name = self.list.template_name self.css_class = self.list.css_class diff --git a/cms/website.py b/cms/website.py index 8802689..982be35 100644 --- a/cms/website.py +++ b/cms/website.py @@ -51,8 +51,7 @@ class Website: self.set_menu(menu) if self.comments_routes: - self.register_comments_routes() - + self.register_comments() def name_of_model(self, model): """ @@ -62,7 +61,7 @@ class Website: if model is _model: return name - def register_comments_routes(self): + def register_comments(self): """ Register routes for comments, for the moment, only ThreadRoute diff --git a/liquidsoap/admin.py b/liquidsoap/admin.py index f82c78b..ed26b1c 100644 --- a/liquidsoap/admin.py +++ b/liquidsoap/admin.py @@ -3,6 +3,6 @@ import aircox.liquidsoap.models as models @admin.register(models.Output) class OutputAdmin (admin.ModelAdmin): - list_display = ('id', 'type', 'station') + list_display = ('id', 'type') diff --git a/liquidsoap/management/commands/liquidsoap.py b/liquidsoap/management/commands/liquidsoap.py index c68ef7d..17a9a4b 100644 --- a/liquidsoap/management/commands/liquidsoap.py +++ b/liquidsoap/management/commands/liquidsoap.py @@ -96,7 +96,7 @@ class Monitor: # - preload next diffusion's tracks args = {'start__gt': prev_diff.start } if prev_diff else {} next_diff = programs.Diffusion \ - .get(controller.station, now, now = True, + .get(now, now = True, type = programs.Diffusion.Type.normal, sounds__isnull = False, **args) \ @@ -194,30 +194,19 @@ class Command (BaseCommand): help='write configuration and playlist' ) - group = parser.add_argument_group('selector') group.add_argument( - '-s', '--station', type=int, action='append', - help='select station(s) with this id' - ) - group.add_argument( - '-a', '--all', action='store_true', - help='select all stations' + '-s', '--station', type=str, + default = 'aircox', + help='use this name as station name (default is "aircox")' ) def handle (self, *args, **options): - # selector - stations = [] - if options.get('all'): - stations = programs.Station.objects.filter(active = True) - elif options.get('station'): - stations = programs.Station.objects.filter( - id__in = options.get('station') - ) - run = options.get('run') monitor = options.get('on_air') or options.get('monitor') - self.controllers = [ utils.Controller(station, connector = monitor) - for station in stations ] + self.controller = utils.Controller( + station = options.get('station'), + connector = monitor + ) # actions if options.get('write') or run: diff --git a/liquidsoap/models.py b/liquidsoap/models.py index 12c5775..6764c84 100644 --- a/liquidsoap/models.py +++ b/liquidsoap/models.py @@ -1,24 +1,19 @@ +from enum import Enum, IntEnum + from django.db import models from django.utils.translation import ugettext as _, ugettext_lazy -import aircox.programs.models as programs - class Output (models.Model): # Note: we don't translate the names since it is project names. - Type = { - 'jack': 0x00, - 'alsa': 0x01, - 'icecast': 0x02, - } + class Type(IntEnum): + jack = 0x00 + alsa = 0x01 + icecast = 0x02 - station = models.ForeignKey( - programs.Station, - verbose_name = _('station'), - ) type = models.SmallIntegerField( _('output type'), - choices = [ (y, x) for x,y in Type.items() ], + choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ], blank = True, null = True ) settings = models.TextField( diff --git a/liquidsoap/utils.py b/liquidsoap/utils.py index 447693d..88b7141 100644 --- a/liquidsoap/utils.py +++ b/liquidsoap/utils.py @@ -295,12 +295,11 @@ class Controller: files dir. """ self.id = station.slug - self.name = station.name - self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, station.slug) + self.name = station + self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, + slugify(station)) - self.station = station - self.station.controller = self - self.outputs = models.Output.objects.filter(station = station) + self.outputs = models.Output.objects.all() self.connector = connector and Connector(self.socket_path) @@ -310,8 +309,7 @@ class Controller: source.id : source for source in [ Source(self, program) - for program in programs.Program.objects.filter(station = station, - active = True) + for program in programs.Program.objects.filter(active = True) if program.stream_set.count() ] } @@ -370,23 +368,3 @@ class Controller: file.write(data) -class Monitor: - """ - Monitor multiple controllers. - """ - controllers = None - - def __init__(self): - self.controllers = { - controller.id : controller - for controller in [ - Controller(station, True) - for station in programs.Station.objects.filter(active = True) - ] - } - - def update(self): - for controller in self.controllers.values(): - controller.update() - - diff --git a/programs/admin.py b/programs/admin.py index b625bf7..30705db 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -24,6 +24,8 @@ class StreamInline(admin.TabularInline): model = Stream extra = 1 +class SoundDiffInline(admin.TabularInline): + model = Diffusion.sounds.through # from suit.admin import SortableTabularInline, SortableModelAdmin #class TrackInline(SortableTabularInline): @@ -45,11 +47,11 @@ class NameableAdmin(admin.ModelAdmin): @admin.register(Sound) class SoundAdmin(NameableAdmin): fields = None - list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed', 'public'] + list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed'] fieldsets = [ (None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ), (None, { 'fields': ['embed', 'duration', 'mtime'] }), - (None, { 'fields': ['removed', 'good_quality', 'public' ] } ) + (None, { 'fields': ['removed', 'good_quality' ] } ) ] readonly_fields = ('path', 'duration',) @@ -59,10 +61,6 @@ class StreamAdmin(admin.ModelAdmin): list_display = ('id', 'program', 'delay', 'begin', 'end') -@admin.register(Station) -class StationAdmin(NameableAdmin): - fields = NameableAdmin.fields + [ 'active', 'public', 'fallback' ] - @admin.register(Program) class ProgramAdmin(NameableAdmin): def schedule(self, obj): @@ -113,8 +111,9 @@ class DiffusionAdmin(admin.ModelAdmin): list_editable = ('type',) ordering = ('-start', 'id') - fields = ['type', 'start', 'end', 'initial', 'program', 'sounds'] - inlines = [ DiffusionInline ] + fields = ['type', 'start', 'end', 'initial', 'program'] + inlines = [ DiffusionInline, SoundDiffInline ] + exclude = ('sounds',) def get_form(self, request, obj=None, **kwargs): diff --git a/programs/models.py b/programs/models.py index 39063c5..1ae6a10 100755 --- a/programs/models.py +++ b/programs/models.py @@ -141,11 +141,6 @@ class Sound(Nameable): default = False, help_text = _('sound\'s quality is okay') ) - public = models.BooleanField( - _('public'), - default = False, - help_text = _('sound\'s is accessible through the website') - ) def get_mtime(self): """ @@ -417,32 +412,6 @@ class Schedule(models.Model): verbose_name_plural = _('Schedules') -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. - """ - active = models.BooleanField( - _('active'), - default = True, - help_text = _('this station is active') - ) - public = models.BooleanField( - _('public'), - default = True, - help_text = _('information are available to the public'), - ) - fallback = models.FilePathField( - _('fallback song'), - match = r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) \ - .replace('.', r'\.') + ')$', - recursive = True, - blank = True, null = True, - help_text = _('use this song file if there is a problem and nothing is ' - 'played') - ) - - class Program(Nameable): """ A Program can either be a Streamed or a Scheduled program. @@ -456,10 +425,6 @@ class Program(Nameable): Renaming a Program rename the corresponding directory to matches the new name if it does not exists. """ - station = models.ForeignKey( - Station, - verbose_name = _('station') - ) active = models.BooleanField( _('active'), default = True, @@ -621,7 +586,7 @@ class Diffusion(models.Model): return r @classmethod - def get(cl, station = None, date = None, + def get(cl, date = None, now = False, next = False, prev = False, queryset = None, **filter_args): @@ -637,9 +602,6 @@ class Diffusion(models.Model): """ #FIXME: conflicts? ( + calling functions) date = date_or_default(date) - if station: - filter_args['program__station'] = station - if queryset is None: queryset = cl.objects diff --git a/website/admin.py b/website/admin.py index bcc8819..5bdfce9 100644 --- a/website/admin.py +++ b/website/admin.py @@ -14,7 +14,7 @@ class TrackInline(SortableTabularInline): sortable = 'position' extra = 10 - +admin.site.register(models.Article, cms.PostAdmin) admin.site.register(models.Program, cms.RelatedPostAdmin) admin.site.register(models.Diffusion, cms.RelatedPostAdmin) diff --git a/website/models.py b/website/models.py index 6a28506..66a8aad 100644 --- a/website/models.py +++ b/website/models.py @@ -1,11 +1,38 @@ +import os +import logging + +logger = logging.getLogger('aircox') + from django.db import models from django.utils.translation import ugettext as _, ugettext_lazy -from aircox.cms.models import RelatedPost, Article +from aircox.cms.models import Post, RelatedPost import aircox.programs.models as programs + +class Article (Post): + """ + Represent an article or a static page on the website. + """ + static_page = models.BooleanField( + _('static page'), + default = False, + ) + focus = models.BooleanField( + _('article is focus'), + default = False, + ) + + class Meta: + verbose_name = _('Article') + verbose_name_plural = _('Articles') + + class Program (RelatedPost): - url = models.URLField(_('website'), blank=True, null=True) + website = models.URLField( + _('website'), + blank=True, null=True + ) # rss = models.URLField() email = models.EmailField( _('email'), blank=True, null=True, @@ -20,6 +47,7 @@ class Program (RelatedPost): rel_to_post = True auto_create = True + class Diffusion (RelatedPost): class Relation: model = programs.Diffusion @@ -59,3 +87,52 @@ class Diffusion (RelatedPost): return _('rerun of %(day)s') % { 'day': self.related.initial.start.strftime('%A %d/%m') } + + +class Sound (RelatedPost): + """ + Publication concerning sound. In order to manage access of sound + files in the filesystem, we use permissions -- it is up to the + user to work select the correct groups and permissions. + """ + embed = models.TextField( + _('embedding code'), + blank=True, null=True, + help_text = _('HTML code used to embed a sound from an external ' + 'plateform'), + ) + """ + Embedding code if the file has been published on an external + plateform. + """ + + auto_chmod = True + """ + change file permission depending on the "published" attribute. + """ + chmod_flags = (750, 700) + """ + chmod bit flags, for (not_published, published) + """ + class Relation: + model = programs.Sound + bindings = { + 'date': 'mtime', + } + rel_to_post = True + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.auto_chmod and not self.related.removed and \ + os.path.exists(self.related.path): + try: + os.chmod(self.related.path, + self.chmod_flags[self.published]) + except PermissionError as err: + logger.error( + 'cannot set permission {} to file {}: {}'.format( + self.chmod_flags[self.published], + self.related.path, err + ) + ) + diff --git a/website/sections.py b/website/sections.py index 2c4a010..4341f67 100644 --- a/website/sections.py +++ b/website/sections.py @@ -9,6 +9,28 @@ import aircox.cms.sections as sections import aircox.website.models as models +class Player(sections.Section): + """ + Display a player that is cool. + """ + template_name = 'aircox/website/player.html' + live_streams = [] + """ + A ListItem objects that display a list of available streams. + """ + #default_sounds + + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + context.update({ + 'live_streams': self.live_streams + }) + return context + + + class Diffusions(sections.List): """ Section that print diffusions. When rendering, if there is no post yet @@ -19,14 +41,19 @@ class Diffusions(sections.List): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.__dict__.update(kwargs) def get_diffs(self, **filter_args): qs = programs.Diffusion.objects.filter( type = programs.Diffusion.Type.normal ) if self.object: - qs = qs.filter(program = self.object.related) + object = self.object.related + if type(object) == programs.Program: + qs = qs.filter(program = object) + elif type(object) == programs.Diffusion: + if object.initial: + object = object.initial + qs = qs.filter(initial = object) | qs.filter(pk = object.pk) if filter_args: qs = qs.filter(**filter_args).order_by('start') @@ -72,6 +99,9 @@ class Diffusions(sections.List): @property def url(self): + if not self.need_url(): + return + if self.object: return models.Diffusion.route_url(routes.ThreadRoute, pk = self.object.id, @@ -114,6 +144,10 @@ class Playlist(sections.List): for track in tracks ] +class Sounds(sections.List): + pass + + class Schedule(Diffusions): """ Render a list of diffusions in the form of a schedule