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