]> git.0d.be Git - django-panik-emissions.git/blob - emissions/models.py
adjust time of dawn
[django-panik-emissions.git] / emissions / models.py
1 import datetime
2 import os
3
4 from django.conf import settings
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.forms import fields
7 from django.core.urlresolvers import reverse
8
9 from django.db import models
10 from django.utils.encoding import python_2_unicode_compatible
11 from django.utils.translation import ugettext
12 from django.utils.translation import ugettext_lazy as _
13
14 from django.db.models.signals import pre_save, post_delete
15 from django.dispatch.dispatcher import receiver
16
17 from ckeditor.fields import RichTextField
18 from taggit.managers import TaggableManager
19
20 from .utils import maybe_resize, get_duration
21
22
23 LICENSES = (
24     ('', _('Unspecified')),
25     ('cc by', _('Creative Commons Attribution')),
26     ('cc by-sa', _('Creative Commons Attribution ShareAlike')),
27     ('cc by-nc', _('Creative Commons Attribution NonCommercial')),
28     ('cc by-nd', _('Creative Commons Attribution NoDerivs')),
29     ('cc by-nc-sa', _('Creative Commons Attribution NonCommercial ShareAlike')),
30     ('cc by-nc-nd', _('Creative Commons Attribution NonCommercial NoDerivs')),
31     ('cc0 / pd', _('Creative Commons Zero / Public Domain')),
32     ('artlibre', _('Art Libre')),
33 )
34
35 PODCAST_SOUND_QUALITY_LIST = (
36     ('standard', _('Standard')),
37     ('high', _('High')),
38     ('highest', _('Highest')),
39 )
40
41
42 class WeekdayMixin(object):
43     DAY_HOUR_START = 4
44     DAY_MINUTE_START = 30
45
46     def get_weekday(self):
47         weekday = self.datetime.weekday() + 7
48         if self.datetime.time() < datetime.time(self.DAY_HOUR_START, self.DAY_MINUTE_START):
49             weekday -= 1
50         weekday %= 7
51         return weekday
52
53     def is_on_weekday(self, day): # day is [1..7]
54         week_day = self.datetime.weekday()
55         if (self.datetime.hour, self.datetime.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START):
56             week_day -= 1
57         week_day = (week_day % 7) + 1
58         if hasattr(self, 'episode'):
59             if (self.datetime.hour, self.datetime.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START) and \
60                     (self.end_datetime.hour, self.end_datetime.minute) >= (self.DAY_HOUR_START, self.DAY_MINUTE_START):
61                 if (self.end_datetime.weekday()+1) == day:
62                     return True
63         return week_day == day
64
65
66 @python_2_unicode_compatible
67 class Category(models.Model):
68
69     class Meta:
70         verbose_name = _('Category')
71         verbose_name_plural = _('Categories')
72         ordering = ['title']
73
74     title = models.CharField(_('Title'), max_length=50)
75     slug = models.SlugField(null=True)
76     itunes_category = models.CharField(_('iTunes Category Name'), max_length=100, null=True)
77
78     def sorted_emission(self):
79         return self.emission_set.order_by('title')
80
81     def __str__(self):
82         return self.title
83
84
85 @python_2_unicode_compatible
86 class Colour(models.Model):
87
88     class Meta:
89         verbose_name = _('Colour')
90         verbose_name_plural = _('Colours')
91         ordering = ['title']
92
93     title = models.CharField(_('Title'), max_length=50)
94     slug = models.SlugField(null=True)
95
96     def sorted_emission(self):
97         return self.emission_set.order_by('title')
98
99     def __str__(self):
100         return self.title
101
102
103 @python_2_unicode_compatible
104 class Format(models.Model):
105
106     class Meta:
107         verbose_name = _('Format')
108         verbose_name_plural = _('Formats')
109         ordering = ['title']
110
111     title = models.CharField(_('Title'), max_length=50)
112     slug = models.SlugField(null=True)
113
114     def __str__(self):
115         return self.title
116
117
118 def get_emission_image_path(instance, filename):
119     return os.path.join('images', instance.slug,
120             os.path.basename(filename))
121
122
123 @python_2_unicode_compatible
124 class Emission(models.Model):
125
126     class Meta:
127         verbose_name = _('Emission')
128         verbose_name_plural = _('Emissions')
129         ordering = ['title']
130
131     title = models.CharField(_('Title'), max_length=200)
132     slug = models.SlugField(max_length=200)
133     subtitle = models.CharField(_('Subtitle'), max_length=80, null=True, blank=True)
134     text = RichTextField(_('Description'), null=True)
135     archived = models.BooleanField(_('Archived'), default=False)
136     categories = models.ManyToManyField(Category, verbose_name=_('Categories'), blank=True)
137     colours = models.ManyToManyField(Colour, verbose_name=_('Colours'), blank=True)
138
139     # XXX: languages (models.ManyToManyField(Language))
140
141     duration = models.IntegerField(_('Duration'), default=60,
142             help_text=_('In minutes'))
143
144     default_license = models.CharField(_('Default license for podcasts'),
145             max_length=20, blank=True, default='', choices=LICENSES)
146     podcast_sound_quality = models.CharField(_('Podcast sound quality'),
147             max_length=20, default='standard', choices=PODCAST_SOUND_QUALITY_LIST)
148     email = models.EmailField(_('Email'), max_length=254, null=True, blank=True)
149     website = models.URLField(_('Website'), null=True, blank=True)
150
151     image = models.ImageField(_('Image'),
152             upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
153
154     chat_open = models.DateTimeField(null=True, blank=True)
155
156     # denormalized from Focus
157     got_focus = models.DateTimeField(default=None, null=True, blank=True)
158     has_focus = models.BooleanField(default=False)
159
160     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
161     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
162
163     def get_absolute_url(self):
164         return reverse('emission-view', kwargs={'slug': str(self.slug)})
165
166     def __str__(self):
167         return self.title
168
169     def save(self, *args, **kwargs):
170         super(Emission, self).save(*args, **kwargs)
171         if self.id is not None and self.image:
172             maybe_resize(self.image.path)
173
174     def get_absolute_url(self):
175         return reverse('emission-view',
176                         kwargs={'slug':  str(self.slug)})
177
178     def get_schedules(self):
179         return Schedule.objects.filter(emission=self).order_by('datetime')
180
181     def get_sorted_episodes(self):
182         return self.episode_set.select_related().extra(select={
183                 'first_diffusion': 'emissions_diffusion.datetime',
184                 },
185                 select_params=(False, True),
186                 where=['''datetime = (SELECT MIN(datetime)
187                                         FROM emissions_diffusion
188                                        WHERE episode_id = emissions_episode.id)'''],
189                 tables=['emissions_diffusion'],
190             ).order_by('-first_diffusion')
191
192     def get_sorted_newsitems(self):
193         return self.newsitem_set.select_related().order_by('-date')
194
195     def get_next_planned_date_and_schedule(self, since=None):
196         schedules = self.schedule_set.filter(rerun=False)
197         if not schedules:
198             return None
199         if since is None:
200             since = datetime.datetime.today()
201         possible_dates = []
202         for schedule in schedules:
203             possible_dates.append((schedule.get_next_planned_date(since), schedule))
204         possible_dates.sort(key=lambda x: x[0])
205         return possible_dates[0]
206
207     def get_next_planned_date(self, since=None):
208         result = self.get_next_planned_date_and_schedule(since=since)
209         return result[0] if result else None
210
211     def get_next_planned_duration(self, since=None):
212         result = self.get_next_planned_date_and_schedule(since=since)
213         if not result:
214             return None
215         return result[1].duration or self.duration
216
217 @python_2_unicode_compatible
218 class Schedule(models.Model, WeekdayMixin):
219
220     class Meta:
221         verbose_name = _('Schedule')
222         verbose_name_plural = _('Schedules')
223         ordering = ['datetime']
224
225     WEEK_CHOICES = (
226         (0b1111, _('Every week')),
227         (0b0001, _('First week')),
228         (0b0010, _('Second week')),
229         (0b0100, _('Third week')),
230         (0b1000, _('Fourth week')),
231         (0b0101, _('First and third week')),
232         (0b1010, _('Second and fourth week'))
233     )
234     emission = models.ForeignKey('Emission', verbose_name=u'Emission')
235     datetime = models.DateTimeField()
236     weeks = models.IntegerField(_('Weeks'), default=15, choices=WEEK_CHOICES)
237     rerun = models.BooleanField(_('Rerun'), default=False)
238     duration = models.IntegerField(_('Duration'), null=True, blank=True,
239             help_text=_('In minutes'))
240
241     @property
242     def weeks_string(self):
243         if self.weeks == 0b0001:
244             return ugettext('1st of the month')
245         elif self.weeks == 0b0010:
246             return ugettext('2nd of the month')
247         elif self.weeks == 0b0100:
248             return ugettext('3rd of the month')
249         elif self.weeks == 0b1000:
250             return ugettext('4th of the month')
251         elif self.weeks == 0b0101:
252             return ugettext('1st and 3rd')
253         elif self.weeks == 0b1010:
254             return ugettext('2nd and 4th')
255         return None
256
257     def week_sort_key(self):
258         order = [
259             0b1111, # Every week
260             0b0001, # First week
261             0b0101, # First and third week
262             0b0010, # Second week
263             0b1010, # Second and fourth week
264             0b0100, # Third week
265             0b1000, # Fourth week
266         ]
267         if self.weeks in order:
268             return order.index(self.weeks)
269         return -1
270
271     def get_duration(self):
272         if self.duration:
273             return self.duration
274         else:
275             return self.emission.duration
276
277     @property
278     def end_datetime(self):
279         return self.datetime + datetime.timedelta(minutes=self.get_duration())
280
281     def match_week(self, week_no):
282         if week_no == 4:
283             # this is the fifth week of the month, only return True for
284             # emissions scheduled every week.
285             return (self.weeks == 0b1111)
286         if (self.weeks & (0b0001<<(week_no)) == 0):
287             return False
288         return True
289
290     def matches(self, dt):
291         weekday = dt.weekday()
292         if (dt.hour, dt.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START):
293             weekday -= 1
294         if weekday != self.get_weekday():
295             return False
296         if self.weeks != 0b1111:
297             week_no = (dt.day-1) // 7
298             if self.match_week(week_no) is False:
299                 return False
300         if dt.time() >= self.datetime.time() and \
301                 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
302             return True
303         return False
304
305     def get_next_planned_date(self, since):
306         possible_dates = []
307         monday_since_date = since - datetime.timedelta(days=since.weekday())
308         start_week_date = self.datetime.replace(
309                 year=monday_since_date.year,
310                 month=monday_since_date.month,
311                 day=monday_since_date.day)
312         start_week_date -= datetime.timedelta(days=start_week_date.weekday())
313         start_week_date += datetime.timedelta(days=self.datetime.weekday())
314         for i in range(6):
315             week_date = start_week_date + datetime.timedelta(days=i*7)
316             if week_date < since:
317                 continue
318             if self.match_week((week_date.day-1)//7):
319                 possible_dates.append(week_date)
320         possible_dates.sort()
321         return possible_dates[0]
322
323
324     def __str__(self):
325         return u'%s at %s' % (self.emission.title,
326                 self.datetime.strftime('%a %H:%M'))
327
328
329 def get_episode_image_path(instance, filename):
330     return os.path.join('images', instance.emission.slug,
331             os.path.basename(filename))
332
333
334 @python_2_unicode_compatible
335 class Episode(models.Model):
336
337     class Meta:
338         verbose_name = _('Episode')
339         verbose_name_plural = _('Episodes')
340         ordering = ['title']
341
342     emission = models.ForeignKey('Emission', verbose_name=_('Emission'))
343     title = models.CharField(_('Title'), max_length=200)
344     slug = models.SlugField(max_length=200)
345     subtitle = models.CharField(_('Subtitle'), max_length=150, null=True, blank=True)
346     text = RichTextField(_('Description'), null=True)
347     tags = TaggableManager(_('Tags'), blank=True)
348     duration = models.IntegerField(_('Duration'), null=True, blank=True,
349             help_text=_('In minutes'))
350
351     image = models.ImageField(_('Image'),
352             upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
353
354     effective_start = models.DateTimeField(null=True, blank=True)
355     effective_end = models.DateTimeField(null=True, blank=True)
356
357     # denormalized from Focus
358     got_focus = models.DateTimeField(default=None, null=True, blank=True)
359     has_focus = models.BooleanField(default=False)
360
361     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
362     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
363
364     # XXX: languages (models.ManyToManyField(Language))
365
366     def __str__(self):
367         return self.title
368
369     def save(self, *args, **kwargs):
370         super(Episode, self).save(*args, **kwargs)
371         if self.id is not None and self.image:
372             maybe_resize(self.image.path)
373
374     def get_duration(self):
375         if self.duration:
376             return self.duration
377         else:
378             return self.emission.duration
379
380     def get_absolute_url(self):
381         return reverse('episode-view',
382                         kwargs={'emission_slug': str(self.emission.slug),
383                                 'slug':  str(self.slug)})
384
385     def has_sound(self):
386         return (self.soundfile_set.count() > 0)
387
388     def get_pige_download_url(self):
389         return '%s/%s-%s-%s.wav' % (
390                 settings.PIGE_DOWNLOAD_BASE_URL,
391                 self.effective_start.strftime('%Y%m%d'),
392                 self.effective_start.strftime('%Hh%Mm%S.%f'),
393                 self.effective_end.strftime('%Hh%Mm%S.%f'))
394
395     _soundfiles = {}
396
397     @classmethod
398     def set_prefetched_soundfiles(cls, soundfiles):
399         cls._soundfiles.update(soundfiles)
400
401     def has_prefetched_soundfile(self):
402         return self.id in self._soundfiles
403
404     def get_prefetched_soundfile(self):
405         return self._soundfiles.get(self.id)
406
407     _main_sound = False
408
409     @property
410     def main_sound(self):
411         if self._main_sound is not False:
412             return self._main_sound
413
414         if self.has_prefetched_soundfile():
415             self._main_sound = self.get_prefetched_soundfile()
416             return self._main_sound
417
418         t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
419         if t:
420             self._main_sound = t[0]
421         else:
422             self._main_sound = None
423
424         return self._main_sound
425
426     @main_sound.setter
427     def main_sound(self, value):
428         self._main_sound = value
429
430     def fragment_sounds(self):
431         return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
432
433     def diffusions(self):
434         return Diffusion.objects.filter(episode=self.id).order_by('datetime')
435
436
437 @python_2_unicode_compatible
438 class Diffusion(models.Model, WeekdayMixin):
439
440     class Meta:
441         verbose_name = _('Diffusion')
442         verbose_name_plural = _('Diffusions')
443         ordering = ['datetime']
444
445     episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
446     datetime = models.DateTimeField(_('Date/time'), db_index=True)
447
448     def __str__(self):
449         return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
450                 self.datetime.minute)
451
452     def get_duration(self):
453         return self.episode.get_duration()
454
455     @property
456     def end_datetime(self):
457         return self.datetime + datetime.timedelta(minutes=self.get_duration())
458
459
460 @python_2_unicode_compatible
461 class Absence(models.Model):
462
463     class Meta:
464         verbose_name = _('Absence')
465         verbose_name_plural = _('Absences')
466
467     emission = models.ForeignKey('Emission', verbose_name=u'Emission')
468     datetime = models.DateTimeField(_('Date/time'), db_index=True)
469
470     def __str__(self):
471         return u'Absence for %s on %s' % (self.emission.title, self.datetime)
472
473
474 def get_sound_path(instance, filename):
475     return os.path.join('sounds.orig', instance.episode.emission.slug,
476             os.path.basename(filename))
477
478
479 @python_2_unicode_compatible
480 class SoundFile(models.Model):
481
482     class Meta:
483         verbose_name = _('Sound file')
484         verbose_name_plural = _('Sound files')
485         ordering = ['creation_timestamp']
486
487     episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
488     file = models.FileField(_('File'), upload_to=get_sound_path, max_length=250)
489     podcastable = models.BooleanField(_('Podcastable'), default=False,
490             db_index=True,
491             help_text=_('The file can be published online according to SABAM rules.'))
492     fragment = models.BooleanField(_('Fragment'), default=False, db_index=True,
493             help_text=_('The file is some segment or extra content, not the complete recording.'))
494     title = models.CharField(_('Title'), max_length=200)
495     duration = models.IntegerField(_('Duration'), null=True, help_text=_('In seconds'))
496
497     format = models.ForeignKey('Format', verbose_name=_('Format'), null=True, blank=True)
498     license = models.CharField(_('License'), max_length=20, blank=True, default='', choices=LICENSES)
499
500     # denormalized from Focus
501     got_focus = models.DateTimeField(default=None, null=True, blank=True)
502     has_focus = models.BooleanField(default=False)
503
504     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
505     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
506
507     def compute_duration(self):
508         for path in (self.get_format_path('ogg'), self.get_format_path('mp3'), self.file.path):
509             self.duration = get_duration(path)
510             if self.duration:
511                 return
512
513     def get_format_filename(self, format):
514         return '%s_%05d__%s.%s' % (
515                 self.episode.slug,
516                 self.id,
517                 (self.fragment and '0' or '1'),
518                 format)
519
520     def get_format_path(self, format):
521         if not self.file:
522             return None
523         return '%s/%s' % (os.path.dirname(self.file.path).replace('.orig', ''), self.get_format_filename(format))
524
525     def get_format_url(self, format):
526         if not self.file:
527             return None
528         return '%s/%s' % (os.path.dirname(self.file.url).replace('.orig', ''), self.get_format_filename(format))
529
530     def get_duration_string(self):
531         if not self.duration:
532             return ''
533         return '%d:%02d' % (self.duration/60, self.duration%60)
534
535     def __str__(self):
536         return '%s - %s' % (self.title or self.id, self.episode.title)
537
538
539 @python_2_unicode_compatible
540 class NewsCategory(models.Model):
541
542     class Meta:
543         verbose_name = _('News Category')
544         verbose_name_plural = _('News Categories')
545         ordering = ['title']
546
547     title = models.CharField(_('Title'), max_length=50)
548     slug = models.SlugField(null=True)
549
550     def __str__(self):
551         return self.title
552
553     def get_sorted_newsitems(self):
554         return self.newsitem_set.select_related().order_by('-date')
555
556
557 def get_newsitem_image_path(instance, filename):
558     return os.path.join('images', 'news', instance.slug,
559             os.path.basename(filename))
560
561
562 @python_2_unicode_compatible
563 class NewsItem(models.Model):
564
565     class Meta:
566         verbose_name = _('News Item')
567         verbose_name_plural = _('News Items')
568         ordering = ['title']
569
570     title = models.CharField(_('Title'), max_length=200)
571     slug = models.SlugField(max_length=200)
572     text = RichTextField(_('Description'))
573     date = models.DateField(_('Publication Date'),
574             help_text=_('The news won\'t appear on the website before this date.'))
575     image = models.ImageField(_('Image'),
576             upload_to=get_newsitem_image_path, max_length=250, null=True, blank=True)
577
578     tags = TaggableManager(_('Tags'), blank=True)
579     category = models.ForeignKey('NewsCategory', verbose_name=_('Category'), null=True, blank=True)
580     emission = models.ForeignKey('Emission', verbose_name=_('Emission'), null=True, blank=True)
581
582     expiration_date = models.DateField(_('Expiration Date'), null=True, blank=True)
583     event_date = models.DateField(_('Event Date'), null=True, blank=True,
584             help_text=_('If this is an event, set the date here so it appears in the agenda.'))
585
586     # denormalized from Focus
587     got_focus = models.DateTimeField(default=None, null=True, blank=True)
588     has_focus = models.BooleanField(default=False)
589
590     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
591     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
592
593     def __str__(self):
594         return self.title
595
596     def get_absolute_url(self):
597         return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
598
599
600 @python_2_unicode_compatible
601 class Nonstop(models.Model):
602
603     class Meta:
604         verbose_name = _('Nonstop zone')
605         verbose_name_plural = _('Nonstop zones')
606         ordering = ['title']
607
608     title = models.CharField(_('Title'), max_length=50)
609     slug = models.SlugField()
610
611     start = models.TimeField(_('Start'))
612     end = models.TimeField(_('End'))
613     text = RichTextField(_('Description'), null=True, blank=True)
614     redirect_path = models.CharField(_('Redirect Path'), max_length=200, blank=True)
615
616     def __str__(self):
617         return self.title
618
619
620 @python_2_unicode_compatible
621 class Focus(models.Model):
622     title = models.CharField(_('Alternate Title'), max_length=50,
623             null=True, blank=True)
624     newsitem = models.ForeignKey('NewsItem', verbose_name=_('News Item'),
625             null=True, blank=True)
626     emission = models.ForeignKey('Emission', verbose_name=_('Emission'),
627             null=True, blank=True)
628     episode = models.ForeignKey('Episode', verbose_name=_('Episode'),
629             null=True, blank=True)
630     soundfile = models.ForeignKey('SoundFile', verbose_name=_('Sound file'),
631             null=True, blank=True)
632     page = models.ForeignKey('data.Page', verbose_name=_('Page'),
633             null=True, blank=True)
634     current = models.BooleanField('Current', default=True)
635
636     creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
637     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
638
639     def __str__(self):
640         if self.newsitem:
641             return u'Newsitem: %s' % self.newsitem.title
642         if self.emission:
643             return u'Emission: %s' % self.emission.title
644         if self.episode:
645             return u'Episode: %s' % self.episode.title
646         if self.soundfile:
647             return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
648         if self.page:
649             return u'Page: %s' % self.page.title
650         return u'%s' % self.id
651
652     def focus_title(self):
653         if self.title:
654             return self.title
655         if self.newsitem:
656             return self.newsitem.title
657         if self.emission:
658             return self.emission.title
659         if self.page:
660             return self.page.title
661
662         episode = None
663         if self.soundfile:
664             if self.soundfile.fragment and self.soundfile.title:
665                 return self.soundfile.title
666             episode = self.soundfile.episode
667         elif self.episode:
668             episode = self.episode
669
670         if episode:
671             if episode.title:
672                 return episode.title
673             else:
674                 return episode.emission.title
675
676         return None
677
678     def content_image(self):
679         if self.newsitem:
680             return self.newsitem.image
681         if self.emission:
682             return self.emission.image
683         if self.page:
684             return self.page.picture
685
686         episode = None
687         if self.soundfile:
688             episode = self.soundfile.episode
689         elif self.episode:
690             episode = self.episode
691
692         if episode:
693             if episode.image:
694                 return episode.image
695             else:
696                 return episode.emission.image
697
698     def content_category_title(self):
699         if self.newsitem:
700             if self.newsitem.category:
701                 return self.newsitem.category.title
702             return _('News')
703         if self.emission:
704             return _('Emission')
705         if self.episode:
706             return self.episode.emission.title
707         if self.soundfile:
708             return self.soundfile.episode.emission.title
709         if self.page:
710             return 'Topik'
711
712     def get_related_object(self):
713         try:
714             if self.newsitem:
715                 return self.newsitem
716             if self.emission:
717                 return self.emission
718             if self.episode:
719                 return self.episode
720             if self.soundfile:
721                 return self.soundfile
722             if self.page:
723                 from panikombo.models import Topik
724                 return Topik.objects.get(page=self.page)
725         except ObjectDoesNotExist:
726             return None
727
728         return None
729
730
731 def get_playlist_sound_path(instance, filename):
732     return os.path.join('playlists', instance.episode.emission.slug,
733             instance.episode.slug, os.path.basename(filename))
734
735
736 class PlaylistElement(models.Model):
737     episode = models.ForeignKey('Episode', null=True)
738     title = models.CharField(_('Title'), max_length=200)
739     notes = models.CharField(_('Notes'), max_length=200, blank=True, default='')
740     sound = models.FileField(_('Sound'), upload_to=get_playlist_sound_path, max_length=250)
741     order = models.PositiveIntegerField()
742
743     class Meta:
744         verbose_name = _('Playlist Element')
745         verbose_name_plural = _('Playlist Elements')
746         ordering = ['order']
747
748     def shortcut(self):
749         return chr(ord('a')+self.order-1)
750
751
752 @receiver(pre_save, sender=Focus, dispatch_uid='focus_pre_save')
753 def set_focus_on_save(sender, instance, **kwargs):
754     object = instance.get_related_object()
755     must_save = False
756     if instance.current != object.has_focus:
757         object.has_focus = instance.current
758         must_save = True
759     if object and not object.got_focus and instance.current:
760         object.got_focus = datetime.datetime.now()
761         must_save = True
762     if must_save:
763         object.save()
764
765 @receiver(post_delete, sender=Focus, dispatch_uid='focus_post_delete')
766 def remove_focus_on_delete(sender, instance, **kwargs):
767     object = instance.get_related_object()
768     if object and (object.got_focus or object.has_focus):
769         object.got_focus = None
770         object.has_focus = False
771         object.save()