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