diff --git a/V2/serverless.yml b/V2/serverless.yml index ba8edb52..9b31ee0b 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -1,6 +1,6 @@ service: azure-time-tracker -frameworkVersion: "2" +frameworkVersion: '2' provider: name: azure @@ -23,18 +23,18 @@ plugins: package: patterns: - - "!env/**" - - "!.env/**" - - "!local.settings.json" - - "!.vscode/**" - - "!__pycache__/**" - - "!node_modules/**" - - "!.python_packages/**" - - "!.funcignore" - - "!package.json" - - "!package-lock.json" - - "!.gitignore" - - "!.git/**" + - '!env/**' + - '!.env/**' + - '!local.settings.json' + - '!.vscode/**' + - '!__pycache__/**' + - '!node_modules/**' + - '!.python_packages/**' + - '!.funcignore' + - '!package.json' + - '!package-lock.json' + - '!.gitignore' + - '!.git/**' functions: get_activities: @@ -117,6 +117,16 @@ functions: route: time-entries/{id} authLevel: anonymous + get_latest_time_entry: + handler: time_tracker/time_entries/interface.get_latest_entries + events: + - http: true + x-azure-settings: + methods: + - GET + route: time-entries/latest/ + authLevel: anonymous + create_customer: handler: time_tracker/customers/interface.create_customer events: @@ -165,4 +175,5 @@ functions: methods: - POST route: projects/ + authLevel: anonymous diff --git a/V2/tests/api/azure/time_entry_azure_endpoints_test.py b/V2/tests/api/azure/time_entry_azure_endpoints_test.py index fcc8dea0..42e3d5ec 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -9,6 +9,7 @@ from time_tracker._infrastructure import DB from time_tracker.time_entries import _domain as domain_time_entries from time_tracker.time_entries import _infrastructure as infrastructure_time_entries +from time_tracker.utils.enums import ResponseEnums TIME_ENTRY_URL = "/api/time-entries/" @@ -208,3 +209,42 @@ def test__update_time_entries_azure_endpoint__returns_a_status_code_400__when_ti assert response.status_code == 400 assert response.get_body() == b'Incorrect time entry body' + + +def test__get_latest_entries_azure_endpoint__returns_a_list_of_latest_time_entries__when_an_owner_id_match( + test_db, time_entry_factory, insert_time_entry, insert_activity, activity_factory, +): + inserted_activity = insert_activity(activity_factory(), test_db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], technologies="[jira,sql]") + inserted_time_entry = insert_time_entry(time_entry_body, test_db).__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=TIME_ENTRY_URL+"latest/", + params={"owner_id": inserted_time_entry["owner_id"]}, + ) + + response = azure_time_entries._get_latest_entries.get_latest_entries(req) + time_entry_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == HTTPStatus.OK + assert time_entry_json_data == [inserted_time_entry] + + +def test__get_latest_entries_azure_endpoint__returns_not_found__when_recieve_an_invalid_owner_id( + test_db, insert_activity, activity_factory, +): + insert_activity(activity_factory(), test_db) + + req = func.HttpRequest( + method='GET', + body=None, + url=TIME_ENTRY_URL+"latest/", + params={"owner_id": Faker().pyint()}, + ) + + response = azure_time_entries._get_latest_entries.get_latest_entries(req) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.get_body().decode("utf-8") == ResponseEnums.NOT_FOUND.value diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py index e78af556..e48241cc 100644 --- a/V2/tests/integration/daos/time_entries_dao_test.py +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -90,7 +90,7 @@ def test_update__returns_an_time_entry_dto__when_found_one_time_entry_to_update( def test_update__returns_none__when_doesnt_found_one_time_entry_to_update( - test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory ): dao = create_fake_dao(test_db) inserted_activity = insert_activity(activity_factory(), dao.db) @@ -153,3 +153,29 @@ def test__get_by_id__returns_none__when_no_time_entry_matches_by_id( time_entry = dao.get_by_id(Faker().pyint()) assert time_entry is None + + +def test_get_latest_entries__returns_a_list_of_latest_time_entries__when_an_owner_id_match( + create_fake_dao, time_entry_factory, insert_activity, activity_factory, test_db +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entry_to_insert = time_entry_factory( + activity_id=inserted_activity.id, + technologies="[jira,sql]") + inserted_time_entry = dao.create(time_entry_to_insert) + + result = dao.get_latest_entries(int(inserted_time_entry.owner_id)) + + assert result == [inserted_time_entry.__dict__] + + +def test_get_latest_entries__returns_none__when_an_owner_id_is_not_found( + create_fake_dao, test_db, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + insert_activity(activity_factory(), dao.db) + + result = dao.get_latest_entries(Faker().pyint()) + + assert result is None diff --git a/V2/tests/unit/services/time_entry_service_test.py b/V2/tests/unit/services/time_entry_service_test.py index 1992324f..d1596471 100644 --- a/V2/tests/unit/services/time_entry_service_test.py +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -72,3 +72,18 @@ def test__get_by_id__uses_the_time_entry_dao__to_retrieve_one_time_entry(mocker) assert time_entry_dao.get_by_id.called assert expected_time_entry == actual_time_entry + + +def test__get_latest_entries__uses_the_time_entry_dao__to_get_last_entries( + mocker, +): + expected_latest_time_entries = mocker.Mock() + time_entry_dao = mocker.Mock( + get_latest_entries=mocker.Mock(return_value=expected_latest_time_entries) + ) + + time_entry_service = TimeEntryService(time_entry_dao) + latest_time_entries = time_entry_service.get_latest_entries(Faker().pyint(), Faker().pyint()) + + assert expected_latest_time_entries == latest_time_entries + assert time_entry_dao.get_latest_entries.called diff --git a/V2/tests/unit/use_cases/time_entries_use_case_test.py b/V2/tests/unit/use_cases/time_entries_use_case_test.py index 05937789..f96666cb 100644 --- a/V2/tests/unit/use_cases/time_entries_use_case_test.py +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -75,3 +75,16 @@ def test__get_time_entry_by_id_function__uses_the_time_entry_service__to_retriev assert time_entry_service.get_by_id.called assert expected_time_entries == actual_time_entry + + +def test__get_latest_entries_function__uses_the_time_entry_service__to_get_last_entries( + mocker: MockFixture, +): + expected_latest_time_entries = mocker.Mock() + time_entry_service = mocker.Mock(get_latest_entries=mocker.Mock(return_value=expected_latest_time_entries)) + + time_entry_use_case = _use_cases.GetLastestTimeEntryUseCase(time_entry_service) + latest_time_entries = time_entry_use_case.get_latest_entries(Faker().pyint(), Faker().pyint()) + + assert time_entry_service.get_latest_entries.called + assert expected_latest_time_entries == latest_time_entries diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index eb817c22..4d003056 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -3,3 +3,4 @@ from ._time_entries import delete_time_entry from ._time_entries import update_time_entry from ._time_entries import get_time_entries +from ._time_entries import get_latest_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py index 382fbbe4..29631650 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -3,3 +3,4 @@ from ._delete_time_entry import delete_time_entry from ._update_time_entry import update_time_entry from ._get_time_entries import get_time_entries +from ._get_latest_entries import get_latest_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py new file mode 100644 index 00000000..b813fb4f --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py @@ -0,0 +1,49 @@ +import json +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB +from time_tracker.utils.enums import ResponseEnums + + +def get_latest_entries(req: func.HttpRequest) -> func.HttpResponse: + database = DB() + time_entry_dao = _infrastructure.TimeEntriesSQLDao(database) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.GetLastestTimeEntryUseCase(time_entry_service) + + try: + owner_id = req.params.get("owner_id") + limit = req.params.get("limit") + + if not owner_id: + return func.HttpResponse( + body=ResponseEnums.NOT_FOUND.value, + status_code=HTTPStatus.NOT_FOUND, + mimetype=ResponseEnums.MIME_TYPE.value, + ) + + time_entries = use_case.get_latest_entries(int(owner_id), int(limit) if limit and int(limit) > 0 else None) + + if not time_entries or len(time_entries) == 0: + return func.HttpResponse( + body=ResponseEnums.NOT_FOUND.value, + status_code=HTTPStatus.NOT_FOUND, + mimetype=ResponseEnums.MIME_TYPE.value, + ) + + return func.HttpResponse( + body=json.dumps(time_entries, default=str), + status_code=HTTPStatus.OK, + mimetype=ResponseEnums.MIME_TYPE.value, + ) + + except ValueError: + return func.HttpResponse( + body=ResponseEnums.INVALID_ID.value, + status_code=HTTPStatus.BAD_REQUEST, + mimetype=ResponseEnums.MIME_TYPE.value, + ) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index 2034f8d3..513877bf 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -7,5 +7,6 @@ DeleteTimeEntryUseCase, UpdateTimeEntryUseCase, GetTimeEntriesUseCase, - GetTimeEntryUseCase + GetTimeEntryUseCase, + GetLastestTimeEntryUseCase, ) diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index ca4ceb98..59b9d975 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -17,9 +17,14 @@ def delete(self, id: int) -> TimeEntry: def update(self, id: int, new_time_entry: dict) -> TimeEntry: pass + @abc.abstractmethod def get_by_id(self, id: int) -> TimeEntry: pass @abc.abstractmethod def get_all(self) -> typing.List[TimeEntry]: pass + + @abc.abstractmethod + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[TimeEntry]: + pass diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index 5b3f4115..0c2b8b9b 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -21,3 +21,6 @@ def get_by_id(self, id: int) -> TimeEntry: def get_all(self) -> typing.List[TimeEntry]: return self.time_entry_dao.get_all() + + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[TimeEntry]: + return self.time_entry_dao.get_latest_entries(owner_id, limit) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py index fdd1258d..055cd850 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -4,3 +4,4 @@ from ._update_time_entry_use_case import UpdateTimeEntryUseCase from ._get_time_entry_use_case import GetTimeEntriesUseCase from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase +from ._get_latest_entries_use_case import GetLastestTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py new file mode 100644 index 00000000..c070f8ac --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_latest_entries_use_case.py @@ -0,0 +1,11 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService +import typing + + +class GetLastestTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_latest_entries(self, owner_id: int, limit: int) -> typing.List[TimeEntry]: + return self.time_entry_service.get_latest_entries(owner_id, limit) diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py index 9e7016d4..5d368e26 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py @@ -90,3 +90,17 @@ def __create_time_entry_dto(self, time_entry: dict) -> domain.TimeEntry: "end_date": str(time_entry.get("end_date"))}) time_entry = {key: time_entry.get(key) for key in self.time_entry_key} return domain.TimeEntry(**time_entry) + + def get_latest_entries(self, owner_id: int, limit: int = 20) -> typing.List[domain.TimeEntry]: + query = ( + self.time_entry.select() + .where(sqlalchemy.and_( + self.time_entry.c.owner_id == owner_id, + self.time_entry.c.deleted.is_(False) + )) + .order_by(self.time_entry.c.start_date.desc()) + .limit(limit) + ) + time_entries_data = self.db.get_session().execute(query) + list_time_entries = [dict(entry) for entry in time_entries_data] + return list_time_entries if len(list_time_entries) > 0 else None diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 8873b93d..87876204 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -3,3 +3,4 @@ from ._application import delete_time_entry from ._application import update_time_entry from ._application import get_time_entries +from ._application import get_latest_entries