# # 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 datetime import datetime from rest_framework.exceptions import ValidationError from versatileimagefield.fields import PPOIField, VersatileImageField from django.contrib.auth.models import User from django.db import models from django.db.models import Q, QuerySet from django.utils.translation import gettext_lazy as _ from program.utils import parse_datetime from steering.settings import THUMBNAIL_SIZES class ScheduleConflictError(ValidationError): def __init__(self, *args, conflicts=None, **kwargs): super().__init__(*args, **kwargs) self.conflicts = conflicts class Type(models.Model): is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) class Meta: ordering = ("name",) def __str__(self): return self.name class Category(models.Model): description = models.TextField(blank=True) is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) subtitle = models.CharField(blank=True, max_length=32) class Meta: ordering = ("name",) verbose_name_plural = "Categories" def __str__(self): return self.name class Topic(models.Model): is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) class Meta: ordering = ("name",) def __str__(self): return self.name class MusicFocus(models.Model): is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) class Meta: ordering = ("name",) verbose_name_plural = "Music Focus" def __str__(self): return self.name class FundingCategory(models.Model): is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) slug = models.SlugField(max_length=32, unique=True) class Meta: ordering = ("name",) verbose_name_plural = "Funding Categories" def __str__(self): return self.name class Language(models.Model): is_active = models.BooleanField(default=True) name = models.CharField(max_length=32) class Meta: ordering = ("name",) def __str__(self): return self.name class License(models.Model): identifier = models.CharField(max_length=32, help_text="Identifier of the license") name = models.CharField(max_length=64, help_text="Name of the license") needs_author = models.BooleanField(default=True) requires_express_permission_for_publication = models.BooleanField(default=True) url = models.URLField(default="", blank=True) class Meta: ordering = ("name",) def __str__(self): return self.identifier class Image(models.Model): alt_text = models.TextField(blank=True, default="") credits = models.TextField(blank=True, default="") is_use_explicitly_granted_by_author = models.BooleanField(default=False) height = models.PositiveIntegerField(blank=True, null=True) image = VersatileImageField( blank=True, height_field="height", null=True, ppoi_field="ppoi", upload_to="images", width_field="width", ) license = models.ForeignKey( License, null=True, on_delete=models.SET_NULL, related_name="images" ) owner = models.CharField(max_length=150) ppoi = PPOIField() width = models.PositiveIntegerField(blank=True, null=True) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.image.name and THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES: self.image.thumbnail = self.image.crop[size].name def delete(self, using=None, keep_parents=False): self.image.delete_all_created_images() self.image.delete(save=False) super().delete(using, keep_parents) class Host(models.Model): biography = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=150) email = models.EmailField(blank=True) image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="hosts") is_active = models.BooleanField(default=True) name = models.CharField(max_length=128) owners = models.ManyToManyField(User, blank=True, related_name="hosts") updated_at = models.DateTimeField(auto_now=True, blank=True, null=True) updated_by = models.CharField(blank=True, default="", max_length=150) class Meta: ordering = ("name",) def __str__(self): return self.name class LinkType(models.Model): name = models.CharField(max_length=32, help_text="Name of the link type") type = models.CharField(max_length=32, help_text="Type of the link") class Meta: ordering = ("name",) def __str__(self): return self.type class Link(models.Model): type = models.CharField(max_length=64) url = models.URLField() class Meta: abstract = True def __str__(self): return self.url class HostLink(Link): host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name="links") class Show(models.Model): category = models.ManyToManyField(Category, blank=True, related_name="shows") cba_series_id = models.IntegerField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=150) default_playlist_id = models.IntegerField(blank=True, null=True) description = models.TextField(blank=True) email = models.EmailField(blank=True, null=True) funding_category = models.ForeignKey( FundingCategory, blank=True, null=True, on_delete=models.CASCADE, related_name="shows" ) hosts = models.ManyToManyField(Host, blank=True, related_name="shows") image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="shows") internal_note = models.TextField(blank=True) is_active = models.BooleanField(default=True) is_public = models.BooleanField(default=False) language = models.ManyToManyField(Language, blank=True, related_name="shows") # TODO: is this really necessary? logo = models.ForeignKey( Image, blank=True, null=True, on_delete=models.CASCADE, related_name="logo_shows" ) music_focus = models.ManyToManyField(MusicFocus, blank=True, related_name="shows") name = models.CharField(max_length=255) owners = models.ManyToManyField(User, blank=True, related_name="shows") predecessor = models.ForeignKey( "self", blank=True, null=True, on_delete=models.CASCADE, related_name="successors" ) short_description = models.TextField() slug = models.CharField(max_length=255, unique=True) topic = models.ManyToManyField(Topic, blank=True, related_name="shows") type = models.ForeignKey( Type, blank=True, null=True, on_delete=models.CASCADE, related_name="shows" ) updated_at = models.DateTimeField(auto_now=True, blank=True, null=True) updated_by = models.CharField(blank=True, default="", max_length=150) class Meta: ordering = ("slug",) def __str__(self): return self.name class ShowLink(Link): show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name="links") class RRule(models.Model): by_set_pos = models.IntegerField( blank=True, choices=[ (1, "first"), (2, "second"), (3, "third"), (4, "fourth"), (5, "fifth"), (-1, "last"), ], null=True, ) by_weekdays = models.CharField( blank=True, choices=[ (None, ""), ("0,1,2,3,4", "business days"), ("5,6", "weekends"), ], null=True, max_length=9, ) count = models.IntegerField( blank=True, null=True, help_text="How many occurrences should be generated.", ) freq = models.IntegerField( choices=[ (0, "once"), (1, "monthly"), (2, "weekly"), (3, "daily"), ] ) interval = models.IntegerField( default=1, help_text="The interval between each freq iteration.", ) name = models.CharField(max_length=32, unique=True) class Meta: ordering = ("pk",) unique_together = ("freq", "interval", "by_set_pos", "by_weekdays") verbose_name = _("recurrence rule") def __str__(self): return self.name class Schedule(models.Model): add_business_days_only = models.BooleanField( default=False, help_text=( "Whether to add add_days_no but skipping the weekends. " "E.g. if weekday is Friday, the date returned will be the next Monday." ), ) add_days_no = models.IntegerField( blank=True, null=True, help_text=( "Add a number of days to the generated dates. " "This can be useful for repetitions, like 'On the following day'." ), ) by_weekday = models.IntegerField( help_text="Number of the Weekday.", choices=[ (0, "Monday"), (1, "Tuesday"), (2, "Wednesday"), (3, "Thursday"), (4, "Friday"), (5, "Saturday"), (6, "Sunday"), ], null=True, ) default_playlist_id = models.IntegerField( blank=True, null=True, help_text="A tank ID in case the timeslot's playlist_id is empty.", ) end_time = models.TimeField(null=True, help_text="End time of schedule.") first_date = models.DateField(help_text="Start date of schedule.") is_repetition = models.BooleanField( default=False, help_text="Whether the schedule is a repetition.", ) last_date = models.DateField(help_text="End date of schedule.", null=True) rrule = models.ForeignKey( RRule, help_text="A recurrence rule.", on_delete=models.CASCADE, related_name="schedules" ) show = models.ForeignKey( Show, help_text="Show the schedule belongs to.", on_delete=models.CASCADE, related_name="schedules", ) start_time = models.TimeField(help_text="Start time of schedule.") class Meta: ordering = ("first_date", "start_time") class TimeSlotManager(models.Manager): @staticmethod def instantiate(start, end, schedule): return TimeSlot( start=parse_datetime(start), end=parse_datetime(end), schedule=schedule, ) @staticmethod def get_timerange_timeslots(start_timerange: datetime, end_timerange: datetime) -> QuerySet: """get the timeslots between start_timerange and end_timerange""" return TimeSlot.objects.filter( # start before start_timerange, end after start_timerange Q(start__lt=start_timerange, end__gt=start_timerange) # start after/at start_timerange, end before/at end_timerange | Q(start__gte=start_timerange, end__lte=end_timerange) # start before end_timerange, end after/at end_timerange | Q(start__lt=end_timerange, end__gte=end_timerange) ) @staticmethod def get_colliding_timeslots(timeslot): return TimeSlot.objects.filter( (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end)) | (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end)) | (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end)) | (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end)) ) class TimeSlot(models.Model): end = models.DateTimeField() language = models.ManyToManyField(Language, blank=True, related_name="timeslots") memo = models.TextField(blank=True) playlist_id = models.IntegerField(null=True) repetition_of = models.ForeignKey( "self", blank=True, null=True, on_delete=models.CASCADE, related_name="repetitions" ) schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name="timeslots") start = models.DateTimeField() topic = models.ManyToManyField(Topic, blank=True, related_name="timeslots") objects = TimeSlotManager() class Meta: ordering = ("start", "end") def __str__(self): if self.start.date() == self.end.date(): time_span = "{0}, {1} - {2}".format( self.start.strftime("%x"), self.start.strftime("%X"), self.end.strftime("%X"), ) else: time_span = "{0} - {1}".format( self.start.strftime("%X %x"), self.end.strftime("%X %x"), ) return f"{str(self.schedule.show)} ({time_span})" @property def hash(self): string = ( str(self.start) + str(self.end) + str(self.schedule.rrule.id) + str(self.schedule.by_weekday) ) return str("".join(s for s in string if s.isdigit())) class Note(models.Model): cba_id = models.IntegerField(blank=True, null=True) content = models.TextField() contributors = models.ManyToManyField(Host, related_name="notes") created_at = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=150) image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="notes") owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notes", default=1) playlist = models.TextField(blank=True) slug = models.SlugField(max_length=32, unique=True) summary = models.TextField(blank=True) tags = models.TextField(blank=True) timeslot = models.OneToOneField(TimeSlot, null=True, on_delete=models.SET_NULL, unique=True) title = models.CharField(max_length=128) updated_at = models.DateTimeField(auto_now=True, blank=True, null=True) updated_by = models.CharField(blank=True, default="", max_length=150) class Meta: ordering = ("timeslot",) def __str__(self): return self.title class NoteLink(Link): note = models.ForeignKey(Note, on_delete=models.CASCADE, related_name="links") class UserProfile(models.Model): cba_username = models.CharField("CBA Username", blank=True, max_length=60) cba_user_token = models.CharField("CBA Token", blank=True, max_length=255) created_at = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=150) updated_at = models.DateTimeField(auto_now=True, blank=True, null=True) updated_by = models.CharField(blank=True, default="", max_length=150) user = models.OneToOneField(User, null=True, on_delete=models.SET_NULL, related_name="profile") def __str__(self): return self.user.username