import datetime as dt
import re
import string
import uuid
from contextlib import suppress
from urllib.parse import urlparse
import pytz
from django.db import models
from django.utils.functional import cached_property
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField
from pretalx.common.mixins.models import LogMixin
from pretalx.common.urls import get_base_url
INSTANCE_IDENTIFIER = None
with suppress(Exception):
from pretalx.common.models.settings import GlobalSettings
INSTANCE_IDENTIFIER = GlobalSettings().get_instance_identifier()
[docs]class TalkSlot(LogMixin, models.Model):
"""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",
null=True,
blank=True,
)
schedule = models.ForeignKey(
to="schedule.Schedule", on_delete=models.PROTECT, related_name="talks"
)
is_visible = models.BooleanField(default=False)
start = models.DateTimeField(null=True)
end = models.DateTimeField(null=True)
description = I18nCharField(null=True)
updated = models.DateTimeField(auto_now=True)
objects = ScopedManager(event="schedule__event")
class Meta:
ordering = ("start",)
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):
from pretalx.common.serialize import serialize_duration
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 [f for f in self._meta.fields if f.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.feature_flags["present_multiple_times"]:
return ""
all_slots = list(
TalkSlot.objects.filter(
submission_id=self.submission_id, schedule_id=self.schedule_id
)
)
if len(all_slots) == 1:
return ""
return "-" + str(all_slots.index(self))
@cached_property
def frab_slug(self):
title = re.sub(r"\W+", "-", self.submission.title)
legal_chars = string.ascii_letters + string.digits + "-"
pattern = f"[^{legal_chars}]+"
title = re.sub(pattern, "", title)
title = title.lower()
title = title.strip("_")
return f"{self.event.slug}-{self.submission.pk}{self.id_suffix}-{title}"
@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(pytz.utc)
netloc = netloc or urlparse(get_base_url(self.event)).netloc
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()