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
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 _
14 from django.db.models.signals import pre_save, post_delete
15 from django.dispatch.dispatcher import receiver
17 from ckeditor.fields import RichTextField
18 from taggit.managers import TaggableManager
20 from .utils import maybe_resize, get_duration
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')),
35 PODCAST_SOUND_QUALITY_LIST = (
36 ('standard', _('Standard')),
38 ('highest', _('Highest')),
42 class WeekdayMixin(object):
45 def get_weekday(self):
46 weekday = self.datetime.weekday() + 7
47 if self.datetime.time() < datetime.time(self.DAY_HOUR_START, 0):
52 def is_on_weekday(self, day): # day is [1..7]
53 week_day = self.datetime.weekday()
54 if self.datetime.hour < self.DAY_HOUR_START:
56 week_day = (week_day % 7) + 1
57 return week_day == day
60 @python_2_unicode_compatible
61 class Category(models.Model):
64 verbose_name = _('Category')
65 verbose_name_plural = _('Categories')
68 title = models.CharField(_('Title'), max_length=50)
69 slug = models.SlugField(null=True)
70 itunes_category = models.CharField(_('iTunes Category Name'), max_length=100, null=True)
72 def sorted_emission(self):
73 return self.emission_set.order_by('title')
79 @python_2_unicode_compatible
80 class Colour(models.Model):
83 verbose_name = _('Colour')
84 verbose_name_plural = _('Colours')
87 title = models.CharField(_('Title'), max_length=50)
88 slug = models.SlugField(null=True)
90 def sorted_emission(self):
91 return self.emission_set.order_by('title')
97 @python_2_unicode_compatible
98 class Format(models.Model):
101 verbose_name = _('Format')
102 verbose_name_plural = _('Formats')
105 title = models.CharField(_('Title'), max_length=50)
106 slug = models.SlugField(null=True)
112 def get_emission_image_path(instance, filename):
113 return os.path.join('images', instance.slug,
114 os.path.basename(filename))
117 @python_2_unicode_compatible
118 class Emission(models.Model):
121 verbose_name = _('Emission')
122 verbose_name_plural = _('Emissions')
125 title = models.CharField(_('Title'), max_length=200)
126 slug = models.SlugField(max_length=200)
127 subtitle = models.CharField(_('Subtitle'), max_length=80, null=True, blank=True)
128 text = RichTextField(_('Description'), null=True)
129 archived = models.BooleanField(_('Archived'), default=False)
130 categories = models.ManyToManyField(Category, verbose_name=_('Categories'), blank=True)
131 colours = models.ManyToManyField(Colour, verbose_name=_('Colours'), blank=True)
133 # XXX: languages (models.ManyToManyField(Language))
135 duration = models.IntegerField(_('Duration'), default=60,
136 help_text=_('In minutes'))
138 default_license = models.CharField(_('Default license for podcasts'),
139 max_length=20, blank=True, default='', choices=LICENSES)
140 podcast_sound_quality = models.CharField(_('Podcast sound quality'),
141 max_length=20, default='standard', choices=PODCAST_SOUND_QUALITY_LIST)
142 email = models.EmailField(_('Email'), max_length=254, null=True, blank=True)
143 website = models.URLField(_('Website'), null=True, blank=True)
145 image = models.ImageField(_('Image'),
146 upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
148 chat_open = models.DateTimeField(null=True, blank=True)
150 # denormalized from Focus
151 got_focus = models.DateTimeField(default=None, null=True, blank=True)
152 has_focus = models.BooleanField(default=False)
154 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
155 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
157 def get_absolute_url(self):
158 return reverse('emission-view', kwargs={'slug': str(self.slug)})
163 def save(self, *args, **kwargs):
164 super(Emission, self).save(*args, **kwargs)
165 if self.id is not None and self.image:
166 maybe_resize(self.image.path)
168 def get_absolute_url(self):
169 return reverse('emission-view',
170 kwargs={'slug': str(self.slug)})
172 def get_schedules(self):
173 return Schedule.objects.filter(emission=self).order_by('datetime')
175 def get_sorted_episodes(self):
176 return self.episode_set.select_related().extra(select={
177 'first_diffusion': 'emissions_diffusion.datetime',
179 select_params=(False, True),
180 where=['''datetime = (SELECT MIN(datetime)
181 FROM emissions_diffusion
182 WHERE episode_id = emissions_episode.id)'''],
183 tables=['emissions_diffusion'],
184 ).order_by('-first_diffusion')
186 def get_sorted_newsitems(self):
187 return self.newsitem_set.select_related().order_by('-date')
189 def get_next_planned_date_and_schedule(self, since=None):
190 schedules = self.schedule_set.filter(rerun=False)
194 since = datetime.datetime.today()
196 for schedule in schedules:
197 possible_dates.append((schedule.get_next_planned_date(since), schedule))
198 possible_dates.sort(key=lambda x: x[0])
199 return possible_dates[0]
201 def get_next_planned_date(self, since=None):
202 result = self.get_next_planned_date_and_schedule(since=since)
203 return result[0] if result else None
205 def get_next_planned_duration(self, since=None):
206 result = self.get_next_planned_date_and_schedule(since=since)
209 return result[1].duration or self.duration
211 @python_2_unicode_compatible
212 class Schedule(models.Model, WeekdayMixin):
215 verbose_name = _('Schedule')
216 verbose_name_plural = _('Schedules')
217 ordering = ['datetime']
220 (0b1111, _('Every week')),
221 (0b0001, _('First week')),
222 (0b0010, _('Second week')),
223 (0b0100, _('Third week')),
224 (0b1000, _('Fourth week')),
225 (0b0101, _('First and third week')),
226 (0b1010, _('Second and fourth week'))
228 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
229 datetime = models.DateTimeField()
230 weeks = models.IntegerField(_('Weeks'), default=15, choices=WEEK_CHOICES)
231 rerun = models.BooleanField(_('Rerun'), default=False)
232 duration = models.IntegerField(_('Duration'), null=True, blank=True,
233 help_text=_('In minutes'))
236 def weeks_string(self):
237 if self.weeks == 0b0001:
238 return ugettext('1st of the month')
239 elif self.weeks == 0b0010:
240 return ugettext('2nd of the month')
241 elif self.weeks == 0b0100:
242 return ugettext('3rd of the month')
243 elif self.weeks == 0b1000:
244 return ugettext('4th of the month')
245 elif self.weeks == 0b0101:
246 return ugettext('1st and 3rd')
247 elif self.weeks == 0b1010:
248 return ugettext('2nd and 4th')
251 def week_sort_key(self):
255 0b0101, # First and third week
256 0b0010, # Second week
257 0b1010, # Second and fourth week
259 0b1000, # Fourth week
261 if self.weeks in order:
262 return order.index(self.weeks)
265 def get_duration(self):
269 return self.emission.duration
271 def end_datetime(self):
272 return self.datetime + datetime.timedelta(minutes=self.get_duration())
274 def match_week(self, week_no):
276 # this is the fifth week of the month, only return True for
277 # emissions scheduled every week.
278 return (self.weeks == 0b1111)
279 if (self.weeks & (0b0001<<(week_no)) == 0):
283 def matches(self, dt):
284 weekday = dt.weekday()
285 if dt.hour < self.DAY_HOUR_START:
287 if weekday != self.get_weekday():
289 if self.weeks != 0b1111:
290 week_no = (dt.day-1) // 7
291 if self.match_week(week_no) is False:
293 if dt.time() >= self.datetime.time() and \
294 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
298 def get_next_planned_date(self, since):
300 monday_since_date = since - datetime.timedelta(days=since.weekday())
301 start_week_date = self.datetime.replace(
302 year=monday_since_date.year,
303 month=monday_since_date.month,
304 day=monday_since_date.day)
305 start_week_date -= datetime.timedelta(days=start_week_date.weekday())
306 start_week_date += datetime.timedelta(days=self.datetime.weekday())
308 week_date = start_week_date + datetime.timedelta(days=i*7)
309 if week_date < since:
311 if self.match_week((week_date.day-1)//7):
312 possible_dates.append(week_date)
313 possible_dates.sort()
314 return possible_dates[0]
318 return u'%s at %s' % (self.emission.title,
319 self.datetime.strftime('%a %H:%M'))
322 def get_episode_image_path(instance, filename):
323 return os.path.join('images', instance.emission.slug,
324 os.path.basename(filename))
327 @python_2_unicode_compatible
328 class Episode(models.Model):
331 verbose_name = _('Episode')
332 verbose_name_plural = _('Episodes')
335 emission = models.ForeignKey('Emission', verbose_name=_('Emission'))
336 title = models.CharField(_('Title'), max_length=200)
337 slug = models.SlugField(max_length=200)
338 subtitle = models.CharField(_('Subtitle'), max_length=150, null=True, blank=True)
339 text = RichTextField(_('Description'), null=True)
340 tags = TaggableManager(_('Tags'), blank=True)
341 duration = models.IntegerField(_('Duration'), null=True, blank=True,
342 help_text=_('In minutes'))
344 image = models.ImageField(_('Image'),
345 upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
347 effective_start = models.DateTimeField(null=True, blank=True)
348 effective_end = models.DateTimeField(null=True, blank=True)
350 # denormalized from Focus
351 got_focus = models.DateTimeField(default=None, null=True, blank=True)
352 has_focus = models.BooleanField(default=False)
354 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
355 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
357 # XXX: languages (models.ManyToManyField(Language))
362 def save(self, *args, **kwargs):
363 super(Episode, self).save(*args, **kwargs)
364 if self.id is not None and self.image:
365 maybe_resize(self.image.path)
367 def get_duration(self):
371 return self.emission.duration
373 def get_absolute_url(self):
374 return reverse('episode-view',
375 kwargs={'emission_slug': str(self.emission.slug),
376 'slug': str(self.slug)})
379 return (self.soundfile_set.count() > 0)
381 def get_pige_download_url(self):
382 return '%s/%s-%s-%s.wav' % (
383 settings.PIGE_DOWNLOAD_BASE_URL,
384 self.effective_start.strftime('%Y%m%d'),
385 self.effective_start.strftime('%Hh%Mm%S.%f'),
386 self.effective_end.strftime('%Hh%Mm%S.%f'))
391 def set_prefetched_soundfiles(cls, soundfiles):
392 cls._soundfiles.update(soundfiles)
394 def has_prefetched_soundfile(self):
395 return self.id in self._soundfiles
397 def get_prefetched_soundfile(self):
398 return self._soundfiles.get(self.id)
403 def main_sound(self):
404 if self._main_sound is not False:
405 return self._main_sound
407 if self.has_prefetched_soundfile():
408 self._main_sound = self.get_prefetched_soundfile()
409 return self._main_sound
411 t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
413 self._main_sound = t[0]
415 self._main_sound = None
417 return self._main_sound
420 def main_sound(self, value):
421 self._main_sound = value
423 def fragment_sounds(self):
424 return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
426 def diffusions(self):
427 return Diffusion.objects.filter(episode=self.id).order_by('datetime')
430 @python_2_unicode_compatible
431 class Diffusion(models.Model, WeekdayMixin):
434 verbose_name = _('Diffusion')
435 verbose_name_plural = _('Diffusions')
436 ordering = ['datetime']
438 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
439 datetime = models.DateTimeField(_('Date/time'), db_index=True)
442 return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
443 self.datetime.minute)
445 def get_duration(self):
446 return self.episode.get_duration()
448 def end_datetime(self):
449 return self.datetime + datetime.timedelta(minutes=self.get_duration())
452 @python_2_unicode_compatible
453 class Absence(models.Model):
456 verbose_name = _('Absence')
457 verbose_name_plural = _('Absences')
459 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
460 datetime = models.DateTimeField(_('Date/time'), db_index=True)
463 return u'Absence for %s on %s' % (self.emission.title, self.datetime)
466 def get_sound_path(instance, filename):
467 return os.path.join('sounds.orig', instance.episode.emission.slug,
468 os.path.basename(filename))
471 @python_2_unicode_compatible
472 class SoundFile(models.Model):
475 verbose_name = _('Sound file')
476 verbose_name_plural = _('Sound files')
477 ordering = ['creation_timestamp']
479 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
480 file = models.FileField(_('File'), upload_to=get_sound_path, max_length=250)
481 podcastable = models.BooleanField(_('Podcastable'), default=False,
483 help_text=_('The file can be published online according to SABAM rules.'))
484 fragment = models.BooleanField(_('Fragment'), default=False, db_index=True,
485 help_text=_('The file is some segment or extra content, not the complete recording.'))
486 title = models.CharField(_('Title'), max_length=200)
487 duration = models.IntegerField(_('Duration'), null=True, help_text=_('In seconds'))
489 format = models.ForeignKey('Format', verbose_name=_('Format'), null=True, blank=True)
490 license = models.CharField(_('License'), max_length=20, blank=True, default='', choices=LICENSES)
492 # denormalized from Focus
493 got_focus = models.DateTimeField(default=None, null=True, blank=True)
494 has_focus = models.BooleanField(default=False)
496 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
497 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
499 def compute_duration(self):
500 for path in (self.get_format_path('ogg'), self.get_format_path('mp3'), self.file.path):
501 self.duration = get_duration(path)
505 def get_format_filename(self, format):
506 return '%s_%05d__%s.%s' % (
509 (self.fragment and '0' or '1'),
512 def get_format_path(self, format):
515 return '%s/%s' % (os.path.dirname(self.file.path).replace('.orig', ''), self.get_format_filename(format))
517 def get_format_url(self, format):
520 return '%s/%s' % (os.path.dirname(self.file.url).replace('.orig', ''), self.get_format_filename(format))
522 def get_duration_string(self):
523 if not self.duration:
525 return '%d:%02d' % (self.duration/60, self.duration%60)
528 return '%s - %s' % (self.title or self.id, self.episode.title)
531 @python_2_unicode_compatible
532 class NewsCategory(models.Model):
535 verbose_name = _('News Category')
536 verbose_name_plural = _('News Categories')
539 title = models.CharField(_('Title'), max_length=50)
540 slug = models.SlugField(null=True)
545 def get_sorted_newsitems(self):
546 return self.newsitem_set.select_related().order_by('-date')
549 def get_newsitem_image_path(instance, filename):
550 return os.path.join('images', 'news', instance.slug,
551 os.path.basename(filename))
554 @python_2_unicode_compatible
555 class NewsItem(models.Model):
558 verbose_name = _('News Item')
559 verbose_name_plural = _('News Items')
562 title = models.CharField(_('Title'), max_length=200)
563 slug = models.SlugField(max_length=200)
564 text = RichTextField(_('Description'))
565 date = models.DateField(_('Publication Date'),
566 help_text=_('The news won\'t appear on the website before this date.'))
567 image = models.ImageField(_('Image'),
568 upload_to=get_newsitem_image_path, max_length=250, null=True, blank=True)
570 tags = TaggableManager(_('Tags'), blank=True)
571 category = models.ForeignKey('NewsCategory', verbose_name=_('Category'), null=True, blank=True)
572 emission = models.ForeignKey('Emission', verbose_name=_('Emission'), null=True, blank=True)
574 expiration_date = models.DateField(_('Expiration Date'), null=True, blank=True)
575 event_date = models.DateField(_('Event Date'), null=True, blank=True,
576 help_text=_('If this is an event, set the date here so it appears in the agenda.'))
578 # denormalized from Focus
579 got_focus = models.DateTimeField(default=None, null=True, blank=True)
580 has_focus = models.BooleanField(default=False)
582 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
583 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
588 def get_absolute_url(self):
589 return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
592 @python_2_unicode_compatible
593 class Nonstop(models.Model):
596 verbose_name = _('Nonstop zone')
597 verbose_name_plural = _('Nonstop zones')
600 title = models.CharField(_('Title'), max_length=50)
601 slug = models.SlugField()
603 start = models.TimeField(_('Start'))
604 end = models.TimeField(_('End'))
605 text = RichTextField(_('Description'), null=True, blank=True)
606 redirect_path = models.CharField(_('Redirect Path'), max_length=200, blank=True)
612 @python_2_unicode_compatible
613 class Focus(models.Model):
614 title = models.CharField(_('Alternate Title'), max_length=50,
615 null=True, blank=True)
616 newsitem = models.ForeignKey('NewsItem', verbose_name=_('News Item'),
617 null=True, blank=True)
618 emission = models.ForeignKey('Emission', verbose_name=_('Emission'),
619 null=True, blank=True)
620 episode = models.ForeignKey('Episode', verbose_name=_('Episode'),
621 null=True, blank=True)
622 soundfile = models.ForeignKey('SoundFile', verbose_name=_('Sound file'),
623 null=True, blank=True)
624 page = models.ForeignKey('data.Page', verbose_name=_('Page'),
625 null=True, blank=True)
626 current = models.BooleanField('Current', default=True)
628 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
629 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
633 return u'Newsitem: %s' % self.newsitem.title
635 return u'Emission: %s' % self.emission.title
637 return u'Episode: %s' % self.episode.title
639 return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
641 return u'Page: %s' % self.page.title
642 return u'%s' % self.id
644 def focus_title(self):
648 return self.newsitem.title
650 return self.emission.title
652 return self.page.title
656 if self.soundfile.fragment and self.soundfile.title:
657 return self.soundfile.title
658 episode = self.soundfile.episode
660 episode = self.episode
666 return episode.emission.title
670 def content_image(self):
672 return self.newsitem.image
674 return self.emission.image
676 from panikombo.models import Topik
677 return Topik.objects.get(page=self.page).image
681 episode = self.soundfile.episode
683 episode = self.episode
689 return episode.emission.image
691 def content_category_title(self):
693 if self.newsitem.category:
694 return self.newsitem.category.title
699 return self.episode.emission.title
701 return self.soundfile.episode.emission.title
705 def get_related_object(self):
714 return self.soundfile
716 from panikombo.models import Topik
717 return Topik.objects.get(page=self.page)
718 except ObjectDoesNotExist:
724 def get_playlist_sound_path(instance, filename):
725 return os.path.join('playlists', instance.episode.emission.slug,
726 instance.episode.slug, os.path.basename(filename))
729 class PlaylistElement(models.Model):
730 episode = models.ForeignKey('Episode', null=True)
731 title = models.CharField(_('Title'), max_length=200)
732 notes = models.CharField(_('Notes'), max_length=200, blank=True, default='')
733 sound = models.FileField(_('Sound'), upload_to=get_playlist_sound_path, max_length=250)
734 order = models.PositiveIntegerField()
737 verbose_name = _('Playlist Element')
738 verbose_name_plural = _('Playlist Elements')
742 return chr(ord('a')+self.order-1)
745 @receiver(pre_save, sender=Focus, dispatch_uid='focus_pre_save')
746 def set_focus_on_save(sender, instance, **kwargs):
747 object = instance.get_related_object()
749 if instance.current != object.has_focus:
750 object.has_focus = instance.current
752 if object and not object.got_focus and instance.current:
753 object.got_focus = datetime.datetime.now()
758 @receiver(post_delete, sender=Focus, dispatch_uid='focus_post_delete')
759 def remove_focus_on_delete(sender, instance, **kwargs):
760 object = instance.get_related_object()
761 if object and (object.got_focus or object.has_focus):
762 object.got_focus = None
763 object.has_focus = False