Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions commons/data_access_layer/cosmos_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def find_all(self, partition_key_value: str, conditions: dict = {}, max_count=No

def partial_update(self, id: str, changes: dict, partition_key_value: str,
peeker: 'function' = None, visible_only=True, mapper: Callable = None):
item_data = self.find(id, partition_key_value, peeker=peeker, visible_only=visible_only, mapper=dict)
item_data = self.find(id, partition_key_value, peeker=peeker,
visible_only=visible_only, mapper=dict)
item_data.update(changes)
return self.update(id, item_data, mapper=mapper)

Expand Down Expand Up @@ -219,8 +220,9 @@ class CosmosDBDao(CRUDDao):
def __init__(self, repository: CosmosDBRepository):
self.repository = repository

def get_all(self) -> list:
return self.repository.find_all(partition_key_value=self.partition_key_value)
def get_all(self, conditions: dict = {}) -> list:
return self.repository.find_all(partition_key_value=self.partition_key_value,
conditions=conditions)

def get(self, id):
return self.repository.find(id, partition_key_value=self.partition_key_value)
Expand Down
2 changes: 1 addition & 1 deletion commons/data_access_layer/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class CRUDDao(abc.ABC):
@abc.abstractmethod
def get_all(self):
def get_all(self, conditions: dict):
raise NotImplementedError # pragma: no cover

@abc.abstractmethod
Expand Down
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import jwt
import pytest
from faker import Faker
from flask import Flask, url_for
from flask import Flask
from flask.testing import FlaskClient

from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime
from commons.data_access_layer.cosmos_db import CosmosDBRepository
from commons.data_access_layer.database import init_sql
from time_tracker_api import create_app
from time_tracker_api.security import get_or_generate_dev_secret_key
Expand Down Expand Up @@ -160,7 +160,7 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository,
yield created_time_entry

time_entry_repository.delete_permanently(id=created_time_entry.id,
partition_key_value=tenant_id)
partition_key_value=tenant_id)


@pytest.fixture(scope="session")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def test_list_all_activities(client: FlaskClient,
assert HTTPStatus.OK == response.status_code
json_data = json.loads(response.data)
assert [] == json_data
repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id)
repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id,
conditions={})


def test_get_activity_should_succeed_with_valid_id(client: FlaskClient,
Expand Down
24 changes: 24 additions & 0 deletions tests/time_tracker_api/api_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from flask_restplus.reqparse import RequestParser
from pytest import fail


def test_create_attributes_filter_with_invalid_attribute_should_fail():
from time_tracker_api.api import create_attributes_filter
from time_tracker_api.projects.projects_namespace import project, ns

try:
create_attributes_filter(ns, project, ['invalid_attribute'])

fail("It was expected to fail")
except Exception as e:
assert type(e) is ValueError


def test_create_attributes_filter_with_valid_attribute_should_succeed():
from time_tracker_api.api import create_attributes_filter
from time_tracker_api.projects.projects_namespace import project, ns

filter = create_attributes_filter(ns, project, ['name'])

assert filter is not None
assert type(filter) is RequestParser
1 change: 0 additions & 1 deletion time_tracker_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from flask import Flask


flask_app: Flask = None


Expand Down
23 changes: 20 additions & 3 deletions time_tracker_api/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError, CosmosHttpResponseError
from faker import Faker
from flask import current_app as app
from flask_restplus import Api, fields
from flask_restplus import Api, fields, Model
from flask_restplus import namespace
from flask_restplus._http import HTTPStatus
from flask_restplus.reqparse import RequestParser

from commons.data_access_layer.cosmos_db import CustomError
from time_tracker_api import security
from time_tracker_api.security import UUID_REGEX
from time_tracker_api.version import __version__

faker = Faker()
Expand All @@ -18,8 +21,22 @@
security="TimeTracker JWT",
)

# For matching UUIDs
UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'

# Filters
def create_attributes_filter(ns: namespace, model: Model, filter_attrib_names: list) -> RequestParser:
attribs_parser = ns.parser()
model_attributes = model.resolved
for attrib in filter_attrib_names:
if attrib not in model_attributes:
raise ValueError(f"{attrib} is not a valid filter attribute for {model.name}")

attribs_parser.add_argument(attrib, required=False,
store_missing=False,
help="(Filter) %s " % model_attributes[attrib].description,
location='args')

return attribs_parser


# Common models structure
common_fields = {
Expand Down
14 changes: 11 additions & 3 deletions time_tracker_api/project_types/project_types_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from flask_restplus import Namespace, Resource, fields
from flask_restplus._http import HTTPStatus

from time_tracker_api.api import common_fields, UUID_REGEX
from time_tracker_api.api import common_fields, create_attributes_filter
from time_tracker_api.project_types.project_types_model import create_dao
from time_tracker_api.security import UUID_REGEX

faker = Faker()

Expand All @@ -16,7 +17,7 @@
required=True,
max_length=50,
description='Name of the project type',
example=faker.random_element(["Customer","Training","Internal"]),
example=faker.random_element(["Customer", "Training", "Internal"]),
),
'description': fields.String(
title='Description',
Expand Down Expand Up @@ -53,14 +54,21 @@

project_type_dao = create_dao()

attributes_filter = create_attributes_filter(ns, project_type, [
"customer_id",
"parent_id",
])


@ns.route('')
class ProjectTypes(Resource):
@ns.doc('list_project_types')
@ns.expect(attributes_filter)
@ns.marshal_list_with(project_type)
def get(self):
"""List all project types"""
return project_type_dao.get_all()
conditions = attributes_filter.parse_args()
return project_type_dao.get_all(conditions=conditions)

@ns.doc('create_project_type')
@ns.response(HTTPStatus.CONFLICT, 'This project type already exists')
Expand Down
15 changes: 11 additions & 4 deletions time_tracker_api/projects/projects_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from flask_restplus import Namespace, Resource, fields
from flask_restplus._http import HTTPStatus

from time_tracker_api.api import common_fields, UUID_REGEX
from time_tracker_api.api import common_fields, create_attributes_filter
from time_tracker_api.projects.projects_model import create_dao
from time_tracker_api.security import UUID_REGEX

faker = Faker()

Expand Down Expand Up @@ -36,8 +37,7 @@
'project_type_id': fields.String(
title='Identifier of the project type',
required=False,
description='This id allows to created a tree-like structure for projects, '
'grouped by project types',
description='Id of the project type it belongs. This allows grouping the projects.',
pattern=UUID_REGEX,
example=faker.uuid4(),
),
Expand All @@ -54,14 +54,21 @@

project_dao = create_dao()

attributes_filter = create_attributes_filter(ns, project, [
"customer_id",
"project_type_id",
])


@ns.route('')
class Projects(Resource):
@ns.doc('list_projects')
@ns.expect(attributes_filter)
@ns.marshal_list_with(project)
def get(self):
"""List all projects"""
return project_dao.get_all()
conditions = attributes_filter.parse_args()
return project_dao.get_all(conditions=conditions)

@ns.doc('create_project')
@ns.response(HTTPStatus.CONFLICT, 'This project already exists')
Expand Down
6 changes: 4 additions & 2 deletions time_tracker_api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
}
}

iss_claim_pattern = re.compile(
r"(.*).b2clogin.com/(?P<tenant_id>[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12})")
# For matching UUIDs
UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'

iss_claim_pattern = re.compile(r"(.*).b2clogin.com/(?P<tenant_id>%s)" % UUID_REGEX)


def current_user_id() -> str:
Expand Down
5 changes: 3 additions & 2 deletions time_tracker_api/time_entries/time_entries_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,10 @@ def check_whether_current_user_owns_item(cls, data: dict):
raise CustomError(HTTPStatus.FORBIDDEN,
"The current user is not the owner of this time entry")

def get_all(self) -> list:
def get_all(self, conditions: dict = {}) -> list:
conditions.update({"owner_id": self.current_user_id()})
return self.repository.find_all(partition_key_value=self.partition_key_value,
conditions={"owner_id": self.current_user_id()})
conditions=conditions)

def get(self, id):
return self.repository.find(id,
Expand Down
12 changes: 10 additions & 2 deletions time_tracker_api/time_entries/time_entries_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

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.api import common_fields, create_attributes_filter
from time_tracker_api.security import UUID_REGEX
from time_tracker_api.time_entries.time_entries_model import create_dao

faker = Faker()
Expand Down Expand Up @@ -103,14 +104,21 @@

time_entries_dao = create_dao()

attributes_filter = create_attributes_filter(ns, time_entry, [
"project_id",
"activity_id",
"uri",
])


@ns.route('')
class TimeEntries(Resource):
@ns.doc('list_time_entries')
@ns.marshal_list_with(time_entry)
def get(self):
"""List all time entries"""
return time_entries_dao.get_all()
conditions = attributes_filter.parse_args()
return time_entries_dao.get_all(conditions=conditions)

@ns.doc('create_time_entry')
@ns.expect(time_entry_input)
Expand Down