Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2026-08-15 14:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('notifications', '0023_alter_sentemail_status'),
]

operations = [
migrations.AlterField(
model_name='emailtemplate',
name='identifier',
field=models.CharField(choices=[('proposal_accepted', 'Proposal accepted'), ('proposal_scheduled', 'Proposal scheduled'), ('proposal_rejected', 'Proposal rejected'), ('proposal_in_waiting_list', 'Proposal in waiting list'), ('proposal_scheduled_time_changed', 'Proposal scheduled time changed'), ('proposal_received_confirmation', 'Proposal received confirmation'), ('speaker_communication', 'Speaker communication'), ('voucher_code', 'Voucher code'), ('reset_password', '[System] Reset password'), ('grant_application_confirmation', 'Grant application confirmation'), ('grant_approved', 'Grant approved'), ('grant_rejected', 'Grant rejected'), ('grant_waiting_list', 'Grant waiting list'), ('grant_waiting_list_update', 'Grant waiting list update'), ('sponsorship_brochure', 'Sponsorship brochure'), ('visa_invitation_letter_download', 'Visa invitation letter download'), ('speaker_video_recording_uploaded', 'Speaker: Video recording uploaded'), ('custom', 'Custom')], max_length=200, verbose_name='identifier'),
),
]
12 changes: 12 additions & 0 deletions backend/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class EmailTemplateIdentifier(models.TextChoices):
_("Visa invitation letter download"),
)

speaker_video_recording_uploaded = (
"speaker_video_recording_uploaded",
_("Speaker: Video recording uploaded"),
)

custom = "custom", _("Custom")


Expand Down Expand Up @@ -165,6 +170,13 @@ class EmailTemplate(TimeStampedModel):
"has_grant",
"user_name",
],
EmailTemplateIdentifier.speaker_video_recording_uploaded: [
*BASE_PLACEHOLDERS,
"user_name",
"video_recording_url",
"schedule_item_title",
"schedule_item_type",
],
}

conference = models.ForeignKey(
Expand Down
1 change: 1 addition & 0 deletions backend/schedule/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ class ScheduleItemSentForVideoUploadAdmin(admin.ModelAdmin):
"status",
"video_uploaded",
"thumbnail_uploaded",
"emails_scheduled",
"attempts",
"last_attempt_at",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2026-08-15 16:41

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('schedule', '0057_room_attendees_total_capacity'),
]

operations = [
migrations.AddField(
model_name='scheduleitemsentforvideoupload',
name='emails_scheduled',
field=models.BooleanField(default=False, verbose_name='Emails scheduled'),
),
]
12 changes: 10 additions & 2 deletions backend/schedule/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
from pycon.constants import COLORS
from submissions.models import Submission

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from users.models import User


@dataclass
class SpeakerEntity:
Expand Down Expand Up @@ -156,6 +161,7 @@ class Status(models.TextChoices):
last_attempt_at = models.DateTimeField(_("Last attempt at"), null=True, blank=True)
video_uploaded = models.BooleanField(_("Video uploaded"), default=False)
thumbnail_uploaded = models.BooleanField(_("Thumbnail uploaded"), default=False)
emails_scheduled = models.BooleanField(_("Emails scheduled"), default=False)
failed_reason = models.TextField(
_("Failed reason"),
blank=True,
Expand Down Expand Up @@ -334,7 +340,7 @@ def actual_attendees_total_capacity(self):
return room.attendees_total_capacity if room else None

@cached_property
def speakers(self):
def speakers(self) -> list["User"]:
speakers = []

if self.submission_id:
Expand All @@ -354,7 +360,9 @@ def speakers(self):
speaker.user
for speaker in sorted(additional_speakers, key=lambda speaker: speaker.id)
)
return [speaker for speaker in speakers if speaker is not None]
return list(
dict.fromkeys(speaker for speaker in speakers if speaker is not None)
)

def clean(self):
if self.type == ScheduleItem.TYPES.submission and not self.submission:
Expand Down
32 changes: 27 additions & 5 deletions backend/schedule/tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.db import transaction
from django.db.models import Q
from conferences.tasks import send_conference_voucher_email
from conferences.vouchers import create_conference_voucher
Expand Down Expand Up @@ -262,7 +263,7 @@ def upload_schedule_item_video(*, sent_for_video_upload_state_id: int):

schedule_item = sent_for_video_upload.schedule_item
remote_video_path = schedule_item.video_uploaded_path
video_id = None
video_id = schedule_item.youtube_video_id

if not sent_for_video_upload.video_uploaded:
logger.info("Uploading video for schedule_item_id=%s", schedule_item.id)
Expand Down Expand Up @@ -292,10 +293,9 @@ def upload_schedule_item_video(*, sent_for_video_upload_state_id: int):
else:
logger.info("Video already uploaded for schedule_item_id=%s", schedule_item.id)

if not sent_for_video_upload.thumbnail_uploaded:
video_id = video_id or schedule_item.youtube_video_id
assert video_id, "Video marked as uploaded but Video ID is missing"
assert video_id, "Video marked as uploaded but Video ID is missing"

if not sent_for_video_upload.thumbnail_uploaded:
logger.info("Extracting thumbnail for schedule_item_id=%s", schedule_item.id)

thumbnail_path = extract_video_thumbnail(
Expand Down Expand Up @@ -331,7 +331,29 @@ def upload_schedule_item_video(*, sent_for_video_upload_state_id: int):

cleanup_local_files(schedule_item.id)

logger.info("Video uploaded for schedule_item_id=%s", schedule_item.id)
if not sent_for_video_upload.emails_scheduled:
conference = schedule_item.conference
all_speakers = schedule_item.speakers
email_template = EmailTemplate.objects.for_conference(
conference
).get_by_identifier(EmailTemplateIdentifier.speaker_video_recording_uploaded)

with transaction.atomic():
for speaker in all_speakers:
email_template.draft_email(
recipient=speaker,
placeholders={
"user_name": get_name(speaker, "there"),
"video_recording_url": f"https://www.youtube.com/watch?v={video_id}",
"schedule_item_title": schedule_item.title,
"schedule_item_type": schedule_item.get_type_display(),
},
)

sent_for_video_upload.emails_scheduled = True
sent_for_video_upload.save(update_fields=["emails_scheduled"])

logger.info("Workflow completed for schedule_item_id=%s", schedule_item.id)
sent_for_video_upload.status = ScheduleItemSentForVideoUpload.Status.completed
sent_for_video_upload.save(update_fields=["status"])

Expand Down
Loading
Loading