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.translation import ugettext
11 from django.utils.translation import ugettext_lazy as _
13 from django.db.models.signals import pre_save, post_delete
14 from django.dispatch.dispatcher import receiver
16 from ckeditor.fields import RichTextField
17 from taggit.managers import TaggableManager
19 from utils import maybe_resize
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')),
35 class WeekdayMixin(object):
38 def get_weekday(self):
39 weekday = self.datetime.weekday() + 7
40 if self.datetime.time() < datetime.time(self.DAY_HOUR_START, 0):
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:
49 week_day = (week_day % 7) + 1
50 return week_day == day
53 class Category(models.Model):
56 verbose_name = _('Category')
57 verbose_name_plural = _('Categories')
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)
64 def sorted_emission(self):
65 return self.emission_set.order_by('title')
67 def __unicode__(self):
71 class Colour(models.Model):
74 verbose_name = _('Colour')
75 verbose_name_plural = _('Colours')
78 title = models.CharField(_('Title'), max_length=50)
79 slug = models.SlugField(null=True)
81 def sorted_emission(self):
82 return self.emission_set.order_by('title')
84 def __unicode__(self):
88 class Format(models.Model):
91 verbose_name = _('Format')
92 verbose_name_plural = _('Formats')
95 title = models.CharField(_('Title'), max_length=50)
96 slug = models.SlugField(null=True)
98 def __unicode__(self):
102 def get_emission_image_path(instance, filename):
103 return os.path.join('images', instance.slug,
104 os.path.basename(filename))
107 class Emission(models.Model):
110 verbose_name = _('Emission')
111 verbose_name_plural = _('Emissions')
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)
122 # XXX: languages (models.ManyToManyField(Language))
124 duration = models.IntegerField(_('Duration'), default=60,
125 help_text=_('In minutes'))
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)
132 image = models.ImageField(_('Image'),
133 upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
135 chat_open = models.DateTimeField(null=True, blank=True)
137 # denormalized from Focus
138 got_focus = models.DateTimeField(default=None, null=True, blank=True)
139 has_focus = models.BooleanField(default=False)
141 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
142 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
144 def get_absolute_url(self):
145 return reverse('emission-view', kwargs={'slug': str(self.slug)})
147 def __unicode__(self):
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)
155 def get_absolute_url(self):
156 return reverse('emission-view',
157 kwargs={'slug': str(self.slug)})
159 def get_schedules(self):
160 return Schedule.objects.filter(emission=self).order_by('datetime')
162 def get_sorted_episodes(self):
163 return self.episode_set.select_related().extra(select={
164 'first_diffusion': 'emissions_diffusion.datetime',
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')
173 def get_sorted_newsitems(self):
174 return self.newsitem_set.select_related().order_by('-date')
176 def get_next_planned_date(self, since=None):
177 schedules = self.schedule_set.filter(rerun=False)
181 since = datetime.datetime.today()
183 for schedule in schedules:
184 possible_dates.append(schedule.get_next_planned_date(since))
185 possible_dates.sort()
186 return possible_dates[0]
189 class Schedule(models.Model, WeekdayMixin):
192 verbose_name = _('Schedule')
193 verbose_name_plural = _('Schedules')
194 ordering = ['datetime']
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'))
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'))
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')
228 def get_duration(self):
232 return self.emission.duration
234 def match_week(self, week_no):
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):
243 def matches(self, dt):
244 weekday = dt.weekday()
245 if dt.hour < self.DAY_HOUR_START:
247 if weekday != self.get_weekday():
249 if self.weeks != 0b1111:
250 week_no = (dt.day-1) // 7
251 if self.match_week(week_no) is False:
253 if dt.time() >= self.datetime.time() and \
254 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
258 def get_next_planned_date(self, since):
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())
268 week_date = start_week_date + datetime.timedelta(days=i*7)
269 if week_date < since:
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]
277 def __unicode__(self):
278 return u'%s at %s' % (self.emission.title,
279 self.datetime.strftime('%a %H:%M'))
282 def get_episode_image_path(instance, filename):
283 return os.path.join('images', instance.emission.slug,
284 os.path.basename(filename))
287 class Episode(models.Model):
290 verbose_name = _('Episode')
291 verbose_name_plural = _('Episodes')
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'))
303 image = models.ImageField(_('Image'),
304 upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
306 effective_start = models.DateTimeField(null=True, blank=True)
307 effective_end = models.DateTimeField(null=True, blank=True)
309 # denormalized from Focus
310 got_focus = models.DateTimeField(default=None, null=True, blank=True)
311 has_focus = models.BooleanField(default=False)
313 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
314 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
316 # XXX: languages (models.ManyToManyField(Language))
318 def __unicode__(self):
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)
326 def get_duration(self):
330 return self.emission.duration
332 def get_absolute_url(self):
333 return reverse('episode-view',
334 kwargs={'emission_slug': str(self.emission.slug),
335 'slug': str(self.slug)})
338 return (self.soundfile_set.count() > 0)
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'))
350 def set_prefetched_soundfiles(cls, soundfiles):
351 cls._soundfiles.update(soundfiles)
353 def has_prefetched_soundfile(self):
354 return self.id in self._soundfiles
356 def get_prefetched_soundfile(self):
357 return self._soundfiles.get(self.id)
362 def main_sound(self):
363 if self._main_sound is not False:
364 return self._main_sound
366 if self.has_prefetched_soundfile():
367 self._main_sound = self.get_prefetched_soundfile()
368 return self._main_sound
370 t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
372 self._main_sound = t[0]
374 self._main_sound = None
376 return self._main_sound
379 def main_sound(self, value):
380 self._main_sound = value
382 def fragment_sounds(self):
383 return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
385 def diffusions(self):
386 return Diffusion.objects.filter(episode=self.id).order_by('datetime')
389 class Diffusion(models.Model, WeekdayMixin):
392 verbose_name = _('Diffusion')
393 verbose_name_plural = _('Diffusions')
395 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
396 datetime = models.DateTimeField(_('Date/time'), db_index=True)
398 def __unicode__(self):
399 return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
400 self.datetime.minute)
402 def get_duration(self):
403 return self.episode.get_duration()
405 def end_datetime(self):
406 return self.datetime + datetime.timedelta(minutes=self.get_duration())
409 class Absence(models.Model):
412 verbose_name = _('Absence')
413 verbose_name_plural = _('Absences')
415 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
416 datetime = models.DateTimeField(_('Date/time'), db_index=True)
418 def __unicode__(self):
419 return u'Absence for %s on %s' % (self.emission.title, self.datetime)
422 def get_sound_path(instance, filename):
423 return os.path.join('sounds.orig', instance.episode.emission.slug,
424 os.path.basename(filename))
426 class SoundFile(models.Model):
429 verbose_name = _('Sound file')
430 verbose_name_plural = _('Sound files')
431 ordering = ['creation_timestamp']
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,
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'))
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)
446 # denormalized from Focus
447 got_focus = models.DateTimeField(default=None, null=True, blank=True)
448 has_focus = models.BooleanField(default=False)
450 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
451 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
453 def get_format_filename(self, format):
454 return '%s_%05d__%s.%s' % (
457 (self.fragment and '0' or '1'),
460 def get_format_path(self, format):
463 return '%s/%s' % (os.path.dirname(self.file.path).replace('.orig', ''), self.get_format_filename(format))
465 def get_format_url(self, format):
468 return '%s/%s' % (os.path.dirname(self.file.url).replace('.orig', ''), self.get_format_filename(format))
470 def get_duration_string(self):
471 if not self.duration:
473 return '%d:%02d' % (self.duration/60, self.duration%60)
475 def __unicode__(self):
476 return '%s - %s' % (self.title or self.id, self.episode.title)
479 class NewsCategory(models.Model):
482 verbose_name = _('News Category')
483 verbose_name_plural = _('News Categories')
486 title = models.CharField(_('Title'), max_length=50)
487 slug = models.SlugField(null=True)
489 def __unicode__(self):
492 def get_sorted_newsitems(self):
493 return self.newsitem_set.select_related().order_by('-date')
496 def get_newsitem_image_path(instance, filename):
497 return os.path.join('images', 'news', instance.slug,
498 os.path.basename(filename))
501 class NewsItem(models.Model):
504 verbose_name = _('News Item')
505 verbose_name_plural = _('News Items')
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)
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)
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.'))
524 # denormalized from Focus
525 got_focus = models.DateTimeField(default=None, null=True, blank=True)
526 has_focus = models.BooleanField(default=False)
528 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
529 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
531 def __unicode__(self):
534 def get_absolute_url(self):
535 return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
538 class Nonstop(models.Model):
541 verbose_name = _('Nonstop zone')
542 verbose_name_plural = _('Nonstop zones')
545 title = models.CharField(_('Title'), max_length=50)
546 slug = models.SlugField()
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)
553 def __unicode__(self):
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)
572 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
573 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
575 def __unicode__(self):
577 return u'Newsitem: %s' % self.newsitem.title
579 return u'Emission: %s' % self.emission.title
581 return u'Episode: %s' % self.episode.title
583 return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
585 return u'Page: %s' % self.page.title
586 return u'%s' % self.id
588 def focus_title(self):
592 return self.newsitem.title
594 return self.emission.title
596 return self.page.title
600 if self.soundfile.fragment and self.soundfile.title:
601 return self.soundfile.title
602 episode = self.soundfile.episode
604 episode = self.episode
610 return episode.emission.title
614 def content_image(self):
616 return self.newsitem.image
618 return self.emission.image
620 from panikombo.models import Topik
621 return Topik.objects.get(page=self.page).image
625 episode = self.soundfile.episode
627 episode = self.episode
633 return episode.emission.image
635 def content_category_title(self):
637 if self.newsitem.category:
638 return self.newsitem.category.title
643 return self.episode.emission.title
645 return self.soundfile.episode.emission.title
649 def get_related_object(self):
658 return self.soundfile
660 from panikombo.models import Topik
661 return Topik.objects.get(page=self.page)
662 except ObjectDoesNotExist:
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))
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()
681 verbose_name = _('Playlist Element')
682 verbose_name_plural = _('Playlist Elements')
686 return chr(ord('a')+self.order-1)
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()
693 if instance.current != object.has_focus:
694 object.has_focus = instance.current
696 if object and not object.got_focus and instance.current:
697 object.got_focus = datetime.datetime.now()
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