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