forked from rc/aircox
fixes, updates
This commit is contained in:
parent
b3663b50b2
commit
4b5f908b3d
12
README.md
12
README.md
|
@ -2,9 +2,13 @@
|
||||||
Platform to manage radio programs, schedules, cms, etc. -- main test repo
|
Platform to manage radio programs, schedules, cms, etc. -- main test repo
|
||||||
|
|
||||||
# Applications
|
# Applications
|
||||||
* **programs**: core application that have all defined models
|
* **programs**: programs, episodes, schedules, sounds and tracks;
|
||||||
|
* **streams**: streams and diffusions, links with LiquidSoap;
|
||||||
# Note
|
* **website**: website rendering, using models defined by the previous apps;
|
||||||
We make the assumption that admin is used with autocomplete-light and django-suit
|
|
||||||
|
|
||||||
|
|
||||||
|
# Code and names conventions and uses
|
||||||
|
* absolute dates: datetime fields, named "begin" "end" for ranges and "date" otherwise
|
||||||
|
* time range: timefield name "duration"
|
||||||
|
* parents: when only one parent, named "parent", otherwise model/reference's name
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,20 @@ import copy
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from suit.admin import SortableTabularInline
|
from suit.admin import SortableTabularInline, SortableModelAdmin
|
||||||
|
from autocomplete_light.contrib.taggit_field import TaggitWidget, TaggitField
|
||||||
|
|
||||||
from programs.forms import *
|
from programs.forms import *
|
||||||
from programs.models import *
|
from programs.models import *
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Inlines
|
# Inlines
|
||||||
#
|
#
|
||||||
# TODO: inherits from the corresponding admin view
|
# TODO: inherits from the corresponding admin view
|
||||||
|
class SoundInline (admin.TabularInline):
|
||||||
|
model = Sound
|
||||||
|
|
||||||
|
|
||||||
class ScheduleInline (admin.TabularInline):
|
class ScheduleInline (admin.TabularInline):
|
||||||
model = Schedule
|
model = Schedule
|
||||||
extra = 1
|
extra = 1
|
||||||
|
@ -20,12 +24,11 @@ class ScheduleInline (admin.TabularInline):
|
||||||
|
|
||||||
class DiffusionInline (admin.TabularInline):
|
class DiffusionInline (admin.TabularInline):
|
||||||
model = Diffusion
|
model = Diffusion
|
||||||
fields = ('episode', 'type', 'begin', 'end', 'stream')
|
fields = ('episode', 'type', 'date', 'stream')
|
||||||
readonly_fields = ('begin', 'end', 'stream')
|
readonly_fields = ('date', 'stream')
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TrackInline (SortableTabularInline):
|
class TrackInline (SortableTabularInline):
|
||||||
fields = ['artist', 'title', 'tags', 'position']
|
fields = ['artist', 'title', 'tags', 'position']
|
||||||
form = TrackForm
|
form = TrackForm
|
||||||
|
@ -34,9 +37,6 @@ class TrackInline (SortableTabularInline):
|
||||||
extra = 10
|
extra = 10
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Parents
|
|
||||||
#
|
|
||||||
class MetadataAdmin (admin.ModelAdmin):
|
class MetadataAdmin (admin.ModelAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
( None, {
|
( None, {
|
||||||
|
@ -47,31 +47,27 @@ class MetadataAdmin (admin.ModelAdmin):
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def save_model (self, request, obj, form, change):
|
def save_model (self, request, obj, form, change):
|
||||||
|
# FIXME: if request.data.author?
|
||||||
if not obj.author:
|
if not obj.author:
|
||||||
obj.author = request.user
|
obj.author = request.user
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
from autocomplete_light.contrib.taggit_field import TaggitWidget, TaggitField
|
|
||||||
class PublicationAdmin (MetadataAdmin):
|
class PublicationAdmin (MetadataAdmin):
|
||||||
fieldsets = copy.deepcopy(MetadataAdmin.fieldsets)
|
fieldsets = copy.deepcopy(MetadataAdmin.fieldsets)
|
||||||
|
|
||||||
list_display = ('id', 'title', 'date', 'public', 'parent')
|
list_display = ('id', 'title', 'date', 'public', 'enumerable', 'parent')
|
||||||
list_filter = ['date', 'public', 'parent', 'author']
|
list_filter = ['date', 'public', 'parent', 'author']
|
||||||
|
list_editable = ('public', 'enumerable')
|
||||||
search_fields = ['title', 'content']
|
search_fields = ['title', 'content']
|
||||||
|
|
||||||
|
|
||||||
fieldsets[0][1]['fields'].insert(1, 'subtitle')
|
fieldsets[0][1]['fields'].insert(1, 'subtitle')
|
||||||
fieldsets[0][1]['fields'] += [ 'img', 'content' ]
|
fieldsets[0][1]['fields'] += [ 'img', 'content' ]
|
||||||
fieldsets[1][1]['fields'] += [ 'parent' ] #, 'meta' ],
|
fieldsets[1][1]['fields'] += [ 'parent' ] #, 'meta' ],
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Sound)
|
||||||
#
|
|
||||||
# ModelAdmin list
|
|
||||||
#
|
|
||||||
class SoundAdmin (MetadataAdmin):
|
class SoundAdmin (MetadataAdmin):
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, { 'fields': ['title', 'tags', 'path' ] } ),
|
(None, { 'fields': ['title', 'tags', 'path' ] } ),
|
||||||
|
@ -79,12 +75,21 @@ class SoundAdmin (MetadataAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Stream)
|
||||||
|
class StreamAdmin (SortableModelAdmin):
|
||||||
|
list_display = ('id', 'name', 'type', 'public', 'enumerable', 'priority')
|
||||||
|
list_editable = ('public', 'enumerable')
|
||||||
|
sortable = "priority"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Article)
|
||||||
class ArticleAdmin (PublicationAdmin):
|
class ArticleAdmin (PublicationAdmin):
|
||||||
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
||||||
|
|
||||||
fieldsets[1][1]['fields'] += ['static_page']
|
fieldsets[1][1]['fields'] += ['static_page']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Program)
|
||||||
class ProgramAdmin (PublicationAdmin):
|
class ProgramAdmin (PublicationAdmin):
|
||||||
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
||||||
inlines = [ ScheduleInline ]
|
inlines = [ ScheduleInline ]
|
||||||
|
@ -92,6 +97,7 @@ class ProgramAdmin (PublicationAdmin):
|
||||||
fieldsets[1][1]['fields'] += ['email', 'url']
|
fieldsets[1][1]['fields'] += ['email', 'url']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Episode)
|
||||||
class EpisodeAdmin (PublicationAdmin):
|
class EpisodeAdmin (PublicationAdmin):
|
||||||
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
|
||||||
list_filter = ['parent'] + PublicationAdmin.list_filter
|
list_filter = ['parent'] + PublicationAdmin.list_filter
|
||||||
|
@ -101,18 +107,20 @@ class EpisodeAdmin (PublicationAdmin):
|
||||||
inlines = (TrackInline, DiffusionInline)
|
inlines = (TrackInline, DiffusionInline)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Diffusion)
|
||||||
class DiffusionAdmin (admin.ModelAdmin):
|
class DiffusionAdmin (admin.ModelAdmin):
|
||||||
list_display = ('type', 'begin', 'end', 'episode', 'program', 'stream')
|
list_display = ('id', 'type', 'date', 'episode', 'program', 'stream')
|
||||||
list_filter = ('type', 'begin', 'program', 'stream')
|
list_filter = ('type', 'date', 'program', 'stream')
|
||||||
|
list_editable = ('type', 'date')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(DiffusionAdmin, self).get_queryset(request)
|
||||||
|
if 'type__exact' in request.GET and \
|
||||||
|
str(Diffusion.Type['unconfirmed']) in request.GET['type__exact']:
|
||||||
|
return qs
|
||||||
|
return qs.exclude(type = Diffusion.Type['unconfirmed'])
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Track)
|
admin.site.register(Track)
|
||||||
admin.site.register(Sound, SoundAdmin)
|
|
||||||
admin.site.register(Schedule)
|
admin.site.register(Schedule)
|
||||||
admin.site.register(Article, ArticleAdmin)
|
|
||||||
admin.site.register(Program, ProgramAdmin)
|
|
||||||
admin.site.register(Episode, EpisodeAdmin)
|
|
||||||
admin.site.register(Diffusion, DiffusionAdmin)
|
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,6 @@ class Command (BaseCommand):
|
||||||
parser.epilog += '\n ' + model.model.type() + ': \n' \
|
parser.epilog += '\n ' + model.model.type() + ': \n' \
|
||||||
+ model.to_string()
|
+ model.to_string()
|
||||||
|
|
||||||
|
|
||||||
def handle (self, *args, **options):
|
def handle (self, *args, **options):
|
||||||
model = options.get('model')
|
model = options.get('model')
|
||||||
if not model:
|
if not model:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.template.defaultfilters import slugify
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone as tz
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
# extensions
|
# extensions
|
||||||
|
@ -16,75 +16,42 @@ from taggit.managers import TaggableManager
|
||||||
import programs.settings as settings
|
import programs.settings as settings
|
||||||
|
|
||||||
|
|
||||||
|
def date_or_default (date, date_only = False):
|
||||||
# Frequency for schedules. Basically, it is a mask of bits where each bit is a
|
"""
|
||||||
# week. Bits > rank 5 are used for special schedules.
|
Return date or default value (now) if not defined, and remove time info
|
||||||
# Important: the first week is always the first week where the weekday of the
|
if date_only is True
|
||||||
# schedule is present.
|
"""
|
||||||
Frequency = {
|
date = date or tz.datetime.today()
|
||||||
'ponctual': 0b000000,
|
if not tz.is_aware(date):
|
||||||
'first': 0b000001,
|
date = tz.make_aware(date)
|
||||||
'second': 0b000010,
|
if date_only:
|
||||||
'third': 0b000100,
|
return date.replace(hour = 0, minute = 0, second = 0, microsecond = 0)
|
||||||
'fourth': 0b001000,
|
return date
|
||||||
'last': 0b010000,
|
|
||||||
'first and third': 0b000101,
|
|
||||||
'second and fourth': 0b001010,
|
|
||||||
'every': 0b011111,
|
|
||||||
'one on two': 0b100000,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Translators: html safe values
|
#class Model (models.Model):
|
||||||
ugettext_lazy('ponctual')
|
# @classmethod
|
||||||
ugettext_lazy('every')
|
# def type (cl):
|
||||||
ugettext_lazy('first')
|
# """
|
||||||
ugettext_lazy('second')
|
# Return a string with the type of the model (class name lowered)
|
||||||
ugettext_lazy('third')
|
# """
|
||||||
ugettext_lazy('fourth')
|
# name = cl.__name__.lower()
|
||||||
ugettext_lazy('first and third')
|
# return name
|
||||||
ugettext_lazy('second and fourth')
|
|
||||||
ugettext_lazy('one on two')
|
# @classmethod
|
||||||
|
# def name (cl, plural = False):
|
||||||
|
# """
|
||||||
|
# Return the name of the model using meta.verbose_name
|
||||||
|
# """
|
||||||
|
# if plural:
|
||||||
|
# return cl._meta.verbose_name_plural.title()
|
||||||
|
# return cl._meta.verbose_name.title()
|
||||||
|
#
|
||||||
|
# class Meta:
|
||||||
|
# abstract = True
|
||||||
|
|
||||||
|
|
||||||
DiffusionType = {
|
class Metadata (models.Model):
|
||||||
'diffuse': 0x01, # the diffusion is planified or done
|
|
||||||
'scheduled': 0x02, # the diffusion has been scheduled automatically
|
|
||||||
'cancel': 0x03, # the diffusion has been canceled from grid; useful to
|
|
||||||
# give the info to the users
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Model (models.Model):
|
|
||||||
@classmethod
|
|
||||||
def type (cl):
|
|
||||||
"""
|
|
||||||
Return a string with the type of the model (class name lowered)
|
|
||||||
"""
|
|
||||||
name = cl.__name__.lower()
|
|
||||||
return name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def type_plural (cl):
|
|
||||||
"""
|
|
||||||
Return a string with the name in plural of the model (cf. name())
|
|
||||||
"""
|
|
||||||
return cl.type() + 's'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def name (cl, plural = False):
|
|
||||||
"""
|
|
||||||
Return the name of the model using meta.verbose_name
|
|
||||||
"""
|
|
||||||
if plural:
|
|
||||||
return cl._meta.verbose_name_plural.title()
|
|
||||||
return cl._meta.verbose_name.title()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata (Model):
|
|
||||||
"""
|
"""
|
||||||
meta is used to extend a model for future needs
|
meta is used to extend a model for future needs
|
||||||
"""
|
"""
|
||||||
|
@ -99,7 +66,7 @@ class Metadata (Model):
|
||||||
)
|
)
|
||||||
date = models.DateTimeField(
|
date = models.DateTimeField(
|
||||||
_('date'),
|
_('date'),
|
||||||
default = timezone.datetime.now,
|
default = tz.datetime.now,
|
||||||
)
|
)
|
||||||
public = models.BooleanField(
|
public = models.BooleanField(
|
||||||
_('public'),
|
_('public'),
|
||||||
|
@ -170,7 +137,7 @@ class Publication (Metadata):
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
res[prefix + 'public'] = False
|
res[prefix + 'public'] = False
|
||||||
res[prefix + 'date__gt'] = timezone.now()
|
res[prefix + 'date__gt'] = tz.now()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -182,7 +149,7 @@ class Publication (Metadata):
|
||||||
Otherwise, return None
|
Otherwise, return None
|
||||||
"""
|
"""
|
||||||
kwargs['public'] = True
|
kwargs['public'] = True
|
||||||
kwargs['date__lte'] = timezone.now()
|
kwargs['date__lte'] = tz.now()
|
||||||
|
|
||||||
e = cl.objects.filter(**kwargs)
|
e = cl.objects.filter(**kwargs)
|
||||||
|
|
||||||
|
@ -197,8 +164,7 @@ class Publication (Metadata):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class Track (models.Model):
|
||||||
class Track (Model):
|
|
||||||
# There are no nice solution for M2M relations ship (even without
|
# There are no nice solution for M2M relations ship (even without
|
||||||
# through) in django-admin. So we unfortunately need to make one-
|
# through) in django-admin. So we unfortunately need to make one-
|
||||||
# to-one relations and add a position argument
|
# to-one relations and add a position argument
|
||||||
|
@ -250,6 +216,11 @@ class Sound (Metadata):
|
||||||
recursive = True,
|
recursive = True,
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
)
|
)
|
||||||
|
embed = models.TextField(
|
||||||
|
_('embed HTML code from external website'),
|
||||||
|
blank = True, null = True,
|
||||||
|
help_text = _('if set, consider the sound podcastable'),
|
||||||
|
)
|
||||||
duration = models.TimeField(
|
duration = models.TimeField(
|
||||||
_('duration'),
|
_('duration'),
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
|
@ -259,11 +230,6 @@ class Sound (Metadata):
|
||||||
default = False,
|
default = False,
|
||||||
help_text = _("the file has been cut"),
|
help_text = _("the file has been cut"),
|
||||||
)
|
)
|
||||||
embed = models.TextField(
|
|
||||||
_('embed HTML code from external website'),
|
|
||||||
blank = True, null = True,
|
|
||||||
help_text = _('if set, consider the sound podcastable'),
|
|
||||||
)
|
|
||||||
removed = models.BooleanField(
|
removed = models.BooleanField(
|
||||||
default = False,
|
default = False,
|
||||||
help_text = _('this sound has been removed from filesystem'),
|
help_text = _('this sound has been removed from filesystem'),
|
||||||
|
@ -274,8 +240,8 @@ class Sound (Metadata):
|
||||||
Get the last modification date from file
|
Get the last modification date from file
|
||||||
"""
|
"""
|
||||||
mtime = os.stat(self.path).st_mtime
|
mtime = os.stat(self.path).st_mtime
|
||||||
mtime = timezone.datetime.fromtimestamp(mtime)
|
mtime = tz.datetime.fromtimestamp(mtime)
|
||||||
return timezone.make_aware(mtime, timezone.get_current_timezone())
|
return tz.make_aware(mtime, timezone.get_current_timezone())
|
||||||
|
|
||||||
def save (self, *args, **kwargs):
|
def save (self, *args, **kwargs):
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
|
@ -290,15 +256,33 @@ class Sound (Metadata):
|
||||||
verbose_name_plural = _('Sounds')
|
verbose_name_plural = _('Sounds')
|
||||||
|
|
||||||
|
|
||||||
class Schedule (Model):
|
class Schedule (models.Model):
|
||||||
|
# Frequency for schedules. Basically, it is a mask of bits where each bit is
|
||||||
|
# a week. Bits > rank 5 are used for special schedules.
|
||||||
|
# Important: the first week is always the first week where the weekday of
|
||||||
|
# the schedule is present.
|
||||||
|
# For ponctual programs, there is no need for a schedule, only a diffusion
|
||||||
|
Frequency = {
|
||||||
|
'first': 0b000001,
|
||||||
|
'second': 0b000010,
|
||||||
|
'third': 0b000100,
|
||||||
|
'fourth': 0b001000,
|
||||||
|
'last': 0b010000,
|
||||||
|
'first and third': 0b000101,
|
||||||
|
'second and fourth': 0b001010,
|
||||||
|
'every': 0b011111,
|
||||||
|
'one on two': 0b100000,
|
||||||
|
}
|
||||||
|
for key, value in Frequency.items():
|
||||||
|
ugettext_lazy(key)
|
||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'Program',
|
'Program',
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
)
|
)
|
||||||
begin = models.DateTimeField(_('begin'))
|
date = models.DateTimeField(_('date'))
|
||||||
end = models.DateTimeField(
|
duration = models.TimeField(
|
||||||
_('end'),
|
_('duration'),
|
||||||
blank = True, null = True,
|
|
||||||
)
|
)
|
||||||
frequency = models.SmallIntegerField(
|
frequency = models.SmallIntegerField(
|
||||||
_('frequency'),
|
_('frequency'),
|
||||||
|
@ -310,16 +294,14 @@ class Schedule (Model):
|
||||||
help_text = "Schedule of a rerun",
|
help_text = "Schedule of a rerun",
|
||||||
)
|
)
|
||||||
|
|
||||||
def match (self, date = None, check_time = False):
|
def match (self, date = None, check_time = True):
|
||||||
"""
|
"""
|
||||||
Return True if the given datetime matches the schedule
|
Return True if the given datetime matches the schedule
|
||||||
"""
|
"""
|
||||||
if not date:
|
date = date_or_default(date)
|
||||||
date = timezone.datetime.today()
|
|
||||||
|
|
||||||
if self.date.weekday() == date.weekday() and self.match_week(date):
|
if self.date.weekday() == date.weekday() and self.match_week(date):
|
||||||
return (check_time and self.date.time() == date.date.time()) \
|
return self.date.time() == date.time() if check_time else True
|
||||||
or True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def match_week (self, date = None):
|
def match_week (self, date = None):
|
||||||
|
@ -328,17 +310,12 @@ class Schedule (Model):
|
||||||
otherwise.
|
otherwise.
|
||||||
If the schedule is ponctual, return None.
|
If the schedule is ponctual, return None.
|
||||||
"""
|
"""
|
||||||
if not date:
|
date = date_or_default(date)
|
||||||
date = timezone.datetime.today()
|
if self.frequency == Schedule.Frequency['one on two']:
|
||||||
|
|
||||||
if self.frequency == Frequency['ponctual']:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.frequency == Frequency['one on two']:
|
|
||||||
week = date.isocalendar()[1]
|
week = date.isocalendar()[1]
|
||||||
return (week % 2) == (self.date.isocalendar()[1] % 2)
|
return (week % 2) == (self.date.isocalendar()[1] % 2)
|
||||||
|
|
||||||
first_of_month = timezone.datetime.date(date.year, date.month, 1)
|
first_of_month = tz.datetime.date(date.year, date.month, 1)
|
||||||
week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
|
week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
|
||||||
|
|
||||||
# weeks of month
|
# weeks of month
|
||||||
|
@ -355,32 +332,23 @@ class Schedule (Model):
|
||||||
|
|
||||||
def dates_of_month (self, date = None):
|
def dates_of_month (self, date = None):
|
||||||
"""
|
"""
|
||||||
Return a list with all matching dates of the month of the given
|
Return a list with all matching dates of date.month (=today)
|
||||||
date (= today).
|
|
||||||
"""
|
"""
|
||||||
if self.frequency == Frequency['ponctual']:
|
date = date_or_default(date, True).replace(day=1)
|
||||||
return None
|
|
||||||
|
|
||||||
if not date:
|
|
||||||
date = timezone.datetime.today()
|
|
||||||
|
|
||||||
date = timezone.datetime(year = date.year, month = date.month, day = 1)
|
|
||||||
wday = self.date.weekday()
|
wday = self.date.weekday()
|
||||||
fwday = date.weekday()
|
fwday = date.weekday()
|
||||||
|
|
||||||
# move date to the date weekday of the schedule
|
# move date to the date weekday of the schedule
|
||||||
# check on SO#3284452 for the formula
|
# check on SO#3284452 for the formula
|
||||||
date += timezone.timedelta(
|
date += tz.timedelta(days = (7 if fwday > wday else 0) - fwday + wday)
|
||||||
days = (7 if fwday > wday else 0) - fwday + wday
|
|
||||||
)
|
|
||||||
fwday = date.weekday()
|
fwday = date.weekday()
|
||||||
|
|
||||||
# special frequency case
|
# special frequency case
|
||||||
weeks = self.frequency
|
weeks = self.frequency
|
||||||
if self.frequency == Frequency['last']:
|
if self.frequency == Schedule.Frequency['last']:
|
||||||
date += timezone.timedelta(month = 1, days = -7)
|
date += tz.timedelta(month = 1, days = -7)
|
||||||
return self.normalize([date])
|
return self.normalize([date])
|
||||||
if weeks == Frequency['one on two']:
|
if weeks == Schedule.Frequency['one on two']:
|
||||||
# if both week are the same, then the date week of the month
|
# if both week are the same, then the date week of the month
|
||||||
# matches. Note: wday % 2 + fwday % 2 => (wday + fwday) % 2
|
# matches. Note: wday % 2 + fwday % 2 => (wday + fwday) % 2
|
||||||
fweek = date.isocalendar()[1]
|
fweek = date.isocalendar()[1]
|
||||||
|
@ -392,61 +360,55 @@ class Schedule (Model):
|
||||||
# there can be five weeks in a month
|
# there can be five weeks in a month
|
||||||
if not weeks & (0b1 << week):
|
if not weeks & (0b1 << week):
|
||||||
continue
|
continue
|
||||||
|
wdate = date + tz.timedelta(days = week * 7)
|
||||||
wdate = date + timezone.timedelta(days = week * 7)
|
|
||||||
if wdate.month == date.month:
|
if wdate.month == date.month:
|
||||||
dates.append(self.normalize(wdate))
|
dates.append(self.normalize(wdate))
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
def diffusions_of_month (self, date = None, exclude_saved = False):
|
def diffusions_of_month (self, date, exclude_saved = False):
|
||||||
"""
|
"""
|
||||||
Return a list of generated (unsaved) diffusions for this program for the
|
Return a list of Diffusion instances, from month of the given date, that
|
||||||
month of the given date. If exclude_saved, exclude all diffusions that
|
can be not in the database.
|
||||||
are yet in the database.
|
|
||||||
|
|
||||||
When a diffusion is created, it tries to attach the corresponding
|
If exclude_saved, exclude all diffusions that are yet in the database.
|
||||||
episode.
|
|
||||||
|
When a Diffusion is created, it tries to attach the corresponding
|
||||||
|
episode using a match of episode.date (and takes care of rerun case);
|
||||||
"""
|
"""
|
||||||
if not date:
|
dates = self.dates_of_month(date)
|
||||||
date = timezone.datetime.today()
|
|
||||||
|
|
||||||
dates = self.dates_of_month()
|
|
||||||
saved = Diffusion.objects.filter(date__in = dates,
|
saved = Diffusion.objects.filter(date__in = dates,
|
||||||
program = self.parent)
|
program = self.parent)
|
||||||
diffusions = []
|
diffusions = []
|
||||||
|
|
||||||
# existing diffusions
|
# existing diffusions
|
||||||
for saved_item in saved:
|
for item in saved:
|
||||||
dates.remove(saved_item.date)
|
if item.date in dates:
|
||||||
|
dates.remove(item.date)
|
||||||
if not exclude_saved:
|
if not exclude_saved:
|
||||||
diffusions.append(saved_item)
|
diffusions.append(item)
|
||||||
|
|
||||||
# others
|
# others
|
||||||
for date in dates:
|
for date in dates:
|
||||||
# get episode
|
|
||||||
ep_date = date
|
ep_date = date
|
||||||
if self.rerun:
|
if self.rerun:
|
||||||
ep_date = self.rerun.date
|
ep_date -= self.date - self.rerun.date
|
||||||
|
|
||||||
episode = Episode.objects().filter(date = ep_date,
|
episode = Episode.objects.filter(date = date,
|
||||||
parent = self.parent)
|
parent = self.parent)
|
||||||
episode = episode[0] if episode.count() else None
|
episode = episode[0] if episode.count() else None
|
||||||
|
|
||||||
# make diffusion
|
diffusions.append(Diffusion(
|
||||||
diffusion = Diffusion(
|
episode = episode,
|
||||||
episode = episode,
|
program = self.parent,
|
||||||
program = self.parent,
|
stream = self.parent.stream,
|
||||||
type = DiffusionType['scheduled'],
|
type = Diffusion.Type['unconfirmed'],
|
||||||
begin = date,
|
date = date,
|
||||||
end = timezone.datetime.combine(date.date(), self.end.time()),
|
))
|
||||||
stream = settings.AIRCOX_SCHEDULED_STREAM
|
|
||||||
)
|
|
||||||
diffusion.program = self.program
|
|
||||||
diffusions.append(diffusion)
|
|
||||||
return diffusions
|
return diffusions
|
||||||
|
|
||||||
def __str__ (self):
|
def __str__ (self):
|
||||||
frequency = [ x for x,y in Frequency.items() if y == self.frequency ]
|
frequency = [ x for x,y in Schedule.Frequency.items()
|
||||||
|
if y == self.frequency ]
|
||||||
return self.parent.title + ': ' + frequency[0]
|
return self.parent.title + ': ' + frequency[0]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -454,7 +416,104 @@ class Schedule (Model):
|
||||||
verbose_name_plural = _('Schedules')
|
verbose_name_plural = _('Schedules')
|
||||||
|
|
||||||
|
|
||||||
|
class Diffusion (models.Model):
|
||||||
|
Type = {
|
||||||
|
'normal': 0x00, # simple diffusion (done/planed)
|
||||||
|
'unconfirmed': 0x01, # scheduled by the generator but not confirmed for diffusion
|
||||||
|
'cancel': 0x02, # cancellation happened; used to inform users
|
||||||
|
'restart': 0x03, # manual restart; used to remix/give up antenna
|
||||||
|
'stop': 0x04, # diffusion has been forced to stop
|
||||||
|
}
|
||||||
|
for key, value in Type.items():
|
||||||
|
ugettext_lazy(key)
|
||||||
|
|
||||||
|
episode = models.ForeignKey (
|
||||||
|
'Episode',
|
||||||
|
blank = True, null = True,
|
||||||
|
verbose_name = _('episode'),
|
||||||
|
)
|
||||||
|
program = models.ForeignKey (
|
||||||
|
'Program',
|
||||||
|
verbose_name = _('program'),
|
||||||
|
)
|
||||||
|
# program.stream can change, but not the stream;
|
||||||
|
stream = models.ForeignKey(
|
||||||
|
'Stream',
|
||||||
|
verbose_name = _('stream'),
|
||||||
|
default = 0,
|
||||||
|
help_text = 'stream id on which the diffusion happens',
|
||||||
|
)
|
||||||
|
type = models.SmallIntegerField(
|
||||||
|
verbose_name = _('type'),
|
||||||
|
choices = [ (y, x) for x,y in Type.items() ],
|
||||||
|
)
|
||||||
|
date = models.DateTimeField( _('start of the diffusion') )
|
||||||
|
|
||||||
|
def save (self, *args, **kwargs):
|
||||||
|
if self.episode: # FIXME self.episode or kwargs['episode']
|
||||||
|
self.program = self.episode.parent
|
||||||
|
# check type against stream's type
|
||||||
|
super(Diffusion, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__ (self):
|
||||||
|
return self.program.title + ' on ' + str(self.date) \
|
||||||
|
+ str(self.type)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Diffusion')
|
||||||
|
verbose_name_plural = _('Diffusions')
|
||||||
|
|
||||||
|
|
||||||
|
class Stream (models.Model):
|
||||||
|
Type = {
|
||||||
|
'random': 0x00, # selection using random function
|
||||||
|
'schedule': 0x01, # selection using schedule
|
||||||
|
}
|
||||||
|
for key, value in Type.items():
|
||||||
|
ugettext_lazy(key)
|
||||||
|
|
||||||
|
# FIXME: id as integer?
|
||||||
|
name = models.CharField(
|
||||||
|
_('name'),
|
||||||
|
max_length = 32,
|
||||||
|
blank = True,
|
||||||
|
null = True,
|
||||||
|
)
|
||||||
|
type = models.SmallIntegerField(
|
||||||
|
verbose_name = _('type'),
|
||||||
|
choices = [ (y, x) for x,y in Type.items() ],
|
||||||
|
)
|
||||||
|
# FIXME unique value / suit's orderable
|
||||||
|
#
|
||||||
|
priority = models.SmallIntegerField(
|
||||||
|
_('priority'),
|
||||||
|
default = 0,
|
||||||
|
help_text = _('priority of the stream')
|
||||||
|
)
|
||||||
|
public = models.BooleanField(
|
||||||
|
_('public'),
|
||||||
|
default = True,
|
||||||
|
help_text = _('content is public'),
|
||||||
|
)
|
||||||
|
enumerable = models.BooleanField(
|
||||||
|
_('enumerable'),
|
||||||
|
default = True,
|
||||||
|
help_text = _('publication is listable'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# get info for:
|
||||||
|
# - random lists
|
||||||
|
# - scheduled lists
|
||||||
|
# link between Streams and Programs:
|
||||||
|
# - hours range (non-stop)
|
||||||
|
# - stream/pgm
|
||||||
|
|
||||||
|
def __str__ (self):
|
||||||
|
return self.name + ' / ' + str(self.priority)
|
||||||
|
|
||||||
|
|
||||||
class Article (Publication):
|
class Article (Publication):
|
||||||
|
# FIXME: move to website?
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'self',
|
'self',
|
||||||
verbose_name = _('parent'),
|
verbose_name = _('parent'),
|
||||||
|
@ -482,6 +541,10 @@ class Program (Publication):
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
help_text = _('parent article'),
|
help_text = _('parent article'),
|
||||||
)
|
)
|
||||||
|
stream = models.ForeignKey(
|
||||||
|
Stream,
|
||||||
|
verbose_name = _('stream'),
|
||||||
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
_('email'),
|
_('email'),
|
||||||
max_length = 128,
|
max_length = 128,
|
||||||
|
@ -491,9 +554,10 @@ class Program (Publication):
|
||||||
_('website'),
|
_('website'),
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
)
|
)
|
||||||
non_stop = models.BooleanField(
|
active = models.BooleanField(
|
||||||
_('non-stop'),
|
_('inactive'),
|
||||||
default = False,
|
default = True,
|
||||||
|
help_text = _('if not set this program is no longer active')
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -507,21 +571,11 @@ class Program (Publication):
|
||||||
"""
|
"""
|
||||||
schedules = Schedule.objects.filter(parent = self)
|
schedules = Schedule.objects.filter(parent = self)
|
||||||
for schedule in schedules:
|
for schedule in schedules:
|
||||||
if schedule.match(date):
|
if schedule.match(date, check_time = False):
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Program')
|
|
||||||
verbose_name_plural = _('Programs')
|
|
||||||
|
|
||||||
|
|
||||||
class Episode (Publication):
|
class Episode (Publication):
|
||||||
# Note:
|
|
||||||
# We do not especially need a duration here, because even if an
|
|
||||||
# program's schedule can have specified durations, in practice this
|
|
||||||
# duration may vary. Furthermore, we want the users have to enter a
|
|
||||||
# minimum of values.
|
|
||||||
# Duration can be retrieved from the sound file if there is one.
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
Program,
|
Program,
|
||||||
verbose_name = _('parent'),
|
verbose_name = _('parent'),
|
||||||
|
@ -538,49 +592,3 @@ class Episode (Publication):
|
||||||
verbose_name_plural = _('Episodes')
|
verbose_name_plural = _('Episodes')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Diffusion (Model):
|
|
||||||
"""
|
|
||||||
Diffusion logs and planifications.
|
|
||||||
|
|
||||||
A diffusion is:
|
|
||||||
- scheduled: when it has been generated following programs' Schedule
|
|
||||||
- planified: when it has been generated manually/ponctually or scheduled
|
|
||||||
"""
|
|
||||||
episode = models.ForeignKey (
|
|
||||||
Episode,
|
|
||||||
blank = True, null = True,
|
|
||||||
verbose_name = _('episode'),
|
|
||||||
)
|
|
||||||
program = models.ForeignKey (
|
|
||||||
Program,
|
|
||||||
verbose_name = _('program'),
|
|
||||||
)
|
|
||||||
type = models.SmallIntegerField(
|
|
||||||
verbose_name = _('type'),
|
|
||||||
choices = [ (y, x) for x,y in DiffusionType.items() ],
|
|
||||||
)
|
|
||||||
begin = models.DateTimeField( _('start of the diffusion') )
|
|
||||||
end = models.DateTimeField(
|
|
||||||
_('end of the diffusion'),
|
|
||||||
blank = True, null = True,
|
|
||||||
)
|
|
||||||
stream = models.SmallIntegerField(
|
|
||||||
verbose_name = _('stream'),
|
|
||||||
default = 0,
|
|
||||||
help_text = 'stream id on which the diffusion happens',
|
|
||||||
)
|
|
||||||
|
|
||||||
def save (self, *args, **kwargs):
|
|
||||||
if self.episode:
|
|
||||||
self.program = self.episode.parent
|
|
||||||
super(Diffusion, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def __str__ (self):
|
|
||||||
return self.program.title + ' on ' + str(self.start) \
|
|
||||||
+ str(self.type)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Diffusion')
|
|
||||||
verbose_name_plural = _('Diffusions')
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Django>=1.9.0
|
Django>=1.9.0
|
||||||
taggit>=0.12.1
|
django-taggit>=0.12.1
|
||||||
sortedm2m>=1.0.2
|
django-autocomplete-light>=2.2.5
|
||||||
|
django-suit>=0.2.14
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user