diff --git a/dash/orgs/views.py b/dash/orgs/views.py index 6c7e0e0..43752ea 100644 --- a/dash/orgs/views.py +++ b/dash/orgs/views.py @@ -356,15 +356,14 @@ class InviteForm(forms.ModelForm): ) def clean_emails(self): - emails = self.cleaned_data["emails"].lower().strip() - if emails: - email_list = emails.split(",") - for email in email_list: - try: - validate_email(email) - except ValidationError: - raise forms.ValidationError(_("One of the emails you entered is invalid.")) - return emails + emails = self.cleaned_data["emails"] + email_list = list(dict.fromkeys(email.strip().lower() for email in emails.split(",") if email.strip())) + for email in email_list: + try: + validate_email(email) + except ValidationError: + raise forms.ValidationError(_("One of the emails you entered is invalid.")) + return ",".join(email_list) class Meta: model = Invitation @@ -422,28 +421,28 @@ def post_save(self, obj): user_group = cleaned_data["user_group"] - emails = cleaned_data["emails"].lower().strip() - email_list = emails.split(",") + # emails is already normalized by clean_emails: comma-joined, stripped, lowercased and de-duplicated + emails = cleaned_data["emails"] + email_list = emails.split(",") if emails else [] - if emails: - for email in email_list: - # if they already have an invite, update it - invites = Invitation.objects.filter(email=email, org=org).order_by("-pk") - invitation = invites.first() + for email in email_list: + # if they already have an invite, update it + invites = Invitation.objects.filter(email=email, org=org).order_by("-pk") + invitation = invites.first() - if invitation: - # remove any old invites - invites.exclude(pk=invitation.pk).delete() + if invitation: + # remove any old invites + invites.exclude(pk=invitation.pk).delete() - invitation.user_group = user_group - invitation.is_active = True - invitation.save() - else: - invitation = Invitation.objects.create( - email=email, org=org, user_group=user_group, created_by=user, modified_by=user - ) + invitation.user_group = user_group + invitation.is_active = True + invitation.save() + else: + invitation = Invitation.objects.create( + email=email, org=org, user_group=user_group, created_by=user, modified_by=user + ) - invitation.send_invitation() + invitation.send_invitation() # remove all the org users for user in self.get_object().get_org_admins(): diff --git a/test_runner/tests.py b/test_runner/tests.py index 0dcfdf0..be25c6a 100644 --- a/test_runner/tests.py +++ b/test_runner/tests.py @@ -1,6 +1,5 @@ import zoneinfo -from dash.tags.models import Tag -from unittest.mock import Mock, patch, call +from unittest.mock import Mock, call, patch import valkey from smartmin.tests import SmartminTest @@ -24,9 +23,11 @@ from dash.orgs.models import Invitation, Org, OrgBackend, OrgBackground, TaskState from dash.orgs.tasks import org_task from dash.orgs.templatetags.dashorgs import display_time, national_phone +from dash.orgs.views import OrgCRUDL from dash.stories.models import Story, StoryImage -from dash.utils import random_string +from dash.tags.models import Tag from dash.test import MockResponse +from dash.utils import random_string class UserTest(SmartminTest): @@ -1012,6 +1013,39 @@ def test_manage_accounts(self): self.assertEqual(3, Invitation.objects.all().count()) self.assertEqual(4, len(mail.outbox)) + # emails with surrounding whitespace and a trailing comma are tolerated + post_data["emails"] = "spaced1@nyaruka.com, spaced2@nyaruka.com ," + post_data["user_group"] = "E" + response = self.client.post(manage_accounts_url, post_data, SERVER_NAME="uganda.ureport.io") + self.assertEqual(302, response.status_code) + + # 2 new invitations are created with clean addresses + self.assertEqual(5, Invitation.objects.all().count()) + self.assertEqual(6, len(mail.outbox)) + self.assertTrue(Invitation.objects.filter(email="spaced1@nyaruka.com", user_group="E").exists()) + self.assertTrue(Invitation.objects.filter(email="spaced2@nyaruka.com", user_group="E").exists()) + + # duplicate addresses are collapsed to a single invitation and email + post_data["emails"] = "dup@nyaruka.com, dup@nyaruka.com" + post_data["user_group"] = "E" + response = self.client.post(manage_accounts_url, post_data, SERVER_NAME="uganda.ureport.io") + self.assertEqual(302, response.status_code) + self.assertEqual(6, Invitation.objects.all().count()) + self.assertEqual(7, len(mail.outbox)) + self.assertEqual(1, Invitation.objects.filter(email="dup@nyaruka.com").count()) + + # separator-only input is treated as no emails + post_data["emails"] = " , , " + response = self.client.post(manage_accounts_url, post_data, SERVER_NAME="uganda.ureport.io") + self.assertEqual(302, response.status_code) + self.assertEqual(6, Invitation.objects.all().count()) + self.assertEqual(7, len(mail.outbox)) + + def test_invite_form_clean_emails(self): + form = OrgCRUDL.ManageAccounts.InviteForm(data={"emails": " A@x.com , b@y.com ,", "user_group": "E"}) + self.assertTrue(form.is_valid(), form.errors) + self.assertEqual("a@x.com,b@y.com", form.cleaned_data["emails"]) + def test_join(self): editor_invitation = Invitation.objects.create( org=self.org, user_group="E", email="norkans7@gmail.com", created_by=self.admin, modified_by=self.admin