4 from django.core.exceptions import ObjectDoesNotExist
5 from django.forms import fields
6 from django.core.urlresolvers import reverse
8 from django.db import models
9 from django.utils.translation import ugettext
10 from django.utils.translation import ugettext_lazy as _
12 from django.db.models.signals import pre_save, post_delete
13 from django.dispatch.dispatcher import receiver
15 from ckeditor.fields import RichTextField
16 from taggit.managers import TaggableManager
18 from utils import maybe_resize
21 class WeekdayMixin(object):
24 def get_weekday(self):
25 weekday = self.datetime.weekday() + 7
26 if self.datetime.time() < datetime.time(self.DAY_HOUR_START, 0):
31 def is_on_weekday(self, day): # day is [1..7]
32 week_day = self.datetime.weekday()
33 if self.datetime.hour < self.DAY_HOUR_START:
35 week_day = (week_day % 7) + 1
36 return week_day == day
39 class Category(models.Model):
42 verbose_name = _('Category')
43 verbose_name_plural = _('Categories')
46 title = models.CharField(_('Title'), max_length=50)
47 slug = models.SlugField(null=True)
49 def sorted_emission(self):
50 return self.emission_set.order_by('title')
52 def __unicode__(self):
56 class Colour(models.Model):
59 verbose_name = _('Colour')
60 verbose_name_plural = _('Colours')
63 title = models.CharField(_('Title'), max_length=50)
64 slug = models.SlugField(null=True)
66 def sorted_emission(self):
67 return self.emission_set.order_by('title')
69 def __unicode__(self):
73 class Format(models.Model):
76 verbose_name = _('Format')
77 verbose_name_plural = _('Formats')
80 title = models.CharField(_('Title'), max_length=50)
81 slug = models.SlugField(null=True)
83 def __unicode__(self):
87 def get_emission_image_path(instance, filename):
88 return os.path.join('images', instance.slug,
89 os.path.basename(filename))
92 class Emission(models.Model):
95 verbose_name = _('Emission')
96 verbose_name_plural = _('Emissions')
99 title = models.CharField(_('Title'), max_length=200)
100 slug = models.SlugField(max_length=200)
101 subtitle = models.CharField(_('Subtitle'), max_length=80, null=True, blank=True)
102 text = RichTextField(_('Description'), null=True)
103 archived = models.BooleanField(_('Archived'), default=False)
104 categories = models.ManyToManyField(Category, verbose_name=_('Categories'), null=True, blank=True)
105 colours = models.ManyToManyField(Colour, verbose_name=_('Colours'), null=True, blank=True)
107 # XXX: languages (models.ManyToManyField(Language))
109 duration = models.IntegerField(_('Duration'), default=60,
110 help_text=_('In minutes'))
112 email = models.EmailField(_('Email'), max_length=254, null=True, blank=True)
113 website = models.URLField(_('Website'), null=True, blank=True)
115 image = models.ImageField(_('Image'),
116 upload_to=get_emission_image_path, max_length=250, null=True, blank=True)
118 chat_open = models.DateTimeField(null=True)
120 # denormalized from Focus
121 got_focus = models.DateTimeField(default=None, null=True, blank=True)
122 has_focus = models.BooleanField(default=False)
124 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
125 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
127 def get_absolute_url(self):
128 return reverse('emission-view', kwargs={'slug': str(self.slug)})
130 def __unicode__(self):
133 def save(self, *args, **kwargs):
134 super(Emission, self).save(*args, **kwargs)
135 if self.id is not None and self.image:
136 maybe_resize(self.image.path)
138 def get_absolute_url(self):
139 return reverse('emission-view',
140 kwargs={'slug': str(self.slug)})
142 def get_schedules(self):
143 return Schedule.objects.filter(emission=self).order_by('datetime')
145 def get_sorted_episodes(self):
146 return self.episode_set.select_related().extra(select={
147 'first_diffusion': 'emissions_diffusion.datetime',
149 select_params=(False, True),
150 where=['''datetime = (SELECT MIN(datetime)
151 FROM emissions_diffusion
152 WHERE episode_id = emissions_episode.id)'''],
153 tables=['emissions_diffusion'],
154 ).order_by('-first_diffusion')
156 def get_sorted_newsitems(self):
157 return self.newsitem_set.select_related().order_by('-date')
159 def get_next_planned_date(self, since=None):
160 schedules = self.schedule_set.filter(rerun=False)
164 since = datetime.datetime.today()
166 for schedule in schedules:
167 possible_dates.append(schedule.get_next_planned_date(since))
168 possible_dates.sort()
169 return possible_dates[0]
172 class Schedule(models.Model, WeekdayMixin):
175 verbose_name = _('Schedule')
176 verbose_name_plural = _('Schedules')
177 ordering = ['datetime']
180 (0b1111, _('Every week')),
181 (0b0001, _('First week')),
182 (0b0010, _('Second week')),
183 (0b0100, _('Third week')),
184 (0b1000, _('Fourth week')),
185 (0b0101, _('First and third week')),
186 (0b1010, _('Second and fourth week'))
188 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
189 datetime = models.DateTimeField()
190 weeks = models.IntegerField(_('Weeks'), default=15, choices=WEEK_CHOICES)
191 rerun = models.BooleanField(_('Rerun'), default=False)
192 duration = models.IntegerField(_('Duration'), null=True, blank=True,
193 help_text=_('In minutes'))
196 def weeks_string(self):
197 if self.weeks == 0b0001:
198 return ugettext('1st of the month')
199 elif self.weeks == 0b0010:
200 return ugettext('2nd of the month')
201 elif self.weeks == 0b0100:
202 return ugettext('3rd of the month')
203 elif self.weeks == 0b1000:
204 return ugettext('4th of the month')
205 elif self.weeks == 0b0101:
206 return ugettext('1st and 3rd')
207 elif self.weeks == 0b1010:
208 return ugettext('2nd and 4th')
211 def get_duration(self):
215 return self.emission.duration
217 def match_week(self, week_no):
219 # this is the fifth week of the month, only return True for
220 # emissions scheduled every week.
221 return (self.weeks == 0b1111)
222 if (self.weeks & (0b0001<<(week_no)) == 0):
226 def matches(self, dt):
227 weekday = dt.weekday()
228 if dt.hour < self.DAY_HOUR_START:
230 if weekday != self.get_weekday():
232 if self.weeks != 0b1111:
233 week_no = (dt.day-1) // 7
234 if self.match_week(week_no) is False:
236 if dt.time() >= self.datetime.time() and \
237 dt.time() <= (self.datetime + datetime.timedelta(minutes=self.get_duration())).time():
241 def get_next_planned_date(self, since):
243 monday_since_date = since - datetime.timedelta(days=since.weekday())
244 start_week_date = self.datetime.replace(
245 year=monday_since_date.year,
246 month=monday_since_date.month,
247 day=monday_since_date.day)
248 start_week_date -= datetime.timedelta(days=start_week_date.weekday())
249 start_week_date += datetime.timedelta(days=self.datetime.weekday())
251 week_date = start_week_date + datetime.timedelta(days=i*7)
252 if week_date < since:
254 if self.match_week((week_date.day-1)//7):
255 possible_dates.append(week_date)
256 possible_dates.sort()
257 return possible_dates[0]
260 def __unicode__(self):
261 return u'%s at %s' % (self.emission.title,
262 self.datetime.strftime('%a %H:%M'))
265 def get_episode_image_path(instance, filename):
266 return os.path.join('images', instance.emission.slug,
267 os.path.basename(filename))
270 class Episode(models.Model):
273 verbose_name = _('Episode')
274 verbose_name_plural = _('Episodes')
277 emission = models.ForeignKey('Emission', verbose_name=_('Emission'))
278 title = models.CharField(_('Title'), max_length=200)
279 slug = models.SlugField(max_length=200)
280 subtitle = models.CharField(_('Subtitle'), max_length=150, null=True, blank=True)
281 text = RichTextField(_('Description'), null=True)
282 tags = TaggableManager(_('Tags'), blank=True)
283 duration = models.IntegerField(_('Duration'), null=True, blank=True,
284 help_text=_('In minutes'))
286 image = models.ImageField(_('Image'),
287 upload_to=get_episode_image_path, max_length=250, null=True, blank=True)
289 # denormalized from Focus
290 got_focus = models.DateTimeField(default=None, null=True, blank=True)
291 has_focus = models.BooleanField(default=False)
293 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
294 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
296 # XXX: languages (models.ManyToManyField(Language))
298 def __unicode__(self):
301 def save(self, *args, **kwargs):
302 super(Episode, self).save(*args, **kwargs)
303 if self.id is not None and self.image:
304 maybe_resize(self.image.path)
306 def get_duration(self):
310 return self.emission.duration
312 def get_absolute_url(self):
313 return reverse('episode-view',
314 kwargs={'emission_slug': str(self.emission.slug),
315 'slug': str(self.slug)})
318 return (self.soundfile_set.count() > 0)
324 def set_prefetched_soundfiles(cls, soundfiles):
325 cls._soundfiles.update(soundfiles)
327 def has_prefetched_soundfile(self):
328 return self.id in self._soundfiles
330 def get_prefetched_soundfile(self):
331 return self._soundfiles.get(self.id)
336 def main_sound(self):
337 if self._main_sound is not False:
338 return self._main_sound
340 if self.has_prefetched_soundfile():
341 self._main_sound = self.get_prefetched_soundfile()
342 return self._main_sound
344 t = self.soundfile_set.exclude(podcastable=False).exclude(fragment=True)
346 self._main_sound = t[0]
348 self._main_sound = None
350 return self._main_sound
353 def main_sound(self, value):
354 self._main_sound = value
356 def fragment_sounds(self):
357 return self.soundfile_set.exclude(podcastable=False).exclude(fragment=False)
359 def diffusions(self):
360 return Diffusion.objects.filter(episode=self.id).order_by('datetime')
363 class Diffusion(models.Model, WeekdayMixin):
366 verbose_name = _('Diffusion')
367 verbose_name_plural = _('Diffusions')
369 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
370 datetime = models.DateTimeField(_('Date/time'), db_index=True)
372 def __unicode__(self):
373 return u'%s at %02d:%02d' % (self.episode.title, self.datetime.hour,
374 self.datetime.minute)
376 def get_duration(self):
377 return self.episode.get_duration()
380 class Absence(models.Model):
383 verbose_name = _('Absence')
384 verbose_name_plural = _('Absences')
386 emission = models.ForeignKey('Emission', verbose_name=u'Emission')
387 datetime = models.DateTimeField(_('Date/time'), db_index=True)
389 def __unicode__(self):
390 return u'Absence for %s on %s' % (self.emission.title, self.datetime)
393 def get_sound_path(instance, filename):
394 return os.path.join('sounds', instance.episode.emission.slug,
395 os.path.basename(filename))
397 class SoundFile(models.Model):
400 verbose_name = _('Sound file')
401 verbose_name_plural = _('Sound files')
402 ordering = ['creation_timestamp']
404 episode = models.ForeignKey('Episode', verbose_name=_('Episode'))
405 file = models.FileField(_('File'), upload_to=get_sound_path, max_length=250)
406 podcastable = models.BooleanField(_('Podcastable'), default=False,
408 help_text=_('The file can be published online according to SABAM rules.'))
409 fragment = models.BooleanField(_('Fragment'), default=False, db_index=True,
410 help_text=_('The file is some segment or extra content, not the complete recording.'))
411 title = models.CharField(_('Title'), max_length=200)
412 duration = models.IntegerField(_('Duration'), null=True, help_text=_('In seconds'))
414 format = models.ForeignKey('Format', verbose_name=_('Format'), null=True, blank=True)
416 # denormalized from Focus
417 got_focus = models.DateTimeField(default=None, null=True, blank=True)
418 has_focus = models.BooleanField(default=False)
420 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
421 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
423 def get_format_filename(self, format):
424 return '%s_%05d__%s.%s' % (
427 (self.fragment and '0' or '1'),
430 def get_format_path(self, format):
433 return '%s/%s' % (os.path.dirname(self.file.path), self.get_format_filename(format))
435 def get_format_url(self, format):
438 return '%s/%s' % (os.path.dirname(self.file.url), self.get_format_filename(format))
440 def get_duration_string(self):
441 if not self.duration:
443 return '%d:%02d' % (self.duration/60, self.duration%60)
445 def __unicode__(self):
446 return '%s - %s' % (self.title or self.id, self.episode.title)
449 class NewsCategory(models.Model):
452 verbose_name = _('News Category')
453 verbose_name_plural = _('News Categories')
456 title = models.CharField(_('Title'), max_length=50)
457 slug = models.SlugField(null=True)
459 def __unicode__(self):
462 def get_sorted_newsitems(self):
463 return self.newsitem_set.select_related().order_by('-date')
466 def get_newsitem_image_path(instance, filename):
467 return os.path.join('images', 'news', instance.slug,
468 os.path.basename(filename))
471 class NewsItem(models.Model):
474 verbose_name = _('News Item')
475 verbose_name_plural = _('News Items')
478 title = models.CharField(_('Title'), max_length=200)
479 slug = models.SlugField(max_length=200)
480 text = RichTextField(_('Description'))
481 date = models.DateField(_('Publication Date'),
482 help_text=_('The news won\'t appear on the website before this date.'))
483 image = models.ImageField(_('Image'),
484 upload_to=get_newsitem_image_path, max_length=250, null=True, blank=True)
486 tags = TaggableManager(_('Tags'), blank=True)
487 category = models.ForeignKey('NewsCategory', verbose_name=_('Category'), null=True, blank=True)
488 emission = models.ForeignKey('Emission', verbose_name=_('Emission'), null=True, blank=True)
490 expiration_date = models.DateField(_('Expiration Date'), null=True, blank=True)
491 event_date = models.DateField(_('Event Date'), null=True, blank=True,
492 help_text=_('If this is an event, set the date here so it appears in the agenda.'))
494 # denormalized from Focus
495 got_focus = models.DateTimeField(default=None, null=True, blank=True)
496 has_focus = models.BooleanField(default=False)
498 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
499 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
501 def __unicode__(self):
504 def get_absolute_url(self):
505 return reverse('newsitem-view', kwargs={'slug': str(self.slug)})
508 class Nonstop(models.Model):
511 verbose_name = _('Nonstop zone')
512 verbose_name_plural = _('Nonstop zones')
515 title = models.CharField(_('Title'), max_length=50)
516 slug = models.SlugField()
518 start = models.TimeField(_('Start'))
519 end = models.TimeField(_('End'))
520 text = RichTextField(_('Description'), null=True, blank=True)
522 def __unicode__(self):
526 class Focus(models.Model):
527 title = models.CharField(_('Alternate Title'), max_length=50,
528 null=True, blank=True)
529 newsitem = models.ForeignKey('NewsItem', verbose_name=_('News Item'),
530 null=True, blank=True)
531 emission = models.ForeignKey('Emission', verbose_name=_('Emission'),
532 null=True, blank=True)
533 episode = models.ForeignKey('Episode', verbose_name=_('Episode'),
534 null=True, blank=True)
535 soundfile = models.ForeignKey('SoundFile', verbose_name=_('Sound file'),
536 null=True, blank=True)
537 page = models.ForeignKey('data.Page', verbose_name=_('Page'),
538 null=True, blank=True)
539 current = models.BooleanField('Current', default=True)
541 creation_timestamp = models.DateTimeField(auto_now_add=True, null=True)
542 last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
544 def __unicode__(self):
546 return u'Newsitem: %s' % self.newsitem.title
548 return u'Emission: %s' % self.emission.title
550 return u'Episode: %s' % self.episode.title
552 return u'Soundfile: %s' % (self.soundfile.title or self.soundfile.episode.title)
554 return u'Page: %s' % self.page.title
555 return u'%s' % self.id
557 def focus_title(self):
561 return self.newsitem.title
563 return self.emission.title
565 return self.page.title
569 if self.soundfile.fragment and self.soundfile.title:
570 return self.soundfile.title
571 episode = self.soundfile.episode
573 episode = self.episode
579 return episode.emission.title
583 def content_image(self):
585 return self.newsitem.image
587 return self.emission.image
589 from panikombo.models import Topik
590 return Topik.objects.get(page=self.page).image
594 episode = self.soundfile.episode
596 episode = self.episode
602 return episode.emission.image
604 def content_category_title(self):
606 if self.newsitem.category:
607 return self.newsitem.category.title
612 return self.episode.emission.title
614 return self.soundfile.episode.emission.title
618 def get_related_object(self):
627 return self.soundfile
629 from panikombo.models import Topik
630 return Topik.objects.get(page=self.page)
631 except ObjectDoesNotExist:
637 def get_playlist_sound_path(instance, filename):
638 return os.path.join('playlists', instance.episode.emission.slug,
639 instance.episode.slug, os.path.basename(filename))
642 class PlaylistElement(models.Model):
643 episode = models.ForeignKey('Episode', null=True)
644 title = models.CharField(_('Title'), max_length=200)
645 notes = models.CharField(_('Notes'), max_length=200, blank=True, default='')
646 sound = models.FileField(_('Sound'), upload_to=get_playlist_sound_path, max_length=250)
647 order = models.PositiveIntegerField()
650 verbose_name = _('Playlist Element')
651 verbose_name_plural = _('Playlist Elements')
655 return chr(ord('a')+self.order-1)
658 @receiver(pre_save, sender=Focus, dispatch_uid='focus_pre_save')
659 def set_focus_on_save(sender, instance, **kwargs):
660 object = instance.get_related_object()
662 if instance.current != object.has_focus:
663 object.has_focus = instance.current
665 if object and not object.got_focus and instance.current:
666 object.got_focus = datetime.datetime.now()
671 @receiver(post_delete, sender=Focus, dispatch_uid='focus_post_delete')
672 def remove_focus_on_delete(sender, instance, **kwargs):
673 object = instance.get_related_object()
674 if object and (object.got_focus or object.has_focus):
675 object.got_focus = None
676 object.has_focus = False