diff --git a/docker-compose.yaml b/docker-compose.yaml index 89291a15..76bd5a21 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,7 +16,7 @@ services: restart: unless-stopped healthcheck: test: ["CMD-SHELL", "sh -c 'pg_isready -U admin -d postgres'"] - interval: 20s + interval: 10s timeout: 10s retries: 5 networks: diff --git a/organizator_api/app/admin.py b/organizator_api/app/admin.py index e69de29b..addf48ef 100644 --- a/organizator_api/app/admin.py +++ b/organizator_api/app/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from app.events.infrastructure.persistance.models.orm_event import ORMEvent as Event + +admin.site.register(Event) diff --git a/organizator_api/app/events/application/requests.py b/organizator_api/app/events/application/requests.py index 002f0dcf..5f738b5a 100644 --- a/organizator_api/app/events/application/requests.py +++ b/organizator_api/app/events/application/requests.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from datetime import datetime +from typing import Optional @dataclass @@ -11,3 +12,14 @@ class CreateEventRequest: end_date: datetime location: str header_image: str + + +@dataclass +class UpdateEventRequest: + name: Optional[str] = None + url: Optional[str] = None + description: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + location: Optional[str] = None + header_image: Optional[str] = None diff --git a/organizator_api/app/events/domain/repositories.py b/organizator_api/app/events/domain/repositories.py index 10f7392c..afadeb3b 100644 --- a/organizator_api/app/events/domain/repositories.py +++ b/organizator_api/app/events/domain/repositories.py @@ -1,5 +1,6 @@ import uuid from abc import ABC, abstractmethod +from datetime import datetime from typing import List from app.events.domain.models.event import Event diff --git a/organizator_api/app/events/domain/use_cases/update_event_use_case.py b/organizator_api/app/events/domain/use_cases/update_event_use_case.py index e69de29b..4bb4536c 100644 --- a/organizator_api/app/events/domain/use_cases/update_event_use_case.py +++ b/organizator_api/app/events/domain/use_cases/update_event_use_case.py @@ -0,0 +1,36 @@ +import uuid +from datetime import timezone, datetime + +from app.events.infrastructure.repository_factories import EventRepositoryFactory +from app.events.domain.models.event import Event +from app.events.application.requests import UpdateEventRequest + + +class UpdateEventUseCase: + def __init__(self) -> None: + self.event_repository = EventRepositoryFactory.create() + + def execute(self, event_id: uuid.UUID, event: UpdateEventRequest) -> Event: + original_event = self.event_repository.get(event_id) + new_event = Event( + id=event_id, + name=event.name if event.name else original_event.name, + description=event.description + if event.description + else original_event.description, + url=event.url if event.url else original_event.url, + start_date=event.start_date + if event.start_date + else original_event.start_date, + end_date=event.end_date if event.end_date else original_event.end_date, + location=event.location if event.location else original_event.location, + header_image=event.header_image + if event.header_image + else original_event.header_image, + created_at=original_event.created_at, + updated_at=datetime.now(tz=timezone.utc), + ) + + self.event_repository.update(new_event) + + return new_event diff --git a/organizator_api/app/events/infrastructure/http/urls.py b/organizator_api/app/events/infrastructure/http/urls.py index 1990fb2c..f830631f 100644 --- a/organizator_api/app/events/infrastructure/http/urls.py +++ b/organizator_api/app/events/infrastructure/http/urls.py @@ -4,10 +4,12 @@ create_new_event, get_all_events, get_event, + update_event, ) urlpatterns = [ path("new", create_new_event), path("", get_all_events), path("", get_event), + path("update/", update_event), ] diff --git a/organizator_api/app/events/infrastructure/http/views.py b/organizator_api/app/events/infrastructure/http/views.py index 83c73ff6..a3279acc 100644 --- a/organizator_api/app/events/infrastructure/http/views.py +++ b/organizator_api/app/events/infrastructure/http/views.py @@ -5,20 +5,19 @@ from django.http import HttpRequest, HttpResponse from django.views.decorators.http import require_http_methods -from app.events.application.requests import CreateEventRequest +from app.events.application.requests import CreateEventRequest, UpdateEventRequest from app.events.domain.use_cases.create_event_use_case import CreateEventUseCase from app.events.domain.exceptions import EventAlreadyExists, EventNotFound from app.events.domain.use_cases.get_all_events_use_case import GetAllEventsUseCase from app.events.application.response import EventResponse from app.events.domain.use_cases.get_event_use_case import GetEventUseCase +from app.events.domain.use_cases.update_event_use_case import UpdateEventUseCase @require_http_methods(["POST"]) def create_new_event(request: HttpRequest) -> HttpResponse: json_body = json.loads(request.body) - print(request) - try: name = json_body["name"] url = json_body["url"] @@ -73,3 +72,39 @@ def get_event(request: HttpRequest, event_id: uuid.UUID) -> HttpResponse: return HttpResponse( status=200, content=json.dumps(event_response), content_type="application/json" ) + + +@require_http_methods(["POST"]) +def update_event(request: HttpRequest, event_id: uuid.UUID) -> HttpResponse: + json_body = json.loads(request.body) + + name = json_body["name"] if "name" in json_body else None + url = json_body["url"] if "url" in json_body else None + description = json_body["description"] if "description" in json_body else None + start_date = json_body["start_date"] if "start_date" in json_body else None + end_date = json_body["end_date"] if "end_date" in json_body else None + location = json_body["location"] if "location" in json_body else None + header_image = json_body["header_image"] if "header_image" in json_body else None + + event_data = UpdateEventRequest( + name=name, + url=url, + description=description, + start_date=datetime.strptime(start_date, "%Y-%m-%dT%H:%M:%SZ") + if start_date + else None, + end_date=datetime.strptime(end_date, "%Y-%m-%dT%H:%M:%SZ") + if end_date + else None, + location=location, + header_image=header_image, + ) + + try: + UpdateEventUseCase().execute(event_id=event_id, event=event_data) + except EventAlreadyExists: + return HttpResponse(status=409, content="Event already exists") + except EventNotFound: + return HttpResponse(status=404, content="Event does not exist") + + return HttpResponse(status=201, content="Event modified correctly") diff --git a/organizator_api/app/events/infrastructure/persistance/orm_event_repository.py b/organizator_api/app/events/infrastructure/persistance/orm_event_repository.py index e9eb7e25..7af65712 100644 --- a/organizator_api/app/events/infrastructure/persistance/orm_event_repository.py +++ b/organizator_api/app/events/infrastructure/persistance/orm_event_repository.py @@ -1,4 +1,5 @@ import uuid +from datetime import datetime, timezone from typing import List from django.db import IntegrityError @@ -17,7 +18,21 @@ def create(self, event: Event) -> None: raise EventAlreadyExists() def update(self, event: Event) -> None: - pass # pragma: no cover + try: + orm_event = ORMEvent.objects.get(id=event.id) + orm_event.name = event.name + orm_event.description = event.description + orm_event.url = event.url + orm_event.start_date = event.start_date + orm_event.end_date = event.end_date + orm_event.location = event.location + orm_event.header_image = event.header_image + orm_event.updated_at = event.updated_at + orm_event.save() + except ORMEvent.DoesNotExist: + raise EventNotFound() + except IntegrityError: + raise EventAlreadyExists() def delete(self, event_id: uuid.UUID) -> None: pass # pragma: no cover @@ -27,7 +42,7 @@ def get(self, event_id: uuid.UUID) -> Event: event = self._to_domain_model(ORMEvent.objects.get(id=event_id)) return event except ORMEvent.DoesNotExist: - raise EventNotFound + raise EventNotFound() def get_all(self) -> List[Event]: return [self._to_domain_model(event) for event in ORMEvent.objects.all()] diff --git a/organizator_api/tests/events/domain/use_cases/test_get_all_events_use_case.py b/organizator_api/tests/events/domain/use_cases/test_get_all_events_use_case.py index a5e887f0..3b46e9c0 100644 --- a/organizator_api/tests/events/domain/use_cases/test_get_all_events_use_case.py +++ b/organizator_api/tests/events/domain/use_cases/test_get_all_events_use_case.py @@ -1,5 +1,4 @@ import uuid -from datetime import datetime, timezone from tests.events.domain.EventFactory import EventFactory from app.events.domain.use_cases.get_all_events_use_case import GetAllEventsUseCase @@ -11,7 +10,10 @@ def setUp(self) -> None: super().setUp() self.event_repository.clear() event = EventFactory().create() - event2 = EventFactory().create(name="HackUPC 2022") + event2 = EventFactory().create( + new_id=uuid.UUID("fb95bfb6-3361-4628-8037-999d58b7183a"), + name="HackUPC 2022", + ) self.event_repository.create(event) self.event_repository.create(event2) diff --git a/organizator_api/tests/events/domain/use_cases/test_update_event_use_case.py b/organizator_api/tests/events/domain/use_cases/test_update_event_use_case.py new file mode 100644 index 00000000..45c77221 --- /dev/null +++ b/organizator_api/tests/events/domain/use_cases/test_update_event_use_case.py @@ -0,0 +1,68 @@ +import uuid +from datetime import datetime + +from app.events.application.requests import UpdateEventRequest +from app.events.domain.use_cases.update_event_use_case import UpdateEventUseCase +from tests.events.domain.EventFactory import EventFactory +from tests.api_tests import ApiTests + + +class TestUpdateEventUseCase(ApiTests): + def setUp(self) -> None: + super().setUp() + self.event_repository.clear() + event = EventFactory().create() + event2 = EventFactory().create( + new_id=uuid.UUID("fb95bfb6-3361-4628-8037-999d58b7183a"), + name="HackUPC 2022", + ) + self.event_repository.create(event) + self.event_repository.create(event2) + + def test__given_a_update_event_request_with_only_name__when_update_an_event_with_the_data__then_the_event_is_updated( + self, + ) -> None: + # Given + new_event = UpdateEventRequest( + name="HackUPC 2021", + ) + + # When + event = UpdateEventUseCase().execute( + uuid.UUID("fb95bfb6-3361-4628-8037-999d58b7183a"), new_event + ) + + # Then + self.assertEqual(event.name, "HackUPC 2021") + + def test__given_a_update_event_request_with_all_data__when_update_an_event_with_the_data__then_the_event_is_updated( + self, + ) -> None: + # Given + new_event = UpdateEventRequest( + name="HackUPC 2021", + description="Hackathon in Barcelona 2021", + url="https://2021.hackupc.com", + start_date=datetime.strptime("2021-10-15T16:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + end_date=datetime.strptime("2021-10-17T16:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + location="The best city in the world", + ) + + # When + event = UpdateEventUseCase().execute( + uuid.UUID("fb95bfb6-3361-4628-8037-999d58b7183a"), new_event + ) + + # Then + self.assertEqual(event.name, "HackUPC 2021") + self.assertEqual(event.description, "Hackathon in Barcelona 2021") + self.assertEqual(event.url, "https://2021.hackupc.com") + self.assertEqual( + event.start_date, + datetime.strptime("2021-10-15T16:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + ) + self.assertEqual( + event.end_date, + datetime.strptime("2021-10-17T16:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + ) + self.assertEqual(event.location, "The best city in the world") diff --git a/organizator_api/tests/events/infrastructure/http/test_views.py b/organizator_api/tests/events/infrastructure/http/test_views.py index b1d9fb6c..a94a5cbd 100644 --- a/organizator_api/tests/events/infrastructure/http/test_views.py +++ b/organizator_api/tests/events/infrastructure/http/test_views.py @@ -148,3 +148,116 @@ def test__when_get_event_by_nonexistent_id__then_returns_the_event( response.content, b"Event does not exist", ) + + def test__given_information_to_update_an_event__when_update_event__then_the_event_is_updated( + self, + ) -> None: + # Given + event = EventFactory().create() + self.event_repository.create(event) + request_body = { + "name": "HackNight Ep.VI", + "url": "https://www.hacknights.dev", + "description": "The best hack-night ever", + "start_date": "2023-11-17T21:00:00Z", + "end_date": "2023-11-18T05:00:00Z", + "location": "Aula d'estudis Campus Nord", + "header_image": "https://www.hacknights.dev/images/hacknight.png", + } + + # When + response = self.client.post( + "/organizator-api/events/update/ef6f6fb3-ba12-43dd-a0da-95de8125b1cc", + json.dumps(request_body), + content_type="application/json", + ) + + # Then + self.assertEqual(response.status_code, 201) + self.assertEqual(response.content, b"Event modified correctly") + + events = self.event_repository.get_all() + self.assertEqual(len(events), 1) + event = events.pop() + self.assertEqual(event.name, "HackNight Ep.VI") + self.assertEqual(event.url, "https://www.hacknights.dev") + self.assertEqual(event.description, "The best hack-night ever") + self.assertEqual(event.start_date, datetime(2023, 11, 17, 21, 0)) + self.assertEqual(event.end_date, datetime(2023, 11, 18, 5, 0)) + self.assertEqual(event.location, "Aula d'estudis Campus Nord") + self.assertEqual( + event.header_image, "https://www.hacknights.dev/images/hacknight.png" + ) + + def test__given_only_some_information_to_update_an_event__when_update_event__then_the_event_is_updated( + self, + ) -> None: + # Given + event = EventFactory().create() + self.event_repository.create(event) + request_body = { + "description": "The biggest student hackathon in Europe taking place in Barcelona", + "url": "https://2023.hackupc.com/", + } + + # When + response = self.client.post( + "/organizator-api/events/update/ef6f6fb3-ba12-43dd-a0da-95de8125b1cc", + json.dumps(request_body), + content_type="application/json", + ) + + # Then + self.assertEqual(response.status_code, 201) + self.assertEqual(response.content, b"Event modified correctly") + + events = self.event_repository.get_all() + self.assertEqual(len(events), 1) + event = events.pop() + self.assertEqual(event.url, "https://2023.hackupc.com/") + self.assertEqual( + event.description, + "The biggest student hackathon in Europe taking place in Barcelona", + ) + + def test__given_a_event_with_the_same_name_as_one_already_created__when_update_event__then_returns_409( + self, + ) -> None: + # Given + event = EventFactory().create() + event2 = EventFactory().create( + new_id=uuid.UUID("be0f4c18-4a7c-4c1e-8a62-fc50916b6c88"), + name="HackUPC 2022", + ) + self.event_repository.create(event) + self.event_repository.create(event2) + + request_body = {"name": "HackUPC 2022"} + + # When + response = self.client.post( + "/organizator-api/events/update/ef6f6fb3-ba12-43dd-a0da-95de8125b1cc", + json.dumps(request_body), + content_type="application/json", + ) + + # Then + self.assertEqual(response.status_code, 409) + self.assertEqual(response.content, b"Event already exists") + + def test__given_no_event_in_db__when_update_event__then_returns_404( + self, + ) -> None: + # Given + request_body = {"name": "HackUPC 2022"} + + # When + response = self.client.post( + "/organizator-api/events/update/ef6f6fb3-ba12-43dd-a0da-95de8125b1cc", + json.dumps(request_body), + content_type="application/json", + ) + + # Then + self.assertEqual(response.status_code, 404) + self.assertEqual(response.content, b"Event does not exist") diff --git a/organizator_api/tests/events/infrastructure/persistance/test_orm_event_repository.py b/organizator_api/tests/events/infrastructure/persistance/test_orm_event_repository.py index e84c240b..b4bb8bd3 100644 --- a/organizator_api/tests/events/infrastructure/persistance/test_orm_event_repository.py +++ b/organizator_api/tests/events/infrastructure/persistance/test_orm_event_repository.py @@ -88,3 +88,49 @@ def test__given_a_event__when_get__then_event_is_returned(self) -> None: def test__when_get_a_non_existing_event__then_raise_event_not_found(self) -> None: with self.assertRaises(EventNotFound): ORMEventRepository().get(event_id=uuid.uuid4()) + + def test__given_a_event__when_update__then_event_is_updated(self) -> None: + # Given + event = EventFactory().create(name="HackUPC 2021") + ORMEventRepository().create(event=event) + + # When + event.name = "HackUPC 2022" + ORMEventRepository().update(event=event) + event = ORMEventRepository().get(event_id=event.id) + + # Then + self.assertEqual(event.name, "HackUPC 2022") + self.assertEqual(type(event.name), str) + + def test__given_a_event__when_update_with_a_name_that_already_exists__then_it_raises_event_already_exists( + self, + ) -> None: + # Given + event = EventFactory().create(name="HackUPC 2021") + event2 = EventFactory().create( + new_id=uuid.UUID("be0f4c18-4a7c-4c1e-8a62-fc50916b6c88"), + name="HackUPC 2022", + ) + ORMEventRepository().create(event=event) + ORMEventRepository().create(event=event2) + + # When + event.name = "HackUPC 2022" + + # Then + with self.assertRaises(EventAlreadyExists): + ORMEventRepository().update(event=event) + + def test__given_no_events_when_update_a_non_existing_event__then_it_raises_event_not_found( + self, + ) -> None: + # Given + event = EventFactory().create(name="HackUPC 2021") + + # When + event.name = "HackUPC 2022" + + # Then + with self.assertRaises(EventNotFound): + ORMEventRepository().update(event=event) diff --git a/organizator_api/tests/events/mocks/event_repository_mock.py b/organizator_api/tests/events/mocks/event_repository_mock.py index 7e320410..1911f61c 100644 --- a/organizator_api/tests/events/mocks/event_repository_mock.py +++ b/organizator_api/tests/events/mocks/event_repository_mock.py @@ -18,7 +18,22 @@ def create(self, event: Event) -> None: self.events.append(event) def update(self, event: Event) -> None: - pass + for e in self.events: + if e.name == event.name and e.id != event.id: + raise EventAlreadyExists + for e in self.events: + if e.id == event.id: + e.name = event.name + e.description = event.description + e.url = event.url + e.start_date = event.start_date + e.end_date = event.end_date + e.location = event.location + e.header_image = event.header_image + e.updated_at = event.updated_at + return + + raise EventNotFound def delete(self, event_id: uuid.UUID) -> None: pass