schedules algorithms
This commit is contained in:
parent
43ef2b390c
commit
8d8bc71572
|
@ -81,7 +81,7 @@ class SoundFileAdmin (MetadataAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
#inlines = [ EpisodeInline ]
|
#inlines = [ EpisodeInline ]
|
||||||
inlines = [ EventInline ]
|
#inlines = [ EventInline ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user