This commit is contained in:
bkfox 2015-10-29 11:01:15 +01:00
parent e2696b7322
commit c3b5104f69
13 changed files with 289 additions and 173 deletions

View File

@ -116,7 +116,7 @@ class RelatedPostBase (models.base.ModelBase):
rel = attrs.get('Relation')
rel = (rel and rel.__dict__) or {}
related_model = rel.get('related_model')
related_model = rel.get('model')
if related_model:
attrs['related'] = models.ForeignKey(related_model)
@ -140,6 +140,14 @@ class RelatedPostBase (models.base.ModelBase):
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
class Meta:
@ -155,11 +163,15 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
It is a dict of { post_attr: rel_attr }
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
mapping = None # values to map { post_attr: rel_attr }
bind = False # update fields of related data on save
thread = None
thread_model = None
def get_attribute (self, attr):
@ -175,7 +187,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
related object.
"""
relation = self._relation
print(relation.__dict__)
thread_model = relation.thread_model
if not thread_model:
return

View File

@ -160,8 +160,7 @@ class SearchRoute (Route):
name = 'search'
@classmethod
def get_queryset (cl, website, model, request, **kwargs):
q = request.GET.get('q') or ''
def get_queryset (cl, website, model, request, q, **kwargs):
qs = model.objects
for search_field in model.search_fields or []:
r = model.objects.filter(**{ search_field + '__icontains': q })

View File

@ -52,12 +52,12 @@ class PostListView (PostBaseView, ListView):
"""
Request availables parameters
"""
embed = False
exclude = None
order = 'desc'
reverse = False
fields = None
page = 1
embed = False # view is embedded (only the list is shown)
exclude = None # exclude item of this id
order = 'desc' # order of the list when query
fields = None # fields to show
page = 1 # page number
q = None # query search
def __init__ (self, query):
if query:
@ -118,7 +118,7 @@ class PostListView (PostBaseView, ListView):
def get_context_data (self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(self.get_base_context())
context.update(self.get_base_context(**kwargs))
context.update({
'title': self.get_title(),
})
@ -273,7 +273,6 @@ class Section (BaseSection):
self.object = object or self.object
if self.object_required and not self.object:
raise ValueError('object is required by this Section but not given')
return super().get(request, **kwargs)

View File

@ -52,7 +52,7 @@ class Actions:
if schedule.match(diffusion.date):
break
else:
print('> #{}: {}'.format(diffusion.date, str(diffusion)))
print('> #{}: {}'.format(diffusion.pk, str(diffusion)))
items.append(diffusion.id)
print('{} diffusions will be removed'.format(len(items)))

View File

@ -42,21 +42,21 @@ class Command (BaseCommand):
def add_arguments (self, parser):
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):
programs = Program.objects.filter()
for program in programs:
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,
)
if options.get('scan'):
self.scan()
if options.get('quality_check'):
self.check_quality()
def get_sound_info (self, path):
@ -111,12 +111,28 @@ class Command (BaseCommand):
diffusion = diffusion[0]
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
update sounds information.
"""
print(' - scan files in', dir_path)
if not os.path.exists(dir_path):
return
@ -154,5 +170,29 @@ class Command (BaseCommand):
"""
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)

View File

@ -66,10 +66,14 @@ class Sound:
def __init__ (self, path, sample_length = None):
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):
print('- Complete file analysis')
print('- complete file analysis')
self.stats = [ Stats(self.path) ]
position = 0
length = self.stats[0].get('length')
@ -77,7 +81,7 @@ class Sound:
if not self.sample_length:
return
print('- Samples analysis: ', end=' ')
print('- samples analysis: ', end=' ')
while position < length:
print(len(self.stats), end=' ')
stats = Stats(self.path, at = position, length = self.sample_length)
@ -85,18 +89,6 @@ class Sound:
position += self.sample_length
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):
self.good = [ index for index, stats in enumerate(self.stats)
if min_val <= stats.get(name) <= max_val ]
@ -104,6 +96,17 @@ class Sound:
if index not in self.good ]
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):
help = __doc__
@ -117,7 +120,7 @@ class Command (BaseCommand):
help='file(s) to analyse'
)
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'
' not analyse by sample',
)
@ -163,11 +166,11 @@ class Command (BaseCommand):
# resume
if options.get('resume'):
if good:
print('Files that did not failed the test:\033[92m\n ',
'\n '.join(good), '\033[0m')
if bad:
if self.good:
print('files that did not failed the test:\033[92m\n ',
'\n '.join([sound.path for sound in self.good]), '\033[0m')
if self.bad:
# bad at the end for ergonomy
print('Files that failed the test:\033[91m\n ',
'\n '.join(bad),'\033[0m')
print('files that failed the test:\033[91m\n ',
'\n '.join([sound.path for sound in self.bad]),'\033[0m')

View File

@ -74,47 +74,60 @@ class Track (Nameable):
class Sound (Nameable):
"""
A Sound is the representation of a sound, that can be:
- An episode podcast/complete record
- An episode partial podcast
- An episode is a part of the episode but not usable for direct podcast
A Sound is the representation of a sound file that can be either an excerpt
or a complete archive of the related episode.
We can manage this using the "public" and "fragment" fields. If a Sound is
public, then we can podcast it. If a Sound is a fragment, then it is not
usable for diffusion.
Each sound can be associated to a filesystem's file or an embedded
code (for external podcasts).
The podcasting and public access permissions of a Sound are managed through
the related program info.
"""
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(
_('file'),
path = settings.AIRCOX_PROGRAMS_DIR,
match = '*(' + '|'.join(settings.AIRCOX_SOUNDFILE_EXT) + ')$',
match = '*(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) + ')$',
recursive = True,
blank = True, null = True,
)
embed = models.TextField(
_('embed HTML code from external website'),
_('embed HTML code'),
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'),
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'),
default = False,
help_text = _("the element is public"),
)
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'),
help_text = _('sound\'s is accessible through the website')
)
def get_mtime (self):
@ -125,10 +138,34 @@ class Sound (Nameable):
mtime = tz.datetime.fromtimestamp(mtime)
return tz.make_aware(mtime, timezone.get_current_timezone())
def save (self, *args, **kwargs):
if not self.pk:
self.date = self.get_mtime()
def file_exists (self):
return os.path.exists(self.path)
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)
def __str__ (self):
@ -194,12 +231,13 @@ class Schedule (models.Model):
otherwise.
If the schedule is ponctual, return None.
"""
# FIXME: does not work if first_day > date_day
date = date_or_default(date)
if self.frequency == Schedule.Frequency['one on two']:
week = date.isocalendar()[1]
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]
# weeks of month
@ -359,8 +397,7 @@ class Stream (models.Model):
name = models.CharField(
_('name'),
max_length = 32,
blank = True,
null = True,
blank = True, null = True,
)
public = models.BooleanField(
_('public'),
@ -376,13 +413,22 @@ class Stream (models.Model):
default = 0,
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:
# - random lists
# - scheduled lists
# link between Streams and Programs:
# - hours range (non-stop)
# - stream/pgm
def __str__ (self):
return '#{} {}'.format(self.priority, self.name)
@ -391,7 +437,7 @@ class Stream (models.Model):
class Program (Nameable):
stream = models.ForeignKey(
Stream,
verbose_name = _('stream'),
verbose_name = _('streams'),
)
active = models.BooleanField(
_('inactive'),

View File

@ -10,15 +10,28 @@ def ensure (key, default):
ensure('AIRCOX_PROGRAMS_DIR',
os.path.join(settings.MEDIA_ROOT, 'programs'))
# Default directory for the sounds
ensure('AIRCOX_SOUNDFILE_DEFAULT_DIR',
os.path.join(AIRCOX_PROGRAMS_DIR + 'default'))
# Default directory for the sounds that not linked to a program
ensure('AIRCOX_SOUND_DEFAULT_DIR',
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
ensure('AIRCOX_SOUNDFILE_EXT',
ensure('AIRCOX_SOUND_FILE_EXT',
('.ogg','.flac','.wav','.mp3','.opus'))
# Stream for the scheduled diffusions
ensure('AIRCOX_SCHEDULED_STREAM', 0)

View File

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

View File

@ -12,79 +12,5 @@ import aircox_programs.settings
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

View File

@ -5,7 +5,7 @@ import aircox_programs.models as programs
class Program (RelatedPost):
class Relation:
related_model = programs.Program
model = programs.Program
bind_mapping = True
mapping = {
'title': 'name',
@ -14,7 +14,7 @@ class Program (RelatedPost):
class Episode (RelatedPost):
class Relation:
related_model = programs.Episode
model = programs.Episode
bind_mapping = True
mapping = {
'thread': 'program',

View File

@ -7,7 +7,7 @@ body {
h1, h2, h3 {
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
}
time {

View File

@ -68,6 +68,7 @@ class PreviousDiffusions (Sections.Posts):
episodes.append(post)
if len(episodes) == self.paginate_by:
break
return episodes