diff --git a/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml new file mode 100644 index 00000000..af4d872a --- /dev/null +++ b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml @@ -0,0 +1,60 @@ +name: Time Tacker V1 CI + +on: + pull_request: + branches: [master] + +jobs: + time-tracker-ci: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.9] + + steps: + - name: Checking out code from the repository + uses: actions/checkout@v2 + + - name: Setting up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements/time_tracker_api/dev.txt + pip install -r requirements/time_tracker_events/dev.txt + + - name: Login to azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Get vault from azure + uses: Azure/get-keyvault-secrets@v1 + with: + keyvault: "time-tracker-secrets" + secrets: "MS-CLIENT-ID, MS-AUTHORITY, MS-SCOPE, MS-SECRET, MS-ENDPOINT, USERID, AZURE-APP-CONFIGURATION-CONNECTION-STRING, DATABASE-ACCOUNT-URI, DATABASE-MASTER-KEY, DATABASE-NAME, AZURE-STORAGE-ACCOUNT-KEY" + id: timeTrackerAzureVault + + - name: Run tests + env: + MS_AUTHORITY: ${{ steps.timeTrackerAzureVault.outputs.MS-AUTHORITY }} + MS_CLIENT_ID: ${{ steps.timeTrackerAzureVault.outputs.MS-CLIENT-ID }} + MS_SCOPE: ${{ steps.timeTrackerAzureVault.outputs.MS-SCOPE }} + MS_SECRET: ${{ steps.timeTrackerAzureVault.outputs.MS-SECRET }} + MS_ENDPOINT: ${{ steps.timeTrackerAzureVault.outputs.MS-ENDPOINT }} + USERID: ${{ steps.timeTrackerAzureVault.outputs.USERID }} + AZURE_APP_CONFIGURATION_CONNECTION_STRING: ${{ steps.timeTrackerAzureVault.outputs.AZURE-APP-CONFIGURATION-CONNECTION-STRING }} + DATABASE_ACCOUNT_URI: ${{ steps.timeTrackerAzureVault.outputs.DATABASE-ACCOUNT-URI }} + DATABASE_MASTER_KEY: ${{ steps.timeTrackerAzureVault.outputs.DATABASE-MASTER-KEY }} + DATABASE_NAME: ${{ steps.timeTrackerAzureVault.outputs.DATABASE-NAME }} + AZURE_STORAGE_ACCOUNT_KEY: ${{ steps.timeTrackerAzureVault.outputs.AZURE-STORAGE-ACCOUNT-KEY }} + run: | + pytest tests + + - name: Test the build of the app + run: | + docker build . diff --git a/V2/serverless.yml b/V2/serverless.yml index c6c5e34b..75f01cab 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: @@ -63,9 +63,9 @@ functions: - http: true x-azure-settings: methods: - - PUT + - PUT route: activities/{id} - authLevel: anonymous + authLevel: anonymous create_activity: handler: time_tracker/activities/interface.create_activity @@ -86,3 +86,23 @@ functions: - POST route: time-entries/ authLevel: anonymous + + delete_time_entry: + handler: time_tracker/time_entries/interface.delete_time_entry + events: + - http: true + x-azure-settings: + methods: + - DELETE + 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 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 f801dad9..fc1e27b1 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -1,12 +1,28 @@ +import pytest import json +import pytest +from faker import Faker import azure.functions as func import time_tracker.time_entries._application._time_entries as azure_time_entries +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 + TIME_ENTRY_URL = "/api/time-entries/" +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry() -> domain_time_entries.TimeEntry: + def _new_time_entry(time_entry: domain_time_entries.TimeEntry, database: DB): + dao = infrastructure_time_entries.TimeEntriesSQLDao(database) + new_time_entry = dao.create(time_entry) + return new_time_entry + return _new_time_entry + + def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_all_attributes( test_db, time_entry_factory, activity_factory, insert_activity ): @@ -26,3 +42,78 @@ def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_ assert response.status_code == 201 assert time_entry_json_data == time_entry_body + + +def test__delete_time_entries_azure_endpoint__returns_an_time_entry_with_true_deleted__when_its_id_is_found( + 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) + + req = func.HttpRequest( + method='DELETE', + body=None, + url=TIME_ENTRY_URL, + route_params={"id": inserted_time_entry.id}, + ) + + response = azure_time_entries._delete_time_entry.delete_time_entry(req) + time_entry_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == 200 + assert time_entry_json_data['deleted'] is True + + +def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( +): + req = func.HttpRequest( + method="DELETE", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_time_entries._delete_time_entry.delete_time_entry(req) + + assert response.status_code == 400 + assert response.get_body() == b'Invalid Format ID' + + +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) + + 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 == 200 + assert time_entry_json_data == [inserted_time_entry] + + +def test__get_latest_entries_azure_endpoint__returns_No_time_entries_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 == 404 + assert response.get_body() == b'No time entries found' diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index 51ee5e5d..8568bdb6 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -10,7 +10,10 @@ @pytest.fixture(name='activity_factory') def _activity_factory() -> activities_domain.Activity: def _make_activity( - name: str = Faker().name(), description: str = Faker().sentence(), deleted: bool = False, status: int = 1 + name: str = Faker().name(), + description: str = Faker().sentence(), + deleted: bool = False, + status: int = 1, ): activity = activities_domain.Activity( id=None, @@ -20,6 +23,7 @@ def _make_activity( status=status ) return activity + return _make_activity diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py index 403f80c6..d2522886 100644 --- a/V2/tests/integration/daos/time_entries_dao_test.py +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -1,5 +1,5 @@ import pytest - +from faker import Faker import time_tracker.time_entries._domain as domain import time_tracker.time_entries._infrastructure as infrastructure @@ -46,3 +46,52 @@ def test__time_entry__returns_None__when_not_saves_correctly( inserted_time_entry = dao.create(time_entry_to_insert) assert inserted_time_entry is None + + +def test_delete__returns_an_time_entry_with_true_deleted__when_an_time_entry_matching_its_id_is_found( + create_fake_dao, test_db, time_entry_factory, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + existent_time_entry = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]") + inserted_time_entry = dao.create(existent_time_entry) + + result = dao.delete(inserted_time_entry.id) + + assert result.deleted is True + + +def test_delete__returns_none__when_no_time_entry_matching_its_id_is_found( + create_fake_dao, test_db +): + dao = create_fake_dao(test_db) + + result = dao.delete(Faker().pyint()) + + assert result 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 bd5ce085..9bf0720b 100644 --- a/V2/tests/unit/services/time_entry_service_test.py +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -1,4 +1,7 @@ +from faker import Faker + from time_tracker.time_entries._domain import TimeEntryService +from faker import Faker def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry(mocker, time_entry_factory): @@ -12,3 +15,33 @@ def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry( assert time_entry_dao.create.called assert expected_time_entry == actual_time_entry + + +def test__delete_time_entry__uses_the_time_entry_dao__to_delete_time_entry_selected( + mocker, +): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + delete=mocker.Mock(return_value=expected_time_entry) + ) + + time_entry_service = TimeEntryService(time_entry_dao) + deleted_time_entry = time_entry_service.delete(Faker().pyint()) + + assert time_entry_dao.delete.called + assert expected_time_entry == deleted_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 d2a31eb7..311db69d 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 @@ -1,4 +1,5 @@ from pytest_mock import MockFixture +from faker import Faker from time_tracker.time_entries._domain import _use_cases @@ -16,3 +17,29 @@ def test__create_time_entry_function__uses_the_time_entries_service__to_create_t assert time_entry_service.create.called assert expected_time_entry == actual_time_entry + + +def test__delete_time_entry_function__uses_the_time_entry_service__to_delete_time_entry_selected( + mocker: MockFixture, +): + expected_time_entry = mocker.Mock() + time_entry_service = mocker.Mock(delete=mocker.Mock(return_value=expected_time_entry)) + + time_entry_use_case = _use_cases.DeleteTimeEntryUseCase(time_entry_service) + deleted_time_entry = time_entry_use_case.delete_time_entry(Faker().pyint()) + + assert time_entry_service.delete.called + assert expected_time_entry == deleted_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 6e4ba9c3..7b74e111 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._time_entries import create_time_entry \ No newline at end of file +from ._time_entries import create_time_entry, delete_time_entry, get_latest_entries \ No newline at end of file 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 b46cddce..a508b990 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1,2 +1,4 @@ # flake8: noqa -from ._create_time_entry import create_time_entry \ No newline at end of file +from ._create_time_entry import create_time_entry +from ._delete_time_entry import delete_time_entry +from ._get_latest_entries import get_latest_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py new file mode 100644 index 00000000..bbf76eab --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py @@ -0,0 +1,36 @@ +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def delete_time_entry(req: func.HttpRequest) -> func.HttpResponse: + time_entry_dao = _infrastructure.TimeEntriesSQLDao(DB()) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.DeleteTimeEntryUseCase(time_entry_service) + + try: + time_entry_id = int(req.route_params.get("id")) + deleted_time_entry = use_case.delete_time_entry(time_entry_id) + if not deleted_time_entry: + return func.HttpResponse( + body="Not found", + status_code=404, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(deleted_time_entry.__dict__, default=str), + status_code=200, + mimetype="application/json", + ) + + except ValueError: + return func.HttpResponse( + body=b"Invalid Format ID", + status_code=400, + mimetype="application/json" + ) 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..8c7e8a10 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_latest_entries.py @@ -0,0 +1,47 @@ +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +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="No owner id found", + status_code=404, + mimetype="application/json" + ) + + 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="No time entries found", + status_code=404, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(time_entries, default=str), + status_code=200, + mimetype="application/json", + ) + + except ValueError: + return func.HttpResponse( + body=b"Invalid Format ID", + status_code=400, + mimetype="application/json" + ) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index a8b2081c..0e6a75ed 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -4,4 +4,6 @@ from ._services import TimeEntryService from ._use_cases import ( CreateTimeEntryUseCase, + DeleteTimeEntryUseCase, + GetLastestTimeEntryUseCase, ) \ No newline at end of file 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 5d04c861..3371781c 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 @@ -1,4 +1,5 @@ import abc +import typing from time_tracker.time_entries._domain import TimeEntry @@ -7,3 +8,11 @@ class TimeEntriesDao(abc.ABC): @abc.abstractmethod def create(self, time_entry_data: TimeEntry) -> TimeEntry: pass + + @abc.abstractmethod + def delete(self, id: int) -> 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 d7aaf3ba..448b07cf 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -1,4 +1,5 @@ from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao +import typing class TimeEntryService: @@ -8,3 +9,9 @@ def __init__(self, time_entry_dao: TimeEntriesDao): def create(self, time_entry_data: TimeEntry) -> TimeEntry: return self.time_entry_dao.create(time_entry_data) + + def delete(self, id: int) -> TimeEntry: + return self.time_entry_dao.delete(id) + + 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 41aca738..9ed353f7 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,2 +1,4 @@ # flake8: noqa from ._create_time_entry_use_case import CreateTimeEntryUseCase +from ._delete_time_entry_use_case import DeleteTimeEntryUseCase +from ._get_latest_entries_use_case import GetLastestTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py new file mode 100644 index 00000000..a195c303 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_delete_time_entry_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService + + +class DeleteTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def delete_time_entry(self, id: int) -> TimeEntry: + return self.time_entry_service.delete(id) 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_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py index d233f3e9..805a9a37 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py @@ -1,6 +1,6 @@ import dataclasses - import sqlalchemy +import typing import time_tracker.time_entries._domain as domain from time_tracker._infrastructure import _db @@ -47,3 +47,28 @@ def create(self, time_entry_data: domain.TimeEntry) -> domain.TimeEntry: def __create_time_entry_dto(self, time_entry: dict) -> domain.TimeEntry: time_entry = {key: time_entry.get(key) for key in self.time_entry_key} return domain.TimeEntry(**time_entry) + + def delete(self, time_entry_id: int) -> domain.TimeEntry: + query = ( + self.time_entry.update() + .where(self.time_entry.c.id == time_entry_id) + .values({"deleted": True}) + ) + self.db.get_session().execute(query) + query_deleted_time_entry = sqlalchemy.sql.select(self.time_entry).where(self.time_entry.c.id == time_entry_id) + time_entry = self.db.get_session().execute(query_deleted_time_entry).one_or_none() + return self.__create_time_entry_dto(dict(time_entry)) if time_entry else None + + 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 d0182780..ca758138 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1,2 +1,4 @@ # flake8: noqa -from ._application import create_time_entry \ No newline at end of file +from ._application import create_time_entry +from ._application import delete_time_entry +from ._application import get_latest_entries