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