module
pretalx.person.domain.profile
Functions 9
adopt_profile_picture(profile, user)source
def adopt_profile_picture(profile, user):
picture = profile.profile_picture
if not picture:
return
if picture.user_id is None:
picture.user = user
picture.save(update_fields=["user"])
if not user.profile_picture_id:
user.profile_picture = picture
user.save(update_fields=["profile_picture"])
apply_speaker_profile_changes(profile, changed_fields, *, old_email=None)sourceRun the side-effects keyed off the fields a caller just persisted on a speaker profile.
def apply_speaker_profile_changes(profile, changed_fields, *, old_email=None):
"""Run the side-effects keyed off the fields a caller just persisted on
a speaker profile.
"""
changed_fields = set(changed_fields)
if "email" in changed_fields and profile.invitation_token:
profile.invitation_token = None
profile.invitation_sent = None
profile.save(update_fields=["invitation_token", "invitation_sent"])
profile.log_action(
"pretalx.speaker.invite.invalidate",
orga=True,
data={"email": old_email, "new_email": profile.effective_email},
)
user = profile.user
if not user:
return
if "name" in changed_fields and profile.name and not user.name:
user.name = profile.name
user.save(update_fields=["name"])
claim_speaker_profile(profile, user)source
@transaction.atomic
def claim_speaker_profile(profile, user):
profile.user = user
profile.invitation_token = None
profile.save(update_fields=["user", "invitation_token"])
adopt_profile_picture(profile, user)
profile.log_action("pretalx.speaker.claim", person=user)
return profile
create_speaker_profile(event, *, name=None, email=None, locale=None, log_user=None)sourceCreate a managed, organiser-created speaker profile.
Invitation sending must be handled separately.
def create_speaker_profile(event, *, name=None, email=None, locale=None, log_user=None):
"""Create a managed, organiser-created speaker profile.
Invitation sending must be handled separately."""
if not email and not name:
raise ValueError("create_speaker_profile() requires an email or a name")
profile = SpeakerProfile(
event=event,
name=name or None,
email=email or None,
locale=locale or None,
origin=SpeakerProfileOrigin.ORGA,
)
profile.save()
profile.log_action(
"pretalx.speaker.create",
person=log_user,
orga=True,
data={"email": profile.effective_email, "name": profile.get_display_name()},
)
return profile
merge_speaker_profiles(merged, survivor, *, choices, user=None)source
@transaction.atomic
def merge_speaker_profiles(merged, survivor, *, choices, user=None):
log_data = {"merged_code": merged.code, "choices": dict(choices)}
for field in MERGE_PROFILE_FIELDS:
if choices.get(field) == "merged":
setattr(survivor, field, getattr(merged, field))
if merged.internal_notes:
log_data["internal_notes_merged"] = True
if survivor.internal_notes:
survivor.internal_notes = (
f"{survivor.internal_notes}\n\n{merged.internal_notes}"
)
else:
survivor.internal_notes = merged.internal_notes
if merged.has_arrived and not survivor.has_arrived:
survivor.has_arrived = True
log_data["has_arrived_merged"] = True
discarded_picture = merged.profile_picture
if choices.get("picture") == "merged" and merged.profile_picture:
discarded_picture = survivor.profile_picture
survivor.profile_picture = merged.profile_picture
merged.profile_picture = None
merged.save(update_fields=["profile_picture"])
adopt_profile_picture(survivor, survivor.user)
if discarded_picture:
# Bump the timestamp so the regular file cleanup picks it up.
discarded_picture.save(update_fields=["updated"])
if choices.get("availability") == "merged":
survivor.availabilities.all().delete()
merged.availabilities.update(person=survivor)
else:
merged.availabilities.all().delete()
survivor_answers = {
answer.question_id: answer
for answer in survivor.answers.select_related("question", "speaker__event")
}
for answer in merged.answers.select_related("question", "speaker__event"):
choice = choices.get(f"question_{answer.question_id}")
existing = survivor_answers.get(answer.question_id)
if choice == "merged" or (choice is None and existing is None):
if existing:
existing.delete()
answer.speaker = survivor
answer.save(update_fields=["speaker"])
else:
answer.delete()
survivor_submissions = set(
SpeakerRole.objects.filter(speaker=survivor).values_list(
"submission_id", flat=True
)
)
for role in SpeakerRole.objects.filter(speaker=merged):
if role.submission_id in survivor_submissions:
role.delete()
else:
role.speaker = survivor
role.save(update_fields=["speaker"])
merged.feedback.update(speaker=survivor)
for mail in merged.mails.all():
mail.to_speakers.add(survivor)
mail.to_speakers.remove(merged)
merged.delete()
survivor.save()
survivor.log_action("pretalx.speaker.merge", person=user, data=log_data)
return survivor
profile_deletable_after_removal(profile, submission, *, user)sourceWhether removing the given profile from a submission leaves a profile that the user user may delete — i.e.
a managed profile whose only session this was.
def profile_deletable_after_removal(profile, submission, *, user):
"""Whether removing the given profile from a submission leaves a profile
that the user user may delete — i.e. a managed profile whose only session
this was."""
if not profile.is_managed:
return False
profile.submission_count = profile.submissions.exclude(pk=submission.pk).count()
return profile.submission_count == 0 and user.has_perm(
"person.delete_speakerprofile", profile
)
retract_speaker_invite(profile, *, log_user=None)source
def retract_speaker_invite(profile, *, log_user=None):
if not profile.invitation_token:
return
email = profile.effective_email
profile.invitation_token = None
profile.save(update_fields=["invitation_token"])
profile.log_action(
"pretalx.speaker.invite.retract",
person=log_user,
orga=True,
data={"email": email},
)
send_speaker_invite(profile, *, subject, text, submission=None, log_user=None)sourceSet (or rotate) the invite token and dispatch the mail.
def send_speaker_invite(profile, *, subject, text, submission=None, log_user=None):
"""Set (or rotate) the invite token and dispatch the mail."""
if profile.user_id:
raise SendMailException(
_("This speaker already has an account and needs no invitation.")
)
if not profile.effective_email:
raise SendMailException(
_(
"This speaker has no contact email address, so no invitation can be sent."
)
)
if not subject or not text:
raise ValueError("send_speaker_invite() requires subject and text")
locale = profile.effective_locale
if submission:
locale = submission.get_email_locale(locale)
context_kwargs = {"user": Recipient(profile)}
if submission:
context_kwargs["submission"] = submission
old_token, old_sent = profile.invitation_token, profile.invitation_sent
profile.invitation_token = get_random_string(32)
profile.invitation_sent = now()
try:
mail = render_to_mail(
subject_template=subject,
text_template=text,
event=profile.event,
locale=locale,
context_kwargs=context_kwargs,
safe_extra_context={"invitation_link": profile.urls.invitation},
)
except (SendMailException, ValueError) as error:
profile.invitation_token, profile.invitation_sent = old_token, old_sent
if isinstance(error, ValueError):
raise SendMailException(f"Invalid invitation text: {error!s}") from error
raise
profile.save(update_fields=["invitation_token", "invitation_sent"])
save_draft(
mail, to_speakers=[profile], submissions=[submission] if submission else None
)
send_draft(mail, requestor=log_user)
profile.log_action(
"pretalx.speaker.invite.send",
person=log_user,
orga=True,
data={"email": profile.effective_email},
)
return mail
shred_speaker_profile(profile, *, user=None)sourceDelete a submission-less speaker profile.
@transaction.atomic
def shred_speaker_profile(profile, *, user=None):
"""Delete a submission-less speaker profile."""
if not profile.is_managed:
raise ValueError("Only managed speaker profiles can be deleted this way.")
if profile.submissions.exists():
raise ValueError("Speaker profiles with submissions cannot be deleted.")
data = {
"code": profile.code,
"name": profile.get_display_name(),
"email": profile.effective_email,
}
event = profile.event
profile.mails.all().delete()
for answer in profile.answers.select_related("question", "speaker__event"):
answer.delete() # iterate to delete answer files too
profile.feedback.all().delete()
profile.logged_actions().delete()
if picture := profile.profile_picture:
# Bump the timestamp so the regular file cleanup picks it up.
picture.save(update_fields=["updated"])
profile.delete()
event.log_action("pretalx.speaker.delete", person=user, orga=True, data=data)