diff --git a/programs/admin.py b/programs/admin.py index 1a24de2..0ba87b4 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -81,6 +81,7 @@ class SoundFileAdmin (MetadataAdmin): ] #inlines = [ EpisodeInline ] + inlines = [ EventInline ] @@ -93,16 +94,15 @@ class ArticleAdmin (PublicationAdmin): class ProgramAdmin (PublicationAdmin): fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) - prepopulated_fields = { 'tag': ('title',) } inlines = [ EpisodeInline, ScheduleInline ] - fieldsets[1][1]['fields'] += ['email', 'url', 'tag'] + fieldsets[1][1]['fields'] += ['email', 'url', 'non_stop'] class EpisodeAdmin (PublicationAdmin): fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) - inlines = [ EventInline, SoundFileInline ] + inlines = [ SoundFileInline ] list_filter = ['parent'] + PublicationAdmin.list_filter fieldsets[0][1]['fields'] += ['tracks'] diff --git a/programs/management/commands/add.py b/programs/management/commands/add.py new file mode 100644 index 0000000..58709fa --- /dev/null +++ b/programs/management/commands/add.py @@ -0,0 +1,143 @@ +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone +import programs.models as models + + +class Model: + # dict: key is the argument name, value is the constructor + required = {} + optional = {} + model = None + + + def __init__ (self, model, required = {}, optional = {}, post = None): + self.model = model + self.required = required + self.optional = optional + self.post = post + + + def check_or_raise (self, options): + for req in self.required: + if req not in options: + raise ValueError('required argument ' + req + ' is missing') + + + def get_kargs (self, options): + kargs = {} + + for i in self.required: + if options.get(i): + fn = self.required[i] + kargs[i] = fn(options[i]) + + for i in self.optional: + if options.get(i): + print(i, options) + fn = self.optional[i] + kargs[i] = fn(options[i]) + + return kargs + + + def make (self, options): + self.check_or_raise(options) + + kargs = self.get_kargs(options) + instance = self.model(**kargs) + instance.save() + + if self.post: + self.post(instance, options) + + print(instance.__dict__) + + +def DateTime (string): + dt = timezone.datetime.strptime(string, '%Y-%m-%d %H:%M:%S') + return timezone.make_aware(dt, timezone.get_current_timezone()) + + +def Time (string): + dt = timezone.datetime.strptime(string, '%H:%M') + return timezone.datetime.time(dt) + + + +def AddTags (instance, options): + if options.get('tags'): + instance.tags.add(*options['tags']) + + +models = { + 'program': Model( models.Program + , { 'title': str } + , { 'subtitle': str, 'can_comment': bool, 'date': DateTime + , 'parent_id': int, 'public': bool + , 'url': str, 'email': str, 'non_stop': bool + } + , AddTags + ) + , 'article': Model( models.Article + , { 'title': str } + , { 'subtitle': str, 'can_comment': bool, 'date': DateTime + , 'parent_id': int, 'public': bool + , 'static_page': bool, 'focus': bool + } + , AddTags + ) + , 'schedule': Model( models.Schedule + , { 'parent_id': int, 'date': DateTime, 'duration': Time + , 'frequency': int } + , { 'rerun': bool } + ) +} + + + +class Command (BaseCommand): + help="Add an element of the given model" + + + def add_arguments (self, parser): + parser.add_argument( 'model', type=str + , metavar="MODEL" + , help="model to add. It must be in [schedule,program,article]") + + # publication/generic + parser.add_argument('--parent_id', type=str) + parser.add_argument('--title', type=str) + parser.add_argument('--subtitle', type=str) + parser.add_argument('--can_comment',action='store_true') + parser.add_argument('--public', action='store_true') + parser.add_argument( '--date', type=str + , help='a valid date time (Y/m/d H:m:s') + parser.add_argument('--tags', type=str, nargs='+') + + # program + parser.add_argument('--url', type=str) + parser.add_argument('--email', type=str) + parser.add_argument('--non_stop', type=int) + + # article + parser.add_argument('--static_page',action='store_true') + parser.add_argument('--focus', action='store_true') + + # schedule + parser.add_argument('--duration', type=str) + parser.add_argument('--frequency', type=int) + parser.add_argument('--rerun', action='store_true') + + + def handle (self, *args, **options): + model = options.get('model') + if not model: + return + + model = model.lower() + if model not in models: + raise ValueError("model {} is not supported".format(str(model))) + + models[model].make(options) + + diff --git a/programs/management/commands/monitor.py b/programs/management/commands/monitor.py deleted file mode 100644 index 60ed99c..0000000 --- a/programs/management/commands/monitor.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -from django.core.management.base import BaseCommand, CommandError -import programs.models as models -import programs.settings - - -class Command (BaseCommand): - help= "Take a look at the programs directory to check on new podcasts" - - - def handle (self, *args, **options): - programs = models.Program.objects.filter(schedule__isnull = True) - - for program in programs: - self.scan(program, program.path + '/public', public = True) - self.scan(program, program.path + '/podcasts', embed = True) - self.scan(program, program.path + '/private') - - - def scan (self, program, path, public = False, embed = False): - try: - for filename in os.listdir(path): - long_filename = path + '/' + filename - - # check for new sound files - # stat the sound files - # match sound files against episodes - if not found, create it - # upload public podcasts to mixcloud if required - except: - pass - diff --git a/programs/management/commands/schedule.py b/programs/management/commands/schedule.py deleted file mode 100644 index 81f374a..0000000 --- a/programs/management/commands/schedule.py +++ /dev/null @@ -1,72 +0,0 @@ -import datetime - -from django.core.management.base import BaseCommand, CommandError -from django.utils import timezone, dateformat - -import programs.models as models -import programs.settings - - -class Diffusion: - ref = None - date_start = None - date_end = None - - def __init__ (self, ref, date_start, date_end): - self.ref = ref - self.date_start = date_start - self.date_end = date_end - - def __lt__ (self, d): - return self.date_start < d.date_start and \ - self.date_end < d.date_end - - - -class Command (BaseCommand): - help= "check sounds to diffuse" - - diffusions = set() - - def handle(self, *args, **options): - self.get_next_events() - self.get_next_episodes() - - for diffusion in self.diffusions: - print( diffusion.ref.__str__() - , diffusion.date_start - , diffusion.date_end) - - - - def get_next_episodes (self): - schedules = models.Schedule.objects.filter() - for schedule in schedules: - date = schedule.next_date() - if not date: - continue - - dt = datetime.timedelta( hours = schedule.duration.hour - , minutes = schedule.duration.minute - , seconds = schedule.duration.second ) - - ref = models.Episode.objects.filter(date = date)[:1] - if not ref: - ref = ( schedule.parent, ) - - diffusion = Diffusion(ref[0], date, date + dt) - self.diffusions.add(diffusion) - - - def get_next_events (self): - events = models.Event.objects.filter(date_end__gt = timezone.now(), - canceled = False) \ - .extra(order_by = ['date'])[:10] - for event in events: - diffusion = Diffusion(event, event.date, event.date_end) - self.diffusions.add(diffusion) - - - - - diff --git a/programs/models.py b/programs/models.py index 1667854..b542a1d 100755 --- a/programs/models.py +++ b/programs/models.py @@ -1,45 +1,38 @@ -import datetime +import os # django from django.db import models from django.contrib.auth.models import User from django.template.defaultfilters import slugify - -from django.http import HttpResponse, Http404 - from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType - -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _, ugettext_lazy from django.utils import timezone from django.utils.html import strip_tags -from django.conf import settings - # extensions from taggit.managers import TaggableManager - import programs.settings as settings -AFrequency = { - 'ponctual': 0x000000, - 'every week': 0b001111, - 'first week': 0x000001, - 'second week': 0x000010, - 'third week': 0x000100, - 'fourth week': 0x001000, - 'first and third': 0x000101, - 'second and fourth': 0x001010, - 'one week on two': 0x010010, - #'uneven week': 0x100000, - # TODO 'every day': 0x110000 +Frequency = { + 'ponctual': 0b000000 + , 'every week': 0b001111 + , 'first week': 0b000001 + , 'second week': 0b000010 + , 'third week': 0b000100 + , 'fourth week': 0b001000 + , 'first and third': 0b000101 + , 'second and fourth': 0b001010 + , 'one week on two': 0b010010 + #'uneven week': 0b100000 + # TODO 'every day': 0b110000 } + # Translators: html safe values ugettext_lazy('ponctual') ugettext_lazy('every week') @@ -52,10 +45,15 @@ ugettext_lazy('second and fourth') ugettext_lazy('one week on two') -Frequency = [ (y, x) for x,y in AFrequency.items() ] -RFrequency = { y: x for x,y in AFrequency.items() } +EventType = { + 'play': 0x02 # the sound is playing / planified to play + , 'cancel': 0x03 # the sound has been canceled from grid; useful to give + # the info to the users + , 'stop': 0x04 # the sound has been arbitrary stopped (non-stop or not) + , 'non-stop': 0x05 # the sound has been played as non-stop +#, 'streaming' +} -Frequency.sort(key = lambda e: e[0]) class Model (models.Model): @@ -106,7 +104,7 @@ class Metadata (Model): ) date = models.DateTimeField( _('date') - , default = datetime.datetime.now + , default = timezone.datetime.now ) public = models.BooleanField( _('public') @@ -190,12 +188,6 @@ class Publication (Metadata): # # Instance's methods # - def get_parent (self, raise_404 = False ): - if not parent and raise_404: - raise Http404 - return parent - - def get_parents ( self, order_by = "desc", include_fields = None ): """ Return an array of the parents of the item. @@ -262,10 +254,9 @@ class SoundFile (Metadata): , blank = True , null = True ) - file = models.FileField( + file = models.FileField( #FIXME: filefield _('file') - , upload_to = "data/tracks" - , blank = True + , upload_to = lambda i, f: SoundFile.__upload_path(i,f) ) duration = models.TimeField( _('duration') @@ -277,12 +268,24 @@ class SoundFile (Metadata): , default = False , help_text = _("the file has been cut") ) - embed = models.TextField ( + 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 __upload_path (self, filename): + if self.parent and self.parent.parent: + path = self.parent.parent.path + else: + path = settings.AIRCOX_SOUNDFILE_DEFAULT_DIR + return os.path.join(path, filename) def __str__ (self): @@ -294,29 +297,44 @@ class SoundFile (Metadata): verbose_name_plural = _('Sounds') - class Schedule (Model): parent = models.ForeignKey( 'Program', blank = True, null = True ) date = models.DateTimeField(_('start')) - duration = models.TimeField(_('duration')) - frequency = models.SmallIntegerField(_('frequency'), choices = Frequency) + duration = models.TimeField( + _('duration') + , blank = True + , null = True + ) + frequency = models.SmallIntegerField( + _('frequency') + , choices = [ (y, x) for x,y in Frequency.items() ] + ) rerun = models.BooleanField(_('rerun'), default = False) - def match_week (self, at = datetime.date.today()): + def match_date (self, at = timezone.datetime.today()): + """ + Return True if the given datetime matches the schedule + """ + if self.date.weekday() == at.weekday() and self.match_week(date): + return self.date.time() == at.date.time() + return False + + + def match_week (self, at = timezone.datetime.today()): """ Return True if the given week number matches the schedule, False otherwise. If the schedule is ponctual, return None. """ - if self.frequency == AFrequency['ponctual']: + if self.frequency == Frequency['ponctual']: return None - if self.frequency == AFrequency['one week on two']: + if self.frequency == Frequency['one week on two']: week = at.isocalendar()[1] return (week % 2) == (self.date.isocalendar()[1] % 2) - first_of_month = datetime.date(at.year, at.month, 1) + first_of_month = timezone.datetime.date(at.year, at.month, 1) week = at.isocalendar()[1] - first_of_month.isocalendar()[1] # weeks of month @@ -326,30 +344,29 @@ class Schedule (Model): return (self.frequency & (0b0001 << week) > 0) - - def next_date (self, at = datetime.date.today()): - if self.frequency == AFrequency['ponctual']: + def next_date (self, at = timezone.datetime.today()): + if self.frequency == Frequency['ponctual']: return None # first day of the week - date = at - datetime.timedelta( days = at.weekday() ) + date = at - timezone.timedelta( days = at.weekday() ) # for the next five week, we look for a matching week. # when found, add the number of day since de start of the # we need to test if the result is >= at for i in range(0,5): if self.match_week(date): - date_ = date + datetime.timedelta( days = self.date.weekday() ) + date_ = date + timezone.timedelta( days = self.date.weekday() ) if date_ >= at: # we don't want past events - return datetime.datetime(date_.year, date_.month, date_.day, + return timezone.datetime(date_.year, date_.month, date_.day, self.date.hour, self.date.minute) - date += datetime.timedelta( days = 7 ) + date += timezone.timedelta( days = 7 ) else: return None - def next_dates (self, at = datetime.date.today(), n = 52): + def next_dates (self, at = timezone.datetime.today(), n = 52): # we could have optimized this function, but since it should not # be use too often, we keep a more readable and easier to debug # solution @@ -362,20 +379,20 @@ class Schedule (Model): if not e: break res.append(e) - at = res[-1] + datetime.timedelta(days = 1) + at = res[-1] + timezone.timedelta(days = 1) return res - def to_string(self): - s = ugettext_lazy( RFrequency[self.frequency] ) - if self.rerun: - return s + ' (' + _('rerun') + ')' - return s - + #def to_string(self): + # s = ugettext_lazy( RFrequency[self.frequency] ) + # if self.rerun: + # return s + ' (' + _('rerun') + ')' + # return s def __str__ (self): - return self.parent.title + ': ' + RFrequency[self.frequency] + frequency = [ x for x,y in Frequency.items() if y == self.frequency ] + return self.parent.title + ': ' + frequency[0] class Meta: @@ -427,15 +444,17 @@ class Program (Publication): , blank = True , null = True ) - tag = models.CharField( - _('tag') - , max_length = 64 - , help_text = _('used in articles to refer to it') + non_stop = models.SmallIntegerField( + _('non-stop priority') + , help_text = _('this program can be used as non-stop') + , default = -1 ) @property def path(self): - return settings.AIRCOX_PROGRAMS_DATA + slugify(self.title + '_' + self.id) + return os.path.join( settings.AIRCOX_PROGRAMS_DIR + , slugify(self.title + '_' + str(self.id)) + ) class Meta: @@ -455,8 +474,6 @@ class Episode (Publication): parent = models.ForeignKey( Program , verbose_name = _('parent') - , blank = True - , null = True ) tracks = models.ManyToManyField( Track @@ -473,37 +490,22 @@ class Episode (Publication): class Event (Model): """ + Event logs and planification of a sound file """ - parent = models.ForeignKey ( - Episode - , verbose_name = _('episode') - , blank = True - , null = True + sound = models.ForeignKey ( + SoundFile + , verbose_name = _('sound file') ) - date = models.DateTimeField( _('date of start') ) - date_end = models.DateTimeField( - _('date of end') - , blank = True - , null = True - ) - public = models.BooleanField( - _('public') - , default = False - , help_text = _('publication is accessible to the public') + type = models.SmallIntegerField( + _('type') + , choices = [ (y, x) for x,y in EventType.items() ] ) + date = models.DateTimeField( _('date of event start') ) meta = models.TextField ( _('meta') , blank = True , null = True ) - canceled = models.BooleanField( _('canceled'), default = False ) - - - def testify (self): - parent = self.parent - self.parent.testify() - self.parent.date = self.date - return self.parent class Meta: diff --git a/programs/settings.py b/programs/settings.py index 334bff9..e03d6ad 100755 --- a/programs/settings.py +++ b/programs/settings.py @@ -1,10 +1,15 @@ +import os + from django.conf import settings def ensure (key, default): globals()[key] = getattr(settings, key, default) -ensure('AIRCOX_PROGRAMS_DATA', settings.MEDIA_ROOT + '/programs') +ensure('AIRCOX_PROGRAMS_DIR', + os.path.join(settings.MEDIA_ROOT, 'programs')) +ensure('AIRCOX_SOUNDFILE_DEFAULT_DIR', + os.path.join(AIRCOX_PROGRAMS_DIR + 'default')) diff --git a/programs/views.py b/programs/views.py index 8e0ebc8..c03618f 100755 --- a/programs/views.py +++ b/programs/views.py @@ -1,2 +1,76 @@ -from django.shortcuts import render +from django.shortcuts import render +from django.core.serializers.json import DjangoJSONEncoder +from django.utils import timezone, dateformat + +import programs.models as models +import programs.settings + + + +class EventList: + type = None + next = None + prev = None + at = None + count = None + + + def __init__ (self, **kwargs): + self.__dict__ = kwargs + if kwargs: + self.get_queryset() + + + def get_queryset (self): + events = models.Event.objects; + + if self.next: events = events.filter( date_end__ge = timezone.now() ) + elif self.prev: events = events.filter( date_end__le = timezone.now() ) + else: events = events.all() + + events = events.extra(order_by = ['date']) + if self.at: events = events[self.at:] + if self.count: events = events[:self.count] + + self.events = events + + + def raw_string(): + """ + Return a string with events rendered as raw + """ + res = [] + for event in events: + r = [ dateformat.format(event.date, "Y/m/d H:i:s") + , str(event.type) + , event.parent.file.path + , event.parent.file.url + ] + + res.push(' '.join(r)) + + return '\n'.join(res) + + + def json_string(): + import json + + res = [] + for event in events: + r = { + 'date': dateformat.format(event.date, "Y/m/d H:i:s") + , 'date_end': dateformat.format(event.date_end, "Y/m/d H:i:s") + , 'type': str(event.type) + , 'file_path': event.parent.file.path + , 'file_url': event.parent.file.url + } + + res.push(json.dumps(r)) + + return '\n'.join(res) + + + + +