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