Skip to content

Commit 44a4260

Browse files
author
EliuX
committed
feat: Close #93 Add filter to projects, project-types and time-entries
2 parents afad986 + beb8c84 commit 44a4260

File tree

12 files changed

+94
-25
lines changed

12 files changed

+94
-25
lines changed

commons/data_access_layer/cosmos_db.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ def find_all(self, partition_key_value: str, conditions: dict = {}, max_count=No
176176

177177
def partial_update(self, id: str, changes: dict, partition_key_value: str,
178178
peeker: 'function' = None, visible_only=True, mapper: Callable = None):
179-
item_data = self.find(id, partition_key_value, peeker=peeker, visible_only=visible_only, mapper=dict)
179+
item_data = self.find(id, partition_key_value, peeker=peeker,
180+
visible_only=visible_only, mapper=dict)
180181
item_data.update(changes)
181182
return self.update(id, item_data, mapper=mapper)
182183

@@ -219,8 +220,9 @@ class CosmosDBDao(CRUDDao):
219220
def __init__(self, repository: CosmosDBRepository):
220221
self.repository = repository
221222

222-
def get_all(self) -> list:
223-
return self.repository.find_all(partition_key_value=self.partition_key_value)
223+
def get_all(self, conditions: dict = {}) -> list:
224+
return self.repository.find_all(partition_key_value=self.partition_key_value,
225+
conditions=conditions)
224226

225227
def get(self, id):
226228
return self.repository.find(id, partition_key_value=self.partition_key_value)

commons/data_access_layer/database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class CRUDDao(abc.ABC):
1717
@abc.abstractmethod
18-
def get_all(self):
18+
def get_all(self, conditions: dict):
1919
raise NotImplementedError # pragma: no cover
2020

2121
@abc.abstractmethod

tests/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import jwt
44
import pytest
55
from faker import Faker
6-
from flask import Flask, url_for
6+
from flask import Flask
77
from flask.testing import FlaskClient
88

9-
from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime
9+
from commons.data_access_layer.cosmos_db import CosmosDBRepository
1010
from commons.data_access_layer.database import init_sql
1111
from time_tracker_api import create_app
1212
from time_tracker_api.security import get_or_generate_dev_secret_key
@@ -160,7 +160,7 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository,
160160
yield created_time_entry
161161

162162
time_entry_repository.delete_permanently(id=created_time_entry.id,
163-
partition_key_value=tenant_id)
163+
partition_key_value=tenant_id)
164164

165165

166166
@pytest.fixture(scope="session")

tests/time_tracker_api/activities/activities_namespace_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def test_list_all_activities(client: FlaskClient,
6767
assert HTTPStatus.OK == response.status_code
6868
json_data = json.loads(response.data)
6969
assert [] == json_data
70-
repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id)
70+
repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id,
71+
conditions={})
7172

7273

7374
def test_get_activity_should_succeed_with_valid_id(client: FlaskClient,

tests/time_tracker_api/api_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from flask_restplus.reqparse import RequestParser
2+
from pytest import fail
3+
4+
5+
def test_create_attributes_filter_with_invalid_attribute_should_fail():
6+
from time_tracker_api.api import create_attributes_filter
7+
from time_tracker_api.projects.projects_namespace import project, ns
8+
9+
try:
10+
create_attributes_filter(ns, project, ['invalid_attribute'])
11+
12+
fail("It was expected to fail")
13+
except Exception as e:
14+
assert type(e) is ValueError
15+
16+
17+
def test_create_attributes_filter_with_valid_attribute_should_succeed():
18+
from time_tracker_api.api import create_attributes_filter
19+
from time_tracker_api.projects.projects_namespace import project, ns
20+
21+
filter = create_attributes_filter(ns, project, ['name'])
22+
23+
assert filter is not None
24+
assert type(filter) is RequestParser

time_tracker_api/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from flask import Flask
55

6-
76
flask_app: Flask = None
87

98

time_tracker_api/api.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError, CosmosHttpResponseError
22
from faker import Faker
33
from flask import current_app as app
4-
from flask_restplus import Api, fields
4+
from flask_restplus import Api, fields, Model
5+
from flask_restplus import namespace
56
from flask_restplus._http import HTTPStatus
7+
from flask_restplus.reqparse import RequestParser
68

79
from commons.data_access_layer.cosmos_db import CustomError
810
from time_tracker_api import security
11+
from time_tracker_api.security import UUID_REGEX
912
from time_tracker_api.version import __version__
1013

1114
faker = Faker()
@@ -18,8 +21,22 @@
1821
security="TimeTracker JWT",
1922
)
2023

21-
# For matching UUIDs
22-
UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'
24+
25+
# Filters
26+
def create_attributes_filter(ns: namespace, model: Model, filter_attrib_names: list) -> RequestParser:
27+
attribs_parser = ns.parser()
28+
model_attributes = model.resolved
29+
for attrib in filter_attrib_names:
30+
if attrib not in model_attributes:
31+
raise ValueError(f"{attrib} is not a valid filter attribute for {model.name}")
32+
33+
attribs_parser.add_argument(attrib, required=False,
34+
store_missing=False,
35+
help="(Filter) %s " % model_attributes[attrib].description,
36+
location='args')
37+
38+
return attribs_parser
39+
2340

2441
# Common models structure
2542
common_fields = {

time_tracker_api/project_types/project_types_namespace.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from flask_restplus import Namespace, Resource, fields
33
from flask_restplus._http import HTTPStatus
44

5-
from time_tracker_api.api import common_fields, UUID_REGEX
5+
from time_tracker_api.api import common_fields, create_attributes_filter
66
from time_tracker_api.project_types.project_types_model import create_dao
7+
from time_tracker_api.security import UUID_REGEX
78

89
faker = Faker()
910

@@ -16,7 +17,7 @@
1617
required=True,
1718
max_length=50,
1819
description='Name of the project type',
19-
example=faker.random_element(["Customer","Training","Internal"]),
20+
example=faker.random_element(["Customer", "Training", "Internal"]),
2021
),
2122
'description': fields.String(
2223
title='Description',
@@ -53,14 +54,21 @@
5354

5455
project_type_dao = create_dao()
5556

57+
attributes_filter = create_attributes_filter(ns, project_type, [
58+
"customer_id",
59+
"parent_id",
60+
])
61+
5662

5763
@ns.route('')
5864
class ProjectTypes(Resource):
5965
@ns.doc('list_project_types')
66+
@ns.expect(attributes_filter)
6067
@ns.marshal_list_with(project_type)
6168
def get(self):
6269
"""List all project types"""
63-
return project_type_dao.get_all()
70+
conditions = attributes_filter.parse_args()
71+
return project_type_dao.get_all(conditions=conditions)
6472

6573
@ns.doc('create_project_type')
6674
@ns.response(HTTPStatus.CONFLICT, 'This project type already exists')

time_tracker_api/projects/projects_namespace.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from flask_restplus import Namespace, Resource, fields
33
from flask_restplus._http import HTTPStatus
44

5-
from time_tracker_api.api import common_fields, UUID_REGEX
5+
from time_tracker_api.api import common_fields, create_attributes_filter
66
from time_tracker_api.projects.projects_model import create_dao
7+
from time_tracker_api.security import UUID_REGEX
78

89
faker = Faker()
910

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

5555
project_dao = create_dao()
5656

57+
attributes_filter = create_attributes_filter(ns, project, [
58+
"customer_id",
59+
"project_type_id",
60+
])
61+
5762

5863
@ns.route('')
5964
class Projects(Resource):
6065
@ns.doc('list_projects')
66+
@ns.expect(attributes_filter)
6167
@ns.marshal_list_with(project)
6268
def get(self):
6369
"""List all projects"""
64-
return project_dao.get_all()
70+
conditions = attributes_filter.parse_args()
71+
return project_dao.get_all(conditions=conditions)
6572

6673
@ns.doc('create_project')
6774
@ns.response(HTTPStatus.CONFLICT, 'This project already exists')

time_tracker_api/security.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
}
2525
}
2626

27-
iss_claim_pattern = re.compile(
28-
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})")
27+
# For matching UUIDs
28+
UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}'
29+
30+
iss_claim_pattern = re.compile(r"(.*).b2clogin.com/(?P<tenant_id>%s)" % UUID_REGEX)
2931

3032

3133
def current_user_id() -> str:

0 commit comments

Comments
 (0)