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