From 19038645232cfb9b7d602887011e6355a4c5f8fb Mon Sep 17 00:00:00 2001
From: ingo <ingo.leindecker@fro.at>
Date: Tue, 27 Mar 2018 14:00:46 +0200
Subject: [PATCH] Save note_id to timeslots

+
* Fixed bug when deleting a note
* Extended schedule list view and filters in admin ui
* Got rid of unnecessary scopes
* Rearranged settings
---
 program/admin.py       |  5 ++-
 program/models.py      | 11 ++---
 program/serializers.py | 30 +++++++++++++-
 program/views.py       | 26 ++++--------
 pv/settings.py         | 91 +++++++++++++++++++++++++++---------------
 pv/urls.py             |  4 +-
 6 files changed, 103 insertions(+), 64 deletions(-)

diff --git a/program/admin.py b/program/admin.py
index 3eeb13fd..57597c60 100644
--- a/program/admin.py
+++ b/program/admin.py
@@ -11,6 +11,7 @@ from .forms import MusicFocusForm
 
 from datetime import date, datetime, time, timedelta
 
+
 class ActivityFilter(admin.SimpleListFilter):
     title = _("Activity")
 
@@ -190,7 +191,7 @@ class TimeSlotAdmin(admin.ModelAdmin):
 class ScheduleAdmin(admin.ModelAdmin):
     actions = ('renew',)
     inlines = (TimeSlotInline,)
-    fields = (('rrule', 'byweekday'), ('dstart', 'tstart', 'tend'), 'until', 'is_repetition', 'automation_id', 'fallback_id')
+    fields = (('rrule', 'byweekday'), ('tstart', 'tend'), 'dstart', 'until', 'is_repetition', ('add_days_no', 'add_business_days_only'), 'fallback_id', 'automation_id', )
     list_display = ('get_show_name', 'byweekday', 'rrule', 'tstart', 'tend', 'until')
     list_filter = (ActiveSchedulesFilter, 'byweekday', 'rrule', 'is_repetition')
     ordering = ('byweekday', 'dstart')
@@ -223,7 +224,7 @@ class ShowAdmin(admin.ModelAdmin):
     filter_horizontal = ('hosts', 'owners', 'musicfocus', 'category', 'topic', 'language')
     inlines = (ScheduleInline,)
     list_display = ('name', 'short_description')
-    list_filter = (ActiveShowsFilter, 'type', 'category', 'topic', 'musicfocus', 'fundingcategory', 'language')
+    list_filter = (ActiveShowsFilter, 'type', 'category', 'topic', 'musicfocus', 'language', 'fundingcategory')
     ordering = ('slug',)
     prepopulated_fields = {'slug': ('name',)}
     search_fields = ('name', 'short_description', 'description')
diff --git a/program/models.py b/program/models.py
index 486b3fb1..c6c70659 100644
--- a/program/models.py
+++ b/program/models.py
@@ -316,13 +316,13 @@ class Show(models.Model):
     height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False)
     width = models.PositiveIntegerField('Image Width', blank=True, null=True,editable=False)
     image = VersatileImageField(_("Image"), blank=True, null=True, upload_to='show_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."))
-    logo = models.ImageField(_("Logo"), blank=True, null=True, upload_to='show_images')
-    short_description = models.TextField(_("Short description"), help_text=_("Describe your show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically."))
+    logo = models.ImageField(_("Logo"), blank=True, null=True, upload_to='show_images', help_text=_("Upload a logo of your show."))
+    short_description = models.TextField(_("Short description"), help_text=_("Describe your show for your listeners in some sentences. Avoid technical data like airing times and contact information. They will be added automatically."))
     description = tinymce_models.HTMLField(_("Description"), blank=True, null=True, help_text=_("Describe your show in detail."))
     email = models.EmailField(_("E-Mail"), blank=True, null=True, help_text=_("The main contact email address for your show."))
     website = models.URLField(_("Website"), blank=True, null=True, help_text=_("Is there a website to your show? Type in its URL."))
-    cba_series_id = models.IntegerField(_("CBA Series ID"), blank=True, null=True, help_text=_("Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your ID under https://cba.fro.at/series"))
-    fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True)
+    cba_series_id = models.IntegerField(_("CBA Series ID"), blank=True, null=True, help_text=_("Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your show's ID under https://cba.fro.at/series"))
+    fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True, help_text=_("If a timeslot of your show is empty, this playlist will be aired as a backup."))
     created = models.DateTimeField(auto_now_add=True, editable=False)
     last_updated = models.DateTimeField(auto_now=True, editable=False)
 
@@ -627,10 +627,7 @@ class Schedule(models.Model):
                            ( Q(start__lte=ts.start) & Q(end__gte=ts.end) )
                         )
 
-            print("testing " + str(ts.start) + " - " + str(ts.end))
-
             if collision:
-                print("collision found: " + str(vars(collision[0])))
                 collisions.append(collision[0]) # TODO: Do we really always retrieve one?
             else:
                 collisions.append(None)
diff --git a/program/serializers.py b/program/serializers.py
index 0a5cb212..f0a47597 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -262,6 +262,7 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer):
         """
         Create and return a new Show instance, given the validated data.
         """
+
         owners = validated_data.pop('owners')
         category = validated_data.pop('category')
         hosts = validated_data.pop('hosts')
@@ -415,7 +416,18 @@ class NoteSerializer(serializers.ModelSerializer):
         # 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)
+        note = Note.objects.create(**validated_data)
+
+        # Assign note to timeslot
+        if note.timeslot_id != None:
+            try:
+                timeslot = TimeSlot.objects.get(pk=note.timeslot_id)
+                timeslot.note_id = note.id
+                timeslot.save(update_fields=["note_id"])
+            except ObjectDoesNotExist:
+                pass
+
+        return note
 
 
     def update(self, instance, validated_data):
@@ -435,4 +447,20 @@ class NoteSerializer(serializers.ModelSerializer):
         instance.audio_url = Note.get_audio_url(instance.cba_id)
 
         instance.save()
+
+        # Remove existing note connections from timeslots
+        timeslots = TimeSlot.objects.filter(note_id=instance.id)
+        for ts in timeslots:
+            ts.note_id = None
+            ts.save(update_fields=["note_id"])
+
+        # Assign note to timeslot
+        if instance.timeslot.id != None:
+            try:
+                timeslot = TimeSlot.objects.get(pk=instance.timeslot.id)
+                timeslot.note_id = instance.id
+                timeslot.save(update_fields=["note_id"])
+            except ObjectDoesNotExist:
+                pass
+
         return instance
\ No newline at end of file
diff --git a/program/views.py b/program/views.py
index 538512ce..8302ac9f 100644
--- a/program/views.py
+++ b/program/views.py
@@ -322,6 +322,7 @@ def json_playout(request):
                         content_type="application/json; charset=utf-8")
 
 
+# Deprecated
 def json_timeslots_specials(request):
     specials = {}
     shows = get_cached_shows()['shows']
@@ -366,8 +367,6 @@ class APIUserViewSet(viewsets.ModelViewSet):
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
     serializer_class = UserSerializer
     queryset = User.objects.none()
-    required_scopes = ['users']
-
 
     def get_queryset(self):
         """Constrain access to oneself except for superusers"""
@@ -459,8 +458,6 @@ class APIShowViewSet(viewsets.ModelViewSet):
     serializer_class = ShowSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
     pagination_class = LimitOffsetPagination
-    required_scopes = ['shows']
-
 
     def get_queryset(self):
 
@@ -569,8 +566,6 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
     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
@@ -698,8 +693,6 @@ class APITimeSlotViewSet(viewsets.ModelViewSet):
     serializer_class = TimeSlotSerializer
     pagination_class = LimitOffsetPagination
     queryset = TimeSlot.objects.none()
-    required_scopes = ['timeslots']
-
 
     def get_queryset(self):
 
@@ -831,8 +824,6 @@ class APINoteViewSet(viewsets.ModelViewSet):
     serializer_class = NoteSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
     pagination_class = LimitOffsetPagination
-    required_scopes = ['notes']
-
 
     def get_queryset(self):
 
@@ -980,7 +971,11 @@ class APINoteViewSet(viewsets.ModelViewSet):
         return Response(status=status.HTTP_400_BAD_REQUEST)
 
 
-    def destroy(self, request, pk=None):
+    def destroy(self, request, pk=None, show_pk=None, schedule_pk=None, timeslot_pk=None):
+        # Allow DELETE only when calling /shows/1/schedules/1/timeslots/1/note/1
+        if show_pk == None or schedule_pk == None or timeslot_pk == None:
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
         note = get_object_or_404(Note, pk=pk)
 
         if Note.is_editable(self, note.id):
@@ -996,11 +991,10 @@ class APICategoryViewSet(viewsets.ModelViewSet):
     /api/v1/categories/?active=true Returns all active categories (GET)
     /api/v1/categories/1            Returns a category by its ID (GET, PUT, DELETE)
     """
-
     queryset = Category.objects.all()
     serializer_class = CategorySerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['categories']
+
 
     def get_queryset(self):
         '''Filters'''
@@ -1021,7 +1015,6 @@ class APITypeViewSet(viewsets.ModelViewSet):
     queryset = Type.objects.all()
     serializer_class = TypeSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['types']
 
     def get_queryset(self):
         '''Filters'''
@@ -1042,7 +1035,6 @@ class APITopicViewSet(viewsets.ModelViewSet):
     queryset = Topic.objects.all()
     serializer_class = TopicSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['topics']
 
     def get_queryset(self):
         '''Filters'''
@@ -1064,7 +1056,6 @@ class APIMusicFocusViewSet(viewsets.ModelViewSet):
     queryset = MusicFocus.objects.all()
     serializer_class = MusicFocusSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['musicfocus']
 
     def get_queryset(self):
         '''Filters'''
@@ -1086,7 +1077,6 @@ class APIFundingCategoryViewSet(viewsets.ModelViewSet):
     queryset = FundingCategory.objects.all()
     serializer_class = FundingCategorySerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['fundingcategories']
 
     def get_queryset(self):
         '''Filters'''
@@ -1108,7 +1098,6 @@ class APILanguageViewSet(viewsets.ModelViewSet):
     queryset = Language.objects.all()
     serializer_class = LanguageSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['languages']
 
     def get_queryset(self):
         '''Filters'''
@@ -1130,7 +1119,6 @@ class APIHostViewSet(viewsets.ModelViewSet):
     queryset = Host.objects.all()
     serializer_class = HostSerializer
     permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
-    required_scopes = ['hosts']
 
     def get_queryset(self):
         '''Filters'''
diff --git a/pv/settings.py b/pv/settings.py
index 1ad5ba3b..fd964b15 100644
--- a/pv/settings.py
+++ b/pv/settings.py
@@ -4,49 +4,70 @@
 
 import os.path
 
+# Paths
+
 PROJECT_DIR = os.path.dirname(__file__)
 
+LOCALE_PATHS = (
+    os.path.join(PROJECT_DIR, 'locale'),
+)
+
+MEDIA_ROOT = os.path.join(PROJECT_DIR, 'site_media')
+MEDIA_URL = '/site_media/'
+
+STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
+STATIC_URL = '/static/'
+
+ROOT_URLCONF = 'pv.urls'
+
 DEBUG = True
+SITE_ID = 1
+ADMINS = ()
+MANAGERS = ADMINS
 
 # Must be set if DEBUG is False
 ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
 
-ADMINS = ()
+# Whitelist IPs that access the API
+CORS_ORIGIN_WHITELIST = (
+   'localhost',
+   'localhost:8080'
+)
 
-MANAGERS = ADMINS
 
+# Define which database backend to use for our apps
 DATABASES = {
+    # SQLITE
     'default': {
         'ENGINE': 'django.db.backends.sqlite3',
         'NAME': os.path.join(PROJECT_DIR, 'dev_data.sqlite'),
     },
+
+    """
+    # MySQL
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'OPTIONS': {
+            'read_default_file': os.path.join(PROJECT_DIR, 'mysql.cnf'),
+        },
+    }
+    """
+    # Deprecated
     'nop': {
         'ENGINE': 'django.db.backends.sqlite3',
         'NAME': os.path.join(PROJECT_DIR, 'dev_nop.sqlite'),
-    }
+    },
+
 }
 
-DATABASE_ROUTERS = ['nop.dbrouter.NopRouter']
+CACHE_BACKEND = 'locmem://'
 
+# LOCALIZATION
 TIME_ZONE = 'Europe/Vienna'
-
 LANGUAGE_CODE = 'de'
-
-SITE_ID = 1
-
 USE_I18N = True
 USE_L10N = True
 
-LOCALE_PATHS = (
-    os.path.join(PROJECT_DIR, 'locale'),
-)
-
-MEDIA_ROOT = os.path.join(PROJECT_DIR, 'site_media')
-MEDIA_URL = '/site_media/'
-
-STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
-STATIC_URL = '/static/'
-
 SECRET_KEY = ''
 
 TEMPLATES = [
@@ -71,21 +92,18 @@ TEMPLATES = [
 ]
 
 MIDDLEWARE = (
+    'corsheaders.middleware.CorsMiddleware',
     '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'
-
 REST_FRAMEWORK = {
     # Use Django's standard `django.contrib.auth` permissions,
     # or allow read-only access for unauthenticated users.
     'DEFAULT_PERMISSION_CLASSES': [
-        #'rest_framework.permissions.IsAuthenticatedOrReadOnly',
         'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
     ],
     'DEFAULT_AUTHENTICATION_CLASSES': [
@@ -102,7 +120,7 @@ INSTALLED_APPS = (
     'django.contrib.admin',
     'django.contrib.staticfiles',
     'program',
-    'nop',
+    'nop', # Deprecated
     'profile',
     'tinymce',
     'versatileimagefield',
@@ -113,6 +131,8 @@ INSTALLED_APPS = (
     'corsheaders',
 )
 
+# Set the desired sizes for your thumbnails (px)
+# Will apply to all uploaded images
 THUMBNAIL_SIZES = ['640x480', '200x200', '150x150']
 
 #TINYMCE_JS_URL = '/static/js/tiny_mce/tiny_mce.js'
@@ -129,28 +149,23 @@ TINYMCE_DEFAULT_CONFIG = {
     'height': 400,
 }
 
-CACHE_BACKEND = 'locmem://'
 
 # When generating schedules/timeslots:
-# If until date wasn't set, add x days to start time
+# If until date wasn't set manually, add x days to the start date
 AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE = 365
 
-# If until date wasn't set, auto-set it to the end of the year
+# If until date wasn't set manually, auto-set it to December 31 of the current year
 # Overrides the above setting if True
 AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR = True
 
-MUSIKPROG_IDS = (
-    1,    # unmodieriertes musikprogramm
-)
-SPECIAL_PROGRAM_IDS = ()
-
 # The station's fallback playlist ID
+# If there's no program, this playlist will be aired as a backup
 STATION_FALLBACK_ID = None
 
 # URL to CBA - Cultural Broadcasting Archive
 CBA_URL = 'https://cba.fro.at'
 
-# Contact cba@fro.at to be whitelisted and get an API KEY
+# Contact cba@fro.at to get whitelisted and get an API KEY
 # Leave empty to disable requests to CBA
 CBA_API_KEY = ''
 
@@ -162,10 +177,20 @@ 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
 
+#WSGI_APPLICATION = 'pv.wsgi.application';
+
+
+# Deprecated
+DATABASE_ROUTERS = ['nop.dbrouter.NopRouter']
+MUSIKPROG_IDS = (
+    1,    # unmodieriertes musikprogramm
+)
+SPECIAL_PROGRAM_IDS = ()
+
+
 
 try:
     from .local_settings import *
diff --git a/pv/urls.py b/pv/urls.py
index 1ce1b772..9ed9a2c3 100644
--- a/pv/urls.py
+++ b/pv/urls.py
@@ -65,10 +65,10 @@ urlpatterns = [
     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'^nop', include('nop.urls')), # Deprecated
     url(r'^api/', include('frapp.urls')),
     #url(r'^tinymce/', include('tinymce.urls')),
-    url(r'^export/timeslots_specials.json$', json_timeslots_specials),
+    url(r'^export/timeslots_specials.json$', json_timeslots_specials), # Deprecated
 ]
 
 if settings.DEBUG:
-- 
GitLab