diff --git a/tests/conftest.py b/tests/conftest.py index 9c9d244f..5cb5c18d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from time_tracker_api import create_app from time_tracker_api.database import init_sql from time_tracker_api.security import get_or_generate_dev_secret_key -from time_tracker_api.time_entries.time_entries_repository import ( +from time_tracker_api.time_entries.time_entries_model import ( TimeEntryCosmosDBRepository, ) 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 7eeda65b..002e8ccf 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 @@ -3,9 +3,10 @@ from commons.data_access_layer.database import EventContext from time_tracker_api.time_entries.time_entries_model import ( + TimeEntryCosmosDBRepository, TimeEntryCosmosDBModel, ) -from time_tracker_api.time_entries.time_entries_repository import TimeEntryCosmosDBRepository + def create_time_entry( start_date: str, diff --git a/time_tracker_api/projects/projects_dao.py b/time_tracker_api/projects/projects_dao.py deleted file mode 100644 index 6abe2cd3..00000000 --- a/time_tracker_api/projects/projects_dao.py +++ /dev/null @@ -1,68 +0,0 @@ -from azure.cosmos import PartitionKey -from commons.data_access_layer.cosmos_db import ( - CosmosDBDao, - CosmosDBRepository, -) -from time_tracker_api.database import CRUDDao, APICosmosDBDao -from time_tracker_api.customers.customers_model import ( - create_dao as customers_create_dao, -) -from time_tracker_api.projects.projects_model import ProjectCosmosDBModel - -from utils.extend_model import add_customer_name_to_projects - - -class ProjectDao(CRUDDao): - pass - - -container_definition = { - 'id': 'project', - 'partition_key': PartitionKey(path='/tenant_id'), - 'unique_key_policy': { - 'uniqueKeys': [{'paths': ['/name', '/customer_id', '/deleted']},] - }, -} - - -class ProjectCosmosDBRepository(CosmosDBRepository): - def __init__(self): - CosmosDBRepository.__init__( - self, - container_id=container_definition['id'], - partition_key_attribute='tenant_id', - mapper=ProjectCosmosDBModel, - ) - - -class ProjectCosmosDBDao(APICosmosDBDao, ProjectDao): - def __init__(self, repository): - CosmosDBDao.__init__(self, repository) - - 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( - max_count=kwargs.get('max_count', None) - ) - - customers_id = [customer.id for customer in customers] - conditions = conditions if conditions else {} - custom_condition = "c.customer_id IN {}".format( - str(tuple(customers_id)) - ) - # TODO this must be refactored to be used from the utils module ↑ - if "custom_sql_conditions" in kwargs: - kwargs["custom_sql_conditions"].append(custom_condition) - else: - kwargs["custom_sql_conditions"] = [custom_condition] - projects = self.repository.find_all(event_ctx, conditions, **kwargs) - - add_customer_name_to_projects(projects, customers) - return projects - - -def create_dao() -> ProjectDao: - repository = ProjectCosmosDBRepository() - - return ProjectCosmosDBDao(repository) diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 30abafad..ff40c7c8 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -1,9 +1,31 @@ from dataclasses import dataclass +from azure.cosmos import PartitionKey from commons.data_access_layer.cosmos_db import ( CosmosDBModel, + CosmosDBDao, + CosmosDBRepository, +) +from time_tracker_api.database import CRUDDao, APICosmosDBDao +from time_tracker_api.customers.customers_model import ( + create_dao as customers_create_dao, ) from time_tracker_api.customers.customers_model import CustomerCosmosDBModel +from utils.extend_model import add_customer_name_to_projects + + +class ProjectDao(CRUDDao): + pass + + +container_definition = { + 'id': 'project', + 'partition_key': PartitionKey(path='/tenant_id'), + 'unique_key_policy': { + 'uniqueKeys': [{'paths': ['/name', '/customer_id', '/deleted']},] + }, +} + @dataclass() class ProjectCosmosDBModel(CosmosDBModel): @@ -30,3 +52,52 @@ def __repr__(self): def __str___(self): return "the project \"%s\"" % self.name # pragma: no cover + + +class ProjectCosmosDBRepository(CosmosDBRepository): + def __init__(self): + CosmosDBRepository.__init__( + self, + container_id=container_definition['id'], + partition_key_attribute='tenant_id', + mapper=ProjectCosmosDBModel, + ) + + +class ProjectCosmosDBDao(APICosmosDBDao, ProjectDao): + def __init__(self, repository): + CosmosDBDao.__init__(self, repository) + + def get_all(self, conditions: dict = None, **kwargs) -> list: + """ + Get all the projects an active client has + :param (dict) conditions: Conditions for querying the database + :param (dict) kwargs: Pass arguments + :return (list): ProjectCosmosDBModel object list + """ + event_ctx = self.create_event_context("read-many") + customer_dao = customers_create_dao() + 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 {} + custom_condition = "c.customer_id IN {}".format( + str(tuple(customers_id)) + ) + # TODO this must be refactored to be used from the utils module ↑ + if "custom_sql_conditions" in kwargs: + kwargs["custom_sql_conditions"].append(custom_condition) + else: + kwargs["custom_sql_conditions"] = [custom_condition] + projects = self.repository.find_all(event_ctx, conditions, **kwargs) + + add_customer_name_to_projects(projects, customers) + return projects + + +def create_dao() -> ProjectDao: + repository = ProjectCosmosDBRepository() + + return ProjectCosmosDBDao(repository) diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index 1f3cc3b0..c64039c8 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -4,7 +4,7 @@ from time_tracker_api.api import common_fields, create_attributes_filter, UUID, api, remove_required_constraint, \ NullableString -from time_tracker_api.projects.projects_dao import create_dao +from time_tracker_api.projects.projects_model import create_dao faker = Faker() diff --git a/time_tracker_api/time_entries/time_entries_dao.py b/time_tracker_api/time_entries/time_entries_dao.py deleted file mode 100644 index ce1355ce..00000000 --- a/time_tracker_api/time_entries/time_entries_dao.py +++ /dev/null @@ -1,253 +0,0 @@ -import abc -from flask_restplus import abort -from flask_restplus._http import HTTPStatus - -from datetime import timedelta - -from commons.data_access_layer.cosmos_db import ( - CosmosDBDao, - CustomError, -) - -from utils.extend_model import ( - create_custom_query_from_str, -) -from utils.time import ( - datetime_str, - str_to_datetime, - get_current_year, - get_current_month, - get_date_range_of_month, - current_datetime_str, -) -from utils import worked_time -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 -from time_tracker_api.time_entries.time_entries_repository import TimeEntryCosmosDBRepository - - -class TimeEntriesDao(CRUDDao): - @staticmethod - def current_user_id(): - return current_user_id() - - @abc.abstractmethod - def find_running(self): - pass - - @abc.abstractmethod - def stop(self, id: str): - pass - - @abc.abstractmethod - def restart(self, id: str): - pass - - -class TimeEntriesCosmosDBDao(APICosmosDBDao, TimeEntriesDao): - def __init__(self, repository): - CosmosDBDao.__init__(self, repository) - - def check_whether_current_user_owns_item(self, data): - if ( - data.owner_id is not None - and data.owner_id != self.current_user_id() - ): - raise CustomError( - HTTPStatus.FORBIDDEN, - "The current user is not the owner of this time entry", - ) - - def check_time_entry_is_not_stopped(self, data): - if data.end_date is not None: - raise CustomError( - HTTPStatus.UNPROCESSABLE_ENTITY, - "The specified time entry is already stopped", - ) - - def check_time_entry_is_not_started(self, data): - if data.end_date is None: - raise CustomError( - HTTPStatus.UNPROCESSABLE_ENTITY, - "The specified time entry is already running", - ) - - def build_custom_query(self, is_admin: bool, conditions: dict = None): - custom_query = [] - if "user_id" in conditions: - if is_admin: - conditions.pop("owner_id") - custom_query = ( - [] - if conditions.get("user_id") == "*" - else [ - create_custom_query_from_str( - conditions.get("user_id"), "c.owner_id" - ) - ] - ) - conditions.pop("user_id") - else: - 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) - return self.repository.find_all( - event_ctx, - conditions=conditions, - custom_sql_conditions=custom_query, - date_range=date_range, - max_count=limit, - ) - - def get_all_paginated(self, conditions: dict = None, **kwargs) -> list: - get_all_conditions = dict(conditions) - get_all_conditions.pop("length") - get_all_conditions.pop("start") - event_ctx = self.create_event_context("read-many") - get_all_conditions.update({"owner_id": event_ctx.user_id}) - custom_query = self.build_custom_query( - is_admin=event_ctx.is_admin, conditions=get_all_conditions, - ) - date_range = self.handle_date_filter_args(args=get_all_conditions) - records_total = self.repository.count( - event_ctx, - conditions=get_all_conditions, - custom_sql_conditions=custom_query, - date_range=date_range, - ) - 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': records_total, - 'data': time_entries, - } - - def get(self, id): - event_ctx = self.create_event_context("read") - - time_entry = self.repository.find(id, event_ctx) - self.check_whether_current_user_owns_item(time_entry) - - 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 - return self.repository.create(data, event_ctx) - - def update(self, id, data: dict, description=None): - event_ctx = self.create_event_context("update", description) - - time_entry = self.repository.find(id, event_ctx) - self.check_whether_current_user_owns_item(time_entry) - - return self.repository.partial_update(id, data, event_ctx,) - - def stop(self, id): - event_ctx = self.create_event_context("update", "Stop time entry") - - time_entry = self.repository.find(id, event_ctx) - self.check_whether_current_user_owns_item(time_entry) - self.check_time_entry_is_not_stopped(time_entry) - - return self.repository.partial_update( - id, {'end_date': current_datetime_str()}, event_ctx, - ) - - def restart(self, id): - event_ctx = self.create_event_context("update", "Restart time entry") - - time_entry = self.repository.find(id, event_ctx) - self.check_whether_current_user_owns_item(time_entry) - self.check_time_entry_is_not_started(time_entry) - - return self.repository.partial_update( - id, {'end_date': None}, event_ctx, - ) - - def delete(self, id): - event_ctx = self.create_event_context("delete") - time_entry = self.repository.find(id, event_ctx) - self.check_whether_current_user_owns_item(time_entry) - self.repository.delete( - id, event_ctx, - ) - - def find_running(self): - event_ctx = self.create_event_context("find_running") - time_entry = self.repository.find_running( - event_ctx.tenant_id, event_ctx.user_id - ) - return time_entry - - def get_worked_time(self, args: dict): - event_ctx = self.create_event_context( - "read", "Summary of worked time in the current month" - ) - - conditions = {"owner_id": event_ctx.user_id} - time_entries = self.repository.find_all_entries( - event_ctx, - conditions=conditions, - date_range=worked_time.date_range(), - ) - return worked_time.summary( - time_entries, time_offset=args.get('time_offset') - ) - - @staticmethod - def handle_date_filter_args(args: dict) -> dict: - if "start_date" and "end_date" in args: - start_date = str_to_datetime(args.pop('start_date')) - end_date = str_to_datetime(args.pop('end_date')) - else: - month = int(args.pop("month", get_current_month())) - year = int(args.pop("year", get_current_year())) - start_date, end_date = get_date_range_of_month(year, month) - - offset_in_minutes = int(args.pop('timezone_offset', 300)) - start_date = start_date + timedelta(minutes=offset_in_minutes) - end_date = end_date + timedelta(minutes=offset_in_minutes) - - return { - 'start_date': datetime_str(start_date), - 'end_date': datetime_str(end_date), - } - - -def create_dao() -> TimeEntriesDao: - repository = TimeEntryCosmosDBRepository() - return TimeEntriesCosmosDBDao(repository) diff --git a/time_tracker_api/time_entries/time_entries_repository.py b/time_tracker_api/time_entries/time_entries_repository.py deleted file mode 100644 index 5eb726ef..00000000 --- a/time_tracker_api/time_entries/time_entries_repository.py +++ /dev/null @@ -1,290 +0,0 @@ -from typing import List, Callable -from azure.cosmos import PartitionKey -from flask_restplus import abort -from flask_restplus._http import HTTPStatus -from commons.data_access_layer.cosmos_db import ( - CosmosDBRepository, - CustomError, -) - -from commons.data_access_layer.database import EventContext -from time_tracker_api.activities import activities_model -from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBModel - -from utils.extend_model import ( - add_project_info_to_time_entries, - add_activity_name_to_time_entries, - create_in_condition, - add_user_email_to_time_entries, -) -from utils.time import ( - current_datetime_str, -) -from utils.azure_users import AzureConnection -from time_tracker_api.projects import projects_model - - -container_definition = { - 'id': 'time_entry', - 'partition_key': PartitionKey(path='/tenant_id'), - 'unique_key_policy': { - 'uniqueKeys': [{'paths': ['/owner_id', '/end_date', '/deleted']}] - }, -} - - -class TimeEntryCosmosDBRepository(CosmosDBRepository): - def __init__(self): - CosmosDBRepository.__init__( - self, - container_id=container_definition['id'], - partition_key_attribute='tenant_id', - order_fields=['start_date DESC'], - mapper=TimeEntryCosmosDBModel, - ) - - @staticmethod - def create_sql_ignore_id_condition(id: str): - if id is None: - return '' - else: - return "AND c.id!=@ignore_id" - - @staticmethod - def create_sql_date_range_filter(date_range: dict) -> str: - if 'start_date' and 'end_date' in date_range: - return """ - ((c.start_date BETWEEN @start_date AND @end_date) OR - (c.end_date BETWEEN @start_date AND @end_date)) - """ - else: - return '' - - def find_all_entries( - self, - event_context: EventContext, - conditions: dict = None, - custom_sql_conditions: List[str] = None, - date_range: dict = None, - ): - conditions = conditions if conditions else {} - custom_sql_conditions = ( - custom_sql_conditions if custom_sql_conditions else [] - ) - date_range = date_range if date_range else {} - - custom_sql_conditions.append( - self.create_sql_date_range_filter(date_range) - ) - - custom_params = self.generate_params(date_range) - time_entries = CosmosDBRepository.find_all( - self, - event_context=event_context, - conditions=conditions, - custom_sql_conditions=custom_sql_conditions, - custom_params=custom_params, - ) - return time_entries - - def count( - self, - event_context: EventContext, - conditions: dict = None, - custom_sql_conditions: List[str] = None, - date_range: dict = None, - ): - conditions = conditions if conditions else {} - custom_sql_conditions = ( - custom_sql_conditions if custom_sql_conditions else [] - ) - date_range = date_range if date_range else {} - - custom_sql_conditions.append( - self.create_sql_date_range_filter(date_range) - ) - - custom_params = self.generate_params(date_range) - counter = CosmosDBRepository.count( - self, - event_context=event_context, - conditions=conditions, - custom_sql_conditions=custom_sql_conditions, - custom_params=custom_params, - ) - return counter - - def find_all( - self, - event_context: EventContext, - conditions: dict = None, - custom_sql_conditions: List[str] = None, - date_range: dict = None, - **kwargs, - ): - conditions = conditions if conditions else {} - custom_sql_conditions = ( - custom_sql_conditions if custom_sql_conditions else [] - ) - date_range = date_range if date_range else {} - - custom_sql_conditions.append( - self.create_sql_date_range_filter(date_range) - ) - - custom_params = self.generate_params(date_range) - time_entries = CosmosDBRepository.find_all( - self, - event_context=event_context, - conditions=conditions, - 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: - custom_conditions = create_in_condition(time_entries, "project_id") - custom_conditions_activity = create_in_condition( - time_entries, "activity_id" - ) - - project_dao = projects_model.create_dao() - projects = project_dao.get_all( - custom_sql_conditions=[custom_conditions], - visible_only=False, - max_count=kwargs.get("max_count", None), - ) - - 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) - - users = AzureConnection().users() - add_user_email_to_time_entries(time_entries, users) - elif not time_entries and len(conditions) > 1: - abort(HTTPStatus.NOT_FOUND, "Time entry not found") - return time_entries - - def on_create(self, new_item_data: dict, event_context: EventContext): - CosmosDBRepository.on_create(self, new_item_data, event_context) - - if new_item_data.get("start_date") is None: - new_item_data['start_date'] = current_datetime_str() - - self.validate_data(new_item_data, event_context) - - def on_update(self, updated_item_data: dict, event_context: EventContext): - CosmosDBRepository.on_update(self, updated_item_data, event_context) - self.validate_data(updated_item_data, event_context) - self.replace_empty_value_per_none(updated_item_data) - - def find_interception_with_date_range( - self, - start_date, - end_date, - owner_id, - tenant_id, - ignore_id=None, - visible_only=True, - mapper: Callable = None, - ): - conditions = { - "owner_id": owner_id, - "tenant_id": tenant_id, - } - params = [ - {"name": "@start_date", "value": start_date}, - {"name": "@end_date", "value": end_date or current_datetime_str()}, - {"name": "@ignore_id", "value": ignore_id}, - ] - params.extend(self.generate_params(conditions)) - result = self.container.query_items( - query=""" - SELECT * FROM c - WHERE ((c.start_date BETWEEN @start_date AND @end_date) - OR (c.end_date BETWEEN @start_date AND @end_date)) - AND c.start_date!= @end_date - AND c.end_date!= @start_date - {conditions_clause} - {ignore_id_condition} - {visibility_condition} - {order_clause} - """.format( - ignore_id_condition=self.create_sql_ignore_id_condition( - ignore_id - ), - visibility_condition=self.create_sql_condition_for_visibility( - visible_only - ), - conditions_clause=self.create_sql_where_conditions(conditions), - order_clause=self.create_sql_order_clause(), - ), - parameters=params, - partition_key=tenant_id, - ) - - function_mapper = self.get_mapper_or_dict(mapper) - return list(map(function_mapper, result)) - - def find_running( - self, tenant_id: str, owner_id: str, mapper: Callable = None - ): - conditions = { - "owner_id": owner_id, - "tenant_id": tenant_id, - } - result = self.container.query_items( - query=""" - SELECT * from c - WHERE (NOT IS_DEFINED(c.end_date) OR c.end_date = null) - {conditions_clause} - {visibility_condition} - OFFSET 0 LIMIT 1 - """.format( - visibility_condition=self.create_sql_condition_for_visibility( - True - ), - conditions_clause=self.create_sql_where_conditions(conditions), - ), - parameters=self.generate_params(conditions), - partition_key=tenant_id, - max_item_count=1, - ) - - function_mapper = self.get_mapper_or_dict(mapper) - return function_mapper(next(result)) - - def validate_data(self, data, event_context: EventContext): - start_date = data.get('start_date') - - if data.get('end_date') is not None: - 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'] >= 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=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: - raise CustomError( - HTTPStatus.UNPROCESSABLE_ENTITY, - description="There is another time entry in that date range", - )