From 6ed33c34a9989a5bef1f695aa954586b802175d7 Mon Sep 17 00:00:00 2001 From: bkfox Date: Tue, 23 Jul 2019 23:04:35 +0200 Subject: [PATCH] bkp before branching --- aircox/admin/base.py | 23 ++- aircox/admin/sound.py | 6 +- aircox/models.py | 135 +++++++++--------- aircox_web/admin.py | 32 +---- aircox_web/assets/styles.scss | 32 +++-- aircox_web/converters.py | 25 ++-- aircox_web/models.py | 114 ++++----------- aircox_web/templates/aircox_web/article.html | 17 --- .../templates/aircox_web/diffusion_item.html | 55 ++++--- .../templates/aircox_web/diffusions.html | 46 ++++-- aircox_web/templates/aircox_web/logs.html | 2 + aircox_web/templates/aircox_web/page.html | 87 +++++------ aircox_web/templates/aircox_web/program.html | 25 +--- .../templates/aircox_web/timetable.html | 6 + aircox_web/templatetags/aircox_web.py | 5 +- aircox_web/urls.py | 17 ++- aircox_web/views.py | 95 +++++++----- 17 files changed, 330 insertions(+), 392 deletions(-) delete mode 100644 aircox_web/templates/aircox_web/article.html diff --git a/aircox/admin/base.py b/aircox/admin/base.py index 2a05917..f5680eb 100644 --- a/aircox/admin/base.py +++ b/aircox/admin/base.py @@ -18,13 +18,6 @@ class StreamInline(admin.TabularInline): model = Stream extra = 1 -class NameableAdmin(admin.ModelAdmin): - fields = [ 'name' ] - - list_display = ['id', 'name'] - list_filter = [] - search_fields = ['name',] - @admin.register(Stream) class StreamAdmin(admin.ModelAdmin): @@ -32,15 +25,19 @@ class StreamAdmin(admin.ModelAdmin): @admin.register(Program) -class ProgramAdmin(NameableAdmin): +class ProgramAdmin(admin.ModelAdmin): def schedule(self, obj): - return Schedule.objects.filter(program = obj).count() > 0 + return Schedule.objects.filter(program=obj).count() > 0 + schedule.boolean = True schedule.short_description = _("Schedule") - list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station') - fields = NameableAdmin.fields + [ 'active', 'station','sync' ] - inlines = [ ScheduleInline, StreamInline ] + list_display = ('name', 'id', 'active', 'schedule', 'sync', 'station') + fields = ['name', 'slug', 'active', 'station', 'sync'] + prepopulated_fields = {'slug': ('name',)} + search_fields = ['name'] + + inlines = [ScheduleInline, StreamInline] @admin.register(Schedule) @@ -64,7 +61,6 @@ class ScheduleAdmin(admin.ModelAdmin): 'time', 'duration', 'timezone', 'rerun'] list_editable = ['time', 'timezone', 'duration'] - def get_readonly_fields(self, request, obj=None): if obj: return ['program', 'date', 'frequency'] @@ -79,6 +75,7 @@ class PortInline(admin.StackedInline): @admin.register(Station) class StationAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ('name',)} inlines = [PortInline] diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py index 06e1a9f..2cdca5c 100644 --- a/aircox/admin/sound.py +++ b/aircox/admin/sound.py @@ -2,19 +2,17 @@ from django.contrib import admin from django.utils.translation import ugettext as _, ugettext_lazy from aircox.models import Sound -from .base import NameableAdmin from .playlist import TracksInline @admin.register(Sound) -class SoundAdmin(NameableAdmin): +class SoundAdmin(admin.ModelAdmin): fields = None list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime', 'public', 'good_quality', 'path'] list_filter = ('program', 'type', 'good_quality', 'public') fieldsets = [ - (None, {'fields': NameableAdmin.fields + - ['path', 'type', 'program', 'diffusion']}), + (None, {'fields': ['name', 'path', 'type', 'program', 'diffusion']}), (None, {'fields': ['embed', 'duration', 'public', 'mtime']}), (None, {'fields': ['good_quality']}) ] diff --git a/aircox/models.py b/aircox/models.py index cb7114e..7961fc0 100755 --- a/aircox/models.py +++ b/aircox/models.py @@ -11,9 +11,9 @@ from django.contrib.contenttypes.fields import (GenericForeignKey, GenericRelation) from django.contrib.contenttypes.models import ContentType from django.db import models -from django.db.models import Q +from django.db.models import F, Q +from django.db.models.functions import Concat, Substr from django.db.transaction import atomic -from django.template.defaultfilters import slugify from django.utils import timezone as tz from django.utils.functional import cached_property from django.utils.html import strip_tags @@ -26,28 +26,6 @@ from taggit.managers import TaggableManager logger = logging.getLogger('aircox.core') -class Nameable(models.Model): - name = models.CharField( - _('name'), - max_length=128, - ) - - class Meta: - abstract = True - - @property - def slug(self): - """ - Slug based on the name. We replace '-' by '_' - """ - return slugify(self.name).replace('-', '_') - - def __str__(self): - # if self.pk: - # return '#{} {}'.format(self.pk, self.name) - return '{}'.format(self.name) - - # # Station related classes # @@ -67,7 +45,7 @@ def default_station(): return Station.objects.default() -class Station(Nameable): +class Station(models.Model): """ Represents a radio station, to which multiple programs are attached and that is used as the top object for everything. @@ -76,6 +54,8 @@ class Station(Nameable): Theses are set up when needed (at the first access to these elements) then cached. """ + name = models.CharField(_('name'), max_length=64) + slug = models.SlugField(_('slug'), max_length=64, unique=True) path = models.CharField( _('path'), help_text=_('path to the working directory'), @@ -199,6 +179,9 @@ class Station(Nameable): logs = logs[:count] return logs + def __str__(self): + return self.name + def save(self, make_sources=True, *args, **kwargs): if not self.path: self.path = os.path.join( @@ -223,7 +206,7 @@ class ProgramManager(models.Manager): return qs.filter(station=station, **kwargs) -class Program(Nameable): +class Program(models.Model): """ A Program can either be a Streamed or a Scheduled program. @@ -241,6 +224,8 @@ class Program(Nameable): verbose_name=_('station'), on_delete=models.CASCADE, ) + name = models.CharField(_('name'), max_length=64) + slug = models.SlugField(_('slug'), max_length=64, unique=True) active = models.BooleanField( _('active'), default=True, @@ -254,14 +239,10 @@ class Program(Nameable): objects = ProgramManager() - # TODO: use unique slug @property def path(self): - """ - Return the path to the programs directory - """ - return os.path.join(settings.AIRCOX_PROGRAMS_DIR, - self.slug + '_' + str(self.id)) + """ Return program's directory path """ + return os.path.join(settings.AIRCOX_PROGRAMS_DIR, self.slug) def ensure_dir(self, subdir=None): """ @@ -299,26 +280,8 @@ class Program(Nameable): def __init__(self, *kargs, **kwargs): super().__init__(*kargs, **kwargs) - if self.name: - self.__original_path = self.path - - def save(self, *kargs, **kwargs): - super().save(*kargs, **kwargs) - - if hasattr(self, '__original_path') and \ - self.__original_path != self.path and \ - os.path.exists(self.__original_path) and \ - not os.path.exists(self.path): - logger.info('program #%s\'s name changed to %s. Change dir name', - self.id, self.name) - shutil.move(self.__original_path, self.path) - - sounds = Sound.objects.filter( - path__startswith=self.__original_path) - - for sound in sounds: - sound.path.replace(self.__original_path, self.path) - sound.save() + if self.slug: + self.__initial_path = self.path @classmethod def get_from_path(cl, path): @@ -346,6 +309,23 @@ class Program(Nameable): def is_show(self): return self.schedule_set.count() != 0 + def __str__(self): + return self.name + + def save(self, *kargs, **kwargs): + super().save(*kargs, **kwargs) + + path_ = getattr(self, '__initial_path', None) + if path_ is not None and path_ != self.path and \ + os.path.exists(path_) and not os.path.exists(self.path): + logger.info('program #%s\'s dir changed to %s - update it.', + self.id, self.name) + + shutil.move(path_, self.path) + Sound.objects.filter(path__startswith=path_) \ + .update(path=Concat('path', Substr(F('path'), len(path_)))) + + class Stream(models.Model): """ @@ -427,15 +407,15 @@ class Schedule(models.Model): _('frequency'), choices=[(int(y), { 'ponctual': _('ponctual'), - 'first': _('first week of the month'), - 'second': _('second week of the month'), - 'third': _('third week of the month'), - 'fourth': _('fourth week of the month'), - 'last': _('last week of the month'), - 'first_and_third': _('first and third weeks of the month'), - 'second_and_fourth': _('second and fourth weeks of the month'), - 'every': _('every week'), - 'one_on_two': _('one week on two'), + 'first': _('1st {day} of the month'), + 'second': _('2nd {day} of the month'), + 'third': _('3rd {day} of the month'), + 'fourth': _('4th {day} of the month'), + 'last': _('last {day} of the month'), + 'first_and_third': _('1st and 3rd {day}s of the month'), + 'second_and_fourth': _('2nd and 4th {day}s of the month'), + 'every': _('{day}'), + 'one_on_two': _('one {day} on two'), }[x]) for x, y in Frequency.__members__.items()], ) initial = models.ForeignKey( @@ -454,11 +434,22 @@ class Schedule(models.Model): return pytz.timezone(self.timezone) - @property - def datetime(self): - """ Datetime for this schedule (timezone unaware) """ - import datetime - return datetime.datetime.combine(self.date, self.time) + @cached_property + def start(self): + """ Datetime of the start (timezone unaware) """ + return tz.datetime.combine(self.date, self.time) + + @cached_property + def end(self): + """ Datetime of the end """ + return self.start + utils.to_timedelta(self.duration) + + def get_frequency_verbose(self): + """ Return frequency formated for display """ + from django.template.defaultfilters import date + return self.get_frequency_display().format( + day=date(self.date, 'l') + ) # initial cached data __initial = None @@ -630,7 +621,7 @@ class Schedule(models.Model): delta = None if self.initial: - delta = self.datetime - self.initial.datetime + delta = self.start - self.initial.start # FIXME: daylight saving bug: delta misses an hour when diffusion and # rerun are not on the same daylight-saving timezone @@ -916,9 +907,12 @@ class Diffusion(models.Model): self.check_conflicts() def __str__(self): - return '{self.program.name} {date} #{self.pk}'.format( - self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z') + str_ = '{self.program.name} {date}'.format( + self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z'), ) + if self.initial: + str_ += ' ({})'.format(_('rerun')) + return str_ class Meta: verbose_name = _('Diffusion') @@ -928,7 +922,7 @@ class Diffusion(models.Model): ) -class Sound(Nameable): +class Sound(models.Model): """ A Sound is the representation of a sound file that can be either an excerpt or a complete archive of the related diffusion. @@ -939,6 +933,7 @@ class Sound(Nameable): excerpt = 0x02, removed = 0x03, + name = models.CharField(_('name'), max_length=64) program = models.ForeignKey( Program, verbose_name=_('program'), diff --git a/aircox_web/admin.py b/aircox_web/admin.py index 5beceb7..b8108b3 100644 --- a/aircox_web/admin.py +++ b/aircox_web/admin.py @@ -4,8 +4,6 @@ from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from content_editor.admin import ContentEditor, ContentEditorInline -from feincms3 import plugins -from feincms3.admin import TreeAdmin from aircox import models as aircox from . import models @@ -38,39 +36,21 @@ class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline): view_obj.save() -@admin.register(models.Page) -class PageAdmin(admin.ModelAdmin): - list_display = ["title", "parent", "status"] - list_editable = ['status'] - prepopulated_fields = {"slug": ("title",)} - - fieldsets = ( - (_('Main'), { - 'fields': ['title', 'slug'] - }), - (_('Settings'), { - 'fields': ['status', 'static_path', 'path'], - }), - ) - - @admin.register(models.Article) -class ArticleAdmin(ContentEditor, PageAdmin): +class ArticleAdmin(ContentEditor): fieldsets = ( (_('Main'), { - 'fields': ['title', 'slug', 'as_program', 'cover', 'headline'], + 'fields': ['title', 'slug', 'cover', 'headline'], 'classes': ('tabbed', 'uncollapse') }), (_('Settings'), { - 'fields': ['featured', 'allow_comments', - 'status', 'static_path', 'path'], + 'fields': ['featured', 'as_program', 'allow_comments', 'status'], 'classes': ('tabbed',) }), - #(_('Infos'), { - # 'fields': ['diffusion'], - # 'classes': ('tabbed',) - #}), ) + list_display = ["title", "parent", "status"] + list_editable = ['status'] + prepopulated_fields = {"slug": ("title",)} inlines = [ ContentEditorInline.create(models.ArticleRichText), diff --git a/aircox_web/assets/styles.scss b/aircox_web/assets/styles.scss index db63020..75b854f 100644 --- a/aircox_web/assets/styles.scss +++ b/aircox_web/assets/styles.scss @@ -27,18 +27,26 @@ $body-background-color: $light; /** page **/ -img.cover { - border: 0.2em black solid; +.page { + .header { + margin-bottom: 1.5em; + } + + .headline { + font-size: 1.4em; + padding: 0.2em 0em; + } + + .cover { + float: right; + max-width: 40%; + margin: 1em; + border: 0.2em black solid; + } + + p { + padding: 0.4em 0em; + } } -.headline { - font-size: 1.2em; - padding: 0.2em 0em; -} - -img.cover { - float: right; - max-width: 40%; -} - diff --git a/aircox_web/converters.py b/aircox_web/converters.py index 89198f1..d6ca605 100644 --- a/aircox_web/converters.py +++ b/aircox_web/converters.py @@ -23,27 +23,26 @@ class PagePathConverter(StringConverter): return mark_safe(value) -#class WeekConverter: -# """ Converter for date as YYYYY/WW """ -# regex = r'[0-9]{4}/[0-9]{2}/?' -# -# def to_python(self, value): -# value = value.split('/') -# return datetime.date(int(value[0]), int(value[1]), int(value[2])) -# -# def to_url(self, value): -# return '{:04d}/{:02d}/'.format(*value.isocalendar()) +class WeekConverter: + """ Converter for date as YYYYY/WW """ + regex = r'[0-9]{4}/[0-9]{2}' + + def to_python(self, value): + return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date() + + def to_url(self, value): + return '{:04d}/{:02d}'.format(*value.isocalendar()) class DateConverter: """ Converter for date as YYYY/MM/DD """ - regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}/?' + regex = r'[0-9]{4}/[0-9]{2}/[0-9]{2}' def to_python(self, value): value = value.split('/') return datetime.date(int(value[0]), int(value[1]), int(value[2])) def to_url(self, value): - return '{:04d}/{:02d}/{:02d}/'.format(value.year, value.month, - value.day) + return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month, + value.day) diff --git a/aircox_web/models.py b/aircox_web/models.py index 0abaec1..ca365e4 100644 --- a/aircox_web/models.py +++ b/aircox_web/models.py @@ -1,7 +1,6 @@ -from django.core.validators import RegexValidator from django.db import models from django.db.models import F, Q -from django.db.models.functions import Concat, Substr +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from content_editor.models import Region, create_plugin_base @@ -13,42 +12,44 @@ from filer.fields.image import FilerImageField from aircox import models as aircox from . import plugins -from .converters import PagePathConverter class Site(models.Model): station = models.ForeignKey( aircox.Station, on_delete=models.SET_NULL, null=True, ) + #hosts = models.TextField( + # _('hosts'), + # help_text=_('website addresses (one per line)'), + #) # main settings title = models.CharField( - _('Title'), max_length=32, - help_text=_('Website title used at various places'), + _('title'), max_length=32, + help_text=_('website title displayed to users'), ) logo = FilerImageField( on_delete=models.SET_NULL, null=True, blank=True, - verbose_name=_('Logo'), + verbose_name=_('logo'), related_name='+', ) favicon = FilerImageField( on_delete=models.SET_NULL, null=True, blank=True, - verbose_name=_('Favicon'), + verbose_name=_('favicon'), related_name='+', ) - - default = models.BooleanField(_('default site'), + default = models.BooleanField(_('is default'), default=False, - help_text=_('Use as default site'), + help_text=_('use this website by default'), ) # meta descriptors description = models.CharField( - _('Description'), max_length=128, + _('description'), max_length=128, blank=True, null=True, ) tags = models.CharField( - _('Tags'), max_length=128, + _('tags'), max_length=128, blank=True, null=True, ) @@ -72,9 +73,8 @@ class SiteLink(plugins.Link, SitePlugin): #----------------------------------------------------------------------- class PageQueryset(InheritanceQuerySet): - def active(self): - return self.filter(Q(status=Page.STATUS.announced) | - Q(status=Page.STATUS.published)) + def live(self): + return self.filter(status=Page.STATUS.published) def descendants(self, page, direct=True, inclusive=True): qs = self.filter(parent=page) if direct else \ @@ -97,7 +97,7 @@ class Page(StatusModel): Base class for views whose url path can be defined by users. Page parenting is based on foreignkey to parent and page path. """ - STATUS = Choices('draft', 'announced', 'published') + STATUS = Choices('draft', 'published', 'trash') parent = models.ForeignKey( 'self', models.CASCADE, @@ -106,31 +106,15 @@ class Page(StatusModel): ) title = models.CharField(max_length=128) slug = models.SlugField(_('slug')) - path = models.CharField( - _("path"), max_length=1000, - blank=True, db_index=True, unique=True, - validators=[RegexValidator( - regex=PagePathConverter.regex, - message=_('Path accepts alphanumeric and "_-" characters ' - 'and must be surrounded by "/"') - )], - ) - static_path = models.BooleanField( - _('static path'), default=False, - # FIXME: help - help_text=_('Update path using parent\'s page path and page title') - ) headline = models.TextField( _('headline'), max_length=128, blank=True, null=True, ) objects = PageQueryset.as_manager() - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._initial_path = self.path - self._initial_parent = self.parent - self._initial_slug = self.slug + @property + def path(self): + return reverse('page', kwargs={'slug': self.slug}) def get_view_class(self): """ Page view class""" @@ -141,52 +125,12 @@ class Page(StatusModel): view = self.get_view_class().as_view(site=site, page=self) return view(request, *args, **kwargs) - def update_descendants(self): - """ Update descendants pages' path if required. """ - if self.path == self._initial_path: - return - - # FIXME: draft -> draft children? - # FIXME: Page.objects (can't use Page since its an abstract model) - if len(self._initial_path): - expr = Concat('path', Substr(F('path'), len(self._initial_path))) - Page.objects.filter(path__startswith=self._initial_path) \ - .update(path=expr) - - def sync_generations(self, update_descendants=True): - """ - Update fields (path, ...) based on parent. Update childrens if - ``update_descendants`` is True. - """ - # TODO: set parent based on path (when static path) - # TODO: ensure unique path fallback - if self.path == self._initial_path and \ - self.slug == self._initial_slug and \ - self.parent == self._initial_parent: - return - - if not self.title or not self.path or self.static_path and \ - self.slug != self._initial_slug: - self.path = self.parent.path + self.slug \ - if self.parent is not None else '/' + self.slug - - if self.path[0] != '/': - self.path = '/' + self.path - if self.path[-1] != '/': - self.path += '/' - if update_descendants: - self.update_descendants() - - def save(self, *args, update_descendants=True, **kwargs): - self.sync_generations(update_descendants) - super().save(*args, **kwargs) - def __str__(self): return '{}: {}'.format(self._meta.verbose_name, self.title or self.pk) -class Article(Page, TimeStampedModel): +class Article(Page): """ User's pages """ regions = [ Region(key="content", title=_("Content")), @@ -195,7 +139,7 @@ class Article(Page, TimeStampedModel): # metadata as_program = models.ForeignKey( aircox.Program, models.SET_NULL, blank=True, null=True, - related_name='published_pages', + related_name='+', # SO#51948640 # limit_choices_to={'schedule__isnull': False}, verbose_name=_('Show program as author'), @@ -216,27 +160,31 @@ class Article(Page, TimeStampedModel): verbose_name=_('Cover'), ) - def get_view_class(self): - from .views import ArticleView - return ArticleView class DiffusionPage(Article): diffusion = models.OneToOneField( aircox.Diffusion, models.CASCADE, related_name='page', + limit_choices_to={'initial__isnull': True} ) + @property + def path(self): + return reverse('diffusion-page', kwargs={'slug': self.slug}) + class ProgramPage(Article): + detail_url_name = 'program-page' + program = models.OneToOneField( aircox.Program, models.CASCADE, related_name='page', ) - def get_view_class(self): - from .views import ProgramView - return ProgramView + @property + def path(self): + return reverse('program-page', kwargs={'slug': self.slug}) #----------------------------------------------------------------------- diff --git a/aircox_web/templates/aircox_web/article.html b/aircox_web/templates/aircox_web/article.html deleted file mode 100644 index eee0760..0000000 --- a/aircox_web/templates/aircox_web/article.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "aircox_web/page.html" %} - - -{% block main %} -
- - {% block headline %} - {{ page.headline }} - {% endblock %} - - {% block content %} - {{ regions.main }} - {% endblock %} -
- -{% endblock %} - diff --git a/aircox_web/templates/aircox_web/diffusion_item.html b/aircox_web/templates/aircox_web/diffusion_item.html index a554e0d..c088709 100644 --- a/aircox_web/templates/aircox_web/diffusion_item.html +++ b/aircox_web/templates/aircox_web/diffusion_item.html @@ -3,6 +3,7 @@ Context variables: - object: the actual diffusion - page: current parent page in which item is rendered +- hide_schedule: if True, do not display start time {% endcomment %} {% with page as context_page %} @@ -10,29 +11,41 @@ Context variables: {% diffusion_page object as page %}
-
- -
+
-
-

- {% if page and context_page != page %} - {{ page.title }} - {% else %} - {{ page.title|default:program.name }} - {% endif %} - {% if object.page is page %} - — {{ program.name }} - {% endif %} - {% if object.initial %} - {% with object.initial.date as date %} - - {% trans "rerun" %} - - {% endwith %} - {% endif %} -
+

+

+ {% if page and context_page != page %} + {{ page.title }} + {% else %} + {{ page.title|default:program.name }} + {% endif %} +

+ + + {% if object.page is page and context_page != program.page %} + — {{ program.page.title }} + {% endif %} + + {% if not hide_schedule %} + + {% endif %} + + {% if object.initial %} + {% with object.initial.date as date %} + + {% trans "rerun" %} + + {% endwith %} + {% endif %} + +
+ +
{{ page.headline|default:program.page.headline }}

diff --git a/aircox_web/templates/aircox_web/diffusions.html b/aircox_web/templates/aircox_web/diffusions.html index d1a0fec..af1d29a 100644 --- a/aircox_web/templates/aircox_web/diffusions.html +++ b/aircox_web/templates/aircox_web/diffusions.html @@ -1,35 +1,53 @@ {% extends "aircox_web/page.html" %} {% load i18n aircox_web %} -{% block main %} -{{ block.super }} +{% block title %} +{% if program %} + {% with program.name as program %} + {% blocktrans %}Diffusions of {{ program }}{% endblocktrans %} + {% endwith %} +{% else %} + {% trans "All diffusions" %} + {% endif %} +{% endblock %} + +{% block header %} +{{ block.super }} +{% if program %} +

+ ❬ {{ program.name }} +

+{% include "aircox_web/program_header.html" %} +{% endif %} +{% endblock %} + + +{% block content %}
{% for object in object_list %} -
-
- -
-
- {% include "aircox_web/diffusion_item.html" %} -
-
+ {% with object.diffusion as object %} + {% include "aircox_web/diffusion_item.html" %} + {% endwith %} {% endfor %} +
{% if is_paginated %}