Skip to content

Commit 450a71d

Browse files
authored
Merge pull request #117 from ioet/feature/get-time-entries-per-month-and-year#106
2 parents 682cd4b + ff4a4d6 commit 450a71d

File tree

5 files changed

+152
-12
lines changed

5 files changed

+152
-12
lines changed

commons/data_access_layer/cosmos_db.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import uuid
44
from datetime import datetime
5-
from typing import Callable
5+
from typing import Callable, List, Dict
66

77
import azure.cosmos.cosmos_client as cosmos_client
88
import azure.cosmos.exceptions as exceptions
@@ -116,7 +116,15 @@ def create_sql_where_conditions(conditions: dict, container_name='c') -> str:
116116
return ""
117117

118118
@staticmethod
119-
def generate_condition_values(conditions: dict) -> dict:
119+
def create_custom_sql_conditions(custom_sql_conditions: List[str]) -> str:
120+
if len(custom_sql_conditions) > 0:
121+
return "AND {custom_sql_conditions_clause}".format(
122+
custom_sql_conditions_clause=" AND ".join(custom_sql_conditions))
123+
else:
124+
return ''
125+
126+
@staticmethod
127+
def generate_params(conditions: dict) -> dict:
120128
result = []
121129
for k, v in conditions.items():
122130
result.append({
@@ -166,23 +174,25 @@ def find(self, id: str, event_context: EventContext, peeker: 'function' = None,
166174
function_mapper = self.get_mapper_or_dict(mapper)
167175
return function_mapper(self.check_visibility(found_item, visible_only))
168176

169-
def find_all(self, event_context: EventContext, conditions: dict = {}, max_count=None,
170-
offset=0, visible_only=True, mapper: Callable = None):
177+
def find_all(self, event_context: EventContext, conditions: dict = {}, custom_sql_conditions: List[str] = [],
178+
custom_params: dict = {}, max_count=None, offset=0, visible_only=True, mapper: Callable = None):
171179
partition_key_value = self.find_partition_key_value(event_context)
172180
max_count = self.get_page_size_or(max_count)
173181
params = [
174182
{"name": "@partition_key_value", "value": partition_key_value},
175183
{"name": "@offset", "value": offset},
176184
{"name": "@max_count", "value": max_count},
177185
]
178-
params.extend(self.generate_condition_values(conditions))
186+
params.extend(self.generate_params(conditions))
187+
params.extend(custom_params)
179188
result = self.container.query_items(query="""
180189
SELECT * FROM c WHERE c.{partition_key_attribute}=@partition_key_value
181-
{conditions_clause} {visibility_condition} {order_clause}
190+
{conditions_clause} {visibility_condition} {custom_sql_conditions_clause} {order_clause}
182191
OFFSET @offset LIMIT @max_count
183192
""".format(partition_key_attribute=self.partition_key_attribute,
184193
visibility_condition=self.create_sql_condition_for_visibility(visible_only),
185194
conditions_clause=self.create_sql_where_conditions(conditions),
195+
custom_sql_conditions_clause=self.create_custom_sql_conditions(custom_sql_conditions),
186196
order_clause=self.create_sql_order_clause()),
187197
parameters=params,
188198
partition_key=partition_key_value,
@@ -298,3 +308,40 @@ def generate_uuid4() -> str:
298308
def init_app(app: Flask) -> None:
299309
global cosmos_helper
300310
cosmos_helper = CosmosDBFacade.from_flask_config(app)
311+
312+
313+
def get_last_day_of_month(year: int, month: int) -> int:
314+
from calendar import monthrange
315+
return monthrange(year=year, month=month)[1]
316+
317+
318+
def get_current_year() -> int:
319+
return datetime.now().year
320+
321+
322+
def get_current_month() -> int:
323+
return datetime.now().month
324+
325+
326+
def get_date_range_of_month(
327+
year: int,
328+
month: int
329+
) -> Dict[str, str]:
330+
first_day_of_month = 1
331+
start_date = datetime(year=year, month=month, day=first_day_of_month)
332+
333+
last_day_of_month = get_last_day_of_month(year=year, month=month)
334+
end_date = datetime(
335+
year=year,
336+
month=month,
337+
day=last_day_of_month,
338+
hour=23,
339+
minute=59,
340+
second=59,
341+
microsecond=999999
342+
)
343+
344+
return {
345+
'start_date': datetime_str(start_date),
346+
'end_date': datetime_str(end_date)
347+
}

tests/commons/data_access_layer/cosmos_db_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ def test_repository_create_sql_where_conditions_with_no_values(cosmos_db_reposit
555555

556556

557557
def test_repository_append_conditions_values(cosmos_db_repository: CosmosDBRepository):
558-
result = cosmos_db_repository.generate_condition_values({'owner_id': 'mark', 'customer_id': 'ioet'})
558+
result = cosmos_db_repository.generate_params({'owner_id': 'mark', 'customer_id': 'ioet'})
559559

560560
assert result is not None
561561
assert result == [{'name': '@owner_id', 'value': 'mark'},

tests/time_tracker_api/time_entries/time_entries_namespace_test.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from flask_restplus._http import HTTPStatus
88
from pytest_mock import MockFixture, pytest
99

10-
from commons.data_access_layer.cosmos_db import current_datetime, current_datetime_str
10+
from commons.data_access_layer.cosmos_db import current_datetime, \
11+
current_datetime_str, get_date_range_of_month, get_current_month, \
12+
get_current_year, datetime_str
13+
from commons.data_access_layer.database import EventContext
1114
from time_tracker_api.time_entries.time_entries_model import TimeEntriesCosmosDBDao
1215

1316
fake = Faker()
@@ -434,3 +437,37 @@ def test_create_with_valid_uuid_format_should_return_created(client: FlaskClient
434437

435438
assert HTTPStatus.CREATED == response.status_code
436439
repository_container_create_item_mock.assert_called()
440+
441+
442+
@pytest.mark.parametrize(
443+
'url,month,year',
444+
[
445+
('/time-entries?month=4&year=2020', 4, 2020),
446+
('/time-entries?month=4', 4, get_current_year()),
447+
('/time-entries', get_current_month(), get_current_year())
448+
]
449+
)
450+
def test_find_all_is_called_with_generated_dates(client: FlaskClient,
451+
mocker: MockFixture,
452+
valid_header: dict,
453+
owner_id: str,
454+
url: str,
455+
month: int,
456+
year: int):
457+
from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao
458+
repository_find_all_mock = mocker.patch.object(time_entries_dao.repository,
459+
'find_all',
460+
return_value=fake_time_entry)
461+
462+
response = client.get(url, headers=valid_header, follow_redirects=True)
463+
464+
date_range = get_date_range_of_month(year, month)
465+
conditions = {
466+
'owner_id': owner_id
467+
}
468+
469+
assert HTTPStatus.OK == response.status_code
470+
assert json.loads(response.data) is not None
471+
repository_find_all_mock.assert_called_once_with(ANY,
472+
conditions=conditions,
473+
date_range=date_range)

time_tracker_api/time_entries/time_entries_model.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from flask_restplus._http import HTTPStatus
77

88
from commons.data_access_layer.cosmos_db import CosmosDBDao, CosmosDBRepository, CustomError, current_datetime_str, \
9-
CosmosDBModel
9+
CosmosDBModel, get_date_range_of_month, get_current_year, get_current_month
1010
from commons.data_access_layer.database import EventContext
1111
from time_tracker_api.database import CRUDDao, APICosmosDBDao
1212
from time_tracker_api.security import current_user_id
@@ -83,6 +83,32 @@ def create_sql_ignore_id_condition(id: str):
8383
else:
8484
return "AND c.id!=@ignore_id"
8585

86+
@staticmethod
87+
def create_sql_date_range_filter(date_range: dict) -> str:
88+
if 'start_date' and 'end_date' in date_range:
89+
return """
90+
((c.start_date BETWEEN @start_date AND @end_date) OR
91+
(c.end_date BETWEEN @start_date AND @end_date))
92+
"""
93+
else:
94+
return ''
95+
96+
def find_all(self, event_context: EventContext, conditions: dict, date_range: dict):
97+
custom_sql_conditions = []
98+
custom_sql_conditions.append(
99+
self.create_sql_date_range_filter(date_range)
100+
)
101+
102+
custom_params = self.generate_params(date_range)
103+
104+
return CosmosDBRepository.find_all(
105+
self,
106+
event_context=event_context,
107+
conditions=conditions,
108+
custom_sql_conditions=custom_sql_conditions,
109+
custom_params=custom_params
110+
)
111+
86112
def on_create(self, new_item_data: dict, event_context: EventContext):
87113
CosmosDBRepository.on_create(self, new_item_data, event_context)
88114

@@ -107,7 +133,7 @@ def find_interception_with_date_range(self, start_date, end_date, owner_id, tena
107133
{"name": "@end_date", "value": end_date or current_datetime_str()},
108134
{"name": "@ignore_id", "value": ignore_id},
109135
]
110-
params.extend(self.generate_condition_values(conditions))
136+
params.extend(self.generate_params(conditions))
111137
result = self.container.query_items(
112138
query="""
113139
SELECT * FROM c WHERE ((c.start_date BETWEEN @start_date AND @end_date)
@@ -138,7 +164,7 @@ def find_running(self, tenant_id: str, owner_id: str, mapper: Callable = None):
138164
visibility_condition=self.create_sql_condition_for_visibility(True),
139165
conditions_clause=self.create_sql_where_conditions(conditions),
140166
),
141-
parameters=self.generate_condition_values(conditions),
167+
parameters=self.generate_params(conditions),
142168
partition_key=tenant_id,
143169
max_item_count=1)
144170

@@ -194,7 +220,11 @@ def checks_owner_and_is_not_started(cls, data: dict):
194220
def get_all(self, conditions: dict = {}) -> list:
195221
event_ctx = self.create_event_context("read-many")
196222
conditions.update({"owner_id": event_ctx.user_id})
197-
return self.repository.find_all(event_ctx, conditions=conditions)
223+
224+
date_range = self.handle_date_filter_args(args=conditions)
225+
return self.repository.find_all(event_ctx,
226+
conditions=conditions,
227+
date_range=date_range)
198228

199229
def get(self, id):
200230
event_ctx = self.create_event_context("read")
@@ -229,6 +259,22 @@ def find_running(self):
229259
event_ctx = self.create_event_context("find_running")
230260
return self.repository.find_running(event_ctx.tenant_id, event_ctx.user_id)
231261

262+
@staticmethod
263+
def handle_date_filter_args(args: dict) -> dict:
264+
if 'month' and 'year' in args:
265+
month = int(args.get("month"))
266+
year = int(args.get("year"))
267+
args.pop('month')
268+
args.pop('year')
269+
elif 'month' in args:
270+
month = int(args.get("month"))
271+
year = get_current_year()
272+
args.pop('month')
273+
else:
274+
month = get_current_month()
275+
year = get_current_year()
276+
return get_date_range_of_month(year, month)
277+
232278

233279
def create_dao() -> TimeEntriesDao:
234280
repository = TimeEntryCosmosDBRepository()

time_tracker_api/time_entries/time_entries_namespace.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,20 @@
108108
"uri",
109109
])
110110

111+
# custom attributes filter
112+
attributes_filter.add_argument('month', required=False,
113+
store_missing=False,
114+
help="(Filter) Month to filter by",
115+
location='args')
116+
attributes_filter.add_argument('year', required=False,
117+
store_missing=False,
118+
help="(Filter) Year to filter by",
119+
location='args')
111120

112121
@ns.route('')
113122
class TimeEntries(Resource):
114123
@ns.doc('list_time_entries')
124+
@ns.expect(attributes_filter)
115125
@ns.marshal_list_with(time_entry)
116126
def get(self):
117127
"""List all time entries"""

0 commit comments

Comments
 (0)