diff --git a/.github/workflows/time-tracker-v1-on-push-workflow.yml b/.github/workflows/time-tracker-v1-on-push-workflow.yml index ed673b64..152998b4 100644 --- a/.github/workflows/time-tracker-v1-on-push-workflow.yml +++ b/.github/workflows/time-tracker-v1-on-push-workflow.yml @@ -2,8 +2,7 @@ name: Time Tacker V1 CI - ON PUSH on: push: - # update to master - branches: [TT-412-onpush] + branches: [master] jobs: time-tracker-ci-v1-on-push: @@ -56,12 +55,13 @@ jobs: run: | pytest tests - - name: Build and push image + - name: Login to docker registry uses: azure/docker-login@v1 with: login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} + - 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 }} diff --git a/V2/Makefile b/V2/Makefile index 135e96d0..cf02904b 100644 --- a/V2/Makefile +++ b/V2/Makefile @@ -1,10 +1,11 @@ .PHONY: help help: @echo "---------------HELP-----------------" - @echo "To install the dependencies type make install" - @echo "To test the project type make test" - @echo "To run the local database type make start-local" - @echo "To run all comands type make ci" + @echo "- make install --> Install the dependencies" + @echo "- make test --> Run all tests" + @echo "- make test specific_test= --> Run specific test" + @echo "- make start-local --> Run local database" + @echo "- make ci --> Install the dependencies and run all tests" @echo "------------------------------------" .PHONY: install @@ -17,13 +18,16 @@ install: .PHONY: test test: export ENVIRONMENT = test -test: export TEST_DB_CONNECTION = sqlite:///:memory: test: @echo "=========================================Lint with flake8=========================================" flake8 . --show-source --statistics @echo "Completed flake8!" @echo "=========================================Test with pytest=========================================" - python -m pytest -v + @if [ "$(specific_test)" ]; then \ + python -m pytest -vv -s -k $(specific_test);\ + else \ + python -m pytest -v;\ + fi @echo "Completed test!" start-local: diff --git a/V2/README.md b/V2/README.md index f414079d..64726b30 100644 --- a/V2/README.md +++ b/V2/README.md @@ -1,23 +1,108 @@ -# time-tracker-api V2 +# **Time-tracker-api V2 Architecture** +Architecture +The application follows a DDD approach with a hexagonal clean architecture. BIG WORDS!, what does it mean? it means the following: -Refer to [Serverless docs](https://serverless.com/framework/docs/providers/azure/guide/intro/) for more information. +We have a directory for each domain entitiy (i.e. time entries, technologies, activities, etc) +Inside each entity directory we have other 3 directories (application, domain and infrastructure) +I'll leave this drawing to understand how these three folders work and what logic should be included in these directories -## Requirements to use makefile +![ddd.png](https://raw.githubusercontent.com/eguezgustavo/time_tracker_app_skeleton/master/ddd.png) +More information [Here](https://github.com/eguezgustavo/time_tracker_app_skeleton) -- Python version 3.6 or 3.7. +## **Stack Technologies** + - [Serverless](https://serverless.com/framework/docs/providers/azure/guide/intro/) + - Python + - Pytest + - Docker Compose + +Recommended link [tdd_dojo](https://github.com/eguezgustavo/tdd_dojo) -- Use an environment to install requirements (pyenv). +## **Setup environment** -## How to use makefile +### **Requeriments** + +- Install python 3.6 or 3.7 (recommendation to install python [pyenv](https://github.com/pyenv/pyenv)) +- Install node (recommendation to install node [nvm](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04-es#:~:text=de%20Node.js.-,Opci%C3%B3n%203%3A%20Instalar%20Node%20usando%20el%20administrador%20de%20versiones%20de%20Node,-Otra%20forma%20de)) + +### **Add variables** +In the root directory /time-tracker-backend create a file .env with these values + +``` +export MS_AUTHORITY=XXX +export MS_CLIENT_ID=XXX +export MS_SCOPE=XXX +export MS_SECRET=XXX +export MS_ENDPOINT=XXX +export DATABASE_ACCOUNT_URI=XXX +export DATABASE_MASTER_KEY=XXX +export DATABASE_NAME=XXX +export FLASK_APP=XXX +export FLASK_ENV=XXX +export AZURE_APP_CONFIGURATION_CONNECTION_STRING=XXX +export USERID=XXX +export FLASK_DEBUG=True +export PYTHONPATH=XXX +export DB_CONNECTION=XXX +export ENVIRONMENT=XXX +``` + +In the directory /V2 create a file .env with these values +``` +DB_USER=XXX +DB_PASS=XXX +DB_NAME=XXX +``` + +### **Install dependencies** +In the Directory /V2 +``` +make install +``` + +## **Start Project** +In the directory /V2 +``` +npm run offline +docker compose up or make start-local +``` + + +## **Makefile to run a locally CI** Execute the next command to show makefile help: ```shell -make help +$ make help ``` - To install the dependencies type the command ```make install``` - - To test the project type the command ```make test``` - - To run the local database type the command ```make start-local``` + +## **How to contribute to the project** +Clone the repository and from the master branch create a new branch for each new task. +### **Branch names format** +For example if your task in Jira is **TT-48 implement semantic versioning** your branch name is: +``` + TT-48-implement-semantic-versioning +``` +### **Commit messages format** + + + Below there are some common examples you can use for your commit messages [semantic version](https://semver.org/) : + + - **feat**: A new feature. + - **fix**: A bug fix. + - **perf**: A code change that improves performance. + - **build**: Changes that affect the build system or external dependencies (example scopes: npm, ts configuration). + - **ci**: Changes to our CI or CD configuration files and scripts (example scopes: Azure devops, github actions). + - **docs**: Documentation only changes. + - **refactor**: A code change that neither fixes a bug nor adds a feature. + It is important to mention that this key is not related to css styles. + - **test**: Adding missing tests or correcting existing tests. + + ### Example + fix: TT-48 implement semantic versioning + + Prefix to use in the space fix: + `(fix: |feat: |perf: |build: |ci: |docs: |refactor: |style: |test: )` \ No newline at end of file diff --git a/V2/serverless.yml b/V2/serverless.yml index e5dea8e9..85ef4621 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -106,3 +106,43 @@ functions: - POST route: customers/ authLevel: anonymous + + get_projects: + handler: time_tracker/projects/interface.get_projects + events: + - http: true + x-azure-settings: + methods: + - GET + route: projects/{id:?} + authLevel: anonymous + + delete_project: + handler: time_tracker/projects/interface.delete_project + events: + - http: true + x-azure-settings: + methods: + - DELETE + route: projects/{id} + authLevel: anonymous + + update_project: + handler: time_tracker/projects/interface.update_project + events: + - http: true + x-azure-settings: + methods: + - PUT + route: projects/{id} + authLevel: anonymous + + create_project: + handler: time_tracker/projects/interface.create_project + events: + - http: true + x-azure-settings: + methods: + - POST + route: projects/ + authLevel: anonymous diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 994c74c7..7c0de311 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -17,8 +17,6 @@ def test__activity_azure_endpoint__returns_all_activities( insert_activity(existent_activities[1], test_db).__dict__ ] - azure_activities._get_activities.DATABASE = test_db - req = func.HttpRequest(method='GET', body=None, url=ACTIVITY_URL) response = azure_activities._get_activities.get_activities(req) activities_json_data = response.get_body().decode("utf-8") @@ -33,7 +31,6 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it existent_activity = activity_factory() inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._get_activities.DATABASE = test_db req = func.HttpRequest( method='GET', body=None, @@ -54,7 +51,6 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe existent_activity = activity_factory() inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._delete_activity.DATABASE = test_db req = func.HttpRequest( method='DELETE', body=None, @@ -76,7 +72,6 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act existent_activity = activity_factory() inserted_activity = insert_activity(existent_activity, test_db).__dict__ - azure_activities._update_activity.DATABASE = test_db activity_body = {"description": Faker().sentence()} req = func.HttpRequest( method='PUT', diff --git a/V2/tests/api/azure/project_azure_endpoints_test.py b/V2/tests/api/azure/project_azure_endpoints_test.py new file mode 100644 index 00000000..a0cbc186 --- /dev/null +++ b/V2/tests/api/azure/project_azure_endpoints_test.py @@ -0,0 +1,254 @@ +import json + +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 +from time_tracker.utils.enums import ResponseEnums as enums + +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 +): + inserted_projects = [ + insert_project().__dict__, + insert_project().__dict__ + ] + + req = func.HttpRequest(method='GET', body=None, url=PROJECT_URL) + response = azure_projects._get_projects.get_projects(req) + projects_json_data = response.get_body().decode("utf-8") + + assert response.status_code == enums.STATUS_OK.value + assert projects_json_data == json.dumps(inserted_projects) + + +def test__project_azure_endpoint__returns_an_project__when_project_matches_its_id( + insert_project +): + inserted_project = insert_project().__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=PROJECT_URL, + route_params={"id": inserted_project["id"]}, + ) + + response = azure_projects._get_projects.get_projects(req) + activitiy_json_data = response.get_body().decode("utf-8") + + assert response.status_code == enums.STATUS_OK.value + assert activitiy_json_data == json.dumps(inserted_project) + + +def test__projects_azure_endpoint__returns_a_status_code_400__when_project_recive_invalid_id( +): + req = func.HttpRequest( + method="GET", + body=None, + url=PROJECT_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_projects._get_projects.get_projects(req) + + assert response.status_code == enums.STATUS_BAD_REQUEST.value + assert response.get_body() == enums.INVALID_ID.value.encode() + + +def test__project_azure_endpoint__returns_an_project_with_inactive_status__when_a_project_matching_its_id_is_found( + insert_project +): + inserted_project = insert_project().__dict__ + + req = func.HttpRequest( + method='DELETE', + body=None, + url=PROJECT_URL, + route_params={"id": inserted_project["id"]}, + ) + + response = azure_projects._delete_project.delete_project(req) + project_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == enums.STATUS_OK.value + assert project_json_data['status'] == 0 + assert project_json_data['deleted'] is True + + +def test__delete_projects_azure_endpoint__returns_a_status_code_400__when_project_recive_invalid_id( +): + req = func.HttpRequest( + method="DELETE", + body=None, + url=PROJECT_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_projects._delete_project.delete_project(req) + + assert response.status_code == enums.STATUS_BAD_REQUEST.value + assert response.get_body() == enums.INVALID_ID.value.encode() + + +def test__delete_projects_azure_endpoint__returns_a_status_code_404__when_no_found_a_project_to_delete( +): + req = func.HttpRequest( + method="DELETE", + body=None, + url=PROJECT_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_projects._delete_project.delete_project(req) + + assert response.status_code == enums.STATUS_NOT_FOUND.value + assert response.get_body() == enums.NOT_FOUND.value.encode() + + +def test__update_project_azure_endpoint__returns_an_project__when_found_a_project_to_update( + insert_project +): + inserted_project = insert_project().__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": inserted_project["id"]}, + ) + + response = azure_projects._update_project.update_project(req) + activitiy_json_data = response.get_body().decode("utf-8") + inserted_project.update(project_body) + + assert response.status_code == enums.STATUS_OK.value + assert activitiy_json_data == json.dumps(inserted_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__ + + req = func.HttpRequest( + method="PUT", + body=json.dumps(project_body).encode("utf-8"), + url=PROJECT_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_projects._update_project.update_project(req) + + assert response.status_code == enums.STATUS_NOT_FOUND.value + assert response.get_body() == enums.NOT_FOUND.value.encode() + + +def test__update_projects_azure_endpoint__returns_a_status_code_400__when_recive_an_incorrect_body( +): + project_body = Faker().pydict(5, True, str) + req = func.HttpRequest( + method="PUT", + body=json.dumps(project_body).encode("utf-8"), + url=PROJECT_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_projects._update_project.update_project(req) + + assert response.status_code == enums.STATUS_BAD_REQUEST.value + assert response.get_body() == enums.INCORRECT_BODY.value.encode() + + +def test__update_projects_azure_endpoint__returns_a_status_code_400__when_project_recive_invalid_id( +): + req = func.HttpRequest( + method="PUT", + body=None, + url=PROJECT_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_projects._update_project.update_project(req) + + assert response.status_code == enums.STATUS_BAD_REQUEST.value + assert response.get_body() == enums.INVALID_ID.value.encode() + + +def test__project_azure_endpoint__creates_an_project__when_project_has_all_attributes( + test_db, project_factory, insert_customer, customer_factory +): + insert_customer(customer_factory(), test_db) + project_body = project_factory().__dict__ + body = json.dumps(project_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body=body, + url=PROJECT_URL, + ) + + response = azure_projects._create_project.create_project(req) + project_json_data = json.loads(response.get_body()) + project_body['id'] = project_json_data['id'] + + assert response.status_code == enums.STATUS_CREATED.value + assert project_json_data == project_body + + +def test__project_azure_endpoint__returns_a_status_code_400__when_project_does_not_all_attributes( + test_db, project_factory, insert_customer, customer_factory +): + inserted_customer = insert_customer(customer_factory(), test_db) + project_body = project_factory(customer_id=inserted_customer.id).__dict__ + project_body.pop('name') + + body = json.dumps(project_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body=body, + url=PROJECT_URL, + ) + + response = azure_projects._create_project.create_project(req) + + assert response.status_code == enums.STATUS_BAD_REQUEST.value + assert response.get_body() == json.dumps(['The name key is missing in the input data']).encode() + + +def test__project_azure_endpoint__returns_a_status_code_500__when_project_recive_incorrect_type_data( + project_factory, insert_customer, customer_factory, test_db +): + insert_customer(customer_factory(), test_db) + project_body = project_factory(technologies=Faker().pylist(2, True, str)).__dict__ + + body = json.dumps(project_body).encode("utf-8") + print(project_body) + req = func.HttpRequest( + method='POST', + body=body, + url=PROJECT_URL, + ) + + response = azure_projects._create_project.create_project(req) + + assert response.status_code == enums.INTERNAL_SERVER_ERROR.value + assert response.get_body() == enums.NOT_CREATED.value.encode() diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index 4ad03c51..ff67203c 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,4 +1,5 @@ # flake8: noqa from fixtures import _activity_factory, _test_db, _insert_activity from fixtures import _time_entry_factory -from fixtures import _customer_factory +from fixtures import _customer_factory, _insert_customer +from fixtures import _project_factory diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index a02a74bb..037ed522 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -2,9 +2,11 @@ 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.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 from time_tracker._infrastructure import DB @@ -94,3 +96,38 @@ def _make_customer( return customer return _make_customer + + +@pytest.fixture(name='project_factory') +def _project_factory() -> projects_domain.Project: + def _make_project( + id=Faker().pyint(), + name=Faker().name(), + description=Faker().sentence(), + project_type_id=Faker().pyint(), + customer_id=Faker().pyint(), + status=Faker().pyint(), + deleted=False, + technologies=str(Faker().pylist()) + ): + project = projects_domain.Project( + id=id, + name=name, + description=description, + project_type_id=project_type_id, + customer_id=customer_id, + status=status, + deleted=deleted, + technologies=technologies + ) + return project + return _make_project + + +@pytest.fixture(name='insert_customer') +def _insert_customer() -> customers_domain.Customer: + def _new_customer(customer: customers_domain.Customer, database: DB): + dao = customers_infrastructure.CustomersSQLDao(database) + new_customer = dao.create(customer) + return new_customer + return _new_customer diff --git a/V2/tests/integration/daos/activities_sql_dao_test.py b/V2/tests/integration/daos/activities_dao_test.py similarity index 98% rename from V2/tests/integration/daos/activities_sql_dao_test.py rename to V2/tests/integration/daos/activities_dao_test.py index 0f0170af..637a7799 100644 --- a/V2/tests/integration/daos/activities_sql_dao_test.py +++ b/V2/tests/integration/daos/activities_dao_test.py @@ -9,7 +9,7 @@ @pytest.fixture(name='create_fake_dao') def _create_fake_dao() -> domain.ActivitiesDao: - db_fake = DB('sqlite:///:memory:') + db_fake = DB() dao = infrastructure.ActivitiesSQLDao(db_fake) return dao @@ -17,7 +17,7 @@ def _create_fake_dao() -> domain.ActivitiesDao: @pytest.fixture(name='clean_database', autouse=True) def _clean_database(): yield - db_fake = DB('sqlite:///:memory:') + db_fake = DB() dao = infrastructure.ActivitiesSQLDao(db_fake) query = dao.activity.delete() dao.db.get_session().execute(query) diff --git a/V2/tests/integration/daos/projects_dao_test.py b/V2/tests/integration/daos/projects_dao_test.py new file mode 100644 index 00000000..64837e37 --- /dev/null +++ b/V2/tests/integration/daos/projects_dao_test.py @@ -0,0 +1,149 @@ +import pytest +import typing +from faker import Faker + +from time_tracker.projects import _domain as domain +from time_tracker.projects import _infrastructure as infrastructure +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() + dao = infrastructure.ProjectsSQLDao(db_fake) + return dao + + +@pytest.fixture(name='clean_database', autouse=True) +def _clean_database(): + yield + db_fake = DB() + dao = infrastructure.ProjectsSQLDao(db_fake) + query = dao.project.delete() + dao.db.get_session().execute(query) + + +def test__create_project__returns_a_project_dto__when_saves_correctly_with_sql_database( + create_fake_dao, project_factory, insert_customer, customer_factory +): + dao = create_fake_dao + inserted_customer = insert_customer(customer_factory(), dao.db) + project_to_insert = project_factory(customer_id=inserted_customer.id) + + inserted_project = dao.create(project_to_insert) + + assert isinstance(inserted_project, domain.Project) + assert inserted_project == project_to_insert + + +def test_update__returns_an_update_project__when_an_project_matching_its_id_is_found_with_sql_database( + create_fake_dao, insert_project +): + dao = create_fake_dao + + inserted_project = insert_project() + + expected_description = Faker().sentence() + updated_project = dao.update(inserted_project.id, {"description": expected_description}) + + assert isinstance(updated_project, domain.Project) + assert updated_project.id == inserted_project.id + assert updated_project.description == expected_description + + +def test_update__returns_none__when_no_project_matching_its_id_is_found_with_sql_database( + create_fake_dao, project_factory +): + dao = create_fake_dao + project_to_insert = project_factory() + + results = dao.update(project_to_insert.id, {"description": Faker().sentence()}) + + assert results is None + + +def test__get_all__returns_a_list_of_project_dto_objects__when_one_or_more_projects_are_found_with_sql_database( + create_fake_dao, insert_project +): + dao = create_fake_dao + + inserted_projects = [ + insert_project(), + insert_project() + ] + + projects = dao.get_all() + assert isinstance(projects, typing.List) + assert projects == inserted_projects + + +def test_get_by_id__returns_an_project_dto__when_found_one_project_that_matches_its_id_with_sql_database( + create_fake_dao, insert_project +): + dao = create_fake_dao + + inserted_project = insert_project() + + project = dao.get_by_id(inserted_project.id) + + assert isinstance(project, domain.Project) + assert project.id == inserted_project.id + assert project == inserted_project + + +def test__get_by_id__returns_none__when_no_project_matches_its_id_with_sql_database( + create_fake_dao, project_factory +): + dao = create_fake_dao + project_to_insert = project_factory() + + project = dao.get_by_id(project_to_insert.id) + + assert project is None + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_projects_with_sql_database( + create_fake_dao +): + projects = create_fake_dao.get_all() + + assert isinstance(projects, typing.List) + assert projects == [] + + +def test_delete__returns_an_project_with_inactive_status__when_an_project_matching_its_id_is_found_with_sql_database( + create_fake_dao, insert_project +): + dao = create_fake_dao + + inserted_project = insert_project() + + project = dao.delete(inserted_project.id) + + assert isinstance(project, domain.Project) + assert project.id == inserted_project.id + assert project.status == 0 + assert project.deleted is True + + +def test_delete__returns_none__when_no_project_matching_its_id_is_found_with_sql_database( + create_fake_dao, project_factory +): + dao = create_fake_dao + project_to_insert = project_factory() + + results = dao.delete(project_to_insert.id) + + assert results is None diff --git a/V2/tests/unit/services/project_service_test.py b/V2/tests/unit/services/project_service_test.py new file mode 100644 index 00000000..9baf657e --- /dev/null +++ b/V2/tests/unit/services/project_service_test.py @@ -0,0 +1,74 @@ +from faker import Faker + +from time_tracker.projects._domain import ProjectService + + +def test__get_all__uses_the_project_dao__to_retrieve_projects(mocker): + expected_projects = mocker.Mock() + project_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_projects) + ) + project_service = ProjectService(project_dao) + + actual_projects = project_service.get_all() + + assert project_dao.get_all.called + assert expected_projects == actual_projects + + +def test__get_by_id__uses_the_project_dao__to_retrieve_one_project(mocker): + expected_project = mocker.Mock() + project_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_project) + ) + project_service = ProjectService(project_dao) + + actual_project = project_service.get_by_id(Faker().pyint()) + + assert project_dao.get_by_id.called + assert expected_project == actual_project + + +def test__delete_project__uses_the_project_dao__to_change_project_status( + mocker, +): + expected_project = mocker.Mock() + project_dao = mocker.Mock( + delete=mocker.Mock(return_value=expected_project) + ) + + project_service = ProjectService(project_dao) + deleted_project = project_service.delete(Faker().pyint()) + + assert project_dao.delete.called + assert expected_project == deleted_project + + +def test__update_project__uses_the_project_dao__to_update_one_project( + mocker, +): + expected_project = mocker.Mock() + project_dao = mocker.Mock( + update=mocker.Mock(return_value=expected_project) + ) + project_service = ProjectService(project_dao) + + updated_project = project_service.update( + Faker().pyint(), Faker().pydict() + ) + + assert project_dao.update.called + assert expected_project == updated_project + + +def test__create_project__uses_the_project_dao__to_create_an_project(mocker, project_factory): + expected_project = mocker.Mock() + project_dao = mocker.Mock( + create=mocker.Mock(return_value=expected_project) + ) + project_service = ProjectService(project_dao) + + actual_project = project_service.create(project_factory()) + + assert project_dao.create.called + assert expected_project == actual_project diff --git a/V2/tests/unit/use_cases/projects_use_case_test.py b/V2/tests/unit/use_cases/projects_use_case_test.py new file mode 100644 index 00000000..22167418 --- /dev/null +++ b/V2/tests/unit/use_cases/projects_use_case_test.py @@ -0,0 +1,80 @@ +from pytest_mock import MockFixture +from faker import Faker + +from time_tracker.projects._domain import _use_cases + + +def test__create_project_function__uses_the_projects_service__to_create_project( + mocker: MockFixture, project_factory +): + expected_project = mocker.Mock() + project_service = mocker.Mock( + create=mocker.Mock(return_value=expected_project) + ) + + project_use_case = _use_cases.CreateProjectUseCase(project_service) + actual_project = project_use_case.create_project(project_factory()) + + assert project_service.create.called + assert expected_project == actual_project + + +def test__delete_project_function__uses_the_project_service__to_delete_project_selected( + mocker: MockFixture, +): + expected_project = mocker.Mock() + project_service = mocker.Mock(delete=mocker.Mock(return_value=expected_project)) + + project_use_case = _use_cases.DeleteProjectUseCase(project_service) + deleted_project = project_use_case.delete_project(Faker().pyint()) + + assert project_service.delete.called + assert expected_project == deleted_project + + +def test__get_list_projects_function__uses_the_project_service__to_retrieve_projects( + mocker: MockFixture, +): + expected_projects = mocker.Mock() + project_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_projects) + ) + + projects_use_case = _use_cases.GetProjectsUseCase(project_service) + actual_projects = projects_use_case.get_projects() + + assert project_service.get_all.called + assert expected_projects == actual_projects + + +def test__get_project_by_id_function__uses_the_project_service__to_retrieve_project( + mocker: MockFixture, +): + expected_project = mocker.Mock() + project_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_project) + ) + + project_use_case = _use_cases.GetProjectUseCase(project_service) + actual_project = project_use_case.get_project_by_id(Faker().pyint()) + + assert project_service.get_by_id.called + assert expected_project == actual_project + + +def test__update_project_function__uses_the_projects_service__to_update_an_project( + mocker: MockFixture, project_factory +): + expected_project = mocker.Mock() + project_service = mocker.Mock( + update=mocker.Mock(return_value=expected_project) + ) + project_to_update = project_factory() + + project_use_case = _use_cases.UpdateProjectUseCase(project_service) + updated_project = project_use_case.update_project( + Faker().pyint(), project_to_update.__dict__ + ) + + assert project_service.update.called + assert expected_project == updated_project diff --git a/V2/time_tracker/activities/_application/_activities/_create_activity.py b/V2/time_tracker/activities/_application/_activities/_create_activity.py index 94f3701d..8d5b912d 100644 --- a/V2/time_tracker/activities/_application/_activities/_create_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_create_activity.py @@ -8,11 +8,10 @@ from ... import _infrastructure from time_tracker._infrastructure import DB -DATABASE = DB() - def create_activity(req: func.HttpRequest) -> func.HttpResponse: - activity_dao = _infrastructure.ActivitiesSQLDao(DATABASE) + database = DB() + activity_dao = _infrastructure.ActivitiesSQLDao(database) activity_service = _domain.ActivityService(activity_dao) use_case = _domain._use_cases.CreateActivityUseCase(activity_service) diff --git a/V2/time_tracker/activities/_application/_activities/_delete_activity.py b/V2/time_tracker/activities/_application/_activities/_delete_activity.py index 14ada8ab..746b1073 100644 --- a/V2/time_tracker/activities/_application/_activities/_delete_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_delete_activity.py @@ -7,8 +7,6 @@ from ... import _infrastructure from time_tracker._infrastructure import DB -DATABASE = DB() - def delete_activity(req: func.HttpRequest) -> func.HttpResponse: logging.info( @@ -29,8 +27,9 @@ def delete_activity(req: func.HttpRequest) -> func.HttpResponse: def _delete(activity_id: int) -> str: + database = DB() activity_use_case = _domain._use_cases.DeleteActivityUseCase( - _create_activity_service(DATABASE) + _create_activity_service(database) ) activity = activity_use_case.delete_activity(activity_id) return json.dumps(activity.__dict__) if activity else b'Not found' diff --git a/V2/time_tracker/activities/_application/_activities/_get_activities.py b/V2/time_tracker/activities/_application/_activities/_get_activities.py index d92503dd..dd6053b0 100644 --- a/V2/time_tracker/activities/_application/_activities/_get_activities.py +++ b/V2/time_tracker/activities/_application/_activities/_get_activities.py @@ -7,10 +7,9 @@ from ... import _infrastructure from time_tracker._infrastructure import DB -DATABASE = DB() - def get_activities(req: func.HttpRequest) -> func.HttpResponse: + database = DB() logging.info( 'Python HTTP trigger function processed a request to get an activity.' ) @@ -19,11 +18,11 @@ def get_activities(req: func.HttpRequest) -> func.HttpResponse: try: if activity_id: - response = _get_by_id(int(activity_id)) + response = _get_by_id(int(activity_id), database) if response == b'Not Found': status_code = 404 else: - response = _get_all() + response = _get_all(database) return func.HttpResponse( body=response, status_code=status_code, mimetype="application/json" @@ -34,18 +33,18 @@ def get_activities(req: func.HttpRequest) -> func.HttpResponse: ) -def _get_by_id(activity_id: int) -> str: +def _get_by_id(activity_id: int, database: DB) -> str: activity_use_case = _domain._use_cases.GetActivityUseCase( - _create_activity_service(DATABASE) + _create_activity_service(database) ) activity = activity_use_case.get_activity_by_id(activity_id) return json.dumps(activity.__dict__) if activity else b'Not Found' -def _get_all() -> str: +def _get_all(database: DB) -> str: activities_use_case = _domain._use_cases.GetActivitiesUseCase( - _create_activity_service(DATABASE) + _create_activity_service(database) ) return json.dumps( [ diff --git a/V2/time_tracker/activities/_application/_activities/_update_activity.py b/V2/time_tracker/activities/_application/_activities/_update_activity.py index 0933fd72..4717042c 100644 --- a/V2/time_tracker/activities/_application/_activities/_update_activity.py +++ b/V2/time_tracker/activities/_application/_activities/_update_activity.py @@ -8,8 +8,6 @@ from ... import _infrastructure from time_tracker._infrastructure import DB -DATABASE = DB() - def update_activity(req: func.HttpRequest) -> func.HttpResponse: logging.info( @@ -37,8 +35,9 @@ def update_activity(req: func.HttpRequest) -> func.HttpResponse: def _update(activity_id: int, activity_data: dict) -> str: + database = DB() activity_use_case = _domain._use_cases.UpdateActivityUseCase( - _create_activity_service(DATABASE) + _create_activity_service(database) ) activity = activity_use_case.update_activity( activity_id, activity_data.get("name"), diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py index 1e7220c5..35c209db 100644 --- a/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py +++ b/V2/time_tracker/activities/_infrastructure/_data_persistence/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._activities_sql_dao import ActivitiesSQLDao +from ._activities_dao import ActivitiesSQLDao diff --git a/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py b/V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_dao.py similarity index 100% rename from V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_sql_dao.py rename to V2/time_tracker/activities/_infrastructure/_data_persistence/_activities_dao.py diff --git a/V2/time_tracker/projects/_application/__init__.py b/V2/time_tracker/projects/_application/__init__.py new file mode 100644 index 00000000..6b48fb8a --- /dev/null +++ b/V2/time_tracker/projects/_application/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa +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 diff --git a/V2/time_tracker/projects/_application/_projects/__init__.py b/V2/time_tracker/projects/_application/_projects/__init__.py new file mode 100644 index 00000000..9f87eef2 --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa +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 diff --git a/V2/time_tracker/projects/_application/_projects/_create_project.py b/V2/time_tracker/projects/_application/_projects/_create_project.py new file mode 100644 index 00000000..7ed423ce --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/_create_project.py @@ -0,0 +1,57 @@ +import dataclasses +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB as database +from time_tracker.utils.enums import ResponseEnums as enums + + +def create_project(req: func.HttpRequest) -> func.HttpResponse: + + project_dao = _infrastructure.ProjectsSQLDao(database()) + project_service = _domain.ProjectService(project_dao) + use_case = _domain._use_cases.CreateProjectUseCase(project_service) + + project_data = req.get_json() + + validation_errors = _validate_project(project_data) + if validation_errors: + status_code = enums.STATUS_BAD_REQUEST.value + response = json.dumps(validation_errors) + else: + project_to_create = _domain.Project( + id=None, + name=project_data["name"], + description=project_data["description"], + project_type_id=project_data["project_type_id"], + customer_id=project_data["customer_id"], + status=project_data["status"], + deleted=False, + technologies=project_data["technologies"] + ) + + created_project = use_case.create_project(project_to_create) + + status_code, response = [ + enums.INTERNAL_SERVER_ERROR.value, enums.NOT_CREATED.value.encode() + ] if not created_project else [enums.STATUS_CREATED.value, json.dumps(created_project.__dict__)] + + return func.HttpResponse( + body=response, + status_code=status_code, + mimetype=enums.MIME_TYPE.value + ) + + +def _validate_project(project_data: dict) -> typing.List[str]: + project_fields = [field.name for field in dataclasses.fields(_domain.Project) + if field.type != typing.Optional[field.type]] + missing_keys = [field for field in project_fields if field not in project_data] + return [ + f'The {missing_key} key is missing in the input data' + for missing_key in missing_keys + ] diff --git a/V2/time_tracker/projects/_application/_projects/_delete_project.py b/V2/time_tracker/projects/_application/_projects/_delete_project.py new file mode 100644 index 00000000..f4c378ea --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/_delete_project.py @@ -0,0 +1,35 @@ +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB +from time_tracker.utils.enums import ResponseEnums as enums + + +def delete_project(req: func.HttpRequest) -> func.HttpResponse: + project_dao = _infrastructure.ProjectsSQLDao(DB()) + project_service = _domain.ProjectService(project_dao) + use_case = _domain._use_cases.DeleteProjectUseCase(project_service) + + try: + project_id = int(req.route_params.get("id")) + deleted_project = use_case.delete_project(project_id) + + status_code, response = [ + enums.STATUS_NOT_FOUND.value, enums.NOT_FOUND.value.encode() + ] if not deleted_project else [enums.STATUS_OK.value, json.dumps(deleted_project.__dict__)] + + return func.HttpResponse( + body=response, + status_code=status_code, + mimetype=enums.MIME_TYPE.value, + ) + + except ValueError: + return func.HttpResponse( + body=enums.INVALID_ID.value.encode(), + status_code=enums.STATUS_BAD_REQUEST.value, + mimetype=enums.MIME_TYPE.value + ) diff --git a/V2/time_tracker/projects/_application/_projects/_get_projects.py b/V2/time_tracker/projects/_application/_projects/_get_projects.py new file mode 100644 index 00000000..0909479b --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/_get_projects.py @@ -0,0 +1,57 @@ +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB +from time_tracker.utils.enums import ResponseEnums as enums + + +def get_projects(req: func.HttpRequest) -> func.HttpResponse: + database = DB() + project_dao = _infrastructure.ProjectsSQLDao(database) + project_service = _domain.ProjectService(project_dao) + + project_id = req.route_params.get("id") + + try: + if project_id: + response = _get_by_id(int(project_id), project_service) + if not response: + return func.HttpResponse( + body=enums.NOT_FOUND.value.encode(), + status_code=enums.STATUS_NOT_FOUND.value, + mimetype=enums.MIME_TYPE.value + ) + else: + response = _get_all(project_service) + + return func.HttpResponse( + body=json.dumps(response), + status_code=enums.STATUS_OK.value, + mimetype=enums.MIME_TYPE.value, + ) + + except ValueError: + return func.HttpResponse( + body=enums.INVALID_ID.value.encode(), + status_code=enums.STATUS_BAD_REQUEST.value, + mimetype=enums.MIME_TYPE.value + ) + + +def _get_by_id(project_id: int, project_service: _domain.ProjectService) -> str: + use_case = _domain._use_cases.GetProjectUseCase(project_service) + project = use_case.get_project_by_id(project_id) + + return project.__dict__ if project else None + + +def _get_all(project_service: _domain.ProjectService) -> typing.List: + use_case = _domain._use_cases.GetProjectsUseCase(project_service) + return [ + project.__dict__ + for project in use_case.get_projects() + ] diff --git a/V2/time_tracker/projects/_application/_projects/_update_project.py b/V2/time_tracker/projects/_application/_projects/_update_project.py new file mode 100644 index 00000000..ecf064bd --- /dev/null +++ b/V2/time_tracker/projects/_application/_projects/_update_project.py @@ -0,0 +1,48 @@ +import dataclasses +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB +from time_tracker.utils.enums import ResponseEnums as enums + + +def update_project(req: func.HttpRequest) -> func.HttpResponse: + database = DB() + project_dao = _infrastructure.ProjectsSQLDao(database) + project_service = _domain.ProjectService(project_dao) + use_case = _domain._use_cases.UpdateProjectUseCase(project_service) + + try: + project_id = int(req.route_params.get("id")) + project_data = req.get_json() + + if not _validate_project(project_data): + status_code = enums.STATUS_BAD_REQUEST.value + response = enums.INCORRECT_BODY.value.encode() + + else: + updated_project = use_case.update_project(project_id, project_data) + status_code, response = [ + enums.STATUS_NOT_FOUND.value, enums.NOT_FOUND.value.encode() + ] if not updated_project else [enums.STATUS_OK.value, json.dumps(updated_project.__dict__)] + + return func.HttpResponse( + body=response, + status_code=status_code, + mimetype=enums.MIME_TYPE.value, + ) + + except ValueError: + return func.HttpResponse( + body=enums.INVALID_ID.value.encode(), + status_code=enums.STATUS_BAD_REQUEST.value, + mimetype=enums.MIME_TYPE.value, + ) + + +def _validate_project(project_data: dict) -> bool: + project_keys = [field.name for field in dataclasses.fields(_domain.Project)] + return all(key in project_keys for key in project_data.keys()) diff --git a/V2/time_tracker/projects/_domain/__init__.py b/V2/time_tracker/projects/_domain/__init__.py new file mode 100644 index 00000000..c90dbcaf --- /dev/null +++ b/V2/time_tracker/projects/_domain/__init__.py @@ -0,0 +1,11 @@ +# flake8: noqa +from ._entities import Project +from ._persistence_contracts import ProjectsDao +from ._services import ProjectService +from ._use_cases import ( + CreateProjectUseCase, + DeleteProjectUseCase, + GetProjectsUseCase, + GetProjectUseCase, + UpdateProjectUseCase +) \ No newline at end of file diff --git a/V2/time_tracker/projects/_domain/_entities/__init__.py b/V2/time_tracker/projects/_domain/_entities/__init__.py new file mode 100644 index 00000000..693c3a41 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_entities/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._project import Project \ 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 new file mode 100644 index 00000000..0b2ffe1a --- /dev/null +++ b/V2/time_tracker/projects/_domain/_entities/_project.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass(frozen=True) +class Project: + id: Optional[int] + name: str + description: str + project_type_id: int + customer_id: int + status: int + deleted: Optional[bool] + technologies: List[str] diff --git a/V2/time_tracker/projects/_domain/_persistence_contracts/__init__.py b/V2/time_tracker/projects/_domain/_persistence_contracts/__init__.py new file mode 100644 index 00000000..b17214a7 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_persistence_contracts/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._projects_dao import ProjectsDao \ No newline at end of file diff --git a/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py b/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py new file mode 100644 index 00000000..f38c8ebd --- /dev/null +++ b/V2/time_tracker/projects/_domain/_persistence_contracts/_projects_dao.py @@ -0,0 +1,25 @@ +import abc + +from .. import Project + + +class ProjectsDao(abc.ABC): + @abc.abstractmethod + def create(self, time_entry_data: Project) -> Project: + pass + + @abc.abstractmethod + def get_all(self) -> Project: + pass + + @abc.abstractmethod + def get_by_id(self, id: int) -> Project: + pass + + @abc.abstractmethod + def update(self, id: int, project_data: dict) -> Project: + pass + + @abc.abstractmethod + def delete(self, id: int) -> Project: + pass diff --git a/V2/time_tracker/projects/_domain/_services/__init__.py b/V2/time_tracker/projects/_domain/_services/__init__.py new file mode 100644 index 00000000..5eb9532b --- /dev/null +++ b/V2/time_tracker/projects/_domain/_services/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._project import ProjectService \ No newline at end of file diff --git a/V2/time_tracker/projects/_domain/_services/_project.py b/V2/time_tracker/projects/_domain/_services/_project.py new file mode 100644 index 00000000..0f99dafb --- /dev/null +++ b/V2/time_tracker/projects/_domain/_services/_project.py @@ -0,0 +1,24 @@ +import typing + +from .. import Project, ProjectsDao + + +class ProjectService: + + def __init__(self, project_dao: ProjectsDao): + self.project_dao = project_dao + + def create(self, project_data: Project) -> Project: + return self.project_dao.create(project_data) + + def get_all(self) -> typing.List[Project]: + return self.project_dao.get_all() + + def get_by_id(self, id: int) -> Project: + return self.project_dao.get_by_id(id) + + def update(self, id: int, project_data: dict) -> Project: + return self.project_dao.update(id, project_data) + + def delete(self, id: int) -> Project: + return self.project_dao.delete(id) diff --git a/V2/time_tracker/projects/_domain/_use_cases/__init__.py b/V2/time_tracker/projects/_domain/_use_cases/__init__.py new file mode 100644 index 00000000..defb127d --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +from ._create_project_use_case import CreateProjectUseCase +from ._delete_project_use_case import DeleteProjectUseCase +from ._get_project_by_id_use_case import GetProjectUseCase +from ._get_projects_use_case import GetProjectsUseCase +from ._update_project_use_case import UpdateProjectUseCase diff --git a/V2/time_tracker/projects/_domain/_use_cases/_create_project_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_create_project_use_case.py new file mode 100644 index 00000000..60b50687 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_create_project_use_case.py @@ -0,0 +1,10 @@ +from .. import Project, ProjectService + + +class CreateProjectUseCase: + + def __init__(self, project_service: ProjectService): + self.project_service = project_service + + def create_project(self, project_data: Project) -> Project: + return self.project_service.create(project_data) diff --git a/V2/time_tracker/projects/_domain/_use_cases/_delete_project_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_delete_project_use_case.py new file mode 100644 index 00000000..9dd91d4b --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_delete_project_use_case.py @@ -0,0 +1,10 @@ +from .. import Project, ProjectService + + +class DeleteProjectUseCase: + + def __init__(self, project_service: ProjectService): + self.project_service = project_service + + def delete_project(self, id: int) -> Project: + return self.project_service.delete(id) diff --git a/V2/time_tracker/projects/_domain/_use_cases/_get_project_by_id_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_get_project_by_id_use_case.py new file mode 100644 index 00000000..94573496 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_get_project_by_id_use_case.py @@ -0,0 +1,9 @@ +from .. import ProjectService, Project + + +class GetProjectUseCase: + def __init__(self, project_service: ProjectService): + self.project_service = project_service + + def get_project_by_id(self, id: int) -> Project: + return self.project_service.get_by_id(id) diff --git a/V2/time_tracker/projects/_domain/_use_cases/_get_projects_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_get_projects_use_case.py new file mode 100644 index 00000000..ccf0b3a4 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_get_projects_use_case.py @@ -0,0 +1,11 @@ +import typing + +from .. import Project, ProjectService + + +class GetProjectsUseCase: + def __init__(self, project_service: ProjectService): + self.project_service = project_service + + def get_projects(self) -> typing.List[Project]: + return self.project_service.get_all() diff --git a/V2/time_tracker/projects/_domain/_use_cases/_update_project_use_case.py b/V2/time_tracker/projects/_domain/_use_cases/_update_project_use_case.py new file mode 100644 index 00000000..628d7437 --- /dev/null +++ b/V2/time_tracker/projects/_domain/_use_cases/_update_project_use_case.py @@ -0,0 +1,9 @@ +from .. import ProjectService, Project + + +class UpdateProjectUseCase: + def __init__(self, projects_service: ProjectService): + self.projects_service = projects_service + + def update_project(self, id: int, project_data: dict) -> Project: + return self.projects_service.update(id, project_data) diff --git a/V2/time_tracker/projects/_infrastructure/__init__.py b/V2/time_tracker/projects/_infrastructure/__init__.py new file mode 100644 index 00000000..b940cba3 --- /dev/null +++ b/V2/time_tracker/projects/_infrastructure/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._data_persistence import ProjectsSQLDao diff --git a/V2/time_tracker/projects/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/projects/_infrastructure/_data_persistence/__init__.py new file mode 100644 index 00000000..b73fcf44 --- /dev/null +++ b/V2/time_tracker/projects/_infrastructure/_data_persistence/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._projects_dao import ProjectsSQLDao diff --git a/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py b/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py new file mode 100644 index 00000000..3f7950ed --- /dev/null +++ b/V2/time_tracker/projects/_infrastructure/_data_persistence/_projects_dao.py @@ -0,0 +1,74 @@ +import typing +import dataclasses + +import sqlalchemy as sq + +from ... import _domain as domain +from time_tracker._infrastructure import _db + + +class ProjectsSQLDao(domain.ProjectsDao): + + def __init__(self, database: _db.DB): + self.project_key = [field.name for field in dataclasses.fields(domain.Project)] + self.db = database + self.project = sq.Table( + 'project', + self.db.metadata, + sq.Column('id', sq.Integer, primary_key=True, autoincrement=True), + sq.Column('name', sq.String), + sq.Column('description', sq.String), + sq.Column('project_type_id', sq.Integer), + sq.Column('customer_id', sq.Integer, sq.ForeignKey('customer.id')), + sq.Column('status', sq.SmallInteger), + sq.Column('deleted', sq.BOOLEAN), + sq.Column( + 'technologies', + sq.ARRAY(sq.String).with_variant(sq.String, "sqlite") + ), + extend_existing=True, + ) + + def create(self, project_data: domain.Project) -> domain.Project: + try: + new_project = project_data.__dict__ + new_project.pop('id', None) + + 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) + + except sq.exc.SQLAlchemyError: + return None + + 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 + + def get_all(self) -> typing.List[domain.Project]: + query = sq.sql.select(self.project) + result = self.db.get_session().execute(query) + return [ + self.__create_project_dto(dict(project)) + for project in result + ] + + def delete(self, id: int) -> domain.Project: + query = ( + self.project.update() + .where(self.project.c.id == id) + .values({"deleted": True, "status": 0}) + ) + self.db.get_session().execute(query) + return self.get_by_id(id) + + def update(self, id: int, project_data: dict) -> domain.Project: + query = self.project.update().where(self.project.c.id == id).values(project_data) + self.db.get_session().execute(query) + return self.get_by_id(id) + + 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 new file mode 100644 index 00000000..2fb3244b --- /dev/null +++ b/V2/time_tracker/projects/interface.py @@ -0,0 +1,5 @@ +# flake8: noqa +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 diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py index a06c212c..95149c55 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -54,8 +54,8 @@ def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: def _validate_time_entry(time_entry_data: dict) -> typing.List[str]: - time_entry_fields = [field.name for field in dataclasses.fields(_domain.TimeEntry)] - time_entry_fields.pop(8) + time_entry_fields = [field.name for field in dataclasses.fields(_domain.TimeEntry) + if field.type != typing.Optional[field.type]] missing_keys = [field for field in time_entry_fields if field not in time_entry_data] return [ f'The {missing_key} key is missing in the input data' diff --git a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py index aa73a879..08df5f8f 100644 --- a/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_entities/_time_entry.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List +from typing import List, Optional @dataclass(frozen=True) class TimeEntry: - id: int + id: Optional[int] start_date: str owner_id: int description: str @@ -12,6 +12,6 @@ class TimeEntry: uri: str technologies: List[str] end_date: str - deleted: bool + deleted: Optional[bool] timezone_offset: str project_id: int diff --git a/V2/time_tracker/utils/enums/__init__.py b/V2/time_tracker/utils/enums/__init__.py new file mode 100644 index 00000000..317ca876 --- /dev/null +++ b/V2/time_tracker/utils/enums/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from .response_enums import ResponseEnums \ No newline at end of file diff --git a/V2/time_tracker/utils/enums/response_enums.py b/V2/time_tracker/utils/enums/response_enums.py new file mode 100644 index 00000000..781cba86 --- /dev/null +++ b/V2/time_tracker/utils/enums/response_enums.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class ResponseEnums(Enum): + STATUS_CREATED = 201 + STATUS_OK = 200 + STATUS_BAD_REQUEST = 400 + STATUS_NOT_FOUND = 404 + INTERNAL_SERVER_ERROR = 500 + + INVALID_ID = "Invalid Format ID" + NOT_FOUND = "Not found" + NOT_CREATED = "could not be created" + INCORRECT_BODY = "Incorrect body" + + MIME_TYPE = "application/json"