From b1fd60216ef577b718d08acef6a55e06c1b8e83c Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 14 May 2020 10:38:43 -0500 Subject: [PATCH 001/300] fix(time-entries): :bug: add instantiation of project_dao --- time_tracker_api/time_entries/time_entries_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index be27d23a..dd13e609 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -1,7 +1,6 @@ import abc from dataclasses import dataclass, field from typing import List, Callable -from flask import jsonify from azure.cosmos import PartitionKey from flask_restplus._http import HTTPStatus @@ -150,6 +149,7 @@ def find_all( custom_params=custom_params, ) + project_dao = projects_model.create_dao() projects = project_dao.get_all() add_project_name_to_time_entries(time_entries, projects) return time_entries From 65adf5d5138a3e7f56685731a3ef8d3c26edbc79 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Thu, 14 May 2020 16:10:57 +0000 Subject: [PATCH 002/300] 0.12.1 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 2c7bffbf..f8d90954 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.12.0' +__version__ = '0.12.1' From f14734cc1f82d880fd9ba509641949e30f05c5b8 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 14 May 2020 11:15:30 -0500 Subject: [PATCH 003/300] refactor(type-hints): :recycle: add Callable as type hint of functions --- commons/data_access_layer/cosmos_db.py | 17 +++++++++++------ .../time_entries/time_entries_model.py | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/commons/data_access_layer/cosmos_db.py b/commons/data_access_layer/cosmos_db.py index b29e8e7c..cf6aa337 100644 --- a/commons/data_access_layer/cosmos_db.py +++ b/commons/data_access_layer/cosmos_db.py @@ -210,7 +210,7 @@ def find( self, id: str, event_context: EventContext, - peeker: 'function' = None, + peeker: Callable = None, visible_only=True, mapper: Callable = None, ): @@ -275,7 +275,7 @@ def partial_update( id: str, changes: dict, event_context: EventContext, - peeker: 'function' = None, + peeker: Callable = None, visible_only=True, mapper: Callable = None, ): @@ -305,7 +305,7 @@ def delete( self, id: str, event_context: EventContext, - peeker: 'function' = None, + peeker: Callable = None, mapper: Callable = None, ): return self.partial_update( @@ -348,7 +348,9 @@ def __init__(self, repository: CosmosDBRepository): def get_all(self, conditions: dict = None, **kwargs) -> list: conditions = conditions if conditions else {} event_ctx = self.create_event_context("read-many") - return self.repository.find_all(event_ctx, conditions=conditions, **kwargs) + return self.repository.find_all( + event_ctx, conditions=conditions, **kwargs + ) def get(self, id): event_ctx = self.create_event_context("read") @@ -406,6 +408,7 @@ def generate_uuid4() -> str: def get_last_day_of_month(year: int, month: int) -> int: from calendar import monthrange + return monthrange(year=year, month=month)[1] @@ -419,7 +422,9 @@ def get_current_month() -> int: def get_date_range_of_month(year: int, month: int) -> Dict[str, str]: first_day_of_month = 1 - start_date = datetime(year=year, month=month, day=first_day_of_month,tzinfo=timezone.utc) + start_date = datetime( + year=year, month=month, day=first_day_of_month, tzinfo=timezone.utc + ) last_day_of_month = get_last_day_of_month(year=year, month=month) end_date = datetime( @@ -430,7 +435,7 @@ def get_date_range_of_month(year: int, month: int) -> Dict[str, str]: minute=59, second=59, microsecond=999999, - tzinfo=timezone.utc + tzinfo=timezone.utc, ) return { diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index be27d23a..aaea297a 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -1,7 +1,6 @@ import abc from dataclasses import dataclass, field from typing import List, Callable -from flask import jsonify from azure.cosmos import PartitionKey from flask_restplus._http import HTTPStatus @@ -115,7 +114,7 @@ def find( self, id: str, event_context: EventContext, - peeker: 'function' = None, + peeker: Callable = None, visible_only=True, mapper: Callable = None, ): @@ -150,6 +149,7 @@ def find_all( custom_params=custom_params, ) + project_dao = projects_model.create_dao() projects = project_dao.get_all() add_project_name_to_time_entries(time_entries, projects) return time_entries From d94fde7df17d4e3b9faa94f73e5e52da80319ff5 Mon Sep 17 00:00:00 2001 From: Dickson Armijos Date: Thu, 14 May 2020 13:38:25 -0500 Subject: [PATCH 004/300] fix: role-based control #122 --- commons/data_access_layer/database.py | 2 +- time_tracker_api/database.py | 15 ++++++++++++--- time_tracker_api/security.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/commons/data_access_layer/database.py b/commons/data_access_layer/database.py index c1497b82..aa895339 100644 --- a/commons/data_access_layer/database.py +++ b/commons/data_access_layer/database.py @@ -33,7 +33,7 @@ def delete(self, id): raise NotImplementedError # pragma: no cover -class EventContext(): +class EventContext: def __init__(self, container_id: str, action: str, description: str = None, user_id: str = None, tenant_id: str = None, session_id: str = None, app_id: str = None): diff --git a/time_tracker_api/database.py b/time_tracker_api/database.py index 467f7900..280cc66e 100644 --- a/time_tracker_api/database.py +++ b/time_tracker_api/database.py @@ -11,7 +11,7 @@ from commons.data_access_layer.cosmos_db import CosmosDBDao from commons.data_access_layer.database import EventContext -from time_tracker_api.security import current_user_id, current_user_tenant_id +from time_tracker_api.security import current_user_id, current_user_tenant_id, current_role_user, roles class CRUDDao(abc.ABC): @@ -37,10 +37,11 @@ def delete(self, id): class ApiEventContext(EventContext): - def __init__(self, container_id: str, action: str, description: str = None, - user_id: str = None, tenant_id: str = None, session_id: str = None): + def __init__(self, container_id: str, action: str, description: str = None, user_id: str = None, + tenant_id: str = None, session_id: str = None, user_role: str = None): super(ApiEventContext, self).__init__(container_id, action, description) self._user_id = user_id + self._user_role = user_role self._tenant_id = tenant_id self._session_id = session_id @@ -50,6 +51,10 @@ def user_id(self) -> str: self._user_id = current_user_id() return self._user_id + @property + def user_role(self) -> str: + return self._user_role if self._user_role else current_role_user() + @property def tenant_id(self) -> str: if self._tenant_id is None: @@ -60,6 +65,10 @@ def tenant_id(self) -> str: def session_id(self) -> str: return self._session_id + @property + def is_admin(self): + return True if self.user_role == roles.get("admin").get("name") else False + class APICosmosDBDao(CosmosDBDao): def create_event_context(self, action: str = None, description: str = None): diff --git a/time_tracker_api/security.py b/time_tracker_api/security.py index b5919a1d..3f6643d1 100644 --- a/time_tracker_api/security.py +++ b/time_tracker_api/security.py @@ -29,6 +29,13 @@ iss_claim_pattern = re.compile(r"(.*).b2clogin.com/(?P%s)" % UUID_REGEX) +default_role = frozenset({"client-role"}) + +roles = { + "admin": {"name": "time-tracker-admin"}, + "client": {"name": "client-role"} +} + def current_user_id() -> str: oid_claim = get_token_json().get("oid") @@ -38,6 +45,11 @@ def current_user_id() -> str: return oid_claim +def current_role_user() -> str: + role_user = get_token_json().get("extension_role", None) + return role_user if role_user else roles.get("client").get("name") + + def current_user_tenant_id() -> str: iss_claim = get_token_json().get("iss") if iss_claim is None: From aae2fac4e61a8af151cf25a7be165f33144ca0d3 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 14 May 2020 18:58:07 -0500 Subject: [PATCH 005/300] fix(time-entries): :ambulance: move logic of project names to time-entries DAO to keep compatibility with repository level functions --- .../time_entries/time_entries_model.py | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index aaea297a..ec73281a 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -110,24 +110,6 @@ def create_sql_date_range_filter(date_range: dict) -> str: else: return '' - def find( - self, - id: str, - event_context: EventContext, - peeker: Callable = None, - visible_only=True, - mapper: Callable = None, - ): - time_entry = CosmosDBRepository.find( - self, id, event_context, peeker, visible_only, mapper, - ) - - project_dao = projects_model.create_dao() - project = project_dao.get(time_entry.project_id) - setattr(time_entry, 'project_name', project.name) - - return time_entry - def find_all( self, event_context: EventContext, @@ -141,7 +123,7 @@ def find_all( custom_params = self.generate_params(date_range) - time_entries = CosmosDBRepository.find_all( + return CosmosDBRepository.find_all( self, event_context=event_context, conditions=conditions, @@ -149,11 +131,6 @@ def find_all( custom_params=custom_params, ) - project_dao = projects_model.create_dao() - projects = project_dao.get_all() - add_project_name_to_time_entries(time_entries, projects) - return time_entries - def on_create(self, new_item_data: dict, event_context: EventContext): CosmosDBRepository.on_create(self, new_item_data, event_context) @@ -310,16 +287,26 @@ def get_all(self, conditions: dict = {}) -> list: conditions.update({"owner_id": event_ctx.user_id}) date_range = self.handle_date_filter_args(args=conditions) - return self.repository.find_all( + time_entries = self.repository.find_all( event_ctx, conditions=conditions, date_range=date_range ) + project_dao = projects_model.create_dao() + projects = project_dao.get_all() + add_project_name_to_time_entries(time_entries, projects) + return time_entries + def get(self, id): event_ctx = self.create_event_context("read") - return self.repository.find( + time_entry = self.repository.find( id, event_ctx, peeker=self.check_whether_current_user_owns_item ) + project_dao = projects_model.create_dao() + project = project_dao.get(time_entry.project_id) + setattr(time_entry, 'project_name', project.name) + return time_entry + def create(self, data: dict): event_ctx = self.create_event_context("create") data['owner_id'] = event_ctx.user_id From bc12d5df95ac77ede9bc7e76f736c097eefad669 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 14 May 2020 20:28:36 -0500 Subject: [PATCH 006/300] test(time-entries): :white_check_mark: update tests to meet changes in DAO --- .../time_entries_namespace_test.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) 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 68d7c7c0..a29dbc98 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 @@ -146,8 +146,8 @@ def test_list_all_time_entries( time_entries_dao, ) - repository_find_all_mock = mocker.patch.object( - time_entries_dao.repository, 'find_all', return_value=[] + dao_get_all_mock = mocker.patch.object( + time_entries_dao, 'get_all', return_value=[] ) response = client.get( @@ -156,7 +156,7 @@ def test_list_all_time_entries( assert HTTPStatus.OK == response.status_code assert [] == json.loads(response.data) - repository_find_all_mock.assert_called_once() + dao_get_all_mock.assert_called_once() def test_get_time_entry_should_succeed_with_valid_id( @@ -166,8 +166,8 @@ def test_get_time_entry_should_succeed_with_valid_id( time_entries_dao, ) - repository_find_mock = mocker.patch.object( - time_entries_dao.repository, 'find', return_value=fake_time_entry + dao_get_mock = mocker.patch.object( + time_entries_dao, 'get', return_value={} ) valid_id = fake.random_int(1, 9999) @@ -179,9 +179,7 @@ def test_get_time_entry_should_succeed_with_valid_id( assert HTTPStatus.OK == response.status_code fake_time_entry == json.loads(response.data) - repository_find_mock.assert_called_once_with( - str(valid_id), ANY, peeker=ANY - ) + dao_get_mock.assert_called_once_with(str(valid_id)) def test_get_time_entry_should_response_with_unprocessable_entity_for_invalid_id_format( @@ -614,20 +612,15 @@ def test_find_all_is_called_with_generated_dates( time_entries_dao, ) - repository_find_all_mock = mocker.patch.object( - time_entries_dao.repository, 'find_all', return_value=[] + dao_get_all_mock = mocker.patch.object( + time_entries_dao, 'get_all', return_value=[] ) response = client.get(url, headers=valid_header, follow_redirects=True) - date_range = get_date_range_of_month(year, month) - conditions = {'owner_id': owner_id} - assert HTTPStatus.OK == response.status_code assert json.loads(response.data) is not None - repository_find_all_mock.assert_called_once_with( - ANY, conditions=conditions, date_range=date_range - ) + dao_get_all_mock.assert_called_once() def test_summary_is_called_with_date_range_from_worked_time_module( From 56af879776c6d7a76f72466eefa5079390959be0 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 15 May 2020 13:37:18 +0000 Subject: [PATCH 007/300] 0.12.2 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index f8d90954..92a60bdf 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.12.1' +__version__ = '0.12.2' From ceb6ced2bdbc2e16f55e81c7e1e9891c94257b51 Mon Sep 17 00:00:00 2001 From: Dickson Armijos Date: Mon, 18 May 2020 16:55:53 -0500 Subject: [PATCH 008/300] fix: Update time-entries model #122 --- commons/data_access_layer/cosmos_db.py | 22 ++++------ time_tracker_api/projects/projects_model.py | 6 ++- .../time_entries/time_entries_model.py | 44 +++++++++++++------ .../time_entries/time_entries_namespace.py | 15 +++++++ 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/commons/data_access_layer/cosmos_db.py b/commons/data_access_layer/cosmos_db.py index b29e8e7c..396390d7 100644 --- a/commons/data_access_layer/cosmos_db.py +++ b/commons/data_access_layer/cosmos_db.py @@ -222,17 +222,8 @@ def find( function_mapper = self.get_mapper_or_dict(mapper) return function_mapper(self.check_visibility(found_item, visible_only)) - def find_all( - self, - event_context: EventContext, - conditions: dict = {}, - custom_sql_conditions: List[str] = [], - custom_params: dict = {}, - max_count=None, - offset=0, - visible_only=True, - mapper: Callable = None, - ): + def find_all(self, event_context: EventContext, conditions: dict = {}, custom_sql_conditions: List[str] = [], + custom_params: dict = {}, max_count=None, offset=0, visible_only=True, mapper: Callable = None): partition_key_value = self.find_partition_key_value(event_context) max_count = self.get_page_size_or(max_count) params = [ @@ -242,8 +233,8 @@ def find_all( ] params.extend(self.generate_params(conditions)) params.extend(custom_params) - result = self.container.query_items( - query=""" + print("before query") + query_str = """ SELECT * FROM c WHERE c.{partition_key_attribute}=@partition_key_value {conditions_clause} @@ -261,7 +252,10 @@ def find_all( custom_sql_conditions ), order_clause=self.create_sql_order_clause(), - ), + ) + + result = self.container.query_items( + query=query_str, parameters=params, partition_key=partition_key_value, max_item_count=max_count, diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 447e5c1a..3c4a3aac 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -73,7 +73,11 @@ def get_all(self, conditions: dict = None, **kwargs) -> list: customers_id = [customer.id for customer in customers] conditions = conditions if conditions else {} custom_condition = "c.customer_id IN {}".format(str(tuple(customers_id))) - return self.repository.find_all(event_ctx, conditions, custom_sql_conditions=[custom_condition], **kwargs) + if "custom_sql_conditions" in kwargs: + kwargs["custom_sql_conditions"].append(custom_condition) + else: + kwargs["custom_sql_conditions"] = [custom_condition] + return self.repository.find_all(event_ctx, conditions, **kwargs) def create_dao() -> ProjectDao: diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index dd13e609..24978277 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -21,6 +21,7 @@ from time_tracker_api.time_entries.custom_modules.utils import ( add_project_name_to_time_entries, ) +from time_tracker_api.projects.projects_model import ProjectCosmosDBModel, create_dao as project_create_dao from time_tracker_api.projects import projects_model from time_tracker_api.database import CRUDDao, APICosmosDBDao from time_tracker_api.security import current_user_id @@ -74,6 +75,14 @@ def __init__(self, data): # pragma: no cover def running(self): return self.end_date is None + def __add__(self, other): + if type(other) is ProjectCosmosDBModel: + time_entry = self.__class__ + time_entry.project_id = other.__dict__ + return time_entry + else: + raise NotImplementedError + def __repr__(self): return '