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
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')),
36 class WeekdayMixin(object):
39 def get_weekday(self):
40 weekday = self.datetime.weekday() + 7
41 if self.datetime.time() < datetime.time(self.DAY_HOUR_START, 0):
46 def is_on_weekday(self, day): # day is [1..7]
47 week_day = self.datetime.weekday()
48 if self.datetime.hour < self.DAY_HOUR_START:
50 week_day = (week_day % 7) + 1
51 return week_day == day
54 @python_2_unicode_compatible
55 class Category(models.Model):
58 verbose_name = _('Category')
59 verbose_name_plural = _('Categories')
62 title = models.CharField(_('Title'), max_length=50)
63 slug = models.SlugField(null=True)
64 itunes_category = models.CharField(_('iTunes Category Name'), max_length=100, null=True)
66 def sorted_emission(self):
67 return self.emission_set.order_by('title')
73 @python_2_unicode_compatible
74 class Colour(models.Model):
77 verbose_name = _('Colour')
78 verbose_name_plural = _('Colours')
81 title = models.CharField(_('Title'), max_length=50)
82 slug = models.SlugField(null=True)
84 def sorted_emission(self):
85 return self.emission_set.order_by('title')
91 @python_2_unicode_compatible
92 class Format(models.Model):
95 verbose_name = _('Format')
96 verbose_name_plural = _('Formats')
99 title = models.CharField(_('Title'), max_length=50)
100 slug = models.SlugField(null=True)
106 def get_emission_image_path(instance, filename):
107 return os.path.join('images', instance.slug,
108 os.path.basename(filename))
111 @python_2_unicode_compatible
112 class Emission(models.Model):
115 verbose_name = _('Emission')
116 verbose_name_plural = _('Emissions')
119 title = models.CharField(_('Title'), max_length=200)
120 slug = models.SlugField(max_length=200)
121 subtitle = models.CharField(_('Subtitle'), max_length=80, null=True, blank=True)
122 text = RichTextField(_('Description'), null=True)
123 archived = models.BooleanField(_('Archived'), default=False)
124 categories = models.ManyToManyField(Category, verbose_name=_('Categories'), blank=True)
125 colours = models.ManyToManyField(Colour, verbose_name=_('Colours'), blank=True)
127 # XXX: languages (models.ManyToManyField(Language))
129 duration = models.IntegerField(_('Duration'), default=60,
130 help_text=_('In minutes'))
132 default_license = models.CharField(_('Default license for podcasts'),
133 max_length=20, blank=True, default='', choices=LICENSES)
134 email = models.EmailField(_('Email'), max_length=254, null=True, blank=True)
135 website = models.URLField(_('Website'), null=True, blank=True)
137 image = models.ImageField(_('Image'),
138 upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
140 chat_open = models.DateTimeField(null=True, blank=True)
142 # denormalized from Focus
143 got_focus = models.DateTimeField(default=None, null=True, blank=True)
144 has_focus = models.BooleanField(default=False)
146 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
147 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
149 def get_absolute_url(self):
150 return reverse('emission-view', kwargs={'slug': str(self.slug)})
155 def save(self, *args, **kwargs):
156 super(Emission, self).save(*args, **kwargs)
157 if self.id is not None and self.image:
158 maybe_resize(self.image.path)
160 def get_absolute_url(self):
161 return reverse('emission-view',
162 kwargs={'slug': str(self.slug)})
164 def get_schedules(self):
165 return Schedule.objects.filter(emission=self).order_by('datetime')
167 def get_sorted_episodes(self):
168 return self.episode_set.select_related().extra(select={
169 'first_diffusion': 'emissions_diffusion.datetime',
171 select_params=(False, True),
172 where=['''datetime = (SELECT MIN(datetime)
173 FROM emissions_diffusion
174 WHERE episode_id = emissions_episode.id)'''],
175 tables=['emissions_diffusion'],
176 ).order_by('-first_diffusion')
178 def get_sorted_newsitems(self):
179 return self.newsitem_set.select_related().order_by('-date')
181 def get_next_planned_date_and_schedule(self, since=None):
182 schedules = self.schedule_set.filter(rerun=False)
186 since = datetime.datetime.today()
188 for schedule in schedules:
189 possible_dates.append((schedule.get_next_planned_date(since), schedule))
190 possible_dates.sort()
191 return possible_dates[0]
193 def get_next_planned_date(self, since=None):
194 result = self.get_next_planned_date_and_schedule(since=since)
195 return result[0] if result else None
197 def get_next_planned_duration(self, since=None):
198 result = self.get_next_planned_date_and_schedule(since=since)
201 return result[1].duration or self.duration
203 @python_2_unicode_compatible
204 class Schedule(models.Model, WeekdayMixin):
207 verbose_name = _('Schedule')
208 verbose_name_plural = _('Schedules')
209 ordering = ['datetime']
212 (0b1111, _('Every week')),
213 (0b0001, _('First week')),
214 (0b0010, _('Second week')),
215 (0b0100, _('Third week')),
216 (0b1000, _('Fourth week')),
217 (0b0101, _('First and third week')),
218 (0b1010, _('Second and fourth week'))
220 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
221 datetime = models.DateTimeField()
222 weeks = models.IntegerField(_('Weeks'), default=15, choices=WEEK_CHOICES)
223 rerun = models.BooleanField(_('Rerun'), default=False)
224 duration = models.IntegerField(_('Duration'), null=True, blank=True,
225 help_text=_('In minutes'))
228 def weeks_string(self):
229 if self.weeks == 0b0001:
230 return ugettext('1st of the month')
231 elif self.weeks == 0b0010:
232 return ugettext('2nd of the month')
233 elif self.weeks == 0b0100:
234 return ugettext('3rd of the month')
235 elif self.weeks == 0b1000:
236 return ugettext('4th of the month')
237 elif self.weeks == 0b0101:
238 return ugettext('1st and 3rd')
239 elif self.weeks == 0b1010:
240 return ugettext('2nd and 4th')
243 def week_sort_key(self):
247 0b0101, # First and third week
248 0b0010, # Second week
249 0b1010, # Second and fourth week
251 0b1000, # Fourth week
253 if self.weeks in order:
254 return order.index(self.weeks)
257 def get_duration(self):
261 return self.emission.duration
263 def end_datetime(self):
264 return self.datetime + datetime.timedelta(minutes=self.get_duration())
266 def match_week(self, week_no):
268 # this is the fifth week of the month, only return True for
269 # emissions scheduled every week.
270 return (self.weeks == 0b1111)
271 if (self.weeks & (0b0001<<(week_no)) == 0):
275 def matches(self, dt):
276 weekday = dt.weekday()
277 if dt.hour < self.DAY_HOUR_START:
279 if weekday != self.get_weekday():
281 if self.weeks != 0b1111:
282 week_no = (dt.day-1) // 7
283 if self.match_week(week_no) is False:
285 if dt.time() >= self.datetime.time() and \
286 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
290 def get_next_planned_date(self, since):
292 monday_since_date = since - datetime.timedelta(days=since.weekday())
293 start_week_date = self.datetime.replace(
294 year=monday_since_date.year,
295 month=monday_since_date.month,
296 day=monday_since_date.day)
297 start_week_date -= datetime.timedelta(days=start_week_date.weekday())
298 start_week_date += datetime.timedelta(days=self.datetime.weekday())
300 week_date = start_week_date + datetime.timedelta(days=i*7)
301 if week_date < since:
303 if self.match_week((week_date.day-1)//7):
304 possible_dates.append(week_date)
305 possible_dates.sort()
306 return possible_dates[0]
310 return u'%s at %s' % (self.emission.title,
311 self.datetime.strftime('%a %H:%M'))
314 def get_episode_image_path(instance, filename):
315 return os.path.join('images', instance.emission.slug,
316 os.path.basename(filename))
319 @python_2_unicode_compatible
320 class Episode(models.Model):
323 verbose_name = _('Episode')
324 verbose_name_plural = _('Episodes')
327 emission = models.ForeignKey('Emission', verbose_name=_('Emission'))
328 title = models.CharField(_('Title'), max_length=200)
329 slug = models.SlugField(max_length=200)
330 subtitle = models.CharField(_('Subtitle'), max_length=150, null=True, blank=True)
331 text = RichTextField(_('Description'), null=True)
332 tags = TaggableManager(_('Tags'), blank=True)
333 duration = models.IntegerField(_('Duration'), null=True, blank=True,
334 help_text=_('In minutes'))
336 image = models.ImageField(_('Image'),
337 upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
339 effective_start = models.DateTimeField(null=True, blank=True)
340 effective_end = models.DateTimeField(null=True, blank=True)
342 # denormalized from Focus
343 got_focus = models.DateTimeField(default=None, null=True, blank=True)
344 has_focus = models.BooleanField(default=False)
346 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
347 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
349 # XXX: languages (models.ManyToManyField(Language))
354 def save(self, *args, **kwargs):
355 super(Episode, self).save(*args, **kwargs)
356 if self.id is not None and self.image:
357 maybe_resize(self.image.path)
359 def get_duration(self):
363 return self.emission.duration
365 def get_absolute_url(self):
366 return reverse('episode-view',
367 kwargs={'emission_slug': str(self.emission.slug),
368 'slug': str(self.slug)})
371 return (self.soundfile_set.count() > 0)
373 def get_pige_download_url(self):
374 return '%s/%s-%s-%s.wav' % (
375 settings.PIGE_DOWNLOAD_BASE_URL,
376 self.effective_start.strftime('%Y%m%d'),
377 self.effective_start.strftime('%Hh%Mm%S.%f'),
378 self.effective_end.strftime('%Hh%Mm%S.%f'))
383 def set_prefetched_soundfiles(cls, soundfiles):
384 cls._soundfiles.update(soundfiles)
386 def has_prefetched_soundfile(self):
387 return self.id in self._soundfiles
389 def get_prefetched_soundfile(self):
390 return self._soundfiles.get(self.id)
395 def main_sound(self):
396 if self._main_sound is not False:
397 return self._main_sound
399 if self.has_prefetched_soundfile():
400 self._main_sound = self.get_prefetched_soundfile()
401 return self._main_sound
403 t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
405 self._main_sound = t[0]
407 self._main_sound = None
409 return self._main_sound
412 def main_sound(self, value):
413 self._main_sound = value
415 def fragment_sounds(self):
416 return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
418 def diffusions(self):
419 return Diffusion.objects.filter(episode=self.id).order_by('datetime')
422 @python_2_unicode_compatible
423 class Diffusion(models.Model, WeekdayMixin):
426 verbose_name = _('Diffusion')
427 verbose_name_plural = _('Diffusions')
428 ordering = ['datetime']
430 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
431 datetime = models.DateTimeField(_('Date/time'), db_index=True)
434 return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
435 self.datetime.minute)
437 def get_duration(self):
438 return self.episode.get_duration()
440 def end_datetime(self):
441 return self.datetime + datetime.timedelta(minutes=self.get_duration())
444 @python_2_unicode_compatible
445 class Absence(models.Model):
448 verbose_name = _('Absence')
449 verbose_name_plural = _('Absences')
451 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
452 datetime = models.DateTimeField(_('Date/time'), db_index=True)
455 return u'Absence for %s on %s' % (self.emission.title, self.datetime)
458 def get_sound_path(instance, filename):
459 return os.path.join('sounds.orig', instance.episode.emission.slug,
460 os.path.basename(filename))
463 @python_2_unicode_compatible
464 class SoundFile(models.Model):
467 verbose_name = _('Sound file')
468 verbose_name_plural = _('Sound files')
469 ordering = ['creation_timestamp']
471 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
472 file = models.FileField(_('File'), upload_to=get_sound_path, max_length=250)
473 podcastable = models.BooleanField(_('Podcastable'), default=False,
475 help_text=_('The file can be published online according to SABAM rules.'))
476 fragment = models.BooleanField(_('Fragment'), default=False, db_index=True,
477 help_text=_('The file is some segment or extra content, not the complete recording.'))
478 title = models.CharField(_('Title'), max_length=200)
479 duration = models.IntegerField(_('Duration'), null=True, help_text=_('In seconds'))
481 format = models.ForeignKey('Format', verbose_name=_('Format'), null=True, blank=True)
482 license = models.CharField(_('License'), max_length=20, blank=True, default='', choices=LICENSES)
484 # denormalized from Focus
485 got_focus = models.DateTimeField(default=None, null=True, blank=True)
486 has_focus = models.BooleanField(default=False)
488 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
489 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
491 def get_format_filename(self, format):
492 return '%s_%05d__%s.%s' % (
495 (self.fragment and '0' or '1'),
498 def get_format_path(self, format):
501 return '%s/%s' % (os.path.dirname(self.file.path).replace('.orig', ''), self.get_format_filename(format))
503 def get_format_url(self, format):
506 return '%s/%s' % (os.path.dirname(self.file.url).replace('.orig', ''), self.get_format_filename(format))
508 def get_duration_string(self):
509 if not self.duration:
511 return '%d:%02d' % (self.duration/60, self.duration%60)
514 return '%s - %s' % (self.title or self.id, self.episode.title)
517 @python_2_unicode_compatible
518 class NewsCategory(models.Model):
521 verbose_name = _('News Category')
522 verbose_name_plural = _('News Categories')
525 title = models.CharField(_('Title'), max_length=50)
526 slug = models.SlugField(null=True)
531 def get_sorted_newsitems(self):
532 return self.newsitem_set.select_related().order_by('-date')
535 def get_newsitem_image_path(instance, filename):
536 return os.path.join('images', 'news', instance.slug,
537 os.path.basename(filename))
540 @python_2_unicode_compatible
541 class NewsItem(models.Model):
544 verbose_name = _('News Item')
545 verbose_name_plural = _('News Items')
548 title = models.CharField(_('Title'), max_length=200)
549 slug = models.SlugField(max_length=200)
550 text = RichTextField(_('Description'))
551 date = models.DateField(_('Publication Date'),
552 help_text=_('The news won\'t appear on the website before this date.'))
553 image = models.ImageField(_('Image'),
554 upload_to=get_newsitem_image_path, max_length=250, null=True, blank=True)
556 tags = TaggableManager(_('Tags'), blank=True)
557 category = models.ForeignKey('NewsCategory', verbose_name=_('Category'), null=True, blank=True)
558 emission = models.ForeignKey('Emission', verbose_name=_('Emission'), null=True, blank=True)
560 expiration_date = models.DateField(_('Expiration Date'), null=True, blank=True)
561 event_date = models.DateField(_('Event Date'), null=True, blank=True,
562 help_text=_('If this is an event, set the date here so it appears in the agenda.'))
564 # denormalized from Focus
565 got_focus = models.DateTimeField(default=None, null=True, blank=True)
566 has_focus = models.BooleanField(default=False)
568 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
569 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
574 def get_absolute_url(self):
575 return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
578 @python_2_unicode_compatible
579 class Nonstop(models.Model):
582 verbose_name = _('Nonstop zone')
583 verbose_name_plural = _('Nonstop zones')
586 title = models.CharField(_('Title'), max_length=50)
587 slug = models.SlugField()
589 start = models.TimeField(_('Start'))
590 end = models.TimeField(_('End'))
591 text = RichTextField(_('Description'), null=True, blank=True)
592 redirect_path = models.CharField(_('Redirect Path'), max_length=200, blank=True)
598 @python_2_unicode_compatible
599 class Focus(models.Model):
600 title = models.CharField(_('Alternate Title'), max_length=50,
601 null=True, blank=True)
602 newsitem = models.ForeignKey('NewsItem', verbose_name=_('News Item'),
603 null=True, blank=True)
604 emission = models.ForeignKey('Emission', verbose_name=_('Emission'),
605 null=True, blank=True)
606 episode = models.ForeignKey('Episode', verbose_name=_('Episode'),
607 null=True, blank=True)
608 soundfile = models.ForeignKey('SoundFile', verbose_name=_('Sound file'),
609 null=True, blank=True)
610 page = models.ForeignKey('data.Page', verbose_name=_('Page'),
611 null=True, blank=True)
612 current = models.BooleanField('Current', default=True)
614 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
615 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
619 return u'Newsitem: %s' % self.newsitem.title
621 return u'Emission: %s' % self.emission.title
623 return u'Episode: %s' % self.episode.title
625 return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
627 return u'Page: %s' % self.page.title
628 return u'%s' % self.id
630 def focus_title(self):
634 return self.newsitem.title
636 return self.emission.title
638 return self.page.title
642 if self.soundfile.fragment and self.soundfile.title:
643 return self.soundfile.title
644 episode = self.soundfile.episode
646 episode = self.episode
652 return episode.emission.title
656 def content_image(self):
658 return self.newsitem.image
660 return self.emission.image
662 from panikombo.models import Topik
663 return Topik.objects.get(page=self.page).image
667 episode = self.soundfile.episode
669 episode = self.episode
675 return episode.emission.image
677 def content_category_title(self):
679 if self.newsitem.category:
680 return self.newsitem.category.title
685 return self.episode.emission.title
687 return self.soundfile.episode.emission.title
691 def get_related_object(self):
700 return self.soundfile
702 from panikombo.models import Topik
703 return Topik.objects.get(page=self.page)
704 except ObjectDoesNotExist:
710 def get_playlist_sound_path(instance, filename):
711 return os.path.join('playlists', instance.episode.emission.slug,
712 instance.episode.slug, os.path.basename(filename))
715 class PlaylistElement(models.Model):
716 episode = models.ForeignKey('Episode', null=True)
717 title = models.CharField(_('Title'), max_length=200)
718 notes = models.CharField(_('Notes'), max_length=200, blank=True, default='')
719 sound = models.FileField(_('Sound'), upload_to=get_playlist_sound_path, max_length=250)
720 order = models.PositiveIntegerField()
723 verbose_name = _('Playlist Element')
724 verbose_name_plural = _('Playlist Elements')
728 return chr(ord('a')+self.order-1)
731 @receiver(pre_save, sender=Focus, dispatch_uid='focus_pre_save')
732 def set_focus_on_save(sender, instance, **kwargs):
733 object = instance.get_related_object()
735 if instance.current != object.has_focus:
736 object.has_focus = instance.current
738 if object and not object.got_focus and instance.current:
739 object.got_focus = datetime.datetime.now()
744 @receiver(post_delete, sender=Focus, dispatch_uid='focus_post_delete')
745 def remove_focus_on_delete(sender, instance, **kwargs):
746 object = instance.get_related_object()
747 if object and (object.got_focus or object.has_focus):
748 object.got_focus = None
749 object.has_focus = False