diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b29b069..8cd38258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.41.0 (2021-10-28) +### Feature +* TT-366 V2 - PUT update activity ([#331](https://github.com/ioet/time-tracker-backend/issues/331)) ([`500a5d0`](https://github.com/ioet/time-tracker-backend/commit/500a5d0261497ce9aa9a9040342fea94dbe70704)) + ## v0.40.0 (2021-10-27) ### Feature * TT-367 V2 - Delete Activity ([#330](https://github.com/ioet/time-tracker-backend/issues/330)) ([`6ba8320`](https://github.com/ioet/time-tracker-backend/commit/6ba8320c6ddd6599679dfbbbaf9ac1dba9addb8d)) diff --git a/V2/serverless.yml b/V2/serverless.yml index 223c8a33..0d085c36 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -63,6 +63,16 @@ functions: - http: true x-azure-settings: methods: - - PUT + - PUT route: activities/{id} + authLevel: anonymous + + create_activity: + handler: time_entries/interface.create_activity + events: + - http: true + x-azure-settings: + methods: + - POST + route: activities/ authLevel: anonymous diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index ef5ba3b0..05b22801 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -80,3 +80,22 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act assert response.status_code == 200 assert activitiy_json_data == json.dumps(new_activity) + +def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes( + create_temp_activities, + ): + activities_json, tmp_directory = create_temp_activities + activities._create_activity._JSON_PATH = tmp_directory + + activity_body = {'id': None, 'name': Faker().user_name(), 'description': Faker().sentence(),'deleted': Faker().uuid4() ,'status': 'active', 'tenant_id': Faker().uuid4()} + body = json.dumps(activity_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body= body, + url=ACTIVITY_URL, + ) + + response = activities.create_activity(req) + activitiy_json_data = response.get_body() + assert response.status_code == 201 + assert activitiy_json_data == body \ No newline at end of file diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py index 00ce99cf..f2b0dacd 100644 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ b/V2/tests/integration/daos/activities_json_dao_test.py @@ -132,3 +132,18 @@ def test_update__returns_none__when_doesnt_found_one_activity_to_update( result = activities_json_dao.update('', activity_data) assert result == None + +def test_create_activity__returns_an_activity_dto__when_create_an_activity_that_matches_attributes(create_fake_activities): + create_fake_activities([]) + + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + activity_data = { + "name": "test_name", + "description": "test_description", + "tenant_id": "test_tenant_id", + "id": "test_id", + "deleted": "test_deleted", + "status": "test_status", + } + result = activities_json_dao.create_activity(activity_data) + assert result == Activity(**activity_data) \ No newline at end of file diff --git a/V2/tests/unit/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py index 772b3e15..9315d24f 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -58,3 +58,15 @@ def test__update_activity__uses_the_activity_dao__to_update_one_activity( assert activity_dao.update.called assert expected_activity == updated_activity + +def test__create_activity__uses_the_activity_dao__to_create_an_activity(mocker): + expected_activity = mocker.Mock() + activity_dao = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + activity_service = ActivityService(activity_dao) + + actual_activity = activity_service.create_activity(Faker().pydict()) + + assert activity_dao.create_activity.called + assert expected_activity == actual_activity diff --git a/V2/tests/unit/use_cases/activities_use_case_test.py b/V2/tests/unit/use_cases/activities_use_case_test.py index f3e9a38b..793c32d6 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -36,6 +36,20 @@ def test__get_activity_by_id_function__uses_the_activity_service__to_retrieve_ac assert expected_activity == actual_activity +def test__create_activity_function__uses_the_activities_service__to_create_activity( + mocker: MockFixture, + ): + expected_activity = mocker.Mock() + activity_service = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + + activity_use_case = _use_cases.CreateActivityUseCase(activity_service) + actual_activity = activity_use_case.create_activity(fake.pydict()) + + assert activity_service.create_activity.called + assert expected_activity == actual_activity + def test__delete_activity_function__uses_the_activity_service__to_change_activity_status( mocker: MockFixture, ): @@ -50,7 +64,6 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit assert activity_service.delete.called assert expected_activity == deleted_activity - def test__update_activity_function__uses_the_activities_service__to_update_an_activity( mocker: MockFixture, ): diff --git a/V2/time_entries/_application/__init__.py b/V2/time_entries/_application/__init__.py index faa68527..c8f26492 100644 --- a/V2/time_entries/_application/__init__.py +++ b/V2/time_entries/_application/__init__.py @@ -1,3 +1,4 @@ from ._activities import get_activities from ._activities import delete_activity from ._activities import update_activity +from ._activities import create_activity diff --git a/V2/time_entries/_application/_activities/__init__.py b/V2/time_entries/_application/_activities/__init__.py index 3482a9c6..ab7d3844 100644 --- a/V2/time_entries/_application/_activities/__init__.py +++ b/V2/time_entries/_application/_activities/__init__.py @@ -1,3 +1,4 @@ from ._get_activities import get_activities from ._delete_activity import delete_activity from ._update_activity import update_activity +from ._create_activity import create_activity diff --git a/V2/time_entries/_application/_activities/_create_activity.py b/V2/time_entries/_application/_activities/_create_activity.py new file mode 100644 index 00000000..29594e27 --- /dev/null +++ b/V2/time_entries/_application/_activities/_create_activity.py @@ -0,0 +1,59 @@ +import json +import logging +import dataclasses +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure + +_JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/activities_data.json' +) + + +def create_activity(req: func.HttpRequest) -> func.HttpResponse: + activity_dao = _infrastructure.ActivitiesJsonDao(_JSON_PATH) + activity_service = _domain.ActivityService(activity_dao) + use_case = _domain._use_cases.CreateActivityUseCase(activity_service) + logging.info( + 'Python HTTP trigger function processed a request to create an activity.' + ) + activity_data = req.get_json() + validation_errors = _validate_activity(activity_data) + if validation_errors: + return func.HttpResponse( + body=validation_errors, status_code=400, mimetype="application/json" + ) + activity_to_create = _domain.Activity( + id= None, + name=activity_data['name'], + description=activity_data['description'], + status=activity_data['status'], + deleted=activity_data['deleted'], + tenant_id=activity_data['tenant_id'] + ) + + created_activity = use_case.create_activity(activity_to_create.__dict__) + if not create_activity: + return func.HttpResponse( + body={'error': 'activity could not be created'}, + status_code=500, + mimetype="application/json", + ) + return func.HttpResponse( + body=json.dumps(created_activity.__dict__), + status_code=201, + mimetype="application/json" + ) + + +def _validate_activity(activity_data: dict) -> typing.List[str]: + activity_fields = [field.name for field in dataclasses.fields(_domain.Activity)] + + missing_keys = [field for field in activity_fields if field not in activity_data] + return [ + f'The {missing_key} key is missing in the input data' + for missing_key in missing_keys + ] \ No newline at end of file diff --git a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py index d2f9e4c7..2037841d 100644 --- a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py @@ -19,3 +19,11 @@ def delete(self, id: str) -> Activity: @abc.abstractmethod def update(self, id: str, new_activity: dict) -> Activity: pass + + @abc.abstractmethod + def create_activity(self, activity_data: dict) -> Activity: + pass + + @abc.abstractmethod + def delete(self, id: str) -> Activity: + pass diff --git a/V2/time_entries/_domain/_services/_activity.py b/V2/time_entries/_domain/_services/_activity.py index f4be7836..8d29a7ab 100644 --- a/V2/time_entries/_domain/_services/_activity.py +++ b/V2/time_entries/_domain/_services/_activity.py @@ -17,3 +17,6 @@ def delete(self, activity_id: str) -> Activity: def update(self, activity_id: str, new_activity: dict) -> Activity: return self.activities_dao.update(activity_id, new_activity) + + def create_activity(self, activity_data: dict) -> Activity: + return self.activities_dao.create_activity(activity_data) diff --git a/V2/time_entries/_domain/_use_cases/__init__.py b/V2/time_entries/_domain/_use_cases/__init__.py index 64c9bb6b..642d2425 100644 --- a/V2/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_entries/_domain/_use_cases/__init__.py @@ -2,3 +2,4 @@ from ._get_activity_by_id_use_case import GetActivityUseCase from ._delete_activity_use_case import DeleteActivityUseCase from ._update_activity_use_case import UpdateActivityUseCase +from ._create_activity_use_case import CreateActivityUseCase diff --git a/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py new file mode 100644 index 00000000..a7f7a66e --- /dev/null +++ b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py @@ -0,0 +1,11 @@ +from time_entries._domain import ActivityService, Activity +import typing + + +class CreateActivityUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def create_activity(self, activity_data: dict ) -> Activity: + return self.activity_service.create_activity(activity_data) + diff --git a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py index dfc41d04..ab8f5765 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py +++ b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py @@ -3,7 +3,6 @@ import json import typing - class ActivitiesJsonDao(ActivitiesDao): def __init__(self, json_data_file_path: str): self.json_data_file_path = json_data_file_path @@ -77,6 +76,19 @@ def update(self, activity_id: str, new_activity: dict) -> Activity: except FileNotFoundError: return None + def create_activity(self, activity_data: dict) -> Activity: + activities = self.__get_activities_from_file() + activities.append(activity_data) + + try: + with open(self.json_data_file_path, 'w') as outfile: + json.dump(activities, outfile) + + return self.__create_activity_dto(activity_data) + except FileNotFoundError: + print("Can not create activity") + + def __get_activities_from_file(self) -> typing.List[dict]: try: file = open(self.json_data_file_path) diff --git a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json index 0d949902..961251db 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json +++ b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json @@ -63,4 +63,3 @@ "_ts": 1632331515 } ] - diff --git a/V2/time_entries/interface.py b/V2/time_entries/interface.py index ffe31e51..1f1fc805 100644 --- a/V2/time_entries/interface.py +++ b/V2/time_entries/interface.py @@ -1,3 +1,4 @@ from ._application import get_activities from ._application import delete_activity from ._application import update_activity +from ._application import create_activity \ No newline at end of file diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index 2e5aee81..9657c071 100644 --- a/requirements/time_tracker_api/dev.txt +++ b/requirements/time_tracker_api/dev.txt @@ -6,6 +6,7 @@ # For development # Tests +Faker==4.0.2 pytest==5.2.0 Flask_sqlalchemy diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index 6fd17f94..77ed3a0a 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -3,11 +3,12 @@ # Dependencies -r ../commons.txt -r ../azure_cosmos.txt --r ../sql_db.txt +# -r ../sql_db.txt # For production releases #Required by Flask +Faker==4.0.2 Flask==1.1.1 Flask-WTF==0.15.1 flake8==3.7.9 diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index eb9b6f12..9f86a39e 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.40.0' +__version__ = '0.41.0'