Skip to content

Commit f659963

Browse files
author
EliuX
committed
feat: Close #89 Add endpoint to get running time entry
1 parent 7beccfa commit f659963

File tree

6 files changed

+98
-5
lines changed

6 files changed

+98
-5
lines changed

tests/conftest.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from flask import Flask
44
from flask.testing import FlaskClient
55

6-
from commons.data_access_layer.cosmos_db import CosmosDBRepository
6+
from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime
77
from time_tracker_api import create_app
88
from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBRepository
99

@@ -128,6 +128,23 @@ def another_item(cosmos_db_repository: CosmosDBRepository, tenant_id: str) -> di
128128
return cosmos_db_repository.create(sample_item_data)
129129

130130

131-
@pytest.yield_fixture(scope="module")
131+
@pytest.fixture(scope="module")
132132
def time_entry_repository() -> TimeEntryCosmosDBRepository:
133133
return TimeEntryCosmosDBRepository()
134+
135+
136+
@pytest.yield_fixture(scope="module")
137+
def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository,
138+
owner_id: str,
139+
tenant_id: str):
140+
created_time_entry = time_entry_repository.create({
141+
"project_id": fake.uuid4(),
142+
"start_date": datetime_str(current_datetime()),
143+
"owner_id": owner_id,
144+
"tenant_id": tenant_id
145+
})
146+
147+
yield created_time_entry
148+
149+
time_entry_repository.delete(id=created_time_entry.id,
150+
partition_key_value=tenant_id)

tests/time_tracker_api/time_entries/time_entries_model_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,19 @@ def test_find_interception_should_ignore_id_of_existing_item(owner_id: str,
7878
assert not any([existing_item.id == item.id for item in non_colliding_result])
7979
finally:
8080
time_entry_repository.delete_permanently(existing_item.id, partition_key_value=existing_item.tenant_id)
81+
82+
83+
def test_find_running_should_return_running_time_entry(running_time_entry,
84+
time_entry_repository: TimeEntryCosmosDBRepository):
85+
found_time_entry = time_entry_repository.find_running(partition_key_value=running_time_entry.tenant_id)
86+
87+
assert found_time_entry is not None
88+
assert found_time_entry.id == running_time_entry.id
89+
90+
91+
def test_find_running_should_not_find_any_item(tenant_id: str,
92+
time_entry_repository: TimeEntryCosmosDBRepository):
93+
try:
94+
time_entry_repository.find_running(partition_key_value=tenant_id)
95+
except Exception as e:
96+
assert type(e) is StopIteration

tests/time_tracker_api/time_entries/time_entries_namespace_test.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
fake_time_entry = ({
2626
"id": fake.random_int(1, 9999),
2727
"running": True,
28-
}).update(valid_time_entry_input)
28+
})
29+
fake_time_entry.update(valid_time_entry_input)
2930

3031

3132
def test_create_time_entry_with_invalid_date_range_should_raise_bad_request_error(client: FlaskClient,
@@ -309,3 +310,29 @@ def test_restart_time_entry_with_id_with_invalid_format(client: FlaskClient, moc
309310
changes={"end_date": None},
310311
partition_key_value=current_user_tenant_id(),
311312
peeker=ANY)
313+
314+
315+
def test_get_running_should_call_find_running(client: FlaskClient, mocker: MockFixture):
316+
from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao
317+
repository_update_mock = mocker.patch.object(time_entries_dao.repository,
318+
'find_running',
319+
return_value=fake_time_entry)
320+
321+
response = client.get("/time-entries/running", follow_redirects=True)
322+
323+
assert HTTPStatus.OK == response.status_code
324+
assert json.loads(response.data) is not None
325+
repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id())
326+
327+
328+
def test_get_running_should_return_not_found_if_find_running_throws_StopIteration(client: FlaskClient,
329+
mocker: MockFixture):
330+
from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao
331+
repository_update_mock = mocker.patch.object(time_entries_dao.repository,
332+
'find_running',
333+
side_effect=StopIteration)
334+
335+
response = client.get("/time-entries/running", follow_redirects=True)
336+
337+
assert HTTPStatus.NOT_FOUND == response.status_code
338+
repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id())

time_tracker_api/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def handle_cosmos_resource_exists_error(error):
7777

7878

7979
@api.errorhandler(CosmosResourceNotFoundError)
80-
def handle_cosmos_resource_not_found_error(error):
80+
@api.errorhandler(StopIteration)
81+
def handle_not_found_errors(error):
8182
return {'message': 'It was not found'}, HTTPStatus.NOT_FOUND
8283

8384

time_tracker_api/time_entries/time_entries_model.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
from dataclasses import dataclass, field
23
from typing import List, Callable
34

@@ -15,6 +16,10 @@ class TimeEntriesDao(CRUDDao):
1516
def current_user_id():
1617
return current_user_id()
1718

19+
@abc.abstractmethod
20+
def find_running(self):
21+
pass
22+
1823

1924
container_definition = {
2025
'id': 'time_entry',
@@ -102,6 +107,19 @@ def find_interception_with_date_range(self, start_date, end_date, owner_id, part
102107
function_mapper = self.get_mapper_or_dict(mapper)
103108
return list(map(function_mapper, result))
104109

110+
def find_running(self, partition_key_value: str, mapper: Callable = None):
111+
result = self.container.query_items(
112+
query="""
113+
SELECT * from c
114+
WHERE NOT IS_DEFINED(c.end_date) OR c.end_date = null
115+
OFFSET 0 LIMIT 1
116+
""",
117+
partition_key=partition_key_value,
118+
max_item_count=1)
119+
120+
function_mapper = self.get_mapper_or_dict(mapper)
121+
return function_mapper(next(result))
122+
105123
def validate_data(self, data):
106124
if data.get('end_date') is not None:
107125
if data['end_date'] <= data.get('start_date'):
@@ -156,6 +174,9 @@ def delete(self, id):
156174
self.repository.delete(id, partition_key_value=self.partition_key_value,
157175
peeker=self.check_whether_current_user_owns_item)
158176

177+
def find_running(self):
178+
return self.repository.find_running(partition_key_value=self.partition_key_value)
179+
159180

160181
def create_dao() -> TimeEntriesDao:
161182
repository = TimeEntryCosmosDBRepository()

time_tracker_api/time_entries/time_entries_namespace.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def post(self, id):
171171

172172
@ns.route('/<string:id>/restart')
173173
@ns.response(HTTPStatus.NOT_FOUND, 'Stopped time entry not found')
174-
@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format.')
174+
@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format')
175175
@ns.param('id', 'The unique identifier of a stopped time entry')
176176
class RestartTimeEntry(Resource):
177177
@ns.doc('restart_time_entry')
@@ -181,3 +181,14 @@ def post(self, id):
181181
return time_entries_dao.update(id, {
182182
'end_date': None
183183
})
184+
185+
186+
@ns.route('/running')
187+
@ns.response(HTTPStatus.OK, 'The time entry that is active: currently running')
188+
@ns.response(HTTPStatus.NOT_FOUND, 'There is no time entry running right now')
189+
class ActiveTimeEntry(Resource):
190+
@ns.doc('running_time_entry')
191+
@ns.marshal_with(time_entry)
192+
def get(self):
193+
"""Find the time entry that is running"""
194+
return time_entries_dao.find_running()

0 commit comments

Comments
 (0)