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