diff --git a/tests/conftest.py b/tests/conftest.py index 1f8afe71..5cb5c18d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,6 +181,15 @@ def time_entry_repository(app: Flask) -> TimeEntryCosmosDBRepository: return TimeEntryCosmosDBRepository() +@pytest.fixture +def time_entries_dao(): + from time_tracker_api.time_entries.time_entries_namespace import ( + time_entries_dao, + ) + + return time_entries_dao + + @pytest.yield_fixture(scope="module") def running_time_entry( time_entry_repository: TimeEntryCosmosDBRepository, diff --git a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py index 214f130b..cb1417dd 100644 --- a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py @@ -639,3 +639,45 @@ def test_summary_is_called_with_date_range_from_worked_time_module( repository_find_all_mock.assert_called_once_with( ANY, conditions=conditions, date_range=date_range ) + + +def test_paginated_fails_with_no_params( + client: FlaskClient, valid_header: dict, +): + response = client.get('/time-entries/paginated', headers=valid_header) + assert HTTPStatus.BAD_REQUEST == response.status_code + + +def test_paginated_succeeds_with_valid_params( + client: FlaskClient, valid_header: dict, +): + response = client.get( + '/time-entries/paginated?start=10&length=10', headers=valid_header + ) + assert HTTPStatus.OK == response.status_code + + +def test_paginated_response_contains_expected_props( + client: FlaskClient, valid_header: dict, +): + response = client.get( + '/time-entries/paginated?start=10&length=10', headers=valid_header + ) + assert 'data' in json.loads(response.data) + assert 'records_total' in json.loads(response.data) + + +def test_paginated_sends_max_count_and_offset_on_call_to_repository( + client: FlaskClient, valid_header: dict, time_entries_dao +): + time_entries_dao.repository.find_all = Mock(return_value=[]) + + response = client.get( + '/time-entries/paginated?start=10&length=10', headers=valid_header + ) + + time_entries_dao.repository.find_all.assert_called_once() + + args, kwargs = time_entries_dao.repository.find_all.call_args + assert 'max_count' in kwargs and kwargs['max_count'] is not None + assert 'offset' in kwargs and kwargs['offset'] is not None diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 62cba129..ff40c7c8 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -77,7 +77,9 @@ def get_all(self, conditions: dict = None, **kwargs) -> list: """ event_ctx = self.create_event_context("read-many") customer_dao = customers_create_dao() - customers = customer_dao.get_all() + customers = customer_dao.get_all( + max_count=kwargs.get('max_count', None) + ) customers_id = [customer.id for customer in customers] conditions = conditions if conditions else {} diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index ff04afa8..7b76b2ab 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -19,7 +19,7 @@ from time_tracker_api.activities import activities_model from utils.extend_model import ( - add_project_name_to_time_entries, + add_project_info_to_time_entries, add_activity_name_to_time_entries, create_in_condition, create_custom_query_from_str, @@ -211,6 +211,7 @@ def find_all( custom_sql_conditions=custom_sql_conditions, custom_params=custom_params, max_count=kwargs.get("max_count", None), + offset=kwargs.get("offset", 0), ) if time_entries: @@ -221,14 +222,18 @@ def find_all( project_dao = projects_model.create_dao() projects = project_dao.get_all( - custom_sql_conditions=[custom_conditions], visible_only=False + custom_sql_conditions=[custom_conditions], + visible_only=False, + max_count=kwargs.get("max_count", None), ) - add_project_name_to_time_entries(time_entries, projects) + + add_project_info_to_time_entries(time_entries, projects) activity_dao = activities_model.create_dao() activities = activity_dao.get_all( custom_sql_conditions=[custom_conditions_activity], visible_only=False, + max_count=kwargs.get("max_count", None), ) add_activity_name_to_time_entries(time_entries, activities) @@ -397,12 +402,10 @@ def stop_time_entry_if_was_left_running( ) raise CosmosResourceNotFoundError() - def get_all(self, conditions: dict = None, **kwargs) -> list: - event_ctx = self.create_event_context("read-many") - conditions.update({"owner_id": event_ctx.user_id}) + def build_custom_query(self, is_admin: bool, conditions: dict = None): custom_query = [] if "user_id" in conditions: - if event_ctx.is_admin: + if is_admin: conditions.pop("owner_id") custom_query = ( [] @@ -418,6 +421,16 @@ def get_all(self, conditions: dict = None, **kwargs) -> list: abort( HTTPStatus.FORBIDDEN, "You don't have enough permissions." ) + return custom_query + + def get_all(self, conditions: dict = None, **kwargs) -> list: + event_ctx = self.create_event_context("read-many") + conditions.update({"owner_id": event_ctx.user_id}) + + custom_query = self.build_custom_query( + is_admin=event_ctx.is_admin, conditions=conditions, + ) + date_range = self.handle_date_filter_args(args=conditions) limit = conditions.get("limit", None) conditions.pop("limit", None) @@ -429,6 +442,36 @@ def get_all(self, conditions: dict = None, **kwargs) -> list: max_count=limit, ) + def get_all_paginated(self, conditions: dict = None, **kwargs) -> list: + event_ctx = self.create_event_context("read-many") + conditions.update({"owner_id": event_ctx.user_id}) + + custom_query = self.build_custom_query( + is_admin=event_ctx.is_admin, conditions=conditions, + ) + + date_range = self.handle_date_filter_args(args=conditions) + + length = conditions.get("length", None) + conditions.pop("length", None) + + start = conditions.get("start", None) + conditions.pop("start", None) + + time_entries = self.repository.find_all( + event_ctx, + conditions=conditions, + custom_sql_conditions=custom_query, + date_range=date_range, + max_count=length, + offset=start, + ) + + return { + 'records_total': len(time_entries), + 'data': time_entries, + } + def get(self, id): event_ctx = self.create_event_context("read") diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index c29b2f2e..bbd58c1c 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -138,6 +138,20 @@ description='Email of the user that owns the time-entry', example=faker.email(), ), + 'customer_id': fields.String( + required=False, + title='Customer ID', + max_length=50, + description='Unique ID for the customer the entry belongs to', + example=faker.uuid4(), + ), + 'customer_name': fields.String( + required=False, + title='Customer Name', + max_length=50, + description='Name of the customer the entry belongs to', + example=faker.company(), + ), } time_entry_response_fields.update(common_fields) @@ -331,3 +345,44 @@ def get(self): """Find the summary of worked time""" conditions = summary_attribs_parser.parse_args() return time_entries_dao.get_worked_time(conditions) + + +time_entry_paginated = ns.model( + 'TimeEntryPaginated', + { + 'records_total': fields.Integer( + title='Records total', description='Total number of entries.', + ), + 'data': fields.List(fields.Nested(time_entry)), + }, +) + +paginated_attribs_parser = ns.parser() +paginated_attribs_parser.add_argument( + 'length', + required=True, + type=int, + help="(Filter) The number of rows the endpoint should return.", + location='args', +) + +paginated_attribs_parser.add_argument( + 'start', + required=True, + type=int, + help="(Filter) The number of rows to be removed from the query. (aka offset)", + location='args', +) + + +@ns.route('/paginated') +@ns.response(HTTPStatus.OK, 'Time Entries paginated') +@ns.response(HTTPStatus.NOT_FOUND, 'Time entry not found') +class PaginatedTimeEntry(Resource): + @ns.expect(paginated_attribs_parser) + @ns.doc('list_time_entries_paginated') + @ns.marshal_list_with(time_entry_paginated) + def get(self): + """List all time entries paginated""" + conditions = paginated_attribs_parser.parse_args() + return time_entries_dao.get_all_paginated(conditions) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 482e4a19..2f15b8cd 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.19.0' +__version__ = '0.20.0' diff --git a/utils/extend_model.py b/utils/extend_model.py index 54d25975..6eecedcf 100644 --- a/utils/extend_model.py +++ b/utils/extend_model.py @@ -16,9 +16,9 @@ def add_customer_name_to_projects(projects, customers): setattr(project, 'customer_name', customer.name) -def add_project_name_to_time_entries(time_entries, projects): +def add_project_info_to_time_entries(time_entries, projects): """ - Add attribute project_name in time-entry model, based on project_id of the + Add project info in time-entry model, based on project_id of the time_entry :param (list) time_entries: time_entries retrieved from time-entry repository :param (list) projects: projects retrieved from project repository @@ -30,6 +30,8 @@ def add_project_name_to_time_entries(time_entries, projects): if time_entry.project_id == project.id: name = project.name + " (archived)" if project.is_deleted() else project.name setattr(time_entry, 'project_name', name) + setattr(time_entry, 'customer_id', project.customer_id) + setattr(time_entry, 'customer_name', project.customer_name) def add_activity_name_to_time_entries(time_entries, activities):