]> git.0d.be Git - panikweb.git/blob - panikweb/views.py
fix item enclosure mime type
[panikweb.git] / panikweb / views.py
1 from datetime import datetime, timedelta
2 import math
3 import os
4 import stat
5 import time
6
7 from django.views.decorators.cache import cache_control
8 from django.views.generic.base import TemplateView
9 from django.views.generic.detail import DetailView
10 from django.views.decorators.csrf import csrf_exempt
11 from django.views.generic.dates import _date_from_string
12
13 from django.core.paginator import Paginator
14
15 from django.contrib.syndication.views import Feed
16 from django.utils.feedgenerator import Atom1Feed
17
18 import fiber.views
19 from haystack.query import SearchQuerySet
20 from jsonresponse import to_json
21
22 from emissions.models import Category, Emission, Episode, Diffusion, SoundFile, \
23         Schedule, Nonstop, NewsItem, NewsCategory
24
25 from emissions.utils import whatsonair, period_program
26
27 from newsletter.forms import SubscribeForm
28
29 from . import utils
30
31
32 class EmissionMixin:
33     def get_emission_context(self, emission, episode_ids=None):
34         context = {}
35
36         # get all episodes, with an additional attribute to get the date of
37         # their first diffusion
38         episodes_queryset = Episode.objects.select_related()
39         if episode_ids is not None:
40             episodes_queryset = episodes_queryset.filter(id__in=episode_ids)
41         else:
42             episodes_queryset = episodes_queryset.filter(emission=emission)
43
44         context['episodes'] = \
45                 episodes_queryset.extra(select={
46                         'first_diffusion': 'emissions_diffusion.datetime',
47                         },
48                         select_params=(False, True),
49                         where=['''datetime = (SELECT MIN(datetime)
50                                                 FROM emissions_diffusion
51                                                WHERE episode_id = emissions_episode.id)'''],
52                         tables=['emissions_diffusion'],
53                     ).order_by('-first_diffusion')
54
55         # get all related soundfiles in a single query
56         soundfiles = {}
57         if episode_ids is not None:
58             for episode_id in episode_ids:
59                 soundfiles[episode_id] = None
60         else:
61             for episode in Episode.objects.filter(emission=emission):
62                 soundfiles[episode.id] = None
63
64         for soundfile in SoundFile.objects.select_related().filter(podcastable=True,
65                 fragment=False, episode__emission=emission):
66             soundfiles[soundfile.episode_id] = soundfile
67
68         Episode.set_prefetched_soundfiles(soundfiles)
69
70         #context['futurEpisodes'] = context['episodes'].filter(first_diffusion='2013')[0:3]
71
72         return context
73
74
75 class EmissionDetailView(DetailView, EmissionMixin):
76     model = Emission
77
78     def get_context_data(self, **kwargs):
79         context = super(EmissionDetailView, self).get_context_data(**kwargs)
80         context['sectionName'] = "Emissions"
81         context['schedules'] = Schedule.objects.select_related().filter(emission=self.object).order_by('datetime')
82         context['news'] = NewsItem.objects.all().filter(emission=self.object.id).order_by('-date')[:3]
83         context.update(self.get_emission_context(self.object))
84         return context
85 emission = EmissionDetailView.as_view()
86
87 class EpisodeDetailView(DetailView, EmissionMixin):
88     model = Episode
89
90     def get_context_data(self, **kwargs):
91         context = super(EpisodeDetailView, self).get_context_data(**kwargs)
92         context['sectionName'] = "Emissions"
93         context['diffusions'] = Diffusion.objects.select_related().filter(episode=self.object.id)
94         context['soundfiles'] = SoundFile.objects.select_related().filter(episode=self.object.id)
95         context['emission'] = Emission.objects.get(slug=self.kwargs.get('emission_slug'))
96         context.update(self.get_emission_context(context['emission']))
97         return context
98 episode = EpisodeDetailView.as_view()
99
100
101 class EmissionEpisodesDetailView(DetailView, EmissionMixin):
102     model = Emission
103     template_name = 'emissions/episodes.html'
104
105     def get_context_data(self, **kwargs):
106         context = super(EmissionEpisodesDetailView, self).get_context_data(**kwargs)
107         context['sectionName'] = "Emissions"
108         context['schedules'] = Schedule.objects.select_related().filter(emission=self.object).order_by('datetime')
109
110         context['search_query'] = self.request.GET.get('q')
111         if context['search_query']:
112             # query string
113             sqs = SearchQuerySet().filter(django_ct='emissions.episode',
114                     emission_slug_exact=self.object.slug, text=context['search_query'])
115             episode_ids = [x.pk for x in sqs]
116         else:
117             episode_ids = None
118
119         context.update(self.get_emission_context(self.object, episode_ids=episode_ids))
120         return context
121 emissionEpisodes = EmissionEpisodesDetailView.as_view()
122
123 class ProgramView(TemplateView):
124     template_name = 'program.html'
125
126     def get_context_data(self, year=None, week=None, **kwargs):
127         context = super(ProgramView, self).get_context_data(**kwargs)
128         context['sectionName'] = "Emissions"
129
130         context['weekday'] = datetime.today().weekday()
131
132         context['week'] = week = int(week) if week is not None else datetime.today().isocalendar()[1]
133         context['year'] = year = int(year) if year is not None else datetime.today().isocalendar()[0]
134         context['week_first_day'] = utils.tofirstdayinisoweek(year, week)
135         context['week_last_day'] = context['week_first_day'] + timedelta(days=6)
136
137         return context
138
139 program = ProgramView.as_view()
140
141 class TimeCell:
142     nonstop = None
143     w = 1
144     h = 1
145     time_label = None
146
147     def __init__(self, i, j):
148         self.x = i
149         self.y = j
150         self.schedules = []
151
152     def add_schedule(self, schedule):
153         end_time = schedule.datetime + timedelta(
154                 minutes=schedule.get_duration())
155         self.time_label = '%02d:%02d-%02d:%02d' % (
156                 schedule.datetime.hour,
157                 schedule.datetime.minute,
158                 end_time.hour,
159                 end_time.minute)
160         self.schedules.append(schedule)
161
162     def __unicode__(self):
163         if self.schedules:
164             return ', '.join([x.emission.title for x in self.schedules])
165         else:
166             return self.nonstop
167
168     def __eq__(self, other):
169         return (unicode(self) == unicode(other) and self.time_label == other.time_label)
170
171
172 class Grid(TemplateView):
173     template_name = 'grid.html'
174
175     def get_context_data(self, **kwargs):
176         context = super(Grid, self).get_context_data(**kwargs)
177         context['sectionName'] = "Emissions"
178
179         nb_lines = 2 * 24 # the cells are half hours
180         grid = []
181
182         times = ['%02d:%02d' % (x/2, x%2*30) for x in range(nb_lines)]
183         # start grid after the night programs
184         times = times[2*Schedule.DAY_HOUR_START:] + times[:2*Schedule.DAY_HOUR_START]
185
186         nonstops = []
187         for nonstop in Nonstop.objects.all():
188             if nonstop.start < nonstop.end:
189                 nonstops.append([nonstop.start.hour + nonstop.start.minute/60.,
190                                  nonstop.end.hour + nonstop.end.minute/60.,
191                                  nonstop.title, nonstop.slug])
192             else:
193                 # crossing midnight
194                 nonstops.append([nonstop.start.hour + nonstop.start.minute/60.,
195                                  24,
196                                  nonstop.title, nonstop.slug])
197                 nonstops.append([0,
198                                  nonstop.end.hour + nonstop.end.minute/60.,
199                                  nonstop.title, nonstop.slug])
200         nonstops.sort()
201
202         for i in range(nb_lines):
203             grid.append([])
204             for j in range(7):
205                 grid[-1].append(TimeCell(i, j))
206
207             nonstop = [x for x in nonstops if i>=x[0]*2 and i<x[1]*2][0]
208             for time_cell in grid[-1]:
209                 time_cell.nonstop = nonstop[2]
210                 time_cell.nonstop_slug = nonstop[3]
211                 if nonstop[1] == 5:
212                     # the one ending at 5am will be cut down, so we inscribe
213                     # its duration manually
214                     time_cell.time_label = '%02d:00-%02d:00' % (
215                             nonstop[0], nonstop[1])
216
217         for schedule in Schedule.objects.prefetch_related(
218                 'emission__categories').select_related().order_by('datetime'):
219             row_start = schedule.datetime.hour * 2 + \
220                     int(math.ceil(schedule.datetime.minute / 30))
221             day_no = schedule.get_weekday()
222
223             for step in range(int(math.ceil(schedule.get_duration() / 30.))):
224                 if grid[(row_start+step)%nb_lines][day_no] is None:
225                     grid[(row_start+step)%nb_lines][day_no] = TimeCell()
226                 grid[(row_start+step)%nb_lines][day_no].add_schedule(schedule)
227
228         # start grid after the night programs
229         grid = grid[2*Schedule.DAY_HOUR_START:] + grid[:2*Schedule.DAY_HOUR_START]
230
231         # look for the case where the same emission has different schedules for
232         # the same time cell, for example if it lasts one hour the first week,
233         # and two hours the third week.
234         for i in range(nb_lines):
235             grid[i] = [x for x in grid[i] if x is not None]
236             for j, cell in enumerate(grid[i]):
237                 if grid[i][j] is None:
238                     continue
239                 if len(grid[i][j].schedules) > 1:
240                     time_cell_emissions = {}
241                     for schedule in grid[i][j].schedules:
242                         if not schedule.emission.id in time_cell_emissions:
243                             time_cell_emissions[schedule.emission.id] = []
244                         time_cell_emissions[schedule.emission.id].append(schedule)
245                     for schedule_list in time_cell_emissions.values():
246                         if len(schedule_list) == 1:
247                             continue
248                         # here it is, same cell, same emission, several
249                         # schedules
250                         schedule_list.sort(lambda x,y: cmp(x.get_duration(), y.get_duration()))
251
252                         schedule = schedule_list[0]
253                         end_time = schedule.datetime + timedelta(
254                                 minutes=schedule.get_duration())
255                         grid[i][j].time_label = '%02d:%02d-%02d:%02d' % (
256                                 schedule.datetime.hour,
257                                 schedule.datetime.minute,
258                                 end_time.hour,
259                                 end_time.minute)
260
261                         for schedule in schedule_list[1:]:
262                             grid[i][j].schedules.remove(schedule)
263                             end_time = schedule.datetime + timedelta(minutes=schedule.get_duration())
264                             schedule_list[0].time_label_extra = ', -%02d:%02d %s' % (
265                                     end_time.hour, end_time.minute, schedule.weeks_string)
266
267         # merge adjacent
268
269         # 1st thing is to merge cells on the same line, this will mostly catch
270         # consecutive nonstop cells
271         for i in range(nb_lines):
272             for j, cell in enumerate(grid[i]):
273                 if grid[i][j] is None:
274                     continue
275                 t = 1
276                 try:
277                     # if the cells are identical, they are removed from the
278                     # grid, and current cell width is increased
279                     while grid[i][j+t] == cell:
280                         cell.w += 1
281                         grid[i][j+t] = None
282                         t += 1
283                 except IndexError:
284                     pass
285
286             # once we're done we remove empty cells
287             grid[i] = [x for x in grid[i] if x is not None]
288
289         # 2nd thing is to merge cells vertically, this is emissions that last
290         # for more than 30 minutes
291         for i in range(nb_lines):
292             grid[i] = [x for x in grid[i] if x is not None]
293             for j, cell in enumerate(grid[i]):
294                 if grid[i][j] is None:
295                     continue
296                 t = 1
297                 try:
298                     while True:
299                         # we look if the next time cell has the same emissions
300                         same_cell_below = [(bj, x) for bj, x in enumerate(grid[i+cell.h])
301                                            if x == cell and x.y == cell.y and x.w == cell.w]
302                         if same_cell_below:
303                             # if the cell was identical, we remove it and
304                             # increase current cell height
305                             bj, same_cell_below = same_cell_below[0]
306                             del grid[i+cell.h][bj]
307                             cell.h += 1
308                         else:
309                             # if the cell is different, we have a closer look
310                             # to it, so we can remove emissions that will
311                             # already be mentioned in the current cell.
312                             #
313                             # For example:
314                             #  - 7am30, seuls contre tout, 1h30
315                             #  - 8am, du pied gauche & la voix de la rue, 1h
316                             # should produce: (this is case A)
317                             #  |      7:30-9:00      |
318                             #  |  seuls contre tout  |
319                             #  |---------------------|
320                             #  |      8:00-9:00      |
321                             #  |   du pied gauche    |
322                             #  |  la voix de la rue  |
323                             #
324                             # On the other hand, if all three emissions started
325                             # at 7am30, we want: (this is case B)
326                             #  |      7:30-9:00      |
327                             #  |  seuls contre tout  |
328                             #  |   du pied gauche    |
329                             #  |  la voix de la rue  |
330                             # that is we merge all of them, ignoring the fact
331                             # that the other emissions will stop at 8am30
332                             current_cell_schedules = set(grid[i][j].schedules)
333                             current_cell_emissions = set([x.emission for x in current_cell_schedules])
334                             cursor = 1
335                             while True and current_cell_schedules:
336                                 same_cell_below = [x for x in grid[i+cursor] if x.y == grid[i][j].y]
337                                 if not same_cell_below:
338                                     cursor += 1
339                                     continue
340                                 same_cell_below = same_cell_below[0]
341                                 same_cell_below_emissions = set([x.emission for x in same_cell_below.schedules])
342
343                                 if current_cell_emissions.issubset(same_cell_below_emissions):
344                                     # this handles case A (see comment above)
345                                     for schedule in current_cell_schedules:
346                                         if schedule in same_cell_below.schedules:
347                                             same_cell_below.schedules.remove(schedule)
348                                 elif same_cell_below_emissions and \
349                                         current_cell_emissions.issuperset(same_cell_below_emissions):
350                                     # this handles case B (see comment above)
351                                     # we set the cell time label to the longest
352                                     # period
353                                     grid[i][j].time_label = same_cell_below.time_label
354                                     # then we sort emissions so the longest are
355                                     # put first
356                                     grid[i][j].schedules.sort(
357                                             lambda x, y: -cmp(x.get_duration(), y.get_duration()))
358                                     # then we add individual time labels to the
359                                     # other schedules
360                                     for schedule in current_cell_schedules:
361                                         if schedule not in same_cell_below.schedules:
362                                             end_time = schedule.datetime + timedelta(
363                                                     minutes=schedule.get_duration())
364                                             schedule.time_label = '%02d:%02d-%02d:%02d' % (
365                                                     schedule.datetime.hour,
366                                                     schedule.datetime.minute,
367                                                     end_time.hour,
368                                                     end_time.minute)
369                                     grid[i][j].h += 1
370                                     grid[i+cursor].remove(same_cell_below)
371                                 elif same_cell_below_emissions and \
372                                         current_cell_emissions.intersection(same_cell_below_emissions):
373                                     same_cell_below.schedules = [x for x in
374                                             same_cell_below.schedules if
375                                             x.emission not in
376                                             current_cell_emissions or
377                                             x.get_duration() < 30]
378                                 cursor += 1
379                             break
380                 except IndexError:
381                     pass
382
383         # cut night at 3am
384         grid = grid[:42]
385         times = times[:42]
386
387         context['grid'] = grid
388         context['times'] = times
389         context['categories'] = Category.objects.all()
390         context['weekdays'] = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi',
391                 'Vendredi', 'Samedi', 'Dimanche']
392
393         return context
394
395 grid = Grid.as_view()
396
397
398 class Home(TemplateView):
399     template_name = 'home.html'
400
401     def get_context_data(self, **kwargs):
402         context = super(Home, self).get_context_data(**kwargs)
403         context['sectionName'] = "Home"
404         context['focus'] = NewsItem.objects.select_related('category').filter(focus=True).exclude(image__isnull=True).exclude(image__exact='').order_by('-date')[:5]
405         context['emissions'] = Emission.objects.filter(archived=False,
406                 creation_timestamp__gte=datetime(2013, 9, 13)).order_by('title')
407         schedules = Schedule.objects.select_related().order_by('datetime')
408         days = []
409         for day in range(7):
410             days.append({'schedules': [x for x in schedules if x.is_on_weekday(day+1)],
411                          'datetime': datetime(2007, 1, day+1)})
412         context['days'] = days
413
414         context['soundfiles'] = SoundFile.objects.prefetch_related('episode__emission__categories').filter(
415                 podcastable=True, fragment=False) \
416                 .select_related().extra(select={
417                     'first_diffusion': 'emissions_diffusion.datetime', },
418                     select_params=(False, True),
419                     where=['''datetime = (SELECT MIN(datetime)
420                                             FROM emissions_diffusion
421                                         WHERE episode_id = emissions_episode.id)'''],
422                     tables=['emissions_diffusion'],).order_by('-first_diffusion') [:6]
423
424         context['newsletter_form'] = SubscribeForm()
425
426         return context
427
428 home = Home.as_view()
429
430 class NewsItemView(DetailView):
431     model = NewsItem
432     def get_context_data(self, **kwargs):
433         context = super(NewsItemView, self).get_context_data(**kwargs)
434         context['sectionName'] = "News"
435         context['focus'] = list(NewsItem.objects.select_related('category').filter(focus=True).exclude(image__isnull=True).exclude(image__exact='').order_by('-date')[0:9])
436         context['categories'] = NewsCategory.objects.all()
437         return context
438 newsitemview = NewsItemView.as_view()
439
440 class News(TemplateView):
441     template_name = 'news.html'
442     def get_context_data(self, **kwargs):
443         context = super(News, self).get_context_data(**kwargs)
444         context['sectionName'] = "News"
445         context['news'] = NewsItem.objects.all().exclude(image__isnull=True).exclude(image__exact='').order_by('-date')[:14]
446         return context
447
448 news = News.as_view()
449
450 class NewsArchives(TemplateView):
451     template_name = 'news/archives.html'
452     def get_context_data(self, **kwargs):
453         context = super(NewsArchives, self).get_context_data(**kwargs)
454         context['sectionName'] = "News"
455         context['search_query'] = self.request.GET.get('q')
456         sqs = SearchQuerySet().filter(django_ct='emissions.newsitem')
457         if context['search_query']:
458             # query string
459             sqs = sqs.filter(text=context['search_query'])
460         else:
461             sqs = sqs.order_by('-date')
462
463         sqs = sqs.load_all()
464
465         context['results'] = sqs
466
467         return context
468
469 newsArchives = NewsArchives.as_view()
470
471 class Emissions(TemplateView):
472     template_name = 'emissions.html'
473
474     def get_context_data(self, **kwargs):
475         context = super(Emissions, self).get_context_data(**kwargs)
476         context['sectionName'] = "Emissions"
477         context['emissions'] = Emission.objects.prefetch_related('categories').filter(archived=False).order_by('title')
478         context['categories'] = Category.objects.all()
479         return context
480
481 emissions = Emissions.as_view()
482
483 class EmissionsArchives(TemplateView):
484     template_name = 'emissions/archives.html'
485
486     def get_context_data(self, **kwargs):
487         context = super(EmissionsArchives, self).get_context_data(**kwargs)
488         context['sectionName'] = "Emissions"
489         context['emissions'] = Emission.objects.prefetch_related('categories').filter(archived=True).order_by('title')
490         context['categories'] = Category.objects.all()
491         return context
492 emissionsArchives = EmissionsArchives.as_view()
493
494
495 class Listen(TemplateView):
496     template_name = 'listen.html'
497
498     def get_context_data(self, **kwargs):
499         context = super(Listen, self).get_context_data(**kwargs)
500         context['sectionName'] = "Listen"
501         context['soundfiles'] = SoundFile.objects.prefetch_related('episode__emission__categories').filter(
502                 podcastable=True, fragment=False) \
503                 .select_related().extra(select={
504                     'first_diffusion': 'emissions_diffusion.datetime', },
505                     select_params=(False, True),
506                     where=['''datetime = (SELECT MIN(datetime)
507                                             FROM emissions_diffusion
508                                         WHERE episode_id = emissions_episode.id)'''],
509                     tables=['emissions_diffusion'],).order_by('-first_diffusion') [:10]
510
511         context['categories'] = Category.objects.all()
512
513         return context
514
515 listen = Listen.as_view()
516
517 @cache_control(max_age=25)
518 @csrf_exempt
519 @to_json('api')
520 def onair(request):
521     d = whatsonair()
522     if d.get('episode'):
523         d['episode'] = {
524             'title': d['episode'].title,
525             'url': d['episode'].get_absolute_url()
526         }
527     if d.get('emission'):
528         d['emission'] = {
529             'title': d['emission'].title,
530             'url': d['emission'].get_absolute_url()
531         }
532     if d.get('nonstop'):
533         d['nonstop'] = {
534             'title': d['nonstop'].title,
535         }
536     if d.get('current_slot'):
537         del d['current_slot']
538     return d
539
540
541 class NewsItemDetailView(DetailView):
542     model = NewsItem
543
544 newsitem = NewsItemDetailView.as_view()
545
546
547 class PodcastsFeed(Feed):
548     title = 'Radio Panik - Podcasts'
549     link = '/'
550     description_template = 'feed/soundfile.html'
551
552     def items(self):
553         return SoundFile.objects.select_related().filter(
554                 podcastable=True).order_by('-creation_timestamp')[:5]
555
556     def item_title(self, item):
557         if item.fragment:
558             return '%s - %s' % (item.title, item.episode.title)
559         return item.episode.title
560
561     def item_link(self, item):
562         return item.episode.get_absolute_url()
563
564     def item_enclosure_url(self, item):
565         return item.get_format_url('mp3')
566
567     def item_enclosure_length(self, item):
568         sound_path = item.get_format_path('mp3')
569         try:
570             return os.stat(sound_path)[stat.ST_SIZE]
571         except OSError:
572             return 0
573
574     def item_enclosure_mime_type(self, item):
575         return 'audio/mpeg'
576
577     def item_pubdate(self, item):
578         return item.creation_timestamp
579
580 podcasts_feed = PodcastsFeed()
581
582
583 class FiberPageView(fiber.views.FiberTemplateView):
584     def get_context_data(self, **kwargs):
585         context = super(FiberPageView, self).get_context_data(**kwargs)
586         context['sectionName'] = 'About'
587         return context
588 fiber_page = FiberPageView.as_view()
589
590
591 class RssNewsFeed(Feed):
592     title = 'Radio Panik'
593     link = '/news/'
594     description_template = 'feed/newsitem.html'
595
596     def items(self):
597         return NewsItem.objects.order_by('-date')[:10]
598
599 rss_news_feed = RssNewsFeed()
600
601 class AtomNewsFeed(RssNewsFeed):
602     feed_type = Atom1Feed
603
604 atom_news_feed = AtomNewsFeed()