Newer
Older
#
# 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 typing import List, TypedDict
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,
from program.utils import delete_links, get_audio_url
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 ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = (
"cba_username",
"cba_user_token",
"created_at",
"created_by",
"updated_at",
"updated_by",
)
read_only_fields = (
"created_at",
"created_by",
"updated_at",
"updated_by",
)
class UserSerializer(serializers.ModelSerializer):
# Add profile fields to JSON
profile = ProfileSerializer(required=False)
class Meta:
model = User
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"])
if profile_data:
cba_username=profile_data.get("cba_username").strip(),
cba_user_token=profile_data.get("cba_user_token").strip(),
created_by=self.context.get("request").user.username,
user=user,
profile.save()
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 = UserProfile.objects.get(user=instance.id)
except ObjectDoesNotExist:
profile = UserProfile.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.updated_by = self.context.get("request").user.username
profile.save()
instance.save()
return instance
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ("description", "id", "is_active", "name", "slug", "subtitle")
class LinkTypeSerializer(serializers.ModelSerializer):
class Meta:
model = LinkType
fields = ("name", "type")
class LicenseSerializer(serializers.ModelSerializer):
model = License
fields = ("id", "name", "identifier")
class HostLinkSerializer(serializers.ModelSerializer):
class Meta:
model = HostLink
class PPOIField(serializers.CharField):
def validate_format(self, value: str):
if not re.match(r"\d(?:\.\d+)?x\d(?:\.\d+)?", value):
raise serializers.ValidationError("PPOI must match format: ${float}x${float}")
def __init__(self, **kwargs):
kwargs["max_length"] = 20
kwargs.setdefault("validators", [])
kwargs["validators"].append(self.validate_format)
super().__init__(**kwargs)
def to_representation(self, value: tuple[float, float]):
[left, top] = value
return f"{left}x{top}"
class Thumbnail(TypedDict):
width: float
height: float
url: str
class ImageSerializer(serializers.ModelSerializer):
license_id = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=License.objects.all(), required=False, source="license"
)
ppoi = PPOIField(required=False)
thumbnails = serializers.SerializerMethodField()
@staticmethod
def get_thumbnails(instance) -> List[Thumbnail]:
"""Returns thumbnails"""
thumbnails = []
if instance.image.name and THUMBNAIL_SIZES:
for size in THUMBNAIL_SIZES:
[width, height] = size.split("x")
thumbnails.append(
{
"width": int(width),
"height": int(height),
"url": instance.image.crop[size].url,
}
)
return thumbnails
class Meta:
model = Image
read_only_fields = (
"height",
"id",
"thumbnails",
"width",
)
fields = (
"alt_text",
"credits",
"image",
) + read_only_fields
def create(self, validated_data):
"""Create and return a new Image instance, given the validated data."""
image = Image.objects.create(
owner=self.context.get("request").user.username, **validated_data
)
image.save()
return image
def update(self, instance, validated_data):
"""Update and return an existing Image instance, given the validated data."""
instance.alt_text = validated_data.get("alt_text", instance.alt_text)
instance.credits = validated_data.get("credits", instance.credits)
instance.image.ppoi = validated_data.get("ppoi", instance.ppoi)
instance.license = validated_data.get("license", instance.license)
instance.save()
return instance
class HostSerializer(serializers.ModelSerializer):
image_id = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=Image.objects.all(), required=False, source="image"
links = HostLinkSerializer(many=True, required=False)
class Meta:
model = Host
read_only_fields = (
"created_at",
"created_by",
"updated_at",
"updated_by",
)
fields = (
"biography",
"email",
"id",
"image_id",
"is_active",
"links",
"name",
) + read_only_fields
def create(self, validated_data):
"""
Create and return a new Host instance, given the validated data.
"""
links_data = validated_data.pop("links", [])
validated_data["image"] = validated_data.pop("image_id", None)
host = Host.objects.create(
created_by=self.context.get("request").user.username, **validated_data
)
for link_data in links_data:
HostLink.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.biography = validated_data.get("biography", instance.biography)
instance.email = validated_data.get("email", instance.email)
instance.image = validated_data.get("image_id", instance.image)
instance.is_active = validated_data.get("is_active", instance.is_active)
instance.name = validated_data.get("name", instance.name)
if links_data := validated_data.get("links"):
for link_data in links_data:
HostLink.objects.create(host=instance, **link_data)
instance.updated_by = self.context.get("request").user.username
instance.save()
return instance
class LanguageSerializer(serializers.ModelSerializer):
class Meta:
model = Language
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = Topic
class MusicFocusSerializer(serializers.ModelSerializer):
class Meta:
model = MusicFocus
class TypeSerializer(serializers.ModelSerializer):

Ingo Leindecker
committed
class Meta:
model = Type

Ingo Leindecker
committed
class FundingCategorySerializer(serializers.ModelSerializer):

Ingo Leindecker
committed
class Meta:

Ingo Leindecker
committed
class ShowLinkSerializer(serializers.ModelSerializer):
class Meta:
model = ShowLink
class ShowSerializer(serializers.HyperlinkedModelSerializer):
category_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=Category.objects.all(), source="category"
)
funding_category_id = serializers.PrimaryKeyRelatedField(
queryset=FundingCategory.objects.all()
)
host_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=Host.objects.all(), source="hosts"
)
image_id = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=Image.objects.all(), required=False
)
language_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=Language.objects.all(), source="language"
links = HostLinkSerializer(many=True, required=False)
logo_id = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=Image.objects.all(), required=False
)
music_focus_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=MusicFocus.objects.all(), source="music_focus"
owner_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=User.objects.all(), source="owners"
predecessor_id = serializers.PrimaryKeyRelatedField(
allow_null=True, queryset=Show.objects.all(), required=False
)
topic_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=Topic.objects.all(), source="topic"
)
type_id = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all())
class Meta:
model = Show
read_only_fields = (
"created_at",
"created_by",
"updated_at",
"updated_by",
)
"funding_category_id",
"host_ids",
"logo_id",
"music_focus_ids",
"owner_ids",
"predecessor_id",
"topic_ids",
"type_id",
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")
links_data = validated_data.pop("links", [])
# required
validated_data["funding_category"] = validated_data.pop("funding_category_id")
validated_data["type"] = validated_data.pop("type_id")
# optional
validated_data["image"] = validated_data.pop("image_id", None)
validated_data["logo"] = validated_data.pop("logo_id", None)
validated_data["predecessor"] = validated_data.pop("predecessor_id", None)
show = Show.objects.create(
created_by=self.context.get("request").user.username,
**validated_data,
# Save many-to-many relationships

jackie / Andrea Ida Malkah Klaura
committed
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)
for link_data in links_data:
ShowLink.objects.create(show=show, **link_data)
def update(self, instance, validated_data):
"""
Update and return an existing Show instance, given the validated data.
"""
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.description = validated_data.get("description", instance.description)
instance.email = validated_data.get("email", instance.email)
instance.funding_category = validated_data.get(
"funding_category_id", instance.funding_category
instance.image = validated_data.get("image_id", instance.image)
instance.internal_note = validated_data.get("internal_note", instance.internal_note)
instance.is_active = validated_data.get("is_active", instance.is_active)
instance.is_public = validated_data.get("is_public", instance.is_public)
instance.logo = validated_data.get("logo_id", instance.logo)
instance.name = validated_data.get("name", instance.name)
instance.predecessor = validated_data.get("predecessor_id", instance.predecessor)
instance.short_description = validated_data.get(
"short_description", instance.short_description
)
instance.slug = validated_data.get("slug", instance.slug)
instance.type = validated_data.get("type_id", instance.type)
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.music_focus.set(validated_data.get("music_focus", instance.music_focus))
instance.owners.set(validated_data.get("owners", instance.owners))
instance.topic.set(validated_data.get("topic", instance.topic))
if links_data := validated_data.get("links"):
for link_data in links_data:
ShowLink.objects.create(show=instance, **link_data)
instance.updated_by = self.context.get("request").user.username
instance.save()
return instance
class RRuleSerializer(serializers.ModelSerializer):
class Meta:
model = RRule
fields = (
"id",
"name",
)
read_only_fields = fields
class ScheduleSerializer(serializers.ModelSerializer):
class Meta:
model = Schedule
"rrule_id",
"show_id",
"start_time",
)
class UnsavedScheduleSerializer(ScheduleSerializer):
id = serializers.IntegerField(allow_null=True)
class ScheduleInRequestSerializer(ScheduleSerializer):
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."
),
)
"default_playlist_id",
"dryrun",
"end_time",
"rrule_id",
"show_id",
"start_time",
)
def create(self, validated_data):
"""Create and return a new Schedule instance, given the validated data."""
validated_data["rrule"] = validated_data.pop("rrule_id")
validated_data["show"] = validated_data.pop("show_id")
schedule = Schedule.objects.create(**validated_data)
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("by_weekday", instance.by_weekday)
instance.first_date = validated_data.get("first_date", instance.first_date)
instance.start_time = validated_data.get("start_time", instance.start_time)
instance.end_time = validated_data.get("end_time", instance.end_time)
instance.last_date = validated_data.get("last_date", 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_id", instance.rrule)
instance.show = validated_data.get("show_id", 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
)
class CollisionSerializer(serializers.Serializer):
id = serializers.IntegerField()
start = serializers.DateTimeField()
end = serializers.DateTimeField()
playlist_id = serializers.IntegerField(allow_null=True)
show_id = serializers.IntegerField()
show_name = serializers.CharField()
repetition_of_id = serializers.IntegerField(allow_null=True)
schedule_id = 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_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()
solutions = serializers.DictField(
child=serializers.ChoiceField(SOLUTION_CHOICES), required=False
)
notes = serializers.DictField(child=serializers.IntegerField(), required=False)
playlists = serializers.DictField(child=serializers.IntegerField(), required=False)
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
)
repetition_of_id = serializers.PrimaryKeyRelatedField(
allow_null=True,
queryset=TimeSlot.objects.all(),
required=False,
)
class Meta:
model = TimeSlot
"id",
"note_id",
"schedule_id",
"show_id",
fields = (
"memo",
"playlist_id",
"repetition_of_id",
@staticmethod
def get_show_id(obj):
return obj.schedule.show.id
@staticmethod
def get_note_id(obj):
return obj.note.id if hasattr(obj, "note") else None
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.repetition_of = validated_data.get("repetition_of_id", instance.repetition_of)
instance.playlist_id = validated_data.get("playlist_id", instance.playlist_id)
instance.save()
return instance
class NoteLinkSerializer(serializers.ModelSerializer):
class Meta:
model = NoteLink
class NoteSerializer(serializers.ModelSerializer):
contributor_ids = serializers.PrimaryKeyRelatedField(
many=True, queryset=Host.objects.all(), source="contributors"
)
image_id = serializers.PrimaryKeyRelatedField(
queryset=Image.objects.all(), required=False, allow_null=True
)
links = NoteLinkSerializer(many=True, required=False)
playlist_id = serializers.IntegerField(required=False)
timeslot_id = serializers.PrimaryKeyRelatedField(
queryset=TimeSlot.objects.all(), required=False
)
class Meta:
model = Note
"created_at",
"created_by",
"updated_at",
"updated_by",
)
"contributor_ids",
"owner_id",
"playlist_id",
"slug",
"summary",
def create(self, validated_data):
"""Create and return a new Note instance, given the validated data."""
links_data = validated_data.pop("links", [])
contributors = validated_data.pop("contributors", [])

Ernesto Rico Schmidt
committed
if "timeslot_id" in validated_data:
validated_data["timeslot"] = validated_data.pop("timeslot_id")
else:
# TODO: Once we remove nested routes, this hack should be removed
timeslot_pk = TimeSlot.objects.get(pk=self.context["request"].path.split("/")[-3])
validated_data["timeslot"] = validated_data.pop("timeslot_id", timeslot_pk)
# optional
validated_data["image"] = validated_data.pop("image_id", None)
# the creator of the note is the owner
note = Note.objects.create(
created_by=self.context.get("request").user.username,
owner=self.context.get("request").user,
**validated_data,
note.contributors.set(contributors)
if cba_id := validated_data.get("cba_id"):
if audio_url := get_audio_url(cba_id):
NoteLink.objects.create(note=note, type="CBA", url=audio_url)
for link_data in links_data:
NoteLink.objects.create(note=note, **link_data)
note.save()
def update(self, instance, validated_data):
"""Update and return an existing Note instance, given the validated data."""
instance.cba_id = validated_data.get("cba_id", instance.cba_id)
instance.content = validated_data.get("content", instance.content)

Ernesto Rico Schmidt
committed
instance.image = validated_data.get("image_id", instance.image)
instance.slug = validated_data.get("slug", instance.slug)
instance.summary = validated_data.get("summary", instance.summary)
instance.tags = validated_data.get("tags", instance.tags)

Ernesto Rico Schmidt
committed
instance.timeslot = validated_data.get("timeslot_id", instance.timeslot)
instance.title = validated_data.get("title", instance.title)
instance.contributors.set(validated_data.get("contributors", instance.contributors))
if cba_id := validated_data.get("cba_id"):
if audio_url := get_audio_url(cba_id):
NoteLink.objects.create(note=instance, type="CBA", url=audio_url)
if links_data := validated_data.get("links"):
for link_data in links_data:
NoteLink.objects.create(note=instance, **link_data)
instance.updated_by = self.context.get("request").user.username
instance.save()

jackie / Andrea Ida Malkah Klaura
committed
return instance