# # steering, Programme/schedule management for AURA # # Copyright (C) 2011-2017, 2020, Ernesto Rico Schmidt # Copyright (C) 2017-2019, Ingo Leindecker # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # from profile.models import Profile from profile.serializers import ProfileSerializer from typing import List from rest_framework import serializers from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from program.models import ( Category, FundingCategory, Host, Language, Link, MusicFocus, Note, RRule, Schedule, Show, TimeSlot, Topic, Type, ) from program.utils import get_audio_url from steering.settings import THUMBNAIL_SIZES SOLUTION_CHOICES = { "theirs": "Discard projected timeslot. Keep existing timeslot(s).", "ours": "Create projected timeslot. Delete existing timeslot(s).", "theirs-start": ( "Keep existing timeslot. Create projected timeslot with start time of existing end." ), "ours-start": ( "Create projected timeslot. Change end of existing timeslot to projected start time." ), "theirs-end": ( "Keep existing timeslot. Create projected timeslot with end of existing start time." ), "ours-end": ( "Create projected timeslot. Change start of existing timeslot to projected end time." ), "theirs-both": ( "Keep existing timeslot. " "Create two projected timeslots with end of existing start and start of existing end." ), "ours-both": ( "Create projected timeslot. Split existing timeslot into two: \n\n" "* set existing end time to projected start,\n" "* create another timeslot with start = projected end and end = existing end." ), } class ErrorSerializer(serializers.Serializer): message = serializers.CharField() code = serializers.CharField(allow_null=True) class UserSerializer(serializers.ModelSerializer): # Add profile fields to JSON profile = ProfileSerializer(required=False) class Meta: model = User fields = ( "id", "username", "first_name", "last_name", "email", "is_staff", "is_active", "is_superuser", "password", "profile", ) def create(self, validated_data): """ Create and return a new User instance, given the validated data. """ profile_data = ( validated_data.pop("profile") if "profile" in validated_data else None ) user = super(UserSerializer, self).create(validated_data) user.date_joined = timezone.now() user.set_password(validated_data["password"]) user.save() if profile_data: profile = Profile( user=user, cba_username=profile_data.get("cba_username").strip(), cba_user_token=profile_data.get("cba_user_token").strip(), ) profile.save() return user def update(self, instance, validated_data): """ Update and return an existing User instance, given the validated data. """ 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) 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 ) profile_data = ( validated_data.pop("profile") if "profile" in validated_data else None ) if profile_data: # TODO: How to hook into this from ProfileSerializer without having to call it here? try: profile = Profile.objects.get(user=instance.id) except ObjectDoesNotExist: profile = Profile.objects.create(user=instance, **profile_data) profile.cba_username = profile_data.get("cba_username") profile.cba_user_token = profile_data.get("cba_user_token") profile.save() instance.save() return instance class CategorySerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated category = serializers.CharField(source="name") class Meta: model = Category # TODO: replace `category` with `name` when the dashboard is updated fields = ("category", "abbrev", "slug", "is_active", "description") class LinkSerializer(serializers.ModelSerializer): class Meta: model = Link fields = ("description", "url") class HostSerializer(serializers.ModelSerializer): links = LinkSerializer(many=True, required=False) thumbnails = serializers.SerializerMethodField() @staticmethod def get_thumbnails(host) -> List[str]: """Returns thumbnails""" thumbnails = [] if host.image.name and THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES: thumbnails.append(host.image.crop[size].name) return thumbnails class Meta: model = Host fields = "__all__" def create(self, validated_data): links_data = validated_data.pop("links", []) host = Host.objects.create(**validated_data) for link_data in links_data: Link.objects.create(host=host, **link_data) host.save() return host def update(self, instance, validated_data): """ Update and return an existing Host instance, given the validated data. """ instance.name = validated_data.get("name", instance.name) instance.is_active = validated_data.get("is_active", instance.is_active) instance.email = validated_data.get("email", instance.email) instance.website = validated_data.get("website", instance.website) instance.biography = validated_data.get("biography", instance.biography) instance.image = validated_data.get("image", instance.image) instance.ppoi = validated_data.get("ppoi", instance.ppoi) if instance.links.count() > 0: for link in instance.links.all(): link.delete(keep_parents=True) if links := validated_data.get("links"): for link_data in links: Link.objects.create(host=instance, **link_data) instance.save() return instance class LanguageSerializer(serializers.ModelSerializer): class Meta: model = Language fields = ("name", "is_active") class TopicSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated topic = serializers.CharField(source="name") class Meta: model = Topic # TODO: replace `topic` with `name` when the dashboard is updated fields = ("topic", "abbrev", "slug", "is_active") class MusicFocusSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated focus = serializers.CharField(source="name") class Meta: model = MusicFocus # TODO: replace `focus` with `name` when the dashboard is updated fields = ("focus", "abbrev", "slug", "is_active") class TypeSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated type = serializers.CharField(source="name") class Meta: model = Type # TODO: replace `type` with `name` when the dashboard is updated fields = ("type", "slug", "is_active") class FundingCategorySerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated fundingcategory = serializers.CharField(source="name") class Meta: model = FundingCategory # TODO: replace `fundingcategory` with `name` when the dashboard is updated fields = ("fundingcategory", "abbrev", "slug", "is_active") class ShowSerializer(serializers.HyperlinkedModelSerializer): 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) # TODO: replace `musicfocs` with `music_focus` and remove the source when the dashboard is # updated musicfocus = serializers.PrimaryKeyRelatedField( queryset=MusicFocus.objects.all(), source="music_focus", many=True ) type = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all()) # TODO: replace `fundingcategory` with `funding_category` and remove the source when the # dashboard is updated fundingcategory = serializers.PrimaryKeyRelatedField( queryset=FundingCategory.objects.all(), source="funding_category" ) predecessor = serializers.PrimaryKeyRelatedField( queryset=Show.objects.all(), required=False, allow_null=True ) thumbnails = serializers.SerializerMethodField() @staticmethod def get_thumbnails(show) -> List[str]: """Returns thumbnails""" thumbnails = [] if show.image.name and THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES: thumbnails.append(show.image.crop[size].name) return thumbnails class Meta: model = Show fields = ( "id", "name", "slug", "image", "ppoi", "logo", "short_description", "description", "email", "website", "type", "fundingcategory", "predecessor", "cba_series_id", "default_playlist_id", "category", "hosts", "owners", "language", "topic", "musicfocus", "thumbnails", "is_active", "is_public", ) def create(self, validated_data): """ 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") language = validated_data.pop("language") topic = validated_data.pop("topic") music_focus = validated_data.pop("music_focus") show = Show.objects.create(**validated_data) # Save many-to-many relationships show.owners.set(owners) show.category.set(category) show.hosts.set(hosts) show.language.set(language) show.topic.set(topic) show.music_focus.set(music_focus) show.save() return show def update(self, instance, validated_data): """ Update and return an existing Show instance, given the validated data. """ 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) instance.email = validated_data.get("email", instance.email) instance.website = validated_data.get("website", instance.website) instance.cba_series_id = validated_data.get( "cba_series_id", instance.cba_series_id ) instance.default_playlist_id = validated_data.get( "default_playlist_id", instance.default_playlist_id ) instance.type = validated_data.get("type", instance.type) instance.funding_category = validated_data.get( "funding_category", instance.funding_category ) instance.predecessor = validated_data.get("predecessor", instance.predecessor) instance.is_active = validated_data.get("is_active", instance.is_active) instance.is_public = validated_data.get("is_public", instance.is_public) instance.owners.set(validated_data.get("owners", instance.owners)) instance.category.set(validated_data.get("category", instance.category)) instance.hosts.set(validated_data.get("hosts", instance.hosts)) instance.language.set(validated_data.get("language", instance.language)) instance.topic.set(validated_data.get("topic", instance.topic)) instance.music_focus.set( validated_data.get("music_focus", instance.music_focus) ) instance.save() return instance class ScheduleSerializer(serializers.ModelSerializer): rrule = serializers.PrimaryKeyRelatedField( queryset=RRule.objects.all(), help_text=Schedule.rrule.field.help_text, ) show = serializers.PrimaryKeyRelatedField( queryset=Show.objects.all(), help_text=Schedule.show.field.help_text, ) # TODO: remove this when the dashboard is updated byweekday = serializers.IntegerField( source="by_weekday", help_text=Schedule.by_weekday.field.help_text, ) dstart = serializers.DateField( source="first_date", help_text=Schedule.first_date.field.help_text, ) tstart = serializers.TimeField( source="start_time", help_text=Schedule.start_time.field.help_text, ) tend = serializers.TimeField( source="end_time", help_text=Schedule.end_time.field.help_text, ) until = serializers.DateField( source="last_date", help_text=Schedule.last_date.field.help_text, ) dryrun = serializers.BooleanField( write_only=True, required=False, help_text=( "Whether to simulate the database changes. If true, no database changes will occur. " "Instead a list of objects that would be created, updated and deleted if dryrun was " "false will be returned." ), ) class Meta: model = Schedule fields = "__all__" def create(self, 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.""" instance.by_weekday = validated_data.get("byweekday", instance.by_weekday) instance.first_date = validated_data.get("dstart", instance.first_date) instance.start_time = validated_data.get("tstart", instance.start_time) instance.end_time = validated_data.get("tend", instance.end_time) instance.last_date = validated_data.get("until", instance.last_date) instance.is_repetition = validated_data.get( "is_repetition", instance.is_repetition ) instance.default_playlist_id = validated_data.get( "default_playlist_id", instance.default_playlist_id ) instance.rrule = validated_data.get("rrule", instance.rrule) instance.show = validated_data.get("show", instance.show) instance.add_days_no = validated_data.get("add_days_no", instance.add_days_no) instance.add_business_days_only = validated_data.get( "add_business_days_only", instance.add_business_days_only ) instance.save() return instance class CollisionSerializer(serializers.Serializer): id = serializers.IntegerField() start = serializers.DateTimeField() end = serializers.DateTimeField() playlist_id = serializers.IntegerField(allow_null=True) show = serializers.IntegerField() show_name = serializers.CharField() is_repetition = serializers.BooleanField() schedule = serializers.IntegerField() memo = serializers.CharField() note_id = serializers.IntegerField(allow_null=True) class ProjectedTimeSlotSerializer(serializers.Serializer): hash = serializers.CharField() start = serializers.DateTimeField() end = serializers.DateTimeField() collisions = CollisionSerializer(many=True) error = serializers.CharField(allow_null=True) solution_choices = serializers.ListField( child=serializers.ChoiceField(SOLUTION_CHOICES) ) class DryRunTimeSlotSerializer(serializers.Serializer): id = serializers.PrimaryKeyRelatedField( queryset=TimeSlot.objects.all(), allow_null=True ) schedule = serializers.PrimaryKeyRelatedField( queryset=Schedule.objects.all(), allow_null=True ) playlist_id = serializers.IntegerField(allow_null=True) start = serializers.DateField() end = serializers.DateField() is_repetition = serializers.BooleanField() memo = serializers.CharField() class ScheduleCreateUpdateRequestSerializer(serializers.Serializer): schedule = ScheduleSerializer() solutions = serializers.DictField(child=serializers.ChoiceField(SOLUTION_CHOICES)) notes = serializers.DictField(child=serializers.IntegerField(), required=False) playlists = serializers.DictField(child=serializers.IntegerField(), required=False) # TODO: There shouldn’t be a separate ScheduleSerializer for use in responses. # Instead the default serializer should be used. Unfortunately, the # code that generates the data creates custom dicts with this particular format. class ScheduleInResponseSerializer(serializers.Serializer): # "Schedule schema type" is the rendered name of the ScheduleSerializer. """ For documentation on the individual fields see the Schedule schema type. """ add_business_days_only = serializers.BooleanField() add_days_no = serializers.IntegerField(allow_null=True) by_weekday = serializers.IntegerField() default_playlist_id = serializers.IntegerField(allow_null=True) end_time = serializers.TimeField() first_date = serializers.DateField() id = serializers.PrimaryKeyRelatedField(queryset=Schedule.objects.all()) is_repetition = serializers.BooleanField() last_date = serializers.DateField() rrule = serializers.PrimaryKeyRelatedField(queryset=RRule.objects.all()) show = serializers.PrimaryKeyRelatedField(queryset=Note.objects.all()) start_time = serializers.TimeField() class ScheduleConflictResponseSerializer(serializers.Serializer): projected = ProjectedTimeSlotSerializer(many=True) solutions = serializers.DictField(child=serializers.ChoiceField(SOLUTION_CHOICES)) notes = serializers.DictField(child=serializers.IntegerField()) playlists = serializers.DictField(child=serializers.IntegerField()) schedule = ScheduleInResponseSerializer() class ScheduleDryRunResponseSerializer(serializers.Serializer): created = DryRunTimeSlotSerializer(many=True) updated = DryRunTimeSlotSerializer(many=True) deleted = DryRunTimeSlotSerializer(many=True) 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.""" return TimeSlot.objects.create(**validated_data) def update(self, instance, 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) instance.save() return instance class NoteSerializer(serializers.ModelSerializer): show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) timeslot = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all()) host = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all()) thumbnails = serializers.SerializerMethodField() @staticmethod def get_thumbnails(note) -> List[str]: """Returns thumbnails""" thumbnails = [] if note.image.name and THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES: thumbnails.append(note.image.crop[size].name) return thumbnails class Meta: model = Note fields = "__all__" def create(self, 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"] = get_audio_url(validated_data.get("cba_id", None)) note = Note.objects.create(**validated_data) # Assign note to timeslot if note.timeslot_id is not 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): """Update and return an existing Note instance, given the validated data.""" 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) 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) instance.audio_url = 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 is not 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