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