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