diff --git a/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml index c35be604..766f09bf 100644 --- a/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml +++ b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml @@ -26,7 +26,6 @@ jobs: 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: @@ -54,7 +53,6 @@ jobs: AZURE_STORAGE_ACCOUNT_KEY: ${{ steps.timeTrackerAzureVault.outputs.AZURE-STORAGE-ACCOUNT-KEY }} run: | pytest tests - - name: Test the build of the app run: | - docker build . + docker build . \ No newline at end of file diff --git a/.github/workflows/time-tracker-v1-on-push-workflow.yml b/.github/workflows/time-tracker-v1-on-push-workflow.yml index 152998b4..095712b9 100644 --- a/.github/workflows/time-tracker-v1-on-push-workflow.yml +++ b/.github/workflows/time-tracker-v1-on-push-workflow.yml @@ -26,7 +26,6 @@ jobs: 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: @@ -54,7 +53,6 @@ jobs: AZURE_STORAGE_ACCOUNT_KEY: ${{ steps.timeTrackerAzureVault.outputs.AZURE-STORAGE-ACCOUNT-KEY }} run: | pytest tests - - name: Login to docker registry uses: azure/docker-login@v1 with: @@ -64,4 +62,4 @@ jobs: - name: Build and push image run: | docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} - docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} + docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} \ No newline at end of file diff --git a/V2/serverless.yml b/V2/serverless.yml index 9b31ee0b..66fa83af 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -36,7 +36,12 @@ package: - '!.gitignore' - '!.git/**' +#region start Functions + functions: + +#region Start Functions Activities + get_activities: handler: time_tracker/activities/interface.get_activities events: @@ -77,6 +82,10 @@ functions: route: activities/ authLevel: anonymous +#endregion End Functions Activities + +#region Start Functions Time-Entries + create_time_entry: handler: time_tracker/time_entries/interface.create_time_entry events: @@ -127,6 +136,10 @@ functions: route: time-entries/latest/ authLevel: anonymous +#endregion End Functions Time-Entries + +#region Start Functions Customers + create_customer: handler: time_tracker/customers/interface.create_customer events: @@ -137,6 +150,40 @@ functions: route: customers/ authLevel: anonymous + get_customers: + handler: time_tracker/customers/interface.get_customers + events: + - http: true + x-azure-settings: + methods: + - GET + route: customers/{id:?} + authLevel: anonymous + + update_customer: + handler: time_tracker/customers/interface.update_customer + events: + - http: true + x-azure-settings: + methods: + - PUT + route: customers/{id} + authLevel: anonymous + + delete_customer: + handler: time_tracker/customers/interface.delete_customer + events: + - http: true + x-azure-settings: + methods: + - DELETE + route: customers/{id} + authLevel: anonymous + +#endregion End Functions Customers + +#region Start Functions Projects + get_projects: handler: time_tracker/projects/interface.get_projects events: @@ -177,3 +224,17 @@ functions: route: projects/ authLevel: anonymous + + get_latest_projects: + handler: time_tracker/projects/interface.get_latest_projects + events: + - http: true + x-azure-settings: + methods: + - GET + route: projects/latest + authLevel: anonymous + +#endregion End Functions Projects + +#endregion End Functions diff --git a/V2/tests/api/azure/customer_azure_endpoints_test.py b/V2/tests/api/azure/customer_azure_endpoints_test.py index 47a619d5..f1f35d4f 100644 --- a/V2/tests/api/azure/customer_azure_endpoints_test.py +++ b/V2/tests/api/azure/customer_azure_endpoints_test.py @@ -1,3 +1,4 @@ +from http import HTTPStatus import json from faker import Faker @@ -8,7 +9,7 @@ CUSTOMER_URL = "/api/customers/" -def test__customer_azure_endpoint__creates_a_customer__when_customer_has_all_necesary_attributes( +def test__create_customer_azure_endpoint__creates_a_customer__when_customer_has_all_necesary_attributes( customer_factory ): customer_body = customer_factory().__dict__ @@ -24,11 +25,11 @@ def test__customer_azure_endpoint__creates_a_customer__when_customer_has_all_nec customer_json_data = json.loads(response.get_body()) customer_body['id'] = customer_json_data['id'] - assert response.status_code == 201 + assert response.status_code == HTTPStatus.CREATED assert customer_json_data == customer_body -def test__customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_necessary_attributes(): +def test__create_customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_necessary_attributes(): customer_to_insert = { "id": None, "name": Faker().user_name(), @@ -45,5 +46,169 @@ def test__customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_n response = azure_customers._create_customer.create_customer(req) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST assert response.get_body() == b'Invalid format or structure of the attributes of the customer' + + +def test__delete_customer_azure_endpoint__returns_a_customer_with_true_deleted__when_its_id_is_found( + test_db, customer_factory, insert_customer +): + customer_preinsert = customer_factory() + inserted_customer = insert_customer(customer_preinsert, test_db).__dict__ + + req = func.HttpRequest( + method='DELETE', + body=None, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + customer_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == HTTPStatus.OK + assert customer_json_data['deleted'] is True + + +def test__delete_customer_azure_endpoint__returns_not_found__when_its_id_is_not_found(): + req = func.HttpRequest( + method='DELETE', + body=None, + url=CUSTOMER_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.get_body() == b'Not found' + + +def test__update_customer_azure_endpoint__returns_an_updated_customer__when_customer_has_all_necesary_attributes( + test_db, customer_factory, insert_customer +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + inserted_customer["description"] = Faker().sentence() + + body = json.dumps(inserted_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._update_customer.update_customer(req) + customer_json_data = json.loads(response.get_body()) + + assert response.status_code == HTTPStatus.OK + assert customer_json_data == inserted_customer + + +def test__update_customer_azure_endpoint__returns_update_a_customer__when_customer_has_all_necesary_attributes( + customer_factory +): + existent_customer = customer_factory().__dict__ + + body = json.dumps(existent_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_customers._update_customer.update_customer(req) + + assert response.status_code == HTTPStatus.CONFLICT + assert response.get_body() == b'This customer does not exist or is duplicated' + + +def test__update_customer_azure_endpoint__returns_invalid_format__when_customer_doesnt_have_all_necesary_attributes( + customer_factory, insert_customer, test_db +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + inserted_customer.pop("name") + + body = json.dumps(inserted_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._update_customer.update_customer(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'Invalid format or structure of the attributes of the customer' + + +def test__delete_customers_azure_endpoint__returns_a_status_code_400__when_customer_recive_invalid_id( +): + req = func.HttpRequest( + method="DELETE", + body=None, + url=CUSTOMER_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'Invalid Format ID' + + +def test__customers_azure_endpoint__returns_all_customers( + test_db, customer_factory, insert_customer +): + customer_to_insert = customer_factory() + + inserted_customer = insert_customer(customer_to_insert, test_db).__dict__ + + req = func.HttpRequest(method='GET', body=None, url=CUSTOMER_URL) + response = azure_customers._get_customers.get_customers(req) + customers_json_data = response.get_body().decode("utf-8") + customer_list = json.loads(customers_json_data) + + assert response.status_code == HTTPStatus.OK + assert customers_json_data <= json.dumps(inserted_customer) + assert customer_list.pop() == inserted_customer + + +def test__customer_azure_endpoint__returns_a_customer__when_customer_matches_its_id( + test_db, customer_factory, insert_customer +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._get_customers.get_customers(req) + customer_json_data = response.get_body().decode("utf-8") + + assert response.status_code == HTTPStatus.OK + assert customer_json_data == json.dumps(inserted_customer) + + +def test__customer_azure_endpoint__returns_invalid_id__when_customer_not_matches_its_id(): + req = func.HttpRequest( + method='GET', + body=None, + url=CUSTOMER_URL, + route_params={"id": "Invalid ID"}, + ) + + response = azure_customers._get_customers.get_customers(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'The id has an invalid format' diff --git a/V2/tests/api/azure/project_azure_endpoints_test.py b/V2/tests/api/azure/project_azure_endpoints_test.py index 232462b7..b48a13dc 100644 --- a/V2/tests/api/azure/project_azure_endpoints_test.py +++ b/V2/tests/api/azure/project_azure_endpoints_test.py @@ -1,29 +1,14 @@ import json from http import HTTPStatus -import pytest from faker import Faker import azure.functions as func from time_tracker.projects._application import _projects as azure_projects -from time_tracker.projects import _domain as domain -from time_tracker.projects import _infrastructure as infrastructure PROJECT_URL = '/api/projects/' -@pytest.fixture(name='insert_project') -def _insert_project(test_db, insert_customer, project_factory, customer_factory) -> domain.Project: - inserted_customer = insert_customer(customer_factory(), test_db) - - def _new_project(): - project_to_insert = project_factory(customer_id=inserted_customer.id) - dao = infrastructure.ProjectsSQLDao(test_db) - inserted_project = dao.create(project_to_insert) - return inserted_project - return _new_project - - def test__project_azure_endpoint__returns_all_projects( insert_project ): @@ -146,19 +131,17 @@ def test__update_project_azure_endpoint__returns_a_project__when_found_a_project def test__update_projects_azure_endpoint__returns_a_status_code_404__when_no_found_a_project_to_update( - project_factory ): - project_body = project_factory().__dict__ + project_body = {"description": Faker().sentence()} req = func.HttpRequest( method="PUT", body=json.dumps(project_body).encode("utf-8"), url=PROJECT_URL, - route_params={"id": project_body["id"]}, + route_params={"id": Faker().pyint()}, ) response = azure_projects._update_project.update_project(req) - assert response.status_code == HTTPStatus.NOT_FOUND assert response.get_body() == b"Not found" @@ -249,3 +232,40 @@ def test__project_azure_endpoint__returns_a_status_code_500__when_project_receiv assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert response.get_body() == b"could not be created" + + +def test__get_latest_projects_azure_endpoint__returns_a_list_of_latest_projects__when_an_owner_id_match( + insert_time_entry +): + inserted_time_entry = insert_time_entry().__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=PROJECT_URL+"latest/", + params={"owner_id": inserted_time_entry["owner_id"]}, + ) + + response = azure_projects._get_latest_projects.get_latest_projects(req) + projects_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == HTTPStatus.OK + assert inserted_time_entry["project_id"] == projects_json_data[0]["id"] + + +def test__get_latest_projects_azure_endpoint__returns_an_empty_list__when_an_owner_id_not_match( + insert_time_entry +): + insert_time_entry().__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=PROJECT_URL+"latest/", + ) + + response = azure_projects._get_latest_projects.get_latest_projects(req) + projects_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == HTTPStatus.OK + assert projects_json_data == [] 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 42e3d5ec..13e3e875 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -46,10 +46,11 @@ def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_ 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, + test_db, time_entry_factory, insert_time_entry, insert_activity, activity_factory, insert_project ): + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), test_db).__dict__ - time_entry_body = time_entry_factory(activity_id=inserted_activity["id"]) + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], project_id=inserted_project.id) inserted_time_entry = insert_time_entry(time_entry_body, test_db) req = func.HttpRequest( @@ -82,10 +83,11 @@ def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_ti def test__time_entry_azure_endpoint__returns_all_time_entries( - test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity, insert_project ): + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), test_db) - time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) inserted_time_entries = insert_time_entry(time_entries_to_insert, test_db).__dict__ req = func.HttpRequest(method="GET", body=None, url=TIME_ENTRY_URL) @@ -99,10 +101,11 @@ def test__time_entry_azure_endpoint__returns_all_time_entries( def test__time_entry_azure_endpoint__returns_an_time_entry__when_time_entry_matches_its_id( - test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity, insert_project ): + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), test_db) - time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) inserted_time_entries = insert_time_entry(time_entries_to_insert, test_db).__dict__ req = func.HttpRequest( @@ -120,10 +123,11 @@ def test__time_entry_azure_endpoint__returns_an_time_entry__when_time_entry_matc def test__get_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( - test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity, insert_project ): + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), test_db) - time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) insert_time_entry(time_entries_to_insert, test_db).__dict__ req = func.HttpRequest( @@ -139,12 +143,53 @@ def test__get_time_entries_azure_endpoint__returns_a_status_code_400__when_time_ 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, insert_project +): + inserted_project = insert_project() + inserted_activity = insert_activity(activity_factory(), test_db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], project_id=inserted_project.id) + 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 == 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'Not found' + + def test__update_time_entry_azure_endpoint__returns_an_time_entry__when_found_an_time_entry_to_update( - test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity, insert_project ): - inserted_activity = insert_activity(activity_factory(), test_db) - existent_time_entries = time_entry_factory(activity_id=inserted_activity.id) - inserted_time_entries = insert_time_entry(existent_time_entries, test_db).__dict__ + inserted_project = insert_project() + inserted_activity = insert_activity(activity_factory(), test_db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], project_id=inserted_project.id) + inserted_time_entry = insert_time_entry(time_entry_body, test_db).__dict__ time_entry_body = {"description": Faker().sentence()} @@ -152,15 +197,15 @@ def test__update_time_entry_azure_endpoint__returns_an_time_entry__when_found_an method='PUT', body=json.dumps(time_entry_body).encode("utf-8"), url=TIME_ENTRY_URL, - route_params={"id": inserted_time_entries["id"]}, + route_params={"id": inserted_time_entry["id"]}, ) response = azure_time_entries._update_time_entry.update_time_entry(req) activitiy_json_data = response.get_body().decode("utf-8") - inserted_time_entries.update(time_entry_body) + inserted_time_entry.update(time_entry_body) assert response.status_code == 200 - assert activitiy_json_data == json.dumps(inserted_time_entries) + assert activitiy_json_data == json.dumps(inserted_time_entry) def test__update_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_format_id(): @@ -211,27 +256,6 @@ def test__update_time_entries_azure_endpoint__returns_a_status_code_400__when_ti 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, ): diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index ff67203c..c11fc951 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,5 +1,5 @@ # flake8: noqa from fixtures import _activity_factory, _test_db, _insert_activity -from fixtures import _time_entry_factory +from fixtures import _time_entry_factory, _insert_time_entry from fixtures import _customer_factory, _insert_customer -from fixtures import _project_factory +from fixtures import _project_factory, _insert_project diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index 2eae7b16..82391ebf 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -2,11 +2,13 @@ from faker import Faker import time_tracker.activities._domain as activities_domain -import time_tracker.activities._infrastructure as activities_infrastructure import time_tracker.time_entries._domain as time_entries_domain +import time_tracker.time_entries._infrastructure as time_entries_infrastructure import time_tracker.customers._domain as customers_domain +import time_tracker.activities._infrastructure as activities_infrastructure import time_tracker.customers._infrastructure as customers_infrastructure import time_tracker.projects._domain as projects_domain +import time_tracker.projects._infrastructure as projects_infrastructure from time_tracker._infrastructure import DB @@ -108,7 +110,8 @@ def _make_project( customer_id=Faker().pyint(), status=Faker().pyint(), deleted=False, - technologies=str(Faker().pylist()) + technologies=str(Faker().pylist()), + customer=None ): project = projects_domain.Project( id=id, @@ -118,7 +121,8 @@ def _make_project( customer_id=customer_id, status=status, deleted=deleted, - technologies=technologies + technologies=technologies, + customer=customer ) return project return _make_project @@ -131,3 +135,34 @@ def _new_customer(customer: customers_domain.Customer, database: DB): new_customer = dao.create(customer) return new_customer return _new_customer + + +@pytest.fixture(name='insert_project') +def _insert_project(test_db, insert_customer, project_factory, customer_factory) -> projects_domain.Project: + inserted_customer = insert_customer(customer_factory(), test_db) + + def _new_project(): + project_to_insert = project_factory(id=None, customer_id=inserted_customer.id, deleted=False) + dao = projects_infrastructure.ProjectsSQLDao(test_db) + inserted_project = dao.create(project_to_insert) + return inserted_project + return _new_project + + +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry( + test_db, insert_project, activity_factory, insert_activity, time_entry_factory +) -> time_entries_domain.TimeEntry: + + inserted_project = insert_project() + inserted_activity = insert_activity(activity_factory(), test_db) + + def _new_time_entry(owner_id: int = Faker().pyint()): + dao = time_entries_infrastructure.TimeEntriesSQLDao(test_db) + time_entries_to_insert = time_entry_factory( + activity_id=inserted_activity.id, project_id=inserted_project.id, owner_id=owner_id + ) + + inserted_time_entries = dao.create(time_entries_to_insert) + return inserted_time_entries + return _new_time_entry diff --git a/V2/tests/integration/daos/customers_dao_test.py b/V2/tests/integration/daos/customers_dao_test.py index b85cd3e3..496aaf47 100644 --- a/V2/tests/integration/daos/customers_dao_test.py +++ b/V2/tests/integration/daos/customers_dao_test.py @@ -1,4 +1,7 @@ +import typing + import pytest +from faker import Faker import time_tracker.customers._domain as domain import time_tracker.customers._infrastructure as infrastructure @@ -22,7 +25,7 @@ def _clean_database(): dao.db.get_session().execute(query) -def test__customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_database( +def test__create_customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_database( test_db, customer_factory, create_fake_dao ): dao = create_fake_dao(test_db) @@ -33,3 +36,104 @@ def test__customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_da assert isinstance(inserted_customer, domain.Customer) assert inserted_customer == customer_to_insert + + +def test__get_all__returns_a_list_of_customer_dto_objects__when_one_or_more_customers_are_found_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + customer_to_insert = customer_factory() + inserted_customer = [dao.create(customer_to_insert)] + + customers = dao.get_all() + + assert isinstance(customers, typing.List) + assert customers == inserted_customer + + +def test_get_by_id__returns_a_customer_dto__when_found_one_customer_that_matches_its_id_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db) + + customer = dao.get_by_id(inserted_customer.id) + + assert isinstance(customer, domain.Customer) + assert customer.id == inserted_customer.id + assert customer == inserted_customer + + +def test__get_by_id__returns_none__when_no_customer_matches_its_id_with_sql_database( + test_db, create_fake_dao, customer_factory +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + customer = dao.get_by_id(existent_customer.id) + + assert customer is None + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_customers_with_sql_database( + test_db, create_fake_dao +): + customers = create_fake_dao(test_db).get_all() + + assert isinstance(customers, typing.List) + assert customers == [] + + +def test_delete__returns_a_customer_with_inactive_status__when_a_customer_matching_its_id_is_found_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db) + + customer = dao.delete(inserted_customer.id) + + assert isinstance(customer, domain.Customer) + assert customer.id == inserted_customer.id + assert customer.status == 1 + assert customer.deleted is True + + +def test_delete__returns_none__when_no_customer_matching_its_id_is_found_with_sql_database( + test_db, create_fake_dao, customer_factory +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + results = dao.delete(existent_customer.id) + + assert results is None + + +def test__update_customer_dao__returns_an_updated_customer_dto__when_updates_correctly_with_sql_database( + test_db, customer_factory, create_fake_dao, insert_customer +): + dao = create_fake_dao(test_db) + + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db).__dict__ + + inserted_customer["description"] = Faker().sentence() + + updated_customer = dao.update(inserted_customer["id"], domain.Customer(**inserted_customer)) + + assert isinstance(updated_customer, domain.Customer) + assert updated_customer.description == inserted_customer["description"] + assert updated_customer.__dict__ == inserted_customer + + +def test__update_customer_dao__returns_none__when_an_incorrect_id_is_passed( + test_db, customer_factory, create_fake_dao, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + updated_customer = dao.update(Faker().pyint(), existent_customer) + + assert updated_customer is None diff --git a/V2/tests/integration/daos/projects_dao_test.py b/V2/tests/integration/daos/projects_dao_test.py index 64837e37..01f5a1a3 100644 --- a/V2/tests/integration/daos/projects_dao_test.py +++ b/V2/tests/integration/daos/projects_dao_test.py @@ -7,18 +7,6 @@ from time_tracker._infrastructure import DB -@pytest.fixture(name='insert_project') -def _insert_project(customer_factory, test_db, insert_customer, create_fake_dao, project_factory) -> domain.Project: - inserted_customer = insert_customer(customer_factory(), test_db) - - def _new_project(): - project_to_insert = project_factory(customer_id=inserted_customer.id) - inserted_project = create_fake_dao.create(project_to_insert) - return inserted_project - - return _new_project - - @pytest.fixture(name='create_fake_dao') def _create_fake_dao() -> domain.ProjectsDao: db_fake = DB() @@ -44,6 +32,9 @@ def test__create_project__returns_a_project_dto__when_saves_correctly_with_sql_d inserted_project = dao.create(project_to_insert) + expected_project = project_to_insert.__dict__ + expected_project.update({"customer": inserted_customer.__dict__}) + assert isinstance(inserted_project, domain.Project) assert inserted_project == project_to_insert @@ -85,6 +76,7 @@ def test__get_all__returns_a_list_of_project_dto_objects__when_one_or_more_proje ] projects = dao.get_all() + assert isinstance(projects, typing.List) assert projects == inserted_projects @@ -147,3 +139,15 @@ def test_delete__returns_none__when_no_project_matching_its_id_is_found_with_sql results = dao.delete(project_to_insert.id) assert results is None + + +def test_get_latest_projects__returns_a_list_of_project_dto_objects__when_find_projects_in_the_latest_time_entries( + create_fake_dao, insert_time_entry +): + dao = create_fake_dao + owner_id = Faker().pyint() + inserted_time_entries = insert_time_entry(owner_id) + latest_projects = dao.get_latest(owner_id) + + assert isinstance(latest_projects, typing.List) + assert latest_projects[0].id == inserted_time_entries.project_id diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py index e48241cc..3c17f7e9 100644 --- a/V2/tests/integration/daos/time_entries_dao_test.py +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -26,12 +26,12 @@ def _clean_database(): def test__time_entry__returns_a_time_entry_dto__when_saves_correctly_with_sql_database( - test_db, time_entry_factory, create_fake_dao, insert_activity, activity_factory + test_db, time_entry_factory, create_fake_dao, insert_activity, activity_factory, insert_project ): 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) + inserted_project = insert_project() + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) inserted_time_entry = dao.create(time_entry_to_insert) @@ -51,12 +51,13 @@ def test__time_entry__returns_None__when_not_saves_correctly( 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 + create_fake_dao, test_db, time_entry_factory, insert_activity, activity_factory, insert_project ): dao = create_fake_dao(test_db) + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), dao.db) - existent_time_entry = time_entry_factory(activity_id=inserted_activity.id) - inserted_time_entry = dao.create(existent_time_entry) + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) + inserted_time_entry = dao.create(time_entry_to_insert) result = dao.delete(inserted_time_entry.id) @@ -73,42 +74,60 @@ def test_delete__returns_none__when_no_time_entry_matching_its_id_is_found( 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, insert_project +): + dao = create_fake_dao(test_db) + inserted_project = insert_project() + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) + inserted_time_entry = dao.create(time_entry_to_insert).__dict__ + + result = dao.get_latest_entries(int(inserted_time_entry["owner_id"])) + + assert result == [inserted_time_entry] + + def test_update__returns_an_time_entry_dto__when_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, insert_project ): dao = create_fake_dao(test_db) + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), dao.db) - existent_time_entries = time_entry_factory(activity_id=inserted_activity.id) - inserted_time_entries = dao.create(existent_time_entries).__dict__ - time_entry_id = inserted_time_entries["id"] - inserted_time_entries.update({"description": "description updated"}) + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) + inserted_time_entry = dao.create(time_entry_to_insert).__dict__ + + time_entry_id = inserted_time_entry["id"] + inserted_time_entry.update({"description": "description updated"}) - time_entry = dao.update(time_entry_id=time_entry_id, time_entry_data=inserted_time_entries) + time_entry = dao.update(time_entry_id=time_entry_id, time_entry_data=inserted_time_entry) assert time_entry.id == time_entry_id - assert time_entry.description == inserted_time_entries.get("description") + assert time_entry.description == inserted_time_entry.get("description") 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, insert_project ): dao = create_fake_dao(test_db) + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), dao.db) - existent_time_entries = time_entry_factory(activity_id=inserted_activity.id) - inserted_time_entries = dao.create(existent_time_entries).__dict__ + time_entry_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) + inserted_time_entry = dao.create(time_entry_to_insert).__dict__ - time_entry = dao.update(0, inserted_time_entries) + time_entry = dao.update(0, inserted_time_entry) assert time_entry is None def test__get_all__returns_a_list_of_time_entries_dto_objects__when_one_or_more_time_entries_are_found_in_sql_database( - test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory, insert_project ): dao = create_fake_dao(test_db) + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), dao.db) - time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) inserted_time_entries = [dao.create(time_entries_to_insert)] time_entry = dao.get_all() @@ -128,11 +147,12 @@ def test__get_all__returns_an_empty_list__when_doesnt_found_any_time_entries_in_ def test__get_by_id__returns_a_time_entry_dto__when_found_one_time_entry_that_match_id_with_sql_database( - test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory, insert_project ): dao = create_fake_dao(test_db) + inserted_project = insert_project() inserted_activity = insert_activity(activity_factory(), dao.db) - time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id, project_id=inserted_project.id) inserted_time_entries = dao.create(time_entries_to_insert) time_entry = dao.get_by_id(time_entries_to_insert.id) @@ -155,21 +175,6 @@ def test__get_by_id__returns_none__when_no_time_entry_matches_by_id( 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 ): diff --git a/V2/tests/unit/services/customer_service_test.py b/V2/tests/unit/services/customer_service_test.py index bb25070f..776d18ee 100644 --- a/V2/tests/unit/services/customer_service_test.py +++ b/V2/tests/unit/services/customer_service_test.py @@ -1,3 +1,5 @@ +from faker import Faker + from time_tracker.customers._domain import CustomerService @@ -12,3 +14,57 @@ def test__create_customer__uses_the_customer_dao__to_create_a_customer(mocker, c assert customer_dao.create.called assert expected_customer == new_customer + + +def test__delete_customer__uses_the_customer_dao__to_delete_customer_selected( + mocker, +): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + delete=mocker.Mock(return_value=expected_customer) + ) + + customer_service = CustomerService(customer_dao) + deleted_customer = customer_service.delete(Faker().pyint()) + + assert customer_dao.delete.called + assert expected_customer == deleted_customer + + +def test__get_all__uses_the_customer_dao__to_retrieve_customers(mocker): + expected_customers = mocker.Mock() + customer_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_customers) + ) + customer_service = CustomerService(customer_dao) + + actual_customers = customer_service.get_all() + + assert customer_dao.get_all.called + assert expected_customers == actual_customers + + +def test__get_by_id__uses_the_customer_dao__to_retrieve_one_customer(mocker): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_customer) + ) + customer_service = CustomerService(customer_dao) + + actual_customer = customer_service.get_by_id(Faker().pyint()) + + assert customer_dao.get_by_id.called + assert expected_customer == actual_customer + + +def test__update_customer__uses_the_customer_dao__to_update_a_customer(mocker, customer_factory): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + update=mocker.Mock(return_value=expected_customer) + ) + customer_service = CustomerService(customer_dao) + + updated_customer = customer_service.update(Faker().pyint(), customer_factory()) + + assert customer_dao.update.called + assert expected_customer == updated_customer diff --git a/V2/tests/unit/services/project_service_test.py b/V2/tests/unit/services/project_service_test.py index 9baf657e..913bd40f 100644 --- a/V2/tests/unit/services/project_service_test.py +++ b/V2/tests/unit/services/project_service_test.py @@ -72,3 +72,18 @@ def test__create_project__uses_the_project_dao__to_create_an_project(mocker, pro assert project_dao.create.called assert expected_project == actual_project + + +def test__get_latest_projects__uses_the_project_dao__to_get_last_projects( + mocker, +): + expected_latest_projects = mocker.Mock() + project_dao = mocker.Mock( + get_latest=mocker.Mock(return_value=expected_latest_projects) + ) + + project_service = ProjectService(project_dao) + latest_projects = project_service.get_latest(Faker().pyint()) + + assert expected_latest_projects == latest_projects + assert project_dao.get_latest.called diff --git a/V2/tests/unit/use_cases/customers_use_case_test.py b/V2/tests/unit/use_cases/customers_use_case_test.py index 3b8566a9..63e03081 100644 --- a/V2/tests/unit/use_cases/customers_use_case_test.py +++ b/V2/tests/unit/use_cases/customers_use_case_test.py @@ -1,4 +1,5 @@ from pytest_mock import MockFixture +from faker import Faker from time_tracker.customers._domain import _use_cases @@ -16,3 +17,61 @@ def test__create_customer_function__uses_the_customer_service__to_create_a_custo assert customer_service.create.called assert expected_customer == new_customer + + +def test__delete_customer_function__uses_the_customer_service__to_delete_customer_selected( + mocker: MockFixture, +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock(delete=mocker.Mock(return_value=expected_customer)) + + customer_use_case = _use_cases.DeleteCustomerUseCase(customer_service) + deleted_customer = customer_use_case.delete_customer(Faker().pyint()) + + assert customer_service.delete.called + assert expected_customer == deleted_customer + + +def test__get_list_customers_function__uses_the_customer_service__to_retrieve_customers( + mocker: MockFixture, +): + expected_customers = mocker.Mock() + customer_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_customers) + ) + + customers_use_case = _use_cases.GetAllCustomerUseCase(customer_service) + actual_customers = customers_use_case.get_all_customer() + + assert customer_service.get_all.called + assert expected_customers == actual_customers + + +def test__get_customer_by_id_function__uses_the_customer_service__to_retrieve_customer( + mocker: MockFixture, +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_customer) + ) + + customer_use_case = _use_cases.GetByIdCustomerUseCase(customer_service) + actual_customer = customer_use_case.get_customer_by_id(Faker().pyint()) + + assert customer_service.get_by_id.called + assert expected_customer == actual_customer + + +def test__update_customer_function__uses_the_customer_service__to_update_a_customer( + mocker: MockFixture, customer_factory +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock( + update=mocker.Mock(return_value=expected_customer) + ) + + customer_use_case = _use_cases.UpdateCustomerUseCase(customer_service) + updated_customer = customer_use_case.update_customer(Faker().pyint(), customer_factory()) + + assert customer_service.update.called + assert expected_customer == updated_customer diff --git a/V2/tests/unit/use_cases/projects_use_case_test.py b/V2/tests/unit/use_cases/projects_use_case_test.py index 22167418..9f5d5f5c 100644 --- a/V2/tests/unit/use_cases/projects_use_case_test.py +++ b/V2/tests/unit/use_cases/projects_use_case_test.py @@ -78,3 +78,16 @@ def test__update_project_function__uses_the_projects_service__to_update_an_proje assert project_service.update.called assert expected_project == updated_project + + +def test__get_latest_projects_function__uses_the_project_service__to_get_latest_project( + mocker: MockFixture, +): + expected_latest_projects = mocker.Mock() + project_service = mocker.Mock(get_latest=mocker.Mock(return_value=expected_latest_projects)) + + project_use_case = _use_cases.GetLatestProjectsUseCase(project_service) + latest_projects = project_use_case.get_latest(Faker().pyint()) + + assert project_service.get_latest.called + assert expected_latest_projects == latest_projects diff --git a/V2/time_tracker/customers/_application/__init__.py b/V2/time_tracker/customers/_application/__init__.py index db2c2c15..d9ba1676 100644 --- a/V2/time_tracker/customers/_application/__init__.py +++ b/V2/time_tracker/customers/_application/__init__.py @@ -1,2 +1,5 @@ # flake8: noqa -from ._customers import create_customer \ No newline at end of file +from ._customers import create_customer +from ._customers import get_customers +from ._customers import delete_customer +from ._customers import update_customer diff --git a/V2/time_tracker/customers/_application/_customers/__init__.py b/V2/time_tracker/customers/_application/_customers/__init__.py index bf1f8460..b07840ce 100644 --- a/V2/time_tracker/customers/_application/_customers/__init__.py +++ b/V2/time_tracker/customers/_application/_customers/__init__.py @@ -1,2 +1,5 @@ # flake8: noqa -from ._create_customer import create_customer \ No newline at end of file +from ._create_customer import create_customer +from ._get_customers import get_customers +from ._delete_customer import delete_customer +from ._update_customer import update_customer diff --git a/V2/time_tracker/customers/_application/_customers/_create_customer.py b/V2/time_tracker/customers/_application/_customers/_create_customer.py index 919c34cb..48e39dc3 100644 --- a/V2/time_tracker/customers/_application/_customers/_create_customer.py +++ b/V2/time_tracker/customers/_application/_customers/_create_customer.py @@ -1,6 +1,7 @@ import dataclasses import json import typing +from http import HTTPStatus import azure.functions as func @@ -17,8 +18,8 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: use_case = _domain._use_cases.CreateCustomerUseCase(customer_service) customer_data = req.get_json() - customer_is_valid = _validate_customer(customer_data) - if not customer_is_valid: + customer_is_invalid = _validate_customer(customer_data) + if customer_is_invalid: raise ValueError customer_to_create = _domain.Customer( @@ -32,10 +33,10 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: if created_customer: body = json.dumps(created_customer.__dict__) - status_code = 201 + status_code = HTTPStatus.CREATED else: body = b'This customer already exists' - status_code = 409 + status_code = HTTPStatus.CONFLICT return func.HttpResponse( body=body, @@ -45,13 +46,11 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: except ValueError: return func.HttpResponse( body=b'Invalid format or structure of the attributes of the customer', - status_code=400, + status_code=HTTPStatus.BAD_REQUEST, mimetype="application/json" ) -def _validate_customer(customer_data: dict) -> bool: - if [field.name for field in dataclasses.fields(_domain.Customer) - if (field.name not in customer_data) and (field.type != typing.Optional[field.type])]: - return False - return True +def _validate_customer(customer_data: dict) -> typing.List[str]: + return [field.name for field in dataclasses.fields(_domain.Customer) + if (field.name not in customer_data) and (field.type != typing.Optional[field.type])] diff --git a/V2/time_tracker/customers/_application/_customers/_delete_customer.py b/V2/time_tracker/customers/_application/_customers/_delete_customer.py new file mode 100644 index 00000000..41fc3464 --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_delete_customer.py @@ -0,0 +1,39 @@ +import json +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATATYPE = "application/json" + + +def delete_customer(req: func.HttpRequest) -> func.HttpResponse: + customer_dao = _infrastructure.CustomersSQLDao(DB()) + customer_service = _domain.CustomerService(customer_dao) + use_case = _domain._use_cases.DeleteCustomerUseCase(customer_service) + + try: + customer_id = int(req.route_params.get("id")) + deleted_customer = use_case.delete_customer(customer_id) + if not deleted_customer: + return func.HttpResponse( + body="Not found", + status_code=HTTPStatus.NOT_FOUND, + mimetype=DATATYPE + ) + + return func.HttpResponse( + body=json.dumps(deleted_customer.__dict__, default=str), + status_code=HTTPStatus.OK, + mimetype=DATATYPE, + ) + + except ValueError: + return func.HttpResponse( + body=b"Invalid Format ID", + status_code=HTTPStatus.BAD_REQUEST, + mimetype=DATATYPE + ) diff --git a/V2/time_tracker/customers/_application/_customers/_get_customers.py b/V2/time_tracker/customers/_application/_customers/_get_customers.py new file mode 100644 index 00000000..8cb9635f --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_get_customers.py @@ -0,0 +1,55 @@ +from http import HTTPStatus +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def get_customers(req: func.HttpRequest) -> func.HttpResponse: + customer_id = req.route_params.get('id') + status_code = HTTPStatus.OK + + try: + if customer_id: + response = _get_by_id(int(customer_id)) + if response == b'This customer does not exist': + status_code = HTTPStatus.NOT_FOUND + else: + response = _get_all() + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b"The id has an invalid format", status_code=HTTPStatus.BAD_REQUEST, mimetype="application/json" + ) + + +def _get_by_id(customer_id: int) -> str: + customer_use_case = _domain._use_cases.GetByIdCustomerUseCase( + _create_customer_service(DB()) + ) + customer = customer_use_case.get_customer_by_id(customer_id) + + return json.dumps(customer.__dict__) if customer else b'This customer does not exist' + + +def _get_all() -> str: + customer_sql = _domain._use_cases.GetAllCustomerUseCase( + _create_customer_service(DB()) + ) + return json.dumps( + [ + customer.__dict__ + for customer in customer_sql.get_all_customer() + ] + ) + + +def _create_customer_service(db: DB) -> _domain.CustomerService: + customer_sql = _infrastructure.CustomersSQLDao(db) + return _domain.CustomerService(customer_sql) diff --git a/V2/time_tracker/customers/_application/_customers/_update_customer.py b/V2/time_tracker/customers/_application/_customers/_update_customer.py new file mode 100644 index 00000000..93524c65 --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_update_customer.py @@ -0,0 +1,53 @@ +import dataclasses +import json +import typing +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def update_customer(req: func.HttpRequest) -> func.HttpResponse: + try: + database = DB() + customer_id = int(req.route_params.get('id')) + customer_dao = _infrastructure.CustomersSQLDao(database) + customer_service = _domain.CustomerService(customer_dao) + use_case = _domain._use_cases.UpdateCustomerUseCase(customer_service) + + customer_data = req.get_json() + customer_is_invalid = _validate_customer(customer_data) + if customer_is_invalid: + raise ValueError + + customer_to_update = _domain.Customer( + **{field.name: customer_data.get(field.name) for field in dataclasses.fields(_domain.Customer)} + ) + updated_customer = use_case.update_customer(customer_id, customer_to_update) + + if updated_customer: + body = json.dumps(updated_customer.__dict__) + status_code = HTTPStatus.OK + else: + body = b'This customer does not exist or is duplicated' + status_code = HTTPStatus.CONFLICT + + return func.HttpResponse( + body=body, + status_code=status_code, + mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b'Invalid format or structure of the attributes of the customer', + status_code=HTTPStatus.BAD_REQUEST, + mimetype="application/json" + ) + + +def _validate_customer(customer_data: dict) -> typing.List[str]: + return [field.name for field in dataclasses.fields(_domain.Customer) + if field.name not in customer_data] diff --git a/V2/time_tracker/customers/_domain/__init__.py b/V2/time_tracker/customers/_domain/__init__.py index 8392b8e9..a2e8014b 100644 --- a/V2/time_tracker/customers/_domain/__init__.py +++ b/V2/time_tracker/customers/_domain/__init__.py @@ -4,4 +4,8 @@ from ._services import CustomerService from ._use_cases import ( CreateCustomerUseCase, + UpdateCustomerUseCase, + GetAllCustomerUseCase, + GetByIdCustomerUseCase, + DeleteCustomerUseCase ) \ No newline at end of file diff --git a/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py b/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py index 35a7a7e9..186d5c86 100644 --- a/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py +++ b/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py @@ -1,4 +1,5 @@ import abc +import typing from time_tracker.customers._domain import Customer @@ -7,3 +8,19 @@ class CustomersDao(abc.ABC): @abc.abstractmethod def create(self, data: Customer) -> Customer: pass + + @abc.abstractmethod + def update(self, id: int, data: Customer) -> Customer: + pass + + @abc.abstractmethod + def get_by_id(self, id: int) -> Customer: + pass + + @abc.abstractmethod + def get_all(self) -> typing.List[Customer]: + pass + + @abc.abstractmethod + def delete(self, id: int) -> Customer: + pass diff --git a/V2/time_tracker/customers/_domain/_services/_customer.py b/V2/time_tracker/customers/_domain/_services/_customer.py index 88633a08..082a7b08 100644 --- a/V2/time_tracker/customers/_domain/_services/_customer.py +++ b/V2/time_tracker/customers/_domain/_services/_customer.py @@ -1,3 +1,5 @@ +import typing + from time_tracker.customers._domain import Customer, CustomersDao @@ -8,3 +10,15 @@ def __init__(self, customer_dao: CustomersDao): def create(self, data: Customer) -> Customer: return self.customer_dao.create(data) + + def update(self, id: int, data: Customer) -> Customer: + return self.customer_dao.update(id, data) + + def get_by_id(self, id: int) -> Customer: + return self.customer_dao.get_by_id(id) + + def get_all(self) -> typing.List[Customer]: + return self.customer_dao.get_all() + + def delete(self, id: int) -> Customer: + return self.customer_dao.delete(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/__init__.py b/V2/time_tracker/customers/_domain/_use_cases/__init__.py index accd4281..4dcb8239 100644 --- a/V2/time_tracker/customers/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/customers/_domain/_use_cases/__init__.py @@ -1,2 +1,6 @@ # flake8: noqa -from ._create_customer_use_case import CreateCustomerUseCase \ No newline at end of file +from ._create_customer_use_case import CreateCustomerUseCase +from ._update_customer_use_case import UpdateCustomerUseCase +from ._get_by_id_customer_use_case import GetByIdCustomerUseCase +from ._get_all_customer_use_case import GetAllCustomerUseCase +from ._delete_customer_use_case import DeleteCustomerUseCase diff --git a/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py new file mode 100644 index 00000000..0477a1f2 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class DeleteCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def delete_customer(self, id: int) -> Customer: + return self.customer_service.delete(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py new file mode 100644 index 00000000..d3780449 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py @@ -0,0 +1,12 @@ +import typing + +from time_tracker.customers._domain import Customer, CustomerService + + +class GetAllCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def get_all_customer(self) -> typing.List[Customer]: + return self.customer_service.get_all() diff --git a/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py new file mode 100644 index 00000000..2372029a --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class GetByIdCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def get_customer_by_id(self, id: int) -> Customer: + return self.customer_service.get_by_id(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py new file mode 100644 index 00000000..318ced28 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class UpdateCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def update_customer(self, id: int, data: Customer) -> Customer: + return self.customer_service.update(id, data) diff --git a/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py b/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py index 2b1f4c0d..f3b15122 100644 --- a/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py +++ b/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py @@ -1,4 +1,5 @@ import dataclasses +import typing import sqlalchemy as sq @@ -22,6 +23,21 @@ def __init__(self, database: _db.DB): extend_existing=True, ) + def get_by_id(self, id: int) -> domain.Customer: + query = sq.sql.select(self.customer).where( + sq.sql.and_(self.customer.c.id == id, self.customer.c.deleted.is_(False)) + ) + customer = self.db.get_session().execute(query).one_or_none() + return self.__create_customer_dto(dict(customer)) if customer else None + + def get_all(self) -> typing.List[domain.Customer]: + query = sq.sql.select(self.customer).where(self.customer.c.deleted.is_(False)) + result = self.db.get_session().execute(query) + return [ + self.__create_customer_dto(dict(customer)) + for customer in result + ] + def create(self, data: domain.Customer) -> domain.Customer: try: new_customer = data.__dict__ @@ -39,3 +55,26 @@ def create(self, data: domain.Customer) -> domain.Customer: def __create_customer_dto(self, customer: dict) -> domain.Customer: customer = {key: customer.get(key) for key in self.customer_key} return domain.Customer(**customer) + + def delete(self, customer_id: int) -> domain.Customer: + query = ( + self.customer.update() + .where(self.customer.c.id == customer_id) + .values({"deleted": True}) + ) + self.db.get_session().execute(query) + query_deleted_customer = sq.sql.select(self.customer).where(self.customer.c.id == customer_id) + customer = self.db.get_session().execute(query_deleted_customer).one_or_none() + return self.__create_customer_dto(dict(customer)) if customer else None + + def update(self, id: int, data: domain.Customer) -> domain.Customer: + try: + new_customer = data.__dict__ + new_customer.pop("id") + + customer_validated = {key: value for (key, value) in new_customer.items() if value is not None} + query = self.customer.update().where(self.customer.c.id == id).values(customer_validated) + self.db.get_session().execute(query) + return self.get_by_id(id) + except sq.exc.SQLAlchemyError: + return None diff --git a/V2/time_tracker/customers/interface.py b/V2/time_tracker/customers/interface.py index e36b8172..9aef2091 100644 --- a/V2/time_tracker/customers/interface.py +++ b/V2/time_tracker/customers/interface.py @@ -1,2 +1,5 @@ # flake8: noqa from ._application import create_customer +from ._application import get_customers +from ._application import delete_customer +from ._application import update_customer diff --git a/V2/time_tracker/projects/_application/__init__.py b/V2/time_tracker/projects/_application/__init__.py index 6b48fb8a..96a6f985 100644 --- a/V2/time_tracker/projects/_application/__init__.py +++ b/V2/time_tracker/projects/_application/__init__.py @@ -2,4 +2,5 @@ from ._projects import create_project from ._projects import delete_project from ._projects import get_projects -from ._projects import update_project \ No newline at end of file +from ._projects import update_project +from ._projects import get_latest_projects \ No newline at end of file diff --git a/V2/time_tracker/projects/_application/_projects/__init__.py b/V2/time_tracker/projects/_application/_projects/__init__.py index 9f87eef2..b7500f9b 100644 --- a/V2/time_tracker/projects/_application/_projects/__init__.py +++ b/V2/time_tracker/projects/_application/_projects/__init__.py @@ -2,4 +2,5 @@ from ._create_project import create_project from ._delete_project import delete_project from ._get_projects import get_projects -from ._update_project import update_project \ No newline at end of file +from ._update_project import update_project +from ._get_latest_projects import get_latest_projects \ No newline at end of file diff --git a/V2/time_tracker/projects/_application/_projects/_create_project.py b/V2/time_tracker/projects/_application/_projects/_create_project.py index 559ba864..1397284a 100644 --- a/V2/time_tracker/projects/_application/_projects/_create_project.py +++ b/V2/time_tracker/projects/_application/_projects/_create_project.py @@ -31,7 +31,8 @@ def create_project(req: func.HttpRequest) -> func.HttpResponse: customer_id=project_data["customer_id"], status=project_data["status"], deleted=False, - technologies=project_data["technologies"] + technologies=project_data["technologies"], + customer=None ) created_project = use_case.create_project(project_to_create) diff --git a/V2/time_tracker/projects/_application/_projects/_get_latest_projects.py b/V2/time_tracker/projects/_application/_projects/_get_latest_projects.py new file mode 100644 index 00000000..0aa9badc --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/_get_latest_projects.py @@ -0,0 +1,26 @@ +import json +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB as database + + +def get_latest_projects(req: func.HttpRequest) -> func.HttpResponse: + project_dao = _infrastructure.ProjectsSQLDao(database()) + project_service = _domain.ProjectService(project_dao) + use_case = _domain._use_cases.GetLatestProjectsUseCase(project_service) + + owner_id = req.params.get('owner_id') + response = [ + project.__dict__ + for project in use_case.get_latest(owner_id) + ] + + return func.HttpResponse( + body=json.dumps(response), + status_code=HTTPStatus.OK, + mimetype="application/json", + ) diff --git a/V2/time_tracker/projects/_domain/__init__.py b/V2/time_tracker/projects/_domain/__init__.py index c90dbcaf..6cdbe548 100644 --- a/V2/time_tracker/projects/_domain/__init__.py +++ b/V2/time_tracker/projects/_domain/__init__.py @@ -7,5 +7,6 @@ DeleteProjectUseCase, GetProjectsUseCase, GetProjectUseCase, - UpdateProjectUseCase + UpdateProjectUseCase, + GetLatestProjectsUseCase ) \ No newline at end of file diff --git a/V2/time_tracker/projects/_domain/_entities/_project.py b/V2/time_tracker/projects/_domain/_entities/_project.py index 0b2ffe1a..75361db8 100644 --- a/V2/time_tracker/projects/_domain/_entities/_project.py +++ b/V2/time_tracker/projects/_domain/_entities/_project.py @@ -12,3 +12,5 @@ class Project: status: int deleted: Optional[bool] technologies: List[str] + + customer: Optional[dict] diff --git a/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py b/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py index f38c8ebd..ef0bb10f 100644 --- a/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py +++ b/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py @@ -1,4 +1,5 @@ import abc +import typing from .. import Project @@ -9,7 +10,7 @@ def create(self, time_entry_data: Project) -> Project: pass @abc.abstractmethod - def get_all(self) -> Project: + def get_all(self) -> typing.List[Project]: pass @abc.abstractmethod @@ -23,3 +24,7 @@ def update(self, id: int, project_data: dict) -> Project: @abc.abstractmethod def delete(self, id: int) -> Project: pass + + @abc.abstractmethod + def get_latest(self, owner_id: int) -> typing.List[Project]: + pass diff --git a/V2/time_tracker/projects/_domain/_services/_project.py b/V2/time_tracker/projects/_domain/_services/_project.py index 0f99dafb..70dfe9c0 100644 --- a/V2/time_tracker/projects/_domain/_services/_project.py +++ b/V2/time_tracker/projects/_domain/_services/_project.py @@ -22,3 +22,6 @@ def update(self, id: int, project_data: dict) -> Project: def delete(self, id: int) -> Project: return self.project_dao.delete(id) + + def get_latest(self, owner_id: int) -> typing.List[Project]: + return self.project_dao.get_latest(owner_id) diff --git a/V2/time_tracker/projects/_domain/_use_cases/__init__.py b/V2/time_tracker/projects/_domain/_use_cases/__init__.py index defb127d..f2a7dfce 100644 --- a/V2/time_tracker/projects/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/projects/_domain/_use_cases/__init__.py @@ -4,3 +4,4 @@ from ._get_project_by_id_use_case import GetProjectUseCase from ._get_projects_use_case import GetProjectsUseCase from ._update_project_use_case import UpdateProjectUseCase +from ._get_latest_projects_use_case import GetLatestProjectsUseCase diff --git a/V2/time_tracker/projects/_domain/_use_cases/_get_latest_projects_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_get_latest_projects_use_case.py new file mode 100644 index 00000000..b26d484c --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_get_latest_projects_use_case.py @@ -0,0 +1,11 @@ +import typing + +from .. import Project, ProjectService + + +class GetLatestProjectsUseCase: + def __init__(self, project_service: ProjectService): + self.project_service = project_service + + def get_latest(self, owner_id: int) -> typing.List[Project]: + return self.project_service.get_latest(owner_id) diff --git a/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py b/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py index 2ec61186..7d99013e 100644 --- a/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py +++ b/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py @@ -5,6 +5,8 @@ from ... import _domain as domain from time_tracker._infrastructure import _db +from time_tracker.time_entries._infrastructure._data_persistence import TimeEntriesSQLDao +from time_tracker.customers._infrastructure._data_persistence import CustomersSQLDao class ProjectsSQLDao(domain.ProjectsDao): @@ -31,13 +33,12 @@ def __init__(self, database: _db.DB): def create(self, project_data: domain.Project) -> domain.Project: try: - new_project = project_data.__dict__ - new_project.pop('id', None) + validated_project = {key: value for (key, value) in project_data.__dict__.items() if value is not None} + + query = self.project.insert().values(validated_project).return_defaults() - query = self.project.insert().values(new_project).return_defaults() project = self.db.get_session().execute(query) - new_project.update({"id": project.inserted_primary_key[0]}) - return self.__create_project_dto(new_project) + return self.get_by_id(project.inserted_primary_key[0]) except sq.exc.SQLAlchemyError: return None @@ -45,14 +46,31 @@ def create(self, project_data: domain.Project) -> domain.Project: def get_by_id(self, id: int) -> domain.Project: query = sq.sql.select(self.project).where(self.project.c.id == id) project = self.db.get_session().execute(query).one_or_none() - return self.__create_project_dto(dict(project)) if project else None + if project: + customer_model = CustomersSQLDao(self.db).customer + query_customer = sq.sql.select(customer_model).where(customer_model.c.id == project["customer_id"]) + customer = self.db.get_session().execute(query_customer).one_or_none() + project = dict(project) + project.update({"customer": dict(customer)if customer else None}) + + return self.__create_project_dto(project) if project else None def get_all(self) -> typing.List[domain.Project]: - query = sq.sql.select(self.project) - result = self.db.get_session().execute(query) + customer_model = CustomersSQLDao(self.db).customer + query = sq.sql.select(self.project, customer_model).join(customer_model) + result = self.db.get_session().execute(query).all() + projects = [] + + for project in result: + query_customer = sq.sql.select(customer_model).where(customer_model.c.id == project["customer_id"]) + customer = self.db.get_session().execute(query_customer).one_or_none() + project = dict(project) + project.update({"customer": dict(customer)if customer else None}) + projects.append(project) + return [ - self.__create_project_dto(dict(project)) - for project in result + self.__create_project_dto(project) + for project in projects ] def delete(self, id: int) -> domain.Project: @@ -72,6 +90,16 @@ def update(self, id: int, project_data: dict) -> domain.Project: except sq.exc.SQLAlchemyError as error: raise Exception(error.orig) + def get_latest(self, owner_id: int) -> typing.List[domain.Project]: + time_entries_dao = TimeEntriesSQLDao(self.db) + latest_time_entries = time_entries_dao.get_latest_entries(owner_id) + latest_projects = [] + if latest_time_entries: + filter_project = typing.Counter(time_entry['project_id'] for time_entry in latest_time_entries) + latest_projects = [self.get_by_id(project_id) for project_id in filter_project] + + return latest_projects + def __create_project_dto(self, project: dict) -> domain.Project: project = {key: project.get(key) for key in self.project_key} return domain.Project(**project) diff --git a/V2/time_tracker/projects/interface.py b/V2/time_tracker/projects/interface.py index 2fb3244b..a0312258 100644 --- a/V2/time_tracker/projects/interface.py +++ b/V2/time_tracker/projects/interface.py @@ -2,4 +2,5 @@ from ._application import create_project from ._application import delete_project from ._application import get_projects -from ._application import update_project \ No newline at end of file +from ._application import update_project +from ._application import get_latest_projects \ 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 29631650..9b48eb2a 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa from ._create_time_entry import create_time_entry from ._delete_time_entry import delete_time_entry +from ._get_latest_entries import get_latest_entries 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/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index 513877bf..f0aec6d0 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -5,6 +5,7 @@ from ._use_cases import ( CreateTimeEntryUseCase, DeleteTimeEntryUseCase, + GetLastestTimeEntryUseCase, UpdateTimeEntryUseCase, GetTimeEntriesUseCase, GetTimeEntryUseCase, 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 055cd850..0dd05666 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,6 +1,7 @@ # 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 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 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 5d368e26..59988205 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 @@ -29,7 +29,7 @@ def __init__(self, database: _db.DB): sqlalchemy.Column('end_date', sqlalchemy.DateTime().with_variant(sqlalchemy.String, "sqlite")), sqlalchemy.Column('deleted', sqlalchemy.Boolean), sqlalchemy.Column('timezone_offset', sqlalchemy.String), - sqlalchemy.Column('project_id', sqlalchemy.Integer), + sqlalchemy.Column('project_id', sqlalchemy.Integer, sqlalchemy.ForeignKey('project.id')), extend_existing=True, ) diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 87876204..1b6c1826 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1,6 +1,7 @@ # flake8: noqa from ._application import create_time_entry from ._application import delete_time_entry +from ._application import get_latest_entries from ._application import update_time_entry from ._application import get_time_entries from ._application import get_latest_entries