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):
46 def get_weekday(self):
47 weekday = self.datetime.weekday() + 7
48 if self.datetime.time() < datetime.time(self.DAY_HOUR_START, self.DAY_MINUTE_START):
53 def is_on_weekday(self, day): # day is [1..7]
54 week_day = self.datetime.weekday()
55 if (self.datetime.hour, self.datetime.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START):
57 week_day = (week_day % 7) + 1
58 if hasattr(self, 'episode'):
59 if (self.datetime.hour, self.datetime.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START) and \
60 (self.end_datetime.hour, self.end_datetime.minute) >= (self.DAY_HOUR_START, self.DAY_MINUTE_START):
61 if (self.end_datetime.weekday()+1) == day:
63 return week_day == day
66 @python_2_unicode_compatible
67 class Category(models.Model):
70 verbose_name = _('Category')
71 verbose_name_plural = _('Categories')
74 title = models.CharField(_('Title'), max_length=50)
75 slug = models.SlugField(null=True)
76 itunes_category = models.CharField(_('iTunes Category Name'), max_length=100, null=True)
78 def sorted_emission(self):
79 return self.emission_set.order_by('title')
85 @python_2_unicode_compatible
86 class Colour(models.Model):
89 verbose_name = _('Colour')
90 verbose_name_plural = _('Colours')
93 title = models.CharField(_('Title'), max_length=50)
94 slug = models.SlugField(null=True)
96 def sorted_emission(self):
97 return self.emission_set.order_by('title')
103 @python_2_unicode_compatible
104 class Format(models.Model):
107 verbose_name = _('Format')
108 verbose_name_plural = _('Formats')
111 title = models.CharField(_('Title'), max_length=50)
112 slug = models.SlugField(null=True)
118 def get_emission_image_path(instance, filename):
119 return os.path.join('images', instance.slug,
120 os.path.basename(filename))
123 @python_2_unicode_compatible
124 class Emission(models.Model):
127 verbose_name = _('Emission')
128 verbose_name_plural = _('Emissions')
131 title = models.CharField(_('Title'), max_length=200)
132 slug = models.SlugField(max_length=200)
133 subtitle = models.CharField(_('Subtitle'), max_length=80, null=True, blank=True)
134 text = RichTextField(_('Description'), null=True)
135 archived = models.BooleanField(_('Archived'), default=False)
136 categories = models.ManyToManyField(Category, verbose_name=_('Categories'), blank=True)
137 colours = models.ManyToManyField(Colour, verbose_name=_('Colours'), blank=True)
139 # XXX: languages (models.ManyToManyField(Language))
141 duration = models.IntegerField(_('Duration'), default=60,
142 help_text=_('In minutes'))
144 default_license = models.CharField(_('Default license for podcasts'),
145 max_length=20, blank=True, default='', choices=LICENSES)
146 podcast_sound_quality = models.CharField(_('Podcast sound quality'),
147 max_length=20, default='standard', choices=PODCAST_SOUND_QUALITY_LIST)
148 email = models.EmailField(_('Email'), max_length=254, null=True, blank=True)
149 website = models.URLField(_('Website'), null=True, blank=True)
151 image = models.ImageField(_('Image'),
152 upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
154 chat_open = models.DateTimeField(null=True, blank=True)
156 # denormalized from Focus
157 got_focus = models.DateTimeField(default=None, null=True, blank=True)
158 has_focus = models.BooleanField(default=False)
160 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
161 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
163 def get_absolute_url(self):
164 return reverse('emission-view', kwargs={'slug': str(self.slug)})
169 def save(self, *args, **kwargs):
170 super(Emission, self).save(*args, **kwargs)
171 if self.id is not None and self.image:
172 maybe_resize(self.image.path)
174 def get_absolute_url(self):
175 return reverse('emission-view',
176 kwargs={'slug': str(self.slug)})
178 def get_schedules(self):
179 return Schedule.objects.filter(emission=self).order_by('datetime')
181 def get_sorted_episodes(self):
182 return self.episode_set.select_related().extra(select={
183 'first_diffusion': 'emissions_diffusion.datetime',
185 select_params=(False, True),
186 where=['''datetime = (SELECT MIN(datetime)
187 FROM emissions_diffusion
188 WHERE episode_id = emissions_episode.id)'''],
189 tables=['emissions_diffusion'],
190 ).order_by('-first_diffusion')
192 def get_sorted_newsitems(self):
193 return self.newsitem_set.select_related().order_by('-date')
195 def get_next_planned_date_and_schedule(self, since=None):
196 schedules = self.schedule_set.filter(rerun=False)
200 since = datetime.datetime.today()
202 for schedule in schedules:
203 possible_dates.append((schedule.get_next_planned_date(since), schedule))
204 possible_dates.sort(key=lambda x: x[0])
205 return possible_dates[0]
207 def get_next_planned_date(self, since=None):
208 result = self.get_next_planned_date_and_schedule(since=since)
209 return result[0] if result else None
211 def get_next_planned_duration(self, since=None):
212 result = self.get_next_planned_date_and_schedule(since=since)
215 return result[1].duration or self.duration
217 @python_2_unicode_compatible
218 class Schedule(models.Model, WeekdayMixin):
221 verbose_name = _('Schedule')
222 verbose_name_plural = _('Schedules')
223 ordering = ['datetime']
226 (0b1111, _('Every week')),
227 (0b0001, _('First week')),
228 (0b0010, _('Second week')),
229 (0b0100, _('Third week')),
230 (0b1000, _('Fourth week')),
231 (0b0101, _('First and third week')),
232 (0b1010, _('Second and fourth week'))
234 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
235 datetime = models.DateTimeField()
236 weeks = models.IntegerField(_('Weeks'), default=15, choices=WEEK_CHOICES)
237 rerun = models.BooleanField(_('Rerun'), default=False)
238 duration = models.IntegerField(_('Duration'), null=True, blank=True,
239 help_text=_('In minutes'))
242 def weeks_string(self):
243 if self.weeks == 0b0001:
244 return ugettext('1st of the month')
245 elif self.weeks == 0b0010:
246 return ugettext('2nd of the month')
247 elif self.weeks == 0b0100:
248 return ugettext('3rd of the month')
249 elif self.weeks == 0b1000:
250 return ugettext('4th of the month')
251 elif self.weeks == 0b0101:
252 return ugettext('1st and 3rd')
253 elif self.weeks == 0b1010:
254 return ugettext('2nd and 4th')
257 def week_sort_key(self):
261 0b0101, # First and third week
262 0b0010, # Second week
263 0b1010, # Second and fourth week
265 0b1000, # Fourth week
267 if self.weeks in order:
268 return order.index(self.weeks)
271 def get_duration(self):
275 return self.emission.duration
278 def end_datetime(self):
279 return self.datetime + datetime.timedelta(minutes=self.get_duration())
281 def match_week(self, week_no):
283 # this is the fifth week of the month, only return True for
284 # emissions scheduled every week.
285 return (self.weeks == 0b1111)
286 if (self.weeks & (0b0001<<(week_no)) == 0):
290 def matches(self, dt):
291 weekday = dt.weekday()
292 if (dt.hour, dt.minute) < (self.DAY_HOUR_START, self.DAY_MINUTE_START):
294 if weekday != self.get_weekday():
296 if self.weeks != 0b1111:
297 week_no = (dt.day-1) // 7
298 if self.match_week(week_no) is False:
300 if dt.time() >= self.datetime.time() and \
301 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
305 def get_next_planned_date(self, since):
307 monday_since_date = since - datetime.timedelta(days=since.weekday())
308 start_week_date = self.datetime.replace(
309 year=monday_since_date.year,
310 month=monday_since_date.month,
311 day=monday_since_date.day)
312 start_week_date -= datetime.timedelta(days=start_week_date.weekday())
313 start_week_date += datetime.timedelta(days=self.datetime.weekday())
315 week_date = start_week_date + datetime.timedelta(days=i*7)
316 if week_date < since:
318 if self.match_week((week_date.day-1)//7):
319 possible_dates.append(week_date)
320 possible_dates.sort()
321 return possible_dates[0]
325 return u'%s at %s' % (self.emission.title,
326 self.datetime.strftime('%a %H:%M'))
329 def get_episode_image_path(instance, filename):
330 return os.path.join('images', instance.emission.slug,
331 os.path.basename(filename))
334 @python_2_unicode_compatible
335 class Episode(models.Model):
338 verbose_name = _('Episode')
339 verbose_name_plural = _('Episodes')
342 emission = models.ForeignKey('Emission', verbose_name=_('Emission'))
343 title = models.CharField(_('Title'), max_length=200)
344 slug = models.SlugField(max_length=200)
345 subtitle = models.CharField(_('Subtitle'), max_length=150, null=True, blank=True)
346 text = RichTextField(_('Description'), null=True)
347 tags = TaggableManager(_('Tags'), blank=True)
348 duration = models.IntegerField(_('Duration'), null=True, blank=True,
349 help_text=_('In minutes'))
351 image = models.ImageField(_('Image'),
352 upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
354 effective_start = models.DateTimeField(null=True, blank=True)
355 effective_end = models.DateTimeField(null=True, blank=True)
357 # denormalized from Focus
358 got_focus = models.DateTimeField(default=None, null=True, blank=True)
359 has_focus = models.BooleanField(default=False)
361 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
362 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
364 # XXX: languages (models.ManyToManyField(Language))
369 def save(self, *args, **kwargs):
370 super(Episode, self).save(*args, **kwargs)
371 if self.id is not None and self.image:
372 maybe_resize(self.image.path)
374 def get_duration(self):
378 return self.emission.duration
380 def get_absolute_url(self):
381 return reverse('episode-view',
382 kwargs={'emission_slug': str(self.emission.slug),
383 'slug': str(self.slug)})
386 return (self.soundfile_set.count() > 0)
388 def get_pige_download_url(self):
389 return '%s/%s-%s-%s.wav' % (
390 settings.PIGE_DOWNLOAD_BASE_URL,
391 self.effective_start.strftime('%Y%m%d'),
392 self.effective_start.strftime('%Hh%Mm%S.%f'),
393 self.effective_end.strftime('%Hh%Mm%S.%f'))
398 def set_prefetched_soundfiles(cls, soundfiles):
399 cls._soundfiles.update(soundfiles)
401 def has_prefetched_soundfile(self):
402 return self.id in self._soundfiles
404 def get_prefetched_soundfile(self):
405 return self._soundfiles.get(self.id)
410 def main_sound(self):
411 if self._main_sound is not False:
412 return self._main_sound
414 if self.has_prefetched_soundfile():
415 self._main_sound = self.get_prefetched_soundfile()
416 return self._main_sound
418 t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
420 self._main_sound = t[0]
422 self._main_sound = None
424 return self._main_sound
427 def main_sound(self, value):
428 self._main_sound = value
430 def fragment_sounds(self):
431 return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
433 def diffusions(self):
434 return Diffusion.objects.filter(episode=self.id).order_by('datetime')
437 @python_2_unicode_compatible
438 class Diffusion(models.Model, WeekdayMixin):
441 verbose_name = _('Diffusion')
442 verbose_name_plural = _('Diffusions')
443 ordering = ['datetime']
445 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
446 datetime = models.DateTimeField(_('Date/time'), db_index=True)
449 return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
450 self.datetime.minute)
452 def get_duration(self):
453 return self.episode.get_duration()
456 def end_datetime(self):
457 return self.datetime + datetime.timedelta(minutes=self.get_duration())
460 @python_2_unicode_compatible
461 class Absence(models.Model):
464 verbose_name = _('Absence')
465 verbose_name_plural = _('Absences')
467 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
468 datetime = models.DateTimeField(_('Date/time'), db_index=True)
471 return u'Absence for %s on %s' % (self.emission.title, self.datetime)
474 def get_sound_path(instance, filename):
475 return os.path.join('sounds.orig', instance.episode.emission.slug,
476 os.path.basename(filename))
479 @python_2_unicode_compatible
480 class SoundFile(models.Model):
483 verbose_name = _('Sound file')
484 verbose_name_plural = _('Sound files')
485 ordering = ['creation_timestamp']
487 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
488 file = models.FileField(_('File'), upload_to=get_sound_path, max_length=250)
489 podcastable = models.BooleanField(_('Podcastable'), default=False,
491 help_text=_('The file can be published online according to SABAM rules.'))
492 fragment = models.BooleanField(_('Fragment'), default=False, db_index=True,
493 help_text=_('The file is some segment or extra content, not the complete recording.'))
494 title = models.CharField(_('Title'), max_length=200)
495 duration = models.IntegerField(_('Duration'), null=True, help_text=_('In seconds'))
497 format = models.ForeignKey('Format', verbose_name=_('Format'), null=True, blank=True)
498 license = models.CharField(_('License'), max_length=20, blank=True, default='', choices=LICENSES)
500 # denormalized from Focus
501 got_focus = models.DateTimeField(default=None, null=True, blank=True)
502 has_focus = models.BooleanField(default=False)
504 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
505 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
507 def compute_duration(self):
508 for path in (self.get_format_path('ogg'), self.get_format_path('mp3'), self.file.path):
509 self.duration = get_duration(path)
513 def get_format_filename(self, format):
514 return '%s_%05d__%s.%s' % (
517 (self.fragment and '0' or '1'),
520 def get_format_path(self, format):
523 return '%s/%s' % (os.path.dirname(self.file.path).replace('.orig', ''), self.get_format_filename(format))
525 def get_format_url(self, format):
528 return '%s/%s' % (os.path.dirname(self.file.url).replace('.orig', ''), self.get_format_filename(format))
530 def get_duration_string(self):
531 if not self.duration:
533 return '%d:%02d' % (self.duration/60, self.duration%60)
536 return '%s - %s' % (self.title or self.id, self.episode.title)
539 @python_2_unicode_compatible
540 class NewsCategory(models.Model):
543 verbose_name = _('News Category')
544 verbose_name_plural = _('News Categories')
547 title = models.CharField(_('Title'), max_length=50)
548 slug = models.SlugField(null=True)
553 def get_sorted_newsitems(self):
554 return self.newsitem_set.select_related().order_by('-date')
557 def get_newsitem_image_path(instance, filename):
558 return os.path.join('images', 'news', instance.slug,
559 os.path.basename(filename))
562 @python_2_unicode_compatible
563 class NewsItem(models.Model):
566 verbose_name = _('News Item')
567 verbose_name_plural = _('News Items')
570 title = models.CharField(_('Title'), max_length=200)
571 slug = models.SlugField(max_length=200)
572 text = RichTextField(_('Description'))
573 date = models.DateField(_('Publication Date'),
574 help_text=_('The news won\'t appear on the website before this date.'))
575 image = models.ImageField(_('Image'),
576 upload_to=get_newsitem_image_path, max_length=250, null=True, blank=True)
578 tags = TaggableManager(_('Tags'), blank=True)
579 category = models.ForeignKey('NewsCategory', verbose_name=_('Category'), null=True, blank=True)
580 emission = models.ForeignKey('Emission', verbose_name=_('Emission'), null=True, blank=True)
582 expiration_date = models.DateField(_('Expiration Date'), null=True, blank=True)
583 event_date = models.DateField(_('Event Date'), null=True, blank=True,
584 help_text=_('If this is an event, set the date here so it appears in the agenda.'))
586 # denormalized from Focus
587 got_focus = models.DateTimeField(default=None, null=True, blank=True)
588 has_focus = models.BooleanField(default=False)
590 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
591 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
596 def get_absolute_url(self):
597 return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
600 @python_2_unicode_compatible
601 class Nonstop(models.Model):
604 verbose_name = _('Nonstop zone')
605 verbose_name_plural = _('Nonstop zones')
608 title = models.CharField(_('Title'), max_length=50)
609 slug = models.SlugField()
611 start = models.TimeField(_('Start'))
612 end = models.TimeField(_('End'))
613 text = RichTextField(_('Description'), null=True, blank=True)
614 redirect_path = models.CharField(_('Redirect Path'), max_length=200, blank=True)
620 @python_2_unicode_compatible
621 class Focus(models.Model):
622 title = models.CharField(_('Alternate Title'), max_length=50,
623 null=True, blank=True)
624 newsitem = models.ForeignKey('NewsItem', verbose_name=_('News Item'),
625 null=True, blank=True)
626 emission = models.ForeignKey('Emission', verbose_name=_('Emission'),
627 null=True, blank=True)
628 episode = models.ForeignKey('Episode', verbose_name=_('Episode'),
629 null=True, blank=True)
630 soundfile = models.ForeignKey('SoundFile', verbose_name=_('Sound file'),
631 null=True, blank=True)
632 page = models.ForeignKey('data.Page', verbose_name=_('Page'),
633 null=True, blank=True)
634 current = models.BooleanField('Current', default=True)
636 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
637 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
641 return u'Newsitem: %s' % self.newsitem.title
643 return u'Emission: %s' % self.emission.title
645 return u'Episode: %s' % self.episode.title
647 return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
649 return u'Page: %s' % self.page.title
650 return u'%s' % self.id
652 def focus_title(self):
656 return self.newsitem.title
658 return self.emission.title
660 return self.page.title
664 if self.soundfile.fragment and self.soundfile.title:
665 return self.soundfile.title
666 episode = self.soundfile.episode
668 episode = self.episode
674 return episode.emission.title
678 def content_image(self):
680 return self.newsitem.image
682 return self.emission.image
684 return self.page.picture
688 episode = self.soundfile.episode
690 episode = self.episode
696 return episode.emission.image
698 def content_category_title(self):
700 if self.newsitem.category:
701 return self.newsitem.category.title
706 return self.episode.emission.title
708 return self.soundfile.episode.emission.title
712 def get_related_object(self):
721 return self.soundfile
723 from panikombo.models import Topik
724 return Topik.objects.get(page=self.page)
725 except ObjectDoesNotExist:
731 def get_playlist_sound_path(instance, filename):
732 return os.path.join('playlists', instance.episode.emission.slug,
733 instance.episode.slug, os.path.basename(filename))
736 class PlaylistElement(models.Model):
737 episode = models.ForeignKey('Episode', null=True)
738 title = models.CharField(_('Title'), max_length=200)
739 notes = models.CharField(_('Notes'), max_length=200, blank=True, default='')
740 sound = models.FileField(_('Sound'), upload_to=get_playlist_sound_path, max_length=250)
741 order = models.PositiveIntegerField()
744 verbose_name = _('Playlist Element')
745 verbose_name_plural = _('Playlist Elements')
749 return chr(ord('a')+self.order-1)
752 @receiver(pre_save, sender=Focus, dispatch_uid='focus_pre_save')
753 def set_focus_on_save(sender, instance, **kwargs):
754 object = instance.get_related_object()
756 if instance.current != object.has_focus:
757 object.has_focus = instance.current
759 if object and not object.got_focus and instance.current:
760 object.got_focus = datetime.datetime.now()
765 @receiver(post_delete, sender=Focus, dispatch_uid='focus_post_delete')
766 def remove_focus_on_delete(sender, instance, **kwargs):
767 object = instance.get_related_object()
768 if object and (object.got_focus or object.has_focus):
769 object.got_focus = None
770 object.has_focus = False