Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: TT-201 add update_last_entry and get_last_entry in reposito…
…ry, update CosmosDBQueryBuilder with order by condition
  • Loading branch information
kellycastrof committed Apr 1, 2021
commit f528a25c2cf6058727cec34a80d7d9e841d0e0cb
12 changes: 7 additions & 5 deletions tests/time_tracker_api/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def test_remove_required_constraint():
), "No attribute should be required"


def test_add_update_last_entry_flag():
from time_tracker_api.api import add_update_last_entry_flag
def test_add_update_last_entry_if_overlap():
from time_tracker_api.api import add_update_last_entry_if_overlap
from flask_restplus import fields
from flask_restplus import Namespace

Expand All @@ -76,9 +76,11 @@ def test_add_update_last_entry_flag():
},
)

new_model = add_update_last_entry_flag(sample_model)
new_model = add_update_last_entry_if_overlap(sample_model)

assert new_model is not sample_model

update_last_entry_flag = new_model.get('update_last_entry')
assert update_last_entry_flag is not None
update_last_entry_if_overlap = new_model.get(
'update_last_entry_if_overlap'
)
assert update_last_entry_if_overlap is not None
77 changes: 65 additions & 12 deletions tests/utils/query_builder_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from unittest.mock import patch
from utils.query_builder import CosmosDBQueryBuilder
from utils.query_builder import CosmosDBQueryBuilder, Order
from utils.repository import remove_white_spaces
import pytest

Expand Down Expand Up @@ -41,7 +41,9 @@ def test_add_select_conditions_should_update_select_list(
],
)
def test_add_sql_in_condition_should_update_where_list(
attribute, ids_list, expected_where_condition_list,
attribute,
ids_list,
expected_where_condition_list,
):
query_builder = CosmosDBQueryBuilder().add_sql_in_condition(
attribute, ids_list
Expand All @@ -66,7 +68,9 @@ def test_add_sql_in_condition_should_update_where_list(
],
)
def test_add_sql_where_equal_condition_should_update_where_params_list(
data, expected_where_list, expected_params,
data,
expected_where_list,
expected_params,
):
query_builder = CosmosDBQueryBuilder().add_sql_where_equal_condition(data)

Expand All @@ -91,7 +95,8 @@ def test_add_sql_where_equal_condition_with_None_should_not_update_lists():
[(True, ['NOT IS_DEFINED(c.deleted)']), (False, [])],
)
def test_add_sql_visibility_condition(
visibility_bool, expected_where_list,
visibility_bool,
expected_where_list,
):
query_builder = CosmosDBQueryBuilder().add_sql_visibility_condition(
visibility_bool
Expand All @@ -102,7 +107,12 @@ def test_add_sql_visibility_condition(


@pytest.mark.parametrize(
"limit_value,expected_limit", [(1, 1), (10, 10), (None, None),],
"limit_value,expected_limit",
[
(1, 1),
(10, 10),
(None, None),
],
)
def test_add_sql_limit_condition(limit_value, expected_limit):
query_builder = CosmosDBQueryBuilder().add_sql_limit_condition(limit_value)
Expand All @@ -111,10 +121,16 @@ def test_add_sql_limit_condition(limit_value, expected_limit):


@pytest.mark.parametrize(
"offset_value,expected_offset", [(1, 1), (10, 10), (None, None),],
"offset_value,expected_offset",
[
(1, 1),
(10, 10),
(None, None),
],
)
def test_add_sql_offset_condition(
offset_value, expected_offset,
offset_value,
expected_offset,
):
query_builder = CosmosDBQueryBuilder().add_sql_offset_condition(
offset_value
Expand All @@ -125,10 +141,15 @@ def test_add_sql_offset_condition(

@pytest.mark.parametrize(
"select_conditions,expected_condition",
[([], "*"), (["c.id"], "c.id"), (["c.id", "c.name"], "c.id,c.name"),],
[
([], "*"),
(["c.id"], "c.id"),
(["c.id", "c.name"], "c.id,c.name"),
],
)
def test__build_select_return_fields_in_select_list(
select_conditions, expected_condition,
select_conditions,
expected_condition,
):
query_builder = CosmosDBQueryBuilder().add_select_conditions(
select_conditions
Expand All @@ -148,7 +169,8 @@ def test__build_select_return_fields_in_select_list(
],
)
def test__build_where_should_return_concatenated_conditions(
fields, expected_condition,
fields,
expected_condition,
):
query_builder = CosmosDBQueryBuilder().add_sql_where_equal_condition(
fields
Expand All @@ -164,7 +186,9 @@ def test__build_where_should_return_concatenated_conditions(
[(1, "OFFSET @offset", [{'name': '@offset', 'value': 1}]), (None, "", [])],
)
def test__build_offset(
offset, expected_condition, expected_params,
offset,
expected_condition,
expected_params,
):
query_builder = CosmosDBQueryBuilder().add_sql_offset_condition(offset)

Expand All @@ -179,7 +203,9 @@ def test__build_offset(
[(1, "LIMIT @limit", [{'name': '@limit', 'value': 1}]), (None, "", [])],
)
def test__build_limit(
limit, expected_condition, expected_params,
limit,
expected_condition,
expected_params,
):
query_builder = CosmosDBQueryBuilder().add_sql_limit_condition(limit)

Expand Down Expand Up @@ -235,3 +261,30 @@ def test_build_with_empty_and_None_attributes_return_query_select_all():
assert query == expected_query
assert len(query_builder.get_parameters()) == 0
assert len(query_builder.where_conditions) == 0


def test_order_by_condition():
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
'start_date', Order.DESC
)

assert len(query_builder.get_parameters()) == 2
assert query_builder.orderBy == True


def test__build_orderBy():
query_builder = CosmosDBQueryBuilder().add_sql_order_by_condition(
'start_date', Order.DESC
)

orderBy_condition = query_builder._CosmosDBQueryBuilder__build_order_By()
expected_order_string = "ORDER BY c.@attribute @order"

assert expected_order_string == orderBy_condition

expected_params = [
{'name': '@attribute', 'value': 'start_date'},
{'name': '@order', 'value': 'DESC'},
]

assert expected_params == query_builder.get_parameters()
6 changes: 3 additions & 3 deletions time_tracker_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ def remove_required_constraint(model: Model):
return result


def add_update_last_entry_flag(time_entry_model: Model):
def add_update_last_entry_if_overlap(time_entry_model: Model):
time_entry_flag = {
'update_last_entry': fields.Boolean(
title='Update last entry',
'update_last_entry_if_overlap': fields.Boolean(
title='Update last entry if overlap',
required=False,
description='Flag that indicates if the last time entry is updated',
example=True,
Expand Down
3 changes: 3 additions & 0 deletions time_tracker_api/time_entries/time_entries_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ def create(self, data: dict):
def update(self, id, data: dict, description=None):
event_ctx = self.create_event_context("update", description)

if data.get('update_last_entry_if_overlap'):
self.repository.update_last_entry(data)

time_entry = self.repository.find(id, event_ctx)
self.check_whether_current_user_owns_item(time_entry)

Expand Down
8 changes: 7 additions & 1 deletion time_tracker_api/time_entries/time_entries_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
UUID,
NullableString,
remove_required_constraint,
update_last_entry_if_overlap,
)
from time_tracker_api.time_entries.time_entries_dao import create_dao

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


update_entry_input = update_last_entry_if_overlap(
remove_required_constraint(time_entry_input)
)


@ns.route('/<string:id>')
@ns.response(HTTPStatus.NOT_FOUND, 'This time entry does not exist')
@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format')
Expand All @@ -288,7 +294,7 @@ def get(self, id):
'A time entry already exists with this new data or there'
' is a bad reference for the project or activity',
)
@ns.expect(remove_required_constraint(time_entry_input))
@ns.expect(update_entry_input)
@ns.marshal_with(time_entry)
def put(self, id):
"""Update a time entry"""
Expand Down
40 changes: 40 additions & 0 deletions time_tracker_api/time_entries/time_entries_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from time_tracker_api.time_entries.time_entries_query_builder import (
TimeEntryQueryBuilder,
)
from utils.query_builder import CosmosDBQueryBuilder, Order
from utils.time import str_to_datetime


class TimeEntryCosmosDBRepository(CosmosDBRepository):
Expand Down Expand Up @@ -233,6 +235,44 @@ def find_all_v2(
function_mapper = self.get_mapper_or_dict(mapper)
return list(map(function_mapper, result))

def get_last_entry(self, owner_id: str, event_context: EventContext):
query_builder = (
CosmosDBQueryBuilder()
.where_conditions({'owner_id': owner_id})
.add_sql_order_by_condition('end_date', Order.DESC)
.limit(1)
.offset(1)
.build()
)

query_str = query_builder.get_query()
params = query_builder.generate_params()
result = self.container.query_items(
query=query_str,
parameters=params,
partition_key=partition_key_value,
)

function_mapper = self.get_mapper_or_dict(mapper)
return list(map(function_mapper, result))

def update_last_entry(self, data: dict, event_context: EventContext):
owner_id = data.get('owner_id')
last_entry = self.get_last_entry(owner_id, event_context)
last_entry = last_entry.pop()

end_date = str_to_datetime(last_entry.end_date)
start_date = data.get('start_date')
start_date = str_to_datetime(start_date)

if start_date < end_date:
update_date = {'end_date': start_date}
return self.partial_update(
last_entry.id, update_date, event_context
)

return False

def on_create(self, new_item_data: dict, event_context: EventContext):
CosmosDBRepository.on_create(self, new_item_data, event_context)

Expand Down
19 changes: 19 additions & 0 deletions utils/query_builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from typing import List
from utils.repository import convert_list_to_tuple_string
from enum import Enum


class Order(Enum):
DESC = 'DESC'
ASC = 'ASC'


class CosmosDBQueryBuilder:
Expand All @@ -13,6 +19,7 @@ def __init__(self):
self.where_conditions = []
self.limit = None
self.offset = None
self.orderBy = False

def add_select_conditions(self, columns: List[str] = None):
columns = columns if columns else ["*"]
Expand Down Expand Up @@ -50,6 +57,12 @@ def add_sql_offset_condition(self, offset):
self.offset = offset
return self

def add_sql_order_by_condition(self, attribute: str, order: Order):
self.orderBy = True
self.parameters.append({'name': '@attribute', 'value': attribute})
self.parameters.append({'name': '@order', 'value': order.name})
return self

def __build_select(self):
if len(self.select_conditions) < 1:
self.select_conditions.append("*")
Expand All @@ -75,15 +88,21 @@ def __build_limit(self):
else:
return ""

def __build_order_By(self):
if self.orderBy:
return "ORDER BY c.@attribute @order"

def build(self):
self.query = """
SELECT {select_conditions} FROM c
{where_conditions}
{order_by_condition}
{offset_condition}
{limit_condition}
""".format(
select_conditions=self.__build_select(),
where_conditions=self.__build_where(),
order_by_condition=self.__build_order_By(),
offset_condition=self.__build_offset(),
limit_condition=self.__build_limit(),
)
Expand Down