diff --git a/.gitignore b/.gitignore index 9699da54..c4f6932f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ migration_status.csv env.* # SSL certificate for cosmos emulator -emulatorcert.crt \ No newline at end of file +emulatorcert.crt + +seed_database.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c121e2..87d384d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.36.3 (2021-07-14) +### Fix +* TT-274 fix error on archive a customer removes the project ([#307](https://github.com/ioet/time-tracker-backend/issues/307)) ([`4538307`](https://github.com/ioet/time-tracker-backend/commit/4538307407d8f482b1419db6889b6ecc3013950e)) + ## v0.36.2 (2021-07-09) ### Fix * TT-287 Fix users email doesn't show in reports page ([#305](https://github.com/ioet/time-tracker-backend/issues/305)) ([`fb222c1`](https://github.com/ioet/time-tracker-backend/commit/fb222c1a5f13b8313a257159f3115cf9c0fae750)) diff --git a/tests/time_tracker_api/projects/projects_model_test.py b/tests/time_tracker_api/projects/projects_model_test.py index 8579ab30..c6b63c40 100644 --- a/tests/time_tracker_api/projects/projects_model_test.py +++ b/tests/time_tracker_api/projects/projects_model_test.py @@ -5,11 +5,18 @@ CustomerCosmosDBModel, CustomerCosmosDBDao, ) +from time_tracker_api.project_types.project_types_model import ( + ProjectTypeCosmosDBModel, + ProjectTypeCosmosDBDao, +) from time_tracker_api.projects.projects_model import ( ProjectCosmosDBRepository, ProjectCosmosDBModel, create_dao, ) +from faker import Faker + +fake = Faker() @patch( @@ -77,3 +84,57 @@ def test_get_project_with_their_customer( assert isinstance(project, ProjectCosmosDBModel) assert project.__dict__['customer_name'] == customer_data['name'] + + +def test_get_all_projects_with_customers( + mocker, +): + customer_id = fake.uuid4() + project_type_id = fake.uuid4() + + customer_data = { + 'id': customer_id, + 'name': fake.company(), + 'description': fake.paragraph(), + 'tenant_id': fake.uuid4(), + } + + project_data = { + 'customer_id': customer_id, + 'id': fake.uuid4(), + 'name': fake.company(), + 'description': fake.paragraph(), + 'project_type_id': project_type_id, + 'tenant_id': fake.uuid4(), + } + + project_type_dao = { + 'id': project_type_id, + 'name': fake.name(), + 'description': fake.paragraph(), + 'tenant_id': fake.uuid4(), + } + + expected_customer = CustomerCosmosDBModel(customer_data) + expected_project = ProjectCosmosDBModel(project_data) + expected_project_type = ProjectTypeCosmosDBModel(project_type_dao) + + customer_dao_get_all_mock = mocker.patch.object( + CustomerCosmosDBDao, 'get_all' + ) + customer_dao_get_all_mock.return_value = [expected_customer] + + projects_repository_find_all_mock = mocker.patch.object( + ProjectCosmosDBRepository, 'find_all' + ) + projects_repository_find_all_mock.return_value = [expected_project] + + project_type_dao_get_all_mock = mocker.patch.object( + ProjectTypeCosmosDBDao, 'get_all' + ) + project_type_dao_get_all_mock.return_value = [expected_project_type] + projects = create_dao().get_all() + + assert isinstance(projects[0], ProjectCosmosDBModel) + assert projects[0].__dict__['customer_name'] == customer_data['name'] + assert len(projects) == 1 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 fd04166e..6ea0d88b 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 @@ -11,221 +11,55 @@ ) -def create_time_entry( - start_date: str, - end_date: str, - owner_id: str, - tenant_id: str, - mocker, - event_context: EventContext, - time_entry_repository: TimeEntryCosmosDBRepository, -) -> TimeEntryCosmosDBModel: - data = { +def test_find_interception_with_date_range_should_return_true_if_there_are_collisions(): + owner_id = Faker().uuid4() + tenant_id = Faker().uuid4() + entry_start_date = "2020-10-01T05:00:00.000Z" + entry_end_date = "2020-10-01T10:00:00.000Z" + + collision_entry = { "project_id": Faker().uuid4(), "activity_id": Faker().uuid4(), "description": Faker().paragraph(nb_sentences=2), - "start_date": start_date, - "end_date": end_date, - "owner_id": owner_id, + "start_date": entry_start_date, + "end_date": entry_end_date, + "owner_id": Faker().uuid4(), "tenant_id": tenant_id, } + time_entry_repository = TimeEntryCosmosDBRepository() + query_items_mock = Mock(return_value=[collision_entry]) + time_entry_repository.container = Mock() + time_entry_repository.container.query_items = query_items_mock - mocker.patch( - 'time_tracker_api.time_entries.time_entries_repository.are_related_entry_entities_valid', - return_value={ - "is_valid": True, - "status_code": HTTPStatus.OK, - "message": "Related entry entities valid", - }, - ) - - created_item = time_entry_repository.create( - data, event_context, mapper=TimeEntryCosmosDBModel - ) - return created_item - - -@pytest.mark.parametrize( - 'start_date,end_date,start_date_,end_date_', - [ - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T07:00:00.000Z", - "2020-10-01T12:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T02:00:00.000Z", - "2020-10-01T07:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T02:00:00.000Z", - "2020-10-01T12:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T06:00:00.000Z", - "2020-10-01T07:00:00.000Z", - ), - ], -) -def test_find_interception_with_date_range_should_find( - start_date: str, - end_date: str, - start_date_: str, - end_date_: str, - owner_id: str, - tenant_id: str, - mocker, - time_entry_repository: TimeEntryCosmosDBRepository, - event_context: EventContext, -): - existing_item = create_time_entry( - start_date, - end_date, - owner_id, - tenant_id, - mocker, - event_context, - time_entry_repository, - ) - - try: - result = time_entry_repository.find_interception_with_date_range( - start_date_, end_date_, owner_id, tenant_id - ) - - assert result is not None - assert len(result) > 0 - assert any([existing_item.id == item.id for item in result]) - finally: - time_entry_repository.delete_permanently( - existing_item.id, event_context + exist_collision_entries = ( + time_entry_repository.find_interception_with_date_range( + start_date=entry_start_date, + end_date=entry_end_date, + owner_id=owner_id, + tenant_id=tenant_id, ) - - -@pytest.mark.parametrize( - 'start_date,end_date,start_date_,end_date_', - [ - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T15:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T12:00:00.000Z", - "2020-10-01T15:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T02:00:00.000Z", - "2020-10-01T05:00:00.000Z", - ), - ( - "2020-10-01T05:00:00.000Z", - "2020-10-01T10:00:00.000Z", - "2020-10-01T02:00:00.000Z", - "2020-10-01T04:00:00.000Z", - ), - ], -) -def test_find_interception_with_date_range_should_not_find( - start_date: str, - end_date: str, - start_date_: str, - end_date_: str, - owner_id: str, - tenant_id: str, - time_entry_repository: TimeEntryCosmosDBRepository, - event_context: EventContext, - mocker, -): - existing_item = create_time_entry( - start_date, - end_date, - owner_id, - tenant_id, - mocker, - event_context, - time_entry_repository, ) + assert exist_collision_entries is True - try: - result = time_entry_repository.find_interception_with_date_range( - start_date_, end_date_, owner_id, tenant_id - ) - - assert result == [] - assert len(result) == 0 - assert not any([existing_item.id == item.id for item in result]) - finally: - time_entry_repository.delete_permanently( - existing_item.id, event_context - ) - - -def test_find_interception_should_ignore_id_of_existing_item( - owner_id: str, - tenant_id: str, - time_entry_repository: TimeEntryCosmosDBRepository, - event_context: EventContext, - mocker, -): - start_date = "2020-10-01T05:00:00.000Z" - end_date = "2020-10-01T10:00:00.000Z" - - existing_item = create_time_entry( - start_date, - end_date, - owner_id, - tenant_id, - mocker, - event_context, - time_entry_repository, - ) - try: - colliding_result = ( - time_entry_repository.find_interception_with_date_range( - start_date, end_date, owner_id, tenant_id - ) - ) +def test_find_interception_with_date_range_should_return_false_if_there_are_not_collisions(): + entry_start_date = "2020-10-01T05:00:00.000Z" + entry_end_date = "2020-10-01T10:00:00.000Z" - non_colliding_result = ( - time_entry_repository.find_interception_with_date_range( - start_date, - end_date, - owner_id, - tenant_id, - ignore_id=existing_item.id, - ) - ) + time_entry_repository = TimeEntryCosmosDBRepository() + query_items_mock = Mock(return_value=[]) + time_entry_repository.container = Mock() + time_entry_repository.container.query_items = query_items_mock - assert colliding_result is not None - assert any([existing_item.id == item.id for item in colliding_result]) - assert non_colliding_result is not None - assert not any( - [existing_item.id == item.id for item in non_colliding_result] - ) - finally: - time_entry_repository.delete_permanently( - existing_item.id, event_context + exist_collision_entries = ( + time_entry_repository.find_interception_with_date_range( + start_date=entry_start_date, + end_date=entry_end_date, + owner_id=Faker().uuid4(), + tenant_id=Faker().uuid4(), ) + ) + assert exist_collision_entries is False def test_find_running_should_return_running_time_entry( diff --git a/time_tracker_api/project_types/project_types_model.py b/time_tracker_api/project_types/project_types_model.py index 47f1eb13..9024e73e 100644 --- a/time_tracker_api/project_types/project_types_model.py +++ b/time_tracker_api/project_types/project_types_model.py @@ -2,7 +2,11 @@ from azure.cosmos import PartitionKey -from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBDao, CosmosDBRepository +from commons.data_access_layer.cosmos_db import ( + CosmosDBModel, + CosmosDBDao, + CosmosDBRepository, +) from time_tracker_api.database import CRUDDao, APICosmosDBDao @@ -17,7 +21,7 @@ class ProjectTypeDao(CRUDDao): 'uniqueKeys': [ {'paths': ['/name', '/customer_id', '/deleted']}, ] - } + }, } @@ -32,7 +36,9 @@ class ProjectTypeCosmosDBModel(CosmosDBModel): tenant_id: str def __init__(self, data): - super(ProjectTypeCosmosDBModel, self).__init__(data) # pragma: no cover + super(ProjectTypeCosmosDBModel, self).__init__( + data + ) # pragma: no cover def __repr__(self): return '' % self.name # pragma: no cover @@ -41,12 +47,13 @@ def __str___(self): return "the project type \"%s\"" % self.name # pragma: no cover -def create_dao() -> ProjectTypeDao: - repository = CosmosDBRepository.from_definition(container_definition, - mapper=ProjectTypeCosmosDBModel) +class ProjectTypeCosmosDBDao(APICosmosDBDao, ProjectTypeDao): + def __init__(self, repository): + CosmosDBDao.__init__(self, repository) - class ProjectTypeCosmosDBDao(APICosmosDBDao, ProjectTypeDao): - def __init__(self): - CosmosDBDao.__init__(self, repository) - return ProjectTypeCosmosDBDao() +def create_dao() -> ProjectTypeDao: + repository = CosmosDBRepository.from_definition( + container_definition, mapper=ProjectTypeCosmosDBModel + ) + return ProjectTypeCosmosDBDao(repository) diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 805d6130..0d767251 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -134,15 +134,17 @@ def get_all( """ event_ctx = self.create_event_context("read-many") customer_dao = customers_create_dao() + customer_status = kwargs.get('customer_status', None) + customer_conditions = ( + {'status': customer_status} if customer_status else None + ) + customers = customer_dao.get_all( - max_count=kwargs.get('max_count', None) + conditions=customer_conditions, + max_count=kwargs.get('max_count', None), ) - customers_id = [ - customer.id - for customer in customers - if customer.status == 'active' - ] + customers_id = [customer.id for customer in customers] conditions = conditions if conditions else {} diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index 82dbaf2b..7030f63e 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -70,39 +70,45 @@ }, ) -project_type_nested_field = ns.model('ProjectType', { - 'name': fields.String( - title='Name', - required=True, - max_length=50, - description='Name of the project type', - example=faker.random_element(["Customer", "Training", "Internal"]), - ), - 'description': NullableString( - title='Description', - required=False, - max_length=250, - description='Comments about the project type', - example=faker.paragraph(), - ) -}) +project_type_nested_field = ns.model( + 'ProjectType', + { + 'name': fields.String( + title='Name', + required=True, + max_length=50, + description='Name of the project type', + example=faker.random_element(["Customer", "Training", "Internal"]), + ), + 'description': NullableString( + title='Description', + required=False, + max_length=250, + description='Comments about the project type', + example=faker.paragraph(), + ), + }, +) -customer_nested_field = ns.model('Customer', { - 'name': fields.String( - title='Name', - required=True, - max_length=50, - description='Name of the customer', - example=faker.company(), - ), - 'description': NullableString( - title='Description', - required=False, - max_length=250, - description='Description about the customer', - example=faker.paragraph(), - ) -}) +customer_nested_field = ns.model( + 'Customer', + { + 'name': fields.String( + title='Name', + required=True, + max_length=50, + description='Name of the customer', + example=faker.company(), + ), + 'description': NullableString( + title='Description', + required=False, + max_length=250, + description='Description about the customer', + example=faker.paragraph(), + ), + }, +) project_response_fields = { # TODO: Remove this DEAD CODE @@ -135,7 +141,9 @@ class Projects(Resource): def get(self): """List all projects""" conditions = attributes_filter.parse_args() - return project_dao.get_all(conditions=conditions) + return project_dao.get_all( + conditions=conditions, customer_status='active' + ) @ns.doc('create_project') @ns.response(HTTPStatus.CONFLICT, 'This project already exists') diff --git a/time_tracker_api/time_entries/time_entries_repository.py b/time_tracker_api/time_entries/time_entries_repository.py index 5abed126..d773f2a9 100644 --- a/time_tracker_api/time_entries/time_entries_repository.py +++ b/time_tracker_api/time_entries/time_entries_repository.py @@ -265,7 +265,9 @@ def find_interception_with_date_range( ) function_mapper = self.get_mapper_or_dict(mapper) - return list(map(function_mapper, result)) + collision_entries = list(map(function_mapper, result)) + exist_collision_entries = len(collision_entries) > 0 + return exist_collision_entries def find_running( self, tenant_id: str, owner_id: str, mapper: Callable = None @@ -331,14 +333,15 @@ def validate_data(self, data, event_context: EventContext): description="You cannot end a time entry in the future", ) - collision = self.find_interception_with_date_range( + exist_collision_entries = self.find_interception_with_date_range( start_date=start_date, end_date=data.get('end_date'), owner_id=event_context.user_id, tenant_id=event_context.tenant_id, ignore_id=data.get('id'), ) - if len(collision) > 0: + + if exist_collision_entries: raise CustomError( HTTPStatus.UNPROCESSABLE_ENTITY, description="There is another time entry in that date range", diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 349a8f7b..9faa3d8f 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.36.2' +__version__ = '0.36.3'