Skip to content

Django个人任务板完整实践 · tasks/admin.py至版本与资料边界 ​

返回分篇目录

适用:Python 3.12、Django 5.2.18(5.2 LTS)。先掌握函数、模块、虚拟环境和 HTTP 的请求/响应。示例未执行;本轮不安装依赖、不启动服务、不运行框架或迁移。

tasks/admin.py ​

py
from django.contrib import admin
from .models import Task

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ["title", "owner", "done", "created_at"]
    list_filter = ["done"]
    search_fields = ["title", "owner__username"]

tasks/migrations/init.py ​

py
# Migration package.

tasks/migrations/0001_initial.py ​

py
# Hand-authored migration, not produced or run by this delivery.
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

class Migration(migrations.Migration):
    initial = True
    dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
    operations = [migrations.CreateModel(
        name="Task",
        fields=[
            ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
            ("title", models.CharField(max_length=120)),
            ("done", models.BooleanField(default=False)),
            ("created_at", models.DateTimeField(auto_now_add=True)),
            ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
        ],
        options={"ordering": ["-created_at", "-pk"], "constraints": [
            models.UniqueConstraint(fields=("owner", "title"), name="task_owner_title_unique")
        ]},
    )]

templates/base.html ​

html
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>个人任务板</title></head>
<body><header><a href="{% url 'tasks:list' %}">任务板</a>
{% if user.is_authenticated %}<span>{{ user.username }}</span>
<form method="post" action="{% url 'logout' %}">{% csrf_token %}<button>退出</button></form>{% endif %}
</header><main>{% block content %}{% endblock %}</main></body></html>

templates/registration/login.html ​

html
{% extends "base.html" %}
{% block content %}<h1>登录任务板</h1>
<form method="post">{% csrf_token %}{{ form.as_p }}
<input type="hidden" name="next" value="{{ next }}"><button>登录</button></form>{% endblock %}

templates/tasks/list.html ​

html
{% extends "base.html" %}
{% block content %}<h1>我的任务</h1>
<nav><a href="?state=all">全部</a> <a href="?state=open">未完成</a> <a href="?state=done">已完成</a> <a href="{% url 'tasks:create' %}">新建</a></nav>
<ul>{% for task in tasks %}<li>{{ task.title }} — {{ task.done|yesno:"已完成,未完成" }}
<a href="{% url 'tasks:edit' task.pk %}">编辑</a>
<form method="post" action="{% url 'tasks:delete' task.pk %}">{% csrf_token %}<button>删除</button></form>
</li>{% empty %}<li>当前筛选没有任务。</li>{% endfor %}</ul>{% endblock %}

templates/tasks/form.html ​

html
{% extends "base.html" %}
{% block content %}<h1>{% if task %}编辑任务{% else %}新建任务{% endif %}</h1>
<form method="post">{% csrf_token %}{{ form.as_p }}<button>保存</button></form>
<a href="{% url 'tasks:list' %}">返回列表</a>{% endblock %}

tasks/tests.py ​

py
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from .models import Task

class TaskboardTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.alice = get_user_model().objects.create_user(username="alice", password="local-test-password")
        cls.bob = get_user_model().objects.create_user(username="bob", password="local-test-password")
        cls.foreign = Task.objects.create(owner=cls.bob, title="Bob private")

    def setUp(self):
        self.client.force_login(self.alice)

    def test_anonymous_redirects(self):
        self.client.logout()
        response = self.client.get(reverse("tasks:list"))
        self.assertEqual(response.status_code, 302)
        self.assertIn(reverse("login"), response.url)

    def test_crud_and_filters(self):
        response = self.client.post(reverse("tasks:create"), {"title": "Read ORM"})
        self.assertRedirects(response, reverse("tasks:list"))
        task = Task.objects.get(owner=self.alice)
        self.assertContains(self.client.get(reverse("tasks:list")), "Read ORM")
        self.assertNotContains(self.client.get(reverse("tasks:list")), "Bob private")
        self.client.post(reverse("tasks:edit", args=[task.pk]), {"title": "Read forms", "done": "on"})
        task.refresh_from_db()
        self.assertTrue(task.done)
        self.assertEqual(task.title, "Read forms")
        self.assertNotContains(self.client.get(reverse("tasks:list"), {"state": "open"}), "Read forms")
        self.assertContains(self.client.get(reverse("tasks:list"), {"state": "done"}), "Read forms")
        self.assertEqual(self.client.get(reverse("tasks:delete", args=[task.pk])).status_code, 405)
        self.assertRedirects(self.client.post(reverse("tasks:delete", args=[task.pk])), reverse("tasks:list"))
        self.assertFalse(Task.objects.filter(pk=task.pk).exists())

    def test_other_owner_cannot_read_edit_delete(self):
        for name in ["tasks:edit", "tasks:delete"]:
            url = reverse(name, args=[self.foreign.pk])
            self.assertEqual(self.client.post(url, {"title": "stolen"}).status_code, 404)
        self.assertEqual(self.client.get(reverse("tasks:edit", args=[self.foreign.pk])).status_code, 404)
        self.foreign.refresh_from_db()
        self.assertEqual(self.foreign.title, "Bob private")

    def test_invalid_duplicate_and_forged_owner(self):
        url = reverse("tasks:create")
        self.assertEqual(self.client.post(url, {}).status_code, 200)
        for title in ["   ", "x" * 121]:
            self.assertEqual(self.client.post(url, {"title": title}).status_code, 200)
        self.assertEqual(Task.objects.filter(owner=self.alice).count(), 0)
        self.client.post(url, {"title": "Unique", "owner": self.bob.pk})
        self.assertTrue(Task.objects.filter(title="Unique", owner=self.alice).exists())
        self.assertContains(self.client.post(url, {"title": "Unique"}), "保存冲突")
        self.assertEqual(Task.objects.filter(owner=self.alice).count(), 1)
        self.assertEqual(self.client.get(reverse("tasks:list")).status_code, 200)
        task = Task.objects.get(owner=self.alice, title="Unique")
        edit_url = reverse("tasks:edit", args=[task.pk])
        self.assertRedirects(self.client.post(edit_url, {"title": "Unique"}), reverse("tasks:list"))
        Task.objects.create(owner=self.bob, title="Unique")
        self.assertRedirects(self.client.post(edit_url, {"title": "Unique"}), reverse("tasks:list"))
        Task.objects.create(owner=self.alice, title="Taken")
        self.assertContains(self.client.post(edit_url, {"title": "Taken"}), "保存冲突")
        task.refresh_from_db()
        self.assertEqual(task.title, "Unique")
        self.assertEqual(Task.objects.filter(owner=self.alice).count(), 2)
        self.assertEqual(self.client.get(reverse("tasks:list")).status_code, 200)

    def test_csrf_rejected_and_valid_token_accepted(self):
        client = Client(enforce_csrf_checks=True)
        client.force_login(self.alice)
        url = reverse("tasks:create")
        self.assertEqual(client.post(url, {"title": "No token"}).status_code, 403)
        self.assertFalse(Task.objects.filter(title="No token").exists())
        client.get(url)
        token = client.cookies["csrftoken"].value
        self.assertEqual(client.post(url, {"title": "With token", "csrfmiddlewaretoken": token}).status_code, 302)
        self.assertTrue(Task.objects.filter(title="With token", owner=self.alice).exists())

    def test_login_and_logout_methods(self):
        self.client.logout()
        self.assertTrue(self.client.login(username="alice", password="local-test-password"))
        self.assertEqual(self.client.get(reverse("logout")).status_code, 405)
        self.assertRedirects(self.client.post(reverse("logout")), reverse("login"))

README.md ​

md
# Django 5.2 个人任务板

示例未执行。交付仅做 AST、文件引用、Markdown/Vue 模板和测试规格静态检查,没有安装 Django,没有创建数据库或启动服务。目标 Python 3.12、Django 5.2.18、SQLite;后续运行前自行核对补丁维护状态。

功能:内置用户登录与 POST 退出,个人任务新增、列表、完成状态筛选、编辑和 POST 删除;所有读写通过 owner 过滤;标题必填、最多120字符,同一用户标题唯一。后台仅用于可信管理员;没有开放注册或多租户管理员隔离。重复标题返回表单错误,不保存第二条。

## 未来自行运行(本轮未执行)

PowerShell,在本目录:

```powershell
py -3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
$env:DJANGO_SECRET_KEY = 'replace-with-long-local-random-value'
$env:DJANGO_DEBUG = '1'
$env:DJANGO_ALLOWED_HOSTS = '127.0.0.1,localhost,testserver'
.\.venv\Scripts\python.exe manage.py check
.\.venv\Scripts\python.exe manage.py makemigrations --check --dry-run
.\.venv\Scripts\python.exe manage.py migrate
.\.venv\Scripts\python.exe manage.py createsuperuser
.\.venv\Scripts\python.exe manage.py test
.\.venv\Scripts\python.exe manage.py runserver 127.0.0.1:8000
```

macOS/Linux Bash/Zsh,在本目录:

```bash
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
export DJANGO_SECRET_KEY='replace-with-long-local-random-value'
export DJANGO_DEBUG=1
export DJANGO_ALLOWED_HOSTS='127.0.0.1,localhost,testserver'
.venv/bin/python manage.py check
.venv/bin/python manage.py makemigrations --check --dry-run
.venv/bin/python manage.py migrate
.venv/bin/python manage.py createsuperuser
.venv/bin/python manage.py test
.venv/bin/python manage.py runserver 127.0.0.1:8000
```

`.env.example` 仅示意变量,代码不自动读取 `.env`。`migrate` 会创建本目录 `db.sqlite3`,迁移源文件手写,需自行用 `makemigrations --check --dry-run` 核对漂移。开发服务器只绑定回环地址。

浏览器打开 http://127.0.0.1:8000/ 应跳转登录;使用创建的用户登录,然后新增“Read ORM”,编辑为完成,筛选完成列表并删除。可通过 admin 的 Users 创建第二个普通用户(设置密码),两用户互不可见任务。管理员后台有跨用户管理能力,不等同普通任务页。

预期测试:六个测试方法全部成功;覆盖 CRUD、筛选、匿名、所有权、空POST/空白/超长、新增和编辑重复冲突、保留原标题与跨用户同名、伪造 owner、CSRF 缺失及有效 token、退出方法限制。普通 TestCase 的客户端默认不强制 CSRF,因此专门建立 enforce_csrf_checks 客户端。测试数据库由 Django 测试运行器管理;本轮未创建。

这不是生产部署配置:部署前提供 WSGI/ASGI 宿主、静态资源、HTTPS、Cookie 策略、数据库备份和密钥管理,运行部署检查并补齐部署环境测试。本工程未验证并发容量、跨平台运行或真实数据库迁移。

人工验收用例和预期页面 ​

操作预期必须额外核对
未登录打开首页跳转登录并保留 next不展示任务内容
Alice新增标题返回列表owner由服务器指定
提交空白或121字符标题停留表单并显示错误不新增记录
Alice新增已有标题显示保存冲突原任务保留、仍可查询
Bob访问Alice编辑/删除地址404原任务未改、未删
GET请求删除或退出405状态不变
缺失CSRF的有效登录POST403未写数据

默认测试用户仅在测试数据库内创建;README 的 createsuperuser 用于未来人工登录。源码没有固定生产账号或密码,测试密码只用于未执行规格。列表不分页,适合小型教学数据;扩大数据量应先定义分页顺序、并发更新与审计要求,再修改实现。

独立练习 ​

不用改 owner 字段实现用户隔离,先独立写出“保留原标题编辑自身成功”的规格,再对照tasks/tests.py本轮补充的断言;同时说明另一个用户的同名任务为何不应冲突。解释为什么重复标题表单错误后应仍能查列表。

先写出判断依据,再核对下节。

参考解答 ​

编辑先按 owner 取实例,再绑定原实例;同一条记录保持标题应允许,另一个用户的同名标题也允许。事务异常已离开 atomic 才处理,连接恢复可继续查询。

版本与资料边界 ​

核对日期2026-10-17。官方5.2.18发布页标注2026-10-06,官方支持表列5.2 LTS扩展支持至2028年4月。这里固定教学补丁,不声称以后仍是最新版本;未来自跑应先检查受支持补丁并重新验收。Windows PowerShell、macOS/Linux Bash/Zsh的命令已在完整工程README分开给出,三平台均未运行。

CSRF;测试客户端;官方版本支持表;5.2.18发布说明。版本依据:支持表与5.2.18发布页。

继续阅读