schedules algorithms

This commit is contained in:
bkfox 2015-08-18 11:27:12 +02:00
parent 43ef2b390c
commit 8d8bc71572
5 changed files with 202 additions and 135 deletions

View File

@ -81,7 +81,7 @@ class SoundFileAdmin (MetadataAdmin):
] ]
#inlines = [ EpisodeInline ] #inlines = [ EpisodeInline ]
inlines = [ EventInline ] #inlines = [ EventInline ]

View File

@ -1,4 +1,5 @@
import argparse import argparse
import json
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone from django.utils import timezone
@ -110,7 +111,7 @@ class Model:
v = getattr(item, f) v = getattr(item, f)
if hasattr(v, 'id'): if hasattr(v, 'id'):
v = v.id v = v.id
r.append(str(v).ljust(len(f))) r.append(v)
items.append(r) items.append(r)
if options.get('head'): if options.get('head'):
@ -118,6 +119,13 @@ class Model:
elif options.get('tail'): elif options.get('tail'):
items = items[-options.get('tail'):] items = items[-options.get('tail'):]
if options.get('json'):
if options.get('fields'):
print(json.dumps(fields))
print(json.dumps(items, default = lambda x: str(x)))
return
if options.get('fields'):
print(' || '.join(fields)) print(' || '.join(fields))
for item in items: for item in items:
print(' || '.join(item)) print(' || '.join(item))
@ -165,7 +173,7 @@ models = {
, 'schedule': Model( models.Schedule , 'schedule': Model( models.Schedule
, { 'parent_id': int, 'date': DateTime, 'duration': Time , { 'parent_id': int, 'date': DateTime, 'duration': Time
, 'frequency': int } , 'frequency': int }
, { 'rerun': bool } , { 'rerun': int } # FIXME: redo
) )
, 'soundfile': Model( models.SoundFile , 'soundfile': Model( models.SoundFile
, { 'parent_id': int, 'date': DateTime, 'file': str , { 'parent_id': int, 'date': DateTime, 'file': str
@ -194,6 +202,8 @@ class Command (BaseCommand):
group.add_argument('--add', action='store_true' group.add_argument('--add', action='store_true'
, help='create or update (if id is given) object') , help='create or update (if id is given) object')
group.add_argument('--delete', action='store_true') group.add_argument('--delete', action='store_true')
group.add_argument('--json', action='store_true'
, help='dump using json')
group = parser.add_argument_group('selector') group = parser.add_argument_group('selector')
@ -207,6 +217,10 @@ class Command (BaseCommand):
group.add_argument('--tail', type=int group.add_argument('--tail', type=int
, help='dump the TAIL last objects only' , help='dump the TAIL last objects only'
) )
group.add_argument('--fields', action='store_true'
, help='print fields before dumping'
)
# publication/generic # publication/generic
group = parser.add_argument_group('fields' group = parser.add_argument_group('fields'
@ -232,7 +246,7 @@ class Command (BaseCommand):
# schedule # schedule
group.add_argument('--duration', type=str) group.add_argument('--duration', type=str)
group.add_argument('--frequency', type=int) group.add_argument('--frequency', type=int)
group.add_argument('--rerun', action='store_true') group.add_argument('--rerun', type=int)
# fields # fields
parser.formatter_class=argparse.RawDescriptionHelpFormatter parser.formatter_class=argparse.RawDescriptionHelpFormatter
@ -258,7 +272,7 @@ class Command (BaseCommand):
models[model].make(options) models[model].make(options)
elif options.get('delete'): elif options.get('delete'):
models[model].delete(options) models[model].delete(options)
else: else: # --dump --json
models[model].dump(options) models[model].dump(options)

View File

@ -17,41 +17,42 @@ import programs.settings as settings
# 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.
Frequency = { Frequency = {
'ponctual': 0b000000 'ponctual': 0b000000
, 'every week': 0b001111 , 'first': 0b000001
, 'first week': 0b000001 , 'second': 0b000010
, 'second week': 0b000010 , 'third': 0b000100
, 'third week': 0b000100 , 'fourth': 0b001000
, 'fourth week': 0b001000 , 'last': 0b010000
, 'first and third': 0b000101 , 'first and third': 0b000101
, 'second and fourth': 0b001010 , 'second and fourth': 0b001010
, 'one week on two': 0b010010 , 'every': 0b011111
#'uneven week': 0b100000 , 'one on two': 0b100000
# TODO 'every day': 0b110000
} }
# Translators: html safe values # Translators: html safe values
ugettext_lazy('ponctual') ugettext_lazy('ponctual')
ugettext_lazy('every week') ugettext_lazy('every')
ugettext_lazy('first week') ugettext_lazy('first')
ugettext_lazy('second week') ugettext_lazy('second')
ugettext_lazy('third week') ugettext_lazy('third')
ugettext_lazy('fourth week') ugettext_lazy('fourth')
ugettext_lazy('first and third') ugettext_lazy('first and third')
ugettext_lazy('second and fourth') ugettext_lazy('second and fourth')
ugettext_lazy('one week on two') ugettext_lazy('one on two')
EventType = { EventType = {
'play': 0x02 # the sound is playing / planified to play 'diffuse': 0x01 # the diffusion is planified or done
, 'cancel': 0x03 # the sound has been canceled from grid; useful to give , 'cancel': 0x03 # the diffusion has been canceled from grid; useful to give
# the info to the users # the info to the users
, 'stop': 0x04 # the sound has been arbitrary stopped (non-stop or not) , 'stop': 0x04 # the diffusion been arbitrary stopped (non-stop or not)
, 'non-stop': 0x05 # the sound has been played as non-stop
#, 'streaming'
} }
@ -109,8 +110,9 @@ class Metadata (Model):
public = models.BooleanField( public = models.BooleanField(
_('public') _('public')
, default = False , default = False
, help_text = _('publication is accessible to the public') , help_text = _('publication is public')
) )
# FIXME: add a field to specify if the element should be listed or not
meta = models.TextField( meta = models.TextField(
_('meta') _('meta')
, blank = True , blank = True
@ -325,33 +327,44 @@ class Schedule (Model):
_('frequency') _('frequency')
, choices = [ (y, x) for x,y in Frequency.items() ] , choices = [ (y, x) for x,y in Frequency.items() ]
) )
rerun = models.BooleanField(_('rerun'), default = False) rerun = models.ForeignKey(
'self'
, blank = True
, null = True
, help_text = "Schedule of a rerun"
)
def match_date (self, at = timezone.datetime.today(), check_time = False): def match (self, date = None, check_time = False):
""" """
Return True if the given datetime matches the schedule Return True if the given datetime matches the schedule
""" """
if self.date.weekday() == at.weekday() and self.match_week(at): if not date:
return (check_time and self.date.time() == at.date.time()) or True date = timezone.datetime.today()
if self.date.weekday() == date.weekday() and self.match_week(date):
return (check_time and self.date.time() == date.date.time()) or True
return False return False
def match_week (self, at = timezone.datetime.today()): 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
otherwise. otherwise.
If the schedule is ponctual, return None. If the schedule is ponctual, return None.
""" """
if not date:
date = timezone.datetime.today()
if self.frequency == Frequency['ponctual']: if self.frequency == Frequency['ponctual']:
return None return None
if self.frequency == Frequency['one week on two']: if self.frequency == Frequency['one on two']:
week = at.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(at.year, at.month, 1) first_of_month = timezone.datetime.date(date.year, date.month, 1)
week = at.isocalendar()[1] - first_of_month.isocalendar()[1] week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
# weeks of month # weeks of month
if week == 4: if week == 4:
@ -360,52 +373,62 @@ class Schedule (Model):
return (self.frequency & (0b0001 << week) > 0) return (self.frequency & (0b0001 << week) > 0)
def next_date (self, at = timezone.datetime.today()): def normalize (self, date):
"""
Set the time of a datetime to the schedule's one
"""
return date.replace( hour = self.date.hour
, minute = self.date.minute )
def dates_of_month (self, date = None):
"""
Return a list with all matching dates of the month of the given
date (= today).
"""
if self.frequency == Frequency['ponctual']: if self.frequency == Frequency['ponctual']:
return None return None
# first day of the week if not date:
date = at - timezone.timedelta( days = at.weekday() ) date = timezone.datetime.today()
# for the next five week, we look for a matching week. date = timezone.datetime( year = date.year
# when found, add the number of day since de start of the , month = date.month
# we need to test if the result is >= at , day = 1 )
for i in range(0,5): wday = self.date.weekday()
if self.match_week(date): fwday = date.weekday()
date_ = date + timezone.timedelta( days = self.date.weekday() )
if date_ >= at:
# we don't want past events
return timezone.datetime(date_.year, date_.month, date_.day,
self.date.hour, self.date.minute)
date += timezone.timedelta( days = 7 )
else:
return None
# move date to the date weekday of the schedule
# check on SO#3284452 for the formula
date += timezone.timedelta(
days = (7 if fwday > wday else 0) - fwday + wday
)
fwday = date.weekday()
def next_dates (self, at = timezone.datetime.today(), n = 52): # special frequency case
# we could have optimized this function, but since it should not weeks = self.frequency
# be use too often, we keep a more readable and easier to debug if self.frequency == Frequency['last']:
# solution date += timezone.timedelta(month = 1, days = -7)
if self.frequency == 0b000000: return self.normalize([date])
return None if weeks == Frequency['one on two']:
# if both week are the same, then the date week of the month
# matches. Note: wday % 2 + fwday % 2 => (wday + fwday) % 2
fweek = date.isocalendar()[1]
week = self.date.isocalendar()[1]
weeks = 0b010101 if not (fweek + week) % 2 else 0b001010
res = [] dates = []
for i in range(n): for week in range(0,5):
e = self.next_date(at) # NB: there can be five weeks in a month
if not e: if not weeks & (0b1 << week):
break continue
res.append(e)
at = res[-1] + timezone.timedelta(days = 1)
return res wdate = date + timezone.timedelta(days = week * 7)
if wdate.month == date.month:
dates.append(self.normalize(wdate))
return dates
#def to_string(self):
# s = ugettext_lazy( RFrequency[self.frequency] )
# if self.rerun:
# return s + ' (' + _('rerun') + ')'
# return s
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]
@ -498,6 +521,7 @@ class Episode (Publication):
# 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.
# #
# FIXME: ponctual replays?
parent = models.ForeignKey( parent = models.ForeignKey(
Program Program
, verbose_name = _('parent') , verbose_name = _('parent')
@ -517,23 +541,35 @@ class Episode (Publication):
class Event (Model): class Event (Model):
""" """
Event logs and planification of a sound file Event logs and planifications.
An event is:
- scheduled: when it has been generated following programs' Schedule
- planified: when it has been generated manually/ponctually or scheduled
""" """
sound = models.ForeignKey ( parent = models.ForeignKey (
SoundFile Episode
, verbose_name = _('sound file') , blank = True
, null = True
)
program = models.ForeignKey (
Program
) )
type = models.SmallIntegerField( type = models.SmallIntegerField(
_('type') _('type')
, choices = [ (y, x) for x,y in EventType.items() ] , choices = [ (y, x) for x,y in EventType.items() ]
) )
date = models.DateTimeField( _('date of event start') ) date = models.DateTimeField( _('date of event start') )
meta = models.TextField ( stream = models.SmallIntegerField(
_('meta') _('stream')
, blank = True , default = 0
, null = True , help_text = 'stream id on which the event happens'
)
scheduled = models.BooleanField(
_('automated')
, default = False
, help_text = 'event generated automatically'
) )
class Meta: class Meta:
verbose_name = _('Event') verbose_name = _('Event')

View File

@ -6,11 +6,19 @@ def ensure (key, default):
globals()[key] = getattr(settings, key, default) globals()[key] = getattr(settings, key, default)
# Directory for the programs data
ensure('AIRCOX_PROGRAMS_DIR', ensure('AIRCOX_PROGRAMS_DIR',
os.path.join(settings.MEDIA_ROOT, 'programs')) os.path.join(settings.MEDIA_ROOT, 'programs'))
# Default directory for the soundfiles
ensure('AIRCOX_SOUNDFILE_DEFAULT_DIR', ensure('AIRCOX_SOUNDFILE_DEFAULT_DIR',
os.path.join(AIRCOX_PROGRAMS_DIR + 'default')) os.path.join(AIRCOX_PROGRAMS_DIR + 'default'))
# Extension of sound files
ensure('AIRCOX_SOUNDFILE_EXT', ensure('AIRCOX_SOUNDFILE_EXT',
('ogg','flac','wav','mp3','opus')) ('ogg','flac','wav','mp3','opus'))
# Stream for the scheduled events
ensure('AIRCOX_SCHEDULED_STREAM', 0)

View File

@ -1,43 +1,52 @@
from django.utils import timezone from django.utils import timezone
from programs.models import Schedule, Event, Episode,\ from programs.models import Schedule, Event, Episode,\
SoundFile, Frequency EventType
def update_scheduled_events (date): def scheduled_month_events (date = None, unsaved_only = False):
""" """
Update planified events from schedules Return a list of scheduled events for the month of the given date. For the
TODO: notification in case of conflicts? non existing events, a program attribute to the corresponding program is
set.
""" """
all_schedules = Schedule.objects().all() if not date:
schedules = [ schedule date = timezone.datetime.today()
for schedule in models.Schedule.objects().all()
if schedule.match-date(date) ]
schedules.sort(key = lambda e: e.date) schedules = Schedule.objects.all()
events = []
for schedule in schedules: for schedule in schedules:
if schedule.frequency == Frequency['ponctual']: dates = schedule.dates_of_month()
for date in dates:
event = Event.objects \
.filter(date = date, parent__parent = schedule.parent)
if event.count():
if not unsaved_only:
events.append(event)
continue continue
ev_date = timezone.datetime(date.year, date.month, date.day, # get episode
schedule.date.hour, schedule.date.minute) ep_date = date
# if event exists, pass
n = Event.objects() \
.filter(date = ev_date, parent__parent = schedule.parent) \
.count()
if n:
continue
ep_date = ev_date
# rerun?
if schedule.rerun: if schedule.rerun:
schedule = schedule.rerun ep_date = schedule.rerun.date
date_ = schedule.date
episode = Episode.objects().filter(date = date) episode = Episode.objects().filter( date = ep_date
, parent = schedule.parent )
episode = episode[0] if episode.count() else None
# make event
event = Event( parent = episode
, program = schedule.parent
, type = EventType['diffuse']
, date = date
, stream = settings.AIRCOX_SCHEDULED_STREAM
, scheduled = True
)
event.program = schedule.program
events.append(event)
return events