changes
This commit is contained in:
parent
e2696b7322
commit
c3b5104f69
|
@ -116,7 +116,7 @@ class RelatedPostBase (models.base.ModelBase):
|
||||||
rel = attrs.get('Relation')
|
rel = attrs.get('Relation')
|
||||||
rel = (rel and rel.__dict__) or {}
|
rel = (rel and rel.__dict__) or {}
|
||||||
|
|
||||||
related_model = rel.get('related_model')
|
related_model = rel.get('model')
|
||||||
if related_model:
|
if related_model:
|
||||||
attrs['related'] = models.ForeignKey(related_model)
|
attrs['related'] = models.ForeignKey(related_model)
|
||||||
|
|
||||||
|
@ -140,6 +140,14 @@ class RelatedPostBase (models.base.ModelBase):
|
||||||
|
|
||||||
|
|
||||||
class RelatedPost (Post, metaclass = RelatedPostBase):
|
class RelatedPost (Post, metaclass = RelatedPostBase):
|
||||||
|
"""
|
||||||
|
Use this post to generate Posts that are related to an external model. An
|
||||||
|
extra field "related" will be generated, and some bindings are possible to
|
||||||
|
update te related object on save if desired;
|
||||||
|
|
||||||
|
This is done through a class name Relation inside the declaration of the new
|
||||||
|
model.
|
||||||
|
"""
|
||||||
related = None
|
related = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -155,11 +163,15 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
||||||
It is a dict of { post_attr: rel_attr }
|
It is a dict of { post_attr: rel_attr }
|
||||||
|
|
||||||
If there is a post_attr "thread", the corresponding rel_attr is used
|
If there is a post_attr "thread", the corresponding rel_attr is used
|
||||||
to update the post thread to the correct Post model.
|
to update the post thread to the correct Post model (in order to
|
||||||
|
establish a parent-child relation between two models)
|
||||||
|
* thread_model: generated by the metaclass that point to the
|
||||||
|
RelatedModel class related to the model that is the parent of
|
||||||
|
the current related one.
|
||||||
"""
|
"""
|
||||||
model = None
|
model = None
|
||||||
mapping = None # values to map { post_attr: rel_attr }
|
mapping = None # values to map { post_attr: rel_attr }
|
||||||
bind = False # update fields of related data on save
|
thread = None
|
||||||
thread_model = None
|
thread_model = None
|
||||||
|
|
||||||
def get_attribute (self, attr):
|
def get_attribute (self, attr):
|
||||||
|
@ -175,7 +187,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
||||||
related object.
|
related object.
|
||||||
"""
|
"""
|
||||||
relation = self._relation
|
relation = self._relation
|
||||||
print(relation.__dict__)
|
|
||||||
thread_model = relation.thread_model
|
thread_model = relation.thread_model
|
||||||
if not thread_model:
|
if not thread_model:
|
||||||
return
|
return
|
||||||
|
|
|
@ -160,8 +160,7 @@ class SearchRoute (Route):
|
||||||
name = 'search'
|
name = 'search'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_queryset (cl, website, model, request, **kwargs):
|
def get_queryset (cl, website, model, request, q, **kwargs):
|
||||||
q = request.GET.get('q') or ''
|
|
||||||
qs = model.objects
|
qs = model.objects
|
||||||
for search_field in model.search_fields or []:
|
for search_field in model.search_fields or []:
|
||||||
r = model.objects.filter(**{ search_field + '__icontains': q })
|
r = model.objects.filter(**{ search_field + '__icontains': q })
|
||||||
|
|
|
@ -52,12 +52,12 @@ class PostListView (PostBaseView, ListView):
|
||||||
"""
|
"""
|
||||||
Request availables parameters
|
Request availables parameters
|
||||||
"""
|
"""
|
||||||
embed = False
|
embed = False # view is embedded (only the list is shown)
|
||||||
exclude = None
|
exclude = None # exclude item of this id
|
||||||
order = 'desc'
|
order = 'desc' # order of the list when query
|
||||||
reverse = False
|
fields = None # fields to show
|
||||||
fields = None
|
page = 1 # page number
|
||||||
page = 1
|
q = None # query search
|
||||||
|
|
||||||
def __init__ (self, query):
|
def __init__ (self, query):
|
||||||
if query:
|
if query:
|
||||||
|
@ -118,7 +118,7 @@ class PostListView (PostBaseView, ListView):
|
||||||
|
|
||||||
def get_context_data (self, **kwargs):
|
def get_context_data (self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context.update(self.get_base_context())
|
context.update(self.get_base_context(**kwargs))
|
||||||
context.update({
|
context.update({
|
||||||
'title': self.get_title(),
|
'title': self.get_title(),
|
||||||
})
|
})
|
||||||
|
@ -273,7 +273,6 @@ class Section (BaseSection):
|
||||||
self.object = object or self.object
|
self.object = object or self.object
|
||||||
if self.object_required and not self.object:
|
if self.object_required and not self.object:
|
||||||
raise ValueError('object is required by this Section but not given')
|
raise ValueError('object is required by this Section but not given')
|
||||||
|
|
||||||
return super().get(request, **kwargs)
|
return super().get(request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ class Actions:
|
||||||
if schedule.match(diffusion.date):
|
if schedule.match(diffusion.date):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print('> #{}: {}'.format(diffusion.date, str(diffusion)))
|
print('> #{}: {}'.format(diffusion.pk, str(diffusion)))
|
||||||
items.append(diffusion.id)
|
items.append(diffusion.id)
|
||||||
|
|
||||||
print('{} diffusions will be removed'.format(len(items)))
|
print('{} diffusions will be removed'.format(len(items)))
|
||||||
|
|
|
@ -42,21 +42,21 @@ class Command (BaseCommand):
|
||||||
|
|
||||||
def add_arguments (self, parser):
|
def add_arguments (self, parser):
|
||||||
parser.formatter_class=RawTextHelpFormatter
|
parser.formatter_class=RawTextHelpFormatter
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--quality_check', action='store_true',
|
||||||
|
help='Enable quality check using sound_quality_check on all ' \
|
||||||
|
'sounds marqued as not good'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--scan', action='store_true',
|
||||||
|
help='Scan programs directories for changes'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle (self, *args, **options):
|
def handle (self, *args, **options):
|
||||||
programs = Program.objects.filter()
|
if options.get('scan'):
|
||||||
|
self.scan()
|
||||||
for program in programs:
|
if options.get('quality_check'):
|
||||||
path = lambda x: os.path.join(program.path, x)
|
|
||||||
self.check_files(
|
|
||||||
program, path(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR),
|
|
||||||
archive = True,
|
|
||||||
)
|
|
||||||
self.check_files(
|
|
||||||
program, path(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR),
|
|
||||||
excerpt = True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.check_quality()
|
self.check_quality()
|
||||||
|
|
||||||
def get_sound_info (self, path):
|
def get_sound_info (self, path):
|
||||||
|
@ -111,12 +111,28 @@ class Command (BaseCommand):
|
||||||
diffusion = diffusion[0]
|
diffusion = diffusion[0]
|
||||||
return diffusion.episode or None
|
return diffusion.episode or None
|
||||||
|
|
||||||
|
def scan (self):
|
||||||
|
print('scan files for all programs...')
|
||||||
|
programs = Program.objects.filter()
|
||||||
|
|
||||||
def check_files (self, program, dir_path, **sound_kwargs):
|
for program in programs:
|
||||||
|
print('- program ', program.name)
|
||||||
|
path = lambda x: os.path.join(program.path, x)
|
||||||
|
self.scan_for_program(
|
||||||
|
program, path(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR),
|
||||||
|
type = Sound.Type['archive'],
|
||||||
|
)
|
||||||
|
self.scan_for_program(
|
||||||
|
program, path(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR),
|
||||||
|
type = Sound.Type['excerpt'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def scan_for_program (self, program, dir_path, **sound_kwargs):
|
||||||
"""
|
"""
|
||||||
Scan a given directory that is associated to the given program, and
|
Scan a given directory that is associated to the given program, and
|
||||||
update sounds information.
|
update sounds information.
|
||||||
"""
|
"""
|
||||||
|
print(' - scan files in', dir_path)
|
||||||
if not os.path.exists(dir_path):
|
if not os.path.exists(dir_path):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -154,5 +170,29 @@ class Command (BaseCommand):
|
||||||
"""
|
"""
|
||||||
Check all files where quality has been set to bad
|
Check all files where quality has been set to bad
|
||||||
"""
|
"""
|
||||||
|
import sound_quality_check as quality_check
|
||||||
|
|
||||||
|
print('start quality check...')
|
||||||
|
files = [ sound.path
|
||||||
|
for sound in Sound.objects.filter(good_quality = False) ]
|
||||||
|
cmd = quality_check.Command()
|
||||||
|
cmd.handle( files = files,
|
||||||
|
**settings.AIRCOX_SOUND_QUALITY )
|
||||||
|
|
||||||
|
print('- update sounds in database')
|
||||||
|
def update_stats(sound_info, sound):
|
||||||
|
stats = sound_info.get_file_stats()
|
||||||
|
if stats:
|
||||||
|
sound.duration = int(stats.get('length'))
|
||||||
|
|
||||||
|
for sound_info in cmd.good:
|
||||||
|
sound = Sound.objects.get(path = sound_info.path)
|
||||||
|
sound.good_quality = True
|
||||||
|
update_stats(sound_info, sound)
|
||||||
|
sound.save(check = False)
|
||||||
|
|
||||||
|
for sound_info in cmd.bad:
|
||||||
|
sound = Sound.objects.get(path = sound_info.path)
|
||||||
|
update_stats(sound_info, sound)
|
||||||
|
sound.save(check = False)
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,14 @@ class Sound:
|
||||||
|
|
||||||
def __init__ (self, path, sample_length = None):
|
def __init__ (self, path, sample_length = None):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.sample_length = sample_length or self.sample_length
|
self.sample_length = sample_length if sample_length is not None \
|
||||||
|
else self.sample_length
|
||||||
|
|
||||||
|
def get_file_stats (self):
|
||||||
|
return self.stats and self.stats[0]
|
||||||
|
|
||||||
def analyse (self):
|
def analyse (self):
|
||||||
print('- Complete file analysis')
|
print('- complete file analysis')
|
||||||
self.stats = [ Stats(self.path) ]
|
self.stats = [ Stats(self.path) ]
|
||||||
position = 0
|
position = 0
|
||||||
length = self.stats[0].get('length')
|
length = self.stats[0].get('length')
|
||||||
|
@ -77,7 +81,7 @@ class Sound:
|
||||||
if not self.sample_length:
|
if not self.sample_length:
|
||||||
return
|
return
|
||||||
|
|
||||||
print('- Samples analysis: ', end=' ')
|
print('- samples analysis: ', end=' ')
|
||||||
while position < length:
|
while position < length:
|
||||||
print(len(self.stats), end=' ')
|
print(len(self.stats), end=' ')
|
||||||
stats = Stats(self.path, at = position, length = self.sample_length)
|
stats = Stats(self.path, at = position, length = self.sample_length)
|
||||||
|
@ -85,18 +89,6 @@ class Sound:
|
||||||
position += self.sample_length
|
position += self.sample_length
|
||||||
print()
|
print()
|
||||||
|
|
||||||
def resume (self):
|
|
||||||
view = lambda array: [
|
|
||||||
'file' if index is 0 else
|
|
||||||
'sample {} (at {} seconds)'.format(index, (index-1) * self.sample_length)
|
|
||||||
for index in self.good
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.good:
|
|
||||||
print('- Good:\033[92m', ', '.join( view(self.good) ), '\033[0m')
|
|
||||||
if self.bad:
|
|
||||||
print('- Bad:\033[91m', ', '.join( view(self.bad) ), '\033[0m')
|
|
||||||
|
|
||||||
def check (self, name, min_val, max_val):
|
def check (self, name, min_val, max_val):
|
||||||
self.good = [ index for index, stats in enumerate(self.stats)
|
self.good = [ index for index, stats in enumerate(self.stats)
|
||||||
if min_val <= stats.get(name) <= max_val ]
|
if min_val <= stats.get(name) <= max_val ]
|
||||||
|
@ -104,6 +96,17 @@ class Sound:
|
||||||
if index not in self.good ]
|
if index not in self.good ]
|
||||||
self.resume()
|
self.resume()
|
||||||
|
|
||||||
|
def resume (self):
|
||||||
|
view = lambda array: [
|
||||||
|
'file' if index is 0 else
|
||||||
|
'sample {} (at {} seconds)'.format(index, (index-1) * self.sample_length)
|
||||||
|
for index in array
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.good:
|
||||||
|
print('- good:\033[92m', ', '.join( view(self.good) ), '\033[0m')
|
||||||
|
if self.bad:
|
||||||
|
print('- bad:\033[91m', ', '.join( view(self.bad) ), '\033[0m')
|
||||||
|
|
||||||
class Command (BaseCommand):
|
class Command (BaseCommand):
|
||||||
help = __doc__
|
help = __doc__
|
||||||
|
@ -117,7 +120,7 @@ class Command (BaseCommand):
|
||||||
help='file(s) to analyse'
|
help='file(s) to analyse'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--sample_length', type=int,
|
'-s', '--sample_length', type=int, default=120,
|
||||||
help='size of sample to analyse in seconds. If not set (or 0), does'
|
help='size of sample to analyse in seconds. If not set (or 0), does'
|
||||||
' not analyse by sample',
|
' not analyse by sample',
|
||||||
)
|
)
|
||||||
|
@ -163,11 +166,11 @@ class Command (BaseCommand):
|
||||||
|
|
||||||
# resume
|
# resume
|
||||||
if options.get('resume'):
|
if options.get('resume'):
|
||||||
if good:
|
if self.good:
|
||||||
print('Files that did not failed the test:\033[92m\n ',
|
print('files that did not failed the test:\033[92m\n ',
|
||||||
'\n '.join(good), '\033[0m')
|
'\n '.join([sound.path for sound in self.good]), '\033[0m')
|
||||||
if bad:
|
if self.bad:
|
||||||
# bad at the end for ergonomy
|
# bad at the end for ergonomy
|
||||||
print('Files that failed the test:\033[91m\n ',
|
print('files that failed the test:\033[91m\n ',
|
||||||
'\n '.join(bad),'\033[0m')
|
'\n '.join([sound.path for sound in self.bad]),'\033[0m')
|
||||||
|
|
||||||
|
|
|
@ -74,47 +74,60 @@ class Track (Nameable):
|
||||||
|
|
||||||
class Sound (Nameable):
|
class Sound (Nameable):
|
||||||
"""
|
"""
|
||||||
A Sound is the representation of a sound, that can be:
|
A Sound is the representation of a sound file that can be either an excerpt
|
||||||
- An episode podcast/complete record
|
or a complete archive of the related episode.
|
||||||
- An episode partial podcast
|
|
||||||
- An episode is a part of the episode but not usable for direct podcast
|
|
||||||
|
|
||||||
We can manage this using the "public" and "fragment" fields. If a Sound is
|
The podcasting and public access permissions of a Sound are managed through
|
||||||
public, then we can podcast it. If a Sound is a fragment, then it is not
|
the related program info.
|
||||||
usable for diffusion.
|
|
||||||
|
|
||||||
Each sound can be associated to a filesystem's file or an embedded
|
|
||||||
code (for external podcasts).
|
|
||||||
"""
|
"""
|
||||||
|
Type = {
|
||||||
|
'other': 0x00,
|
||||||
|
'archive': 0x01,
|
||||||
|
'excerpt': 0x02,
|
||||||
|
}
|
||||||
|
for key, value in Type.items():
|
||||||
|
ugettext_lazy(key)
|
||||||
|
|
||||||
|
type = models.SmallIntegerField(
|
||||||
|
verbose_name = _('type'),
|
||||||
|
choices = [ (y, x) for x,y in Type.items() ],
|
||||||
|
blank = True, null = True
|
||||||
|
)
|
||||||
path = models.FilePathField(
|
path = models.FilePathField(
|
||||||
_('file'),
|
_('file'),
|
||||||
path = settings.AIRCOX_PROGRAMS_DIR,
|
path = settings.AIRCOX_PROGRAMS_DIR,
|
||||||
match = '*(' + '|'.join(settings.AIRCOX_SOUNDFILE_EXT) + ')$',
|
match = '*(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) + ')$',
|
||||||
recursive = True,
|
recursive = True,
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
)
|
)
|
||||||
embed = models.TextField(
|
embed = models.TextField(
|
||||||
_('embed HTML code from external website'),
|
_('embed HTML code'),
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
help_text = _('if set, consider the sound podcastable'),
|
help_text = _('HTML code used to embed a sound from external plateform'),
|
||||||
)
|
)
|
||||||
duration = models.TimeField(
|
duration = models.TimeField(
|
||||||
_('duration'),
|
_('duration'),
|
||||||
blank = True, null = True,
|
blank = True, null = True,
|
||||||
)
|
)
|
||||||
|
date = models.DateTimeField(
|
||||||
|
_('date'),
|
||||||
|
blank = True, null = True,
|
||||||
|
help_text = _('last modification date and time'),
|
||||||
|
)
|
||||||
|
removed = models.BooleanField(
|
||||||
|
_('removed'),
|
||||||
|
default = False,
|
||||||
|
help_text = _('this sound has been removed from filesystem'),
|
||||||
|
)
|
||||||
|
good_quality = models.BooleanField(
|
||||||
|
_('good quality'),
|
||||||
|
default = False,
|
||||||
|
help_text = _('sound\'s quality is okay')
|
||||||
|
)
|
||||||
public = models.BooleanField(
|
public = models.BooleanField(
|
||||||
_('public'),
|
_('public'),
|
||||||
default = False,
|
default = False,
|
||||||
help_text = _("the element is public"),
|
help_text = _('sound\'s is accessible through the website')
|
||||||
)
|
|
||||||
fragment = models.BooleanField(
|
|
||||||
_('incomplete sound'),
|
|
||||||
default = False,
|
|
||||||
help_text = _("the file is a cut"),
|
|
||||||
)
|
|
||||||
removed = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
help_text = _('this sound has been removed from filesystem'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_mtime (self):
|
def get_mtime (self):
|
||||||
|
@ -125,10 +138,34 @@ class Sound (Nameable):
|
||||||
mtime = tz.datetime.fromtimestamp(mtime)
|
mtime = tz.datetime.fromtimestamp(mtime)
|
||||||
return tz.make_aware(mtime, timezone.get_current_timezone())
|
return tz.make_aware(mtime, timezone.get_current_timezone())
|
||||||
|
|
||||||
def save (self, *args, **kwargs):
|
def file_exists (self):
|
||||||
if not self.pk:
|
return os.path.exists(self.path)
|
||||||
self.date = self.get_mtime()
|
|
||||||
|
|
||||||
|
def check_on_file (self):
|
||||||
|
"""
|
||||||
|
Check sound file info again'st self, and update informations if
|
||||||
|
needed. Return True if there was changes.
|
||||||
|
"""
|
||||||
|
if not self.file_exists():
|
||||||
|
if self.removed:
|
||||||
|
return
|
||||||
|
self.removed = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
mtime = self.get_mtime()
|
||||||
|
if self.date != mtime:
|
||||||
|
self.date = mtime
|
||||||
|
self.good_quality = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save (self, check = True, *args, **kwargs):
|
||||||
|
if check:
|
||||||
|
self.check_on_file()
|
||||||
|
|
||||||
|
if not self.name and self.path:
|
||||||
|
self.name = os.path.basename(self.path) \
|
||||||
|
.splitext() \
|
||||||
|
.replace('_', ' ')
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__ (self):
|
def __str__ (self):
|
||||||
|
@ -194,12 +231,13 @@ class Schedule (models.Model):
|
||||||
otherwise.
|
otherwise.
|
||||||
If the schedule is ponctual, return None.
|
If the schedule is ponctual, return None.
|
||||||
"""
|
"""
|
||||||
|
# FIXME: does not work if first_day > date_day
|
||||||
date = date_or_default(date)
|
date = date_or_default(date)
|
||||||
if self.frequency == Schedule.Frequency['one on two']:
|
if self.frequency == Schedule.Frequency['one on two']:
|
||||||
week = date.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 = tz.datetime.date(date.year, date.month, 1)
|
first_of_month = date.replace(day = 1)
|
||||||
week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
|
week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
|
||||||
|
|
||||||
# weeks of month
|
# weeks of month
|
||||||
|
@ -359,8 +397,7 @@ class Stream (models.Model):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
_('name'),
|
_('name'),
|
||||||
max_length = 32,
|
max_length = 32,
|
||||||
blank = True,
|
blank = True, null = True,
|
||||||
null = True,
|
|
||||||
)
|
)
|
||||||
public = models.BooleanField(
|
public = models.BooleanField(
|
||||||
_('public'),
|
_('public'),
|
||||||
|
@ -376,13 +413,22 @@ class Stream (models.Model):
|
||||||
default = 0,
|
default = 0,
|
||||||
help_text = _('priority of the stream')
|
help_text = _('priority of the stream')
|
||||||
)
|
)
|
||||||
|
time_start = models.TimeField(
|
||||||
|
_('start'),
|
||||||
|
blank = True, null = True,
|
||||||
|
help_text = _('if random, used to define a time range this stream is'
|
||||||
|
'played')
|
||||||
|
)
|
||||||
|
time_end = models.TimeField(
|
||||||
|
_('end'),
|
||||||
|
blank = True, null = True,
|
||||||
|
help_text = _('if random, used to define a time range this stream is'
|
||||||
|
'played')
|
||||||
|
)
|
||||||
|
|
||||||
# get info for:
|
# get info for:
|
||||||
# - random lists
|
# - random lists
|
||||||
# - scheduled lists
|
# - scheduled lists
|
||||||
# link between Streams and Programs:
|
|
||||||
# - hours range (non-stop)
|
|
||||||
# - stream/pgm
|
|
||||||
|
|
||||||
def __str__ (self):
|
def __str__ (self):
|
||||||
return '#{} {}'.format(self.priority, self.name)
|
return '#{} {}'.format(self.priority, self.name)
|
||||||
|
@ -391,7 +437,7 @@ class Stream (models.Model):
|
||||||
class Program (Nameable):
|
class Program (Nameable):
|
||||||
stream = models.ForeignKey(
|
stream = models.ForeignKey(
|
||||||
Stream,
|
Stream,
|
||||||
verbose_name = _('stream'),
|
verbose_name = _('streams'),
|
||||||
)
|
)
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
_('inactive'),
|
_('inactive'),
|
||||||
|
|
|
@ -10,15 +10,28 @@ def ensure (key, default):
|
||||||
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 sounds
|
# Default directory for the sounds that not linked to a program
|
||||||
ensure('AIRCOX_SOUNDFILE_DEFAULT_DIR',
|
ensure('AIRCOX_SOUND_DEFAULT_DIR',
|
||||||
os.path.join(AIRCOX_PROGRAMS_DIR + 'default'))
|
os.path.join(AIRCOX_PROGRAMS_DIR, 'defaults'))
|
||||||
|
# Sub directory used for the complete episode sounds
|
||||||
|
ensure('AIRCOX_SOUND_ARCHIVES_SUBDIR', 'archives')
|
||||||
|
# Sub directory used for the excerpts of the episode
|
||||||
|
ensure('AIRCOX_SOUND_EXCERPTS_SUBDIR', 'excerpts')
|
||||||
|
|
||||||
|
# Quality attributes passed to sound_quality_check from sounds_monitor
|
||||||
|
ensure('AIRCOX_SOUND_QUALITY', {
|
||||||
|
'attribute': 'RMS lev dB',
|
||||||
|
'range': ('-18.0', '-8.0'),
|
||||||
|
'sample_length': '120',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Extension of sound files
|
# Extension of sound files
|
||||||
ensure('AIRCOX_SOUNDFILE_EXT',
|
ensure('AIRCOX_SOUND_FILE_EXT',
|
||||||
('.ogg','.flac','.wav','.mp3','.opus'))
|
('.ogg','.flac','.wav','.mp3','.opus'))
|
||||||
|
|
||||||
# Stream for the scheduled diffusions
|
# Stream for the scheduled diffusions
|
||||||
ensure('AIRCOX_SCHEDULED_STREAM', 0)
|
ensure('AIRCOX_SCHEDULED_STREAM', 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,81 @@
|
||||||
from django.test import TestCase
|
import datetime
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
from aircox_programs.models import *
|
||||||
|
|
||||||
|
class Programs (TestCase):
|
||||||
|
def setUp (self):
|
||||||
|
stream = Stream.objects.get_or_create(
|
||||||
|
name = 'diffusions',
|
||||||
|
defaults = { 'type': Stream.Type['schedule'] }
|
||||||
|
)[0]
|
||||||
|
Program.objects.create(name = 'source', stream = stream)
|
||||||
|
Program.objects.create(name = 'microouvert', stream = stream)
|
||||||
|
#Stream.objects.create(name = 'bns', type = Stream.Type['random'], priority = 1)
|
||||||
|
#Stream.objects.create(name = 'jingle', type = Stream.Type['random'] priority = 2)
|
||||||
|
#Stream.objects.create(name = 'loves', type = Stream.Type['random'], priority = 3)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_programs_schedules (self):
|
||||||
|
program = Program.objects.get(name = 'source')
|
||||||
|
|
||||||
|
sched_0 = self.create_schedule(program, 'one on two', [
|
||||||
|
tz.datetime(2015, 10, 2, 18),
|
||||||
|
tz.datetime(2015, 10, 16, 18),
|
||||||
|
tz.datetime(2015, 10, 30, 18),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
sched_1 = self.create_schedule(program, 'one on two', [
|
||||||
|
tz.datetime(2015, 10, 5, 18),
|
||||||
|
tz.datetime(2015, 10, 19, 18),
|
||||||
|
],
|
||||||
|
rerun = sched_0
|
||||||
|
)
|
||||||
|
|
||||||
|
program = Program.objects.get(name = 'microouvert')
|
||||||
|
# special case with november first week starting on sunday
|
||||||
|
sched_2 = self.create_schedule(program, 'first and third', [
|
||||||
|
tz.datetime(2015, 11, 6, 18),
|
||||||
|
tz.datetime(2015, 11, 20, 18),
|
||||||
|
],
|
||||||
|
date = tz.datetime(2015, 10, 23, 18),
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_schedule (self, program, frequency, dates, date = None, rerun = None):
|
||||||
|
frequency = Schedule.Frequency[frequency]
|
||||||
|
schedule = Schedule(
|
||||||
|
program = program,
|
||||||
|
frequency = frequency,
|
||||||
|
date = date or dates[0],
|
||||||
|
rerun = rerun,
|
||||||
|
duration = datetime.time(1, 30)
|
||||||
|
)
|
||||||
|
print(schedule.__dict__)
|
||||||
|
schedule.save()
|
||||||
|
|
||||||
|
self.check_schedule(schedule, dates)
|
||||||
|
return schedule
|
||||||
|
|
||||||
|
def check_schedule (self, schedule, dates):
|
||||||
|
dates = [ tz.make_aware(date) for date in dates ]
|
||||||
|
dates.sort()
|
||||||
|
|
||||||
|
# match date and weeks
|
||||||
|
for date in dates:
|
||||||
|
#self.assertTrue(schedule.match(date, check_time = False))
|
||||||
|
#self.assertTrue(schedule.match_week(date))
|
||||||
|
|
||||||
|
# dates
|
||||||
|
dates_ = schedule.dates_of_month(dates[0])
|
||||||
|
dates_.sort()
|
||||||
|
self.assertEqual(dates_, dates)
|
||||||
|
|
||||||
|
# diffusions
|
||||||
|
dates_ = schedule.diffusions_of_month(dates[0])
|
||||||
|
dates_ = [date_.date for date_ in dates_]
|
||||||
|
dates_.sort()
|
||||||
|
self.assertEqual(dates_, dates)
|
||||||
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
|
@ -12,79 +12,5 @@ import aircox_programs.settings
|
||||||
import aircox_programs.utils
|
import aircox_programs.utils
|
||||||
|
|
||||||
|
|
||||||
class ListQueries:
|
|
||||||
@staticmethod
|
|
||||||
def search (qs, q):
|
|
||||||
qs = qs.filter(tags__slug__in = re.compile(r'(\s|\+)+').split(q)) | \
|
|
||||||
qs.filter(title__icontains = q) | \
|
|
||||||
qs.filter(subtitle__icontains = q) | \
|
|
||||||
qs.filter(content__icontains = q)
|
|
||||||
qs.distinct()
|
|
||||||
return qs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def thread (qs, q):
|
|
||||||
return qs.filter(parent = q)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def next (qs, q):
|
|
||||||
qs = qs.filter(date__gte = timezone.now())
|
|
||||||
if q:
|
|
||||||
qs = qs.filter(parent = q)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prev (qs, q):
|
|
||||||
qs = qs.filter(date__lte = timezone.now())
|
|
||||||
if q:
|
|
||||||
qs = qs.filter(parent = q)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def date (qs, q):
|
|
||||||
if not q:
|
|
||||||
q = timezone.datetime.today()
|
|
||||||
if type(q) is str:
|
|
||||||
q = timezone.datetime.strptime(q, '%Y/%m/%d').date()
|
|
||||||
|
|
||||||
return qs.filter(date__startswith = q)
|
|
||||||
|
|
||||||
class Diffusion:
|
|
||||||
@staticmethod
|
|
||||||
def episode (qs, q):
|
|
||||||
return qs.filter(episode = q)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def program (qs, q):
|
|
||||||
return qs.filter(program = q)
|
|
||||||
|
|
||||||
class ListQuery:
|
|
||||||
model = None
|
|
||||||
qs = None
|
|
||||||
|
|
||||||
def __init__ (self, model, *kwargs):
|
|
||||||
self.model = model
|
|
||||||
self.__dict__.update(kwargs)
|
|
||||||
|
|
||||||
def get_queryset (self, by, q):
|
|
||||||
qs = model.objects.all()
|
|
||||||
if model._meta.get_field_by_name('public'):
|
|
||||||
qs = qs.filter(public = True)
|
|
||||||
|
|
||||||
# run query set
|
|
||||||
queries = Queries.__dict__.get(self.model) or Queries
|
|
||||||
filter = queries.__dict__.get(by)
|
|
||||||
if filter:
|
|
||||||
qs = filter(qs, q)
|
|
||||||
|
|
||||||
# order
|
|
||||||
if self.sort == 'asc':
|
|
||||||
qs = qs.order_by('date', 'id')
|
|
||||||
else:
|
|
||||||
qs = qs.order_by('-date', '-id')
|
|
||||||
|
|
||||||
# exclude
|
|
||||||
qs = qs.exclude(id = exclude)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import aircox_programs.models as programs
|
||||||
|
|
||||||
class Program (RelatedPost):
|
class Program (RelatedPost):
|
||||||
class Relation:
|
class Relation:
|
||||||
related_model = programs.Program
|
model = programs.Program
|
||||||
bind_mapping = True
|
bind_mapping = True
|
||||||
mapping = {
|
mapping = {
|
||||||
'title': 'name',
|
'title': 'name',
|
||||||
|
@ -14,7 +14,7 @@ class Program (RelatedPost):
|
||||||
|
|
||||||
class Episode (RelatedPost):
|
class Episode (RelatedPost):
|
||||||
class Relation:
|
class Relation:
|
||||||
related_model = programs.Episode
|
model = programs.Episode
|
||||||
bind_mapping = True
|
bind_mapping = True
|
||||||
mapping = {
|
mapping = {
|
||||||
'thread': 'program',
|
'thread': 'program',
|
||||||
|
|
|
@ -7,7 +7,7 @@ body {
|
||||||
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
h1, h2, h3 {
|
||||||
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif
|
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
time {
|
time {
|
||||||
|
|
|
@ -68,6 +68,7 @@ class PreviousDiffusions (Sections.Posts):
|
||||||
episodes.append(post)
|
episodes.append(post)
|
||||||
if len(episodes) == self.paginate_by:
|
if len(episodes) == self.paginate_by:
|
||||||
break
|
break
|
||||||
|
|
||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user