Skip to content

Fix/tasks#44

Merged
eiidoubleyuwes merged 8 commits into
stagingfrom
fix/tasks
Apr 29, 2026
Merged

Fix/tasks#44
eiidoubleyuwes merged 8 commits into
stagingfrom
fix/tasks

Conversation

@IamMuuo
Copy link
Copy Markdown
Contributor

@IamMuuo IamMuuo commented Apr 29, 2026

Enhanced Task management API

Summary by CodeRabbit

  • New Features

    • Added task reopen, move to different lists, convert subtasks, and promote to top-level operations.
  • Bug Fixes

    • Improved task ownership verification for parent task and task list validation.
  • Chores

    • Refactored task API endpoints with UUID-based task identifiers and reorganized routing structure.

@IamMuuo IamMuuo requested a review from eiidoubleyuwes April 29, 2026 19:20
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76b5532e-beaf-421d-a883-ee979d4ed44e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tasks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
todos/tests.py (2)

314-315: Remove leftover debug prints.

These add noise to local test runs and CI logs without validating behavior.

Also applies to: 409-409

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/tests.py` around lines 314 - 315, Remove the leftover debug print
statements that produce noise in test output by deleting the
print(response.json()) calls found in todos/tests.py (the occurrences near the
response variable around the test bodies shown in the diff, including the second
occurrence reported). Locate the tests where response is used (e.g., the test
functions that call the API and assign to response) and remove the print(...)
lines; if needed, replace them with meaningful assertions on
response.status_code or response.json() to validate behavior instead of
printing.

398-410: Assert the state change, not just the 200.

Both tests currently pass even if the endpoint returns success without actually changing parent. Please refresh the task from the DB and assert child.parent_id == parent.id after convert, then child.parent_id is None after promote.

Also applies to: 412-427

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/tests.py` around lines 398 - 410, Update the tests to assert the actual
DB state change instead of only checking HTTP 200: after calling the convert
endpoint in test_convert_to_subtask_view reload the child task from persistence
(e.g., via TaskService.get_task(child.id) or Task.objects.get(pk=child.id)) and
assert child.parent_id == parent.id; likewise in the corresponding promote test
(the one around 412-427) reload the child after the promote endpoint and assert
child.parent_id is None. Ensure you reload the task after each request before
making the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@todos/services/tests.py`:
- Line 381: The failing precondition compares different types (object vs UUID):
change the assertion so it compares the task-list IDs instead of the TaskList
instance to the UUID; e.g., use created_task.task_list.id (or
created_task.task_list.pk) and compare that to school_task_list.id so the test
actually verifies the task was not already pointing at the destination list
(references: created_task, task_list, school_task_list, id).

In `@todos/views.py`:
- Around line 467-468: Multiple generic "except Exception as e" handlers in
todos/views.py (the catch-alls at the shown locations) are returning raw
exception strings as HTTP 400s and leaking internals; replace each catch-all to
log the full exception (use the module logger or logger.exception) and return a
generic Response with status=status.HTTP_500_INTERNAL_SERVER_ERROR and a
non-sensitive message (e.g., "Internal server error"); keep existing 400/404
responses only for known validation paths and do not expose str(e) in responses.
Ensure you update each handler that currently matches "except Exception as e:"
(the occurrences around lines referenced) to follow this pattern.
- Around line 448-459: Serializer is instantiated without owner context so
TaskSerializer.validate (which reads self.context["owner_id"] for parent_id
ownership checks) rejects valid requests; update the instantiation in the create
flow that uses self.serializer_class (where serializer =
self.serializer_class(data=request.data)) to pass context={"owner_id": user_id}
(or merge with existing context) so validation can see owner_id before you copy
serializer.validated_data and call TaskService.create_task with owner_id and
validated_data.
- Around line 521-533: Patch code currently copies raw request.data and forwards
it to TaskService.update_task, allowing clients to overwrite protected fields;
instead, instantiate TaskSerializer with the existing Task instance and
request.data using partial=True, call is_valid(raise_exception=True), use
serializer.validated_data (not raw request.data) as the update payload, and map
parent_id to parent via the serializer or by converting
validated_data.pop("parent_id", None) into a Task.objects.get(...) before
calling TaskService.update_task(owner_id=user_id, task_id=task_id,
**validated_data); also explicitly remove or ignore any protected fields (e.g.,
owner_id, deleted, sync_status) from validated_data before passing to
TaskService.update_task.

---

Nitpick comments:
In `@todos/tests.py`:
- Around line 314-315: Remove the leftover debug print statements that produce
noise in test output by deleting the print(response.json()) calls found in
todos/tests.py (the occurrences near the response variable around the test
bodies shown in the diff, including the second occurrence reported). Locate the
tests where response is used (e.g., the test functions that call the API and
assign to response) and remove the print(...) lines; if needed, replace them
with meaningful assertions on response.status_code or response.json() to
validate behavior instead of printing.
- Around line 398-410: Update the tests to assert the actual DB state change
instead of only checking HTTP 200: after calling the convert endpoint in
test_convert_to_subtask_view reload the child task from persistence (e.g., via
TaskService.get_task(child.id) or Task.objects.get(pk=child.id)) and assert
child.parent_id == parent.id; likewise in the corresponding promote test (the
one around 412-427) reload the child after the promote endpoint and assert
child.parent_id is None. Ensure you reload the task after each request before
making the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08815b2e-39f6-4380-a8b5-ac169bee72eb

📥 Commits

Reviewing files that changed from the base of the PR and between d9508bf and 2931da6.

📒 Files selected for processing (6)
  • todos/services/__init__.py
  • todos/services/task_service.py
  • todos/services/tests.py
  • todos/tests.py
  • todos/urls.py
  • todos/views.py

Comment thread todos/services/tests.py Outdated
Comment thread todos/views.py Outdated
Comment on lines +448 to +459
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

try:
validated_data = serializer.validated_data.copy()
if "parent_id" in validated_data:
validated_data["parent"] = validated_data.pop("parent_id")

created_task = TaskService.create_task(
owner_id=user_id,
**validated_data,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass owner_id into TaskSerializer on create.

TaskSerializer.validate() uses self.context["owner_id"] for parent_id ownership checks. Building the serializer without that context means valid create requests with parent_id will be rejected before TaskService.create_task() runs. See todos/serializers.py:310-341.

Suggested fix
-        serializer = self.serializer_class(data=request.data)
+        serializer = self.serializer_class(
+            data=request.data,
+            context={"owner_id": user_id},
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/views.py` around lines 448 - 459, Serializer is instantiated without
owner context so TaskSerializer.validate (which reads self.context["owner_id"]
for parent_id ownership checks) rejects valid requests; update the instantiation
in the create flow that uses self.serializer_class (where serializer =
self.serializer_class(data=request.data)) to pass context={"owner_id": user_id}
(or merge with existing context) so validation can see owner_id before you copy
serializer.validated_data and call TaskService.create_task with owner_id and
validated_data.

Comment thread todos/views.py
Comment on lines +467 to +468
except Exception as e:
return Response({"message": str(e)}, status=status.HTTP_400_BAD_REQUEST)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop returning raw exception strings as 400s.

These catch-alls turn unexpected server faults into client errors and leak internal exception details in the response body. Keep 400/404 for known validation paths, and log unknown failures before returning a generic 500.

Also applies to: 497-500, 548-551, 575-578, 612-615, 645-648, 678-681, 729-732, 780-783, 813-816

🧰 Tools
🪛 Ruff (0.15.12)

[warning] 467-467: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/views.py` around lines 467 - 468, Multiple generic "except Exception as
e" handlers in todos/views.py (the catch-alls at the shown locations) are
returning raw exception strings as HTTP 400s and leaking internals; replace each
catch-all to log the full exception (use the module logger or logger.exception)
and return a generic Response with status=status.HTTP_500_INTERNAL_SERVER_ERROR
and a non-sensitive message (e.g., "Internal server error"); keep existing
400/404 responses only for known validation paths and do not expose str(e) in
responses. Ensure you update each handler that currently matches "except
Exception as e:" (the occurrences around lines referenced) to follow this
pattern.

Comment thread todos/views.py
Comment on lines +521 to +533
validated_data = request.data.copy()

# Rename parent_id to parent for update_task
if "parent_id" in validated_data:
parent_id = validated_data.pop("parent_id")
if parent_id:
validated_data["parent"] = Task.objects.get(id=parent_id)
else:
validated_data["parent"] = None

updated_task = TaskService.update_task(
owner_id=user_id, task_id=task_id, **validated_data
)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't mass-assign raw PATCH data into TaskService.update_task.

This path bypasses TaskSerializer entirely and forwards arbitrary request keys to TaskService.update_task(), which then setattr()s every field onto the model. A client can patch protected fields like owner_id, deleted, or sync_status, which is an auth/data-integrity bug.

Suggested direction
-            validated_data = request.data.copy()
-
-            # Rename parent_id to parent for update_task
-            if "parent_id" in validated_data:
-                parent_id = validated_data.pop("parent_id")
-                if parent_id:
-                    validated_data["parent"] = Task.objects.get(id=parent_id)
-                else:
-                    validated_data["parent"] = None
+            task = TaskService.get_task(user_id, task_id)
+            serializer = self.serializer_class(
+                task,
+                data=request.data,
+                partial=True,
+                context={"owner_id": user_id},
+            )
+            serializer.is_valid(raise_exception=True)
+            validated_data = serializer.validated_data.copy()
+            if "parent_id" in validated_data:
+                validated_data["parent"] = validated_data.pop("parent_id")
 
             updated_task = TaskService.update_task(
                 owner_id=user_id, task_id=task_id, **validated_data
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todos/views.py` around lines 521 - 533, Patch code currently copies raw
request.data and forwards it to TaskService.update_task, allowing clients to
overwrite protected fields; instead, instantiate TaskSerializer with the
existing Task instance and request.data using partial=True, call
is_valid(raise_exception=True), use serializer.validated_data (not raw
request.data) as the update payload, and map parent_id to parent via the
serializer or by converting validated_data.pop("parent_id", None) into a
Task.objects.get(...) before calling TaskService.update_task(owner_id=user_id,
task_id=task_id, **validated_data); also explicitly remove or ignore any
protected fields (e.g., owner_id, deleted, sync_status) from validated_data
before passing to TaskService.update_task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of UpdateTaskView.patch method
rg -n "validated_data|serializer_class|is_valid|partial=True" todos/views.py -A2 -B2 | grep -A30 "class UpdateTaskView"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the UpdateTaskView class implementation
ast-grep --pattern 'class UpdateTaskView($$$) {
  $$$
}'

# Also try with rg to get the patch method content
rg -n "UpdateTaskView|validated_data = request.data|serializer_class\(.*partial" todos/views.py -C3

Length of output: 668


@IamMuuo, after checking the current branch, the fix doesn't appear to be in place yet. Line 521 of todos/views.py still reads:

validated_data = request.data.copy()

The raw request.data is still being forwarded directly to TaskService.update_task without serializer validation. Here's the fix that needs to be applied to UpdateTaskView.patch:

def patch(self, request, task_id, *args, **kwargs):
    user_id, error_response = self.get_user_id(request)
    if error_response:
        return error_response

    try:
        task = TaskService.get_task(user_id, task_id)
        serializer = self.serializer_class(
            task,
            data=request.data,
            partial=True,
            context={"owner_id": user_id},
        )
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        validated_data = serializer.validated_data.copy()
        if "parent_id" in validated_data:
            parent_id = validated_data.pop("parent_id")
            validated_data["parent"] = Task.objects.get(id=parent_id) if parent_id else None

        updated_task = TaskService.update_task(
            owner_id=user_id, task_id=task_id, **validated_data
        )
        return Response(
            data=self.serializer_class(updated_task).data,
            status=status.HTTP_200_OK,
        )
    except Task.DoesNotExist:
        return Response(
            {"message": "Task not found"},
            status=status.HTTP_404_NOT_FOUND,
        )
    except ValueError as e:
        return Response(
            {"message": str(e)},
            status=status.HTTP_400_BAD_REQUEST,
        )
    except Exception as e:
        return Response(
            {"message": str(e)},
            status=status.HTTP_400_BAD_REQUEST,
        )

Could you push the fix to the branch? The key change is routing request.data through TaskSerializer with partial=True before passing to TaskService.

IamMuuo and others added 4 commits April 29, 2026 22:39
…tasks

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
TaskSerializer.validate() uses self.context[owner_id] for parent_id ownership checks.
Building the serializer without that context means valid create requests with parent_id
will be rejected before TaskService.create_task()
Copy link
Copy Markdown
Contributor

@eiidoubleyuwes eiidoubleyuwes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@eiidoubleyuwes eiidoubleyuwes merged commit 633fedd into staging Apr 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants