Skip to content

Commit 6f2a211

Browse files
authored
refactor: use individual API for single-task hard delete in TUI (#806)
RemoveTaskUseCase and the DELETE endpoint now return TaskOperationOutput, consistent with all other single-task operations. This allows HardDeleteCommand to implement execute_single() so that deleting one task uses the individual API instead of the bulk endpoint. Changes: - core: RemoveTaskUseCase returns TaskOperationOutput (captured before deletion) - core: TaskCrudController.remove_task returns TaskOperationOutput - server: DELETE /api/v1/tasks/{id} returns TaskOperationResponse (200 instead of 204), removes redundant pre-fetch of task data - client: remove_task returns TaskOperationOutput - TUI: HardDeleteCommand adds execute_single for individual API
1 parent 56296f8 commit 6f2a211

10 files changed

Lines changed: 63 additions & 49 deletions

File tree

packages/taskdog-client/src/taskdog_client/task_client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,17 @@ def restore_task(self, task_id: int) -> TaskOperationOutput:
201201
"""
202202
return self._base.lifecycle_operation(task_id, "restore")
203203

204-
def remove_task(self, task_id: int) -> None:
204+
def remove_task(self, task_id: int) -> TaskOperationOutput:
205205
"""Permanently delete a task.
206206
207207
Args:
208208
task_id: Task ID
209209
210+
Returns:
211+
TaskOperationOutput with deleted task data
212+
210213
Raises:
211214
TaskNotFoundException: If task not found
212215
"""
213-
response = self._base._safe_request("delete", f"/api/v1/tasks/{task_id}")
214-
if not response.is_success:
215-
self._base._handle_error(response)
216+
data = self._base._request_json("delete", f"/api/v1/tasks/{task_id}")
217+
return convert_to_task_operation_output(data)

packages/taskdog-client/src/taskdog_client/taskdog_api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def restore_task(self, task_id: int) -> TaskOperationOutput:
189189
"""Restore an archived task."""
190190
return self._tasks.restore_task(task_id)
191191

192-
def remove_task(self, task_id: int) -> None:
192+
def remove_task(self, task_id: int) -> TaskOperationOutput:
193193
"""Permanently delete a task."""
194194
return self._tasks.remove_task(task_id)
195195

packages/taskdog-client/tests/test_task_client.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,14 @@ def test_archive_restore_operations(self, method_name, expected_operation):
139139
assert result == mock_output
140140

141141
def test_remove_task(self):
142-
"""Test remove_task makes correct API call."""
143-
mock_response = Mock()
144-
mock_response.is_success = True
145-
self.mock_base._safe_request.return_value = mock_response
142+
"""Test remove_task makes correct API call and returns TaskOperationOutput."""
143+
mock_data = {"id": 1, "name": "Task 1", "status": "PENDING", "priority": 50}
144+
self.mock_base._request_json.return_value = mock_data
146145

147-
self.client.remove_task(task_id=1)
146+
result = self.client.remove_task(task_id=1)
148147

149-
self.mock_base._safe_request.assert_called_once_with(
148+
self.mock_base._request_json.assert_called_once_with(
150149
"delete", "/api/v1/tasks/1"
151150
)
152-
153-
def test_remove_task_not_found(self):
154-
"""Test remove_task handles not found error."""
155-
mock_response = Mock()
156-
mock_response.is_success = False
157-
self.mock_base._safe_request.return_value = mock_response
158-
159-
self.client.remove_task(task_id=999)
160-
161-
self.mock_base._handle_error.assert_called_once_with(mock_response)
151+
assert result.id == 1
152+
assert result.name == "Task 1"

packages/taskdog-core/src/taskdog_core/application/services/optimization/earliest_deadline_optimization_strategy.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ def _sort_tasks(self, tasks: list[Task], start_date: datetime) -> list[Task]:
2424
"""Sort tasks by deadline (earliest first)."""
2525
return sorted(
2626
tasks,
27-
key=lambda t: t.deadline
28-
if t.deadline is not None
29-
else datetime(9999, 12, 31, 23, 59, 59),
27+
key=lambda t: (
28+
t.deadline
29+
if t.deadline is not None
30+
else datetime(9999, 12, 31, 23, 59, 59)
31+
),
3032
)

packages/taskdog-core/src/taskdog_core/application/use_cases/remove_task.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Use case for removing a task."""
22

33
from taskdog_core.application.dto.base import SingleTaskInput
4+
from taskdog_core.application.dto.task_operation_output import TaskOperationOutput
45
from taskdog_core.application.use_cases.base import UseCase
56
from taskdog_core.domain.repositories.notes_repository import NotesRepository
67
from taskdog_core.domain.repositories.task_repository import TaskRepository
78

89

9-
class RemoveTaskUseCase(UseCase[SingleTaskInput, None]):
10+
class RemoveTaskUseCase(UseCase[SingleTaskInput, TaskOperationOutput]):
1011
"""Use case for removing tasks."""
1112

1213
def __init__(self, repository: TaskRepository, notes_repository: NotesRepository):
@@ -19,21 +20,30 @@ def __init__(self, repository: TaskRepository, notes_repository: NotesRepository
1920
self.repository = repository
2021
self.notes_repository = notes_repository
2122

22-
def execute(self, input_dto: SingleTaskInput) -> None:
23+
def execute(self, input_dto: SingleTaskInput) -> TaskOperationOutput:
2324
"""Execute task removal.
2425
2526
Deletes both the task and its associated notes file (if any).
27+
Returns task information captured before deletion.
2628
2729
Args:
2830
input_dto: Task removal input data
2931
32+
Returns:
33+
TaskOperationOutput containing the deleted task's information
34+
3035
Raises:
3136
TaskNotFoundException: If task doesn't exist
3237
"""
33-
self._get_task_or_raise(self.repository, input_dto.task_id)
38+
task = self._get_task_or_raise(self.repository, input_dto.task_id)
39+
40+
# Capture task info before deletion
41+
result = TaskOperationOutput.from_task(task)
3442

3543
# Delete notes first (idempotent - won't fail if notes don't exist)
3644
self.notes_repository.delete_notes(input_dto.task_id)
3745

3846
# Then delete the task
3947
self.repository.delete(input_dto.task_id)
48+
49+
return result

packages/taskdog-core/src/taskdog_core/controllers/task_crud_controller.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,17 @@ def restore_task(self, task_id: int) -> TaskOperationOutput:
231231

232232
return result
233233

234-
def remove_task(self, task_id: int) -> None:
234+
def remove_task(self, task_id: int) -> TaskOperationOutput:
235235
"""Remove a task (hard delete).
236236
237237
Permanently deletes the task and its associated notes from storage.
238238
239239
Args:
240240
task_id: ID of the task to remove
241241
242+
Returns:
243+
TaskOperationOutput containing the deleted task's information
244+
242245
Raises:
243246
TaskNotFoundException: If task not found
244247
"""
@@ -248,8 +251,10 @@ def remove_task(self, task_id: int) -> None:
248251

249252
use_case = RemoveTaskUseCase(self.repository, self.notes_repository)
250253
request = SingleTaskInput(task_id=task_id)
251-
use_case.execute(request)
254+
result = use_case.execute(request)
252255

253256
self.logger.info(
254257
f"Task removed successfully: task_id={task_id}", task_id=task_id
255258
)
259+
260+
return result

packages/taskdog-core/tests/application/queries/filters/test_task_filter.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,15 @@ def test_rshift_operator_with_none_returns_self(self):
123123
"composed_factory,expected_filter_count",
124124
[
125125
(
126-
lambda: (PendingOnlyFilter() >> HighPriorityFilter())
127-
>> ConcreteFilter(),
126+
lambda: (
127+
(PendingOnlyFilter() >> HighPriorityFilter()) >> ConcreteFilter()
128+
),
128129
3,
129130
),
130131
(
131-
lambda: PendingOnlyFilter()
132-
>> (HighPriorityFilter() >> ConcreteFilter()),
132+
lambda: (
133+
PendingOnlyFilter() >> (HighPriorityFilter() >> ConcreteFilter())
134+
),
133135
3,
134136
),
135137
],

packages/taskdog-server/src/taskdog_server/api/routers/tasks.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from fastapi import APIRouter, Query, status
77

88
from taskdog_core.application.dto.query_inputs import ListTasksInput
9-
from taskdog_core.domain.exceptions.task_exceptions import TaskNotFoundException
109
from taskdog_server.api.converters import (
1110
convert_to_task_detail_response,
1211
convert_to_task_list_response,
@@ -371,47 +370,43 @@ async def restore_task(
371370
return TaskOperationResponse.from_dto(result)
372371

373372

374-
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
373+
@router.delete("/{task_id}")
375374
@handle_task_errors
376375
async def delete_task(
377376
task_id: int,
378377
controller: CrudControllerDep,
379-
query_controller: QueryControllerDep,
380378
broadcaster: EventBroadcasterDep,
381379
audit_controller: AuditLogControllerDep,
382380
client_name: AuthenticatedClientDep,
383-
) -> None:
381+
) -> TaskOperationResponse:
384382
"""Permanently delete a task.
385383
386384
Args:
387385
task_id: Task ID
388386
controller: CRUD controller dependency
389-
query_controller: Query controller dependency (for fetching task name before deletion)
390387
broadcaster: Event broadcaster dependency
391388
audit_controller: Audit log controller dependency
392389
client_name: Authenticated client name (for broadcast payload)
393390
391+
Returns:
392+
Deleted task data
393+
394394
Raises:
395395
HTTPException: 404 if task not found
396396
"""
397-
# Get task name before deletion for notification
398-
task_output = query_controller.get_task_by_id(task_id)
399-
if task_output is None or task_output.task is None:
400-
raise TaskNotFoundException(f"Task {task_id} not found")
401-
task_name = task_output.task.name
402-
403-
# Delete task
404-
controller.remove_task(task_id)
397+
result = controller.remove_task(task_id)
405398

406399
# Broadcast WebSocket event in background
407-
broadcaster.task_deleted(task_id, task_name, client_name)
400+
broadcaster.task_deleted(task_id, result.name, client_name)
408401

409402
# Audit log
410403
audit_controller.log_operation(
411404
operation="delete_task",
412405
resource_type="task",
413406
resource_id=task_id,
414-
resource_name=task_name,
407+
resource_name=result.name,
415408
client_name=client_name,
416409
success=True,
417410
)
411+
412+
return TaskOperationResponse.from_dto(result)

packages/taskdog-server/tests/api/routers/test_tasks.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,10 @@ def test_delete_task_success(self, client, repository, task_factory):
311311
response = client.delete(f"/api/v1/tasks/{task.id}")
312312

313313
# Assert
314-
assert response.status_code == 204
314+
assert response.status_code == 200
315+
data = response.json()
316+
assert data["id"] == task.id
317+
assert data["name"] == "Test Task"
315318

316319
# Verify task is deleted
317320
deleted_task = repository.get_by_id(task.id)

packages/taskdog-ui/src/taskdog/tui/commands/hard_delete.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from taskdog.tui.commands.batch_command_base import BatchCommandBase
44
from taskdog_core.application.dto.bulk_operation_output import BulkOperationOutput
5+
from taskdog_core.application.dto.task_operation_output import TaskOperationOutput
56

67

78
class HardDeleteCommand(BatchCommandBase):
@@ -19,6 +20,9 @@ def get_confirmation_config(self) -> tuple[str, str, str]:
1920
"[!] All tasks will be completely removed from the database.",
2021
)
2122

23+
def execute_single(self, task_id: int) -> TaskOperationOutput:
24+
return self.context.api_client.remove_task(task_id)
25+
2226
def execute_bulk(self, task_ids: list[int]) -> BulkOperationOutput:
2327
"""Permanently delete tasks (hard delete) via Bulk API."""
2428
return self.context.api_client.bulk_delete(task_ids)

0 commit comments

Comments
 (0)