-
Notifications
You must be signed in to change notification settings - Fork 24
feat: Add co-speakers support for submissions #4522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,15 +21,84 @@ | |
| from i18n.strings import LazyI18nString | ||
| from languages.models import Language | ||
| from participants.models import Participant | ||
| from submissions.models import ProposalMaterial, Submission as SubmissionModel | ||
| from submissions.models import ProposalMaterial, Submission as SubmissionModel, SubmissionCoSpeaker | ||
| from submissions.tasks import notify_new_cfp_submission | ||
| from users.models import User | ||
|
|
||
| from .types import Submission, SubmissionMaterialInput | ||
|
|
||
| FACEBOOK_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?facebook\.com\/") | ||
| LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/") | ||
|
|
||
|
|
||
| def handle_co_speakers(submission, co_speaker_emails, submitter, conference): | ||
| """ | ||
| Handle co-speakers for a submission. | ||
| Creates users if they don't exist and sends invitation emails to new co-speakers. | ||
| """ | ||
| from users.managers import UserManager | ||
|
|
||
| # Get existing co-speakers for this submission | ||
| existing_co_speakers = set( | ||
| submission.co_speakers.values_list("user__email", flat=True) | ||
| ) | ||
| existing_co_speakers_lower = {email.lower() for email in existing_co_speakers} | ||
|
|
||
| # Normalize incoming emails | ||
| normalized_emails = [email.lower().strip() for email in co_speaker_emails] | ||
|
|
||
| # Find co-speakers to add (new ones) | ||
| emails_to_add = [ | ||
| email for email in normalized_emails if email not in existing_co_speakers_lower | ||
| ] | ||
|
|
||
| # Find co-speakers to remove (ones no longer in the list) | ||
| emails_to_remove = [ | ||
| email for email in existing_co_speakers_lower if email not in normalized_emails | ||
| ] | ||
|
|
||
| # Remove co-speakers that are no longer in the list | ||
| if emails_to_remove: | ||
| SubmissionCoSpeaker.objects.filter( | ||
| submission=submission, user__email__in=emails_to_remove | ||
| ).delete() | ||
|
|
||
| # Add new co-speakers | ||
| for email in emails_to_add: | ||
| # Get or create user | ||
| user = User.objects.filter(email__iexact=email).first() | ||
| user_already_had_account = user is not None | ||
|
|
||
| if not user: | ||
| # Create user without password | ||
| user = User.objects.create_user(email=email, password=None) | ||
|
|
||
| # Create co-speaker relationship | ||
| SubmissionCoSpeaker.objects.create(submission=submission, user=user) | ||
|
|
||
| # Send invitation email | ||
| try: | ||
| email_template = EmailTemplate.objects.for_conference( | ||
| conference | ||
| ).get_by_identifier(EmailTemplateIdentifier.co_speaker_invitation) | ||
|
|
||
| email_template.send_email( | ||
| recipient=user, | ||
| placeholders={ | ||
| "user_name": get_name(user, email), | ||
| "proposal_title": submission.title.localize("en"), | ||
| "proposal_type": str(submission.type), | ||
| "submitter_name": get_name(submitter, submitter.email), | ||
| "submitter_email": submitter.email, | ||
| "user_already_had_account": "true" if user_already_had_account else "false", | ||
| }, | ||
| ) | ||
| except Exception: | ||
| # If email template doesn't exist, skip sending email | ||
| # This allows the feature to work even if the template hasn't been created yet | ||
| pass | ||
|
|
||
|
|
||
| @strawberry.type | ||
| class ProposalMaterialErrors: | ||
| file_id: list[str] = strawberry.field(default_factory=list) | ||
|
|
@@ -67,6 +136,8 @@ class _SendSubmissionErrors: | |
| speaker_facebook_url: list[str] = strawberry.field(default_factory=list) | ||
| speaker_mastodon_handle: list[str] = strawberry.field(default_factory=list) | ||
|
|
||
| co_speaker_emails: list[str] = strawberry.field(default_factory=list) | ||
|
|
||
| non_field_errors: list[str] = strawberry.field(default_factory=list) | ||
|
|
||
| errors: _SendSubmissionErrors = None | ||
|
|
@@ -115,7 +186,7 @@ def multi_lingual_validation( | |
| f"{to_text[language]}: Cannot be more than {max_length} chars", | ||
| ) | ||
|
|
||
| def validate(self, conference: Conference): | ||
| def validate(self, conference: Conference, current_user=None): | ||
| errors = SendSubmissionErrors() | ||
|
|
||
| if not self.tags: | ||
|
|
@@ -133,6 +204,27 @@ def validate(self, conference: Conference): | |
| if not self.languages: | ||
| errors.add_error("languages", "You need to add at least one language") | ||
|
|
||
| # Validate co-speaker emails | ||
| if hasattr(self, "co_speaker_emails") and self.co_speaker_emails: | ||
| if len(self.co_speaker_emails) > 2: | ||
| errors.add_error("co_speaker_emails", "You can only add up to 2 co-speakers") | ||
|
|
||
| # Check for duplicate emails | ||
| seen_emails = set() | ||
| for email in self.co_speaker_emails: | ||
| email_lower = email.lower().strip() | ||
| if email_lower in seen_emails: | ||
| errors.add_error("co_speaker_emails", f"Duplicate email: {email}") | ||
| seen_emails.add(email_lower) | ||
|
|
||
| # Validate email format (basic check) | ||
| if "@" not in email or "." not in email: | ||
| errors.add_error("co_speaker_emails", f"Invalid email format: {email}") | ||
|
|
||
|
||
| # Check if user is trying to add themselves as co-speaker | ||
| if current_user and email_lower == current_user.email.lower(): | ||
| errors.add_error("co_speaker_emails", "You cannot add yourself as a co-speaker") | ||
|
|
||
| self.multi_lingual_validation(errors, conference) | ||
|
|
||
| max_lengths = { | ||
|
|
@@ -228,6 +320,7 @@ class SendSubmissionInput(BaseSubmissionInput): | |
| topic: Optional[ID] = strawberry.field(default=None) | ||
| tags: list[ID] = strawberry.field(default_factory=list) | ||
| do_not_record: bool = strawberry.field(default=False) | ||
| co_speaker_emails: list[str] = strawberry.field(default_factory=list) | ||
|
|
||
|
|
||
| @strawberry.input | ||
|
|
@@ -259,9 +352,10 @@ class UpdateSubmissionInput(BaseSubmissionInput): | |
| tags: list[ID] = strawberry.field(default_factory=list) | ||
| materials: list[SubmissionMaterialInput] = strawberry.field(default_factory=list) | ||
| do_not_record: bool = strawberry.field(default=False) | ||
| co_speaker_emails: list[str] = strawberry.field(default_factory=list) | ||
|
|
||
| def validate(self, conference: Conference, submission: SubmissionModel): | ||
| errors = super().validate(conference) | ||
| def validate(self, conference: Conference, submission: SubmissionModel, current_user=None): | ||
| errors = super().validate(conference, current_user) | ||
|
|
||
| if self.materials: | ||
| if len(self.materials) > 3: | ||
|
|
@@ -303,7 +397,7 @@ def update_submission( | |
|
|
||
| conference = instance.conference | ||
|
|
||
| errors = input.validate(conference=conference, submission=instance) | ||
| errors = input.validate(conference=conference, submission=instance, current_user=request.user) | ||
|
|
||
| if errors.has_errors: | ||
| return errors | ||
|
|
@@ -390,6 +484,14 @@ def update_submission( | |
| }, | ||
| ) | ||
|
|
||
| # Handle co-speakers | ||
| handle_co_speakers( | ||
| submission=instance, | ||
| co_speaker_emails=input.co_speaker_emails, | ||
| submitter=request.user, | ||
| conference=conference, | ||
| ) | ||
|
|
||
| trigger_frontend_revalidate(conference, instance) | ||
|
|
||
| instance.__strawberry_definition__ = Submission.__strawberry_definition__ | ||
|
|
@@ -407,7 +509,7 @@ def send_submission( | |
| if not conference: | ||
| return SendSubmissionErrors.with_error("conference", "Invalid conference") | ||
|
|
||
| errors = input.validate(conference=conference) | ||
| errors = input.validate(conference=conference, current_user=request.user) | ||
|
|
||
| if not conference.is_cfp_open: | ||
| errors.add_error("non_field_errors", "The call for paper is not open!") | ||
|
|
@@ -490,6 +592,15 @@ def send_submission( | |
| }, | ||
| ) | ||
|
|
||
| # Handle co-speakers | ||
| if input.co_speaker_emails: | ||
| handle_co_speakers( | ||
| submission=instance, | ||
| co_speaker_emails=input.co_speaker_emails, | ||
| submitter=request.user, | ||
| conference=conference, | ||
| ) | ||
|
|
||
| def _notify_new_submission(): | ||
| notify_new_cfp_submission.delay( | ||
| submission_id=instance.id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Generated by Django 5.2.8 on 2026-01-05 19:22 | ||
|
|
||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
| import django.db.models.deletion | ||
| import django.utils.timezone | ||
| import model_utils.fields | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('submissions', '0030_submissiontype_is_recordable'), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='SubmissionCoSpeaker', | ||
| fields=[ | ||
| ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
| ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), | ||
| ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), | ||
| ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='co_speakers', to='submissions.submission', verbose_name='submission')), | ||
| ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='co_speaker_submissions', to=settings.AUTH_USER_MODEL, verbose_name='co-speaker')), | ||
| ], | ||
| options={ | ||
| 'verbose_name': 'co-speaker', | ||
| 'verbose_name_plural': 'co-speakers', | ||
| 'unique_together': {('submission', 'user')}, | ||
| }, | ||
| ), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@claude
current_useris never NoneUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude encountered an error —— View job
I'll analyze this and get back to you.