]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/views.py
stats: add support for empty querysets
[django-panik-nonstop.git] / nonstop / views.py
1 import copy
2 import csv
3 import datetime
4 import os
5 import tempfile
6
7 import mutagen
8
9 from django.core.files.storage import default_storage
10 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
11 from django.core.urlresolvers import reverse
12 from django.contrib import messages
13 from django.db.models import Q, Sum
14 from django.http import HttpResponse, HttpResponseRedirect, FileResponse
15 from django.utils.six import StringIO
16 from django.utils.translation import ugettext_lazy as _
17 from django.views.generic.base import RedirectView, TemplateView
18 from django.views.generic.dates import DayArchiveView
19 from django.views.generic.detail import DetailView
20 from django.views.generic.edit import CreateView, FormView, UpdateView
21 from django.views.generic.list import ListView
22
23 from .forms import UploadTracksForm, TrackMetaForm, TrackSearchForm, CleanupForm
24 from .models import (SomaLogLine, Track, Artist, NonstopFile, ScheduledDiffusion, Jingle, Stream)
25 from emissions.models import Nonstop, Diffusion
26 from emissions.utils import period_program
27
28 from . import utils
29 from .app_settings import app_settings
30
31
32 class SomaDayArchiveView(DayArchiveView):
33     queryset = SomaLogLine.objects.all()
34     date_field = "play_timestamp"
35     make_object_list = True
36     allow_future = False
37     month_format = '%m'
38
39
40 class SomaDayArchiveCsvView(SomaDayArchiveView):
41     def render_to_response(self, context, **response_kwargs):
42         out = StringIO()
43         writer = csv.writer(out)
44         for line in context['object_list']:
45             if line.filepath.track:
46                 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
47                     line.filepath.short,
48                     line.filepath.track.title,
49                     line.filepath.track.artist.name,
50                     line.filepath.track.language,
51                     line.filepath.track.instru and 'instru' or '',
52                     line.filepath.track.cfwb and 'cfwb' or '',
53                     line.filepath.track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.track.added_to_nonstop_timestamp else '',
54                     ])
55             else:
56                 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
57                                 line.filepath.short])
58         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
59
60
61 class RedirectTodayView(RedirectView):
62     def get_redirect_url(self, *args, **kwargs):
63         today = datetime.datetime.today()
64         return reverse('archive_day', kwargs={
65                         'year': today.year,
66                         'month': today.month,
67                         'day': today.day})
68
69
70 class TrackDetailView(DetailView):
71     model = Track
72
73     def get_context_data(self, **kwargs):
74         ctx = super(TrackDetailView, self).get_context_data(**kwargs)
75         ctx['metadata_form'] = TrackMetaForm(instance=self.object)
76         return ctx
77
78     def post(self, request, *args, **kwargs):
79         assert self.request.user.has_perm('nonstop.add_track')
80         instance = self.get_object()
81         old_nonstop_zones = copy.copy(instance.nonstop_zones.all())
82         form = TrackMetaForm(request.POST, instance=instance)
83         form.save()
84         new_nonstop_zones = self.get_object().nonstop_zones.all()
85         if set(old_nonstop_zones) != set(new_nonstop_zones):
86             instance.sync_nonstop_zones()
87         return HttpResponseRedirect('.')
88
89
90 class ArtistDetailView(DetailView):
91     model = Artist
92
93
94 class ArtistListView(ListView):
95     model = Artist
96
97
98 class ZoneStats(object):
99     def __init__(self, zone, from_date=None, until_date=None, **kwargs):
100         self.zone = zone
101         self.qs = Track.objects.filter(nonstop_zones=self.zone, **kwargs)
102         self.from_date = from_date
103         if from_date:
104             self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__gte=from_date)
105         if until_date:
106             self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
107         self.qs = self.qs.distinct()
108
109     def total_duration(self, **kwargs):
110         try:
111             total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
112         except AttributeError:
113             # 'NoneType' object has no attribute 'total_seconds', if there's no
114             # track in queryset
115             return '-'
116         if total > 3600 * 2:
117             duration = _('%d hours') % (total / 3600)
118         else:
119             duration = _('%d minutes') % (total / 60)
120         start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
121         end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
122         if end < start:
123             end = end + datetime.timedelta(days=1)
124         return duration + _(', → %d days') % (total // (end - start).total_seconds())
125
126     def count(self, **kwargs):
127         return self.qs.filter(**kwargs).count()
128
129     def percentage(self, **kwargs):
130         total = self.count()
131         if total == 0:
132             return '-'
133         return '%.2f%%' % (100. * self.count(**kwargs) / total)
134
135     def instru(self):
136         return self.count(instru=True)
137
138     def instru_percentage(self):
139         return self.percentage(instru=True)
140
141     def sabam(self):
142         return self.count(sabam=True)
143
144     def sabam_percentage(self):
145         return self.percentage(sabam=True)
146
147     def cfwb(self):
148         return self.count(cfwb=True)
149
150     def cfwb_percentage(self):
151         return self.percentage(cfwb=True)
152
153     def french(self):
154         return self.count(language='fr')
155
156     def french_percentage(self):
157         considered_tracks = self.count() - self.instru()
158         if considered_tracks == 0:
159             return '-'
160         return '%.2f%%' % (100. * self.french() / considered_tracks)
161
162     def quota_french(self):
163         # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
164         # langue française
165         considered_tracks = self.count() - self.instru()
166         if considered_tracks == 0:
167             return True
168         return (100. * self.french() / considered_tracks) > 30.
169
170     def quota_cfwb(self):
171         # obligation de diffuser annuellement au moins 4,5% d'œuvres musicales
172         # émanant de la Communauté française
173         considered_tracks = self.count()
174         if considered_tracks == 0:
175             return True
176         return (100. * self.cfwb() / considered_tracks) > 4.5
177
178     def new_files(self):
179         return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
180
181     def percent_new_files(self):
182         return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
183
184
185 def parse_date(date):
186     if date.endswith('d'):
187         return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
188     return datetime.datetime.strptime(date, '%Y-%m-%d').date()
189
190 class StatisticsView(TemplateView):
191     template_name = 'nonstop/statistics.html'
192
193     def get_context_data(self, **kwargs):
194         context = super(StatisticsView, self).get_context_data(**kwargs)
195         context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
196         kwargs = {}
197         if 'from' in self.request.GET:
198             kwargs['from_date'] = parse_date(self.request.GET['from'])
199             context['from_date'] = kwargs['from_date']
200         if 'until' in self.request.GET:
201             kwargs['until_date'] = parse_date(self.request.GET['until'])
202         if 'onair' in self.request.GET:
203             kwargs['nonstopfile__somalogline__on_air'] = True
204         for zone in context['zones']:
205             zone.stats = ZoneStats(zone, **kwargs)
206         return context
207
208
209 class UploadTracksView(FormView):
210     form_class = UploadTracksForm
211     template_name = 'nonstop/upload.html'
212     success_url = '.'
213
214     def post(self, request, *args, **kwargs):
215         assert self.request.user.has_perm('nonstop.add_track')
216         form_class = self.get_form_class()
217         form = self.get_form(form_class)
218         tracks = request.FILES.getlist('tracks')
219         if not form.is_valid():
220             return self.form_invalid(form)
221         missing_metadata = []
222         metadatas = {}
223         for f in tracks:
224             with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
225                 tmpfile.write(f.read())
226                 f.seek(0)
227                 metadata = mutagen.File(tmpfile.name, easy=True)
228             if not metadata or not metadata.get('artist') or not metadata.get('title'):
229                 missing_metadata.append(f.name)
230             else:
231                 metadatas[f.name] = metadata
232         if missing_metadata:
233             form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
234             return self.form_invalid(form)
235
236         for f in tracks:
237             metadata = metadatas[f.name]
238             artist_name = metadata.get('artist')[0]
239             track_title = metadata.get('title')[0]
240
241             monthdir = datetime.datetime.today().strftime('%Y-%m')
242             filepath = '%s/%s - %s - %s%s' % (monthdir,
243                 datetime.datetime.today().strftime('%y%m%d'),
244                 artist_name[:50].replace('/', ' ').strip(),
245                 track_title[:80].replace('/', ' ').strip(),
246                 os.path.splitext(f.name)[-1])
247
248             artist, created = Artist.objects.get_or_create(name=artist_name)
249             track, created = Track.objects.get_or_create(title=track_title, artist=artist,
250                     defaults={'uploader': self.request.user})
251             if created or not track.file_exists():
252                 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
253                 nonstop_file = NonstopFile()
254                 nonstop_file.set_track_filepath(filepath)
255                 nonstop_file.track = track
256                 nonstop_file.save()
257             else:
258                 # don't keep duplicated file and do not create a duplicated nonstop file object
259                 pass
260             if request.POST.get('nonstop_zone'):
261                 track.nonstop_zones.add(
262                         Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
263             track.sync_nonstop_zones()
264
265         messages.info(self.request, '%d new track(s)' % len(tracks))
266         return self.form_valid(form)
267
268
269 class RecentTracksView(ListView):
270     template_name = 'nonstop/recent_tracks.html'
271
272     def get_queryset(self):
273         return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
274
275     def post(self, request, *args, **kwargs):
276         assert self.request.user.has_perm('nonstop.add_track')
277         for track_id in request.POST.getlist('track'):
278             track = Track.objects.get(id=track_id)
279             track.language = request.POST.get('lang-%s' % track_id, '')
280             track.instru = 'instru-%s' % track_id in request.POST
281             track.sabam = 'sabam-%s' % track_id in request.POST
282             track.cfwb = 'cfwb-%s' % track_id in request.POST
283             track.save()
284         return HttpResponseRedirect('.')
285
286
287 class QuickLinksView(TemplateView):
288     template_name = 'nonstop/quick_links.html'
289
290     def get_context_data(self, **kwargs):
291         context = super().get_context_data(**kwargs)
292         day = datetime.datetime.today()
293         context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
294         return context
295
296
297 class SearchView(TemplateView):
298     template_name = 'nonstop/search.html'
299
300     def get_queryset(self):
301         queryset = Track.objects.all()
302
303         q = self.request.GET.get('q')
304         if q:
305             queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
306
307         zone = self.request.GET.get('zone')
308         if zone:
309             from emissions.models import Nonstop
310             if zone == 'none':
311                 queryset = queryset.filter(nonstop_zones=None)
312             elif zone == 'any':
313                 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
314             else:
315                 queryset = queryset.filter(nonstop_zones=zone)
316
317         order = self.request.GET.get('order_by') or 'title'
318         if order:
319             if 'added_to_nonstop_timestamp' in order:
320                 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
321             queryset = queryset.order_by(order)
322         return queryset
323
324     def get_context_data(self, **kwargs):
325         ctx = super(SearchView, self).get_context_data(**kwargs)
326         ctx['form'] = TrackSearchForm(self.request.GET)
327         queryset = self.get_queryset()
328         qs = self.request.GET.copy()
329         qs.pop('page', None)
330         ctx['qs'] = qs.urlencode()
331
332         tracks = Paginator(queryset.select_related(), 20)
333
334         page = self.request.GET.get('page')
335         try:
336             ctx['tracks'] = tracks.page(page)
337         except PageNotAnInteger:
338             ctx['tracks'] = tracks.page(1)
339         except EmptyPage:
340             ctx['tracks'] = tracks.page(tracks.num_pages)
341
342         return ctx
343
344
345 class SearchCsvView(SearchView):
346     def get(self, request, *args, **kwargs):
347         out = StringIO()
348         writer = csv.writer(out)
349         writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
350         for track in self.get_queryset():
351             writer.writerow([
352                 track.title if track.title else 'Inconnu',
353                 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
354                 ' + '.join([x.title for x in track.nonstop_zones.all()]),
355                 track.language or '',
356                 track.instru and 'instru' or '',
357                 track.cfwb and 'cfwb' or '',
358                 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
359                 ])
360         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
361
362
363 class CleanupView(TemplateView):
364     template_name = 'nonstop/cleanup.html'
365
366     def get_context_data(self, **kwargs):
367         ctx = super(CleanupView, self).get_context_data(**kwargs)
368         ctx['form'] = CleanupForm()
369
370         zone = self.request.GET.get('zone')
371         if zone:
372             from emissions.models import Nonstop
373             ctx['zone'] = Nonstop.objects.get(id=zone)
374             ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
375             ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
376                             'added_to_nonstop_timestamp').select_related()[:30]
377         return ctx
378
379     def post(self, request, *args, **kwargs):
380         assert self.request.user.has_perm('nonstop.add_track')
381         count = 0
382         for track_id in request.POST.getlist('track'):
383             if request.POST.get('remove-%s' % track_id):
384                 track = Track.objects.get(id=track_id)
385                 track.nonstop_zones.clear()
386                 track.sync_nonstop_zones()
387                 count += 1
388         if count:
389             messages.info(self.request, 'Removed %d new track(s)' % count)
390         return HttpResponseRedirect('.')
391
392
393 class AddSomaDiffusionView(CreateView):
394     model = ScheduledDiffusion
395     fields = ['jingle', 'stream']
396     template_name = 'nonstop/streamed-diffusion.html'
397
398     @property
399     def fields(self):
400         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
401         fields = ['jingle']
402         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
403             fields.append('stream')
404         return fields
405
406     def get_initial(self):
407         initial = super(AddSomaDiffusionView, self).get_initial()
408         initial['jingle'] = None
409         if 'stream' in self.fields:
410             initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
411         initial['stream'] = Stream.objects.all().first()
412         return initial
413
414     def form_valid(self, form):
415         form.instance.diffusion_id = self.kwargs['pk']
416         episode = form.instance.diffusion.episode
417         if 'stream' in self.fields and form.instance.stream_id is None:
418             messages.error(self.request, _('missing stream'))
419             return HttpResponseRedirect(reverse('episode-view', kwargs={
420                 'emission_slug': episode.emission.slug,
421                 'slug': episode.slug}))
422         response = super(AddSomaDiffusionView, self).form_valid(form)
423         messages.info(self.request, _('%s added to schedule') % episode.emission.title)
424         return response
425
426     def get_success_url(self):
427         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
428         episode = diffusion.episode
429         return reverse('episode-view', kwargs={
430             'emission_slug': episode.emission.slug,
431             'slug': episode.slug})
432
433
434 class DelSomaDiffusionView(RedirectView):
435     def get_redirect_url(self, pk):
436         soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
437         episode = soma_diffusion.episode
438         ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
439         messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
440         return reverse('episode-view', kwargs={
441             'emission_slug': episode.emission.slug,
442             'slug': episode.slug})
443
444
445 class DiffusionPropertiesView(UpdateView):
446     model = ScheduledDiffusion
447     fields = ['jingle', 'stream']
448     template_name = 'nonstop/streamed-diffusion.html'
449
450     @property
451     def fields(self):
452         diffusion = self.get_object().diffusion
453         fields = ['jingle']
454         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
455             fields.append('stream')
456         return fields
457
458     def form_valid(self, form):
459         episode = self.get_object().diffusion.episode
460         if 'stream' in self.fields and form.instance.stream_id is None:
461             messages.error(self.request, _('missing stream'))
462             return HttpResponseRedirect(reverse('episode-view', kwargs={
463                 'emission_slug': episode.emission.slug,
464                 'slug': episode.slug}))
465         response = super(DiffusionPropertiesView, self).form_valid(form)
466         messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
467         return response
468
469     def get_success_url(self):
470         episode = self.get_object().diffusion.episode
471         return reverse('episode-view', kwargs={
472             'emission_slug': episode.emission.slug,
473             'slug': episode.slug})
474
475
476 def jingle_audio_view(request, *args, **kwargs):
477     jingle = Jingle.objects.get(id=kwargs['pk'])
478     return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
479
480
481 class AjaxProgram(TemplateView):
482     template_name = 'nonstop/program-fragment.html'
483
484     def get_context_data(self, date, **kwargs):
485         context = super().get_context_data(**kwargs)
486         now = datetime.datetime.now()
487         if date:
488             date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
489         else:
490             date_start = datetime.datetime.today()
491         date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
492         today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
493         date_end = date_start + datetime.timedelta(days=1)
494         context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
495         for x in context['day_program']:
496             x.klass = x.__class__.__name__
497         previous_prog = None
498         for i, x in enumerate(context['day_program']):
499             if today and x.datetime > now and previous_prog:
500                 previous_prog.now = True
501                 break
502             previous_prog = x
503         return context