diff --git a/todo/repositories/task_repository.py b/todo/repositories/task_repository.py index eb27c0b2..06f0fc4c 100644 --- a/todo/repositories/task_repository.py +++ b/todo/repositories/task_repository.py @@ -17,16 +17,36 @@ class TaskRepository(MongoRepository): @classmethod def _build_status_filter(cls, status_filter: str = None) -> dict: - """ - Build status filter for task queries. + now = datetime.now(timezone.utc) + + if status_filter == TaskStatus.DEFERRED.value: + return { + "$and": [ + {"deferredDetails": {"$ne": None}}, + {"deferredDetails.deferredTill": {"$gt": now}}, + ] + } + + elif status_filter == TaskStatus.DONE.value: + return { + "$or": [ + {"deferredDetails": None}, + {"deferredDetails.deferredTill": {"$lt": now}}, + ] + } - """ - if status_filter: - if status_filter == TaskStatus.DONE.value: - return {} # No status filtering, include all tasks - return {"status": status_filter} else: - return {"status": {"$ne": TaskStatus.DONE.value}} + return { + "$and": [ + {"status": {"$ne": TaskStatus.DONE.value}}, + { + "$or": [ + {"deferredDetails": None}, + {"deferredDetails.deferredTill": {"$lt": now}}, + ] + }, + ] + } @classmethod def list( diff --git a/todo/services/task_service.py b/todo/services/task_service.py index 3c79f6b4..d73ab863 100644 --- a/todo/services/task_service.py +++ b/todo/services/task_service.py @@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse_lazy from urllib.parse import urlencode -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from todo.dto.deferred_details_dto import DeferredDetailsDTO from todo.dto.label_dto import LabelDTO from todo.dto.task_dto import TaskDTO, CreateTaskDTO @@ -28,7 +28,6 @@ from todo.constants.task import ( TaskStatus, TaskPriority, - MINIMUM_DEFERRAL_NOTICE_DAYS, ) from todo.constants.messages import ApiErrors, ValidationErrors from django.conf import settings @@ -173,6 +172,11 @@ def prepare_task_dto(cls, task_model: TaskModel, user_id: str = None) -> TaskDTO if watchlist_entry: in_watchlist = watchlist_entry.isActive + task_status = task_model.status + + if task_model.deferredDetails and task_model.deferredDetails.deferredTill > datetime.now(timezone.utc): + task_status = TaskStatus.DEFERRED.value + return TaskDTO( id=str(task_model.id), displayId=task_model.displayId, @@ -183,7 +187,7 @@ def prepare_task_dto(cls, task_model: TaskModel, user_id: str = None) -> TaskDTO labels=label_dtos, startedAt=task_model.startedAt, dueAt=task_model.dueAt, - status=task_model.status, + status=task_status, priority=task_model.priority, deferredDetails=deferred_details, in_watchlist=in_watchlist, @@ -420,6 +424,16 @@ def update_task_with_assignee_from_dict(cls, task_id: str, validated_data: dict, if validated_data.get("status") == TaskStatus.IN_PROGRESS and not current_task.startedAt: update_payload["startedAt"] = datetime.now(timezone.utc) + if ( + validated_data.get("status") is not None + and validated_data.get("status") != TaskStatus.DEFERRED.value + and current_task.deferredDetails + ): + update_payload["deferredDetails"] = None + + if validated_data.get("status") == TaskStatus.DEFERRED.value: + update_payload["status"] = current_task.status + # Update task if there are changes if update_payload: update_payload["updatedBy"] = user_id @@ -547,9 +561,7 @@ def defer_task(cls, task_id: str, deferred_till: datetime, user_id: str) -> Task else current_task.dueAt.astimezone(timezone.utc) ) - defer_limit = due_at - timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS) - - if deferred_till > defer_limit: + if deferred_till >= due_at: raise UnprocessableEntityException( ValidationErrors.CANNOT_DEFER_TOO_CLOSE_TO_DUE_DATE, source={ApiErrorSource.PARAMETER: "deferredTill"}, @@ -562,6 +574,7 @@ def defer_task(cls, task_id: str, deferred_till: datetime, user_id: str) -> Task ) update_payload = { + "status": TaskStatus.TODO.value, "deferredDetails": deferred_details.model_dump(), "updatedBy": user_id, } diff --git a/todo/tests/integration/test_task_defer_api.py b/todo/tests/integration/test_task_defer_api.py index c4ac2cc4..c20393be 100644 --- a/todo/tests/integration/test_task_defer_api.py +++ b/todo/tests/integration/test_task_defer_api.py @@ -3,7 +3,7 @@ from bson import ObjectId from django.urls import reverse from todo.constants.messages import ApiErrors, ValidationErrors -from todo.constants.task import MINIMUM_DEFERRAL_NOTICE_DAYS, TaskPriority, TaskStatus +from todo.constants.task import TaskPriority, TaskStatus from todo.tests.integration.base_mongo_test import AuthenticatedMongoTestCase from todo.tests.fixtures.task import tasks_db_data @@ -52,7 +52,7 @@ def _insert_task(self, *, status: str = TaskStatus.TODO.value, due_at: datetime def test_defer_task_success(self): now = datetime.now(timezone.utc) - due_at = now + timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS + 30) + due_at = now + timedelta(days=30) task_id = self._insert_task(due_at=due_at) deferred_till = now + timedelta(days=10) @@ -76,11 +76,10 @@ def test_defer_task_success(self): def test_defer_task_too_close_to_due_date_returns_422(self): now = datetime.now(timezone.utc) - due_at = now + timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS + 5) + due_at = now + timedelta(days=5) task_id = self._insert_task(due_at=due_at) - defer_limit = due_at - timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS) - deferred_till = defer_limit + timedelta(days=1) + deferred_till = due_at + timedelta(days=1) url = reverse("task_detail", args=[task_id]) + "?action=defer" response = self.client.patch(url, data={"deferredTill": deferred_till.isoformat()}, format="json") @@ -129,7 +128,7 @@ def test_defer_task_with_missing_date_returns_400(self): def test_defer_task_unauthorized(self): now = datetime.now(timezone.utc) - due_at = now + timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS + 30) + due_at = now + timedelta(days=30) task_id = self._insert_task(due_at=due_at) deferred_till = now + timedelta(days=10) url = reverse("task_detail", args=[task_id]) + "?action=defer" diff --git a/todo/tests/unit/repositories/test_task_repository.py b/todo/tests/unit/repositories/test_task_repository.py index 6a3e4fcb..f24029a1 100644 --- a/todo/tests/unit/repositories/test_task_repository.py +++ b/todo/tests/unit/repositories/test_task_repository.py @@ -97,7 +97,12 @@ def test_count_returns_total_task_count(self): result = TaskRepository.count() self.assertEqual(result, 42) - self.mock_collection.count_documents.assert_called_once_with({"status": {"$ne": "DONE"}}) + + self.mock_collection.count_documents.assert_called_once() + actual_filter = self.mock_collection.count_documents.call_args[0][0] + self.assertIn("$and", actual_filter) + self.assertIn("status", actual_filter["$and"][0]) + self.assertIn("$or", actual_filter["$and"][1]) def test_get_all_returns_all_tasks(self): self.mock_collection.find.return_value = self.task_data diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index 603929ab..7fe1d1e8 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -1024,7 +1024,7 @@ def test_defer_task_success(self, mock_prepare_dto, mock_repo_update, mock_repo_ @patch("todo.services.task_service.TaskRepository.get_by_id") def test_defer_task_too_close_to_due_date_raises_exception(self, mock_repo_get_by_id): mock_repo_get_by_id.return_value = self.task_model - deferred_till = self.due_at - timedelta(days=1) + deferred_till = self.due_at + timedelta(days=1) with self.assertRaises(UnprocessableEntityException): TaskService.defer_task(self.task_id, deferred_till, self.user_id)