From 37dca7d57019ca016d6367a1560924b36a1258a2 Mon Sep 17 00:00:00 2001
From: ingo <ingo.leindecker@fro.at>
Date: Mon, 26 Feb 2018 16:17:11 +0100
Subject: [PATCH] Made OIDC Authentication work with REST API

* added corsheaders middleware to work properly
* fixed bug when updating a timeslot
* after updating a timeslot, its repetition will be returned if there's one
* included field 'ppoi' in shows

See #22 #23
---
 program/auth.py                               | 26 +++++++++++++++++++
 program/migrations/0016_auto_20180222_1253.py | 21 +++++++++++++++
 program/models.py                             |  4 +--
 program/serializers.py                        |  5 +++-
 program/views.py                              | 21 +++++++++++----
 pv/settings.py                                | 23 ++++++++--------
 pv/urls.py                                    |  2 --
 7 files changed, 80 insertions(+), 22 deletions(-)
 create mode 100644 program/auth.py
 create mode 100644 program/migrations/0016_auto_20180222_1253.py

diff --git a/program/auth.py b/program/auth.py
new file mode 100644
index 00000000..958b9a74
--- /dev/null
+++ b/program/auth.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+from rest_framework import authentication
+from rest_framework import exceptions
+
+from oidc_provider.models import Token
+from oidc_provider.lib.utils.oauth2 import extract_access_token
+
+
+class OidcOauth2Auth(authentication.BaseAuthentication):
+    def authenticate(self, request):
+        access_token = extract_access_token(request)
+
+        if not access_token:
+            # not this kind of auth
+            return None
+        oauth2_token = None
+        try:
+            oauth2_token = Token.objects.get(access_token=access_token)
+        except Token.DoesNotExist:
+            raise exceptions.AuthenticationFailed("The oauth2 token is invalid")
+
+        if oauth2_token.has_expired():
+            raise exceptions.AuthenticationFailed("The oauth2 token has expired")
+
+        return oauth2_token.user, None
\ No newline at end of file
diff --git a/program/migrations/0016_auto_20180222_1253.py b/program/migrations/0016_auto_20180222_1253.py
new file mode 100644
index 00000000..d9cb5da4
--- /dev/null
+++ b/program/migrations/0016_auto_20180222_1253.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2018-02-22 12:53
+from __future__ import unicode_literals
+
+from django.db import migrations
+import versatileimagefield.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('program', '0015_auto_20180218_1111'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='note',
+            name='image',
+            field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your note. 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'),
+        ),
+    ]
diff --git a/program/models.py b/program/models.py
index a1f047e7..4a3fe716 100644
--- a/program/models.py
+++ b/program/models.py
@@ -780,7 +780,7 @@ class Schedule(models.Model):
             return {'detail': _("Start and until dates mustn't be the same")}
 
         if schedule.until < schedule.dstart:
-            return {'detail': _("Until date mustn't before start")}
+            return {'detail': _("Until date mustn't be before start")}
 
         num_conflicts = len([pr for pr in conflicts['projected'] if len(pr['collisions']) > 0])
 
@@ -1168,7 +1168,7 @@ class Note(models.Model):
     ppoi = PPOIField('Image PPOI')
     height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False)
     width = models.PositiveIntegerField('Image Width', blank=True, null=True,editable=False)
-    image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', ppoi_field='ppoi', 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."))
+    image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', ppoi_field='ppoi', help_text=_("Upload an image to your note. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save."))
     status = models.IntegerField(_("Status"), choices=STATUS_CHOICES, default=1)
     start = models.DateTimeField(editable=False)
     show = models.ForeignKey(Show, related_name='notes', editable=True)
diff --git a/program/serializers.py b/program/serializers.py
index dcc20c72..696f8eed 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -128,6 +128,7 @@ class HostSerializer(serializers.ModelSerializer):
         instance.dorftv_url = validated_data.get('dorftv_url', instance.dorftv_url)
         instance.cba_url = validated_data.get('cba_url', instance.cba_url)
         instance.image = validated_data.get('image', instance.image)
+        instance.ppoi = validated_data.get('ppoi', instance.ppoi)
 
         instance.save()
         return instance
@@ -251,7 +252,7 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer):
 
     class Meta:
         model = Show
-        fields = ('id', 'name', 'slug', 'image', 'logo', 'short_description', 'description',
+        fields = ('id', 'name', 'slug', 'image', 'ppoi', 'logo', 'short_description', 'description',
                   'email', 'website', 'created', 'last_updated', 'type', 'fundingcategory',
                   'predecessor_id', 'cba_series_id', 'fallback_id', 'category', 'hosts',
                   'owners', 'language', 'topic', 'musicfocus', 'thumbnails')
@@ -292,6 +293,7 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer):
         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.ppoi = validated_data.get('ppoi', instance.ppoi)
         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)
@@ -424,6 +426,7 @@ class NoteSerializer(serializers.ModelSerializer):
         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.ppoi = validated_data.get('ppoi', instance.ppoi)
         instance.status = validated_data.get('status', instance.status)
         instance.host = validated_data.get('host', instance.host)
         instance.cba_id = validated_data.get('cba_id', instance.cba_id)
diff --git a/program/views.py b/program/views.py
index 5aa7a5aa..7bebcfb4 100644
--- a/program/views.py
+++ b/program/views.py
@@ -678,6 +678,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
 
 
 
+
 class APITimeSlotViewSet(viewsets.ModelViewSet):
     """
     /api/v1/timeslots                                                     Returns timeslots of the next 60 days (GET) - Timeslots may only be added by creating/updating a schedule
@@ -687,7 +688,7 @@ class APITimeSlotViewSet(viewsets.ModelViewSet):
     /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/1                               Returns a timeslot by its ID (GET, DELETE). If PUT, the next repetition is returned or nothing if the next timeslot isn't one
     /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
     """
 
@@ -762,16 +763,26 @@ class APITimeSlotViewSet(viewsets.ModelViewSet):
     def update(self, request, pk=None, schedule_pk=None, show_pk=None):
         """Link a playlist_id to a timeslot"""
 
-        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):
+        if schedule_pk == None or show_pk == None or not Show.is_editable(self, show_pk):
             return Response(status=status.HTTP_401_UNAUTHORIZED)
 
+        timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk)
+
         serializer = TimeSlotSerializer(timeslot, data=request.data)
         if serializer.is_valid():
             serializer.save()
-            return Response(serializer.data, status=status.HTTP_200_OK)
+
+            # Return the next repetition
+            # We do this because the Dashboard needs to update the repetition timeslot as well
+            # but with another playlist containing the recording instead of the original playlist
+            ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0]
+            if ts.is_repetition:
+                serializer = TimeSlotSerializer(ts)
+                return Response(serializer.data, status=status.HTTP_200_OK)
+
+            # ...or nothing if there isn't one
+            return Response(status=status.HTTP_200_OK)
 
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
diff --git a/pv/settings.py b/pv/settings.py
index 7c40541c..ae747e6e 100644
--- a/pv/settings.py
+++ b/pv/settings.py
@@ -30,9 +30,6 @@ DATABASE_ROUTERS = ['nop.dbrouter.NopRouter']
 
 TIME_ZONE = 'Europe/Vienna'
 
-USE_TZ = True # django-oidc-provider needs timezones in database
-LOGIN_URL = '/admin/login/' # Login page OIDC redirects to
-
 LANGUAGE_CODE = 'de'
 
 SITE_ID = 1
@@ -73,12 +70,13 @@ TEMPLATES = [
     },
 ]
 
-MIDDLEWARE_CLASSES = (
+MIDDLEWARE = (
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
+    'corsheaders.middleware.CorsMiddleware',
 )
 
 ROOT_URLCONF = 'pv.urls'
@@ -90,16 +88,11 @@ REST_FRAMEWORK = {
         #'rest_framework.permissions.IsAuthenticatedOrReadOnly',
         'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
     ],
-    'DEFAULT AUTHENTICATION_CLASSES': [
+    'DEFAULT_AUTHENTICATION_CLASSES': [
+        'program.auth.OidcOauth2Auth',
     ],
 }
 
-CORS_ALLOW_CREDENTIALS = True
-CORS_ORIGIN_WHITELIST = (
-   'localhost',
-   'localhost:8080',
-)
-
 INSTALLED_APPS = (
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -117,7 +110,7 @@ INSTALLED_APPS = (
     'rest_framework_nested',
     'frapp',
     'oidc_provider',
-    'corsheaders'
+    'corsheaders',
 )
 
 THUMBNAIL_SIZES = ['640x480', '200x200', '150x150']
@@ -165,6 +158,12 @@ CBA_AJAX_URL = CBA_URL + '/wp-admin/admin-ajax.php'
 CBA_REST_API_URL = CBA_URL + '/wp-json/wp/v2/'
 
 
+# OIDC Provider Settings
+
+USE_TZ = True # django-oidc-provider needs timezones in database
+LOGIN_URL = '/admin/login/' # Login page OIDC redirects to
+
+
 try:
     from .local_settings import *
 except ImportError:
diff --git a/pv/urls.py b/pv/urls.py
index e7baee15..1ce1b772 100644
--- a/pv/urls.py
+++ b/pv/urls.py
@@ -5,8 +5,6 @@ from django.conf.urls import url, include
 from django.contrib import admin
 from django.views.static import serve
 from rest_framework_nested import routers
-from rest_framework.authtoken import views
-from oidc_provider import urls
 
 from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIFundingCategoryViewSet, APILanguageViewSet, json_day_schedule, json_playout, json_timeslots_specials
 
-- 
GitLab