]> git.0d.be Git - panikweb.git/blob - panikweb/views.py
e42fd1d01aaa2019b6fd5a5c1ac0732d4107b18b
[panikweb.git] / panikweb / views.py
1 from datetime import datetime, timedelta, date
2 import math
3 import random
4 import os
5 import stat
6 import time
7
8 from django.core.urlresolvers import reverse
9 from django.conf import settings
10 from django.http import Http404, JsonResponse
11 from django.utils.encoding import force_text
12 from django.utils.encoding import python_2_unicode_compatible
13 from django.utils.six.moves.urllib import parse as urlparse
14 from django.views.decorators.cache import cache_control
15 from django.views.generic.base import TemplateView
16 from django.views.generic.detail import DetailView
17 from django.views.decorators.csrf import csrf_exempt
18 from django.views.generic.dates import _date_from_string
19 from django.views.generic.dates import MonthArchiveView
20
21 from django.core.paginator import Paginator
22
23 from django.contrib.sites.models import Site
24 from django.contrib.syndication.views import Feed, add_domain
25 from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
26
27 from haystack.query import SearchQuerySet
28
29 from emissions.models import Category, Emission, Episode, Diffusion, SoundFile, \
30         Schedule, Nonstop, NewsItem, NewsCategory, Focus
31 from emissions.views import EmissionEpisodeMixin
32 from emissions.utils import whatsonair, period_program
33
34 from newsletter.forms import SubscribeForm
35 from nonstop.utils import get_current_nonstop_track
36 from nonstop.models import SomaLogLine
37
38 from panikombo.models import ItemTopik
39
40 from . import utils
41
42
43 class EmissionMixin:
44     def get_emission_context(self, emission, episode_ids=None):
45         context = {}
46
47         # get all episodes, with an additional attribute to get the date of
48         # their first diffusion
49         episodes_queryset = Episode.objects.select_related()
50         if episode_ids is not None:
51             episodes_queryset = episodes_queryset.filter(id__in=episode_ids)
52         else:
53             episodes_queryset = episodes_queryset.filter(emission=emission)
54
55         context['episodes'] = \
56                 episodes_queryset.extra(select={
57                         'first_diffusion': 'emissions_diffusion.datetime',
58                         },
59                         select_params=(False, True),
60                         where=['''datetime = (SELECT MIN(datetime)
61                                                 FROM emissions_diffusion
62                                                WHERE episode_id = emissions_episode.id
63                                                  AND datetime <= CURRENT_TIMESTAMP)'''],
64                         tables=['emissions_diffusion'],
65                     ).order_by('-first_diffusion').distinct()
66
67         context['futurEpisodes'] = \
68                 episodes_queryset.extra(select={
69                         'first_diffusion': 'emissions_diffusion.datetime',
70                         },
71                         select_params=(False, True),
72                         where=['''datetime = (SELECT MIN(datetime)
73                                                 FROM emissions_diffusion
74                                                WHERE episode_id = emissions_episode.id
75                                                  AND datetime > CURRENT_TIMESTAMP)'''],
76                         tables=['emissions_diffusion'],
77                     ).order_by('first_diffusion').distinct()
78
79
80         # get all related soundfiles in a single query
81         soundfiles = {}
82         if episode_ids is not None:
83             for episode_id in episode_ids:
84                 soundfiles[episode_id] = None
85         else:
86             for episode in Episode.objects.filter(emission=emission):
87                 soundfiles[episode.id] = None
88
89         for soundfile in SoundFile.objects.select_related().filter(podcastable=True,
90                 fragment=False, episode__emission=emission):
91             soundfiles[soundfile.episode_id] = soundfile
92
93         Episode.set_prefetched_soundfiles(soundfiles)
94
95         #context['futurEpisodes'] = context['episodes'].filter(first_diffusion='2013')[0:3]
96
97         return context
98
99
100 class EmissionDetailView(DetailView, EmissionMixin):
101     model = Emission
102
103     def get_context_data(self, **kwargs):
104         context = super(EmissionDetailView, self).get_context_data(**kwargs)
105         context['schedules'] = Schedule.objects.select_related().filter(
106                 emission=self.object).order_by('rerun', 'datetime')
107         context['news'] = NewsItem.objects.all(
108                 ).filter(emission=self.object.id
109                 ).exclude(expiration_date__lt=date.today()  # expiration date
110                 ).exclude(date__lt=date.today() - timedelta(days=60)
111                 ).order_by('-date')[:3]
112         try:
113             nonstop_object = Nonstop.objects.get(slug=self.object.slug)
114         except Nonstop.DoesNotExist:
115             pass
116         else:
117             today = date.today()
118             dates = [today - timedelta(days=x) for x in range(7)]
119             if datetime.now().time() < nonstop_object.start:
120                 dates = dates[1:]
121             context['nonstop'] = nonstop_object
122             context['nonstop_dates'] = dates
123         context.update(self.get_emission_context(self.object))
124         return context
125 emission = EmissionDetailView.as_view()
126
127
128 class EpisodeDetailView(EmissionEpisodeMixin, DetailView, EmissionMixin):
129     model = Episode
130
131     def get_context_data(self, **kwargs):
132         context = super(EpisodeDetailView, self).get_context_data(**kwargs)
133         context['diffusions'] = Diffusion.objects.select_related().filter(
134                 episode=self.object.id).order_by('datetime')
135         try:
136             context['emission'] = context['episode'].emission
137         except Emission.DoesNotExist:
138             raise Http404()
139         if self.kwargs.get('emission_slug') != context['emission'].slug:
140             raise Http404()
141         context.update(self.get_emission_context(context['emission']))
142         context['topiks'] = [x.topik for x in ItemTopik.objects.filter(episode=self.object)]
143         return context
144 episode = EpisodeDetailView.as_view()
145
146
147 class NonstopPlaylistView(TemplateView):
148     template_name = 'nonstop_playlist.html'
149
150     def get_context_data(self, **kwargs):
151         context = super(NonstopPlaylistView, self).get_context_data(**kwargs)
152         try:
153             context['emission'] = Emission.objects.get(slug=kwargs.get('slug'))
154         except Emission.DoesNotExist:
155             raise Http404()
156         context['date'] = date(int(kwargs.get('year')),
157                 int(kwargs.get('month')), int(kwargs.get('day')))
158         context['future'] = (context['date'] >= date.today())
159
160         nonstop_object = Nonstop.objects.get(slug=kwargs.get('slug'))
161         start = datetime(
162                 int(kwargs.get('year')), int(kwargs.get('month')), int(kwargs.get('day')),
163                 nonstop_object.start.hour, nonstop_object.start.minute)
164         end = datetime(
165                 int(kwargs.get('year')), int(kwargs.get('month')), int(kwargs.get('day')),
166                 nonstop_object.end.hour, nonstop_object.end.minute)
167         if end < start:
168             end = end + timedelta(days=1)
169         context['tracks'] = SomaLogLine.objects.filter(
170                 play_timestamp__gte=start,
171                 play_timestamp__lte=end,
172                 on_air=True).select_related()
173         return context
174
175 nonstop_playlist = NonstopPlaylistView.as_view()
176
177 class EmissionEpisodesDetailView(DetailView, EmissionMixin):
178     model = Emission
179     template_name = 'emissions/episodes.html'
180
181     def get_context_data(self, **kwargs):
182         context = super(EmissionEpisodesDetailView, self).get_context_data(**kwargs)
183         context['schedules'] = Schedule.objects.select_related().filter(
184                 emission=self.object).order_by('rerun', 'datetime')
185
186         context['search_query'] = self.request.GET.get('q')
187         if context['search_query']:
188             # query string
189             sqs = SearchQuerySet().models(Episode).filter(
190                     emission_slug_exact=self.object.slug, text=context['search_query'])
191             episode_ids = [x.pk for x in sqs]
192         else:
193             episode_ids = None
194
195         context.update(self.get_emission_context(self.object, episode_ids=episode_ids))
196         return context
197 emissionEpisodes = EmissionEpisodesDetailView.as_view()
198
199
200 class SoundFileEmbedView(DetailView):
201     model = SoundFile
202     template_name = 'soundfiles/embed.html'
203
204     def get_context_data(self, **kwargs):
205         context = super(SoundFileEmbedView, self).get_context_data(**kwargs)
206         if self.kwargs.get('episode_slug') != self.object.episode.slug:
207             raise Http404()
208         if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
209             raise Http404()
210         context['episode'] = self.object.episode
211         return context
212 soundfile_embed = SoundFileEmbedView.as_view()
213
214
215 class SoundFileDialogEmbedView(DetailView):
216     model = SoundFile
217     template_name = 'soundfiles/dialog-embed.html'
218
219     def get_context_data(self, **kwargs):
220         context = super(SoundFileDialogEmbedView, self).get_context_data(**kwargs)
221         if self.kwargs.get('episode_slug') != self.object.episode.slug:
222             raise Http404()
223         if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
224             raise Http404()
225         context['episode'] = self.object.episode
226         return context
227 soundfile_dlg_embed = SoundFileDialogEmbedView.as_view()
228
229
230
231 class ProgramView(TemplateView):
232     template_name = 'program.html'
233
234     def get_context_data(self, year=None, week=None, **kwargs):
235         context = super(ProgramView, self).get_context_data(**kwargs)
236
237         context['weekday'] = datetime.today().weekday()
238
239         context['week'] = week = int(week) if week is not None else datetime.today().isocalendar()[1]
240         context['year'] = year = int(year) if year is not None else datetime.today().isocalendar()[0]
241         context['week_first_day'] = utils.tofirstdayinisoweek(year, week)
242         context['week_last_day'] = context['week_first_day'] + timedelta(days=6)
243
244         return context
245
246 program = ProgramView.as_view()
247
248 @python_2_unicode_compatible
249 class TimeCell:
250     nonstop = None
251     w = 1
252     h = 1
253     time_label = None
254
255     def __init__(self, i, j):
256         self.x = i
257         self.y = j
258         self.schedules = []
259
260     def add_schedule(self, schedule):
261         end_time = schedule.datetime + timedelta(
262                 minutes=schedule.get_duration())
263         self.time_label = '%02d:%02d-%02d:%02d' % (
264                 schedule.datetime.hour,
265                 schedule.datetime.minute,
266                 end_time.hour,
267                 end_time.minute)
268         self.schedules.append(schedule)
269
270     def sorted_schedules(self):
271         return sorted(self.schedules, key=lambda x: x.week_sort_key())
272
273     def __str__(self):
274         if self.schedules:
275             return ', '.join([x.emission.title for x in self.schedules])
276         else:
277             return self.nonstop
278
279     def __eq__(self, other):
280         return (force_text(self) == force_text(other) and self.time_label == other.time_label)
281
282
283 class Grid(TemplateView):
284     template_name = 'grid.html'
285
286     def get_context_data(self, **kwargs):
287         context = super(Grid, self).get_context_data(**kwargs)
288
289         nb_lines = 2 * 24 # the cells are half hours
290         grid = []
291
292         times = ['%02d:%02d' % (x/2, x%2*30) for x in range(nb_lines)]
293         # start grid after the night programs
294         times = times[2*Schedule.DAY_HOUR_START:] + times[:2*Schedule.DAY_HOUR_START]
295
296         nonstops = []
297         for nonstop in Nonstop.objects.all():
298             if nonstop.start == nonstop.end:
299                 continue
300             if nonstop.start < nonstop.end:
301                 nonstops.append([nonstop.start.hour + nonstop.start.minute/60.,
302                                  nonstop.end.hour + nonstop.end.minute/60.,
303                                  nonstop.title, nonstop.slug, nonstop])
304             else:
305                 # crossing midnight
306                 nonstops.append([nonstop.start.hour + nonstop.start.minute/60.,
307                                  24,
308                                  nonstop.title, nonstop.slug, nonstop])
309                 nonstops.append([0,
310                                  nonstop.end.hour + nonstop.end.minute/60.,
311                                  nonstop.title, nonstop.slug, nonstop])
312         nonstops.sort()
313
314         for i in range(nb_lines):
315             grid.append([])
316             for j in range(7):
317                 grid[-1].append(TimeCell(i, j))
318
319             nonstop = [x for x in nonstops if i>=x[0]*2 and i<x[1]*2][0]
320             for time_cell in grid[-1]:
321                 time_cell.nonstop = nonstop[2]
322                 time_cell.nonstop_slug = nonstop[3]
323                 time_cell.redirect_path = nonstop[4].redirect_path
324                 if nonstop[1] == 5:
325                     # the one ending at 5am will be cut down, so we inscribe
326                     # its duration manually
327                     time_cell.time_label = '%02d:00-%02d:00' % (
328                             nonstop[0], nonstop[1])
329
330         for schedule in Schedule.objects.prefetch_related(
331                 'emission__categories').select_related().order_by('datetime'):
332             row_start = schedule.datetime.hour * 2 + \
333                     int(math.ceil(schedule.datetime.minute / 30))
334             day_no = schedule.get_weekday()
335
336             for step in range(int(math.ceil(schedule.get_duration() / 30.))):
337                 if grid[(row_start+step)%nb_lines][day_no] is None:
338                     grid[(row_start+step)%nb_lines][day_no] = TimeCell()
339                 grid[(row_start+step)%nb_lines][day_no].add_schedule(schedule)
340
341         # start grid after the night programs
342         grid = grid[2*Schedule.DAY_HOUR_START:] + grid[:2*Schedule.DAY_HOUR_START]
343
344         # look for the case where the same emission has different schedules for
345         # the same time cell, for example if it lasts one hour the first week,
346         # and two hours the third week.
347         for i in range(nb_lines):
348             grid[i] = [x for x in grid[i] if x is not None]
349             for j, cell in enumerate(grid[i]):
350                 if grid[i][j] is None:
351                     continue
352                 if len(grid[i][j].schedules) > 1:
353                     time_cell_emissions = {}
354                     for schedule in grid[i][j].schedules:
355                         if not schedule.emission.id in time_cell_emissions:
356                             time_cell_emissions[schedule.emission.id] = []
357                         time_cell_emissions[schedule.emission.id].append(schedule)
358                     for schedule_list in time_cell_emissions.values():
359                         if len(schedule_list) == 1:
360                             continue
361                         # here it is, same cell, same emission, several
362                         # schedules
363                         schedule_list.sort(key=lambda x: x.get_duration())
364
365                         schedule = schedule_list[0]
366                         end_time = schedule.datetime + timedelta(
367                                 minutes=schedule.get_duration())
368                         grid[i][j].time_label = '%02d:%02d-%02d:%02d' % (
369                                 schedule.datetime.hour,
370                                 schedule.datetime.minute,
371                                 end_time.hour,
372                                 end_time.minute)
373
374                         for schedule in schedule_list[1:]:
375                             grid[i][j].schedules.remove(schedule)
376                             end_time = schedule.datetime + timedelta(minutes=schedule.get_duration())
377                             schedule_list[0].time_label_extra = ', -%02d:%02d %s' % (
378                                     end_time.hour, end_time.minute, schedule.weeks_string)
379
380         # merge adjacent
381
382         # 1st thing is to merge cells on the same line, this will mostly catch
383         # consecutive nonstop cells
384         for i in range(nb_lines):
385             for j, cell in enumerate(grid[i]):
386                 if grid[i][j] is None:
387                     continue
388                 t = 1
389                 try:
390                     # if the cells are identical, they are removed from the
391                     # grid, and current cell width is increased
392                     while grid[i][j+t] == cell:
393                         cell.w += 1
394                         grid[i][j+t] = None
395                         t += 1
396                 except IndexError:
397                     pass
398
399             # once we're done we remove empty cells
400             grid[i] = [x for x in grid[i] if x is not None]
401
402         # 2nd thing is to merge cells vertically, this is emissions that last
403         # for more than 30 minutes
404         for i in range(nb_lines):
405             grid[i] = [x for x in grid[i] if x is not None]
406             for j, cell in enumerate(grid[i]):
407                 if grid[i][j] is None:
408                     continue
409                 t = 1
410                 try:
411                     while True:
412                         # we look if the next time cell has the same emissions
413                         same_cell_below = [(bj, x) for bj, x in enumerate(grid[i+cell.h])
414                                            if x == cell and x.y == cell.y and x.w == cell.w]
415                         if same_cell_below:
416                             # if the cell was identical, we remove it and
417                             # increase current cell height
418                             bj, same_cell_below = same_cell_below[0]
419                             del grid[i+cell.h][bj]
420                             cell.h += 1
421                         else:
422                             # if the cell is different, we have a closer look
423                             # to it, so we can remove emissions that will
424                             # already be mentioned in the current cell.
425                             #
426                             # For example:
427                             #  - 7am30, seuls contre tout, 1h30
428                             #  - 8am, du pied gauche & la voix de la rue, 1h
429                             # should produce: (this is case A)
430                             #  |      7:30-9:00      |
431                             #  |  seuls contre tout  |
432                             #  |---------------------|
433                             #  |      8:00-9:00      |
434                             #  |   du pied gauche    |
435                             #  |  la voix de la rue  |
436                             #
437                             # On the other hand, if all three emissions started
438                             # at 7am30, we want: (this is case B)
439                             #  |      7:30-9:00      |
440                             #  |  seuls contre tout  |
441                             #  |   du pied gauche    |
442                             #  |  la voix de la rue  |
443                             # that is we merge all of them, ignoring the fact
444                             # that the other emissions will stop at 8am30
445                             current_cell_schedules = set(grid[i][j].schedules)
446                             current_cell_emissions = set([x.emission for x in current_cell_schedules])
447                             cursor = 1
448                             while True and current_cell_schedules:
449                                 same_cell_below = [x for x in grid[i+cursor] if x.y == grid[i][j].y]
450                                 if not same_cell_below:
451                                     cursor += 1
452                                     continue
453                                 same_cell_below = same_cell_below[0]
454                                 same_cell_below_emissions = set([x.emission for x in same_cell_below.schedules])
455
456                                 if current_cell_emissions.issubset(same_cell_below_emissions):
457                                     # this handles case A (see comment above)
458                                     for schedule in current_cell_schedules:
459                                         if schedule in same_cell_below.schedules:
460                                             same_cell_below.schedules.remove(schedule)
461                                 elif same_cell_below_emissions and \
462                                         current_cell_emissions.issuperset(same_cell_below_emissions):
463                                     # this handles case B (see comment above)
464                                     # we set the cell time label to the longest
465                                     # period
466                                     grid[i][j].time_label = same_cell_below.time_label
467                                     # then we sort emissions so the longest are
468                                     # put first
469                                     grid[i][j].schedules.sort(key=lambda x: -x.get_duration())
470                                     # then we add individual time labels to the
471                                     # other schedules
472                                     for schedule in current_cell_schedules:
473                                         if schedule not in same_cell_below.schedules:
474                                             end_time = schedule.datetime + timedelta(
475                                                     minutes=schedule.get_duration())
476                                             schedule.time_label = '%02d:%02d-%02d:%02d' % (
477                                                     schedule.datetime.hour,
478                                                     schedule.datetime.minute,
479                                                     end_time.hour,
480                                                     end_time.minute)
481                                     grid[i][j].h += 1
482                                     grid[i+cursor].remove(same_cell_below)
483                                 elif same_cell_below_emissions and \
484                                         current_cell_emissions.intersection(same_cell_below_emissions):
485                                     same_cell_below.schedules = [x for x in
486                                             same_cell_below.schedules if
487                                             x.emission not in
488                                             current_cell_emissions or
489                                             x.get_duration() < 30]
490                                 cursor += 1
491                             break
492                 except IndexError:
493                     pass
494
495         # cut night at 3am
496         grid = grid[:42]
497         times = times[:42]
498
499         context['grid'] = grid
500         context['times'] = times
501         context['categories'] = Category.objects.all()
502         context['weekdays'] = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi',
503                 'Vendredi', 'Samedi', 'Dimanche']
504
505         return context
506
507 grid = Grid.as_view()
508
509
510 class Home(TemplateView):
511     template_name = 'home.html'
512
513     def get_context_data(self, **kwargs):
514         context = super(Home, self).get_context_data(**kwargs)
515         context['emissions'] = Emission.objects.filter(archived=False).order_by('-creation_timestamp')[:3]
516         context['newsitems'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')[:3]
517
518         context['soundfiles'] = SoundFile.objects.prefetch_related('episode__emission__categories').filter(
519                 podcastable=True, fragment=False) \
520                 .select_related().extra(select={
521                     'first_diffusion': 'emissions_diffusion.datetime', },
522                     select_params=(False, True),
523                     where=['''datetime = (SELECT MIN(datetime)
524                                             FROM emissions_diffusion
525                                         WHERE episode_id = emissions_episode.id)'''],
526                     tables=['emissions_diffusion'],).order_by('-creation_timestamp').distinct() [:3]
527
528         context['newsletter_form'] = SubscribeForm()
529
530         return context
531
532 home = Home.as_view()
533
534 class NewsItemView(DetailView):
535     model = NewsItem
536     def get_context_data(self, **kwargs):
537         context = super(NewsItemView, self).get_context_data(**kwargs)
538         context['categories'] = NewsCategory.objects.all()
539         context['news'] = NewsItem.objects.all().order_by('-date')
540         context['topiks'] = [x.topik for x in ItemTopik.objects.filter(newsitem=self.object)]
541         return context
542 newsitemview = NewsItemView.as_view()
543
544 class News(TemplateView):
545     template_name = 'news.html'
546     def get_context_data(self, **kwargs):
547         context = super(News, self).get_context_data(**kwargs)
548         context['focus'] = NewsItem.objects.exclude(date__gt=date.today()  # publication date
549                 ).exclude(expiration_date__lt=date.today()  # expiration date
550                 ).filter(got_focus__isnull=False
551                 ).select_related('category').order_by('-date')[:10]
552         context['news'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')
553         return context
554
555 news = News.as_view()
556
557
558 class Agenda(TemplateView):
559     template_name = 'agenda.html'
560     def get_context_data(self, **kwargs):
561         context = super(Agenda, self).get_context_data(**kwargs)
562         context['agenda'] = NewsItem.objects.exclude(date__gt=date.today()).filter(
563                 event_date__gte=date.today()).order_by('date')[:20]
564         context['news'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')
565         context['previous_month'] = datetime.today().replace(day=1) - timedelta(days=2)
566         return context
567
568 agenda = Agenda.as_view()
569
570
571 class AgendaByMonth(MonthArchiveView):
572     template_name = 'agenda.html'
573     queryset = NewsItem.objects.filter(event_date__isnull=False)
574     allow_future = True
575     date_field = 'event_date'
576     month_format = '%m'
577
578     def get_context_data(self, **kwargs):
579         context = super(AgendaByMonth, self).get_context_data(**kwargs)
580         context['agenda'] = context['object_list']
581         context['news'] = NewsItem.objects.all().order_by('-date')
582         return context
583
584 agenda_by_month = AgendaByMonth.as_view()
585
586
587 class Emissions(TemplateView):
588     template_name = 'emissions.html'
589
590     def get_context_data(self, **kwargs):
591         context = super(Emissions, self).get_context_data(**kwargs)
592         context['emissions'] = Emission.objects.prefetch_related('categories').filter(archived=False).order_by('title')
593         context['categories'] = Category.objects.all()
594         return context
595
596 emissions = Emissions.as_view()
597
598 class EmissionsArchives(TemplateView):
599     template_name = 'emissions/archives.html'
600
601     def get_context_data(self, **kwargs):
602         context = super(EmissionsArchives, self).get_context_data(**kwargs)
603         context['emissions'] = Emission.objects.prefetch_related('categories').filter(archived=True).order_by('title')
604         context['categories'] = Category.objects.all()
605         return context
606 emissionsArchives = EmissionsArchives.as_view()
607
608
609 class Listen(TemplateView):
610     template_name = 'listen.html'
611
612     def get_context_data(self, **kwargs):
613         context = super(Listen, self).get_context_data(**kwargs)
614         context['focus'] = SoundFile.objects.prefetch_related('episode__emission__categories').filter(
615                 podcastable=True, got_focus__isnull=False) \
616                 .select_related().extra(select={
617                     'first_diffusion': 'emissions_diffusion.datetime', },
618                     select_params=(False, True),
619                     where=['''datetime = (SELECT MIN(datetime)
620                                             FROM emissions_diffusion
621                                         WHERE episode_id = emissions_episode.id)'''],
622                     tables=['emissions_diffusion'],).order_by('-first_diffusion').distinct() [:10]
623         context['soundfiles'] = SoundFile.objects.prefetch_related('episode__emission__categories').filter(
624                 podcastable=True) \
625                 .select_related().extra(select={
626                     'first_diffusion': 'emissions_diffusion.datetime', },
627                     select_params=(False, True),
628                     where=['''datetime = (SELECT MIN(datetime)
629                                             FROM emissions_diffusion
630                                         WHERE episode_id = emissions_episode.id)'''],
631                     tables=['emissions_diffusion'],).order_by('-creation_timestamp').distinct()
632
633
634         return context
635
636 listen = Listen.as_view()
637
638 @cache_control(max_age=15)
639 @csrf_exempt
640 def onair(request):
641     d = whatsonair()
642     if d.get('episode'):
643         d['episode'] = {
644             'title': d['episode'].title,
645             'url': d['episode'].get_absolute_url()
646         }
647     if d.get('emission'):
648         chat_url = None
649         if d['emission'].chat_open:
650             chat_url = reverse('emission-chat', kwargs={'slug': d['emission'].slug})
651         d['emission'] = {
652             'title': d['emission'].title,
653             'url': d['emission'].get_absolute_url(),
654             'chat': chat_url,
655         }
656     if d.get('nonstop'):
657         redirect_path = d['nonstop'].redirect_path
658         d['nonstop'] = {
659             'title': d['nonstop'].title,
660         }
661         if redirect_path:
662             d['nonstop']['url'] = redirect_path
663         d.update(get_current_nonstop_track())
664     if d.get('current_slot'):
665         del d['current_slot']
666     return JsonResponse({'data': d})
667
668
669 class NewsItemDetailView(DetailView):
670     model = NewsItem
671
672 newsitem = NewsItemDetailView.as_view()
673
674 class RssCustomPodcastsFeed(Rss201rev2Feed):
675     def add_root_elements(self, handler):
676         super(RssCustomPodcastsFeed, self).add_root_elements(handler)
677         emission = self.feed.get('emission')
678         if emission and emission.image and emission.image.url:
679             image_url = emission.image.url
680         else:
681             image_url = '/static/img/logo-panik-500.png'
682         image_url = urlparse.urljoin(self.feed['link'], image_url)
683         handler.startElement('image', {})
684         if emission:
685             handler.addQuickElement('title', emission.title)
686         else:
687             handler.addQuickElement('title', settings.RADIO_NAME)
688         handler.addQuickElement('url', image_url)
689         handler.endElement('image')
690         handler.addQuickElement('itunes:explicit', 'no')  # invidividual items will get their own value
691         handler.addQuickElement('itunes:image', None, {'href': image_url})
692         if emission:
693             if emission.subtitle:
694                 handler.addQuickElement('itunes:subtitle', emission.subtitle)
695             for category in emission.categories.all():
696                 if category.itunes_category:
697                     handler.addQuickElement('itunes:category', None, {'text': category.itunes_category})
698
699             handler.addQuickElement('itunes:author', emission.title)
700             handler.startElement('itunes:owner', {})
701             if emission.email:
702                 handler.addQuickElement('itunes:email', emission.email)
703             handler.addQuickElement('itunes:name', emission.title)
704             handler.endElement('itunes:owner')
705         else:
706             handler.addQuickElement('itunes:author', settings.RADIO_NAME)
707             handler.startElement('itunes:owner', {})
708             handler.addQuickElement('itunes:email', 'info@radiopanik.org')
709             handler.addQuickElement('itunes:name', settings.RADIO_NAME)
710             handler.endElement('itunes:owner')
711
712     def root_attributes(self):
713         attrs = super(RssCustomPodcastsFeed, self).root_attributes()
714         attrs['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
715         attrs['xmlns:itunes'] = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
716         return attrs
717
718     def add_item_elements(self, handler, item):
719         super(RssCustomPodcastsFeed, self).add_item_elements(handler, item)
720         explicit = 'no'
721         for tag in item.get('tags') or []:
722             handler.addQuickElement('dc:subject', tag)
723             if tag == 'explicit':
724                 explicit = 'yes'
725         if item.get('tags'):
726             handler.addQuickElement('itunes:keywords', ','.join(item.get('tags')))
727         handler.addQuickElement('itunes:explicit', explicit)
728         episode = item.get('episode')
729         if episode and episode.image and episode.image.url:
730             image_url = urlparse.urljoin(self.feed['link'], episode.image.url)
731             handler.addQuickElement('itunes:image', None, {'href': image_url})
732         soundfile = item.get('soundfile')
733         if soundfile.duration:
734             handler.addQuickElement('itunes:duration', '%02d:%02d:%02d' % (
735                 soundfile.duration / 3600,
736                 soundfile.duration % 3600 / 60,
737                 soundfile.duration % 60))
738
739
740 class PodcastsFeed(Feed):
741     title = '%s - Podcasts' % settings.RADIO_NAME
742     link = '/'
743     description_template = 'feed/soundfile.html'
744     feed_type = RssCustomPodcastsFeed
745
746     def items(self):
747         return SoundFile.objects.select_related().filter(
748                 podcastable=True).order_by('-creation_timestamp')[:50]
749
750     def item_title(self, item):
751         if item.fragment:
752             return '[%s] %s - %s' % (item.episode.emission.title, item.title, item.episode.title)
753         return '[%s] %s' % (item.episode.emission.title, item.episode.title)
754
755     def item_link(self, item):
756         if item.fragment:
757             return item.episode.get_absolute_url() + '#%s' % item.id
758         return item.episode.get_absolute_url()
759
760     def item_enclosure_url(self, item):
761         current_site = Site.objects.get(id=settings.SITE_ID)
762         return add_domain(current_site.domain, item.get_format_url('mp3'))
763
764     def item_enclosure_length(self, item):
765         sound_path = item.get_format_path('mp3')
766         try:
767             return os.stat(sound_path)[stat.ST_SIZE]
768         except OSError:
769             return 0
770
771     def item_enclosure_mime_type(self, item):
772         return 'audio/mpeg'
773
774     def item_pubdate(self, item):
775         return item.creation_timestamp
776
777     def item_extra_kwargs(self, item):
778         return {'tags': [x.name for x in item.episode.tags.all()],
779                 'soundfile': item,
780                 'episode': item.episode}
781
782 podcasts_feed = PodcastsFeed()
783
784
785 class RssNewsFeed(Feed):
786     title = settings.RADIO_NAME
787     link = '/news/'
788     description_template = 'feed/newsitem.html'
789
790     def items(self):
791         return NewsItem.objects.order_by('-date')[:20]
792
793 rss_news_feed = RssNewsFeed()
794
795 class Atom1FeedWithBaseXml(Atom1Feed):
796     def root_attributes(self):
797         root_attributes = super(Atom1FeedWithBaseXml, self).root_attributes()
798         scheme, netloc, path, params, query, fragment  = urlparse.urlparse(self.feed['feed_url'])
799         root_attributes['xml:base'] = urlparse.urlunparse((scheme, netloc, '/', params, query, fragment))
800         return root_attributes
801
802 class AtomNewsFeed(RssNewsFeed):
803     feed_type = Atom1FeedWithBaseXml
804
805 atom_news_feed = AtomNewsFeed()
806
807
808 class EmissionPodcastsFeed(PodcastsFeed):
809     description_template = 'feed/soundfile.html'
810     feed_type = RssCustomPodcastsFeed
811
812     def __call__(self, request, *args, **kwargs):
813         self.emission = Emission.objects.get(slug=kwargs.get('slug'))
814         return super(EmissionPodcastsFeed, self).__call__(request, *args, **kwargs)
815
816     def item_title(self, item):
817         if item.fragment:
818             return '%s - %s' % (item.title, item.episode.title)
819         return item.episode.title
820
821     @property
822     def title(self):
823         return self.emission.title
824
825     @property
826     def description(self):
827         return self.emission.subtitle
828
829     @property
830     def link(self):
831         return reverse('emission-view', kwargs={'slug': self.emission.slug})
832
833     def feed_extra_kwargs(self, obj):
834         return {'emission': self.emission}
835
836     def items(self):
837         return SoundFile.objects.select_related().filter(
838                 podcastable=True,
839                 episode__emission__slug=self.emission.slug).order_by('-creation_timestamp')[:50]
840
841 emission_podcasts_feed = EmissionPodcastsFeed()
842
843
844 class Party(TemplateView):
845     template_name = 'party.html'
846
847     def get_context_data(self, **kwargs):
848         context = super(Party, self).get_context_data(**kwargs)
849         t = random.choice(['newsitem']*2 + ['emission']*3 + ['soundfile']*1 + ['episode']*2)
850         focus = Focus()
851         if t == 'newsitem':
852             focus.newsitem = NewsItem.objects.exclude(
853                     image__isnull=True).exclude(image__exact='').order_by('?')[0]
854         elif t == 'emission':
855             focus.emission = Emission.objects.exclude(
856                     image__isnull=True).exclude(image__exact='').order_by('?')[0]
857         elif t == 'episode':
858             focus.episode = Episode.objects.exclude(
859                     image__isnull=True).exclude(image__exact='').order_by('?')[0]
860         elif t == 'soundfile':
861             focus.soundfile = SoundFile.objects.exclude(
862                     episode__image__isnull=True).exclude(episode__image__exact='').order_by('?')[0]
863
864         context['focus'] = focus
865
866         return context
867
868 party = Party.as_view()
869
870
871
872 class Chat(DetailView, EmissionMixin):
873     model = Emission
874     template_name = 'chat.html'
875
876 chat = cache_control(max_age=15)(Chat.as_view())