diff --git a/commons/data_access_layer/cosmos_db.py b/commons/data_access_layer/cosmos_db.py index 4778d5d2..f8f6c391 100644 --- a/commons/data_access_layer/cosmos_db.py +++ b/commons/data_access_layer/cosmos_db.py @@ -139,7 +139,8 @@ def create(self, data: dict, mapper: Callable = None): function_mapper = self.get_mapper_or_dict(mapper) return function_mapper(self.container.create_item(body=data)) - def find(self, id: str, partition_key_value, peeker: 'function' = None, visible_only=True, mapper: Callable = None): + def find(self, id: str, partition_key_value, peeker: 'function' = None, + visible_only=True, mapper: Callable = None): found_item = self.container.read_item(id, partition_key_value) if peeker: peeker(found_item) @@ -201,7 +202,7 @@ def get_page_size_or(self, custom_page_size: int) -> int: def on_create(self, new_item_data: dict): if new_item_data.get('id') is None: - new_item_data['id'] = str(uuid.uuid4()) + new_item_data['id'] = generate_uuid4() def on_update(self, update_item_data: dict): pass @@ -245,17 +246,25 @@ def __init__(self, status_code: int, description: str = None): self.description = description -def current_datetime(): +def current_datetime() -> datetime: return datetime.utcnow() -def datetime_str(value: datetime): +def datetime_str(value: datetime) -> str: if value is not None: return value.isoformat() else: return None +def current_datetime_str() -> str: + return datetime_str(current_datetime()) + + +def generate_uuid4() -> str: + return str(uuid.uuid4()) + + def init_app(app: Flask) -> None: global cosmos_helper cosmos_helper = CosmosDBFacade.from_flask_config(app) diff --git a/tests/conftest.py b/tests/conftest.py index 97b80b65..f42686eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,7 +129,13 @@ def another_item(cosmos_db_repository: CosmosDBRepository, tenant_id: str) -> di @pytest.fixture(scope="module") -def time_entry_repository() -> TimeEntryCosmosDBRepository: +def time_entry_repository(app: Flask) -> TimeEntryCosmosDBRepository: + with app.app_context(): + from commons.data_access_layer.cosmos_db import init_app, cosmos_helper + + if cosmos_helper is None: + init_app(app) + return TimeEntryCosmosDBRepository() @@ -139,12 +145,11 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, tenant_id: str): created_time_entry = time_entry_repository.create({ "project_id": fake.uuid4(), - "start_date": datetime_str(current_datetime()), "owner_id": owner_id, "tenant_id": tenant_id }) yield created_time_entry - time_entry_repository.delete(id=created_time_entry.id, + time_entry_repository.delete_permanently(id=created_time_entry.id, partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/time_entries/time_entries_model_test.py b/tests/time_tracker_api/time_entries/time_entries_model_test.py index 2b58c036..43bd998c 100644 --- a/tests/time_tracker_api/time_entries/time_entries_model_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_model_test.py @@ -48,7 +48,7 @@ def test_find_interception_with_date_range_should_find(start_date: datetime, partition_key_value=tenant_id) assert result is not None - assert len(result) >= 0 + assert len(result) > 0 assert any([existing_item.id == item.id for item in result]) finally: time_entry_repository.delete_permanently(existing_item.id, partition_key_value=existing_item.tenant_id) diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index f2646edc..1b63202b 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -5,8 +5,8 @@ from azure.cosmos import PartitionKey from flask_restplus._http import HTTPStatus -from commons.data_access_layer.cosmos_db import CosmosDBDao, CosmosDBRepository, CustomError, CosmosDBModel, \ - current_datetime, datetime_str +from commons.data_access_layer.cosmos_db import CosmosDBDao, CosmosDBRepository, CustomError, current_datetime_str, \ + CosmosDBModel from commons.data_access_layer.database import CRUDDao from time_tracker_api.security import current_user_id @@ -34,15 +34,15 @@ def find_running(self): @dataclass() class TimeEntryCosmosDBModel(CosmosDBModel): - id: str - description: str project_id: str - activity_id: str + start_date: str owner_id: str + id: str tenant_id: str + description: str = field(default=None) + activity_id: str = field(default=None) uri: str = field(default=None) technologies: List[str] = field(default_factory=list) - start_date: str = field(default_factory=current_datetime) end_date: str = field(default=None) deleted: str = field(default=None) @@ -76,6 +76,10 @@ def create_sql_ignore_id_condition(id: str): def on_create(self, new_item_data: dict): CosmosDBRepository.on_create(self, new_item_data) + + if new_item_data.get("start_date") is None: + new_item_data['start_date'] = current_datetime_str() + self.validate_data(new_item_data) def on_update(self, updated_item_data: dict): @@ -88,7 +92,7 @@ def find_interception_with_date_range(self, start_date, end_date, owner_id, part params = self.append_conditions_values([ {"name": "@partition_key_value", "value": partition_key_value}, {"name": "@start_date", "value": start_date}, - {"name": "@end_date", "value": end_date or datetime_str(current_datetime())}, + {"name": "@end_date", "value": end_date or current_datetime_str()}, {"name": "@ignore_id", "value": ignore_id}, ], conditions) result = self.container.query_items( @@ -121,15 +125,17 @@ def find_running(self, partition_key_value: str, mapper: Callable = None): return function_mapper(next(result)) def validate_data(self, data): + start_date = data.get('start_date') + if data.get('end_date') is not None: - if data['end_date'] <= data.get('start_date'): + if data['end_date'] <= start_date: raise CustomError(HTTPStatus.BAD_REQUEST, description="You must end the time entry after it started") - if data['end_date'] >= datetime_str(current_datetime()): + if data['end_date'] >= current_datetime_str(): raise CustomError(HTTPStatus.BAD_REQUEST, description="You cannot end a time entry in the future") - collision = self.find_interception_with_date_range(start_date=data.get('start_date'), + collision = self.find_interception_with_date_range(start_date=start_date, end_date=data.get('end_date'), owner_id=data.get('owner_id'), partition_key_value=data.get('tenant_id'), diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index a7bc2a42..e107729b 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -4,7 +4,7 @@ from flask_restplus import fields, Resource, Namespace from flask_restplus._http import HTTPStatus -from commons.data_access_layer.cosmos_db import current_datetime, datetime_str +from commons.data_access_layer.cosmos_db import current_datetime, datetime_str, current_datetime_str from commons.data_access_layer.database import COMMENTS_MAX_LENGTH from time_tracker_api.api import common_fields, UUID_REGEX from time_tracker_api.time_entries.time_entries_model import create_dao @@ -25,7 +25,7 @@ 'start_date': fields.DateTime( dt_format='iso8601', title='Start date', - required=True, + required=False, description='When the user started doing this activity', example=datetime_str(current_datetime() - timedelta(days=1)), ), @@ -48,7 +48,7 @@ title='End date', required=False, description='When the user ended doing this activity', - example=datetime_str(current_datetime()), + example=current_datetime_str(), ), 'uri': fields.String( title='Uniform Resource identifier', @@ -165,7 +165,7 @@ class StopTimeEntry(Resource): def post(self, id): """Stop a running time entry""" return time_entries_dao.update(id, { - 'end_date': datetime_str(current_datetime()) + 'end_date': current_datetime_str() })