code style + add non_stop field to episode

This commit is contained in:
bkfox 2015-08-26 10:58:24 +02:00
parent 682d8b9189
commit 7e511e5076
2 changed files with 247 additions and 286 deletions

View File

@ -3,7 +3,8 @@ Platform to manage radio programs, schedules, cms, etc. -- main test repo
# Applications # Applications
* **programs**: core application that have all defined models * **programs**: core application that have all defined models
* **monitor**: monitor file system for new podcasts and sounds
# Note
We make the assumption that admin is used with autocomplete-light and django-suit

View File

@ -22,16 +22,16 @@ import programs.settings as settings
# Important: the first week is always the first week where the weekday of the # Important: the first week is always the first week where the weekday of the
# schedule is present. # schedule is present.
Frequency = { Frequency = {
'ponctual': 0b000000 'ponctual': 0b000000,
, 'first': 0b000001 'first': 0b000001,
, 'second': 0b000010 'second': 0b000010,
, 'third': 0b000100 'third': 0b000100,
, 'fourth': 0b001000 'fourth': 0b001000,
, 'last': 0b010000 'last': 0b010000,
, 'first and third': 0b000101 'first and third': 0b000101,
, 'second and fourth': 0b001010 'second and fourth': 0b001010,
, 'every': 0b011111 'every': 0b011111,
, 'one on two': 0b100000 'one on two': 0b100000,
} }
@ -49,10 +49,10 @@ ugettext_lazy('one on two')
DiffusionType = { DiffusionType = {
'diffuse': 0x01 # the diffusion is planified or done 'diffuse': 0x01, # the diffusion is planified or done
, 'scheduled': 0x02 # the diffusion been scheduled automatically 'scheduled': 0x02, # the diffusion has been scheduled automatically
, 'cancel': 0x03 # the diffusion has been canceled from grid; useful to give 'cancel': 0x03, # the diffusion has been canceled from grid; useful to
# the info to the users # give the info to the users
} }
@ -73,7 +73,6 @@ class Model (models.Model):
""" """
return cl.type() + 's' return cl.type() + 's'
@classmethod @classmethod
def name (cl, plural = False): def name (cl, plural = False):
""" """
@ -83,77 +82,89 @@ class Model (models.Model):
return cl._meta.verbose_name_plural.title() return cl._meta.verbose_name_plural.title()
return cl._meta.verbose_name.title() return cl._meta.verbose_name.title()
class Meta: class Meta:
abstract = True abstract = True
class Metadata (Model): class Metadata (Model):
""" """
meta is used to extend a model for future needs meta is used to extend a model for future needs
""" """
author = models.ForeignKey ( author = models.ForeignKey (
User User,
, verbose_name = _('author') verbose_name = _('author'),
, blank = True blank = True,
, null = True null = True,
) )
title = models.CharField( title = models.CharField(
_('title') _('title'),
, max_length = 128 max_length = 128,
) )
date = models.DateTimeField( date = models.DateTimeField(
_('date') _('date'),
, default = timezone.datetime.now default = timezone.datetime.now,
) )
public = models.BooleanField( public = models.BooleanField(
_('public') _('public'),
, default = True default = True,
, help_text = _('publication is public') help_text = _('publication is public'),
) )
enumerable = models.BooleanField( enumerable = models.BooleanField(
_('enumerable') _('enumerable'),
, default = True default = True,
, help_text = _('publication is listable') help_text = _('publication is listable'),
) )
tags = TaggableManager( tags = TaggableManager(
_('tags') _('tags'),
, blank = True blank = True,
) )
class Meta: class Meta:
abstract = True abstract = True
class Publication (Metadata): class Publication (Metadata):
subtitle = models.CharField(
_('subtitle'),
max_length = 128,
blank = True,
)
img = models.ImageField(
_('image'),
upload_to = "images",
blank = True,
)
content = models.TextField(
_('content'),
blank = True,
)
commentable = models.BooleanField(
_('enable comments'),
default = True,
help_text = _('comments are enabled on this publication'),
)
def get_slug_name (self): def get_slug_name (self):
return slugify(self.title) return slugify(self.title)
def __str__ (self): def get_parents (self, order_by = "desc", include_fields = None):
return self.title + ' (' + str(self.id) + ')' """
Return an array of the parents of the item.
subtitle = models.CharField( If include_fields is an array of files to include.
_('subtitle') """
, max_length = 128 # TODO: fields included
, blank = True # FIXME: parameter name + container
) parents = [ self ]
img = models.ImageField( while parents[-1].parent:
_('image') parent = parents[-1].parent
, upload_to = "images" if parent not in parents:
, blank = True # avoid cycles
) parents.append(parent)
content = models.TextField( parents = parents[1:]
_('content')
, blank = True
)
commentable = models.BooleanField(
_('enable comments')
, default = True
, help_text = _('comments are enabled on this publication')
)
if order_by == 'desc':
return reversed(parents)
return parents
@staticmethod @staticmethod
def _exclude_args (allow_unpublished = False, prefix = ''): def _exclude_args (allow_unpublished = False, prefix = ''):
@ -165,7 +176,6 @@ class Publication (Metadata):
res[prefix + 'date__gt'] = timezone.now() res[prefix + 'date__gt'] = timezone.now()
return res return res
@classmethod @classmethod
def get_available (cl, first = False, **kwargs): def get_available (cl, first = False, **kwargs):
""" """
@ -183,70 +193,46 @@ class Publication (Metadata):
return (e and e[0]) or None return (e and e[0]) or None
return e or None return e or None
def __str__ (self):
def get_parents (self, order_by = "desc", include_fields = None): return self.title + ' (' + str(self.id) + ')'
"""
Return an array of the parents of the item.
If include_fields is an array of files to include.
"""
# TODO: fields included
# FIXME: parameter name + container
parents = [ self ]
while parents[-1].parent:
parent = parents[-1].parent
if parent not in parents:
# avoid cycles
parents.append(parent)
parents = parents[1:]
if order_by == 'desc':
return reversed(parents)
return parents
class Meta: class Meta:
abstract = True abstract = True
#
# Usable models
#
class Track (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
episode = models.ForeignKey( episode = models.ForeignKey(
'Episode' 'Episode',
, null = True null = True,
) )
artist = models.CharField( artist = models.CharField(
_('artist') _('artist'),
, max_length = 128 max_length = 128,
) )
title = models.CharField( title = models.CharField(
_('title') _('title'),
, max_length = 128 max_length = 128,
) )
tags = TaggableManager( blank = True ) tags = TaggableManager( blank = True )
# position can be used to specify a position in seconds # position can be used to specify a position in seconds for non-stop
# programs
position = models.SmallIntegerField( position = models.SmallIntegerField(
default = 0 default = 0,
, help_text=_('position in the playlist') help_text=_('position in the playlist'),
) )
def __str__(self): def __str__(self):
return ' '.join([self.artist, ':', self.title]) return ' '.join([self.artist, ':', self.title])
class Meta: class Meta:
verbose_name = _('Track') verbose_name = _('Track')
verbose_name_plural = _('Tracks') verbose_name_plural = _('Tracks')
class Sound (Metadata): class Sound (Metadata):
""" """
A Sound is the representation of a sound, that can be: A Sound is the representation of a sound, that can be:
@ -261,44 +247,34 @@ class Sound (Metadata):
Each sound file can be associated to a filesystem's file or an embedded Each sound file can be associated to a filesystem's file or an embedded
code (for external podcasts). code (for external podcasts).
""" """
def get_upload_path (self, filename): path = models.FilePathField(
if self.parent and self.parent.parent: _('file'),
path = self.parent.parent.path path = settings.AIRCOX_PROGRAMS_DIR,
else: match = '*(' + '|'.join(settings.AIRCOX_SOUNDFILE_EXT) + ')$',
path = settings.AIRCOX_SOUNDFILE_DEFAULT_DIR recursive = True,
return os.path.join(path, filename) blank = True,
null = True,
)
path = models.FilePathField( #FIXME: filefield duration = models.TimeField(
_('file') _('duration'),
, path = settings.AIRCOX_PROGRAMS_DIR blank = True,
, match = '*(' \ null = True,
+ '|'.join(settings.AIRCOX_SOUNDFILE_EXT) + ')$' )
, recursive = True fragment = models.BooleanField(
, blank = True _('incomplete sound'),
, null = True default = False,
) help_text = _("the file has been cut"),
duration = models.TimeField( )
_('duration') embed = models.TextField(
, blank = True _('embed HTML code from external website'),
, null = True blank = True,
) null = True,
fragment = models.BooleanField( help_text = _('if set, consider the sound podcastable'),
_('incomplete sound') )
, default = False removed = models.BooleanField(
, help_text = _("the file has been cut") default = False,
) help_text = _('this sound has been removed from filesystem'),
embed = models.TextField( )
_('embed HTML code from external website')
, blank = True
, null = True
, help_text = _('if set, consider the sound podcastable from there')
)
removed = models.BooleanField(
default = False
, help_text = _('this sound has been removed from filesystem')
)
def get_mtime (self): def get_mtime (self):
""" """
@ -308,47 +284,41 @@ class Sound (Metadata):
mtime = timezone.datetime.fromtimestamp(mtime) mtime = timezone.datetime.fromtimestamp(mtime)
return timezone.make_aware(mtime, timezone.get_current_timezone()) return timezone.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:
self.date = self.get_mtime() self.date = self.get_mtime()
super(Sound, self).save(*args, **kwargs) super(Sound, self).save(*args, **kwargs)
@staticmethod
def autocomplete_search_fields():
return ("id__iexact", "path__icontains", 'title__icontains')
def __str__ (self): def __str__ (self):
return '/'.join(self.path.split('/')[-3:]) return '/'.join(self.path.split('/')[-3:])
class Meta: class Meta:
verbose_name = _('Sound') verbose_name = _('Sound')
verbose_name_plural = _('Sounds') verbose_name_plural = _('Sounds')
class Schedule (Model): class Schedule (Model):
parent = models.ForeignKey( 'Program', blank = True, null = True ) parent = models.ForeignKey(
date = models.DateTimeField(_('start')) 'Program',
duration = models.TimeField( blank = True,
_('duration') null = True,
, blank = True )
, null = True begin = models.DateTimeField(_('begin'))
) end = models.DateTimeField(
frequency = models.SmallIntegerField( _('end'),
_('frequency') blank = True,
, choices = [ (y, x) for x,y in Frequency.items() ] null = True,
) )
rerun = models.ForeignKey( frequency = models.SmallIntegerField(
'self' _('frequency'),
, blank = True choices = [ (y, x) for x,y in Frequency.items() ],
, null = True )
, help_text = "Schedule of a rerun" rerun = models.ForeignKey(
) 'self',
blank = True,
null = True,
help_text = "Schedule of a rerun",
)
def match (self, date = None, check_time = False): def match (self, date = None, check_time = False):
""" """
@ -358,10 +328,10 @@ class Schedule (Model):
date = timezone.datetime.today() 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()) or True return (check_time and self.date.time() == date.date.time()) \
or True
return False return False
def match_week (self, date = None): def match_week (self, date = None):
""" """
Return True if the given week number matches the schedule, False Return True if the given week number matches the schedule, False
@ -387,14 +357,12 @@ class Schedule (Model):
return self.frequency == 0b1111 return self.frequency == 0b1111
return (self.frequency & (0b0001 << week) > 0) return (self.frequency & (0b0001 << week) > 0)
def normalize (self, date): def normalize (self, date):
""" """
Set the time of a datetime to the schedule's one Set the time of a datetime to the schedule's one
""" """
return date.replace( hour = self.date.hour return date.replace(hour = self.date.hour,
, minute = self.date.minute ) minute = self.date.minute)
def dates_of_month (self, date = None): def dates_of_month (self, date = None):
""" """
@ -407,9 +375,9 @@ class Schedule (Model):
if not date: if not date:
date = timezone.datetime.today() date = timezone.datetime.today()
date = timezone.datetime( year = date.year date = timezone.datetime(year = date.year,
, month = date.month month = date.month,
, day = 1 ) day = 1)
wday = self.date.weekday() wday = self.date.weekday()
fwday = date.weekday() fwday = date.weekday()
@ -417,7 +385,7 @@ class Schedule (Model):
# check on SO#3284452 for the formula # check on SO#3284452 for the formula
date += timezone.timedelta( date += timezone.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
@ -443,7 +411,6 @@ class Schedule (Model):
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 = None, exclude_saved = False):
""" """
Return a list of generated (unsaved) diffusions for this program for the Return a list of generated (unsaved) diffusions for this program for the
@ -456,11 +423,10 @@ class Schedule (Model):
if not date: if not date:
date = timezone.datetime.today() date = timezone.datetime.today()
diffusions = []
dates = self.dates_of_month() 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 = []
# existing diffusions # existing diffusions
for saved_item in saved: for saved_item in saved:
@ -475,124 +441,116 @@ class Schedule (Model):
if self.rerun: if self.rerun:
ep_date = self.rerun.date ep_date = self.rerun.date
episode = Episode.objects().filter( date = ep_date episode = Episode.objects().filter(date = ep_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 # make diffusion
diffusion = Diffusion( parent = episode diffusion = Diffusion(
, program = self.parent episode = episode,
, type = DiffusionType['diffuse'] program = self.parent,
, date = date type = DiffusionType['scheduled'],
, stream = settings.AIRCOX_SCHEDULED_STREAM begin = date,
, selfd = True end = timezone.datetime.combine(date.date(),
) self.end.time()),
stream = settings.AIRCOX_SCHEDULED_STREAM,
)
diffusion.program = self.program diffusion.program = self.program
diffusions.append(diffusion) 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 Frequency.items() if y == self.frequency ]
return self.parent.title + ': ' + frequency[0] return self.parent.title + ': ' + frequency[0]
class Meta: class Meta:
verbose_name = _('Schedule') verbose_name = _('Schedule')
verbose_name_plural = _('Schedules') verbose_name_plural = _('Schedules')
class Article (Publication): class Article (Publication):
parent = models.ForeignKey( parent = models.ForeignKey(
'self' 'self',
, verbose_name = _('parent') verbose_name = _('parent'),
, blank = True blank = True,
, null = True null = True,
, help_text = _('parent article') help_text = _('parent article'),
) )
static_page = models.BooleanField( static_page = models.BooleanField(
_('static page') _('static page'),
, default = False default = False,
) )
focus = models.BooleanField( focus = models.BooleanField(
_('article is focus') _('article is focus'),
, blank = True blank = True,
, default = False default = False,
) )
class Meta: class Meta:
verbose_name = _('Article') verbose_name = _('Article')
verbose_name_plural = _('Articles') verbose_name_plural = _('Articles')
class Program (Publication): class Program (Publication):
parent = models.ForeignKey( parent = models.ForeignKey(
Article Article,
, verbose_name = _('parent') verbose_name = _('parent'),
, blank = True blank = True,
, null = True null = True,
, help_text = _('parent article') help_text = _('parent article'),
) )
email = models.EmailField(
email = models.EmailField( _('email'),
_('email') max_length = 128,
, max_length = 128 null = True,
, null = True blank = True,
, blank = True )
) url = models.URLField(
_('website'),
url = models.URLField( blank = True,
_('website') null = True,
, blank = True )
, null = True non_stop = models.BooleanField(
) _('non-stop'),
default = False,
)
@property @property
def path (self): def path (self):
return os.path.join( settings.AIRCOX_PROGRAMS_DIR return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
, slugify(self.title + '_' + str(self.id)) slugify(self.title + '_' + str(self.id)) )
)
def find_schedule (self, date): def find_schedule (self, date):
""" """
Return the first schedule that matches a given date Return the first schedule that matches a given date
""" """
print(self)
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):
return schedule return schedule
class Meta: class Meta:
verbose_name = _('Program') verbose_name = _('Program')
verbose_name_plural = _('Programs') verbose_name_plural = _('Programs')
class Episode (Publication): class Episode (Publication):
# Note: # Note:
# We do not especially need a duration here, because even if an # We do not especially need a duration here, because even if an
# emussion's schedule can have specified durations, in practice this # program's schedule can have specified durations, in practice this
# duration may vary. Furthermore, we want the users have to enter a # duration may vary. Furthermore, we want the users have to enter a
# minimum of values. # minimum of values.
# Duration can be retrieved from the sound file if there is one. # Duration can be retrieved from the sound file if there is one.
# parent = models.ForeignKey(
# FIXME: ponctual replays? Program,
parent = models.ForeignKey( verbose_name = _('parent'),
Program help_text = _('parent program'),
, verbose_name = _('parent') )
, help_text = _('parent program') sounds = models.ManyToManyField(
) Sound,
sounds = models.ManyToManyField( blank = True,
Sound verbose_name = _('sounds'),
, blank = True )
, verbose_name = _('sounds')
)
class Meta: class Meta:
verbose_name = _('Episode') verbose_name = _('Episode')
@ -608,28 +566,31 @@ class Diffusion (Model):
- scheduled: when it has been generated following programs' Schedule - scheduled: when it has been generated following programs' Schedule
- planified: when it has been generated manually/ponctually or scheduled - planified: when it has been generated manually/ponctually or scheduled
""" """
episode = models.ForeignKey ( episode = models.ForeignKey (
Episode Episode,
, blank = True blank = True,
, null = True null = True,
, verbose_name = _('episode') verbose_name = _('episode'),
) )
program = models.ForeignKey ( program = models.ForeignKey (
Program Program,
, verbose_name = _('program') verbose_name = _('program'),
) )
type = models.SmallIntegerField( type = models.SmallIntegerField(
verbose_name = _('type') verbose_name = _('type'),
, choices = [ (y, x) for x,y in DiffusionType.items() ] choices = [ (y, x) for x,y in DiffusionType.items() ],
) )
begin = models.DateTimeField( _('start of diffusion start') ) begin = models.DateTimeField( _('start of the diffusion') )
end = models.DateTimeField( _('stop of diffusion stop') ) end = models.DateTimeField(
stream = models.SmallIntegerField( _('end of the diffusion'),
verbose_name = _('stream') blank = True,
, default = 0 null = True,
, help_text = 'stream id on which the diffusion happens' )
) stream = models.SmallIntegerField(
verbose_name = _('stream'),
default = 0,
help_text = 'stream id on which the diffusion happens',
)
def save (self, *args, **kwargs): def save (self, *args, **kwargs):
if self.episode: if self.episode:
@ -640,7 +601,6 @@ class Diffusion (Model):
return self.program.title + ' on ' + str(self.start) \ return self.program.title + ' on ' + str(self.start) \
+ str(self.type) + str(self.type)
class Meta: class Meta:
verbose_name = _('Diffusion') verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions') verbose_name_plural = _('Diffusions')