diff --git a/solution/services/backend/api/v1/clients/__init__.py b/solution/services/backend/api/v1/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/services/backend/api/v1/clients/schemas.py b/solution/services/backend/api/v1/clients/schemas.py new file mode 100644 index 0000000..4be98e5 --- /dev/null +++ b/solution/services/backend/api/v1/clients/schemas.py @@ -0,0 +1,14 @@ +import typing +from uuid import UUID + +from ninja import ModelSchema + +from apps.client.models import Client + + +class Client(ModelSchema): + client_id: UUID + + class Meta: + model = Client + exclude: typing.ClassVar[tuple[str]] = (Client.id.field.name,) diff --git a/solution/services/backend/api/v1/clients/tests.py b/solution/services/backend/api/v1/clients/tests.py new file mode 100644 index 0000000..03a323f --- /dev/null +++ b/solution/services/backend/api/v1/clients/tests.py @@ -0,0 +1,156 @@ +from http import HTTPStatus as status +from django.test import TestCase +from django.urls import reverse +import json +from uuid import uuid4 +from apps.client.models import Client + + +class ClientTests(TestCase): + def setUp(self): + self.client_1 = Client.objects.create( + login="testuser1", age=25, location="City1", gender="MALE" + ) + self.client_2 = Client.objects.create( + login="testuser2", age=30, location="City2", gender="FEMALE" + ) + + self.bulk_url = "/clients/bulk" + self.get_url = "/clients" + + def test_bulk_create_or_update(self): + client_3_id = str(uuid4()) + client_data = [ + { + "client_id": client_3_id, + "login": "newusers", + "age": 21, + "location": "City1", + "gender": "FEMALE", + }, + { + "client_id": str(self.client_1.id), + "login": "updateduser", + "age": 26, + "location": "City1", + "gender": "MALE", + }, + { + "client_id": client_3_id, + "login": "newusersa", + "age": 25, + "location": "City1", + "gender": "FEMALE", + }, + { + "client_id": client_3_id, + "login": "newuser", + "age": 22, + "location": "City3", + "gender": "MALE", + }, + ] + response = self.client.post( + self.bulk_url, + data=json.dumps(client_data), + content_type="application/json", + ) + client_3 = Client.objects.get(id=client_3_id) + self.client_1.refresh_from_db() + + self.assertEqual(response.status_code, status.CREATED) + self.assertEqual(Client.objects.count(), 3) + self.assertEqual(self.client_1.login, "updateduser") + self.assertEqual(client_3.location, "City3") + self.assertEqual(client_3.login, "newuser") + self.assertEqual(len(response.json()), 2) + self.assertEqual(response.json()[1]["client_id"], str(client_3.id)) + self.assertEqual(response.json()[1]["login"], client_3.login) + + def test_bulk_create_invalid_data(self): + client_data = [ + { + "client_id": "invalid_uuid", + "login": "baduser", + "age": 150, + "location": "City4", + "gender": "UNKNOWN", + } + ] + response = self.client.post( + self.bulk_url, + data=json.dumps(client_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_duplicate_advertiser_ids(self): + adv_id = uuid4() + data = [ + { + "client_id": str(adv_id), + "login": "baduser", + "age": 10, + "location": "City4", + "gender": "FEMALE", + }, + { + "client_id": str(adv_id), + "login": "Last", + "age": 14, + "location": "City4", + "gender": "MALE", + }, + ] + response = self.client.post( + self.bulk_url, json.dumps(data), content_type="application/json" + ) + client = Client.objects.get(id=adv_id) + + self.assertEqual(response.status_code, status.CREATED) + self.assertEqual(client.login, "Last") + self.assertEqual(client.age, 14) + self.assertEqual(client.gender, "MALE") + + def test_invalid_client_id_format(self): + client_data = [ + { + "client_id": "invalid_uuid", + "login": "baduser", + "age": 150, + "location": "City4", + "gender": "UNKNOWN", + } + ] + response = self.client.post( + self.bulk_url, + json.dumps(client_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_empty_bulk_request(self): + response = self.client.post( + self.bulk_url, json.dumps([]), content_type="application/json" + ) + + self.assertEqual(response.status_code, status.CREATED) + self.assertEqual(len(response.json()), 0) + + def test_get_client_success(self): + response = self.client.get(f"{self.get_url}/{self.client_1.id}") + + self.assertEqual(response.status_code, status.OK) + self.assertEqual(response.json()["login"], self.client_1.login) + + def test_get_client_not_found(self): + response = self.client.get(f"{self.get_url}/{uuid4()}") + + self.assertEqual(response.status_code, status.NOT_FOUND) + + def test_get_client_invalid_uuid(self): + response = self.client.get(f"{self.get_url}/invalid_uuid") + + self.assertEqual(response.status_code, status.BAD_REQUEST) diff --git a/solution/services/backend/api/v1/clients/views.py b/solution/services/backend/api/v1/clients/views.py new file mode 100644 index 0000000..020da79 --- /dev/null +++ b/solution/services/backend/api/v1/clients/views.py @@ -0,0 +1,63 @@ +from collections import defaultdict +from http import HTTPStatus as status +from uuid import UUID + +from django.db import transaction +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from ninja import Router + +from api.v1 import schemas as global_schemas +from api.v1.clients import schemas +from apps.client.models import Client + +router = Router(tags=["clients"]) + + +@router.post( + "/bulk", + response={ + status.CREATED: list[schemas.Client], + status.BAD_REQUEST: global_schemas.BadRequestError, + }, +) +def bulk_create_or_update( + request: HttpRequest, data: list[schemas.Client] +) -> tuple[status, list[Client]]: + latest_clients = defaultdict(lambda: None) + + for item in reversed(data): + Client(id=item.client_id, **item.dict(exclude={"client_id"})).validate( + validate_unique=False, + validate_constraints=False, + ) + if latest_clients[item.client_id] is None: + latest_clients[item.client_id] = item + + unique_clients = reversed(list(latest_clients.values())) + + result = [] + + with transaction.atomic(): + for client in unique_clients: + client_instance, _ = Client.objects.update_or_create( + id=client.client_id, + defaults={**dict(client)}, + ) + result.append(client_instance) + + return status.CREATED, result + + +@router.get( + "/{client_id}", + response={ + status.OK: schemas.Client, + status.BAD_REQUEST: global_schemas.BadRequestError, + status.NOT_FOUND: global_schemas.NotFoundError, + }, +) +def get_client( + request: HttpRequest, client_id: UUID +) -> tuple[status, schemas.Client]: + return status.OK, get_object_or_404(Client, id=client_id)