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