From 442a348e75d2313ddcdc822ab786cd09868bfeb0 Mon Sep 17 00:00:00 2001 From: EliuX Date: Wed, 25 Mar 2020 17:03:31 -0500 Subject: [PATCH 1/2] Close #13 Test the api ns for time entries --- README.md | 16 +- tests/projects/projects_namespace_test.py | 36 +-- tests/smoke_test.py | 26 ++ tests/time_entries/__init__.py | 0 .../time_entries_namespace_test.py | 247 ++++++++++++++++++ time_tracker_api/api.py | 13 +- time_tracker_api/config.py | 5 +- time_tracker_api/database.py | 19 +- time_tracker_api/projects/projects_model.py | 3 +- .../projects/projects_namespace.py | 4 +- time_tracker_api/sql_repository.py | 9 +- .../time_entries/time_entries_model.py | 21 +- .../time_entries/time_entries_namespace.py | 10 +- 13 files changed, 337 insertions(+), 72 deletions(-) create mode 100644 tests/time_entries/__init__.py create mode 100644 tests/time_entries/time_entries_namespace_test.py diff --git a/README.md b/README.md index c012b5f0..6d419955 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ automatically [pip](https://pip.pypa.io/en/stable/) as well. - A virtual environment, namely [venv](https://docs.python.org/3/library/venv.html). ### Setup - - Create and activate the environment, In Windows: @@ -111,8 +110,9 @@ python3 -m pytest -v ``` The database tests will be done in the table `tests` of the database specified by the variable `DATABASE_URI`. If this -variable is not specified it will automatically connect to `sqlite:///tests.db`. This will do, because we are using SQL -Alchemy to be able connect to any SQL database maintaining the same codebase. +variable is not specified it will automatically connect to SQLite database in-memory. This will do, because we are using +[SQL Alchemy](https://www.sqlalchemy.org/features.html) to be able connect to any SQL database maintaining the same +codebase. The option `-v` shows which tests failed or succeeded. Have into account that you can also debug each test @@ -132,10 +132,12 @@ To get a report table ``` To get a full report in html + ```bash coverage html ``` -Then check in the [htmlcov/index.html](./htmlcov/index.html) to see it + +Then check in the [htmlcov/index.html](./htmlcov/index.html) to see it. If you want that previously collected coverage data is erased, you can execute: @@ -144,7 +146,6 @@ coverage erase ``` ### CLI - There are available commands, aware of the API, that can be very helpful to you. You can check them out by running @@ -160,7 +161,6 @@ python cli.py gen_swagger_json -f ~/Downloads/swagger.json ``` ## Run as docker container - 1. Build image ```bash docker build -t time_tracker_api:local . @@ -178,8 +178,8 @@ docker run -p 5000:5000 time_tracker_api:local the win. - [Flask](http://flask.pocoo.org/) as the micro framework of choice. - [Flask RestPlus](https://flask-restplus.readthedocs.io/en/stable/) for building Restful APIs with Swagger. -- [Pytest](https://docs.pytest.org/en/latest/index.html) for tests -- [Coverage](https://coverage.readthedocs.io/en/coverage-4.5.4/) for coverage +- [Pytest](https://docs.pytest.org/en/latest/index.html) for tests. +- [Coverage](https://coverage.readthedocs.io/en/coverage-4.5.4/) for coverage. - [Swagger](https://swagger.io/) for documentation and standardization, taking into account the [API import restrictions and known issues](https://docs.microsoft.com/en-us/azure/api-management/api-management-api-import-restrictions) in Azure. diff --git a/tests/projects/projects_namespace_test.py b/tests/projects/projects_namespace_test.py index 283d85c4..4d800b25 100644 --- a/tests/projects/projects_namespace_test.py +++ b/tests/projects/projects_namespace_test.py @@ -1,6 +1,7 @@ from faker import Faker from flask import json from flask.testing import FlaskClient +from flask_restplus._http import HTTPStatus from pytest_mock import MockFixture from time_tracker_api.projects.projects_model import PROJECT_TYPE @@ -25,7 +26,7 @@ def test_create_project_should_succeed_with_valid_request(client: FlaskClient, m response = client.post("/projects", json=valid_project_data, follow_redirects=True) - assert 201 == response.status_code + assert HTTPStatus.CREATED == response.status_code repository_create_mock.assert_called_once_with(valid_project_data) @@ -40,7 +41,7 @@ def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: M response = client.post("/projects", json=invalid_project_data, follow_redirects=True) - assert 400 == response.status_code + assert HTTPStatus.BAD_REQUEST == response.status_code repository_create_mock.assert_not_called() @@ -52,24 +53,21 @@ def test_list_all_projects(client: FlaskClient, mocker: MockFixture): response = client.get("/projects", follow_redirects=True) - assert 200 == response.status_code - json_data = json.loads(response.data) - assert [] == json_data + assert HTTPStatus.OK == response.status_code + assert [] == json.loads(response.data) repository_find_all_mock.assert_called_once() def test_get_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): from time_tracker_api.projects.projects_namespace import project_dao - valid_id = fake.random_int(1, 9999) - repository_find_mock = mocker.patch.object(project_dao.repository, 'find', return_value=fake_project) response = client.get("/projects/%s" % valid_id, follow_redirects=True) - assert 200 == response.status_code + assert HTTPStatus.OK == response.status_code fake_project == json.loads(response.data) repository_find_mock.assert_called_once_with(str(valid_id)) @@ -86,11 +84,12 @@ def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient response = client.get("/projects/%s" % invalid_id, follow_redirects=True) - assert 404 == response.status_code + assert HTTPStatus.NOT_FOUND == response.status_code repository_find_mock.assert_called_once_with(str(invalid_id)) -def test_get_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import UnprocessableEntity @@ -102,7 +101,7 @@ def test_get_project_should_return_422_for_invalid_id_format(client: FlaskClient response = client.get("/projects/%s" % invalid_id, follow_redirects=True) - assert 422 == response.status_code + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_find_mock.assert_called_once_with(str(invalid_id)) @@ -116,7 +115,7 @@ def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mock valid_id = fake.random_int(1, 9999) response = client.put("/projects/%s" % valid_id, json=valid_project_data, follow_redirects=True) - assert 200 == response.status_code + assert HTTPStatus.OK == response.status_code fake_project == json.loads(response.data) repository_update_mock.assert_called_once_with(str(valid_id), valid_project_data) @@ -133,7 +132,7 @@ def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: M valid_id = fake.random_int(1, 9999) response = client.put("/projects/%s" % valid_id, json=invalid_project_data, follow_redirects=True) - assert 400 == response.status_code + assert HTTPStatus.BAD_REQUEST == response.status_code repository_update_mock.assert_not_called() @@ -151,7 +150,7 @@ def test_update_project_should_return_not_found_with_invalid_id(client: FlaskCli json=valid_project_data, follow_redirects=True) - assert 404 == response.status_code + assert HTTPStatus.NOT_FOUND == response.status_code repository_update_mock.assert_called_once_with(str(invalid_id), valid_project_data) @@ -166,7 +165,7 @@ def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker response = client.delete("/projects/%s" % valid_id, follow_redirects=True) - assert 204 == response.status_code + assert HTTPStatus.NO_CONTENT == response.status_code assert b'' == response.data repository_remove_mock.assert_called_once_with(str(valid_id)) @@ -183,11 +182,12 @@ def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskCli response = client.delete("/projects/%s" % invalid_id, follow_redirects=True) - assert 404 == response.status_code + assert HTTPStatus.NOT_FOUND == response.status_code repository_remove_mock.assert_called_once_with(str(invalid_id)) -def test_delete_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import UnprocessableEntity @@ -199,5 +199,5 @@ def test_delete_project_should_return_422_for_invalid_id_format(client: FlaskCli response = client.delete("/projects/%s" % invalid_id, follow_redirects=True) - assert 422 == response.status_code + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_remove_mock.assert_called_once_with(str(invalid_id)) diff --git a/tests/smoke_test.py b/tests/smoke_test.py index de74bab8..a6cc7417 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -1,2 +1,28 @@ +import pytest +import sqlalchemy +from flask.testing import FlaskClient +from flask_restplus._http import HTTPStatus +from pytest_mock import MockFixture + + def test_app_exists(app): assert app is not None + + +unexpected_errors_to_be_handled = [sqlalchemy.exc.IntegrityError] + + +@pytest.mark.parametrize("error_type", unexpected_errors_to_be_handled) +def test_exceptions_are_handled(error_type, client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_get_all_all_mock = mocker.patch.object(time_entries_dao, + "get_all", + side_effect=error_type) + + response = client.get('/time-entries', follow_redirects=True) + + assert repository_get_all_all_mock.assert_not_called() + assert HTTPStatus.INTERNAL_SERVER_ERROR != response.status_code + + + diff --git a/tests/time_entries/__init__.py b/tests/time_entries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/time_entries/time_entries_namespace_test.py b/tests/time_entries/time_entries_namespace_test.py new file mode 100644 index 00000000..7d1f931e --- /dev/null +++ b/tests/time_entries/time_entries_namespace_test.py @@ -0,0 +1,247 @@ +from faker import Faker +from flask import json +from flask.testing import FlaskClient +from flask_restplus._http import HTTPStatus +from pytest_mock import MockFixture + +fake = Faker() + +valid_time_entry_input = { + "project_id": fake.random_int(1, 9999), + "activity_id": fake.random_int(1, 9999), + "technologies": fake.words(3, ['java', 'javascript', 'python', 'azure'], unique=True), + "description": fake.paragraph(nb_sentences=2), + "start_date": fake.iso8601(end_datetime=None), + "end_date": fake.iso8601(end_datetime=None), +} +fake_time_entry = ({ + "id": fake.random_int(1, 9999), + "running": True, +}).update(valid_time_entry_input) + + +def test_create_time_entry_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_create_mock = mocker.patch.object(time_entries_dao.repository, + 'create', + return_value=fake_time_entry) + + response = client.post("/time-entries", json=valid_time_entry_input, follow_redirects=True) + + assert HTTPStatus.CREATED == response.status_code + repository_create_mock.assert_called_once_with(valid_time_entry_input) + + +def test_create_time_entry_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + invalid_time_entry_input = valid_time_entry_input.copy().update({ + "project_id": None, + }) + repository_create_mock = mocker.patch.object(time_entries_dao.repository, + 'create', + return_value=fake_time_entry) + + response = client.post("/time-entries", json=invalid_time_entry_input, follow_redirects=True) + + assert HTTPStatus.BAD_REQUEST == response.status_code + repository_create_mock.assert_not_called() + + +def test_list_all_time_entries(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_find_all_mock = mocker.patch.object(time_entries_dao.repository, + 'find_all', + return_value=[]) + + response = client.get("/time-entries", follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + assert [] == json.loads(response.data) + repository_find_all_mock.assert_called_once() + + +def test_get_time_entry_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + valid_id = fake.random_int(1, 9999) + repository_find_mock = mocker.patch.object(time_entries_dao.repository, + 'find', + return_value=fake_time_entry) + + response = client.get("/time-entries/%s" % valid_id, follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + fake_time_entry == json.loads(response.data) + repository_find_mock.assert_called_once_with(str(valid_id)) + + +def test_get_time_entry_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import UnprocessableEntity + + invalid_id = fake.word() + + repository_find_mock = mocker.patch.object(time_entries_dao.repository, + 'find', + side_effect=UnprocessableEntity) + + response = client.get("/time-entries/%s" % invalid_id, follow_redirects=True) + + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code + repository_find_mock.assert_called_once_with(str(invalid_id)) + + +def test_update_time_entry_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + return_value=fake_time_entry) + + valid_id = fake.random_int(1, 9999) + response = client.put("/time-entries/%s" % valid_id, + json=valid_time_entry_input, + follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + fake_time_entry == json.loads(response.data) + repository_update_mock.assert_called_once_with(str(valid_id), valid_time_entry_input) + + +def test_update_time_entry_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + invalid_time_entry_data = valid_time_entry_input.copy().update({ + "project_id": 'anything', + }) + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + return_value=fake_time_entry) + valid_id = fake.random_int(1, 9999) + + response = client.put("/time-entries/%s" % valid_id, + json=invalid_time_entry_data, + follow_redirects=True) + + assert HTTPStatus.BAD_REQUEST == response.status_code + repository_update_mock.assert_not_called() + + +def test_update_time_entry_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import NotFound + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + side_effect=NotFound) + invalid_id = fake.random_int(1, 9999) + + response = client.put("/time-entries/%s" % invalid_id, + json=valid_time_entry_input, + follow_redirects=True) + + assert HTTPStatus.NOT_FOUND == response.status_code + repository_update_mock.assert_called_once_with(str(invalid_id), valid_time_entry_input) + + +def test_delete_time_entry_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_remove_mock = mocker.patch.object(time_entries_dao.repository, + 'remove', + return_value=None) + valid_id = fake.random_int(1, 9999) + + response = client.delete("/time-entries/%s" % valid_id, follow_redirects=True) + + assert HTTPStatus.NO_CONTENT == response.status_code + assert b'' == response.data + repository_remove_mock.assert_called_once_with(str(valid_id)) + + +def test_delete_time_entry_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import NotFound + repository_remove_mock = mocker.patch.object(time_entries_dao.repository, + 'remove', + side_effect=NotFound) + invalid_id = fake.random_int(1, 9999) + + response = client.delete("/time-entries/%s" % invalid_id, follow_redirects=True) + + assert HTTPStatus.NOT_FOUND == response.status_code + repository_remove_mock.assert_called_once_with(str(invalid_id)) + + +def test_delete_time_entry_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import UnprocessableEntity + repository_remove_mock = mocker.patch.object(time_entries_dao.repository, + 'remove', + side_effect=UnprocessableEntity) + invalid_id = fake.word() + + response = client.delete("/time-entries/%s" % invalid_id, follow_redirects=True) + + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code + repository_remove_mock.assert_called_once_with(str(invalid_id)) + + +def test_stop_time_entry_with_valid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + return_value=fake_time_entry) + valid_id = fake.random_int(1, 9999) + + response = client.post("/time-entries/%s/stop" % valid_id, follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + repository_update_mock.assert_called_once_with(str(valid_id), { + "end_date": mocker.ANY + }) + + +def test_stop_time_entry_with_invalid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import UnprocessableEntity + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + side_effect=UnprocessableEntity) + invalid_id = fake.word() + + response = client.post("/time-entries/%s/stop" % invalid_id, follow_redirects=True) + + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code + repository_update_mock.assert_called_once_with(invalid_id, { + "end_date": mocker.ANY + }) + + +def test_restart_time_entry_with_valid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + return_value=fake_time_entry) + valid_id = fake.random_int(1, 9999) + + response = client.post("/time-entries/%s/restart" % valid_id, follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + repository_update_mock.assert_called_once_with(str(valid_id), { + "end_date": None + }) + + +def test_restart_time_entry_with_invalid_id(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + from werkzeug.exceptions import UnprocessableEntity + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'update', + side_effect=UnprocessableEntity) + invalid_id = fake.word() + + response = client.post("/time-entries/%s/restart" % invalid_id, follow_redirects=True) + + assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code + repository_update_mock.assert_called_once_with(invalid_id, { + "end_date": None + }) diff --git a/time_tracker_api/api.py b/time_tracker_api/api.py index a81a3c9b..d0d65a6d 100644 --- a/time_tracker_api/api.py +++ b/time_tracker_api/api.py @@ -4,8 +4,7 @@ from faker import Faker from flask import current_app as app from flask_restplus import Api, fields -from flask_restplus.fields import Raw, to_marshallable_type, MarshallingError -from werkzeug.exceptions import Conflict +from flask_restplus._http import HTTPStatus faker = Faker() @@ -65,24 +64,24 @@ def handle_db_integrity_error(e): """Handles errors related to data consistency""" if e.code == 'gkpj': - return {'message': 'It already exists or references data that does not exist.'}, 409 + return {'message': 'It already exists or references data that does not exist.'}, HTTPStatus.CONFLICT else: - return {'message': 'Data integrity issues.'}, 409 + return {'message': 'Data integrity issues.'}, HTTPStatus.CONFLICT @api.errorhandler(sqlalchemy.exc.DataError) def handle_invalid_data_error(e): """Return a 422 because the user entered data of an invalid type""" - return {'message': 'The processed data was invalid. Please correct it.'}, 422 + return {'message': 'The processed data was invalid. Please correct it.'}, HTTPStatus.UNPROCESSABLE_ENTITY @api.errorhandler(pyodbc.OperationalError) def handle_connection_error(e): """Return a 500 due to a issue in the connection to a 3rd party service""" - return {'message': 'Connection issues. Please try again in a few minutes.'}, 500 + return {'message': 'Connection issues. Please try again in a few minutes.'}, HTTPStatus.INTERNAL_SERVER_ERROR @api.errorhandler def generic_exception_handler(e): app.logger.error(e) - return {'message': 'An unhandled exception occurred.'}, 500 + return {'message': 'An unhandled exception occurred.'}, HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/time_tracker_api/config.py b/time_tracker_api/config.py index fbc3e265..ab947500 100644 --- a/time_tracker_api/config.py +++ b/time_tracker_api/config.py @@ -8,6 +8,7 @@ class Config: DATABASE_URI = os.environ.get('DATABASE_URI') PROPAGATE_EXCEPTIONS = True RESTPLUS_VALIDATE = True + DEBUG = True class DevelopmentConfig(Config): @@ -26,11 +27,13 @@ class TestConfig(SQLConfig): TESTING = True FLASK_DEBUG = True TEST_TABLE = 'tests' - DATABASE_URI = os.environ.get('DATABASE_URI', 'sqlite:///tests.db') + DATABASE_URI = os.environ.get('DATABASE_URI', 'sqlite:///:memory:') SQLALCHEMY_DATABASE_URI = DATABASE_URI class ProductionConfig(Config): + DEBUG = False + FLASK_DEBUG = True FLASK_ENV = 'production' diff --git a/time_tracker_api/database.py b/time_tracker_api/database.py index 513eef0f..39fa155d 100644 --- a/time_tracker_api/database.py +++ b/time_tracker_api/database.py @@ -9,40 +9,43 @@ from flask import Flask +COMMENTS_MAX_LENGTH = 500 +ID_MAX_LENGTH = 64 + class CRUDDao(abc.ABC): @abc.abstractmethod def get_all(self): - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abc.abstractmethod def get(self, id): - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abc.abstractmethod def create(self, project): - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abc.abstractmethod def update(self, id, data): - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abc.abstractmethod def delete(self, id): - raise NotImplementedError + raise NotImplementedError # pragma: no cover class Seeder(abc.ABC): @abc.abstractmethod def run(self): - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abc.abstractmethod def fresh(self): - raise NotImplementedError + raise NotImplementedError # pragma: no cover def __call__(self, *args, **kwargs): - self.run() + self.run() # pragma: no cover class DatabaseModel: diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index f21db855..c49b42b4 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -18,13 +18,14 @@ class ProjectDao(CRUDDao): def create_dao() -> ProjectDao: from time_tracker_api.sql_repository import db + from time_tracker_api.database import COMMENTS_MAX_LENGTH from time_tracker_api.sql_repository import SQLCRUDDao, AuditedSQLModel, SQLModel class ProjectSQLModel(db.Model, SQLModel, AuditedSQLModel): __tablename__ = 'project' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) - description = db.Column(db.String(250), unique=False, nullable=False) + description = db.Column(db.String(COMMENTS_MAX_LENGTH), unique=False, nullable=False) type = db.Column(db.String(10), nullable=False) active = db.Column(db.Boolean, default=True) diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index 47162279..51c20e51 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -76,7 +76,7 @@ def get(self): @ns.marshal_with(project, code=HTTPStatus.CREATED) def post(self): """Create a project""" - return project_dao.create(ns.payload) + return project_dao.create(ns.payload), HTTPStatus.CREATED @ns.route('/') @@ -92,6 +92,8 @@ def get(self, id): return project_dao.get(id) @ns.doc('update_project') + @ns.response(HTTPStatus.BAD_REQUEST, 'Invalid format or structure ' + 'of the attributes of the project') @ns.response(HTTPStatus.CONFLICT, 'A project already exists with this new data') @ns.expect(project_input) @ns.marshal_with(project) diff --git a/time_tracker_api/sql_repository.py b/time_tracker_api/sql_repository.py index eb29041b..6869c7d1 100644 --- a/time_tracker_api/sql_repository.py +++ b/time_tracker_api/sql_repository.py @@ -3,12 +3,9 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy -from time_tracker_api.database import CRUDDao, Seeder, DatabaseModel +from time_tracker_api.database import CRUDDao, Seeder, DatabaseModel, ID_MAX_LENGTH from time_tracker_api.security import current_user_id -LIST_SEPARATOR_CHAR = ";" - - db: SQLAlchemy = None SQLModel = None AuditedSQLModel = None @@ -37,8 +34,8 @@ def init_app(app: Flask) -> None: class AuditedSQLModelClass(): created_at = db.Column(db.DateTime, server_default=db.func.now()) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) - created_by = db.Column(db.String, default=current_user_id) - updated_by = db.Column(db.String, onupdate=current_user_id) + created_by = db.Column(db.String(ID_MAX_LENGTH), default=current_user_id) + updated_by = db.Column(db.String(ID_MAX_LENGTH), onupdate=current_user_id) AuditedSQLModel = AuditedSQLModelClass diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index c866d635..ea3d427f 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -2,8 +2,6 @@ from time_tracker_api.database import CRUDDao -COMMENTS_MAX_NUMBER_CHARS = 500 - class TimeEntriesDao(CRUDDao): pass @@ -11,12 +9,13 @@ class TimeEntriesDao(CRUDDao): def create_dao() -> TimeEntriesDao: from time_tracker_api.sql_repository import db + from time_tracker_api.database import COMMENTS_MAX_LENGTH from time_tracker_api.sql_repository import SQLCRUDDao, AuditedSQLModel, SQLModel class TimeEntrySQLModel(db.Model, SQLModel, AuditedSQLModel): __tablename__ = 'time_entry' id = db.Column(db.Integer, primary_key=True) - description = db.Column(db.String(COMMENTS_MAX_NUMBER_CHARS)) + description = db.Column(db.String(COMMENTS_MAX_LENGTH)) start_date = db.Column(db.DateTime, server_default=db.func.now()) end_date = db.Column(db.DateTime) project_id = db.Column(db.Integer, @@ -25,25 +24,11 @@ class TimeEntrySQLModel(db.Model, SQLModel, AuditedSQLModel): activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False) - technologies = db.Column(ScalarListType()) @property def running(self): - return self.end_date == None - - # @property - # def technologies(self): - # return [str(x) for x in self._technologies.split(';')] - # - # @technologies.setter - # def technologies(self, value): - # if value is Iterable: - # self._technologies = LIST_SEPARATOR_CHAR.join(value) - # elif type(value) == str: - # self._technologies = value - # else: - # raise UnprocessableEntity + return self.end_date is None def __repr__(self): return '