]> git.0d.be Git - django-panik-nonstop.git/blobdiff - nonstop/views.py
add view to generate munin data
[django-panik-nonstop.git] / nonstop / views.py
index cbab3c472b4db5d5d8fc08b4bc61eb6250098ff3..c8218cf475469fb05161be4b171f2b712c2f4cb8 100644 (file)
@@ -1,25 +1,34 @@
+import copy
 import csv
 import datetime
-from cStringIO import StringIO
 import os
 import tempfile
 
 import mutagen
 
 from django.core.files.storage import default_storage
-from django.core.urlresolvers import reverse
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.urlresolvers import reverse, reverse_lazy
 from django.contrib import messages
-from django.http import HttpResponse
+from django.db.models import Q, Sum
+from django.http import HttpResponse, HttpResponseRedirect, FileResponse, Http404
+from django.utils.six import StringIO
 from django.utils.translation import ugettext_lazy as _
 from django.views.generic.base import RedirectView, TemplateView
 from django.views.generic.dates import DayArchiveView
 from django.views.generic.detail import DetailView
-from django.views.generic.edit import FormView
+from django.views.generic.edit import CreateView, FormView, UpdateView
 from django.views.generic.list import ListView
 
-from .forms import UploadTracksForm
-from .models import SomaLogLine, Track, Artist, NonstopFile
-from emissions.models import Nonstop
+from .forms import UploadTracksForm, TrackMetaForm, TrackSearchForm, CleanupForm, ZoneSettingsForm
+from .models import (SomaLogLine, Track, Artist, NonstopFile,
+        ScheduledDiffusion, Jingle, Stream, NonstopZoneSettings)
+from emissions.models import Nonstop, Diffusion
+from emissions.utils import period_program
+
+from . import utils
+from .app_settings import app_settings
+
 
 class SomaDayArchiveView(DayArchiveView):
     queryset = SomaLogLine.objects.all()
@@ -36,15 +45,17 @@ class SomaDayArchiveCsvView(SomaDayArchiveView):
         for line in context['object_list']:
             if line.filepath.track:
                 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
-                    line.filepath.short.encode('utf-8', 'replace'),
-                    line.filepath.track.title.encode('utf-8', 'replace'),
-                    line.filepath.track.artist.name.encode('utf-8', 'replace'),
+                    line.filepath.short,
+                    line.filepath.track.title,
+                    line.filepath.track.artist.name,
                     line.filepath.track.language,
                     line.filepath.track.instru and 'instru' or '',
-                    line.filepath.track.cfwb and 'cfwb' or ''])
+                    line.filepath.track.cfwb and 'cfwb' or '',
+                    line.filepath.track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.track.added_to_nonstop_timestamp else '',
+                    ])
             else:
                 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
-                                line.filepath.short.encode('utf-8', 'replace')])
+                                line.filepath.short])
         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
 
 
@@ -60,6 +71,22 @@ class RedirectTodayView(RedirectView):
 class TrackDetailView(DetailView):
     model = Track
 
+    def get_context_data(self, **kwargs):
+        ctx = super(TrackDetailView, self).get_context_data(**kwargs)
+        ctx['metadata_form'] = TrackMetaForm(instance=self.object)
+        return ctx
+
+    def post(self, request, *args, **kwargs):
+        assert self.request.user.has_perm('nonstop.add_track')
+        instance = self.get_object()
+        old_nonstop_zones = copy.copy(instance.nonstop_zones.all())
+        form = TrackMetaForm(request.POST, instance=instance)
+        form.save()
+        new_nonstop_zones = self.get_object().nonstop_zones.all()
+        if set(old_nonstop_zones) != set(new_nonstop_zones):
+            instance.sync_nonstop_zones()
+        return HttpResponseRedirect('.')
+
 
 class ArtistDetailView(DetailView):
     model = Artist
@@ -80,6 +107,23 @@ class ZoneStats(object):
             self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
         self.qs = self.qs.distinct()
 
+    def total_duration(self, **kwargs):
+        try:
+            total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
+        except AttributeError:
+            # 'NoneType' object has no attribute 'total_seconds', if there's no
+            # track in queryset
+            return '-'
+        if total > 3600 * 2:
+            duration = _('%d hours') % (total / 3600)
+        else:
+            duration = _('%d minutes') % (total / 60)
+        start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
+        end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
+        if end < start:
+            end = end + datetime.timedelta(days=1)
+        return duration + _(', → %d days') % (total // (end - start).total_seconds())
+
     def count(self, **kwargs):
         return self.qs.filter(**kwargs).count()
 
@@ -107,15 +151,43 @@ class ZoneStats(object):
     def cfwb_percentage(self):
         return self.percentage(cfwb=True)
 
+    def language_set(self):
+        return self.count() - self.language_unset()
+
+    def language_unset(self):
+        return self.count(language='')
+
+    def unset_language_percentage(self):
+        return self.percentage(language='')
+
     def french(self):
         return self.count(language='fr')
 
+    def unset_or_na_language(self):
+        return self.qs.filter(Q(language='') | Q(language='na')).count()
+
     def french_percentage(self):
-        considered_tracks = self.count() - self.instru()
+        considered_tracks = self.count() - self.unset_or_na_language()
         if considered_tracks == 0:
             return '-'
         return '%.2f%%' % (100. * self.french() / considered_tracks)
 
+    def quota_french(self):
+        # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
+        # langue française
+        considered_tracks = self.count() - self.unset_or_na_language()
+        if considered_tracks == 0:
+            return True
+        return (100. * self.french() / considered_tracks) > 30.
+
+    def quota_cfwb(self):
+        # obligation de diffuser annuellement au moins 4,5% d'œuvres musicales
+        # émanant de la Communauté française
+        considered_tracks = self.count()
+        if considered_tracks == 0:
+            return True
+        return (100. * self.cfwb() / considered_tracks) > 4.5
+
     def new_files(self):
         return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
 
@@ -128,12 +200,13 @@ def parse_date(date):
         return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
     return datetime.datetime.strptime(date, '%Y-%m-%d').date()
 
+
 class StatisticsView(TemplateView):
     template_name = 'nonstop/statistics.html'
 
     def get_context_data(self, **kwargs):
         context = super(StatisticsView, self).get_context_data(**kwargs)
-        context['zones'] = Nonstop.objects.all().order_by('start')
+        context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
         kwargs = {}
         if 'from' in self.request.GET:
             kwargs['from_date'] = parse_date(self.request.GET['from'])
@@ -153,6 +226,7 @@ class UploadTracksView(FormView):
     success_url = '.'
 
     def post(self, request, *args, **kwargs):
+        assert self.request.user.has_perm('nonstop.add_track')
         form_class = self.get_form_class()
         form = self.get_form(form_class)
         tracks = request.FILES.getlist('tracks')
@@ -174,19 +248,361 @@ class UploadTracksView(FormView):
             return self.form_invalid(form)
 
         for f in tracks:
-            monthdir = datetime.datetime.today().strftime('%Y-%m')
-            filepath = '%s/%s' % (monthdir, f.name)
-            default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
-            nonstop_file = NonstopFile()
-            nonstop_file.set_track_filepath(filepath)
             metadata = metadatas[f.name]
-            artist, created = Artist.objects.get_or_create(name=metadata.get('artist')[0])
-            track, created = Track.objects.get_or_create(title=metadata.get('title')[0], artist=artist)
-            nonstop_file.track = track
-            nonstop_file.save()
+            artist_name = metadata.get('artist')[0]
+            track_title = metadata.get('title')[0]
+
+            monthdir = datetime.datetime.today().strftime('%Y-%m')
+            filepath = '%s/%s - %s - %s%s' % (monthdir,
+                datetime.datetime.today().strftime('%y%m%d'),
+                artist_name[:50].replace('/', ' ').strip(),
+                track_title[:80].replace('/', ' ').strip(),
+                os.path.splitext(f.name)[-1])
+
+            artist, created = Artist.objects.get_or_create(name=artist_name)
+            track, created = Track.objects.get_or_create(title=track_title, artist=artist,
+                    defaults={'uploader': self.request.user})
+            if created or not track.file_exists():
+                default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
+                nonstop_file = NonstopFile()
+                nonstop_file.set_track_filepath(filepath)
+                nonstop_file.track = track
+                nonstop_file.save()
+            else:
+                # don't keep duplicated file and do not create a duplicated nonstop file object
+                pass
             if request.POST.get('nonstop_zone'):
                 track.nonstop_zones.add(
                         Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
+            track.sync_nonstop_zones()
 
         messages.info(self.request, '%d new track(s)' % len(tracks))
         return self.form_valid(form)
+
+
+class TracksMetadataView(ListView):
+    template_name = 'nonstop/tracks_metadata.html'
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['view'] = self
+        return context
+
+    def get_queryset(self):
+        return Track.objects.exclude(nonstop_zones__isnull=True)
+
+    def post(self, request, *args, **kwargs):
+        assert self.request.user.has_perm('nonstop.add_track')
+        for track_id in request.POST.getlist('track'):
+            track = Track.objects.get(id=track_id)
+            track.language = request.POST.get('lang-%s' % track_id, '')
+            track.instru = 'instru-%s' % track_id in request.POST
+            track.sabam = 'sabam-%s' % track_id in request.POST
+            track.cfwb = 'cfwb-%s' % track_id in request.POST
+            track.save()
+        return HttpResponseRedirect('.')
+
+
+class RandomTracksMetadataView(TracksMetadataView):
+    page_title = _('Metadata of random tracks')
+
+    def get_queryset(self):
+        return super().get_queryset().filter(Q(language='') | Q(language__isnull=True)).order_by('?')[:50]
+
+
+class RecentTracksMetadataView(TracksMetadataView):
+    page_title = _('Metadata of recent tracks')
+
+    def get_queryset(self):
+        return super().get_queryset().exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
+
+
+class ArtistTracksMetadataView(TracksMetadataView):
+
+    @property
+    def page_title(self):
+        return _('Metadata of tracks from %s') % Artist.objects.get(id=self.kwargs['artist_pk']).name
+
+    def get_queryset(self):
+        return Track.objects.filter(artist_id=self.kwargs['artist_pk']).order_by('title')
+
+
+class QuickLinksView(TemplateView):
+    template_name = 'nonstop/quick_links.html'
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        day = datetime.datetime.today()
+        context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
+        return context
+
+
+class SearchView(TemplateView):
+    template_name = 'nonstop/search.html'
+
+    def get_queryset(self):
+        queryset = Track.objects.all()
+
+        q = self.request.GET.get('q')
+        if q:
+            queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
+
+        zone = self.request.GET.get('zone')
+        if zone:
+            from emissions.models import Nonstop
+            if zone == 'none':
+                queryset = queryset.filter(nonstop_zones=None)
+            elif zone == 'any':
+                queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
+            else:
+                queryset = queryset.filter(nonstop_zones=zone)
+
+        order = self.request.GET.get('order_by') or 'title'
+        if order:
+            if 'added_to_nonstop_timestamp' in order:
+                queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
+            queryset = queryset.order_by(order)
+        return queryset
+
+    def get_context_data(self, **kwargs):
+        ctx = super(SearchView, self).get_context_data(**kwargs)
+        ctx['form'] = TrackSearchForm(self.request.GET)
+        queryset = self.get_queryset()
+        qs = self.request.GET.copy()
+        qs.pop('page', None)
+        ctx['qs'] = qs.urlencode()
+
+        tracks = Paginator(queryset.select_related(), 20)
+
+        page = self.request.GET.get('page')
+        try:
+            ctx['tracks'] = tracks.page(page)
+        except PageNotAnInteger:
+            ctx['tracks'] = tracks.page(1)
+        except EmptyPage:
+            ctx['tracks'] = tracks.page(tracks.num_pages)
+
+        return ctx
+
+
+class SearchCsvView(SearchView):
+    def get(self, request, *args, **kwargs):
+        out = StringIO()
+        writer = csv.writer(out)
+        writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
+        for track in self.get_queryset():
+            writer.writerow([
+                track.title if track.title else 'Inconnu',
+                track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
+                ' + '.join([x.title for x in track.nonstop_zones.all()]),
+                track.language or '',
+                track.instru and 'instru' or '',
+                track.cfwb and 'cfwb' or '',
+                track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
+                ])
+        return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
+
+
+class CleanupView(TemplateView):
+    template_name = 'nonstop/cleanup.html'
+
+    def get_context_data(self, **kwargs):
+        ctx = super(CleanupView, self).get_context_data(**kwargs)
+        ctx['form'] = CleanupForm()
+
+        zone = self.request.GET.get('zone')
+        if zone:
+            from emissions.models import Nonstop
+            ctx['zone'] = Nonstop.objects.get(id=zone)
+            ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
+            ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
+                            'added_to_nonstop_timestamp').select_related()[:30]
+        return ctx
+
+    def post(self, request, *args, **kwargs):
+        assert self.request.user.has_perm('nonstop.add_track')
+        count = 0
+        for track_id in request.POST.getlist('track'):
+            if request.POST.get('remove-%s' % track_id):
+                track = Track.objects.get(id=track_id)
+                track.nonstop_zones.clear()
+                track.sync_nonstop_zones()
+                count += 1
+        if count:
+            messages.info(self.request, 'Removed %d new track(s)' % count)
+        return HttpResponseRedirect('.')
+
+
+class AddSomaDiffusionView(CreateView):
+    model = ScheduledDiffusion
+    fields = ['jingle', 'stream']
+    template_name = 'nonstop/streamed-diffusion.html'
+
+    @property
+    def fields(self):
+        diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
+        fields = ['jingle']
+        if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
+            fields.append('stream')
+        return fields
+
+    def get_initial(self):
+        initial = super(AddSomaDiffusionView, self).get_initial()
+        initial['jingle'] = None
+        if 'stream' in self.fields:
+            initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
+        initial['stream'] = Stream.objects.all().first()
+        return initial
+
+    def form_valid(self, form):
+        form.instance.diffusion_id = self.kwargs['pk']
+        episode = form.instance.diffusion.episode
+        if 'stream' in self.fields and form.instance.stream_id is None:
+            messages.error(self.request, _('missing stream'))
+            return HttpResponseRedirect(reverse('episode-view', kwargs={
+                'emission_slug': episode.emission.slug,
+                'slug': episode.slug}))
+        response = super(AddSomaDiffusionView, self).form_valid(form)
+        messages.info(self.request, _('%s added to schedule') % episode.emission.title)
+        return response
+
+    def get_success_url(self):
+        diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
+        episode = diffusion.episode
+        return reverse('episode-view', kwargs={
+            'emission_slug': episode.emission.slug,
+            'slug': episode.slug})
+
+
+class DelSomaDiffusionView(RedirectView):
+    def get_redirect_url(self, pk):
+        soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
+        episode = soma_diffusion.episode
+        ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
+        messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
+        return reverse('episode-view', kwargs={
+            'emission_slug': episode.emission.slug,
+            'slug': episode.slug})
+
+
+class DiffusionPropertiesView(UpdateView):
+    model = ScheduledDiffusion
+    fields = ['jingle', 'stream']
+    template_name = 'nonstop/streamed-diffusion.html'
+
+    @property
+    def fields(self):
+        diffusion = self.get_object().diffusion
+        fields = ['jingle']
+        if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
+            fields.append('stream')
+        return fields
+
+    def form_valid(self, form):
+        episode = self.get_object().diffusion.episode
+        if 'stream' in self.fields and form.instance.stream_id is None:
+            messages.error(self.request, _('missing stream'))
+            return HttpResponseRedirect(reverse('episode-view', kwargs={
+                'emission_slug': episode.emission.slug,
+                'slug': episode.slug}))
+        response = super(DiffusionPropertiesView, self).form_valid(form)
+        messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
+        return response
+
+    def get_success_url(self):
+        episode = self.get_object().diffusion.episode
+        return reverse('episode-view', kwargs={
+            'emission_slug': episode.emission.slug,
+            'slug': episode.slug})
+
+
+def jingle_audio_view(request, *args, **kwargs):
+    jingle = Jingle.objects.get(id=kwargs['pk'])
+    return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
+
+
+class AjaxProgram(TemplateView):
+    template_name = 'nonstop/program-fragment.html'
+
+    def get_context_data(self, date, **kwargs):
+        context = super().get_context_data(**kwargs)
+        now = datetime.datetime.now()
+        if date:
+            date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
+        else:
+            date_start = datetime.datetime.today()
+        date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
+        today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
+        date_end = date_start + datetime.timedelta(days=1)
+        context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
+        for x in context['day_program']:
+            x.klass = x.__class__.__name__
+        previous_prog = None
+        for i, x in enumerate(context['day_program']):
+            if today and x.datetime > now and previous_prog:
+                previous_prog.now = True
+                break
+            previous_prog = x
+        return context
+
+
+class ZoneSettings(FormView):
+    form_class = ZoneSettingsForm
+    template_name = 'nonstop/zone_settings.html'
+    success_url = reverse_lazy('nonstop-quick-links')
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['zone'] = Nonstop.objects.get(slug=self.kwargs['slug'])
+        return context
+
+    def get_initial(self):
+        try:
+            zone = Nonstop.objects.get(slug=self.kwargs['slug'])
+        except Nonstop.DoesNotExist:
+            raise Http404()
+        zone_settings = zone.nonstopzonesettings_set.first()
+        if zone_settings is None:
+            zone_settings = NonstopZoneSettings(nonstop=zone)
+            zone_settings.save()
+        initial = super().get_initial()
+        initial['start'] = zone.start.strftime('%H:%M') if zone.start else None
+        initial['end'] = zone.end.strftime('%H:%M') if zone.end else None
+        initial['intro_jingle'] = zone_settings.intro_jingle_id
+        initial['jingles'] = [x.id for x in zone_settings.jingles.all()]
+        return initial
+
+    def form_valid(self, form):
+        zone = Nonstop.objects.get(slug=self.kwargs['slug'])
+        zone_settings = zone.nonstopzonesettings_set.first()
+        zone.start = form.cleaned_data['start']
+        zone.end = form.cleaned_data['end']
+        zone_settings.jingles.set(form.cleaned_data['jingles'])
+        zone_settings.intro_jingle_id = form.cleaned_data['intro_jingle']
+        zone.save()
+        zone_settings.save()
+        return super().form_valid(form)
+
+
+class MuninTracks(StatisticsView):
+    template_name = 'nonstop/munin_tracks.txt'
+    content_type = 'text/plain; charset=utf-8'
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['nonstop_general_total'] = Track.objects.count()
+        active_tracks_qs = Track.objects.filter(nonstop_zones__isnull=False).distinct()
+        context['nonstop_general_active'] = active_tracks_qs.count()
+        context['nonstop_percentages_instru'] = 100 * (
+                active_tracks_qs.filter(instru=True).count() /
+                context['nonstop_general_active'])
+        context['nonstop_percentages_cfwb'] = 100 * (
+                active_tracks_qs.filter(cfwb=True).count() /
+                context['nonstop_general_active'])
+        context['nonstop_percentages_langset'] = 100 * (
+                active_tracks_qs.exclude(language='').count() /
+                context['nonstop_general_active'])
+        context['nonstop_percentages_french'] = 100 * (
+                active_tracks_qs.filter(language='fr').count() /
+                active_tracks_qs.exclude(language__isnull=True).exclude(language__in=('', 'na')).count())
+        return context