2121from i18n .strings import LazyI18nString
2222from languages .models import Language
2323from participants .models import Participant
24- from submissions .models import ProposalMaterial , Submission as SubmissionModel
24+ from submissions .models import ProposalMaterial , Submission as SubmissionModel , CoSpeaker
2525from submissions .tasks import notify_new_cfp_submission
26+ from users .models import User
2627
2728from .types import Submission , SubmissionMaterialInput
2829
2930FACEBOOK_LINK_MATCH = re .compile (r"^http(s)?:\/\/(www\.)?facebook\.com\/" )
3031LINKEDIN_LINK_MATCH = re .compile (r"^http(s)?:\/\/(www\.)?linkedin\.com\/" )
3132
3233
34+ def handle_co_speakers (submission , co_speaker_emails , submitter , conference ):
35+ """
36+ Handle co-speakers for a submission.
37+ Creates users if they don't exist and sends invitation emails to new co-speakers.
38+ """
39+ from users .managers import UserManager
40+
41+ # Get existing co-speakers for this submission
42+ existing_co_speakers = set (
43+ submission .co_speakers .values_list ("user__email" , flat = True )
44+ )
45+ existing_co_speakers_lower = {email .lower () for email in existing_co_speakers }
46+
47+ # Normalize incoming emails
48+ normalized_emails = [email .lower ().strip () for email in co_speaker_emails ]
49+
50+ # Find co-speakers to add (new ones)
51+ emails_to_add = [
52+ email for email in normalized_emails if email not in existing_co_speakers_lower
53+ ]
54+
55+ # Find co-speakers to remove (ones no longer in the list)
56+ emails_to_remove = [
57+ email for email in existing_co_speakers_lower if email not in normalized_emails
58+ ]
59+
60+ # Remove co-speakers that are no longer in the list
61+ if emails_to_remove :
62+ CoSpeaker .objects .filter (
63+ submission = submission , user__email__in = emails_to_remove
64+ ).delete ()
65+
66+ # Add new co-speakers
67+ for email in emails_to_add :
68+ # Get or create user
69+ user = User .objects .filter (email__iexact = email ).first ()
70+ user_already_had_account = user is not None
71+
72+ if not user :
73+ # Create user without password
74+ user = User .objects .create_user (email = email , password = None )
75+
76+ # Create co-speaker relationship
77+ CoSpeaker .objects .create (submission = submission , user = user )
78+
79+ # Send invitation email
80+ try :
81+ email_template = EmailTemplate .objects .for_conference (
82+ conference
83+ ).get_by_identifier (EmailTemplateIdentifier .co_speaker_invitation )
84+
85+ email_template .send_email (
86+ recipient = user ,
87+ placeholders = {
88+ "user_name" : get_name (user , email ),
89+ "proposal_title" : submission .title .localize ("en" ),
90+ "proposal_type" : str (submission .type ),
91+ "submitter_name" : get_name (submitter , submitter .email ),
92+ "submitter_email" : submitter .email ,
93+ "user_already_had_account" : "true" if user_already_had_account else "false" ,
94+ },
95+ )
96+ except Exception :
97+ # If email template doesn't exist, skip sending email
98+ # This allows the feature to work even if the template hasn't been created yet
99+ pass
100+
101+
33102@strawberry .type
34103class ProposalMaterialErrors :
35104 file_id : list [str ] = strawberry .field (default_factory = list )
@@ -67,6 +136,8 @@ class _SendSubmissionErrors:
67136 speaker_facebook_url : list [str ] = strawberry .field (default_factory = list )
68137 speaker_mastodon_handle : list [str ] = strawberry .field (default_factory = list )
69138
139+ co_speaker_emails : list [str ] = strawberry .field (default_factory = list )
140+
70141 non_field_errors : list [str ] = strawberry .field (default_factory = list )
71142
72143 errors : _SendSubmissionErrors = None
@@ -115,7 +186,7 @@ def multi_lingual_validation(
115186 f"{ to_text [language ]} : Cannot be more than { max_length } chars" ,
116187 )
117188
118- def validate (self , conference : Conference ):
189+ def validate (self , conference : Conference , current_user = None ):
119190 errors = SendSubmissionErrors ()
120191
121192 if not self .tags :
@@ -133,6 +204,27 @@ def validate(self, conference: Conference):
133204 if not self .languages :
134205 errors .add_error ("languages" , "You need to add at least one language" )
135206
207+ # Validate co-speaker emails
208+ if hasattr (self , "co_speaker_emails" ) and self .co_speaker_emails :
209+ if len (self .co_speaker_emails ) > 2 :
210+ errors .add_error ("co_speaker_emails" , "You can only add up to 2 co-speakers" )
211+
212+ # Check for duplicate emails
213+ seen_emails = set ()
214+ for email in self .co_speaker_emails :
215+ email_lower = email .lower ().strip ()
216+ if email_lower in seen_emails :
217+ errors .add_error ("co_speaker_emails" , f"Duplicate email: { email } " )
218+ seen_emails .add (email_lower )
219+
220+ # Validate email format (basic check)
221+ if "@" not in email or "." not in email :
222+ errors .add_error ("co_speaker_emails" , f"Invalid email format: { email } " )
223+
224+ # Check if user is trying to add themselves as co-speaker
225+ if current_user and email_lower == current_user .email .lower ():
226+ errors .add_error ("co_speaker_emails" , "You cannot add yourself as a co-speaker" )
227+
136228 self .multi_lingual_validation (errors , conference )
137229
138230 max_lengths = {
@@ -228,6 +320,7 @@ class SendSubmissionInput(BaseSubmissionInput):
228320 topic : Optional [ID ] = strawberry .field (default = None )
229321 tags : list [ID ] = strawberry .field (default_factory = list )
230322 do_not_record : bool = strawberry .field (default = False )
323+ co_speaker_emails : list [str ] = strawberry .field (default_factory = list )
231324
232325
233326@strawberry .input
@@ -259,9 +352,10 @@ class UpdateSubmissionInput(BaseSubmissionInput):
259352 tags : list [ID ] = strawberry .field (default_factory = list )
260353 materials : list [SubmissionMaterialInput ] = strawberry .field (default_factory = list )
261354 do_not_record : bool = strawberry .field (default = False )
355+ co_speaker_emails : list [str ] = strawberry .field (default_factory = list )
262356
263- def validate (self , conference : Conference , submission : SubmissionModel ):
264- errors = super ().validate (conference )
357+ def validate (self , conference : Conference , submission : SubmissionModel , current_user = None ):
358+ errors = super ().validate (conference , current_user )
265359
266360 if self .materials :
267361 if len (self .materials ) > 3 :
@@ -303,7 +397,7 @@ def update_submission(
303397
304398 conference = instance .conference
305399
306- errors = input .validate (conference = conference , submission = instance )
400+ errors = input .validate (conference = conference , submission = instance , current_user = request . user )
307401
308402 if errors .has_errors :
309403 return errors
@@ -390,6 +484,14 @@ def update_submission(
390484 },
391485 )
392486
487+ # Handle co-speakers
488+ handle_co_speakers (
489+ submission = instance ,
490+ co_speaker_emails = input .co_speaker_emails ,
491+ submitter = request .user ,
492+ conference = conference ,
493+ )
494+
393495 trigger_frontend_revalidate (conference , instance )
394496
395497 instance .__strawberry_definition__ = Submission .__strawberry_definition__
@@ -407,7 +509,7 @@ def send_submission(
407509 if not conference :
408510 return SendSubmissionErrors .with_error ("conference" , "Invalid conference" )
409511
410- errors = input .validate (conference = conference )
512+ errors = input .validate (conference = conference , current_user = request . user )
411513
412514 if not conference .is_cfp_open :
413515 errors .add_error ("non_field_errors" , "The call for paper is not open!" )
@@ -490,6 +592,15 @@ def send_submission(
490592 },
491593 )
492594
595+ # Handle co-speakers
596+ if input .co_speaker_emails :
597+ handle_co_speakers (
598+ submission = instance ,
599+ co_speaker_emails = input .co_speaker_emails ,
600+ submitter = request .user ,
601+ conference = conference ,
602+ )
603+
493604 def _notify_new_submission ():
494605 notify_new_cfp_submission .delay (
495606 submission_id = instance .id ,
0 commit comments