diff --git a/program/admin.py b/program/admin.py index 9d4bff8fa9be27543053b0d8f8e00980f1b9f980..b4ff8fe933ad2cdaf890510cb38aadb256d5d8f5 100644 --- a/program/admin.py +++ b/program/admin.py @@ -87,7 +87,7 @@ class HostAdmin(admin.ModelAdmin): class NoteAdmin(admin.ModelAdmin): date_hierarchy = 'start' - list_display = ('title', 'show', 'start', 'status') + list_display = ('title', 'show', 'start', 'status', 'user') fields = (( 'show', 'timeslot'), 'title', 'slug', 'summary', 'content', 'image', 'status', 'cba_id') prepopulated_fields = {'slug': ('title',)} list_filter = ('status',) @@ -146,28 +146,8 @@ class NoteAdmin(admin.ModelAdmin): if not change: obj.user = request.user - obj.audio_url = '' - - # Retrieve the direct URL to the mp3 in CBA - # In order to retrieve the URL, stations need - # - to be whitelisted by CBA - # - an API Key - # - # Therefore contact cba@fro.at - from pv.settings import CBA_AJAX_URL, CBA_API_KEY - - if obj.cba_id != '' and CBA_API_KEY != '': - from urllib.request import urlopen - import json - - url = CBA_AJAX_URL + '?action=cba_ajax_get_filename&post_id=' + str(obj.cba_id) + '&api_key=' + CBA_API_KEY - - # For momentary testing without being whitelisted - TODO: delete the line - url = 'https://cba.fro.at/wp-content/plugins/cba/ajax/cba-get-filename.php?post_id=' + str(obj.cba_id) + '&c=Ml3fASkfwR8' - - with urlopen(url) as conn: - audio_url = conn.read().decode('utf-8-sig') - obj.audio_url = json.loads(audio_url) + # Try to get direct audio URL from CBA + obj.audio_url = Note.get_audio_url(obj.cba_id) obj.save() @@ -283,7 +263,6 @@ class ShowAdmin(admin.ModelAdmin): * or redirect to the original show-form if the resolving process has been finished (= if either max_steps was surpassed or end_reached was True) """ - self.end_reached = False schedule_instances = formset.save(commit=False) diff --git a/program/models.py b/program/models.py index e856f8cd594425fed83f6558df21ff1867ced70f..378a2aa175d11d1d57948cca808c5c23541a15f8 100644 --- a/program/models.py +++ b/program/models.py @@ -312,6 +312,9 @@ class Show(models.Model): verbose_name_plural = _("Shows") def __str__(self): + if self.id == None: + return '%s' % (self.name) + return '%04d | %s' % (self.id, self.name) def get_absolute_url(self): @@ -327,12 +330,10 @@ class Show(models.Model): @return boolean """ if self.request.user.is_superuser: - show_ids = Show.objects.all().values_list('id', flat=True) + return True else: show_ids = self.request.user.shows.all().values_list('id', flat=True) - - return int(show_id) in show_ids - + return int(show_id) in show_ids class RRule(models.Model): @@ -530,7 +531,6 @@ class Schedule(models.Model): # TODO: Test if auto_now_add and auto_now really always work #if not self.id or self.id == None: # self.created = datetime.today() - super(Schedule, self).save(*args, **kwargs) @@ -663,6 +663,36 @@ class Note(models.Model): def __str__(self): return '%s - %s' % (self.title, self.timeslot) + def get_audio_url(cba_id): + """ + Retrieve the direct URL to the mp3 in CBA + In order to retrieve the URL, stations need + - to be whitelisted by CBA + - an API Key + + Therefore contact cba@fro.at + """ + + from pv.settings import CBA_AJAX_URL, CBA_API_KEY + + audio_url = '' + + if cba_id != '' and CBA_API_KEY != '': + from urllib.request import urlopen + import json + + url = CBA_AJAX_URL + '?action=cba_ajax_get_filename&post_id=' + str(cba_id) + '&api_key=' + CBA_API_KEY + + # For momentary testing without being whitelisted - TODO: delete the line + url = 'https://cba.fro.at/wp-content/plugins/cba/ajax/cba-get-filename.php?post_id=' + str(cba_id) + '&c=Ml3fASkfwR8' + + with urlopen(url) as conn: + audio_url_json = conn.read().decode('utf-8-sig') + audio_url = json.loads(audio_url_json) + + return audio_url + + def save(self, *args, **kwargs): self.start = self.timeslot.start self.show = self.timeslot.schedule.show diff --git a/program/serializers.py b/program/serializers.py index 3b5ce0cb28c9a956d79782fc491718b961b5144b..4d0a5f2c55e58ed6d94094df91447669eab31e5d 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -1,11 +1,11 @@ from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from rest_framework import serializers, status from rest_framework.response import Response -from program.models import Show, Schedule, TimeSlot, Category, RTRCategory, Host, Language, Topic, MusicFocus, Note, Type, Language +from program.models import Show, Schedule, TimeSlot, Category, RTRCategory, Host, Language, Topic, MusicFocus, Note, Type, Language, RRule from profile.models import Profile from profile.serializers import ProfileSerializer - +from datetime import datetime class UserSerializer(serializers.ModelSerializer): # Add profile fields to JSON @@ -22,6 +22,7 @@ class UserSerializer(serializers.ModelSerializer): """ profile_data = validated_data.pop('profile') + user = super(UserSerializer, self).create(validated_data) user.date_joined = datetime.today() user.set_password(validated_data['password']) @@ -38,10 +39,19 @@ class UserSerializer(serializers.ModelSerializer): Update and return an existing User instance, given the validated data. """ + user = self.context['user'] + 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) + if user.is_superuser: + instance.groups = validated_data.get('groups', instance.groups) + instance.user_permissions = validated_data.get('user_permissions', instance.user_permissions) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.is_staff = validated_data.get('is_staff', instance.is_staff) + instance.is_superuser = validated_data.get('is_superuser', instance.is_superuser) + # TODO: How to hook into this from ProfileSerializer without having to call it here? try: profile = Profile.objects.get(user=instance.id) @@ -206,25 +216,46 @@ class OwnersSerializer(serializers.ModelSerializer): class ShowSerializer(serializers.HyperlinkedModelSerializer): - category = CategorySerializer(many=True) - hosts = HostSerializer(many=True) - language = LanguageSerializer(many=True) - topic = TopicSerializer(many=True) - musicfocus = MusicFocusSerializer(many=True) + owners = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(),many=True) + category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(),many=True) + hosts = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all(),many=True) + language = serializers.PrimaryKeyRelatedField(queryset=Language.objects.all(),many=True) + topic = serializers.PrimaryKeyRelatedField(queryset=Topic.objects.all(),many=True) + musicfocus = serializers.PrimaryKeyRelatedField(queryset=MusicFocus.objects.all(),many=True) + type = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all()) + rtrcategory = serializers.PrimaryKeyRelatedField(queryset=RTRCategory.objects.all()) class Meta: model = Show fields = ('id', 'name', 'slug', 'image', 'logo', 'short_description', 'description', - 'email', 'website', 'created', 'last_updated', 'type_id', 'rtrcategory_id', + 'email', 'website', 'created', 'last_updated', 'type', 'rtrcategory', 'predecessor_id', 'cba_series_id', 'fallback_pool', 'category', 'hosts', - 'language', 'topic', 'musicfocus') + 'owners', '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) + owners = validated_data.pop('owners') + category = validated_data.pop('category') + hosts = validated_data.pop('hosts') + language = validated_data.pop('language') + topic = validated_data.pop('topic') + musicfocus = validated_data.pop('musicfocus') + + show = Show.objects.create(**validated_data) + + # Save many-to-many relationships + show.owners = owners + show.category = category + show.hosts = hosts + show.language = language + show.topic = topic + show.musicfocus = musicfocus + + show.save() + return show def update(self, instance, validated_data): @@ -232,6 +263,8 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): Update and return an existing Show instance, given the validated data. """ + user = self.context['user'] + instance.name = validated_data.get('name', instance.name) instance.slug = validated_data.get('slug', instance.slug) instance.image = validated_data.get('image', instance.image) @@ -240,50 +273,83 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): 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.predecessor_id = validated_data.get('predecessor_id', instance.predecessor_id) 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) + # Only superusers may update the following fields + if user.is_superuser: + instance.owners = validated_data.get('owners', instance.owners) + instance.category = validated_data.get('category', instance.category) + instance.hosts = validated_data.get('hosts', instance.hosts) + instance.language = validated_data.get('language', instance.language) + instance.topic = validated_data.get('topic', instance.topic) + instance.musicfocus = validated_data.get('musicfocus', instance.musicfocus) + instance.type = validated_data.get('type', instance.type) + instance.rtrcategory = validated_data.get('rtrcategory', instance.rtrcategory) + instance.save() return instance # TODO: collision detection class ScheduleSerializer(serializers.ModelSerializer): + rrule = serializers.PrimaryKeyRelatedField(queryset=RRule.objects.all()) + show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) + class Meta: model = Schedule fields = '__all__' def create(self, validated_data): - """ - Create and return a new Schedule instance, given the validated data. - """ - return Schedule.objects.create(**validated_data) + """Create and return a new Schedule instance, given the validated data.""" + + rrule = validated_data.pop('rrule') + show = validated_data.pop('show') + + schedule = Schedule.objects.create(**validated_data) + schedule.rrule = rrule + schedule.show = show + + schedule.save() + return schedule def update(self, instance, validated_data): - """ - Update and return an existing Schedule instance, given the validated data. - """ + """Update and return an existing Schedule instance, given the validated data.""" + + instance.byweekday = validated_data.get('byweekday', instance.byweekday) + instance.dstart = validated_data.get('dstart', instance.dstart) + instance.tstart = validated_data.get('tstart', instance.tstart) + instance.tend = validated_data.get('tend', instance.tend) + instance.until = validated_data.get('until', instance.until) + instance.is_repetition = validated_data.get('is_repetition', instance.is_repetition) + instance.fallback_playlist_id = validated_data.get('fallback_playlist_id', instance.fallback_playlist_id) + instance.automation_id = validated_data.get('automation_id', instance.automation_id) + instance.rrule = validated_data.get('rrule', instance.rrule) + instance.show = validated_data.get('show', instance.show) + + instance.save() return instance class TimeSlotSerializer(serializers.ModelSerializer): + show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) + schedule = serializers.PrimaryKeyRelatedField(queryset=Schedule.objects.all()) + class Meta: model = TimeSlot fields = '__all__' def create(self, validated_data): - """ - Create and return a new TimeSlot instance, given the 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. - """ + """Update and return an existing Show instance, given the validated data.""" + # Only save certain fields 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) @@ -292,25 +358,30 @@ class TimeSlotSerializer(serializers.ModelSerializer): class NoteSerializer(serializers.ModelSerializer): + show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) + timeslot = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all()) + class Meta: model = Note fields = '__all__' def create(self, validated_data): - """ - Create and return a new Note instance, given the validated data. - """ + """Create and return a new Note instance, given the validated data.""" + + # Save the creator + validated_data['user_id'] = self.context['user_id'] + + # Try to retrieve audio URL from CBA + validated_data['audio_url'] = Note.get_audio_url(validated_data['cba_id']) return Note.objects.create(**validated_data) def update(self, instance, validated_data): - """ - Update and return an existing Note instance, given the 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.show = validated_data.get('show', instance.show) + instance.timeslot = validated_data.get('timeslot', instance.timeslot) instance.title = validated_data.get('title', instance.title) instance.slug = validated_data.get('slug', instance.slug) instance.summary = validated_data.get('summary', instance.summary) @@ -318,6 +389,7 @@ class NoteSerializer(serializers.ModelSerializer): 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.audio_url = Note.get_audio_url(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 4f8d7f4f153c4a314bf68a456f603cfee332b6d0..da3bf26eff92cefe361a62bb28adb4570cb1b87e 100644 --- a/program/templates/calendar.html +++ b/program/templates/calendar.html @@ -147,7 +147,7 @@ }, weekNumberCalculation: 'ISO', // Week begins with Monday firstDay: 1, // Week begins with Monday - events: '/api/v1/week_schedule', + events: '/api/v1/program/week', eventRender: function(event, element) { element.find('.fc-content').append( '<span class="closeon">X</span>' ); element.find('.closeon').click(function() { diff --git a/program/views.py b/program/views.py index 001ee6618f1d445da79bb7fa082328d198b19c6e..2adb044950be3034a7c5760d910685df36ad0832 100644 --- a/program/views.py +++ b/program/views.py @@ -4,7 +4,6 @@ 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.contrib.auth.models import User from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 @@ -14,7 +13,6 @@ 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 from program.models import Type, MusicFocus, Language, Note, Show, Category, RTRCategory, Topic, TimeSlot, Host, Schedule from program.serializers import TypeSerializer, LanguageSerializer, MusicFocusSerializer, NoteSerializer, ShowSerializer, ScheduleSerializer, CategorySerializer, RTRCategorySerializer, TopicSerializer, TimeSlotSerializer, HostSerializer, UserSerializer @@ -329,13 +327,13 @@ def json_timeslots_specials(request): 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 + /api/v1/users Returns oneself - Superusers see all users (GET, POST) + /api/v1/users/1 Used for retrieving or updating a single user (GET, PUT, DELETE) Superusers may access and update all users """ - permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = UserSerializer queryset = User.objects.none() required_scopes = ['users'] @@ -357,23 +355,42 @@ class APIUserViewSet(viewsets.ModelViewSet): def retrieve(self, request, pk=None): """Returns a single user""" - user = get_object_or_404(User, pk=pk) # Common users may only see themselves - if not request.user.is_superuser and user.id != request.user.id: + if not request.user.is_superuser and int(pk) != request.user.id: return Response(status=status.HTTP_401_UNAUTHORIZED) + user = get_object_or_404(User, pk=pk) serializer = UserSerializer(user) return Response(serializer.data) - def partial_update(self, request, pk=None): + def create(self, request, pk=None): + """ + Create a User + Only superusers may create a user + """ + + if not request.user.is_superuser: + return Response(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) + + + def 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) + user = get_object_or_404(User, pk=pk) + serializer = UserSerializer(user, data=request.data, context={ 'user': request.user }) if serializer.is_valid(): serializer.save(); @@ -383,27 +400,25 @@ class APIUserViewSet(viewsets.ModelViewSet): - class APIShowViewSet(viewsets.ModelViewSet): """ - /api/v1/shows/ Returns shows a user owns - /api/v1/shows/?active=true Returns all active shows (no matter if owned by user) - /api/v1/shows/1 Used for retrieving a single show or update (if owned) - /api/v1/shows/1/schedules Returns all schedules for the given show - /api/v1/shows/1/timeslots Returns all timeslots for the given show - /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-12-31 Returns all timeslots for the given show - - Superusers may access and update all shows + /api/v1/shows/ Returns shows a user owns (GET, POST) + /api/v1/shows/?active=true Returns all active shows (no matter if owned by user) (GET) + /api/v1/shows/1 Used for retrieving a single show or update (if owned) (GET, PUT, DELETE) + /api/v1/shows/1/schedules Returns all schedules of the show (GET, POST) + /api/v1/shows/1/timeslots Returns all timeslots of the show (GET) - Timeslots may only be added by creating/updating a schedule + /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-12-31 Returns all timeslots of the show within the given timerange (GET) + + Only superusers may add and delete shows """ queryset = Show.objects.none() serializer_class = ShowSerializer - permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['shows'] def get_queryset(self): - """Constrain access to owners except for superusers""" if self.request.GET.get('active') == 'true': '''Filter for retrieving currently running shows''' @@ -416,49 +431,32 @@ class APIShowViewSet(viewsets.ModelViewSet): return Show.objects.filter(id__in=schedules) - if self.request.user.is_superuser: - return Show.objects.all() - else: - return self.request.user.shows.all() + return Show.objects.all() - @detail_route(methods=['get'], url_path='schedules') - def schedules(self, request, pk=None): - """Return all schedules of the show""" - show = get_object_or_404(Show, pk=pk) - schedules = Schedule.objects.filter(show=show) - serializer = ScheduleSerializer(schedules, many=True) + def list(self, request): + """List shows""" + shows = self.get_queryset() + serializer = ShowSerializer(shows, many=True) return Response(serializer.data) - @detail_route(methods=['get'], url_path='timeslots') - def timeslots(self, request, pk=None): - """Return timeslots of the show for the next 60 days or the given timerange""" - show = get_object_or_404(Show, pk=pk) - - # Return next 60 days by default - start = datetime.combine(date.today(), time(0, 0)) - end = start + timedelta(days=60) - - if self.request.GET.get('start') and self.request.GET.get('end'): - start = datetime.combine( datetime.strptime(self.request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) - end = datetime.combine( datetime.strptime(self.request.GET.get('end'), '%Y-%m-%d').date(), time(23, 59)) - - timeslots = TimeSlot.objects.filter(show=show, start__gte=start, end__lte=end).order_by('start') - serializer = TimeSlotSerializer(timeslots, many=True) - return Response(serializer.data) + def create(self, request, pk=None): + """ + Create a show + Only superusers may create a show + """ + if not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) - def list(self, request): - """Lists shows""" - shows = self.get_queryset() - serializer = ShowSerializer(shows, many=True) - return Response(serializer.data) + serializer = ShowSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) - def create(self, request): - """Create is not allowed at the moment""" - return Response(status=status.HTTP_401_UNAUTHORIZED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, pk=None): @@ -468,12 +466,17 @@ class APIShowViewSet(viewsets.ModelViewSet): return Response(serializer.data) - def partial_update(self, request, pk=None): - serializer = ShowSerializer(data=request.data) + def update(self, request, pk=None): + """ + Update a show + Common users may only update shows they own + """ - # For common user and not owner of show: Permission denied if not Show.is_editable(self, pk): - return Response(serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED) + return Response(status=status.HTTP_401_UNAUTHORIZED) + + show = get_object_or_404(Show, pk=pk) + serializer = ShowSerializer(show, data=request.data, context={ 'user': request.user }) if serializer.is_valid(): serializer.save(); @@ -482,21 +485,137 @@ class APIShowViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def destroy(self, request, pk=None): + """ + Delete a show + Only superusers may delete shows + """ + + if not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + show = get_object_or_404(Show, pk=pk) + Show.objects.delete(pk=pk) + + return Response(status=status.HTTP_204_NO_CONTENT) + + + +class APIScheduleViewSet(viewsets.ModelViewSet): + """ + /api/v1/schedules/ Returns schedules (GET) - POST not allowed at this level + /api/v1/schedules/1 Returns the given schedule (GET) - POST not allowed at this level + /api/v1/shows/1/schedules Returns schedules of the show (GET, POST) + /api/v1/shows/1/schedules/1 Returns schedules by its ID (GET, PUT, DELETE) + + Only superusers may create and update schedules + """ + + queryset = Schedule.objects.none() + serializer_class = ScheduleSerializer + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] + required_scopes = ['schedules'] + + + def get_queryset(self): + show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None + + if show_pk != None: + return Schedule.objects.filter(show=show_pk) + + return Schedule.objects.all() + + + def list(self, request, show_pk=None, pk=None): + """List Schedules of a show""" + schedules = self.get_queryset() + serializer = ScheduleSerializer(schedules, many=True) + return Response(serializer.data) + + + def retrieve(self, request, pk=None, show_pk=None): + + if show_pk != None: + schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) + else: + schedule = get_object_or_404(Schedule, pk=pk) + + serializer = ScheduleSerializer(schedule) + return Response(serializer.data) + + + def create(self, request, show_pk=None, pk=None): + """ + TODO: Create a schedule, generate timeslots, test for collisions and resolve them including notes + Only superusers may add schedules + """ + + # Only allow creating when calling /shows/1/schedules/1 + if show_pk == None or not request.user.is_superuser: + return Response(status=HTTP_401_UNAUTHORIZED) + + return Response(status=HTTP_401_UNAUTHORIZED) + + + def update(self, request, pk=None, show_pk=None): + """ + TODO: Update a schedule, generate timeslots, test for collisions and resolve them including notes + Only superusers may update schedules + """ + + # Only allow updating when calling /shows/1/schedules/1 + if show_pk == None or not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) + + serializer = ScheduleSerializer(schedule, 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): + """ + Delete a schedule + Only superusers may delete schedules + """ + + if not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + schedule = get_object_or_404(Schedule, pk=pk) + Schedule.objects.delete(pk=pk) + + return Response(status=status.HTTP_204_NO_CONTENT) + + + class APITimeSlotViewSet(viewsets.ModelViewSet): """ - /api/v1/timeslots Returns timeslots of the next 60 days - /api/v1/timeslots/?start=2017-01-01&end=2017-02-01 Returns timeslots within the given timerange - /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 + /api/v1/timeslots Returns timeslots of the next 60 days (GET) - Timeslots may only be added by creating/updating a schedule + /api/v1/timeslots/1 Returns the given timeslot (GET) - PUT/DELETE not allowed at this level + /api/v1/timeslots/?start=2017-01-01&end=2017-02-01 Returns timeslots within the given timerange (GET) + /api/v1/shows/1/timeslots Returns timeslots of the show (GET, POST) + /api/v1/shows/1/timeslots/1 Returns a timeslots by its ID (GET, PUT, DELETE) + /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-02-01 Returns timeslots of the show within the given timerange + /api/v1/shows/1/schedules/1/timeslots Returns all timeslots of the schedule (GET, POST) + /api/v1/shows/1/schedules/1/timeslots/1 Returns a timeslot by its ID (GET, PUT, DELETE) + /api/v1/shows/1/schedules/1/timeslots?start=2017-01-01&end=2017-02-01 Returns all timeslots of the schedule within the given timerange """ - permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = TimeSlotSerializer queryset = TimeSlot.objects.none() required_scopes = ['timeslots'] def get_queryset(self): + show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None + schedule_pk = self.kwargs['schedule_pk'] if 'schedule_pk' in self.kwargs else None + # Return next 60 days by default start = datetime.combine(date.today(), time(0, 0)) end = start + timedelta(days=60) @@ -506,34 +625,50 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): end = datetime.combine( datetime.strptime(self.request.GET.get('end'), '%Y-%m-%d').date(), time(23, 59)) # If show is given: Return corresponding timeslots - if self.request.GET.get('show_id') != None: - show_id = int(self.request.GET.get('show_id')) - - if not Show.is_editable(self, show_id): - return Response(status=status.HTTP_401_UNAUTHORIZED) - - return TimeSlot.objects.filter(show=show_id, start__gte=start, end__lte=end).order_by('start') - - # Otherwise return all show timeslots - return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by('start') + if show_pk != None and schedule_pk != None: + # /shows/1/schedules/1/timeslots/ returns timeslots of the given schedule and show + return TimeSlot.objects.filter(show=show_pk, schedule=schedule_pk, start__gte=start, end__lte=end).order_by('start') + elif show_pk != None and schedule_pk == None: + # /shows/1/timeslots returns timeslots of the show + return TimeSlot.objects.filter(show=show_pk, start__gte=start, end__lte=end).order_by('start') + else: + # Otherwise return all timeslots + return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by('start') - def list(self, request): + def list(self, request, show_pk=None, schedule_pk=None): """Lists timeslots of a show""" timeslots = self.get_queryset() serializer = TimeSlotSerializer(timeslots, many=True) return Response(serializer.data) + def retrieve(self, request, pk=None, schedule_pk=None, show_pk=None): + + if show_pk != None: + timeslot = get_object_or_404(TimeSlot, pk=pk, show=show_pk) + else: + timeslot = get_object_or_404(TimeSlot, pk=pk) + + serializer = TimeSlotSerializer(timeslot) + return Response(serializer.data) + + def create(self, request): """Timeslots may only be created by adding/updating schedules""" return Response(status=HTTP_401_UNAUTHORIZED) - def partial_update(self, request, pk=None): + def update(self, request, pk=None, schedule_pk=None, show_pk=None): """Link a playlist_id to a timeslot""" - serializer = TimeSlotSerializer(data=request.data) + timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk) + + # Update is only allowed when calling /shows/1/schedules/1/timeslots/1 and if user owns the show + if schedule_pk == None or show_pk==None or not Show.is_editable(self, timeslot.show_id): + return Response(status=status.HTTP_401_UNAUTHORIZED) + + serializer = TimeSlotSerializer(timeslot, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) @@ -541,24 +676,49 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def destroy(self, request, pk=None): + """ + Delete a timeslot + Only superusers may delete timeslots + """ + + if not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + timeslot = get_object_or_404(TimeSlot, pk=pk) + TimeSlot.objects.delete(pk=pk) + + return Response(status=status.HTTP_204_NO_CONTENT) + + class APINoteViewSet(viewsets.ModelViewSet): """ - /api/v1/notes/ Returns all notes the user owns - /ap1/v1/notes/1 Returns a single note (if owned) - /api/v1/notes/?ids=1,2,3,4,5 Returns given notes (if owned) + /api/v1/notes/ Returns all notes (GET) - POST not allowed at this level + /ap1/v1/notes/1 Returns a single note (if owned) (GET) - PUT/DELETE not allowed at this level + /api/v1/notes/?ids=1,2,3,4,5 Returns given notes (if owned) (GET) + /api/v1/shows/1/notes Returns all notes of a show (GET) - POST not allowed at this level + /api/v1/shows/1/notes/1 Returns a note by its ID (GET) - PUT/DELETE not allowed at this level + /api/v1/shows/1/timeslots/1/notes/ Returns a note of the timeslot (GET) - POST not allowed at this level + /api/v1/shows/1/timeslots/1/notes/1 Returns a note by its ID (GET) - PUT/DELETE not allowed at this level + /api/v1/shows/1/schedules/1/timeslots/1/notes Returns a note to the timeslot (GET, POST) - Only one note allowed per timeslot + /api/v1/shows/1/schedules/1/timeslots/1/notes/1 Returns a note by its ID (GET, PUT, DELETE) Superusers may access and update all notes """ queryset = Note.objects.none() serializer_class = NoteSerializer - permission_classes = [permissions.IsAuthenticated, permissions.DjangoModelPermissions] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['notes'] def get_queryset(self): + pk = self.kwargs['pk'] if 'pk' in self.kwargs else None + timeslot_pk = self.kwargs['timeslot_pk'] if 'timeslot_pk' in self.kwargs else None + show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None + # TODO: Should users be able to edit other's notes if they're part of a show they own? if self.request.GET.get('ids') != None: note_ids = self.request.GET.get('ids').split(',') @@ -568,31 +728,42 @@ class APINoteViewSet(viewsets.ModelViewSet): # Common users only retrieve notes they own notes = Note.objects.filter(id__in=note_ids,user=self.request.user.id) else: - notes = Note.objects.filter(user=self.request.user.id) + + if show_pk != None and timeslot_pk != None: + # /shows/1/schedules/1/timeslots/1/notes + # /shows/1/timeslots/1/notes/ + # ...return notes to the timeslot + notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) + elif show_pk != None and timeslot_pk == None: + # /shows/1/notes returns notes to the show + notes = Note.objects.filter(show=show_pk) + else: + # /notes returns all notes + notes = Note.objects.all() + #notes = Note.objects.filter(user=self.request.user.id) return notes - def list(self, request): + def list(self, request, pk=None, timeslot_pk=None, schedule_pk=None, show_pk=None): """Lists notes""" notes = self.get_queryset() serializer = NoteSerializer(notes, many=True) return Response(serializer.data) - def create(self, request): - """ - Creates a note - """ + def create(self, request, pk=None, timeslot_pk=None, schedule_pk=None, show_pk=None): + """Create a note""" - # Only create a note if show_id and timeslot_id is given - if request.POST.get('show') == None or request.POST.get('timeslot') == None: + # Only create a note if show_id, timeslot_id and schedule_id is given + if show_pk == None or schedule_pk == None or timeslot_pk == None: return Response(status=status.HTTP_400_BAD_REQUEST) - if not Show.is_editable(self, request.POST.get('show')): + if not Show.is_editable(self, show_pk): return Response(status=status.HTTP_401_UNAUTHORIZED) - serializer = NoteSerializer(data=request.data) + serializer = NoteSerializer(data=request.data, context={ 'user_id': request.user.id }) + if serializer.is_valid(): serializer.save() return Response(serializer.data) @@ -600,25 +771,45 @@ class APINoteViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def retrieve(self, request, pk=None): - """Returns a single note""" + def retrieve(self, request, pk=None, timeslot_pk=None, schedule_pk=None, show_pk=None): + """ + Returns a single note - note = get_object_or_404(Note, pk=pk) + Called by: + /notes/1 + /shows/1/notes/1 + /shows/1/timeslots/1/notes/1 + /shows/1/schedules/1/timeslots/1/notes/1 + """ - if not request.user.is_superuser and int(pk) not in self.get_queryset().values_list('id', flat=True): - return Response(status=status.HTTP_401_UNAUTHORIZED) + if show_pk != None and timeslot_pk == None and schedule_pk == None: + # /shows/1/notes/1 + note = get_object_or_404(Note, pk=pk, show=show_pk) + elif show_pk != None and timeslot_pk != None: + # /shows/1/timeslots/1/notes/1 + # /shows/1/schedules/1/timeslots/1/notes/1 + note = get_object_or_404(Note, pk=pk, show=show_pk, timeslot=timeslot_pk) + else: + # /notes/1 + note = get_object_or_404(Note, pk=pk) serializer = NoteSerializer(note) return Response(serializer.data) - def partial_update(self, request, pk=None): - note = get_object_or_404(Note, pk=pk) + def update(self, request, pk=None, show_pk=None, schedule_pk=None, timeslot_pk=None): + + # Allow PUT only when calling /shows/1/schedules/1/timeslots/1/notes/1 + if show_pk == None or schedule_pk == None or timeslot_pk == None: + return Response(status=status.HTTP_401_UNAUTHORIZED) + note = get_object_or_404(Note, pk=pk, timeslot=timeslot_pk, show=show_pk) + + # Commons users may only edit their own notes 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) + serializer = NoteSerializer(note, data=request.data) if serializer.is_valid(): serializer.save(); @@ -630,6 +821,7 @@ class APINoteViewSet(viewsets.ModelViewSet): def destroy(self, request, pk=None): note = get_object_or_404(Note, pk=pk) + # Commons users may only delete their own notes if not request.user.is_superuser and note.user_id != request.user.id: return Response(status=status.HTTP_401_UNAUTHORIZED) @@ -637,77 +829,92 @@ class APINoteViewSet(viewsets.ModelViewSet): return Response(status=status.HTTP_204_NO_CONTENT) + class APICategoryViewSet(viewsets.ModelViewSet): """ + /api/v1/categories/ Returns all categories (GET, POST) + /api/v1/categories/1 Returns a category by its ID (GET, PUT, DELETE) """ queryset = Category.objects.all() serializer_class = CategorySerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['categories'] class APITypeViewSet(viewsets.ModelViewSet): """ + /api/v1/types/ Returns all types (GET, POST) + /api/v1/types/1 Returns a type by its ID (GET, PUT, DELETE) """ queryset = Type.objects.all() serializer_class = TypeSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['types'] class APITopicViewSet(viewsets.ModelViewSet): """ + /api/v1/topics/ Returns all topics (GET, POST) + /api/v1/topics/1 Returns a topic by its ID (GET, PUT, DELETE) """ queryset = Topic.objects.all() serializer_class = TopicSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['topics'] class APIMusicFocusViewSet(viewsets.ModelViewSet): """ + /api/v1/musicfocus/ Returns all musicfocuses (GET, POST) + /api/v1/musicfocus/1 Returns a musicfocus by its ID (GET, PUT, DELETE) """ queryset = MusicFocus.objects.all() serializer_class = MusicFocusSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['musicfocus'] class APIRTRCategoryViewSet(viewsets.ModelViewSet): """ + /api/v1/rtrcategories/ Returns all rtrcategories (GET, POST) + /api/v1/rtrcategories/1 Returns a rtrcategory by its ID (GET, PUT, DELETE) """ queryset = RTRCategory.objects.all() serializer_class = RTRCategorySerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['rtrcategories'] class APILanguageViewSet(viewsets.ModelViewSet): """ + /api/v1/languages/ Returns all languages (GET, POST) + /api/v1/languages/1 Returns a language by its ID (GET, PUT, DELETE) """ queryset = Language.objects.all() serializer_class = LanguageSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['languages'] class APIHostViewSet(viewsets.ModelViewSet): """ + /api/v1/hosts/ Returns all hosts (GET, POST) + /api/v1/hosts/1 Returns a host by its ID (GET, PUT, DELETE) """ queryset = Host.objects.all() serializer_class = HostSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['hosts'] @@ -718,6 +925,6 @@ class APIOwnersViewSet(viewsets.ModelViewSet): queryset = Owners.objects.all() serializer_class = OwnersSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] required_scopes = ['owners'] ''' \ No newline at end of file diff --git a/pv/settings.py b/pv/settings.py index 74d3a2c8874d24c306efcb19419b90a43f49571b..e9edf97ed45f8c453bc2b02426015d7a21895f5f 100644 --- a/pv/settings.py +++ b/pv/settings.py @@ -81,11 +81,13 @@ 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' + #'rest_framework.permissions.IsAuthenticatedOrReadOnly', + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ], 'DEFAULT AUTHENTICATION_CLASSES': [ - ] + ], + #'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + #'PAGE_SIZE': 100 } INSTALLED_APPS = ( @@ -102,6 +104,7 @@ INSTALLED_APPS = ( 'tinymce', 'versatileimagefield', 'rest_framework', + 'rest_framework_nested', 'frapp', ) diff --git a/pv/urls.py b/pv/urls.py index ea38cf199dfa914787a248638d328f1e520d6be0..8d89b1ec4a2c3609553c294e6ca709c6c26799a9 100644 --- a/pv/urls.py +++ b/pv/urls.py @@ -2,10 +2,10 @@ 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_nested import routers from rest_framework.authtoken import views -from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIRTRCategoryViewSet, APILanguageViewSet, json_day_schedule, json_week_schedule, json_timeslots_specials +from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIRTRCategoryViewSet, APILanguageViewSet, json_day_schedule, json_week_schedule, json_timeslots_specials admin.autodiscover() @@ -14,6 +14,7 @@ router.register(r'users', APIUserViewSet) #router.register(r'owners', APIOwnerViewSet) router.register(r'hosts', APIHostViewSet) router.register(r'shows', APIShowViewSet) +router.register(r'schedules', APIScheduleViewSet) router.register(r'timeslots', APITimeSlotViewSet) router.register(r'notes', APINoteViewSet) router.register(r'categories', APICategoryViewSet) @@ -23,14 +24,48 @@ router.register(r'musicfocus', APIMusicFocusViewSet) router.register(r'rtrcategories', APIRTRCategoryViewSet) router.register(r'languages', APILanguageViewSet) + +'''Nested Routers''' + +show_router = routers.NestedSimpleRouter(router, r'shows', lookup='show') + +# /shows/1/schedules +show_router.register(r'schedules', APIScheduleViewSet, base_name='show-schedules') + +# /shows/1/notes +show_router.register(r'notes', APINoteViewSet, base_name='show-notes') + +# /shows/1/timeslots +show_router.register(r'timeslots', APITimeSlotViewSet, base_name='show-timeslots') +show_timeslot_router = routers.NestedSimpleRouter(show_router, r'timeslots', lookup='timeslot') + +# /shows/1/timeslots/1/notes/ +show_timeslot_router.register(r'notes', APINoteViewSet, base_name='show-timeslots-note') + +# /shows/1/schedules +schedule_router = routers.NestedSimpleRouter(show_router, r'schedules', lookup='schedule') + +# /shows/1/schedules/1/timeslots +schedule_router.register(r'timeslots', APITimeSlotViewSet, base_name='schedule-timeslots') +timeslot_router = routers.NestedSimpleRouter(schedule_router, r'timeslots', lookup='timeslot') + +# /shows/1/schedules/1/timeslots/1/notes +timeslot_router.register(r'notes', APINoteViewSet, base_name='timeslots-notes') + + urlpatterns = [ url(r'^api/v1/', include(router.urls) ), + url(r'^api/v1/', include(show_router.urls)), + url(r'^api/v1/', include(show_timeslot_router.urls)), + url(r'^api/v1/', include(schedule_router.urls)), + url(r'^api/v1/', include(timeslot_router.urls)), + url(r'^api/v1/playout', json_week_schedule), url(r'^api/v1/program/week', json_week_schedule), url(r'^api/v1/program/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', json_day_schedule), url(r'^admin/', admin.site.urls), url(r'^program/', include('program.urls')), url(r'^nop', include('nop.urls')), - url(r'^', include('frapp.urls')), + url(r'^api/', include('frapp.urls')), #url(r'^tinymce/', include('tinymce.urls')), url(r'^export/timeslots_specials.json$', json_timeslots_specials), ] diff --git a/requirements.txt b/requirements.txt index d1ce91ba7d65e4cb941abca43a2e01c6f756cac0..aa962397cfdb466b2608f6983d33b9a6e331c333 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ django-tinymce==2.6.0 python-dateutil==2.6.0 django-versatileimagefield==1.8.1 djangorestframework +drf-nested-routers==0.90.0 django-oauth-toolkit \ No newline at end of file