Repository: DjangoGirls/djangogirls

Issue:
IntegrityError: duplicate key value violates unique constraint "applications_application_form_id_b47c846a_uniq"
Sentry Issue: [DJANGO-GIRLS-WEBSITE-43](https://sentry.io/organizations/django-girls/issues/3486070965/?referrer=github_integration)

```
UniqueViolation: duplicate key value violates unique constraint "applications_application_form_id_b47c846a_uniq"
DETAIL:  Key (form_id, email)=(675, ) already exists.

  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

IntegrityError: duplicate key value violates unique constraint "applications_application_form_id_b47c846a_uniq"
DETAIL:  Key (form_id, email)=(675, ) already exists.

(12 additional frame(s) were not displayed)
...
  File "django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
```


Discussion:
```python
def save(self, *args, **kwargs):
    application = Application.objects.create(form=self.form)
```

This seems to be related to this line, in `ApplicationForm` we initially create an application for that `form_id` with no e-mail. Afterwards, we loop through question types and assign an e-mail to the application.

According to code, it looks like there could be forms that don't require e-mails, but I wonder if we accept applications without an e-mail? Maybe instead of calling `objects.create` we can just instantiate an `Application` record and save it when we post process the forms questions and answers?

Resolving patch:
diff --git a/applications/forms.py b/applications/forms.py
--- a/applications/forms.py
+++ b/applications/forms.py
@@ -4,6 +4,7 @@
 from django import forms
 from django.conf import settings
 from django.core.mail import EmailMessage
+from django.db import transaction
 from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
 
@@ -52,7 +53,8 @@ def clean(self):
         return cleaned_data
 
     def save(self, *args, **kwargs):
-        application = Application.objects.create(form=self.form)
+        application = Application(form=self.form)
+        answers = []
 
         for name in self.cleaned_data:
             question = None
@@ -66,20 +68,14 @@ def save(self, *args, **kwargs):
                         application.newsletter_optin = True
                     else:
                         application.newsletter_optin = False
-                    application.save()
 
             value = ', '.join(value) if type(value) == list else value
 
             if question:
-                Answer.objects.create(
-                    application=application,
-                    question=question,
-                    answer=value
-                )
+                answers.append(Answer(question=question, answer=value))
 
                 if question.question_type == 'email':
                     application.email = value
-                    application.save()
 
         if not self.form.event.email:
             # If event doesn't have an email (legacy events), create
@@ -124,6 +120,12 @@ def save(self, *args, **kwargs):
                     url, auth=('user', settings.MAILCHIMP_API_KEY), json=payload
                 )
 
+        with transaction.atomic():
+            application.save()
+            for answer in answers:
+                answer.application = application
+            Answer.objects.bulk_create(answers)
+
 
 class ScoreForm(forms.ModelForm):
 
diff --git a/applications/migrations/0006_auto_20221231_1925.py b/applications/migrations/0006_auto_20221231_1925.py
new file mode 100644
--- /dev/null
+++ b/applications/migrations/0006_auto_20221231_1925.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.14 on 2022-12-31 19:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('applications', '0005_auto_20210906_0915'),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name='application',
+            unique_together=set(),
+        ),
+        migrations.AddConstraint(
+            model_name='application',
+            constraint=models.UniqueConstraint(condition=models.Q(('email__isnull', False)), fields=('form', 'email'), name='unique_form_email_email_not_null'),
+        ),
+    ]
diff --git a/applications/models.py b/applications/models.py
--- a/applications/models.py
+++ b/applications/models.py
@@ -188,7 +188,13 @@ class Application(models.Model):
     rsvp_no_code = models.CharField(max_length=24, null=True, blank=True)
 
     class Meta:
-        unique_together = ("form", "email")
+        constraints = [
+            models.UniqueConstraint(
+                fields=["form", "email"],
+                condition=models.Q(email__isnull=False),
+                name="unique_form_email_email_not_null",
+            )
+        ]
 
     def save(self, *args, **kwargs):
         if self.pk is None:


Test patch: