import copy
import csv
import datetime
-from cStringIO import StringIO
import os
import tempfile
from django.core.files.storage import default_storage
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
-from django.db.models import Q
-from django.http import HttpResponse, HttpResponseRedirect
+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, TrackMetaForm, TrackSearchForm, CleanupForm
-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()
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.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.added_to_nonstop_timestamp else '',
+ 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')
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()
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)
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'])
track_title[:80].replace('/', ' ').strip(),
os.path.splitext(f.name)[-1])
- default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
-
- nonstop_file = NonstopFile()
- nonstop_file.set_track_filepath(filepath)
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})
- nonstop_file.track = track
- nonstop_file.save()
+ 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')))
- nonstop_file.track.sync_nonstop_zones()
+ track.sync_nonstop_zones()
messages.info(self.request, '%d new track(s)' % len(tracks))
return self.form_valid(form)
-class RecentTracksView(ListView):
- template_name = 'nonstop/recent_tracks.html'
+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(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
+ return Track.objects.exclude(nonstop_zones__isnull=True)
def post(self, request, *args, **kwargs):
assert self.request.user.has_perm('nonstop.add_track')
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'
writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
for track in self.get_queryset():
writer.writerow([
- track.title.encode('utf-8', 'replace') if track.title else 'Inconnu',
- track.artist.name.encode('utf-8', 'replace') if (track.artist and track.artist.name) else 'Inconnu',
- ' + '.join([x.title.encode('utf-8') for x in track.nonstop_zones.all()]),
+ 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.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')
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