bkp before branching

This commit is contained in:
bkfox 2019-07-23 23:04:35 +02:00
parent 3e432c42b0
commit 6ed33c34a9
17 changed files with 330 additions and 392 deletions

View File

@ -18,13 +18,6 @@ class StreamInline(admin.TabularInline):
model = Stream model = Stream
extra = 1 extra = 1
class NameableAdmin(admin.ModelAdmin):
fields = [ 'name' ]
list_display = ['id', 'name']
list_filter = []
search_fields = ['name',]
@admin.register(Stream) @admin.register(Stream)
class StreamAdmin(admin.ModelAdmin): class StreamAdmin(admin.ModelAdmin):
@ -32,15 +25,19 @@ class StreamAdmin(admin.ModelAdmin):
@admin.register(Program) @admin.register(Program)
class ProgramAdmin(NameableAdmin): class ProgramAdmin(admin.ModelAdmin):
def schedule(self, obj): def schedule(self, obj):
return Schedule.objects.filter(program = obj).count() > 0 return Schedule.objects.filter(program=obj).count() > 0
schedule.boolean = True schedule.boolean = True
schedule.short_description = _("Schedule") schedule.short_description = _("Schedule")
list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station') list_display = ('name', 'id', 'active', 'schedule', 'sync', 'station')
fields = NameableAdmin.fields + [ 'active', 'station','sync' ] fields = ['name', 'slug', 'active', 'station', 'sync']
inlines = [ ScheduleInline, StreamInline ] prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']
inlines = [ScheduleInline, StreamInline]
@admin.register(Schedule) @admin.register(Schedule)
@ -64,7 +61,6 @@ class ScheduleAdmin(admin.ModelAdmin):
'time', 'duration', 'timezone', 'rerun'] 'time', 'duration', 'timezone', 'rerun']
list_editable = ['time', 'timezone', 'duration'] list_editable = ['time', 'timezone', 'duration']
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if obj: if obj:
return ['program', 'date', 'frequency'] return ['program', 'date', 'frequency']
@ -79,6 +75,7 @@ class PortInline(admin.StackedInline):
@admin.register(Station) @admin.register(Station)
class StationAdmin(admin.ModelAdmin): class StationAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
inlines = [PortInline] inlines = [PortInline]

View File

@ -2,19 +2,17 @@ from django.contrib import admin
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from aircox.models import Sound from aircox.models import Sound
from .base import NameableAdmin
from .playlist import TracksInline from .playlist import TracksInline
@admin.register(Sound) @admin.register(Sound)
class SoundAdmin(NameableAdmin): class SoundAdmin(admin.ModelAdmin):
fields = None fields = None
list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime', list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime',
'public', 'good_quality', 'path'] 'public', 'good_quality', 'path']
list_filter = ('program', 'type', 'good_quality', 'public') list_filter = ('program', 'type', 'good_quality', 'public')
fieldsets = [ fieldsets = [
(None, {'fields': NameableAdmin.fields + (None, {'fields': ['name', 'path', 'type', 'program', 'diffusion']}),
['path', 'type', 'program', 'diffusion']}),
(None, {'fields': ['embed', 'duration', 'public', 'mtime']}), (None, {'fields': ['embed', 'duration', 'public', 'mtime']}),
(None, {'fields': ['good_quality']}) (None, {'fields': ['good_quality']})
] ]

View File

@ -11,9 +11,9 @@ from django.contrib.contenttypes.fields import (GenericForeignKey,
GenericRelation) GenericRelation)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models 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.db.transaction import atomic
from django.template.defaultfilters import slugify
from django.utils import timezone as tz from django.utils import timezone as tz
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import strip_tags from django.utils.html import strip_tags
@ -26,28 +26,6 @@ from taggit.managers import TaggableManager
logger = logging.getLogger('aircox.core') 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 # Station related classes
# #
@ -67,7 +45,7 @@ def default_station():
return Station.objects.default() return Station.objects.default()
class Station(Nameable): class Station(models.Model):
""" """
Represents a radio station, to which multiple programs are attached Represents a radio station, to which multiple programs are attached
and that is used as the top object for everything. 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) Theses are set up when needed (at the first access to these elements)
then cached. then cached.
""" """
name = models.CharField(_('name'), max_length=64)
slug = models.SlugField(_('slug'), max_length=64, unique=True)
path = models.CharField( path = models.CharField(
_('path'), _('path'),
help_text=_('path to the working directory'), help_text=_('path to the working directory'),
@ -199,6 +179,9 @@ class Station(Nameable):
logs = logs[:count] logs = logs[:count]
return logs return logs
def __str__(self):
return self.name
def save(self, make_sources=True, *args, **kwargs): def save(self, make_sources=True, *args, **kwargs):
if not self.path: if not self.path:
self.path = os.path.join( self.path = os.path.join(
@ -223,7 +206,7 @@ class ProgramManager(models.Manager):
return qs.filter(station=station, **kwargs) return qs.filter(station=station, **kwargs)
class Program(Nameable): class Program(models.Model):
""" """
A Program can either be a Streamed or a Scheduled program. A Program can either be a Streamed or a Scheduled program.
@ -241,6 +224,8 @@ class Program(Nameable):
verbose_name=_('station'), verbose_name=_('station'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
name = models.CharField(_('name'), max_length=64)
slug = models.SlugField(_('slug'), max_length=64, unique=True)
active = models.BooleanField( active = models.BooleanField(
_('active'), _('active'),
default=True, default=True,
@ -254,14 +239,10 @@ class Program(Nameable):
objects = ProgramManager() objects = ProgramManager()
# TODO: use unique slug
@property @property
def path(self): def path(self):
""" """ Return program's directory path """
Return the path to the programs directory return os.path.join(settings.AIRCOX_PROGRAMS_DIR, self.slug)
"""
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):
""" """
@ -299,26 +280,8 @@ class Program(Nameable):
def __init__(self, *kargs, **kwargs): def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
if self.name: if self.slug:
self.__original_path = self.path self.__initial_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()
@classmethod @classmethod
def get_from_path(cl, path): def get_from_path(cl, path):
@ -346,6 +309,23 @@ class Program(Nameable):
def is_show(self): def is_show(self):
return self.schedule_set.count() != 0 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): class Stream(models.Model):
""" """
@ -427,15 +407,15 @@ class Schedule(models.Model):
_('frequency'), _('frequency'),
choices=[(int(y), { choices=[(int(y), {
'ponctual': _('ponctual'), 'ponctual': _('ponctual'),
'first': _('first week of the month'), 'first': _('1st {day} of the month'),
'second': _('second week of the month'), 'second': _('2nd {day} of the month'),
'third': _('third week of the month'), 'third': _('3rd {day} of the month'),
'fourth': _('fourth week of the month'), 'fourth': _('4th {day} of the month'),
'last': _('last week of the month'), 'last': _('last {day} of the month'),
'first_and_third': _('first and third weeks of the month'), 'first_and_third': _('1st and 3rd {day}s of the month'),
'second_and_fourth': _('second and fourth weeks of the month'), 'second_and_fourth': _('2nd and 4th {day}s of the month'),
'every': _('every week'), 'every': _('{day}'),
'one_on_two': _('one week on two'), 'one_on_two': _('one {day} on two'),
}[x]) for x, y in Frequency.__members__.items()], }[x]) for x, y in Frequency.__members__.items()],
) )
initial = models.ForeignKey( initial = models.ForeignKey(
@ -454,11 +434,22 @@ class Schedule(models.Model):
return pytz.timezone(self.timezone) return pytz.timezone(self.timezone)
@property @cached_property
def datetime(self): def start(self):
""" Datetime for this schedule (timezone unaware) """ """ Datetime of the start (timezone unaware) """
import datetime return tz.datetime.combine(self.date, self.time)
return datetime.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 cached data
__initial = None __initial = None
@ -630,7 +621,7 @@ class Schedule(models.Model):
delta = None delta = None
if self.initial: 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 # FIXME: daylight saving bug: delta misses an hour when diffusion and
# rerun are not on the same daylight-saving timezone # rerun are not on the same daylight-saving timezone
@ -916,9 +907,12 @@ class Diffusion(models.Model):
self.check_conflicts() self.check_conflicts()
def __str__(self): def __str__(self):
return '{self.program.name} {date} #{self.pk}'.format( str_ = '{self.program.name} {date}'.format(
self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z') self=self, date=self.local_start.strftime('%Y/%m/%d %H:%M%z'),
) )
if self.initial:
str_ += ' ({})'.format(_('rerun'))
return str_
class Meta: class Meta:
verbose_name = _('Diffusion') 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 A Sound is the representation of a sound file that can be either an excerpt
or a complete archive of the related diffusion. or a complete archive of the related diffusion.
@ -939,6 +933,7 @@ class Sound(Nameable):
excerpt = 0x02, excerpt = 0x02,
removed = 0x03, removed = 0x03,
name = models.CharField(_('name'), max_length=64)
program = models.ForeignKey( program = models.ForeignKey(
Program, Program,
verbose_name=_('program'), verbose_name=_('program'),

View File

@ -4,8 +4,6 @@ from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from content_editor.admin import ContentEditor, ContentEditorInline from content_editor.admin import ContentEditor, ContentEditorInline
from feincms3 import plugins
from feincms3.admin import TreeAdmin
from aircox import models as aircox from aircox import models as aircox
from . import models from . import models
@ -38,39 +36,21 @@ class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
view_obj.save() 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) @admin.register(models.Article)
class ArticleAdmin(ContentEditor, PageAdmin): class ArticleAdmin(ContentEditor):
fieldsets = ( fieldsets = (
(_('Main'), { (_('Main'), {
'fields': ['title', 'slug', 'as_program', 'cover', 'headline'], 'fields': ['title', 'slug', 'cover', 'headline'],
'classes': ('tabbed', 'uncollapse') 'classes': ('tabbed', 'uncollapse')
}), }),
(_('Settings'), { (_('Settings'), {
'fields': ['featured', 'allow_comments', 'fields': ['featured', 'as_program', 'allow_comments', 'status'],
'status', 'static_path', 'path'],
'classes': ('tabbed',) 'classes': ('tabbed',)
}), }),
#(_('Infos'), {
# 'fields': ['diffusion'],
# 'classes': ('tabbed',)
#}),
) )
list_display = ["title", "parent", "status"]
list_editable = ['status']
prepopulated_fields = {"slug": ("title",)}
inlines = [ inlines = [
ContentEditorInline.create(models.ArticleRichText), ContentEditorInline.create(models.ArticleRichText),

View File

@ -27,18 +27,26 @@ $body-background-color: $light;
/** page **/ /** page **/
img.cover { .page {
border: 0.2em black solid; .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%;
}

View File

@ -23,27 +23,26 @@ class PagePathConverter(StringConverter):
return mark_safe(value) return mark_safe(value)
#class WeekConverter: class WeekConverter:
# """ Converter for date as YYYYY/WW """ """ Converter for date as YYYYY/WW """
# regex = r'[0-9]{4}/[0-9]{2}/?' regex = r'[0-9]{4}/[0-9]{2}'
#
# def to_python(self, value): def to_python(self, value):
# value = value.split('/') return datetime.datetime.strptime(value + '/1', '%G/%V/%u').date()
# return datetime.date(int(value[0]), int(value[1]), int(value[2]))
# def to_url(self, value):
# def to_url(self, value): return '{:04d}/{:02d}'.format(*value.isocalendar())
# return '{:04d}/{:02d}/'.format(*value.isocalendar())
class DateConverter: class DateConverter:
""" Converter for date as YYYY/MM/DD """ """ 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): def to_python(self, value):
value = value.split('/') value = value.split('/')
return datetime.date(int(value[0]), int(value[1]), int(value[2])) return datetime.date(int(value[0]), int(value[1]), int(value[2]))
def to_url(self, value): def to_url(self, value):
return '{:04d}/{:02d}/{:02d}/'.format(value.year, value.month, return '{:04d}/{:02d}/{:02d}'.format(value.year, value.month,
value.day) value.day)

View File

@ -1,7 +1,6 @@
from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.db.models import F, Q 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 django.utils.translation import ugettext_lazy as _
from content_editor.models import Region, create_plugin_base 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 aircox import models as aircox
from . import plugins from . import plugins
from .converters import PagePathConverter
class Site(models.Model): class Site(models.Model):
station = models.ForeignKey( station = models.ForeignKey(
aircox.Station, on_delete=models.SET_NULL, null=True, aircox.Station, on_delete=models.SET_NULL, null=True,
) )
#hosts = models.TextField(
# _('hosts'),
# help_text=_('website addresses (one per line)'),
#)
# main settings # main settings
title = models.CharField( title = models.CharField(
_('Title'), max_length=32, _('title'), max_length=32,
help_text=_('Website title used at various places'), help_text=_('website title displayed to users'),
) )
logo = FilerImageField( logo = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True, on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Logo'), verbose_name=_('logo'),
related_name='+', related_name='+',
) )
favicon = FilerImageField( favicon = FilerImageField(
on_delete=models.SET_NULL, null=True, blank=True, on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Favicon'), verbose_name=_('favicon'),
related_name='+', related_name='+',
) )
default = models.BooleanField(_('is default'),
default = models.BooleanField(_('default site'),
default=False, default=False,
help_text=_('Use as default site'), help_text=_('use this website by default'),
) )
# meta descriptors # meta descriptors
description = models.CharField( description = models.CharField(
_('Description'), max_length=128, _('description'), max_length=128,
blank=True, null=True, blank=True, null=True,
) )
tags = models.CharField( tags = models.CharField(
_('Tags'), max_length=128, _('tags'), max_length=128,
blank=True, null=True, blank=True, null=True,
) )
@ -72,9 +73,8 @@ class SiteLink(plugins.Link, SitePlugin):
#----------------------------------------------------------------------- #-----------------------------------------------------------------------
class PageQueryset(InheritanceQuerySet): class PageQueryset(InheritanceQuerySet):
def active(self): def live(self):
return self.filter(Q(status=Page.STATUS.announced) | return self.filter(status=Page.STATUS.published)
Q(status=Page.STATUS.published))
def descendants(self, page, direct=True, inclusive=True): def descendants(self, page, direct=True, inclusive=True):
qs = self.filter(parent=page) if direct else \ 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. Base class for views whose url path can be defined by users.
Page parenting is based on foreignkey to parent and page path. Page parenting is based on foreignkey to parent and page path.
""" """
STATUS = Choices('draft', 'announced', 'published') STATUS = Choices('draft', 'published', 'trash')
parent = models.ForeignKey( parent = models.ForeignKey(
'self', models.CASCADE, 'self', models.CASCADE,
@ -106,31 +106,15 @@ class Page(StatusModel):
) )
title = models.CharField(max_length=128) title = models.CharField(max_length=128)
slug = models.SlugField(_('slug')) 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 = models.TextField(
_('headline'), max_length=128, blank=True, null=True, _('headline'), max_length=128, blank=True, null=True,
) )
objects = PageQueryset.as_manager() objects = PageQueryset.as_manager()
def __init__(self, *args, **kwargs): @property
super().__init__(*args, **kwargs) def path(self):
self._initial_path = self.path return reverse('page', kwargs={'slug': self.slug})
self._initial_parent = self.parent
self._initial_slug = self.slug
def get_view_class(self): def get_view_class(self):
""" Page view class""" """ Page view class"""
@ -141,52 +125,12 @@ class Page(StatusModel):
view = self.get_view_class().as_view(site=site, page=self) view = self.get_view_class().as_view(site=site, page=self)
return view(request, *args, **kwargs) 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): def __str__(self):
return '{}: {}'.format(self._meta.verbose_name, return '{}: {}'.format(self._meta.verbose_name,
self.title or self.pk) self.title or self.pk)
class Article(Page, TimeStampedModel): class Article(Page):
""" User's pages """ """ User's pages """
regions = [ regions = [
Region(key="content", title=_("Content")), Region(key="content", title=_("Content")),
@ -195,7 +139,7 @@ class Article(Page, TimeStampedModel):
# metadata # metadata
as_program = models.ForeignKey( as_program = models.ForeignKey(
aircox.Program, models.SET_NULL, blank=True, null=True, aircox.Program, models.SET_NULL, blank=True, null=True,
related_name='published_pages', related_name='+',
# SO#51948640 # SO#51948640
# limit_choices_to={'schedule__isnull': False}, # limit_choices_to={'schedule__isnull': False},
verbose_name=_('Show program as author'), verbose_name=_('Show program as author'),
@ -216,27 +160,31 @@ class Article(Page, TimeStampedModel):
verbose_name=_('Cover'), verbose_name=_('Cover'),
) )
def get_view_class(self):
from .views import ArticleView
return ArticleView
class DiffusionPage(Article): class DiffusionPage(Article):
diffusion = models.OneToOneField( diffusion = models.OneToOneField(
aircox.Diffusion, models.CASCADE, aircox.Diffusion, models.CASCADE,
related_name='page', related_name='page',
limit_choices_to={'initial__isnull': True}
) )
@property
def path(self):
return reverse('diffusion-page', kwargs={'slug': self.slug})
class ProgramPage(Article): class ProgramPage(Article):
detail_url_name = 'program-page'
program = models.OneToOneField( program = models.OneToOneField(
aircox.Program, models.CASCADE, aircox.Program, models.CASCADE,
related_name='page', related_name='page',
) )
def get_view_class(self): @property
from .views import ProgramView def path(self):
return ProgramView return reverse('program-page', kwargs={'slug': self.slug})
#----------------------------------------------------------------------- #-----------------------------------------------------------------------

View File

@ -1,17 +0,0 @@
{% extends "aircox_web/page.html" %}
{% block main %}
<section class="is-inline-block">
<img class="cover" src="{{ page.cover.url }}"/>
{% block headline %}
{{ page.headline }}
{% endblock %}
{% block content %}
{{ regions.main }}
{% endblock %}
</section>
{% endblock %}

View File

@ -3,6 +3,7 @@
Context variables: Context variables:
- object: the actual diffusion - object: the actual diffusion
- page: current parent page in which item is rendered - page: current parent page in which item is rendered
- hide_schedule: if True, do not display start time
{% endcomment %} {% endcomment %}
{% with page as context_page %} {% with page as context_page %}
@ -10,29 +11,41 @@ Context variables:
{% diffusion_page object as page %} {% diffusion_page object as page %}
<article class="media"> <article class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-64x64"> <img src="{% thumbnail page.cover|default:site.logo 128x128 crop=scale %}">
<img src="{% thumbnail page.cover|default:site.logo 128x128 crop=scale %}">
</figure>
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="content"> <div>
<p> <h4 class="subtitle is-size-4 is-inline-block">
{% if page and context_page != page %} {% if page and context_page != page %}
<strong><a href="{{ page.path }}">{{ page.title }}</a></strong> <a href="{{ page.path }}">{{ page.title }}</a>
{% else %} {% else %}
<strong>{{ page.title|default:program.name }}</strong> {{ page.title|default:program.name }}
{% endif %} {% endif %}
{% if object.page is page %} </h4>
&mdash; <a href="{{ program.page.path }}">{{ program.name }}</a></small>
{% endif %} <span class="has-text-weight-normal">
{% if object.initial %} {% if object.page is page and context_page != program.page %}
{% with object.initial.date as date %} &mdash; <a href="{% url "program-page" slug=program.page.slug %}">{{ program.page.title }}</a>
<span class="tag is-info" title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}"> {% endif %}
{% trans "rerun" %}
</span> {% if not hide_schedule %}
{% endwith %} <time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}"
{% endif %} class="has-text-weight-light is-size-6">
<br> &mdash; {{ object.start|date:"d M, H:i" }}
</time>
{% endif %}
{% if object.initial %}
{% with object.initial.date as date %}
<span class="tag is-info" title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
{% trans "rerun" %}
</span>
{% endwith %}
{% endif %}
</span>
</div>
<div class="content">
{{ page.headline|default:program.page.headline }} {{ page.headline|default:program.page.headline }}
</p> </p>
</div> </div>

View File

@ -1,35 +1,53 @@
{% extends "aircox_web/page.html" %} {% extends "aircox_web/page.html" %}
{% load i18n aircox_web %} {% load i18n aircox_web %}
{% block main %} {% block title %}
{{ block.super }} {% 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 %}
<h4 class="subtitle is-size-3">
<a href="{% url "page" path=program.page.path %}">&#10092; {{ program.name }}</a></li>
</h4>
{% include "aircox_web/program_header.html" %}
{% endif %}
{% endblock %}
{% block content %}
<section class="section"> <section class="section">
{% for object in object_list %} {% for object in object_list %}
<div class="columns"> {% with object.diffusion as object %}
<div class="column is-one-fifth has-text-right"> {% include "aircox_web/diffusion_item.html" %}
<time datetime="{{ object.start|date:"c" }}" title="{{ object.start }}"> {% endwith %}
{{ object.start|date:"d M, H:i" }}
</time>
</div>
<div class="column">
{% include "aircox_web/diffusion_item.html" %}
</div>
</div>
{% endfor %} {% endfor %}
</section>
{% if is_paginated %} {% if is_paginated %}
<nav class="pagination is-centered" role="pagination" aria-label="{% trans "pagination" %}"> <nav class="pagination is-centered" role="pagination" aria-label="{% trans "pagination" %}">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}" class="pagination-previous"> <a href="?page={{ page_obj.previous_page_number }}" class="pagination-previous">
{% trans "Previous" %}</a> {% else %}
<a class="pagination-previous" disabled>
{% endif %} {% endif %}
{% trans "Previous" %}</a>
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="pagination-next"> <a href="?page={{ page_obj.next_page_number }}" class="pagination-next">
{% trans "Next" %}</a> {% else %}
<a class="pagination-next" disabled>
{% endif %} {% endif %}
{% trans "Next" %}</a>
<ul class="pagination-list"> <ul class="pagination-list">
{% for i in paginator.page_range %} {% for i in paginator.page_range %}

View File

@ -25,6 +25,7 @@
{% endif %} {% endif %}
{# <h4 class="subtitle size-4">{{ date }}</h4> #} {# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% with True as hide_schedule %}
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth">
{% for object in object_list reversed %} {% for object in object_list reversed %}
<tr> <tr>
@ -46,6 +47,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endwith %}
</section> </section>
{% endblock %} {% endblock %}

View File

@ -1,59 +1,36 @@
{% extends "aircox_web/base.html" %}
{% load static i18n thumbnail %} {% load static i18n thumbnail %}
<html> {% comment %}
<head> Context:
<meta charset="utf-8"> - cover: cover image
<meta name="application-name" content="aircox"> - title: title
<meta name="description" content="{{ site.description }}"> - page: page
<meta name="keywords" content="{{ site.tags }}"> {% endcomment %}
<link rel="icon" href="{% thumbnail site.favicon 32x32 crop %}" />
{% block assets %} {% block head_title %}
<link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/> {% block title %}{{ title }}{% endblock %}
<script src="{% static "aircox_web/assets/main.js" %}"></script> {% if title %} &mdash; {% endif %}
<script src="{% static "aircox_web/assets/vendor.js" %}"></script> {{ site.title }}
{% endblock %} {% endblock %}
<title>{% block title %}{% if title %}{{ title }} -- {% endif %}{{ site.title }}{% endblock %}</title>
{% block main %}
{% block extra_head %}{% endblock %} {% block headline %}
</head> {% if page and page.headline %}
<body> <p class="headline">{{ page.headline }}</p>
<div id="app"> {% endif %}
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation"> {% endblock %}
<div class="container">
<div class="navbar-brand"> {% block content %}
<a href="/" title="{% trans "Home" %}" class="navbar-item"> {{ regions.content }}
<img src="{{ site.logo.url }}" class="logo"/> {% endblock %}
</a> {% endblock %}
</div>
<div class="navbar-menu">
<div class="navbar-start"> {% block side_nav %}
{{ site_regions.topnav }} {% if cover is not None %}
</div> <img class="cover" src="{{ cover.url }}" class="cover"/>
</div> {% endif %}
</div> {% endblock %}
</nav>
<div class="container">
<div class="columns">
<aside class="column is-one-quarter">
{% block left-sidebar %}
{{ site_regions.sidenav }}
{% endblock %}
</aside>
<main class="column page">
<header class="header">
{% block header %}
<h1 class="title is-1">{{ title }}</h1>
{% endblock %}
</header>
{% block main %}{% endblock main %}
</main>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,27 +1,8 @@
{% extends "aircox_web/article.html" %} {% extends "aircox_web/page.html" %}
{% load i18n %}
{% block headline %}
<section class="is-size-5">
{% for schedule in program.schedule_set.all %}
<p>
<strong>{{ schedule.datetime|date:"l H:i" }}</strong>
<small>
{{ schedule.get_frequency_display }}
{% if schedule.initial %}
{% with schedule.initial.date as date %}
<span title="{% blocktrans %}Rerun of {{ date }}{% endblocktrans %}">
/ {% trans "rerun" %}
</span>
{% endwith %}
{% endif %}
</small>
</p>
{% endfor %}
</section>
{% block header %}
{{ block.super }} {{ block.super }}
{% include "aircox_web/program_header.html" %}
{% endblock %} {% endblock %}

View File

@ -1,6 +1,10 @@
{% extends "aircox_web/page.html" %} {% extends "aircox_web/page.html" %}
{% load i18n aircox_web %} {% load i18n aircox_web %}
{% block title %}
{% trans "Timetable" %}
{% endblock %}
{% block main %} {% block main %}
{{ block.super }} {{ block.super }}
@ -27,6 +31,7 @@
</li> </li>
</template> </template>
{% with True as hide_schedule %}
<template v-slot:default="{value}"> <template v-slot:default="{value}">
{% for day, diffusions in by_date.items %} {% for day, diffusions in by_date.items %}
<noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript> <noscript><h4 class="subtitle is-4">{{ day|date:"l d F Y" }}</h4></noscript>
@ -46,6 +51,7 @@
</div> </div>
{% endfor %} {% endfor %}
</template> </template>
{% endwith %}
</a-tabs> </a-tabs>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -8,19 +8,22 @@ from aircox_web.models import Page
random.seed() random.seed()
register = template.Library() register = template.Library()
@register.simple_tag(name='diffusion_page') @register.simple_tag(name='diffusion_page')
def do_diffusion_page(diffusion): def do_diffusion_page(diffusion):
""" Return page for diffusion. """ """ Return page for diffusion. """
for obj in (diffusion, diffusion.program): for obj in (diffusion, diffusion.program):
page = getattr(obj, 'page', None) page = getattr(obj, 'page', None)
if page is not None and page.status is not Page.STATUS.draft: if page is not None and page.status == Page.STATUS.published:
return page return page
@register.simple_tag(name='unique_id') @register.simple_tag(name='unique_id')
def do_unique_id(prefix=''): def do_unique_id(prefix=''):
value = str(random.random()).replace('.', '') value = str(random.random()).replace('.', '')
return prefix + '_' + value if prefix else value return prefix + '_' + value if prefix else value
@register.filter(name='is_diffusion') @register.filter(name='is_diffusion')
def do_is_diffusion(obj): def do_is_diffusion(obj):
return isinstance(obj, aircox.Diffusion) return isinstance(obj, aircox.Diffusion)

View File

@ -2,22 +2,29 @@ from django.conf.urls import url
from django.urls import path, register_converter from django.urls import path, register_converter
from . import views, models from . import views, models
from .converters import PagePathConverter, DateConverter from .converters import PagePathConverter, DateConverter, WeekConverter
register_converter(PagePathConverter, 'page_path') register_converter(PagePathConverter, 'page_path')
register_converter(DateConverter, 'date') register_converter(DateConverter, 'date')
register_converter(WeekConverter, 'week')
urlpatterns = [ urlpatterns = [
path('programs/<slug:slug>/',
views.ProgramPageView.as_view(), name='program-page'),
path('programs/<slug:program_slug>/diffusions/',
views.DiffusionsView.as_view(), name='diffusion-list'),
path('diffusion/<slug:slug>/',
views.ProgramPageView.as_view(), name='diffusion-page'),
path('diffusions/', path('diffusions/',
views.TimetableView.as_view(), name='timetable'), views.TimetableView.as_view(), name='timetable'),
path('diffusions/<date:date>', path('diffusions/<week:date>/',
views.TimetableView.as_view(), name='timetable'), views.TimetableView.as_view(), name='timetable'),
path('diffusions/all', path('diffusions/all',
views.DiffusionsView.as_view(), name='diffusion-list'), views.DiffusionsView.as_view(), name='diffusion-list'),
path('diffusions/<slug:program>',
views.DiffusionsView.as_view(), name='diffusion-list'),
path('logs/', views.LogsView.as_view(), name='logs'), path('logs/', views.LogsView.as_view(), name='logs'),
path('logs/<date:date>', views.LogsView.as_view(), name='logs'), path('logs/<date:date>/', views.LogsView.as_view(), name='logs'),
path('<page_path:path>', views.route_page, name='page'), path('<page_path:path>', views.route_page, name='page'),
] ]

View File

@ -1,16 +1,17 @@
from collections import OrderedDict, deque from collections import OrderedDict, deque
import datetime import datetime
from django.db.models import Q
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, ListView from django.views.generic import TemplateView, DetailView, ListView
from django.views.generic.base import TemplateResponseMixin, ContextMixin from django.views.generic.base import TemplateResponseMixin, ContextMixin
from content_editor.contents import contents_for_item from content_editor.contents import contents_for_item
from aircox import models as aircox from aircox import models as aircox
from .models import Site, Page from .models import Site, Page, DiffusionPage, ProgramPage
from .renderer import site_renderer, page_renderer from .renderer import site_renderer, page_renderer
@ -35,6 +36,7 @@ def route_page(request, path=None, *args, model=None, site=None, **kwargs):
class BaseView(TemplateResponseMixin, ContextMixin): class BaseView(TemplateResponseMixin, ContextMixin):
title = None title = None
cover = None
site = None site = None
def dispatch(self, request, *args, site=None, **kwargs): def dispatch(self, request, *args, site=None, **kwargs):
@ -48,64 +50,85 @@ class BaseView(TemplateResponseMixin, ContextMixin):
kwargs['site_regions'] = contents.render_regions(site_renderer) kwargs['site_regions'] = contents.render_regions(site_renderer)
kwargs.setdefault('site', self.site) kwargs.setdefault('site', self.site)
if self.title is not None: kwargs.setdefault('cover', self.cover)
kwargs.setdefault('title', self.title)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class ArticleView(BaseView, TemplateView): class PageView(BaseView):
""" Base view class for pages. """ """ Base view class for pages. """
template_name = 'aircox_web/article.html' template_name = 'aircox_web/page.html'
context_object_name = 'page'
page = None page = None
def get_context_data(self, **kwargs): def get_queryset(self):
# article content return super().get_queryset().live()
page = kwargs.setdefault('page', self.page or self.kwargs.get('page'))
if kwargs.get('regions') is None:
contents = contents_for_item(page, page_renderer._renderers.keys())
kwargs['regions'] = contents.render_regions(page_renderer)
kwargs.setdefault('title', page.title) def get_context_data(self, **kwargs):
page = getattr(self, 'object', None)
if page is not None:
if kwargs.get('regions') is None:
contents = contents_for_item(
page, page_renderer._renderers.keys())
kwargs['regions'] = contents.render_regions(page_renderer)
kwargs.setdefault('title', page.title)
kwargs.setdefault('cover', page.cover)
kwargs.setdefault('page', page)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class ProgramView(ArticleView): class ProgramPageView(PageView, DetailView):
""" Base view class for pages. """ """ Base view class for pages. """
template_name = 'aircox_web/program.html' template_name = 'aircox_web/program.html'
next_diffs_count = 5 model = ProgramPage
def get_queryset(self):
return super().get_queryset().select_related('program')
def get_context_data(self, program=None, **kwargs): def get_context_data(self, program=None, **kwargs):
# TODO: pagination kwargs.setdefault('program', self.object.program)
program = program or self.page.program kwargs['diffusions'] = DiffusionPage.objects.filter(
#next_diffs = program.diffusion_set.on_air().after().order_by('start') diffusion__program=kwargs['program']
return super().get_context_data(
program=program,
# next_diffs=next_diffs[:self.next_diffs_count],
**kwargs,
) )
return super().get_context_data(**kwargs)
class DiffusionView(ArticleView): class DiffusionView(PageView):
template_name = 'aircox_web/diffusion.html' template_name = 'aircox_web/diffusion.html'
# TODO: pagination: in template, only a limited number of pages displayed
# DiffusionsView use diffusion instead of diffusion page for different reasons:
# more straightforward, it handles reruns
class DiffusionsView(BaseView, ListView): class DiffusionsView(BaseView, ListView):
template_name = 'aircox_web/diffusions.html' template_name = 'aircox_web/diffusions.html'
model = aircox.Diffusion model = DiffusionPage
paginate_by = 10 paginate_by = 30
title = _('Diffusions')
program = None program = None
# TODO: get program object + display program title when filtered by program def get(self, request, *args, **kwargs):
# TODO: pagination: in template, only a limited number of pages displayed program_slug = kwargs.get('program_slug')
if program_slug:
self.program = get_object_or_404(
aircox.Program, slug=kwargs.get('program_slug'))
return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().station(self.site.station).on_air() \ qs = super().get_queryset().live() \
.filter(initial__isnull=True) #TODO, page__isnull=False) .select_related('diffusion')
program = self.kwargs.get('program') if self.program:
if program: qs = qs.filter(diffusion__program=self.program)
qs = qs.filter(program__page__slug=program) else:
return qs.order_by('-start') qs = qs.select_related('diffusion__program')
return qs.order_by('-diffusion__start')
def get_context_data(self, **kwargs):
program = kwargs.setdefault('program', self.program)
if program is not None and hasattr(program, 'page'):
kwargs.setdefault('cover', program.page.cover)
kwargs.setdefault('page', program.page)
return super().get_context_data(**kwargs)
class TimetableView(BaseView, ListView): class TimetableView(BaseView, ListView):
@ -120,9 +143,9 @@ class TimetableView(BaseView, ListView):
end = None end = None
def get_queryset(self): def get_queryset(self):
self.date = self.kwargs.get('date', datetime.date.today()) self.date = self.kwargs.get('date') or datetime.date.today()
self.start = self.date - datetime.timedelta(days=self.date.weekday()) self.start = self.date - datetime.timedelta(days=self.date.weekday())
self.end = self.date + datetime.timedelta(days=7-self.date.weekday()) self.end = self.start + datetime.timedelta(days=7)
return super().get_queryset().station(self.site.station) \ return super().get_queryset().station(self.site.station) \
.range(self.start, self.end) \ .range(self.start, self.end) \
.order_by('start') .order_by('start')