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