Skip to content

Commit

Permalink
Merge pull request #61 from City-of-Turku/develop
Browse files Browse the repository at this point in the history
Production update
  • Loading branch information
juuso-j committed May 6, 2024
2 parents 8a3b096 + 6ef0b02 commit 006f039
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 22 deletions.
2 changes: 1 addition & 1 deletion account/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions account/api/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,7 +24,7 @@ class MailRateThrottle(AnonRateThrottle):
The IP address of the incoming request is used to generate a unique key to throttle against.
"""

rate = "10/day"
rate = "50/day"


class ProfileViewSet(UpdateModelMixin, viewsets.GenericViewSet):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions account/migrations/0015_email_and_mailing_list_jointly_unique.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
17 changes: 17 additions & 0 deletions account/migrations/0016_order_users_by_date_joined.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.13 on 2024-04-30 11:01

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("account", "0015_email_and_mailing_list_jointly_unique"),
]

operations = [
migrations.AlterModelOptions(
name="user",
options={"ordering": ["-date_joined"]},
),
]
16 changes: 14 additions & 2 deletions account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def save(self, *args, **kwargs):

class Meta:
db_table = "auth_user"
ordering = ["-date_joined"]


class Profile(models.Model):
Expand Down Expand Up @@ -72,12 +73,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",
)
]
18 changes: 16 additions & 2 deletions account/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

from account.api.views import ProfileViewSet
from account.models import MailingList, MailingListEmail, Profile, User
from profiles.models import Result

Expand Down Expand Up @@ -29,7 +30,20 @@ def api_client_with_custom_ip_address(ip_address):
def users(results):
[
User.objects.create(username=f"test{i}", result=results[i % results.count()])
for i in range(20)
for i in range(4)
]
return User.objects.all()


@pytest.fixture
def throttling_users(results):
num_users = (
int(ProfileViewSet.unsubscribe.kwargs["throttle_classes"][0].rate.split("/")[0])
+ 2
)
[
User.objects.create(username=f"t_test{i}", result=results[i % results.count()])
for i in range(num_users)
]
return User.objects.all()

Expand Down Expand Up @@ -60,6 +74,6 @@ def mailing_list_emails(mailing_lists):
MailingListEmail.objects.create(
email=f"test_{c}@test.com", mailing_list=mailing_lists.first()
)
for c in range(20)
for c in range(52)
]
return MailingListEmail.objects.all()
20 changes: 10 additions & 10 deletions account/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_unauthenticated_cannot_do_anything(api_client, users):
@pytest.mark.parametrize(
"ip_address",
[
("199.168.1.40"),
("1.1.1.1"),
],
)
def test_mailing_list_unsubscribe_throttling(
Expand All @@ -59,7 +59,7 @@ def test_mailing_list_unsubscribe_throttling(
response = api_client_with_custom_ip_address.post(
url, {"email": f"test_{count}@test.com"}
)
assert response.status_code == 200
assert response.status_code == 200, response.content
count += 1
time.sleep(2)
response = api_client_with_custom_ip_address.post(
Expand Down Expand Up @@ -245,11 +245,11 @@ def test_mailing_user_has_subscribed(api_client, users, results, mailing_lists):
@pytest.mark.parametrize(
"ip_address",
[
("92.68.21.220"),
("1.1.1.2"),
],
)
def test_mailing_list_subscribe_throttling(
api_client_with_custom_ip_address, mailing_list_emails, users
api_client_with_custom_ip_address, throttling_users
):
num_requests = int(
ProfileViewSet.subscribe.kwargs["throttle_classes"][0].rate.split("/")[0]
Expand All @@ -261,15 +261,15 @@ def test_mailing_list_subscribe_throttling(
url,
{
"email": f"throttlling_test_{count}@test.com",
"user": users[count].id,
"user": throttling_users[count].id,
},
)
assert response.status_code == 201
assert response.status_code == 201, response.content
count += 1

time.sleep(2)
response = api_client_with_custom_ip_address.post(
url, {"email": f"test_{count}@test.com", "user": users[count].id}
url, {"email": f"test_{count}@test.com", "user": throttling_users[count].id}
)
assert response.status_code == 429

Expand Down Expand Up @@ -322,7 +322,7 @@ def test_mailing_list_subscribe_with_invalid_post_data(
@pytest.mark.parametrize(
"ip_address",
[
("100.1.1.40"),
("1.1.1.3"),
],
)
def test_mailing_list_unsubscribe(
Expand All @@ -343,7 +343,7 @@ def test_mailing_list_unsubscribe(
@pytest.mark.parametrize(
"ip_address",
[
("101.1.1.40"),
("1.1.1.4"),
],
)
def test_mailing_list_unsubscribe_non_existing_email(
Expand All @@ -365,7 +365,7 @@ def test_mailing_list_unsubscribe_non_existing_email(
@pytest.mark.parametrize(
"ip_address",
[
("12.6.121.22"),
("1.1.1.5"),
],
)
def test_mailing_list_unsubscribe_email_not_provided(
Expand Down
1 change: 1 addition & 0 deletions mpbackend/excluded_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"/api/v1/question/",
"/api/v1/answer/",
"/api/v1/postalcoderesult/",
"/api/v1/cumulativeresult/",
"/api/account/",
]

Expand Down
15 changes: 15 additions & 0 deletions profiles/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from profiles.models import (
Answer,
CumulativeResultCount,
Option,
PostalCode,
PostalCodeResult,
Expand Down Expand Up @@ -149,3 +150,17 @@ class PostalCodeTypeSerializer(serializers.ModelSerializer):
class Meta:
model = PostalCodeType
fields = "__all__"


class CumulativeResultSerializer(serializers.ModelSerializer):
class Meta:
model = CumulativeResultCount
fields = "__all__"

def to_representation(self, instance):
type_name = self.context.get("type_name")
representation = super().to_representation(instance)
representation["sum_of_count"] = instance.get_sum_of_count(
postal_code_type_name=type_name
)
return representation
2 changes: 1 addition & 1 deletion profiles/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class StartPollRateThrottle(AnonRateThrottle):
The IP address of the incoming request is used to generate a unique key to throttle against.
"""

rate = "10/day"
rate = "50/day"


class CustomValidationError(ValidationError):
Expand Down
43 changes: 42 additions & 1 deletion profiles/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from rest_framework import status, viewsets
from rest_framework.authtoken.models import Token
from rest_framework.decorators import action
from rest_framework.mixins import CreateModelMixin
from rest_framework.exceptions import ParseError
from rest_framework.mixins import CreateModelMixin, ListModelMixin
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
Expand All @@ -27,6 +28,7 @@
from profiles.api.serializers import (
AnswerRequestSerializer,
AnswerSerializer,
CumulativeResultSerializer,
InConditionResponseSerializer,
OptionSerializer,
PostalCodeResultSerializer,
Expand All @@ -44,6 +46,7 @@
)
from profiles.models import (
Answer,
CumulativeResultCount,
Option,
PostalCode,
PostalCodeResult,
Expand Down Expand Up @@ -738,6 +741,44 @@ class PostalCodeViewSet(viewsets.ReadOnlyModelViewSet):
register_view(PostalCodeViewSet, "postalcode")


@extend_schema_view(
list=extend_schema(
parameters=[
POSTAL_CODE_TYPE_PARAM,
],
description="Returns cumulative result count for every result.",
)
)
class CumulativeResultsViewSet(ListModelMixin, GenericViewSet):
queryset = CumulativeResultCount.objects.all()
serializer_class = CumulativeResultSerializer

def list(self, request, *args, **kwargs):
queryset = self.queryset
type_name = ""
postal_code_type_id = request.query_params.get(
"postal_code_type",
PostalCodeType.objects.get(type_name=PostalCodeType.HOME_POSTAL_CODE).id,
)
try:
postal_code_type_id = int(postal_code_type_id)
except ValueError:
raise ParseError("'postal_code_type' must be int")
postal_code_type = PostalCodeType.objects.filter(id=postal_code_type_id).first()
if not postal_code_type:
queryset = CumulativeResultCount.objects.none()
else:
type_name = postal_code_type.type_name
page = self.paginate_queryset(queryset)
serializer = self.serializer_class(
page, many=True, context={"type_name": type_name}
)
return self.get_paginated_response(serializer.data)


register_view(CumulativeResultsViewSet, "cumulativeresult")


class PostalCodeTypeViewSet(viewsets.ReadOnlyModelViewSet):
queryset = PostalCodeType.objects.all()
serializer_class = PostalCodeTypeSerializer
Expand Down
23 changes: 23 additions & 0 deletions profiles/migrations/0022_cumulativeresultcount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.1.13 on 2024-04-30 07:23

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("profiles", "0021_result_num_options"),
]

operations = [
migrations.CreateModel(
name="CumulativeResultCount",
fields=[],
options={
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("profiles.result",),
),
]
12 changes: 12 additions & 0 deletions profiles/tests/api/test_postal_code_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,15 @@ def test_non_existing_postal_code_string(api_client):
response = api_client.get(url)
assert response.status_code == 200
assert response.json()["count"] == 0


@pytest.mark.django_db
def test_cumulative_results(
api_client, results, postal_code_types, postal_code_results
):
url = "/api/v1/cumulativeresult/"
response = api_client.get(url)
assert response.status_code == 200
json_data = response.json()
assert json_data["count"] == results.count()
assert json_data["results"][0]["sum_of_count"] == 6
Loading

0 comments on commit 006f039

Please sign in to comment.