# SPDX-FileCopyrightText: 2017-present Tobias Kunze
# SPDX-License-Identifier: AGPL-3.0-only WITH LicenseRef-Pretalx-AGPL-3.0-Terms
import uuid
from django.conf import settings
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretalx.agenda.rules import can_view_schedule, is_speaker_viewable
from pretalx.common.models.fields import MarkdownField
from pretalx.common.models.mixins import GenerateCode, PretalxModel
from pretalx.common.models.settings import GlobalSettings
from pretalx.common.text.phrases import phrases
from pretalx.common.urls import EventUrls
from pretalx.orga.rules import can_view_speaker_names
from pretalx.person.enums import SpeakerProfileOrigin
from pretalx.person.models.picture import ProfilePictureMixin
from pretalx.person.rules import (
can_mark_speakers_arrived,
is_administrator,
is_deletable_speaker_profile,
is_reviewer,
)
from pretalx.schedule.models import Availability
from pretalx.submission.rules import orga_can_change_submissions
[docs]
class SpeakerProfile(ProfilePictureMixin, GenerateCode, PretalxModel):
"""A speaker in a specific event.
If a speaker has no user, it is "managed". If a speaker has a
user, empty fields fall back on the corresponding user fields.
"""
code_scope = ("event",)
user = models.ForeignKey(
to="person.User",
related_name="profiles",
on_delete=models.CASCADE,
null=True,
blank=True,
)
event = models.ForeignKey(
to="event.Event", related_name="+", on_delete=models.CASCADE
)
name = models.CharField(
max_length=120, null=True, blank=True, verbose_name=_("Name")
)
code = models.CharField(max_length=16)
email = models.EmailField(null=True, blank=True, verbose_name=_("Contact email"))
locale = models.CharField(
max_length=32,
null=True,
blank=True,
choices=settings.LANGUAGES,
verbose_name=_("Preferred language"),
)
invitation_token = models.CharField(
max_length=64, null=True, blank=True, unique=True
)
invitation_sent = models.DateTimeField(null=True, blank=True)
origin = models.CharField(
max_length=8,
choices=SpeakerProfileOrigin.choices,
default=SpeakerProfileOrigin.CFP,
)
guid = models.CharField(max_length=36, editable=False)
biography = MarkdownField(verbose_name=_("Biography"), null=True, blank=True)
has_arrived = models.BooleanField(
default=False, verbose_name=_("The speaker has arrived")
)
internal_notes = models.TextField(
null=True,
blank=True,
verbose_name=phrases.base.internal_notes,
help_text=phrases.base.internal_notes_help,
)
profile_picture = models.ForeignKey(
"person.ProfilePicture",
verbose_name=_("Profile picture"),
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="speakers",
)
log_prefix = "pretalx.user.profile"
class Meta:
unique_together = (("event", "code"), ("event", "user"))
# These permissions largely apply to event-scoped user actions
rules_permissions = {
"list": can_view_schedule | (is_reviewer & can_view_speaker_names),
"reviewer_list": is_reviewer & can_view_speaker_names,
"orga_list": orga_can_change_submissions
| (is_reviewer & can_view_speaker_names),
"view": is_speaker_viewable
| orga_can_change_submissions
| (is_reviewer & can_view_speaker_names),
"orga_view": orga_can_change_submissions
| (is_reviewer & can_view_speaker_names),
"create": is_administrator,
"update": orga_can_change_submissions,
"mark_arrived": orga_can_change_submissions & can_mark_speakers_arrived,
"delete": (is_administrator | orga_can_change_submissions)
& is_deletable_speaker_profile,
}
class urls(EventUrls):
public = "{self.event.urls.base}speaker/{self.code}/"
social_image = "{public}og-image"
talks_ical = "{public}talks.ics"
invitation = "{self.event.urls.base}invite/speaker/{self.invitation_token}/"
class orga_urls(EventUrls):
base = "{self.event.orga_urls.speakers}{self.code}/"
password_reset = "{base}reset" # noqa: S105 -- URL pattern, not a password
toggle_arrived = "{base}toggle-arrived"
invite = "{base}invite"
retract_invitation = "{base}invite/retract"
delete = "{base}delete"
send_mail = "{self.event.orga_urls.compose_mails_sessions}?speakers={self.code}"
def __str__(self):
"""Help when debugging."""
return (
f"SpeakerProfile(event={self.event.slug}, user={self.get_display_name()})"
)
def get_display_name(self, allow_empty=False):
name = self.name or (self.user.name if self.user else None)
if name or allow_empty:
return name or ""
return str(_("Unnamed speaker"))
def assign_code(self, length=None):
super().assign_code(length=length)
self.guid = self.compute_guid()
def save(self, *args, **kwargs):
if not self.guid:
new_fields = {"guid"}
if not self.user_id and not self.code:
new_fields.add(self.code_property)
else:
self.guid = self.compute_guid()
if update_fields := kwargs.get("update_fields"):
kwargs["update_fields"] = new_fields.union(update_fields)
return super().save(*args, **kwargs)
def compute_guid(self) -> str | None:
prefix = None
code = None
if self.user_id:
prefix = "user"
code = self.user.code
if not code:
prefix = "speaker"
code = self.code
if not code:
# code is always set except for unsaved objects
return None
return str(
uuid.uuid5(GlobalSettings().get_instance_identifier(), f"{prefix}:{code}")
)
@property
def is_managed(self) -> bool:
return self.user_id is None
@property
def has_pending_invitation(self) -> bool:
return self.is_managed and bool(self.invitation_token)
@property
def effective_email(self) -> str | None:
return self.email or (self.user.email if self.user_id else None)
@property
def effective_locale(self) -> str:
locale = self.locale or (self.user.locale if self.user_id else None)
if locale and locale in self.event.locales:
return locale
return self.event.locale
@cached_property
def talks(self):
"""A queryset of.
:class:`~pretalx.submission.models.submission.Submission` objects.
Contains all visible talks by this user on this event.
"""
return self.event.talks.filter(speakers=self)
@cached_property
def current_talk_slots(self):
from pretalx.person.domain.queries.profile import ( # noqa: PLC0415 -- thin method
visible_talk_slots,
)
return visible_talk_slots(self)
def get_instance_data(self):
data = {}
if not self._state.adding:
data = {
"name": self.name or (self.user.name if self.user else None),
"email": self.email,
"user_email": self.user.email if self.user else None,
"profile_picture": (
self.profile_picture.avatar.name
if self.profile_picture_id and self.profile_picture.avatar
else None
),
}
result = super().get_instance_data() | data
result.pop("invitation_token", None)
return result
@cached_property
def full_availability(self):
return Availability.union(self.availabilities.all())