]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/views.py
f30846edf75e686f2b6b1365702aff83739ff6e0
[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         total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
111         if total > 3600 * 2:
112             duration = _('%d hours') % (total / 3600)
113         else:
114             duration = _('%d minutes') % (total / 60)
115         start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
116         end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
117         if end < start:
118             end = end + datetime.timedelta(days=1)
119         return duration + _(', → %d days') % (total // (end - start).total_seconds())
120
121     def count(self, **kwargs):
122         return self.qs.filter(**kwargs).count()
123
124     def percentage(self, **kwargs):
125         total = self.count()
126         if total == 0:
127             return '-'
128         return '%.2f%%' % (100. * self.count(**kwargs) / total)
129
130     def instru(self):
131         return self.count(instru=True)
132
133     def instru_percentage(self):
134         return self.percentage(instru=True)
135
136     def sabam(self):
137         return self.count(sabam=True)
138
139     def sabam_percentage(self):
140         return self.percentage(sabam=True)
141
142     def cfwb(self):
143         return self.count(cfwb=True)
144
145     def cfwb_percentage(self):
146         return self.percentage(cfwb=True)
147
148     def french(self):
149         return self.count(language='fr')
150
151     def french_percentage(self):
152         considered_tracks = self.count() - self.instru()
153         if considered_tracks == 0:
154             return '-'
155         return '%.2f%%' % (100. * self.french() / considered_tracks)
156
157     def new_files(self):
158         return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
159
160     def percent_new_files(self):
161         return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
162
163
164 def parse_date(date):
165     if date.endswith('d'):
166         return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
167     return datetime.datetime.strptime(date, '%Y-%m-%d').date()
168
169 class StatisticsView(TemplateView):
170     template_name = 'nonstop/statistics.html'
171
172     def get_context_data(self, **kwargs):
173         context = super(StatisticsView, self).get_context_data(**kwargs)
174         context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
175         kwargs = {}
176         if 'from' in self.request.GET:
177             kwargs['from_date'] = parse_date(self.request.GET['from'])
178             context['from_date'] = kwargs['from_date']
179         if 'until' in self.request.GET:
180             kwargs['until_date'] = parse_date(self.request.GET['until'])
181         if 'onair' in self.request.GET:
182             kwargs['nonstopfile__somalogline__on_air'] = True
183         for zone in context['zones']:
184             zone.stats = ZoneStats(zone, **kwargs)
185         return context
186
187
188 class UploadTracksView(FormView):
189     form_class = UploadTracksForm
190     template_name = 'nonstop/upload.html'
191     success_url = '.'
192
193     def post(self, request, *args, **kwargs):
194         assert self.request.user.has_perm('nonstop.add_track')
195         form_class = self.get_form_class()
196         form = self.get_form(form_class)
197         tracks = request.FILES.getlist('tracks')
198         if not form.is_valid():
199             return self.form_invalid(form)
200         missing_metadata = []
201         metadatas = {}
202         for f in tracks:
203             with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
204                 tmpfile.write(f.read())
205                 f.seek(0)
206                 metadata = mutagen.File(tmpfile.name, easy=True)
207             if not metadata or not metadata.get('artist') or not metadata.get('title'):
208                 missing_metadata.append(f.name)
209             else:
210                 metadatas[f.name] = metadata
211         if missing_metadata:
212             form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
213             return self.form_invalid(form)
214
215         for f in tracks:
216             metadata = metadatas[f.name]
217             artist_name = metadata.get('artist')[0]
218             track_title = metadata.get('title')[0]
219
220             monthdir = datetime.datetime.today().strftime('%Y-%m')
221             filepath = '%s/%s - %s - %s%s' % (monthdir,
222                 datetime.datetime.today().strftime('%y%m%d'),
223                 artist_name[:50].replace('/', ' ').strip(),
224                 track_title[:80].replace('/', ' ').strip(),
225                 os.path.splitext(f.name)[-1])
226
227             artist, created = Artist.objects.get_or_create(name=artist_name)
228             track, created = Track.objects.get_or_create(title=track_title, artist=artist,
229                     defaults={'uploader': self.request.user})
230             if created or not track.file_exists():
231                 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
232                 nonstop_file = NonstopFile()
233                 nonstop_file.set_track_filepath(filepath)
234                 nonstop_file.track = track
235                 nonstop_file.save()
236             else:
237                 # don't keep duplicated file and do not create a duplicated nonstop file object
238                 pass
239             if request.POST.get('nonstop_zone'):
240                 track.nonstop_zones.add(
241                         Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
242             track.sync_nonstop_zones()
243
244         messages.info(self.request, '%d new track(s)' % len(tracks))
245         return self.form_valid(form)
246
247
248 class RecentTracksView(ListView):
249     template_name = 'nonstop/recent_tracks.html'
250
251     def get_queryset(self):
252         return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
253
254     def post(self, request, *args, **kwargs):
255         assert self.request.user.has_perm('nonstop.add_track')
256         for track_id in request.POST.getlist('track'):
257             track = Track.objects.get(id=track_id)
258             track.language = request.POST.get('lang-%s' % track_id, '')
259             track.instru = 'instru-%s' % track_id in request.POST
260             track.sabam = 'sabam-%s' % track_id in request.POST
261             track.cfwb = 'cfwb-%s' % track_id in request.POST
262             track.save()
263         return HttpResponseRedirect('.')
264
265
266 class QuickLinksView(TemplateView):
267     template_name = 'nonstop/quick_links.html'
268
269     def get_context_data(self, **kwargs):
270         context = super().get_context_data(**kwargs)
271         day = datetime.datetime.today()
272         context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
273         return context
274
275
276 class SearchView(TemplateView):
277     template_name = 'nonstop/search.html'
278
279     def get_queryset(self):
280         queryset = Track.objects.all()
281
282         q = self.request.GET.get('q')
283         if q:
284             queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
285
286         zone = self.request.GET.get('zone')
287         if zone:
288             from emissions.models import Nonstop
289             if zone == 'none':
290                 queryset = queryset.filter(nonstop_zones=None)
291             elif zone == 'any':
292                 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
293             else:
294                 queryset = queryset.filter(nonstop_zones=zone)
295
296         order = self.request.GET.get('order_by') or 'title'
297         if order:
298             if 'added_to_nonstop_timestamp' in order:
299                 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
300             queryset = queryset.order_by(order)
301         return queryset
302
303     def get_context_data(self, **kwargs):
304         ctx = super(SearchView, self).get_context_data(**kwargs)
305         ctx['form'] = TrackSearchForm(self.request.GET)
306         queryset = self.get_queryset()
307         qs = self.request.GET.copy()
308         qs.pop('page', None)
309         ctx['qs'] = qs.urlencode()
310
311         tracks = Paginator(queryset.select_related(), 20)
312
313         page = self.request.GET.get('page')
314         try:
315             ctx['tracks'] = tracks.page(page)
316         except PageNotAnInteger:
317             ctx['tracks'] = tracks.page(1)
318         except EmptyPage:
319             ctx['tracks'] = tracks.page(tracks.num_pages)
320
321         return ctx
322
323
324 class SearchCsvView(SearchView):
325     def get(self, request, *args, **kwargs):
326         out = StringIO()
327         writer = csv.writer(out)
328         writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
329         for track in self.get_queryset():
330             writer.writerow([
331                 track.title if track.title else 'Inconnu',
332                 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
333                 ' + '.join([x.title for x in track.nonstop_zones.all()]),
334                 track.language or '',
335                 track.instru and 'instru' or '',
336                 track.cfwb and 'cfwb' or '',
337                 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
338                 ])
339         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
340
341
342 class CleanupView(TemplateView):
343     template_name = 'nonstop/cleanup.html'
344
345     def get_context_data(self, **kwargs):
346         ctx = super(CleanupView, self).get_context_data(**kwargs)
347         ctx['form'] = CleanupForm()
348
349         zone = self.request.GET.get('zone')
350         if zone:
351             from emissions.models import Nonstop
352             ctx['zone'] = Nonstop.objects.get(id=zone)
353             ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
354             ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
355                             'added_to_nonstop_timestamp').select_related()[:30]
356         return ctx
357
358     def post(self, request, *args, **kwargs):
359         assert self.request.user.has_perm('nonstop.add_track')
360         count = 0
361         for track_id in request.POST.getlist('track'):
362             if request.POST.get('remove-%s' % track_id):
363                 track = Track.objects.get(id=track_id)
364                 track.nonstop_zones.clear()
365                 track.sync_nonstop_zones()
366                 count += 1
367         if count:
368             messages.info(self.request, 'Removed %d new track(s)' % count)
369         return HttpResponseRedirect('.')
370
371
372 class AddSomaDiffusionView(CreateView):
373     model = ScheduledDiffusion
374     fields = ['jingle', 'stream']
375     template_name = 'nonstop/streamed-diffusion.html'
376
377     @property
378     def fields(self):
379         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
380         fields = ['jingle']
381         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
382             fields.append('stream')
383         return fields
384
385     def get_initial(self):
386         initial = super(AddSomaDiffusionView, self).get_initial()
387         initial['jingle'] = None
388         if 'stream' in self.fields:
389             initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
390         initial['stream'] = Stream.objects.all().first()
391         return initial
392
393     def form_valid(self, form):
394         form.instance.diffusion_id = self.kwargs['pk']
395         episode = form.instance.diffusion.episode
396         if 'stream' in self.fields and form.instance.stream_id is None:
397             messages.error(self.request, _('missing stream'))
398             return HttpResponseRedirect(reverse('episode-view', kwargs={
399                 'emission_slug': episode.emission.slug,
400                 'slug': episode.slug}))
401         response = super(AddSomaDiffusionView, self).form_valid(form)
402         messages.info(self.request, _('%s added to schedule') % episode.emission.title)
403         return response
404
405     def get_success_url(self):
406         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
407         episode = diffusion.episode
408         return reverse('episode-view', kwargs={
409             'emission_slug': episode.emission.slug,
410             'slug': episode.slug})
411
412
413 class DelSomaDiffusionView(RedirectView):
414     def get_redirect_url(self, pk):
415         soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
416         episode = soma_diffusion.episode
417         ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
418         messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
419         return reverse('episode-view', kwargs={
420             'emission_slug': episode.emission.slug,
421             'slug': episode.slug})
422
423
424 class DiffusionPropertiesView(UpdateView):
425     model = ScheduledDiffusion
426     fields = ['jingle', 'stream']
427     template_name = 'nonstop/streamed-diffusion.html'
428
429     @property
430     def fields(self):
431         diffusion = self.get_object().diffusion
432         fields = ['jingle']
433         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
434             fields.append('stream')
435         return fields
436
437     def form_valid(self, form):
438         episode = self.get_object().diffusion.episode
439         if 'stream' in self.fields and form.instance.stream_id is None:
440             messages.error(self.request, _('missing stream'))
441             return HttpResponseRedirect(reverse('episode-view', kwargs={
442                 'emission_slug': episode.emission.slug,
443                 'slug': episode.slug}))
444         response = super(DiffusionPropertiesView, self).form_valid(form)
445         messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
446         return response
447
448     def get_success_url(self):
449         episode = self.get_object().diffusion.episode
450         return reverse('episode-view', kwargs={
451             'emission_slug': episode.emission.slug,
452             'slug': episode.slug})
453
454
455 def jingle_audio_view(request, *args, **kwargs):
456     jingle = Jingle.objects.get(id=kwargs['pk'])
457     return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
458
459
460 class AjaxProgram(TemplateView):
461     template_name = 'nonstop/program-fragment.html'
462
463     def get_context_data(self, date, **kwargs):
464         context = super().get_context_data(**kwargs)
465         now = datetime.datetime.now()
466         if date:
467             date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
468         else:
469             date_start = datetime.datetime.today()
470         date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
471         today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
472         date_end = date_start + datetime.timedelta(days=1)
473         context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
474         for x in context['day_program']:
475             x.klass = x.__class__.__name__
476         previous_prog = None
477         for i, x in enumerate(context['day_program']):
478             if today and x.datetime > now and previous_prog:
479                 previous_prog.now = True
480                 break
481             previous_prog = x
482         return context