From 41ebf15984bfcac0ff0f0fd5bf838c391d0ec202 Mon Sep 17 00:00:00 2001
From: ingo <ingo.leindecker@fro.at>
Date: Thu, 21 Dec 2017 00:04:33 +0100
Subject: [PATCH] Added nested endpoints as well as some filters

/api/v1/shows/
/api/v1/shows/?active=true
/api/v1/shows/1/
/api/v1/shows/1/schedules
/api/v1/shows/1/schedules/1
/api/v1/shows/1/schedules/1/timeslots
/api/v1/shows/1/schedules/1/timeslots/1/
/api/v1/shows/1/schedules/1/timeslots/1/notes
/api/v1/shows/1/schedules/1/timeslots/1/notes/1
/api/v1/shows/1/notes
/api/v1/shows/1/notes/1
/api/v1/shows/1/timeslots
/api/v1/shows/1/timeslots/1
/api/v1/shows/1/timeslots/1/notes
/api/v1/shows/1/timeslots/1/notes/1

On every /timeslot endpoint, filter variables ?start=2017-01-01&end=2017-12-31 are available.

Properly create, update and delete:

* Users
  - only superusers may create
  - common users can only retrieve/update themselves
  - update is constrained to certain non-crucial fields for non-superusers

* Shows
  - only superusers may create
  - update is constrained to certain fields for non-superusers

* Notes
  - creating/updating is constrained to shows and timeslots the non-superuser owns

* Schedules
  - only superusers may add/update/delete
  - collision detection still needed

* Timeslots
  - direct creation is prohibited for everyone (only by adding/updating schedules)
  - update is constrained to certain fields
  - only superusers may delete


Available GET endpoints for (only superusers may add/update/delete)

* Hosts
* Types
* Topics
* Categories
* RTRCategories
* Languages


See #22 #23
---
 program/admin.py                |  27 +-
 program/models.py               |  40 ++-
 program/serializers.py          | 136 ++++++++---
 program/templates/calendar.html |   2 +-
 program/views.py                | 419 ++++++++++++++++++++++++--------
 pv/settings.py                  |   9 +-
 pv/urls.py                      |  41 +++-
 requirements.txt                |   1 +
 8 files changed, 501 insertions(+), 174 deletions(-)

diff --git a/program/admin.py b/program/admin.py
index 9d4bff8f..b4ff8fe9 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 e856f8cd..378a2aa1 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 3b5ce0cb..4d0a5f2c 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 4f8d7f4f..da3bf26e 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 001ee661..2adb0449 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 74d3a2c8..e9edf97e 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 ea38cf19..8d89b1ec 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 d1ce91ba..aa962397 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
-- 
GitLab