new management tool: add

This commit is contained in:
bkfox 2015-06-17 14:57:47 +02:00
parent 57f9159dbb
commit f7d36467f0
7 changed files with 317 additions and 197 deletions

View File

@ -81,6 +81,7 @@ class SoundFileAdmin (MetadataAdmin):
] ]
#inlines = [ EpisodeInline ] #inlines = [ EpisodeInline ]
inlines = [ EventInline ]
@ -93,16 +94,15 @@ class ArticleAdmin (PublicationAdmin):
class ProgramAdmin (PublicationAdmin): class ProgramAdmin (PublicationAdmin):
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
prepopulated_fields = { 'tag': ('title',) }
inlines = [ EpisodeInline, ScheduleInline ] inlines = [ EpisodeInline, ScheduleInline ]
fieldsets[1][1]['fields'] += ['email', 'url', 'tag'] fieldsets[1][1]['fields'] += ['email', 'url', 'non_stop']
class EpisodeAdmin (PublicationAdmin): class EpisodeAdmin (PublicationAdmin):
fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) fieldsets = copy.deepcopy(PublicationAdmin.fieldsets)
inlines = [ EventInline, SoundFileInline ] inlines = [ SoundFileInline ]
list_filter = ['parent'] + PublicationAdmin.list_filter list_filter = ['parent'] + PublicationAdmin.list_filter
fieldsets[0][1]['fields'] += ['tracks'] fieldsets[0][1]['fields'] += ['tracks']

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -1,45 +1,38 @@
import datetime import os
# django # django
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.http import HttpResponse, Http404
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.shortcuts import get_object_or_404
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
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.conf import settings
# extensions # extensions
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
import programs.settings as settings import programs.settings as settings
AFrequency = { Frequency = {
'ponctual': 0x000000, 'ponctual': 0b000000
'every week': 0b001111, , 'every week': 0b001111
'first week': 0x000001, , 'first week': 0b000001
'second week': 0x000010, , 'second week': 0b000010
'third week': 0x000100, , 'third week': 0b000100
'fourth week': 0x001000, , 'fourth week': 0b001000
'first and third': 0x000101, , 'first and third': 0b000101
'second and fourth': 0x001010, , 'second and fourth': 0b001010
'one week on two': 0x010010, , 'one week on two': 0b010010
#'uneven week': 0x100000, #'uneven week': 0b100000
# TODO 'every day': 0x110000 # 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 week')
@ -52,10 +45,15 @@ ugettext_lazy('second and fourth')
ugettext_lazy('one week on two') ugettext_lazy('one week on two')
Frequency = [ (y, x) for x,y in AFrequency.items() ] EventType = {
RFrequency = { y: x for x,y in AFrequency.items() } '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): class Model (models.Model):
@ -106,7 +104,7 @@ class Metadata (Model):
) )
date = models.DateTimeField( date = models.DateTimeField(
_('date') _('date')
, default = datetime.datetime.now , default = timezone.datetime.now
) )
public = models.BooleanField( public = models.BooleanField(
_('public') _('public')
@ -190,12 +188,6 @@ class Publication (Metadata):
# #
# Instance's methods # 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 ): def get_parents ( self, order_by = "desc", include_fields = None ):
""" """
Return an array of the parents of the item. Return an array of the parents of the item.
@ -262,10 +254,9 @@ class SoundFile (Metadata):
, blank = True , blank = True
, null = True , null = True
) )
file = models.FileField( file = models.FileField( #FIXME: filefield
_('file') _('file')
, upload_to = "data/tracks" , upload_to = lambda i, f: SoundFile.__upload_path(i,f)
, blank = True
) )
duration = models.TimeField( duration = models.TimeField(
_('duration') _('duration')
@ -283,6 +274,18 @@ class SoundFile (Metadata):
, null = True , null = True
, help_text = _('if set, consider the sound podcastable from there') , 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): def __str__ (self):
@ -294,29 +297,44 @@ class SoundFile (Metadata):
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( 'Program', blank = True, null = True )
date = models.DateTimeField(_('start')) date = models.DateTimeField(_('start'))
duration = models.TimeField(_('duration')) duration = models.TimeField(
frequency = models.SmallIntegerField(_('frequency'), choices = Frequency) _('duration')
, blank = True
, null = True
)
frequency = models.SmallIntegerField(
_('frequency')
, choices = [ (y, x) for x,y in Frequency.items() ]
)
rerun = models.BooleanField(_('rerun'), default = False) 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 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 self.frequency == AFrequency['ponctual']: if self.frequency == Frequency['ponctual']:
return None return None
if self.frequency == AFrequency['one week on two']: if self.frequency == Frequency['one week on two']:
week = at.isocalendar()[1] week = at.isocalendar()[1]
return (week % 2) == (self.date.isocalendar()[1] % 2) 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] week = at.isocalendar()[1] - first_of_month.isocalendar()[1]
# weeks of month # weeks of month
@ -326,30 +344,29 @@ class Schedule (Model):
return (self.frequency & (0b0001 << week) > 0) return (self.frequency & (0b0001 << week) > 0)
def next_date (self, at = timezone.datetime.today()):
def next_date (self, at = datetime.date.today()): if self.frequency == Frequency['ponctual']:
if self.frequency == AFrequency['ponctual']:
return None return None
# first day of the week # 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. # for the next five week, we look for a matching week.
# when found, add the number of day since de start of the # when found, add the number of day since de start of the
# we need to test if the result is >= at # we need to test if the result is >= at
for i in range(0,5): for i in range(0,5):
if self.match_week(date): if self.match_week(date):
date_ = date + datetime.timedelta( days = self.date.weekday() ) date_ = date + timezone.timedelta( days = self.date.weekday() )
if date_ >= at: if date_ >= at:
# we don't want past events # 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) self.date.hour, self.date.minute)
date += datetime.timedelta( days = 7 ) date += timezone.timedelta( days = 7 )
else: else:
return None 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 # we could have optimized this function, but since it should not
# be use too often, we keep a more readable and easier to debug # be use too often, we keep a more readable and easier to debug
# solution # solution
@ -362,20 +379,20 @@ class Schedule (Model):
if not e: if not e:
break break
res.append(e) res.append(e)
at = res[-1] + datetime.timedelta(days = 1) at = res[-1] + timezone.timedelta(days = 1)
return res return res
def to_string(self): #def to_string(self):
s = ugettext_lazy( RFrequency[self.frequency] ) # s = ugettext_lazy( RFrequency[self.frequency] )
if self.rerun: # if self.rerun:
return s + ' (' + _('rerun') + ')' # return s + ' (' + _('rerun') + ')'
return s # return s
def __str__ (self): 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: class Meta:
@ -427,15 +444,17 @@ class Program (Publication):
, blank = True , blank = True
, null = True , null = True
) )
tag = models.CharField( non_stop = models.SmallIntegerField(
_('tag') _('non-stop priority')
, max_length = 64 , help_text = _('this program can be used as non-stop')
, help_text = _('used in articles to refer to it') , default = -1
) )
@property @property
def path(self): 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: class Meta:
@ -455,8 +474,6 @@ class Episode (Publication):
parent = models.ForeignKey( parent = models.ForeignKey(
Program Program
, verbose_name = _('parent') , verbose_name = _('parent')
, blank = True
, null = True
) )
tracks = models.ManyToManyField( tracks = models.ManyToManyField(
Track Track
@ -473,37 +490,22 @@ class Episode (Publication):
class Event (Model): class Event (Model):
""" """
Event logs and planification of a sound file
""" """
parent = models.ForeignKey ( sound = models.ForeignKey (
Episode SoundFile
, verbose_name = _('episode') , verbose_name = _('sound file')
, blank = True
, null = True
) )
date = models.DateTimeField( _('date of start') ) type = models.SmallIntegerField(
date_end = models.DateTimeField( _('type')
_('date of end') , choices = [ (y, x) for x,y in EventType.items() ]
, blank = True
, null = True
)
public = models.BooleanField(
_('public')
, default = False
, help_text = _('publication is accessible to the public')
) )
date = models.DateTimeField( _('date of event start') )
meta = models.TextField ( meta = models.TextField (
_('meta') _('meta')
, blank = True , blank = True
, null = 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: class Meta:

View File

@ -1,10 +1,15 @@
import os
from django.conf import settings from django.conf import settings
def ensure (key, default): def ensure (key, default):
globals()[key] = getattr(settings, 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'))

View File

@ -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)