diff --git a/profile/migrations/0002_auto_20171129_1828.py b/profile/migrations/0002_auto_20171129_1828.py new file mode 100644 index 0000000000000000000000000000000000000000..737c88e2a016c6b5f5f4248cd2757fbe2f43c753 --- /dev/null +++ b/profile/migrations/0002_auto_20171129_1828.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-11-29 18:28 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import tinymce.models +import versatileimagefield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('profile', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='biography', + field=tinymce.models.HTMLField(blank=True, help_text='Describe yourself and your fields of interest in a few sentences.', null=True, verbose_name='Biography'), + ), + migrations.AlterField( + model_name='profile', + name='cba_url', + field=models.URLField(blank=True, help_text='URL to your CBA profile.', verbose_name='CBA URL'), + ), + migrations.AlterField( + model_name='profile', + name='cba_user_token', + field=models.CharField(blank=True, help_text='The CBA upload token for your account. This is NOT your password which you use to log into CBA!', max_length=255, verbose_name='CBA Token'), + ), + migrations.AlterField( + model_name='profile', + name='cba_username', + field=models.CharField(blank=True, help_text='Your username in CBA. This is necessary for uploading files to your account.', max_length=60, verbose_name='CBA Username'), + ), + migrations.AlterField( + model_name='profile', + name='dorftv_url', + field=models.URLField(blank=True, help_text='URL to your dorfTV channel.', verbose_name='DorfTV URL'), + ), + migrations.AlterField( + model_name='profile', + name='facebook_url', + field=models.URLField(blank=True, help_text='URL to your Facebook profile.', verbose_name='Facebook URL'), + ), + migrations.AlterField( + model_name='profile', + name='googleplus_url', + field=models.URLField(blank=True, help_text='URL to your Google+ profile.', verbose_name='Google+ URL'), + ), + migrations.AlterField( + model_name='profile', + name='image', + field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload a picture of yourself. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='user_images', verbose_name='Profile picture', width_field='width'), + ), + migrations.AlterField( + model_name='profile', + name='linkedin_url', + field=models.URLField(blank=True, help_text='URL to your LinkedIn profile.', verbose_name='LinkedIn URL'), + ), + migrations.AlterField( + model_name='profile', + name='twitter_url', + field=models.URLField(blank=True, help_text='URL to your Twitter profile.', verbose_name='Twitter URL'), + ), + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='profile', + name='website', + field=models.URLField(blank=True, help_text='URL to your personal website.', verbose_name='Website'), + ), + migrations.AlterField( + model_name='profile', + name='youtube_url', + field=models.URLField(blank=True, help_text='URL to your Youtube channel.', verbose_name='Youtube URL'), + ), + ] diff --git a/profile/models.py b/profile/models.py index a02b556b0fd63ca0346f5bbf800fda9b1ba7fd48..bcb5ebd4b4c84f52fe8b1706c8d808d7c7b749be 100644 --- a/profile/models.py +++ b/profile/models.py @@ -11,7 +11,7 @@ import os from tinymce import models as tinymce_models class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', editable=False) biography = tinymce_models.HTMLField(_("Biography"), blank=True, null=True, help_text=_("Describe yourself and your fields of interest in a few sentences.")) website = models.URLField(_("Website"), blank=True, help_text=_("URL to your personal website.")) googleplus_url = models.URLField(_("Google+ URL"), blank=True, help_text=_("URL to your Google+ profile.")) diff --git a/program/migrations/0012_auto_20171122_1718.py b/program/migrations/0012_auto_20171129_1828.py similarity index 77% rename from program/migrations/0012_auto_20171122_1718.py rename to program/migrations/0012_auto_20171129_1828.py index 3141a578f5162e2d4a1f7f2880dbf6863d15b641..b9e2d992eb496711873a14b358dde40e4cabd354 100644 --- a/program/migrations/0012_auto_20171122_1718.py +++ b/program/migrations/0012_auto_20171129_1828.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-11-22 17:18 +# Generated by Django 1.11.3 on 2017-11-29 18:28 from __future__ import unicode_literals from django.conf import settings @@ -153,7 +153,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='note', name='cba_id', - field=models.IntegerField(blank=True, null=True, verbose_name='CBA ID'), + field=models.IntegerField(blank=True, help_text="Link the note to a certain CBA post by giving its ID. (E.g. if your post's CBA URL is https://cba.fro.at/1234, then your CBA ID is 1234)", null=True, verbose_name='CBA ID'), ), migrations.AddField( model_name='note', @@ -163,7 +163,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='note', name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), + field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), ), migrations.AddField( model_name='note', @@ -173,13 +173,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='note', name='slug', - field=models.SlugField(default=1, max_length=32, unique=True, verbose_name='Slug'), + field=models.SlugField(default=1, help_text='A simple to read URL for your show.', max_length=32, unique=True, verbose_name='Slug'), preserve_default=False, ), migrations.AddField( model_name='note', name='summary', - field=tinymce.models.HTMLField(blank=True, verbose_name='Summary'), + field=models.TextField(blank=True, help_text='Describe your upcoming show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Summary'), ), migrations.AddField( model_name='note', @@ -194,7 +194,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='show', name='cba_series_id', - field=models.IntegerField(blank=True, null=True, verbose_name='CBA Series ID'), + field=models.IntegerField(blank=True, help_text='Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your ID under https://cba.fro.at/series', null=True, verbose_name='CBA Series ID'), ), migrations.AddField( model_name='show', @@ -231,6 +231,11 @@ class Migration(migrations.Migration): name='memo', field=models.TextField(blank=True, verbose_name='Memo'), ), + migrations.AddField( + model_name='timeslot', + name='playlist_id', + field=models.IntegerField(null=True, verbose_name='Playlist ID'), + ), migrations.AlterField( model_name='musicfocus', name='big_button', @@ -246,15 +251,55 @@ class Migration(migrations.Migration): name='button_hover', field=models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)'), ), + migrations.AlterField( + model_name='note', + name='content', + field=tinymce.models.HTMLField(help_text='Describe your upcoming show in detail.', verbose_name='Content'), + ), migrations.AlterField( model_name='note', name='show', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='program.Show'), ), + migrations.AlterField( + model_name='note', + name='title', + field=models.CharField(help_text="Give your note a good headline. What will your upcoming show be about? Try to arouse interest to listen to it!<br>Avoid technical data like the show's name, its airing times or its episode number. These data are added automatically.", max_length=128, verbose_name='Title'), + ), + migrations.AlterField( + model_name='show', + name='description', + field=tinymce.models.HTMLField(blank=True, help_text='Describe your show in detail.', null=True, verbose_name='Description'), + ), + migrations.AlterField( + model_name='show', + name='email', + field=models.EmailField(blank=True, help_text='The main contact email address for your show.', max_length=254, null=True, verbose_name='E-Mail'), + ), migrations.AlterField( model_name='show', name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='show_images', verbose_name='Image', width_field='width'), + field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='show_images', verbose_name='Image', width_field='width'), + ), + migrations.AlterField( + model_name='show', + name='name', + field=models.CharField(help_text="The show's name. Avoid a subtitle.", max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='show', + name='short_description', + field=models.TextField(help_text='Describe your show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Short description'), + ), + migrations.AlterField( + model_name='show', + name='slug', + field=models.CharField(help_text='A simple to read URL for your show', max_length=255, unique=True, verbose_name='Slug'), + ), + migrations.AlterField( + model_name='show', + name='website', + field=models.URLField(blank=True, help_text='Is there a website to your show? Type in its URL.', null=True, verbose_name='Website'), ), migrations.AlterField( model_name='timeslot', diff --git a/program/models.py b/program/models.py index a5f8b494d887fe48b48959ddba800a43cb8cdb78..a3a56598ca1de3b4d46359173fafa74ad5e199d6 100644 --- a/program/models.py +++ b/program/models.py @@ -566,6 +566,7 @@ class TimeSlot(models.Model): show = models.ForeignKey(Show, editable=False, related_name='timeslots') memo = models.TextField(_("Memo"), blank=True) is_repetition = models.BooleanField(_("WH"), default=False) + playlist_id = models.IntegerField(_("Playlist ID"), null=True) objects = TimeSlotManager() @@ -604,7 +605,7 @@ class Note(models.Model): timeslot = models.OneToOneField(TimeSlot, verbose_name=_("Time slot"), unique=True) title = models.CharField(_("Title"), max_length=128, help_text=_("Give your note a good headline. What will your upcoming show be about? Try to arouse interest to listen to it!<br>Avoid technical data like the show's name, its airing times or its episode number. These data are added automatically.")) slug = models.SlugField(_("Slug"), max_length=32, unique=True, help_text=_("A simple to read URL for your show.")) - summary = tinymce_models.HTMLField(_("Summary"), blank=True, help_text=_("Describe your upcoming show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.")) + summary = models.TextField(_("Summary"), blank=True, help_text=_("Describe your upcoming show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.")) content = tinymce_models.HTMLField(_("Content"), help_text=_("Describe your upcoming show in detail.")) ppoi = PPOIField('Image PPOI') height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False) diff --git a/program/serializers.py b/program/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..fc82e6089b2e090482f0100794cb2ed9de2ac95f --- /dev/null +++ b/program/serializers.py @@ -0,0 +1,177 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.auth.models import User +from rest_framework import serializers, status +from rest_framework.response import Response +from program.models import Show, TimeSlot, Category, Host, Language, Topic, MusicFocus, Note +from profile.models import Profile + + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = '__all__' + + +class UserSerializer(serializers.ModelSerializer): + # Add profile fields to JSON + profile = ProfileSerializer() + + class Meta: + model = User + exclude = ('password',) + #fields = '__all__' + + + def update(self, instance, validated_data): + """ + Update and return an existing User instance, given the validated data. + """ + + instance.first_name = validated_data.get('first_name', instance.first_name) + instance.last_name = validated_data.get('last_name', instance.last_name) + instance.email = validated_data.get('email', instance.email) + + profile = Profile.objects.get(user=instance.id) + profile.biography = validated_data['profile'].get('biography') + profile.website = validated_data['profile'].get('website') + profile.googleplus_url = validated_data['profile'].get('googleplus_url') + profile.facebook_url = validated_data['profile'].get('facebook_url') + profile.twitter_url = validated_data['profile'].get('twitter_url') + profile.linkedin_url = validated_data['profile'].get('linkedin_url') + profile.youtube_url = validated_data['profile'].get('youtube_url') + profile.dorftv_url = validated_data['profile'].get('dorftv_url') + profile.cba_url = validated_data['profile'].get('cba_url') + profile.cba_username = validated_data['profile'].get('cba_username') + profile.cba_user_token = validated_data['profile'].get('cba_user_token') + profile.save() + + instance.save() + return instance + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = '__all__' + + +class HostSerializer(serializers.ModelSerializer): + class Meta: + model = Host + fields = '__all__' + + +class LanguageSerializer(serializers.ModelSerializer): + class Meta: + model = Language + fields = '__all__' + + +class TopicSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + fields = '__all__' + + +class MusicFocusSerializer(serializers.ModelSerializer): + class Meta: + model = MusicFocus + fields = '__all__' + + +class ShowSerializer(serializers.HyperlinkedModelSerializer): + category = CategorySerializer(many=True) + hosts = HostSerializer(many=True) + language = LanguageSerializer(many=True) + topic = TopicSerializer(many=True) + musicfocus = MusicFocusSerializer(many=True) + + class Meta: + model = Show + fields = ('id', 'name', 'slug', 'image', 'logo', 'short_description', 'description', + 'email', 'website', 'created', 'last_updated', 'type_id', 'rtrcategory_id', + 'predecessor_id', 'cba_series_id', 'fallback_pool', 'category', 'hosts', + 'language', 'topic', 'musicfocus') + + + def create(self, validated_data): + """ + Create and return a new Show instance, given the validated data. + """ + return Show.objects.create(**validated_data) + + + def update(self, instance, validated_data): + """ + Update and return an existing Show instance, given the validated data. + """ + + instance.name = validated_data.get('name', instance.name) + instance.slug = validated_data.get('slug', instance.slug) + instance.image = validated_data.get('image', instance.image) + instance.logo = validated_data.get('logo', instance.logo) + instance.short_description = validated_data.get('short_description', instance.short_description) + instance.description = validated_data.get('description', instance.description) + instance.email = validated_data.get('email', instance.email) + instance.website = validated_data.get('website', instance.website) + instance.cba_series_id = validated_data.get('cba_series_id', instance.cba_series_id) + instance.fallback_pool = validated_data.get('fallback_pool', instance.fallback_pool) + instance.save() + return instance + + +class TimeSlotSerializer(serializers.ModelSerializer): + class Meta: + model = TimeSlot + fields = '__all__' + + + def create(self, validated_data): + """ + Create and return a new TimeSlot instance, given the validated data. + """ + return TimeSlot.objects.create(**validated_data) + + + def update(self, instance, validated_data): + """ + Update and return an existing Show instance, given the validated data. + """ + + instance.memo = validated_data.get('memo', instance.memo) + instance.is_repetition = validated_data.get('is_repetition', instance.is_repetition) + instance.playlist_id = validated_data.get('playlist_id', instance.playlist_id) + instance.save() + return instance + + +class NoteSerializer(serializers.ModelSerializer): + class Meta: + model = Note + fields = '__all__' + + + def create(self, validated_data): + """ + Create and return a new Note instance, given the validated data. + """ + + return Note.objects.create(**validated_data) + + + def update(self, instance, validated_data): + """ + Update and return an existing Note instance, given the validated data. + """ + + instance.show_id = validated_data.get('show_id', instance.show_id) + instance.timeslot_id = validated_data.get('timeslot_id', instance.timeslot_id) + instance.title = validated_data.get('title', instance.title) + instance.slug = validated_data.get('slug', instance.slug) + instance.summary = validated_data.get('summary', instance.summary) + instance.content = validated_data.get('content', instance.content) + instance.image = validated_data.get('image', instance.image) + instance.status = validated_data.get('status', instance.status) + instance.cba_id = validated_data.get('cba_id', instance.cba_id) + instance.save() + return instance \ No newline at end of file diff --git a/program/templates/calendar.html b/program/templates/calendar.html index 318f052ebb5cf638b0bd3aba685ece1f0eab590a..4f8d7f4f153c4a314bf68a456f603cfee332b6d0 100644 --- a/program/templates/calendar.html +++ b/program/templates/calendar.html @@ -67,6 +67,14 @@ font-size:.8em; } + .default { + background-color:#3a87ad; + } + .danger { + background-color:#D60935; + border-color:#222; + } + </style> </head> @@ -85,14 +93,23 @@ <div class="calendar-container"> <div id="calendar"></div> <div id="sidebar"> - <div id="timeslot-id"></div> + <!--<div id="timeslot-id"></div>--> + <div> + <span id="show-name"></span> + <span id="show-id"></span> + </div> <div id="timeslot-start"></div> <div id="timeslot-end"></div> - <div id="show-name"></div> - <div id="show-id"></div> - <div id="show-hosts"></div> + <p></p> + <div id="playlist-id"></div> <div id="is-repetition"></div> <div id="fallback-playlist-id"></div> + <p></p> + <div id="show-hosts"></div> + <div id="show-categories"></div> + <div id="show-type"></div> + <div id="show-musicfocus"></div> + <div id="show-rtrcategory"></div> <div id="response-message"></div> </div> </div> @@ -130,7 +147,7 @@ }, weekNumberCalculation: 'ISO', // Week begins with Monday firstDay: 1, // Week begins with Monday - events: '/export/week_schedule', + events: '/api/v1/week_schedule', eventRender: function(event, element) { element.find('.fc-content').append( '<span class="closeon">X</span>' ); element.find('.closeon').click(function() { @@ -139,7 +156,7 @@ return false; // Delete from database - jQuery.post( '/program/calendar/', { 'action': 'delete_timeslot', 'id': event._id } ) + jQuery.post( '/api/v1/timeslots/' + event._id + '/', { 'action': 'delete_timeslot', 'id': event._id } ) .done(function( data ) { // Remove element from DOM jQuery('#calendar').fullCalendar('removeEvents', event._id ); @@ -164,31 +181,21 @@ // Load the timeslot into the sidebar form eventClick: function(calEvent, jsEvent, view) { console.log(calEvent); - jQuery.ajax({ - url: '/export/get_timeslot', - type: 'GET', - data: { - 'timeslot_id': calEvent.id, - 'csrfmiddlewaretoken': jQuery('input[name="csrfmiddlewartetoken"]').val() - }, - success: function(timeslot) { - jQuery("#timeslot-id").html(timeslot.id); - jQuery("#timeslot-start").html(timeslot.start); - jQuery("#timeslot-end").html(timeslot.end); - jQuery("#show-name").html(timeslot.show_name); - jQuery("#show-id").html(timeslot.show_id); - jQuery("#show-hosts").html(timeslot.show_hosts); - jQuery("#is-repetition").html(timeslot.is_repetition); - jQuery("#fallback-playlist-id").html(timeslot.fallback_playlist_id); - jQuery("#memo").html(timeslot.memo); - jQuery("#response-message").html("Success"); - }, - error: function() { - jQuery("#response-message").html("An error occured"); - } - - }); + jQuery("#timeslot-id").html(calEvent.id); + jQuery("#timeslot-start").html('Start: ' + moment(calEvent.start).format("DD.MM. YYYY HH:SS")); + jQuery("#timeslot-end").html('End: ' + moment(calEvent.end).format("DD.MM. YYYY HH:SS")); + jQuery("#show-name").html(calEvent.show_name); + jQuery("#show-id").html('(ID ' + calEvent.show_id + ')'); + jQuery("#show-hosts").html('Hosts: ' + calEvent.show_hosts); + jQuery("#show-type").html('Type: ' + calEvent.show_type); + jQuery("#show-categories").html('Categories: ' + calEvent.show_categories); + jQuery("#show-topics").html('Topics: ' + calEvent.show_topics); + jQuery("#show-musicfocus").html('Music focus: ' + calEvent.show_musicfocus); + jQuery("#is-repetition").html('WH: ' + calEvent.is_repetition); + jQuery("#playlist-id").html('Playlist ID: ' + calEvent.playlist_id); + jQuery("#fallback-playlist-id").html('Fallback ID: ' + calEvent.fallback_playlist_id); + jQuery("#memo").html(calEvent.memo); }, // How is this callback triggered? diff --git a/program/views.py b/program/views.py index 70471c00ab6595414f753b86da50767c38561b9b..efef269ed1e4e34f1c27d3b991721d748e99216c 100644 --- a/program/views.py +++ b/program/views.py @@ -2,15 +2,25 @@ import json from datetime import date, datetime, time, timedelta from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist from django.forms.models import model_to_dict -from django.http import HttpResponse, JsonResponse +from django.contrib.auth.models import User +from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 from django.views.generic.base import TemplateView from django.views.generic.detail import DetailView from django.views.generic.list import ListView +from rest_framework import permissions, serializers, status, viewsets +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.decorators import detail_route, list_route -from .models import Type, MusicFocus, Note, Show, Category, Topic, TimeSlot, Host +from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope + +from program.models import Type, MusicFocus, Note, Show, Category, RTRCategory, Topic, TimeSlot, Host +from profile.models import Profile +from program.serializers import ShowSerializer, TimeSlotSerializer, UserSerializer, NoteSerializer from program.utils import tofirstdayinisoweek, get_cached_shows @@ -229,9 +239,7 @@ def json_week_schedule(request): Returns all timeslots of the next 7 days """ - start = request.GET.get('start') - - if start == None: + if request.GET.get('start') == None: start = datetime.combine(date.today(), time(0, 0)) else: start = datetime.combine( datetime.strptime(request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) @@ -242,26 +250,42 @@ def json_week_schedule(request): is_repetition = ' ' + _('WH') if ts.schedule.is_repetition is 1 else '' - hosts = '' + hosts = ', '.join(ts.show.hosts.values_list('name', flat=True)) + categories = ', '.join(ts.show.category.values_list('category', flat=True)) + topics = ', '.join(ts.show.topic.values_list('topic', flat=True)) + musicfocus = ', '.join(ts.show.musicfocus.values_list('focus', flat=True)) + languages = ', '.join(ts.show.language.values_list('name', flat=True)) + rtrcategory = RTRCategory.objects.get(pk=ts.show.rtrcategory_id) + type = Type.objects.get(pk=ts.show.type_id) + + classname = 'default' - for host in ts.show.hosts.all(): - hosts = host.name + ', ' + hosts + if ts.playlist_id is None or ts.playlist_id == 0: + classname = 'danger' entry = { + 'id': ts.id, 'start': ts.start.strftime('%Y-%m-%dT%H:%M:%S'), 'end': ts.end.strftime('%Y-%m-%dT%H:%M:%S'), - 'title': ts.show.name + is_repetition, - 'id': ts.id, #show.id, + 'title': ts.show.name + is_repetition, # For JS Calendar 'automation-id': -1, 'schedule_id': ts.schedule.id, + 'is_repetition': ts.is_repetition, + 'playlist_id': ts.playlist_id, + 'schedule_fallback_id': ts.schedule.fallback_playlist_id, # The schedule's fallback + 'show_fallback_id': ts.show.fallback_pool, # The show's fallback 'show_id': ts.show.id, - 'show_name': ts.show.name, + 'show_name': ts.show.name + is_repetition, 'show_hosts': hosts, - 'is_repetition': ts.is_repetition, - 'fallback_playlist_id': ts.schedule.fallback_playlist_id, # the schedule's fallback playlist - 'show_fallback_pool': ts.show.fallback_pool, # the show's fallback - # TODO - #'station_fallback_pool': # the station's global fallback (might change suddenly) + 'show_type': type.type, + 'show_categories': categories, + 'show_topics': topics, + 'show_musicfocus': musicfocus, + 'show_languages': languages, + 'show_rtrcategory': rtrcategory.rtrcategory, + 'station_fallback_id': 0, # TODO: The station's global fallback (might change) + 'memo': ts.memo, + 'className': classname, } if ts.schedule.automation_id: @@ -299,40 +323,22 @@ def json_timeslots_specials(request): content_type="application/json; charset=utf-8") -def json_get_timeslot(request): - if not request.user.is_authenticated(): - return JsonResponse(_('Permission denied.')) - - if request.method == 'GET': - try: - timeslot = TimeSlot.objects.get(pk=int(request.GET.get('timeslot_id'))) - - returnvar = { 'id': timeslot.id, 'start': timeslot.start, 'end': timeslot.end, - 'schedule_id': timeslot.schedule.id, 'show_name': timeslot.show.name, - 'is_repetition': timeslot.schedule.is_repetition, - 'fallback_playlist_id': timeslot.schedule.fallback_playlist_id, - 'memo': timeslot.memo } - return JsonResponse( returnvar, safe=False ) - except ObjectDoesNotExist: - return JsonResponse( _('Error') ); - - def json_get_timeslots_by_show(request): ''' Returns a JSON object of timeslots of a given show from 4 weeks ago until 12 weeks in the future - Called by /export/get_timeslot_by_show/?show_id=1 to populate a timeslot-select for being assigned to a note + Called by /api/v1/timeslots/?show_id=1 to populate a timeslot-select for being assigned to a note ''' if not request.user.is_authenticated(): return JsonResponse(_('Permission denied.')) - if request.method == 'GET' and int(request.GET.get('show_id')): + if request.method == 'GET' and request.GET.get('show_id') != None: four_weeks_ago = datetime.now() - timedelta(weeks=4) in_twelve_weeks = datetime.now() + timedelta(weeks=12) timeslots = [] - saved_timeslot_id = int(request.GET.get('timeslot_id')) + saved_timeslot_id = 0 if request.GET.get('timeslot_id') == None else int(request.GET.get('timeslot_id')) # If the saved timeslot is part of the currently selected show, # include it as the first select-option in order not to lose it if it's past @@ -352,4 +358,307 @@ def json_get_timeslots_by_show(request): return JsonResponse( timeslots, safe=False ) else: - return JsonResponse( _('No show_id given.'), safe=False ) \ No newline at end of file + return JsonResponse( _('No show_id given.'), safe=False ) + + + + +#################################################################### +# REST API View Sets +#################################################################### + + +class APIUserViewSet(viewsets.ModelViewSet): + """ + /api/v1/users Returns oneself - Superusers see all users + /api/v1/users/1 Used for retrieving or updating a single user + + Superusers may access and update all users + """ + + permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] #, TokenHasReadWriteScope] + serializer_class = UserSerializer + queryset = User.objects.all() + required_scopes = ['users'] + + def list(self, request): + # Commons users only see themselves + if request.user.is_superuser: + users = User.objects.all() + else: + users = User.objects.filter(pk=request.user.id) + + serializer = UserSerializer(users, many=True) + return Response(serializer.data) + + + def retrieve(self, request, pk=None): + """Returns a single user""" + if pk != None: + pk = int(pk) + try: + user = User.objects.get(pk=pk) + except ObjectDoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + # Common users may only see themselves + if not request.user.is_superuser and user.id != request.user.id: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + serializer = UserSerializer(user) + return Response(serializer.data) + + return Response(status=status.HTTP_400_BAD_REQUEST) + + + def partial_update(self, request, pk=None): + + # Common users may only edit themselves + if not request.user.is_superuser and int(pk) != request.user.id: + return Response(serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED) + + serializer = UserSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save(); + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class APIShowViewSet(viewsets.ModelViewSet): + """ + /api/v1/shows/ Returns shows a user owns + /api/v1/shows/1 Used for retrieving a single show or update (if owned) + + Superusers may access and update all shows + """ + + queryset = Show.objects.all() + serializer_class = ShowSerializer + permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] #, TokenHasReadWriteScope] + required_scopes = ['shows'] + + + def list(self, request): + """Lists all shows""" + + # Commons users only see shows they own + if request.user.is_superuser: + shows = Show.objects.all() + else: + shows = request.user.shows.all() + + serializer = ShowSerializer(shows, many=True) + return Response(serializer.data) + + + def create(self, request): + """Create is not allowed at the moment""" + return Response(status=status.HTTP_401_UNAUTHORIZED) + + + def retrieve(self, request, pk=None): + """Returns a single show""" + + if pk != None and int(pk): + pk = int(pk) + + # Common users may only retrieve shows they own + if not request.user.is_superuser and pk not in list(request.user.shows.all().values_list('id', flat=True)): + return Response(status=status.HTTP_401_UNAUTHORIZED) + + try: + show = Show.objects.get(pk=pk) + except ObjectDoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = ShowSerializer(show) + return Response(serializer.data) + + return Response(status=status.HTTP_400_BAD_REQUEST) + + + def partial_update(self, request, pk=None): + serializer = ShowSerializer(data=request.data) + + # For common user and not owner of show: Permission denied + if not request.user.is_superuser and int(pk) not in list(request.user.shows.all().values_list('id', flat=True)): + return Response(serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED) + + if serializer.is_valid(): + serializer.save(); + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def destroy(self, request, pk=None): + '''Deleting is not allowed at the moment''' + return Response(status=status.HTTP_401_UNAUTHORIZED) + + +class APITimeSlotViewSet(viewsets.ModelViewSet): + """ + /api/v1/timeslots Returns nothing + /api/v1/timeslots/?show_id=1 Returns upcoming timeslots of a show 60 days in the future + /api/v1/timeslots/?show_id=1&start=2017-01-01&end=2017-02-01 Returns timeslots of a show within the given timerange + + TODO: Test for permissions to show + """ + + permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] #, TokenHasReadWriteScope] + serializer_class = TimeSlotSerializer + queryset = TimeSlot.objects.all() + required_scopes = ['timeslots'] + + + def list(self, request): + """Lists timeslots of a show""" + + if request.GET.get('show_id') != None: + show_id = int(request.GET.get('show_id')) + + if not request.user.is_superuser and show_id not in list(request.user.shows.all().values_list('id', flat=True)): + return Response(status=status.HTTP_401_UNAUTHORIZED) + + # Return next 60 days by default + start = datetime.combine(date.today(), time(0, 0)) + end = start + timedelta(days=60) + + if request.GET.get('start') and request.GET.get('end'): + start = datetime.combine( datetime.strptime(request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) + end = datetime.combine( datetime.strptime(request.GET.get('end'), '%Y-%m-%d').date(), time(23, 59)) + + timeslots = TimeSlot.objects.filter(show=show_id, start__gte=start, end__lte=end).order_by('start') + serializer = TimeSlotSerializer(timeslots, many=True) + + return Response(serializer.data) + + return Response(status=status.HTTP_400_BAD_REQUEST) + + + def create(self, request): + return Response(status=HTTP_401_UNAUTHORIZED) + + + def partial_update(self, request, pk=None): + """Link a playlist_id to a timeslot""" + + serializer = TimeSlotSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def destroy(self, request, pk=None): + """Deleting is not allowed at the moment""" + return Response(status=status.HTTP_401_UNAUTHORIZED) + + + +class APINoteViewSet(viewsets.ModelViewSet): + """ + /api/v1/notes/ Returns nothing + /ap1/v1/notes/1 Returns a single not (if owned) + /api/v1/notes/?ids=1,2,3,4,5 Returns given notes (if owned) + + Superusers may access and update all notes + """ + + queryset = Note.objects.all() + serializer_class = NoteSerializer + permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] #, TokenHasReadWriteScope] + required_scopes = ['notes'] + + + def list(self, request): + """Lists notes""" + + if request.GET.get('ids') != None: + note_ids = request.GET.get('ids').split(',') + if request.user.is_superuser: + notes = Note.objects.filter(id__in=note_ids) + else: + # Common users only retrieve notes they own + notes = Note.objects.filter(id__in=note_ids,user=request.user.id) + else: + notes = Note.objects.none() + + serializer = NoteSerializer(notes, many=True) + return Response(serializer.data) + + + def create(self, request): + """ + Creates a note + TODO: Test! + """ + + # Only create a note if show_id and timeslot_id is given + if not int(validated_data.get('show_id')) and not int(validated_data.get('timeslot_id')): + return Response(status=status.HTTP_400_BAD_REQUEST) + + serializer = NoteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + def retrieve(self, request, pk=None): + """Returns a single note""" + + if pk != None: + try: + note = Note.objects.get(pk=pk) + except ObjectDoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + if not request.user.is_superuser and note.user_id != request.user.id: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + serializer = NoteSerializer(note) + return Response(serializer.data) + + return Response(status=status.HTTP_400_BAD_REQUEST) + + + def partial_update(self, request, pk=None): + if pk != None: + pk = int(pk) + try: + note = Note.objects.get(pk=pk) + except ObjectDoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + if not request.user.is_superuser and note.user_id != request.user.id: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + serializer = NoteSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save(); + return Response(serializer.data) + + return Response(status=status.HTTP_400_BAD_REQUEST) + + + def destroy(self, request, pk=None): + if pk != None: + pk = int(pk) + try: + note = Note.objects.get(pk=pk) + except ObjectDoesNotExist: + return Response(status.HTTP_404_NOT_FOUND) + + if not request.user.is_superuser and note.user_id != request.user.id: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + Note.objects.delete(pk=pk) + return Response(status=status.HTTP_204_NO_CONTENT) + + return Response(status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/pv/settings.py b/pv/settings.py index 654c0caea608971ee26c9714bbab190450c412d0..ca142790d1a9423a931f81c5eec767cd7c09cfd9 100644 --- a/pv/settings.py +++ b/pv/settings.py @@ -77,6 +77,23 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = 'pv.urls' +OAUTH2_PROVIDER = { + # this is the list of available scopes + 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'shows': 'Access to shows', 'notes': 'Access to notes', 'timeslots': 'Access to timeslots'} +} + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + #'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ], + 'DEFAULT AUTHENTICATION_CLASSES': [ + 'oauth2_provider.ext.rest_framework.OAuth2Authentication', + ] +} + INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', @@ -90,15 +107,24 @@ INSTALLED_APPS = ( 'profile', 'tinymce', 'versatileimagefield', + 'rest_framework', + 'oauth2_provider', ) THUMBNAIL_SIZES = ['200x200', '150x150'] -TINYMCE_JS_URL = '/static/js/tiny_mce/tiny_mce.js' +#TINYMCE_JS_URL = '/static/js/tiny_mce/tiny_mce.js' TINYMCE_DEFAULT_CONFIG = { - 'plugins': 'contextmenu', + #'plugins': 'contextmenu', + 'selector': 'textarea', 'theme': 'advanced', 'theme_advanced_toolbar_location': 'top', + 'theme_advanced_buttons1' : 'bold,italic,underline,separator,bullist,numlist,separator,link,unlink,separator,undo,redo,separator,formatselect', + 'theme_advanced_blockformats': 'p,h1,h2,h3,blockquote', + 'theme_advanced_font_sizes': '14px,16px', + 'cleanup_on_startup': True, + 'width': 620, + 'height': 400, } CACHE_BACKEND = 'locmem://' diff --git a/pv/urls.py b/pv/urls.py index 69ca4947c67d2cc2c1829d1d40418d7794814176..ff20888bc11d29c3027d8e25c6d94bbfb7c31369 100644 --- a/pv/urls.py +++ b/pv/urls.py @@ -2,21 +2,35 @@ from django.conf import settings from django.conf.urls import url, include from django.contrib import admin from django.views.static import serve +from rest_framework import routers +from rest_framework.authtoken import views -from program.views import json_day_schedule, json_week_schedule, json_timeslots_specials, json_get_timeslot, json_get_timeslots_by_show +from program.views import APIUserViewSet, APIShowViewSet, APITimeSlotViewSet, APINoteViewSet, json_day_schedule, json_week_schedule, json_timeslots_specials, json_get_timeslots_by_show + +from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope admin.autodiscover() +router = routers.DefaultRouter() +router.register(r'users', APIUserViewSet) +router.register(r'shows', APIShowViewSet) +router.register(r'timeslots', APITimeSlotViewSet) +router.register(r'notes', APINoteViewSet) + urlpatterns = [ + url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^api-token-auth/', views.obtain_auth_token), + url(r'^api/v1/', include(router.urls) ), + url(r'^api/v1/timeslots-by-show$', json_get_timeslots_by_show, name='json_get_timeslots_by_show'), url(r'^admin/', admin.site.urls), url(r'^program/', include('program.urls')), url(r'^nop', include('nop.urls')), url(r'^tinymce/', include('tinymce.urls')), url(r'^export/day_schedule/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', json_day_schedule), - url(r'^export/week_schedule$', json_week_schedule), + url(r'^api/v1/program$', json_week_schedule), + url(r'^api/v1/week_schedule$', json_week_schedule), url(r'^export/timeslots_specials.json$', json_timeslots_specials), - url(r'^export/get_timeslot$', json_get_timeslot, name='get-timeslot'), - url(r'^export/get_timeslots_by_show$', json_get_timeslots_by_show, name='get-timeslots-by-show'), ] if settings.DEBUG: diff --git a/requirements.txt b/requirements.txt index 1a32e6057c7de8d2891e470f589a34ff2cff9d60..d1ce91ba7d65e4cb941abca43a2e01c6f756cac0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ Pillow==4.2.1 PyYAML==3.12 django-tinymce==2.6.0 python-dateutil==2.6.0 -django-versatileimagefield==1.8.1 \ No newline at end of file +django-versatileimagefield==1.8.1 +djangorestframework +django-oauth-toolkit \ No newline at end of file