Source code for pretalx.schedule.models.slot

# SPDX-FileCopyrightText: 2017-present Tobias Kunze
# SPDX-License-Identifier: AGPL-3.0-only WITH LicenseRef-Pretalx-AGPL-3.0-Terms
#
# This file contains Apache-2.0 licensed contributions copyrighted by the following contributors:
# SPDX-FileContributor: Franziska Kunsmann

import datetime as dt
import re
import string
import unicodedata
import uuid
from zoneinfo import ZoneInfo

from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField

from pretalx.agenda.rules import is_agenda_submission_visible, is_agenda_visible
from pretalx.common.models.fields import DateTimeField
from pretalx.common.models.mixins import PretalxModel
from pretalx.common.text.serialize import serialize_duration
from pretalx.common.urls import get_netloc
from pretalx.schedule.ical import get_slot_ical, patch_out_timezone_cache
from pretalx.submission.rules import is_break, is_wip, orga_can_change_submissions

INSTANCE_IDENTIFIER = None
WHITESPACE_REGEX = re.compile(r"\W+")
FRAB_SLUG_REGEX = re.compile(f"[^{string.ascii_letters + string.digits + '-'}]")


[docs] class TalkSlot(PretalxModel): """The TalkSlot object is the scheduled version of a. :class:`~pretalx.submission.models.submission.Submission`. TalkSlots always belong to one submission and one :class:`~pretalx.schedule.models.schedule.Schedule`. :param is_visible: This parameter is set on schedule release. Only confirmed talks will be visible. """ submission = models.ForeignKey( to="submission.Submission", on_delete=models.PROTECT, related_name="slots", null=True, blank=True, # If the submission is empty, this is a break or similar event ) room = models.ForeignKey( to="schedule.Room", on_delete=models.PROTECT, related_name="talks", verbose_name=_("Room"), help_text=_("The room this talk is scheduled in, if any"), null=True, blank=True, ) schedule = models.ForeignKey( to="schedule.Schedule", on_delete=models.PROTECT, related_name="talks" ) is_visible = models.BooleanField(default=False) start = DateTimeField( null=True, verbose_name=_("Start"), help_text=_("When the talk starts, if it is currently scheduled"), ) end = DateTimeField( null=True, verbose_name=_("End"), help_text=_("When the talk ends, if it is currently scheduled"), ) description = I18nCharField(null=True) objects = ScopedManager(event="schedule__event") class Meta: ordering = ("start",) rules_permissions = { "list": is_agenda_visible | orga_can_change_submissions, "view": ( # public view is only possible for non-wip slots ~is_wip # visibility then is down to the submission being visible in the # agenda or the slot being a break. further filtering for is_visible # is down to the API/view & ((is_break & is_agenda_visible) | is_agenda_submission_visible) ) | orga_can_change_submissions, "update": is_wip & orga_can_change_submissions, } def __str__(self): """Help when debugging.""" return f'TalkSlot(event={self.schedule.event.slug}, submission={getattr(self.submission, "title", None)}, schedule={self.schedule.version})' @cached_property def event(self): return self.submission.event if self.submission else self.schedule.event @property def duration(self) -> int: """Returns the actual duration in minutes if the talk is scheduled, and the planned duration in minutes otherwise.""" if self.start and self.end: return int((self.end - self.start).total_seconds() / 60) if not self.submission: return None return self.submission.get_duration() @cached_property def export_duration(self): return serialize_duration(minutes=self.duration) @cached_property def pentabarf_export_duration(self): duration = dt.timedelta(minutes=self.duration) days = duration.days hours = duration.total_seconds() // 3600 - days * 24 minutes = duration.seconds // 60 % 60 return f"{hours:02}{minutes:02}00" @cached_property def local_start(self): if self.start: return self.start.astimezone(self.event.tz) @cached_property def real_end(self): """Guaranteed to provide a useful end datetime if ``start`` is set, even if ``end`` is empty.""" return self.end or ( self.start + dt.timedelta(minutes=self.duration) if self.start else None ) @cached_property def local_end(self): if self.real_end: return self.real_end.astimezone(self.event.tz) @cached_property def as_availability(self): """'Casts' a slot as. :class:`~pretalx.schedule.models.availability.Availability`, useful for availability arithmetic. """ from pretalx.schedule.models import Availability return Availability( start=self.start, end=self.real_end, )
[docs] def copy_to_schedule(self, new_schedule, save=True): """Create a new slot for the given. :class:`~pretalx.schedule.models.schedule.Schedule` with all other fields identical to this one. """ new_slot = TalkSlot(schedule=new_schedule) for field in ( fn for fn in self._meta.fields if fn.name not in ("id", "schedule") ): setattr(new_slot, field.name, getattr(self, field.name)) if save: new_slot.save() return new_slot
copy_to_schedule.alters_data = True
[docs] def is_same_slot(self, other_slot) -> bool: """Checks if both slots have the same room and start time.""" return self.room == other_slot.room and self.start == other_slot.start
@cached_property def id_suffix(self): if not self.event.get_feature_flag("present_multiple_times"): return "" all_slots = list( TalkSlot.objects.filter( submission_id=self.submission_id, schedule_id=self.schedule_id ).order_by("start") ) if len(all_slots) == 1: return "" return "-" + str(all_slots.index(self)) @cached_property def frab_slug(self): title = re.sub(WHITESPACE_REGEX, "-", self.submission.title) title = title.lower() title = unicodedata.normalize("NFD", title).encode("ASCII", "ignore").decode() title = re.sub(FRAB_SLUG_REGEX, "", title) title = title.strip("-") if title: return f"{self.event.slug}-{self.submission.pk}{self.id_suffix}-{title}" return f"{self.event.slug}-{self.submission.pk}{self.id_suffix}" @cached_property def uuid(self): """A UUID5, calculated from the submission code and the instance identifier.""" global INSTANCE_IDENTIFIER if not INSTANCE_IDENTIFIER: from pretalx.common.models.settings import GlobalSettings INSTANCE_IDENTIFIER = GlobalSettings().get_instance_identifier() return uuid.uuid5(INSTANCE_IDENTIFIER, self.submission.code + self.id_suffix) def build_ical(self, calendar, creation_time=None, netloc=None): if not self.start or not self.local_end or not self.room or not self.submission: return creation_time = creation_time or dt.datetime.now(ZoneInfo("UTC")) netloc = netloc or get_netloc(self.event) with patch_out_timezone_cache(self.event.tz): vevent = calendar.add("vevent") vevent.add("summary").value = ( f"{self.submission.title} - {self.submission.display_speaker_names}" ) vevent.add("dtstamp").value = creation_time vevent.add("location").value = str(self.room.name) vevent.add("uid").value = "pretalx-{}-{}{}@{}".format( self.submission.event.slug, self.submission.code, self.id_suffix, netloc ) vevent.add("dtstart").value = self.local_start vevent.add("dtend").value = self.local_end vevent.add("description").value = self.submission.abstract or "" vevent.add("url").value = self.submission.urls.public.full() def full_ical(self): return get_slot_ical(self)