Skip to content

Commit a408915

Browse files
O
Merge branch 'TT-201-validate-the-current-entry-and-last-entry' of github.com:ioet/time-tracker-backend into TT-201-validate-the-current-entry-and-last-entry
2 parents 0d8520e + 8d34667 commit a408915

File tree

8 files changed

+228
-20
lines changed

8 files changed

+228
-20
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,34 @@ 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=[expected_item])
334+
time_entry_repository.container = Mock()
335+
time_entry_repository.container.query_items = query_items_mock
336+
337+
result = time_entry_repository.get_last_entry('id1', event_context)
338+
find_partition_key_value_mock.assert_called_once()
339+
340+
assert len(result) == 1
341+
time_entry = result.pop()
342+
assert isinstance(time_entry, TimeEntryCosmosDBModel)
343+
assert time_entry.__dict__ == expected_item

tests/utils/query_builder_test.py

Lines changed: 58 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,23 @@ 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+
def test_order_by_condition():
267+
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
268+
'start_date', Order.DESC
269+
)
270+
271+
assert len(query_builder.order_by) == 2
272+
assert query_builder.order_by == ('start_date', 'DESC')
273+
274+
275+
def test__build_orderBy():
276+
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
277+
'start_date', Order.DESC
278+
)
279+
280+
orderBy_condition = query_builder._CosmosDBQueryBuilder__build_order_By()
281+
expected_order_string = "ORDER BY c.start_date DESC"
282+
283+
assert expected_order_string == orderBy_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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ def create(self, data: dict):
226226
def update(self, id, data: dict, description=None):
227227
event_ctx = self.create_event_context("update", description)
228228

229+
if data.get('update_last_entry_if_overlap'):
230+
self.repository.update_last_entry(data, event_ctx)
231+
229232
time_entry = self.repository.find(id, event_ctx)
230233
self.check_whether_current_user_owns_item(time_entry)
231234

time_tracker_api/time_entries/time_entries_namespace.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
UUID,
1616
NullableString,
1717
remove_required_constraint,
18+
add_update_last_entry_if_overlap,
1819
)
1920
from time_tracker_api.time_entries.time_entries_dao import create_dao
2021

@@ -267,6 +268,11 @@ def get(self):
267268
return time_entries_dao.get_lastest_entries_by_project(conditions={})
268269

269270

271+
update_entry_input = add_update_last_entry_if_overlap(
272+
remove_required_constraint(time_entry_input)
273+
)
274+
275+
270276
@ns.route('/<string:id>')
271277
@ns.response(HTTPStatus.NOT_FOUND, 'This time entry does not exist')
272278
@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format')
@@ -288,7 +294,7 @@ def get(self, id):
288294
'A time entry already exists with this new data or there'
289295
' is a bad reference for the project or activity',
290296
)
291-
@ns.expect(remove_required_constraint(time_entry_input))
297+
@ns.expect(update_entry_input)
292298
@ns.marshal_with(time_entry)
293299
def put(self, id):
294300
"""Update a time entry"""

time_tracker_api/time_entries/time_entries_repository.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from time_tracker_api.time_entries.time_entries_query_builder import (
2828
TimeEntryQueryBuilder,
2929
)
30+
from utils.query_builder import CosmosDBQueryBuilder, Order
31+
from utils.time import str_to_datetime
3032

3133

3234
class TimeEntryCosmosDBRepository(CosmosDBRepository):
@@ -233,6 +235,52 @@ def find_all_v2(
233235
function_mapper = self.get_mapper_or_dict(mapper)
234236
return list(map(function_mapper, result))
235237

238+
def get_last_entry(
239+
self,
240+
owner_id: str,
241+
event_context: EventContext,
242+
visible_only=True,
243+
mapper: Callable = None,
244+
):
245+
query_builder = (
246+
CosmosDBQueryBuilder()
247+
.add_sql_where_equal_condition({'owner_id': owner_id})
248+
.add_sql_order_by_condition('end_date', Order.DESC)
249+
.add_sql_limit_condition(1)
250+
.add_sql_offset_condition(1)
251+
.build()
252+
)
253+
254+
query_str = query_builder.get_query()
255+
params = query_builder.get_parameters()
256+
257+
partition_key_value = self.find_partition_key_value(event_context)
258+
result = self.container.query_items(
259+
query=query_str,
260+
parameters=params,
261+
partition_key=partition_key_value,
262+
)
263+
264+
function_mapper = self.get_mapper_or_dict(mapper)
265+
return list(map(function_mapper, result))
266+
267+
def update_last_entry(self, data: dict, event_context: EventContext):
268+
owner_id = data.get('owner_id')
269+
last_entry = self.get_last_entry(owner_id, event_context)
270+
last_entry = last_entry.pop()
271+
272+
end_date = str_to_datetime(last_entry.end_date)
273+
start_date = data.get('start_date')
274+
start_date = str_to_datetime(start_date)
275+
276+
if start_date < end_date:
277+
update_date = {'end_date': data.get('start_date')}
278+
return self.partial_update(
279+
last_entry.id, update_date, event_context
280+
)
281+
282+
return False
283+
236284
def on_create(self, new_item_data: dict, event_context: EventContext):
237285
CosmosDBRepository.on_create(self, new_item_data, event_context)
238286

0 commit comments

Comments
 (0)