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;...

This commit is contained in:
bkfox 2016-06-12 21:34:31 +02:00
parent 13bf57b401
commit c3ae0e012c
18 changed files with 180 additions and 196 deletions

View File

@ -50,6 +50,9 @@ class MyModelPost(RelatedPost):
Note: it is possible to assign a function as a bounded value; in such case, the 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)**. 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
Routes are used to generate the URLs of the website. We provide some of the 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 common routes: for the detail view of course, but also to select all posts or

View File

@ -91,7 +91,6 @@ def inject_inline(model, inline, prepend = False):
registry[model].inlines = inlines registry[model].inlines = inlines
admin.site.register(models.Article, PostAdmin)
admin.site.register(models.Comment, CommentAdmin) admin.site.register(models.Comment, CommentAdmin)

View File

@ -166,7 +166,10 @@ class Post (models.Model, Routable):
) )
return qs return qs
def detail_url(self): def url(self):
"""
Return an url to the post detail view.
"""
return self.route_url( return self.route_url(
routes.DetailRoute, routes.DetailRoute,
pk = self.pk, slug = slugify(self.title) pk = self.pk, slug = slugify(self.title)
@ -209,24 +212,6 @@ class Post (models.Model, Routable):
abstract = True 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): class RelatedPostBase (models.base.ModelBase):
""" """
Metaclass for RelatedPost children. Metaclass for RelatedPost children.
@ -356,6 +341,7 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
class Meta: class Meta:
abstract = True abstract = True
# FIXME: declare a binding only for init
class Relation: class Relation:
""" """
Relation descriptor used to generate and manage the related object. Relation descriptor used to generate and manage the related object.

View File

@ -203,7 +203,7 @@ class ListItem:
date = None date = None
image = None image = None
info = None info = None
detail_url = None url = None
css_class = None css_class = None
attrs = None attrs = None
@ -222,8 +222,8 @@ class ListItem:
continue continue
if hasattr(post, i) and not getattr(self, i): if hasattr(post, i) and not getattr(self, i):
setattr(self, i, getattr(post, i)) setattr(self, i, getattr(post, i))
if not self.detail_url and hasattr(post, 'detail_url'): if not self.url and hasattr(post, 'url'):
self.detail_url = post.detail_url() self.url = post.url()
class List(Section): class List(Section):
@ -243,6 +243,7 @@ class List(Section):
object_list = None object_list = None
url = None url = None
message_empty = _('nothing') message_empty = _('nothing')
paginate_by = 4
fields = [ 'date', 'time', 'image', 'title', 'content', 'info' ] fields = [ 'date', 'time', 'image', 'title', 'content', 'info' ]
image_size = '64x64' image_size = '64x64'
@ -264,20 +265,32 @@ class List(Section):
def get_object_list(self): def get_object_list(self):
return self.object_list return self.object_list
def get_context_data(self, *args, **kwargs): def get_context_data(self, request, object=None, *args, **kwargs):
context = super().get_context_data(*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() object_list = self.object_list or self.get_object_list()
if not object_list and not self.message_empty: if not object_list and not self.message_empty:
return return
self.object_list = object_list
context = super().get_context_data(request, object, *args, **kwargs)
context.update({ context.update({
'base_template': 'aircox/cms/section.html', 'base_template': 'aircox/cms/section.html',
'list': self, '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 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): class Comments(List):
""" """

View File

@ -1,51 +0,0 @@
{% extends "admin/base.html" %}
{% block extrahead %}
{% include 'autocomplete_light/static.html' %}
<style>
/** autocomplete override **/
.autocomplete-light-widget .deck [data-value] .remove {
float: right;
}
.autocomplete-light-widget .deck [data-value],
.autocomplete-light-widget .deck .choice {
display: block;
}
.control-group .add-related,
.inline-group .add-related {
vertical-align: bottom;
}
/** suit **/
.controls textarea,
.controls .vTextField {
width: calc(100% - 10px);
}
/** grappelli **/
.grp-autocomplete-wrapper-m2m:focus, .grp-autocomplete-wrapper-m2m.grp-state-focus,
.grp-autocomplete-wrapper-m2m {
background: rgba(255, 255, 255, 0.2);
border: none;
box-shadow: none;
}
.grp-autocomplete-wrapper-m2m ul.grp-repr li.grp-search {
background-color: #FDFDFD;
border: 1px solid #CCC;
}
.grp-autocomplete-wrapper-m2m ul.grp-repr li {
float: none;
display: block;
}
</style>
{% endblock %}

View File

@ -13,8 +13,8 @@
{% for k, v in item.attrs.items %} {% for k, v in item.attrs.items %}
{{ k }} = "{{ v|addslashes }}" {{ k }} = "{{ v|addslashes }}"
{% endfor %} > {% endfor %} >
{% if item.detail_url %} {% if item.url %}
<a href="{{ item.detail_url }}"> <a href="{{ item.url }}">
{% endif %} {% endif %}
{% if 'image' in list.fields and item.image %} {% if 'image' in list.fields and item.image %}
<img src="{% thumbnail item.image list.image_size crop %}"> <img src="{% thumbnail item.image list.image_size crop %}">
@ -65,7 +65,7 @@
</div> </div>
{% if item.detail_url %} {% if item.url %}
</a> </a>
{% endif %} {% endif %}
{% empty %} {% empty %}

View File

@ -16,7 +16,7 @@ def threads(post, sep = '/'):
posts.insert(0, post) posts.insert(0, post)
return sep.join([ return sep.join([
'<a href="{}">{}</a>'.format(post.detail_url(), post.title) '<a href="{}">{}</a>'.format(post.url(), post.title)
for post in posts if post.published for post in posts if post.published
]) ])

View File

@ -118,10 +118,11 @@ class PostListView(PostBaseView, ListView):
if not self.list: if not self.list:
self.list = sections.List( self.list = sections.List(
truncate = 32, truncate = 32,
paginate_by = 0,
fields = ['date', 'time', 'image', 'title', 'content'], fields = ['date', 'time', 'image', 'title', 'content'],
) )
else: else:
self.list = self.list() self.list = self.list(paginate_by = 0)
self.template_name = self.list.template_name self.template_name = self.list.template_name
self.css_class = self.list.css_class self.css_class = self.list.css_class

View File

@ -51,8 +51,7 @@ class Website:
self.set_menu(menu) self.set_menu(menu)
if self.comments_routes: if self.comments_routes:
self.register_comments_routes() self.register_comments()
def name_of_model(self, model): def name_of_model(self, model):
""" """
@ -62,7 +61,7 @@ class Website:
if model is _model: if model is _model:
return name return name
def register_comments_routes(self): def register_comments(self):
""" """
Register routes for comments, for the moment, only Register routes for comments, for the moment, only
ThreadRoute ThreadRoute

View File

@ -3,6 +3,6 @@ import aircox.liquidsoap.models as models
@admin.register(models.Output) @admin.register(models.Output)
class OutputAdmin (admin.ModelAdmin): class OutputAdmin (admin.ModelAdmin):
list_display = ('id', 'type', 'station') list_display = ('id', 'type')

View File

@ -96,7 +96,7 @@ class Monitor:
# - preload next diffusion's tracks # - preload next diffusion's tracks
args = {'start__gt': prev_diff.start } if prev_diff else {} args = {'start__gt': prev_diff.start } if prev_diff else {}
next_diff = programs.Diffusion \ next_diff = programs.Diffusion \
.get(controller.station, now, now = True, .get(now, now = True,
type = programs.Diffusion.Type.normal, type = programs.Diffusion.Type.normal,
sounds__isnull = False, sounds__isnull = False,
**args) \ **args) \
@ -194,30 +194,19 @@ class Command (BaseCommand):
help='write configuration and playlist' help='write configuration and playlist'
) )
group = parser.add_argument_group('selector')
group.add_argument( group.add_argument(
'-s', '--station', type=int, action='append', '-s', '--station', type=str,
help='select station(s) with this id' default = 'aircox',
) help='use this name as station name (default is "aircox")'
group.add_argument(
'-a', '--all', action='store_true',
help='select all stations'
) )
def handle (self, *args, **options): 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') run = options.get('run')
monitor = options.get('on_air') or options.get('monitor') monitor = options.get('on_air') or options.get('monitor')
self.controllers = [ utils.Controller(station, connector = monitor) self.controller = utils.Controller(
for station in stations ] station = options.get('station'),
connector = monitor
)
# actions # actions
if options.get('write') or run: if options.get('write') or run:

View File

@ -1,24 +1,19 @@
from enum import Enum, IntEnum
from django.db import models from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
import aircox.programs.models as programs
class Output (models.Model): class Output (models.Model):
# Note: we don't translate the names since it is project names. # Note: we don't translate the names since it is project names.
Type = { class Type(IntEnum):
'jack': 0x00, jack = 0x00
'alsa': 0x01, alsa = 0x01
'icecast': 0x02, icecast = 0x02
}
station = models.ForeignKey(
programs.Station,
verbose_name = _('station'),
)
type = models.SmallIntegerField( type = models.SmallIntegerField(
_('output type'), _('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 blank = True, null = True
) )
settings = models.TextField( settings = models.TextField(

View File

@ -295,12 +295,11 @@ class Controller:
files dir. files dir.
""" """
self.id = station.slug self.id = station.slug
self.name = station.name self.name = station
self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, station.slug) self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA,
slugify(station))
self.station = station self.outputs = models.Output.objects.all()
self.station.controller = self
self.outputs = models.Output.objects.filter(station = station)
self.connector = connector and Connector(self.socket_path) self.connector = connector and Connector(self.socket_path)
@ -310,8 +309,7 @@ class Controller:
source.id : source source.id : source
for source in [ for source in [
Source(self, program) Source(self, program)
for program in programs.Program.objects.filter(station = station, for program in programs.Program.objects.filter(active = True)
active = True)
if program.stream_set.count() if program.stream_set.count()
] ]
} }
@ -370,23 +368,3 @@ class Controller:
file.write(data) 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()

View File

@ -24,6 +24,8 @@ class StreamInline(admin.TabularInline):
model = Stream model = Stream
extra = 1 extra = 1
class SoundDiffInline(admin.TabularInline):
model = Diffusion.sounds.through
# from suit.admin import SortableTabularInline, SortableModelAdmin # from suit.admin import SortableTabularInline, SortableModelAdmin
#class TrackInline(SortableTabularInline): #class TrackInline(SortableTabularInline):
@ -45,11 +47,11 @@ class NameableAdmin(admin.ModelAdmin):
@admin.register(Sound) @admin.register(Sound)
class SoundAdmin(NameableAdmin): class SoundAdmin(NameableAdmin):
fields = None fields = None
list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed', 'public'] list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed']
fieldsets = [ fieldsets = [
(None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ), (None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ),
(None, { 'fields': ['embed', 'duration', 'mtime'] }), (None, { 'fields': ['embed', 'duration', 'mtime'] }),
(None, { 'fields': ['removed', 'good_quality', 'public' ] } ) (None, { 'fields': ['removed', 'good_quality' ] } )
] ]
readonly_fields = ('path', 'duration',) readonly_fields = ('path', 'duration',)
@ -59,10 +61,6 @@ class StreamAdmin(admin.ModelAdmin):
list_display = ('id', 'program', 'delay', 'begin', 'end') list_display = ('id', 'program', 'delay', 'begin', 'end')
@admin.register(Station)
class StationAdmin(NameableAdmin):
fields = NameableAdmin.fields + [ 'active', 'public', 'fallback' ]
@admin.register(Program) @admin.register(Program)
class ProgramAdmin(NameableAdmin): class ProgramAdmin(NameableAdmin):
def schedule(self, obj): def schedule(self, obj):
@ -113,8 +111,9 @@ class DiffusionAdmin(admin.ModelAdmin):
list_editable = ('type',) list_editable = ('type',)
ordering = ('-start', 'id') ordering = ('-start', 'id')
fields = ['type', 'start', 'end', 'initial', 'program', 'sounds'] fields = ['type', 'start', 'end', 'initial', 'program']
inlines = [ DiffusionInline ] inlines = [ DiffusionInline, SoundDiffInline ]
exclude = ('sounds',)
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):

View File

@ -141,11 +141,6 @@ class Sound(Nameable):
default = False, default = False,
help_text = _('sound\'s quality is okay') 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): def get_mtime(self):
""" """
@ -417,32 +412,6 @@ class Schedule(models.Model):
verbose_name_plural = _('Schedules') 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): class Program(Nameable):
""" """
A Program can either be a Streamed or a Scheduled program. 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 Renaming a Program rename the corresponding directory to matches the new
name if it does not exists. name if it does not exists.
""" """
station = models.ForeignKey(
Station,
verbose_name = _('station')
)
active = models.BooleanField( active = models.BooleanField(
_('active'), _('active'),
default = True, default = True,
@ -621,7 +586,7 @@ class Diffusion(models.Model):
return r return r
@classmethod @classmethod
def get(cl, station = None, date = None, def get(cl, date = None,
now = False, next = False, prev = False, now = False, next = False, prev = False,
queryset = None, queryset = None,
**filter_args): **filter_args):
@ -637,9 +602,6 @@ class Diffusion(models.Model):
""" """
#FIXME: conflicts? ( + calling functions) #FIXME: conflicts? ( + calling functions)
date = date_or_default(date) date = date_or_default(date)
if station:
filter_args['program__station'] = station
if queryset is None: if queryset is None:
queryset = cl.objects queryset = cl.objects

View File

@ -14,7 +14,7 @@ class TrackInline(SortableTabularInline):
sortable = 'position' sortable = 'position'
extra = 10 extra = 10
admin.site.register(models.Article, cms.PostAdmin)
admin.site.register(models.Program, cms.RelatedPostAdmin) admin.site.register(models.Program, cms.RelatedPostAdmin)
admin.site.register(models.Diffusion, cms.RelatedPostAdmin) admin.site.register(models.Diffusion, cms.RelatedPostAdmin)

View File

@ -1,11 +1,38 @@
import os
import logging
logger = logging.getLogger('aircox')
from django.db import models from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy 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 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): class Program (RelatedPost):
url = models.URLField(_('website'), blank=True, null=True) website = models.URLField(
_('website'),
blank=True, null=True
)
# rss = models.URLField() # rss = models.URLField()
email = models.EmailField( email = models.EmailField(
_('email'), blank=True, null=True, _('email'), blank=True, null=True,
@ -20,6 +47,7 @@ class Program (RelatedPost):
rel_to_post = True rel_to_post = True
auto_create = True auto_create = True
class Diffusion (RelatedPost): class Diffusion (RelatedPost):
class Relation: class Relation:
model = programs.Diffusion model = programs.Diffusion
@ -59,3 +87,52 @@ class Diffusion (RelatedPost):
return _('rerun of %(day)s') % { return _('rerun of %(day)s') % {
'day': self.related.initial.start.strftime('%A %d/%m') '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
)
)

View File

@ -9,6 +9,28 @@ import aircox.cms.sections as sections
import aircox.website.models as models 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): class Diffusions(sections.List):
""" """
Section that print diffusions. When rendering, if there is no post yet 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.__dict__.update(kwargs)
def get_diffs(self, **filter_args): def get_diffs(self, **filter_args):
qs = programs.Diffusion.objects.filter( qs = programs.Diffusion.objects.filter(
type = programs.Diffusion.Type.normal type = programs.Diffusion.Type.normal
) )
if self.object: 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: if filter_args:
qs = qs.filter(**filter_args).order_by('start') qs = qs.filter(**filter_args).order_by('start')
@ -72,6 +99,9 @@ class Diffusions(sections.List):
@property @property
def url(self): def url(self):
if not self.need_url():
return
if self.object: if self.object:
return models.Diffusion.route_url(routes.ThreadRoute, return models.Diffusion.route_url(routes.ThreadRoute,
pk = self.object.id, pk = self.object.id,
@ -114,6 +144,10 @@ class Playlist(sections.List):
for track in tracks ] for track in tracks ]
class Sounds(sections.List):
pass
class Schedule(Diffusions): class Schedule(Diffusions):
""" """
Render a list of diffusions in the form of a schedule Render a list of diffusions in the form of a schedule