diff --git a/account/admin.py b/account/admin.py index d6e28b4..bb74f79 100644 --- a/account/admin.py +++ b/account/admin.py @@ -35,7 +35,7 @@ class Meta: class MailingListAdmin(DisableDeleteAdminMixin, DisableAddAdminMixin, admin.ModelAdmin): - list_display = ("result", "csv_emails") + list_display = ("result", "number_of_emails") readonly_fields = ("result",) form = MailingListAdminForm diff --git a/account/api/views.py b/account/api/views.py index 94c94e5..9ebb5ae 100644 --- a/account/api/views.py +++ b/account/api/views.py @@ -1,6 +1,7 @@ from django import db from django.core.exceptions import ValidationError from django.core.validators import validate_email +from django.db.utils import IntegrityError from drf_spectacular.utils import extend_schema, OpenApiResponse from rest_framework import status, viewsets from rest_framework.decorators import action @@ -93,8 +94,7 @@ def subscribe(self, request): email = request.data.get("email", None) if not email: return Response("No 'email' provided", status=status.HTTP_400_BAD_REQUEST) - if MailingListEmail.objects.filter(email=email).count() > 0: - return Response("'email' exists", status=status.HTTP_400_BAD_REQUEST) + try: validate_email(email) except ValidationError as e: @@ -103,8 +103,13 @@ def subscribe(self, request): if not mailing_list: # In case mailing list is not created for the result, it is created. mailing_list = MailingList.objects.create(result=result) - - MailingListEmail.objects.create(mailing_list=mailing_list, email=email) + try: + MailingListEmail.objects.create(mailing_list=mailing_list, email=email) + except IntegrityError: + return Response( + "'email' and 'result' must be jointly null", + status=status.HTTP_400_BAD_REQUEST, + ) user.has_subscribed = True user.save() return Response("subscribed", status=status.HTTP_201_CREATED) diff --git a/account/migrations/0015_email_and_mailing_list_jointly_unique.py b/account/migrations/0015_email_and_mailing_list_jointly_unique.py new file mode 100644 index 0000000..8e913dd --- /dev/null +++ b/account/migrations/0015_email_and_mailing_list_jointly_unique.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.13 on 2024-04-30 07:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("account", "0014_remove_profile_is_filled_for_fun"), + ] + + operations = [ + migrations.AlterField( + model_name="mailinglistemail", + name="email", + field=models.EmailField(max_length=254), + ), + migrations.AddConstraint( + model_name="mailinglistemail", + constraint=models.UniqueConstraint( + fields=("email", "mailing_list"), + name="email_and_mailing_list_must_be_jointly:unique", + ), + ), + ] diff --git a/account/models.py b/account/models.py index 50a8ba8..fc994b3 100644 --- a/account/models.py +++ b/account/models.py @@ -72,12 +72,23 @@ def __str__(self): def csv_emails(self): return ",".join([e.email for e in self.emails.all()]) + def number_of_emails(self): + return self.emails.count() + class MailingListEmail(models.Model): - email = models.EmailField(unique=True) + email = models.EmailField() mailing_list = models.ForeignKey( MailingList, related_name="emails", on_delete=models.CASCADE ) def __str__(self): - return self.email + return f"{self.email} {self.mailing_list}" + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["email", "mailing_list"], + name="email_and_mailing_list_must_be_jointly:unique", + ) + ] diff --git a/account/tests/test_api.py b/account/tests/test_api.py index c0a4b05..4ddf6af 100644 --- a/account/tests/test_api.py +++ b/account/tests/test_api.py @@ -374,3 +374,34 @@ def test_mailing_list_unsubscribe_email_not_provided( url = reverse("account:profiles-unsubscribe") response = api_client_with_custom_ip_address.post(url) assert response.status_code == 400 + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "ip_address", + [ + ("100.19.91.40"), + ], +) +def test_mailing_list_unique_constraints(api_client_with_custom_ip_address, users): + url = reverse("account:profiles-subscribe") + user = users.get(username="test1") + response = api_client_with_custom_ip_address.post( + url, {"email": "test@test.com", "user": user.id} + ) + assert response.status_code == 201 + assert MailingListEmail.objects.count() == 1 + # Subscribed as the result (MailingList) is different than for user 'test1' + user = users.get(username="test2") + response = api_client_with_custom_ip_address.post( + url, {"email": "test@test.com", "user": user.id} + ) + assert response.status_code == 201 + assert MailingListEmail.objects.count() == 2 + # Fails, as the email and result are not jointly unique. + user = users.get(username="test3") + response = api_client_with_custom_ip_address.post( + url, {"email": "test@test.com", "user": user.id} + ) + assert response.status_code == 400 + assert MailingListEmail.objects.count() == 2