Task tracker API: owner-scoped ViewSet with an object-level permission
Python · Django · advanced · modification
Adds the task-tracker API: `Task` model (with the initial migration), serializer, an `IsTaskOwner` object-level permission, and a `TaskViewSet` wired through a router. The queryset is scoped to `request.user` and the permission class is applied to every action, so ownership is enforced twice. `owner` and the timestamps are read-only over the wire; creates assign the owner server-side. Statuses and due dates are intentionally unconstrained — the tracker has no workflow rules.
A small team's internal project tracker. The API is internet-exposed and every user must only ever see and touch their own tasks; a leak across users is the one thing this endpoint cannot do. The `tracker` app (package and `apps.py` from `startapp`) and `rest_framework` are already in `INSTALLED_APPS`.
Requirements
- Add a project task-tracker API: a `Task` model and a DRF `ModelViewSet` registered with a `DefaultRouter` under `/api/tasks/`.
- Task fields: `title` (non-blank, max 200 chars), `description` (optional, defaults to empty string), `status` (one of `todo`, `in_progress`, `done`; defaults to `todo`), `due_date` (nullable), `owner` (FK to the user model, `on_delete=CASCADE` — a user's tasks die with the user), plus `created_at`/`updated_at` maintained by Django. `USE_TZ = True` in settings.
- Any `due_date` is accepted, including past dates — the tracker deliberately allows backdating. Any status-to-status transition is allowed; there are no workflow rules.
- Every action requires an authenticated user; unauthenticated requests are rejected by DRF's standard 401/403 handling before any view code or queryset evaluation runs.
- Ownership scoping is required in BOTH places, as defense in depth: `get_queryset` filters to the requesting user's own tasks (so `list` and `retrieve` can never see another user's task), AND an object-level permission class applied to every action re-checks `obj.owner` against the requesting user.
- On create, `owner` is always set server-side to the requesting user; clients can never set or change `owner` through the API (the field is read-only in the serializer). `created_at`/`updated_at` are also read-only over the API.
- Update and delete are owner-only; both full (PUT) and partial (PATCH) updates are supported.
- Task lists are ordered newest-first by `created_at`, declared on the model.
- Serialization uses explicit field lists (no `fields = "__all__"`), per the project's API guidelines.
Files touched
- tracker/models.py
- tracker/serializers.py
- tracker/permissions.py
- tracker/views.py
- tracker/migrations/0001_initial.py
- tracker/urls.py
- config/urls.py
--- tracker/models.py +from django.conf import settings +from django.db import models + + +class Task(models.Model): + class Status(models.TextChoices): + TODO = "todo", "To do" + IN_PROGRESS = "in_progress", "In progress" + DONE = "done", "Done" + + title = models.CharField(max_length=200) + description = models.TextField(blank=True, default="") + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.TODO, + ) + due_date = models.DateField(null=True, blank=True) + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="tasks", + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-created_at"] +