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