Skip to content
Snippets Groups Projects
serializers.py 57.3 KiB
Newer Older
  • Learn to ignore specific revisions
  •             "add_business_days_only", instance.add_business_days_only
            )
    
    class CollisionSerializer(serializers.Serializer):
        start = serializers.DateTimeField()
        end = serializers.DateTimeField()
        playlist_id = serializers.IntegerField(allow_null=True)
    
        show_id = serializers.IntegerField()
    
        show_name = serializers.CharField()
    
        schedule_id = serializers.IntegerField()
    
        memo = serializers.CharField()
    
        timeslot_id = serializers.IntegerField()
    
        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_id = serializers.PrimaryKeyRelatedField(
            queryset=Schedule.objects.all(), allow_null=True
        )
    
        playlist_id = serializers.IntegerField(allow_null=True)
        start = serializers.DateField()
        end = serializers.DateField()
    
        repetition_of_id = serializers.IntegerField(allow_null=True)
    
        memo = serializers.CharField()
    
    
    class ScheduleCreateUpdateRequestSerializer(serializers.Serializer):
    
        schedule = ScheduleInRequestSerializer(help_text="`Schedule` object.")
    
            child=serializers.ChoiceField(SOLUTION_CHOICES),
            required=False,
            help_text="Array of solution choices.",
        )
        notes = serializers.DictField(
            child=serializers.IntegerField(), required=False, help_text="Array of `Note` objects."
        )
        playlists = serializers.DictField(
            child=serializers.IntegerField(), required=False, help_text="Array of `Playlist` IDs."
    
    class ScheduleResponseSerializer(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 = ScheduleSerializer()
    
    
    class ScheduleConflictResponseSerializer(ScheduleResponseSerializer):
        schedule = UnsavedScheduleSerializer()
    
    
    
    class ScheduleDryRunResponseSerializer(serializers.Serializer):
        created = DryRunTimeSlotSerializer(many=True)
        updated = DryRunTimeSlotSerializer(many=True)
        deleted = DryRunTimeSlotSerializer(many=True)
    
    
    
    class TimeSlotSerializer(serializers.ModelSerializer):
    
        note_id = serializers.SerializerMethodField()
        show_id = serializers.SerializerMethodField()
    
        schedule_id = serializers.PrimaryKeyRelatedField(
    
            queryset=Schedule.objects.all(),
            required=False,
            source="schedule",
            help_text="`Schedule` ID of this timeslot.",
    
        )
        repetition_of_id = serializers.PrimaryKeyRelatedField(
    
            allow_null=True,
            queryset=TimeSlot.objects.all(),
            required=False,
            source="repetition_of",
            help_text="This timeslot is a repetition of `Timeslot` ID.",
    
        playlist_id = serializers.PrimaryKeyRelatedField(
            allow_null=True,
            help_text="",
            queryset=Playlist.objects.all(),
            required=False,
            source="playlist",
        )
    
            read_only_fields = (
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                "start",
    
            fields = (
                "memo",
                "playlist_id",
    
            ) + read_only_fields
    
        def get_show_id(obj) -> int:
    
            return obj.schedule.show.id
    
        @staticmethod
    
        def get_note_id(obj) -> int:
    
            return obj.note.id if hasattr(obj, "note") else None
    
        @staticmethod
        def get_start(obj) -> datetime:
            return obj.start.astimezone(tz=ZoneInfo(settings.TIME_ZONE))
    
        @staticmethod
        def get_end(obj) -> datetime:
            return obj.end.astimezone(tz=ZoneInfo(settings.TIME_ZONE))
    
    
        def to_representation(self, instance):
            representation = super().to_representation(instance)
    
    
            if not (self.context.get("request") and self.context.get("request").user.is_authenticated):
    
        def update(self, instance, validated_data):
    
            """Update and return an existing Show instance, given the validated data."""
    
            user = self.context.get("request").user
            user_is_owner = user in instance.schedule.show.owners.all()
    
            # Having the update_timeslot permission overrides the ownership
            if not (
                user.has_perm("program.update_timeslot")
                or (user.has_perm("program.change_timeslot") and user_is_owner)
            ):
                raise exceptions.PermissionDenied(
                    detail="You are not allowed to update this timeslot."
                )
    
    
            if "memo" in validated_data:
                instance.memo = validated_data.get("memo")
    
            if "repetition_of" in validated_data:
                instance.repetition_of = validated_data.get("repetition_of")
    
            if "playlist_id" in validated_data:
    
                instance.playlist = validated_data.get("playlist_id")
    
    class NoteLinkSerializer(serializers.ModelSerializer):
    
        type_id = serializers.PrimaryKeyRelatedField(queryset=LinkType.objects.all(), source="type")
    
    
            fields = ("type_id", "url")
    
    tags_json_schema = {"type": "array", "items": {"type": "string"}}
    
    @extend_schema_field(list[str])
    class TagsField(JSONSchemaField):
        def __init__(self, *args, **kwargs):
            super().__init__({"type": "array", "items": {"type": "string"}}, *args, **kwargs)
    
    
    
    class NoteSerializer(serializers.ModelSerializer):
    
        contributor_ids = serializers.PrimaryKeyRelatedField(
    
            queryset=Profile.objects.all(),
    
            required=False,
            source="contributors",
    
            help_text="`Profile` IDs that contributed to this episode.",
    
        image_id = serializers.PrimaryKeyRelatedField(
    
            queryset=Image.objects.all(),
            required=False,
            allow_null=True,
            source="image",
            help_text="`Image` ID.",
    
        language_ids = serializers.PrimaryKeyRelatedField(
            allow_null=True,
            many=True,
            queryset=Language.objects.all(),
            required=False,
            source="language",
    
            help_text="Array of `Language` IDs.",
    
        links = NoteLinkSerializer(many=True, required=False, help_text="Array of `Link` objects.")
        playlist_id = serializers.IntegerField(required=False, help_text="Array of `Playlist` IDs.")
        tags = JSONSchemaField(tags_json_schema, required=False, help_text="Tags of the Note.")
    
        timeslot_id = serializers.PrimaryKeyRelatedField(
    
            queryset=TimeSlot.objects.all(),
            required=False,
            source="timeslot",
            help_text="`Timeslot` ID.",
    
        topic_ids = serializers.PrimaryKeyRelatedField(
    
            allow_null=True,
            many=True,
            queryset=Topic.objects.all(),
            required=False,
            source="topic",
            help_text="Array of `Topic`IDs.",
    
    Ingo Leindecker's avatar
    Ingo Leindecker committed
    
    
            read_only_fields = (
    
                "created_at",
                "created_by",
                "updated_at",
                "updated_by",
            )
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                "cba_id",
                "content",
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                "links",
    
    Ernesto Rico Schmidt's avatar
    Ernesto Rico Schmidt committed
                "title",
    
            ) + read_only_fields
    
    
        def update(self, instance, validated_data):
    
            """Update and return an existing Note instance, given the validated data."""
    
    
            user = self.context.get("request").user
    
            user_is_owner = user in instance.timeslot.schedule.show.owners.all()
    
            # Having the update_note permission overrides the ownership
    
            if not (
                user.has_perm("program.update_note")
                or (user.has_perm("program.change_note") and user_is_owner)
            ):
    
                raise exceptions.PermissionDenied(detail="You are not allowed to update this note.")
    
            if "cba_id" in validated_data:
                instance.cba_id = validated_data.get("cba_id")
    
            if "content" in validated_data:
                instance.content = validated_data.get("content")
    
            if "image" in validated_data:
                instance.image = validated_data.get("image")
    
            if "summary" in validated_data:
                instance.summary = validated_data.get("summary")
    
            if "timeslot" in validated_data:
                instance.timeslot = validated_data.get("timeslot")
    
            if "tags" in validated_data:
                instance.tags = validated_data.get("tags")
    
            if "title" in validated_data:
                instance.title = validated_data.get("title")
    
            if "contributors" in validated_data:
                instance.contributors.set(validated_data.get("contributors", []))
    
            if "language" in validated_data:
                instance.language.set(validated_data.get("language", []))
    
            # Only update this field if the user has the update_note permission, ignore otherwise
            if "topic" in validated_data and user.has_perm("program.update_note"):
    
                instance.topic.set(validated_data.get("topic", []))
    
            # optional nested objects
    
            if "links" in validated_data:
    
                instance = update_links(instance, validated_data.get("links"))
    
            instance.updated_by = self.context.get("request").user.username
    
    
    
    class RadioSettingsSerializer(serializers.ModelSerializer):
        cba = serializers.SerializerMethodField()
    
        image_requirements = serializers.SerializerMethodField()
    
        playout = serializers.SerializerMethodField()
    
        program = serializers.SerializerMethodField()
    
        station = serializers.SerializerMethodField()
    
        class Meta:
    
            fields = read_only_fields = (
                "id",
                "cba",
                "image_requirements",
                "playout",
    
            model = RadioSettings
    
    
        def get_cba(self, obj) -> RadioCBASettings:
    
            if self.context.get("request").user.is_authenticated:
    
                return RadioCBASettings(
                    api_key=obj.cba_api_key,
                    domains=obj.cba_domains,
                )
    
                return RadioCBASettings(domains=obj.cba_domains)
    
        @staticmethod
        def get_image_requirements(obj) -> RadioImageRequirementsSettings:
    
            def get_aspect_ratio(field) -> tuple[int, int] | tuple[float, float]:
                """return the tuple of ints or floats representing the aspect ratio of the image."""
    
                try:
                    return int(values[0]), int(values[1])
                except ValueError:
                    return float(values[0]), float(values[1])
    
                "note.image": get_aspect_ratio(obj.note_image_aspect_ratio),
    
                "profile.image": get_aspect_ratio(obj.profile_image_aspect_ratio),
    
                "show.image": get_aspect_ratio(obj.show_image_aspect_ratio),
                "show.logo": get_aspect_ratio(obj.show_logo_aspect_ratio),
    
                        "aspect_ratio": aspect_ratios["note.image"],
                        "shape": obj.profile_image_shape,
    
                        "aspect_ratio": aspect_ratios["profile.image"],
                        "shape": obj.profile_image_shape,
    
                    }
                },
                "show.image": {
                    "frame": {
                        "aspect_ratio": aspect_ratios["show.image"],
    
                        "shape": obj.profile_image_shape,
    
                    }
                },
                "show.logo": {
                    "frame": {
                        "aspect_ratio": aspect_ratios["show.logo"],
    
                        "shape": obj.profile_image_shape,
    
        @staticmethod
    
        def get_program(obj) -> RadioProgramSettings:
    
            return RadioProgramSettings(
                micro=MicroProgram(show_id=obj.micro_show.id if obj.micro_show else None),
                fallback=ProgramFallback(
                    show_id=obj.fallback_show.id if obj.fallback_show else None,
                    default_pool="fallback" if obj.fallback_default_pool else "",
                ),
            )
    
        def get_playout(obj) -> RadioPlayoutSettings:
    
            return RadioPlayoutSettings(
                line_in_channels=obj.line_in_channels,
                pools=obj.pools,
            )
    
        def get_station(obj) -> RadioStationSettings:
    
            logo = (
                Logo(
                    url=f"{settings.SITE_URL}{obj.station_logo.url}",
                    height=obj.station_logo.height,
                    width=obj.station_logo.width,
                )
                if obj.station_logo
                else None
            )
    
            return RadioStationSettings(
                name=obj.station_name,
                logo=logo,
                website=obj.station_website,
            )
    
    class BasicProgramEntrySerializer(serializers.Serializer):
    
        id = serializers.UUIDField()
    
        start = serializers.DateTimeField()
        end = serializers.DateTimeField()
        timeslot_id = serializers.IntegerField(allow_null=True, source="timeslot.id")
    
        playlist_id = serializers.IntegerField(allow_null=True)
    
        show_id = serializers.IntegerField(source="show.id")
    
    
    
    class PlayoutProgramEntrySerializer(BasicProgramEntrySerializer):
        class PlayoutShowSerializer(serializers.ModelSerializer):
            class Meta:
                model = Show
                fields = ["id", "name", "default_playlist_id"]
    
        class PlayoutScheduleSerializer(serializers.ModelSerializer):
            class Meta:
                model = Schedule
                fields = ["id", "default_playlist_id"]
    
        class PlayoutEpisodeSerializer(serializers.ModelSerializer):
            class Meta:
                model = Note
                fields = ["id", "title"]
    
    
        timeslot = TimeSlotSerializer(allow_null=True)
    
        show = PlayoutShowSerializer()
    
        episode = PlayoutEpisodeSerializer(allow_null=True, source="timeslot.note")
        schedule = PlayoutScheduleSerializer(allow_null=True, source="timeslot.schedule")
    
    
    
    class CalendarSchemaSerializer(serializers.Serializer):
        class Wrapper:
            def __init__(self, program: list[ProgramEntry]):
                self.program = program
    
            @cached_property
            def shows(self):
                show_ids = set(entry.show.id for entry in self.program)
                return Show.objects.distinct().filter(id__in=show_ids)
    
            @cached_property
            def timeslots(self):
                timeslot_ids = set(entry.timeslot.id for entry in self.program if entry.timeslot)
                return TimeSlot.objects.distinct().filter(id__in=timeslot_ids)
    
            @cached_property
            def episodes(self):
                return Note.objects.distinct().filter(timeslot__in=self.timeslots)
    
            @cached_property
            def profiles(self):
                return Profile.objects.distinct().filter(
                    Q(shows__in=self.shows) | Q(notes__in=self.episodes)
                )
    
            @property
            def categories(self):
                return Category.objects.distinct().filter(shows__in=self.shows)
    
            @property
            def funding_categories(self):
                return FundingCategory.objects.distinct().filter(shows__in=self.shows)
    
            @property
            def types(self):
                return Type.objects.distinct().filter(shows__in=self.shows)
    
            @property
            def topics(self):
                return Topic.objects.distinct().filter(
                    Q(shows__in=self.shows) | Q(episodes__in=self.episodes)
                )
    
            @property
            def languages(self):
                return Language.objects.distinct().filter(
                    Q(shows__in=self.shows) | Q(episodes__in=self.episodes)
                )
    
            @property
            def music_focuses(self):
                return MusicFocus.objects.distinct().filter(shows__in=self.shows)
    
            @cached_property
            def images(self):
                return Image.objects.distinct().filter(
                    Q(logo_shows__in=self.shows)
                    | Q(shows__in=self.shows)
                    | Q(profiles__in=self.profiles)
                    | Q(notes__in=self.episodes)
                )
    
            @property
            def licenses(self):
                return License.objects.distinct().filter(images__in=self.images)
    
            @property
            def link_types(self):
                return LinkType.objects.all()
    
    
        class CalendarTimeslotSerializer(TimeSlotSerializer):
            class Meta(TimeSlotSerializer.Meta):
                fields = [f for f in TimeSlotSerializer.Meta.fields if f != "memo"]
    
        class CalendarEpisodeSerializer(NoteSerializer):
            class Meta(NoteSerializer.Meta):
                fields = [
                    field
                    for field in NoteSerializer.Meta.fields
                    if field not in ["created_at", "created_by", "updated_at", "updated_by"]
                ]
    
    
        class CalendarProfileSerializer(ProfileSerializer):
            class Meta(ProfileSerializer.Meta):
                fields = [
                    field
                    for field in ProfileSerializer.Meta.fields
                    if field
                    not in [
                        "created_at",
                        "created_by",
                        "owner_ids",
                        "updated_at",
                        "updated_by",
                    ]
                ]
    
    
        class CalendarShowSerializer(ShowSerializer):
            class Meta(ShowSerializer.Meta):
                fields = [
                    field
                    for field in ShowSerializer.Meta.fields
                    if field
                    not in [
                        "created_at",
                        "created_by",
                        "internal_note",
                        "owner_ids",
                        "updated_at",
                        "updated_by",
                    ]
                ]
    
        shows = CalendarShowSerializer(many=True)
    
        timeslots = CalendarTimeslotSerializer(many=True)
    
        categories = CategorySerializer(many=True)
        funding_categories = FundingCategorySerializer(many=True)
        types = TypeSerializer(many=True)
        images = ImageSerializer(many=True)
        topics = TopicSerializer(many=True)
        languages = LanguageSerializer(many=True)
        music_focuses = MusicFocusSerializer(many=True)
        program = BasicProgramEntrySerializer(many=True)
    
        episodes = CalendarEpisodeSerializer(many=True)
    
        licenses = LicenseSerializer(many=True)
        link_types = LinkTypeSerializer(many=True)
    
    
    
    class ApplicationStatePurgeSerializer(serializers.Serializer):
        @staticmethod
        def _render_model_category_definitions():
            yield "<dl>"
            for category_name, models in application_state_manager.categorized_models.items():
                model_names = ", ".join(sorted(model._meta.label for model in models))
                yield f"<dt>{category_name}</dt>"
                yield f"<dd>{model_names}</dd>"
            yield "</dl>"
    
        models = serializers.MultipleChoiceField(
            choices=application_state_manager.model_choices, default=set()
        )
        model_categories = serializers.MultipleChoiceField(
            choices=application_state_manager.model_category_choices,
            default=set(),
            help_text=(
                "Selects multiple models by their categorization. "
                "Models included in the categories are: "
                f"{''.join(_render_model_category_definitions())}"
            ),
        )
        invert_selection = serializers.BooleanField(
            default=False,
            help_text=(
                "Inverts the model selection that is selected through other filters. "
                "Selects all models if set to true and no other filters have been set."
            ),
        )
    
    
    
    class PlaylistSerializer(serializers.ModelSerializer):
        class PlaylistEntrySerializer(serializers.ModelSerializer):
            class Meta:
                model = PlaylistEntry
                fields = (
                    "duration",
                    "file_id",
                    "uri",
                )
    
    
        entries = PlaylistEntrySerializer(many=True, required=False)
        show_id = serializers.PrimaryKeyRelatedField(
            queryset=Show.objects.all(),
            required=True,
            source="show",
        )
    
    
        class Meta:
            model = Playlist
            read_only_fields = (
                "id",
                "created_at",
                "created_by",
                "updated_at",
                "updated_by",
            )
            fields = (
                "description",
                "entries",
                "playout_mode",
                "show_id",
            ) + read_only_fields
    
    
        def create(self, validated_data):
    
            """Create a new Playlist instance, given the validated data.
    
            A ValidationError is raised if a playlist entry is invalid or if multiple null duration
            entries are present."""
    
    
            user = self.context["request"].user
            user_is_owner = user in validated_data.get("show").owners.all()
    
            # having the create_playlist permission overrules the ownership
            if not (user.has_perm("program.create_playlist") or user_is_owner):
                raise exceptions.PermissionDenied(detail="You are not allowed to create a playlist.")
    
            entries = validated_data.pop("entries", [])
    
    
            with transaction.atomic():
                playlist = Playlist.objects.create(created_by=user.username, **validated_data)
    
                for order, entry_data in enumerate(entries, start=1):
                    entry_data.update({"order": order})
    
    
                    try:
                        PlaylistEntry.objects.create(playlist=playlist, **entry_data)
                    except IntegrityError:
                        raise exceptions.ValidationError(
                            code="playlist-entry-file-id-or-uri",
                            detail="playlist entries must either have file id or uri.",
                        )
    
                if playlist.entries.filter(duration__isnull=True).count() > 1:
    
                        code="multiple-null-duration-playlist-entries",
                        detail="playlist may only have one entry without duration",
    
    
        def update(self, instance, validated_data):
    
            """Update an existing Playlist instance, given the validated data.
    
            A ValidationError is raised if a playlist entry is invalid or if multiple null duration
            entries are present."""
    
    
            user = self.context["request"].user
            user_is_owner = user in instance.show.owners.all()
    
            # having the update_playlist permission overrules the ownership
            if not (user.has_perm("program.update_playlist") or user_is_owner):
                raise exceptions.PermissionDenied(
                    detail="You are not allowed to update this playlist."
                )
    
    
            with transaction.atomic():
                if "description" in validated_data:
                    instance.description = validated_data.pop("description")
    
                if "playout_mode" in validated_data:
                    instance.playout_mode = validated_data.pop("playout_mode")
    
                if "entries" in validated_data:
                    if instance.entries.count() > 0:
                        for entry in instance.entries.all():
                            entry.delete(keep_parents=True)
    
                    for order, entry_data in enumerate(validated_data.get("entries"), start=1):
                        entry_data.update({"order": order})
    
    
                        try:
                            PlaylistEntry.objects.create(playlist=instance, **entry_data)
                        except IntegrityError:
                            raise exceptions.ValidationError(
                                code="playlist-entry-file-id-or-uri",
                                detail="playlist entries must either have file id or uri.",
                            )
    
                    if instance.entries.filter(duration__isnull=True).count() > 1:
    
                            code="multiple-null-duration-playlist-entries",
                            detail="playlist may only have one entry without duration",