Skip to content

Commit c133c5d

Browse files
TT-201 validate the current entry and last entry (#273)
* refactor: TT-201 add fucntion to add update last entry flag in model * refactor: TT-201 add update_last_entry and get_last_entry in repository, update CosmosDBQueryBuilder with order by condition * test: TT-201 add test method for get_last_entry * fix: TT-201 fix import errors and function calls in time_entry_repository * fix: TT-201 fix return None in orderBy condition * fix: TT-201 missed and bad arguments and order by params not supported by cosmosdb * refactor: TT-201 delete unnecessary parameters in query_builder and its tests * refactor: TT-201 Add tests for update last entry and update TimeEntriesDao * refactor: TT-201 apply changes * refactor: TT-201 add type hinting and rename start_date_tmp Co-authored-by: kelly <[email protected]>
1 parent c1c40e6 commit c133c5d

File tree

9 files changed

+339
-21
lines changed

9 files changed

+339
-21
lines changed

tests/time_tracker_api/api_test.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,57 @@ def test_remove_required_constraint():
3030
from flask_restplus import Namespace
3131

3232
ns = Namespace('todos', description='Namespace for testing')
33-
sample_model = ns.model('Todo', {
34-
'id': fields.Integer(readonly=True, description='The task unique identifier'),
35-
'task': fields.String(required=True, description='The task details'),
36-
'done': fields.Boolean(required=False, description='Has it being done or not')
37-
})
33+
sample_model = ns.model(
34+
'Todo',
35+
{
36+
'id': fields.Integer(
37+
readonly=True, description='The task unique identifier'
38+
),
39+
'task': fields.String(
40+
required=True, description='The task details'
41+
),
42+
'done': fields.Boolean(
43+
required=False, description='Has it being done or not'
44+
),
45+
},
46+
)
3847

3948
new_model = remove_required_constraint(sample_model)
4049

4150
assert new_model is not sample_model
4251

4352
for attrib in sample_model:
44-
assert new_model[attrib].required is False, "No attribute should be required"
45-
assert new_model[attrib] is not sample_model[attrib], "No attribute should be required"
53+
assert (
54+
new_model[attrib].required is False
55+
), "No attribute should be required"
56+
assert (
57+
new_model[attrib] is not sample_model[attrib]
58+
), "No attribute should be required"
59+
60+
61+
def test_add_update_last_entry_if_overlap():
62+
from time_tracker_api.api import add_update_last_entry_if_overlap
63+
from flask_restplus import fields
64+
from flask_restplus import Namespace
65+
66+
ns = Namespace('todos', description='Namespace for testing')
67+
sample_model = ns.model(
68+
'Todo',
69+
{
70+
'id': fields.Integer(
71+
readonly=True, description='The task unique identifier'
72+
),
73+
'task': fields.String(
74+
required=True, description='The task details'
75+
),
76+
},
77+
)
78+
79+
new_model = add_update_last_entry_if_overlap(sample_model)
80+
81+
assert new_model is not sample_model
82+
83+
update_last_entry_if_overlap = new_model.get(
84+
'update_last_entry_if_overlap'
85+
)
86+
assert update_last_entry_if_overlap is not None

tests/time_tracker_api/time_entries/time_entries_model_test.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,98 @@ def test_find_all_v2(
310310
time_entry = result[0]
311311
assert isinstance(time_entry, TimeEntryCosmosDBModel)
312312
assert time_entry.__dict__ == expected_item
313+
314+
315+
@patch(
316+
'time_tracker_api.time_entries.time_entries_repository.TimeEntryCosmosDBRepository.find_partition_key_value'
317+
)
318+
def test_get_last_entry(
319+
find_partition_key_value_mock,
320+
event_context: EventContext,
321+
time_entry_repository: TimeEntryCosmosDBRepository,
322+
):
323+
expected_item = {
324+
'id': 'id',
325+
'start_date': '2021-03-22T10:00:00.000Z',
326+
'end_date': "2021-03-22T11:00:00.000Z",
327+
'description': 'do some testing',
328+
'tenant_id': 'tenant_id',
329+
'project_id': 'project_id',
330+
'activity_id': 'activity_id',
331+
'technologies': ['python'],
332+
}
333+
query_items_mock = Mock(return_value=iter([expected_item]))
334+
time_entry_repository.container = Mock()
335+
time_entry_repository.container.query_items = query_items_mock
336+
337+
time_entry = time_entry_repository.get_last_entry('id1', event_context)
338+
339+
find_partition_key_value_mock.assert_called_once()
340+
assert isinstance(time_entry, TimeEntryCosmosDBModel)
341+
assert time_entry.__dict__ == expected_item
342+
343+
344+
expected_item = {
345+
'id': 'id',
346+
'owner_id': '1',
347+
'start_date': '2021-03-22T10:00:00.000Z',
348+
'end_date': "2021-03-22T11:00:00.000Z",
349+
'description': 'do some testing',
350+
'tenant_id': 'tenant_id',
351+
'project_id': 'project_id',
352+
'activity_id': 'activity_id',
353+
'technologies': ['python'],
354+
}
355+
356+
running_item = {
357+
'id': 'id',
358+
'owner_id': '1',
359+
'update_last_entry_if_overlap': True,
360+
'start_date': '2021-03-22T10:30:00.000Z',
361+
'end_date': '2021-03-22T11:30:00.000Z',
362+
'description': 'do some testing',
363+
'tenant_id': 'tenant_id',
364+
'project_id': 'project_id',
365+
'activity_id': 'activity_id',
366+
'technologies': ['python'],
367+
}
368+
369+
last_item_update = {
370+
'id': 'id',
371+
'owner_id': '1',
372+
'start_date': '2021-03-22T10:00:00.000Z',
373+
'end_date': "2021-03-22T10:30:00.000Z",
374+
'description': 'do some testing',
375+
'tenant_id': 'tenant_id',
376+
'project_id': 'project_id',
377+
'activity_id': 'activity_id',
378+
'technologies': ['python'],
379+
}
380+
381+
382+
@pytest.mark.parametrize(
383+
"expected_item, running_item, last_item_update",
384+
[(expected_item, running_item, last_item_update)],
385+
)
386+
def test_update_last_entry(
387+
event_context: EventContext,
388+
time_entry_repository: TimeEntryCosmosDBRepository,
389+
expected_item,
390+
running_item,
391+
last_item_update,
392+
):
393+
query_items_mock = Mock(return_value=iter([expected_item]))
394+
time_entry_repository.container = Mock()
395+
time_entry_repository.container.query_items = query_items_mock
396+
397+
partial_update_mock = Mock(return_value=[last_item_update])
398+
time_entry_repository.partial_update = partial_update_mock
399+
400+
time_entry_repository.update_last_entry(
401+
running_item.get('owner_id'),
402+
running_item.get('start_date'),
403+
event_context,
404+
)
405+
406+
partial_update_mock.assert_called_once()
407+
query_items_mock.assert_called_once()

tests/time_tracker_api/time_entries/time_entries_namespace_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,3 +881,32 @@ def test_paginated_sends_max_count_and_offset_on_call_to_repository(
881881
_, kwargs = time_entries_dao.repository.find_all.call_args
882882
assert 'max_count' in kwargs and kwargs['max_count'] is not None
883883
assert 'offset' in kwargs and kwargs['offset'] is not None
884+
885+
886+
def test_update_time_entry_calls_update_last_entry(
887+
client: FlaskClient,
888+
mocker: MockFixture,
889+
valid_header: dict,
890+
valid_id: str,
891+
time_entries_dao,
892+
):
893+
time_entries_dao.repository.partial_update = Mock(return_value={})
894+
time_entries_dao.repository.find = Mock(return_value={})
895+
time_entries_dao.check_whether_current_user_owns_item = Mock()
896+
time_entries_dao.repository.update_last_entry = Mock(return_value={})
897+
898+
update_time_entry = valid_time_entry_input.copy()
899+
update_time_entry['update_last_entry_if_overlap'] = True
900+
901+
response = client.put(
902+
f'/time-entries/{valid_id}',
903+
headers=valid_header,
904+
json=update_time_entry,
905+
follow_redirects=True,
906+
)
907+
908+
assert HTTPStatus.OK == response.status_code
909+
time_entries_dao.repository.partial_update.assert_called_once()
910+
time_entries_dao.repository.find.assert_called_once()
911+
time_entries_dao.check_whether_current_user_owns_item.assert_called_once()
912+
time_entries_dao.repository.update_last_entry.assert_called_once()

tests/utils/query_builder_test.py

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from unittest.mock import patch
2-
from utils.query_builder import CosmosDBQueryBuilder
2+
from utils.query_builder import CosmosDBQueryBuilder, Order
33
from utils.repository import remove_white_spaces
44
import pytest
55

@@ -41,7 +41,9 @@ def test_add_select_conditions_should_update_select_list(
4141
],
4242
)
4343
def test_add_sql_in_condition_should_update_where_list(
44-
attribute, ids_list, expected_where_condition_list,
44+
attribute,
45+
ids_list,
46+
expected_where_condition_list,
4547
):
4648
query_builder = CosmosDBQueryBuilder().add_sql_in_condition(
4749
attribute, ids_list
@@ -66,7 +68,9 @@ def test_add_sql_in_condition_should_update_where_list(
6668
],
6769
)
6870
def test_add_sql_where_equal_condition_should_update_where_params_list(
69-
data, expected_where_list, expected_params,
71+
data,
72+
expected_where_list,
73+
expected_params,
7074
):
7175
query_builder = CosmosDBQueryBuilder().add_sql_where_equal_condition(data)
7276

@@ -91,7 +95,8 @@ def test_add_sql_where_equal_condition_with_None_should_not_update_lists():
9195
[(True, ['NOT IS_DEFINED(c.deleted)']), (False, [])],
9296
)
9397
def test_add_sql_visibility_condition(
94-
visibility_bool, expected_where_list,
98+
visibility_bool,
99+
expected_where_list,
95100
):
96101
query_builder = CosmosDBQueryBuilder().add_sql_visibility_condition(
97102
visibility_bool
@@ -102,7 +107,12 @@ def test_add_sql_visibility_condition(
102107

103108

104109
@pytest.mark.parametrize(
105-
"limit_value,expected_limit", [(1, 1), (10, 10), (None, None),],
110+
"limit_value,expected_limit",
111+
[
112+
(1, 1),
113+
(10, 10),
114+
(None, None),
115+
],
106116
)
107117
def test_add_sql_limit_condition(limit_value, expected_limit):
108118
query_builder = CosmosDBQueryBuilder().add_sql_limit_condition(limit_value)
@@ -111,10 +121,16 @@ def test_add_sql_limit_condition(limit_value, expected_limit):
111121

112122

113123
@pytest.mark.parametrize(
114-
"offset_value,expected_offset", [(1, 1), (10, 10), (None, None),],
124+
"offset_value,expected_offset",
125+
[
126+
(1, 1),
127+
(10, 10),
128+
(None, None),
129+
],
115130
)
116131
def test_add_sql_offset_condition(
117-
offset_value, expected_offset,
132+
offset_value,
133+
expected_offset,
118134
):
119135
query_builder = CosmosDBQueryBuilder().add_sql_offset_condition(
120136
offset_value
@@ -125,10 +141,15 @@ def test_add_sql_offset_condition(
125141

126142
@pytest.mark.parametrize(
127143
"select_conditions,expected_condition",
128-
[([], "*"), (["c.id"], "c.id"), (["c.id", "c.name"], "c.id,c.name"),],
144+
[
145+
([], "*"),
146+
(["c.id"], "c.id"),
147+
(["c.id", "c.name"], "c.id,c.name"),
148+
],
129149
)
130150
def test__build_select_return_fields_in_select_list(
131-
select_conditions, expected_condition,
151+
select_conditions,
152+
expected_condition,
132153
):
133154
query_builder = CosmosDBQueryBuilder().add_select_conditions(
134155
select_conditions
@@ -148,7 +169,8 @@ def test__build_select_return_fields_in_select_list(
148169
],
149170
)
150171
def test__build_where_should_return_concatenated_conditions(
151-
fields, expected_condition,
172+
fields,
173+
expected_condition,
152174
):
153175
query_builder = CosmosDBQueryBuilder().add_sql_where_equal_condition(
154176
fields
@@ -164,7 +186,9 @@ def test__build_where_should_return_concatenated_conditions(
164186
[(1, "OFFSET @offset", [{'name': '@offset', 'value': 1}]), (None, "", [])],
165187
)
166188
def test__build_offset(
167-
offset, expected_condition, expected_params,
189+
offset,
190+
expected_condition,
191+
expected_params,
168192
):
169193
query_builder = CosmosDBQueryBuilder().add_sql_offset_condition(offset)
170194

@@ -179,7 +203,9 @@ def test__build_offset(
179203
[(1, "LIMIT @limit", [{'name': '@limit', 'value': 1}]), (None, "", [])],
180204
)
181205
def test__build_limit(
182-
limit, expected_condition, expected_params,
206+
limit,
207+
expected_condition,
208+
expected_params,
183209
):
184210
query_builder = CosmosDBQueryBuilder().add_sql_limit_condition(limit)
185211

@@ -235,3 +261,44 @@ def test_build_with_empty_and_None_attributes_return_query_select_all():
235261
assert query == expected_query
236262
assert len(query_builder.get_parameters()) == 0
237263
assert len(query_builder.where_conditions) == 0
264+
265+
266+
@pytest.mark.parametrize(
267+
"attribute,order,expected_order_by",
268+
[
269+
('start_date', Order.DESC, ('start_date', 'DESC')),
270+
('start_date', Order.ASC, ('start_date', 'ASC')),
271+
],
272+
)
273+
def test_add_sql_order_by_condition(
274+
attribute,
275+
order,
276+
expected_order_by,
277+
):
278+
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
279+
attribute, order
280+
)
281+
282+
assert len(query_builder.order_by) == 2
283+
assert query_builder.order_by == expected_order_by
284+
285+
286+
@pytest.mark.parametrize(
287+
"attribute,order,expected_order_by_condition",
288+
[
289+
('start_date', Order.DESC, "ORDER BY c.start_date DESC"),
290+
('start_date', Order.ASC, "ORDER BY c.start_date ASC"),
291+
],
292+
)
293+
def test__build_order_by(
294+
attribute,
295+
order,
296+
expected_order_by_condition,
297+
):
298+
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
299+
attribute, order
300+
)
301+
302+
orderBy_condition = query_builder._CosmosDBQueryBuilder__build_order_by()
303+
304+
assert orderBy_condition == expected_order_by_condition

time_tracker_api/api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ def remove_required_constraint(model: Model):
3434
return result
3535

3636

37+
def add_update_last_entry_if_overlap(time_entry_model: Model):
38+
time_entry_flag = {
39+
'update_last_entry_if_overlap': fields.Boolean(
40+
title='Update last entry if overlap',
41+
required=False,
42+
description='Flag that indicates if the last time entry is updated',
43+
example=True,
44+
)
45+
}
46+
new_model = time_entry_model.clone('TimeEntryInput', time_entry_flag)
47+
return new_model
48+
49+
3750
def create_attributes_filter(
3851
ns: namespace, model: Model, filter_attrib_names: list
3952
) -> RequestParser:

time_tracker_api/time_entries/time_entries_dao.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,14 @@ def create(self, data: dict):
225225

226226
def update(self, id, data: dict, description=None):
227227
event_ctx = self.create_event_context("update", description)
228-
229228
time_entry = self.repository.find(id, event_ctx)
230229
self.check_whether_current_user_owns_item(time_entry)
231230

231+
if data.get('update_last_entry_if_overlap', None):
232+
self.repository.update_last_entry(
233+
data.get('owner_id'), data.get('start_date'), event_ctx
234+
)
235+
232236
return self.repository.partial_update(
233237
id,
234238
data,

0 commit comments

Comments
 (0)