Skip to content

Commit 31f054f

Browse files
author
Juan Gabriel Guzman
committed
feat: Fixing paginated time entries endpoint
1 parent ed86633 commit 31f054f

File tree

8 files changed

+178
-37
lines changed

8 files changed

+178
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ automatically [pip](https://pip.pypa.io/en/stable/) as well.
3939
python3 -m pip install -r requirements/<app>/<stage>.txt
4040
```
4141
42-
Where <app> is one of the executable app namespace, e.g. `time_tracker_api` or `time_tracker_events`.
42+
Where `<app>` is one of the executable app namespace, e.g. `time_tracker_api` or `time_tracker_events`.
4343
The `stage` can be
4444
4545
* `dev`: Used for working locally

commons/data_access_layer/cosmos_db.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import azure.cosmos.cosmos_client as cosmos_client
66
import azure.cosmos.exceptions as exceptions
7+
import flask
78
from azure.cosmos import ContainerProxy, PartitionKey
89
from flask import Flask
910
from werkzeug.exceptions import HTTPException
@@ -101,8 +102,8 @@ def __init__(
101102
raise ValueError("The cosmos_db module has not been initialized!")
102103
self.mapper = mapper
103104
self.order_fields = order_fields if order_fields else []
104-
self.container: ContainerProxy = self.cosmos_helper.db.get_container_client(
105-
container_id
105+
self.container: ContainerProxy = (
106+
self.cosmos_helper.db.get_container_client(container_id)
106107
)
107108
self.partition_key_attribute = partition_key_attribute
108109

@@ -266,7 +267,6 @@ def find_all(
266267
),
267268
order_clause=self.create_sql_order_clause(),
268269
)
269-
270270
result = self.container.query_items(
271271
query=query_str,
272272
parameters=params,
@@ -277,6 +277,50 @@ def find_all(
277277
function_mapper = self.get_mapper_or_dict(mapper)
278278
return list(map(function_mapper, result))
279279

280+
def count(
281+
self,
282+
event_context: EventContext,
283+
conditions: dict = None,
284+
custom_sql_conditions: List[str] = None,
285+
custom_params: dict = None,
286+
visible_only=True,
287+
):
288+
conditions = conditions if conditions else {}
289+
custom_sql_conditions = (
290+
custom_sql_conditions if custom_sql_conditions else []
291+
)
292+
custom_params = custom_params if custom_params else {}
293+
partition_key_value = self.find_partition_key_value(event_context)
294+
params = [
295+
{"name": "@partition_key_value", "value": partition_key_value},
296+
]
297+
params.extend(self.generate_params(conditions))
298+
params.extend(custom_params)
299+
query_str = """
300+
SELECT VALUE COUNT(1) FROM c
301+
WHERE c.{partition_key_attribute}=@partition_key_value
302+
{conditions_clause}
303+
{visibility_condition}
304+
{custom_sql_conditions_clause}
305+
""".format(
306+
partition_key_attribute=self.partition_key_attribute,
307+
visibility_condition=self.create_sql_condition_for_visibility(
308+
visible_only
309+
),
310+
conditions_clause=self.create_sql_where_conditions(conditions),
311+
custom_sql_conditions_clause=self.create_custom_sql_conditions(
312+
custom_sql_conditions
313+
),
314+
)
315+
316+
flask.current_app.logger.debug(query_str)
317+
result = self.container.query_items(
318+
query=query_str,
319+
parameters=params,
320+
partition_key=partition_key_value,
321+
)
322+
return result.next()
323+
280324
def partial_update(
281325
self,
282326
id: str,
@@ -286,7 +330,10 @@ def partial_update(
286330
mapper: Callable = None,
287331
):
288332
item_data = self.find(
289-
id, event_context, visible_only=visible_only, mapper=dict,
333+
id,
334+
event_context,
335+
visible_only=visible_only,
336+
mapper=dict,
290337
)
291338
item_data.update(changes)
292339
return self.update(id, item_data, event_context, mapper=mapper)
@@ -304,7 +351,10 @@ def update(
304351
return function_mapper(self.container.replace_item(id, body=item_data))
305352

306353
def delete(
307-
self, id: str, event_context: EventContext, mapper: Callable = None,
354+
self,
355+
id: str,
356+
event_context: EventContext,
357+
mapper: Callable = None,
308358
):
309359
return self.partial_update(
310360
id,
@@ -327,7 +377,7 @@ def get_mapper_or_dict(self, alternative_mapper: Callable) -> Callable:
327377
def get_page_size_or(self, custom_page_size: int) -> int:
328378
# TODO The default value should be taken from the Azure Feature Manager
329379
# or any other repository for the settings
330-
return custom_page_size or 100
380+
return custom_page_size or 9999
331381

332382
def on_update(self, update_item_data: dict, event_context: EventContext):
333383
pass

tests/commons/data_access_layer/cosmos_db_test.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ def test_create_with_diff_unique_data_but_same_tenant_should_succeed(
8282
):
8383
new_data = sample_item.copy()
8484
new_data.update(
85-
{'id': fake.uuid4(), 'email': fake.safe_email(),}
85+
{
86+
'id': fake.uuid4(),
87+
'email': fake.safe_email(),
88+
}
8689
)
8790

8891
result = cosmos_db_repository.create(new_data, event_context)
@@ -97,7 +100,9 @@ def test_create_with_same_id_should_fail(
97100
try:
98101
new_data = sample_item.copy()
99102
new_data.update(
100-
{'email': fake.safe_email(),}
103+
{
104+
'email': fake.safe_email(),
105+
}
101106
)
102107

103108
cosmos_db_repository.create(new_data, event_context)
@@ -134,7 +139,9 @@ def test_create_with_same_id_but_diff_partition_key_attrib_should_succeed(
134139
new_data = sample_item.copy()
135140

136141
new_data.update(
137-
{'tenant_id': another_tenant_id,}
142+
{
143+
'tenant_id': another_tenant_id,
144+
}
138145
)
139146

140147
result = cosmos_db_repository.create(new_data, another_event_context)
@@ -310,6 +317,15 @@ def test_find_all_with_offset(
310317
assert result_after_the_second_item == result_all_items[2:]
311318

312319

320+
def test_count(
321+
cosmos_db_repository: CosmosDBRepository, event_context: EventContext
322+
):
323+
counter = cosmos_db_repository.count(event_context)
324+
print('test counter: ', counter)
325+
326+
assert counter == 10
327+
328+
313329
@pytest.mark.parametrize(
314330
'mapper,expected_type', [(None, dict), (dict, dict), (Person, Person)]
315331
)
@@ -405,7 +421,10 @@ def test_update_with_mapper(
405421
):
406422
changed_item = sample_item.copy()
407423
changed_item.update(
408-
{'name': fake.name(), 'email': fake.safe_email(),}
424+
{
425+
'name': fake.name(),
426+
'email': fake.safe_email(),
427+
}
409428
)
410429

411430
updated_item = cosmos_db_repository.update(

tests/time_tracker_api/time_entries/time_entries_namespace_test.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -730,26 +730,31 @@ def test_summary_is_called_with_date_range_from_worked_time_module(
730730

731731

732732
def test_paginated_fails_with_no_params(
733-
client: FlaskClient, valid_header: dict,
733+
client: FlaskClient,
734+
valid_header: dict,
734735
):
735736
response = client.get('/time-entries/paginated', headers=valid_header)
736737
assert HTTPStatus.BAD_REQUEST == response.status_code
737738

738739

739740
def test_paginated_succeeds_with_valid_params(
740-
client: FlaskClient, valid_header: dict,
741+
client: FlaskClient,
742+
valid_header: dict,
741743
):
742744
response = client.get(
743-
'/time-entries/paginated?start=10&length=10', headers=valid_header
745+
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
746+
headers=valid_header,
744747
)
745748
assert HTTPStatus.OK == response.status_code
746749

747750

748751
def test_paginated_response_contains_expected_props(
749-
client: FlaskClient, valid_header: dict,
752+
client: FlaskClient,
753+
valid_header: dict,
750754
):
751755
response = client.get(
752-
'/time-entries/paginated?start=10&length=10', headers=valid_header
756+
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
757+
headers=valid_header,
753758
)
754759
assert 'data' in json.loads(response.data)
755760
assert 'records_total' in json.loads(response.data)
@@ -761,7 +766,8 @@ def test_paginated_sends_max_count_and_offset_on_call_to_repository(
761766
time_entries_dao.repository.find_all = Mock(return_value=[])
762767

763768
response = client.get(
764-
'/time-entries/paginated?start=10&length=10', headers=valid_header
769+
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
770+
headers=valid_header,
765771
)
766772

767773
time_entries_dao.repository.find_all.assert_called_once()

time_tracker_api/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
flask_app: Flask = None
77

88

9-
def create_app(config_path='time_tracker_api.config.DefaultConfig',
10-
config_data=None):
9+
def create_app(
10+
config_path='time_tracker_api.config.DefaultConfig', config_data=None
11+
):
1112
global flask_app
1213
flask_app = Flask(__name__)
1314

@@ -36,13 +37,15 @@ def init_app_config(app: Flask, config_path: str, config_data: dict = None):
3637

3738
def init_app(app: Flask):
3839
from time_tracker_api.database import init_app as init_database
40+
3941
init_database(app)
4042

4143
from time_tracker_api.api import init_app
44+
4245
init_app(app)
4346

4447
if app.config.get('DEBUG'):
45-
app.logger.setLevel(logging.INFO)
48+
app.logger.setLevel(logging.DEBUG)
4649
add_debug_toolbar(app)
4750

4851
add_werkzeug_proxy_fix(app)
@@ -62,22 +65,25 @@ def add_debug_toolbar(app: Flask):
6265
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
6366
'flask_debugtoolbar.panels.logger.LoggingPanel',
6467
'flask_debugtoolbar.panels.route_list.RouteListDebugPanel',
65-
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel'
68+
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
6669
)
6770

6871
from flask_debugtoolbar import DebugToolbarExtension
72+
6973
toolbar = DebugToolbarExtension()
7074
toolbar.init_app(app)
7175

7276

7377
def enable_cors(app: Flask, cors_origins: str):
7478
from flask_cors import CORS
79+
7580
cors_origins_list = cors_origins.split(",")
7681
CORS(app, resources={r"/*": {"origins": cors_origins_list}})
7782
app.logger.info("Set CORS access to [%s]" % cors_origins)
7883

7984

8085
def add_werkzeug_proxy_fix(app: Flask):
8186
from werkzeug.middleware.proxy_fix import ProxyFix
87+
8288
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
8389
app.logger.info("Add ProxyFix to serve swagger.json over https.")

time_tracker_api/api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,20 @@ def init_app(app: Flask):
126126

127127
@api.errorhandler(CosmosResourceExistsError)
128128
def handle_cosmos_resource_exists_error(error):
129+
app.logger.error(error)
129130
return {'message': 'It already exists'}, HTTPStatus.CONFLICT
130131

131132

132133
@api.errorhandler(CosmosResourceNotFoundError)
133134
@api.errorhandler(StopIteration)
134135
def handle_not_found_errors(error):
136+
app.logger.error(error)
135137
return {'message': 'It was not found'}, HTTPStatus.NOT_FOUND
136138

137139

138140
@api.errorhandler(CosmosHttpResponseError)
139141
def handle_cosmos_http_response_error(error):
142+
app.logger.error(error)
140143
return (
141144
{'message': 'Invalid request. Please verify your data.'},
142145
HTTPStatus.BAD_REQUEST,
@@ -145,6 +148,7 @@ def handle_cosmos_http_response_error(error):
145148

146149
@api.errorhandler(AttributeError)
147150
def handle_attribute_error(error):
151+
app.logger.error(error)
148152
return (
149153
{'message': "There are missing attributes"},
150154
HTTPStatus.UNPROCESSABLE_ENTITY,
@@ -153,6 +157,7 @@ def handle_attribute_error(error):
153157

154158
@api.errorhandler(CustomError)
155159
def handle_custom_error(error):
160+
app.logger.error(error)
156161
return {'message': error.description}, error.code
157162

158163

0 commit comments

Comments
 (0)