Skip to content

Django个人任务板完整实践 · Django任务板完整工程包含哪些文件至全部工程文件与同源正文 ​

返回分篇目录

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

Django个人任务板完整实践 ​

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

Django任务板完整工程包含哪些文件 ​

完整工程固定依赖 Django 5.2.18,目标解释器 Python 3.12,包含配置、URL、模型、表单、视图、后台、模板、手写迁移和测试规格。数据保存在本地 SQLite;不依赖外部数据库服务。环境变量直接从进程读取,.env.example 是输入说明,不会自动加载。

成功标准是已登录用户完成新增、筛选、编辑和删除,并且不能读取或修改别人的任务。新增和编辑共享表单,但不新增可编辑 owner。源码与下方所有 include 同源,README 包含未来自行安装和启动的三平台命令;交付时示例未执行。

Django绑定表单怎样保留错误输入 ​

GET 创建未绑定表单,POST 把 request.POST 交给绑定表单。不能写 request.POST or None,因为空 POST 也必须被视为一次提交并报告缺失字段。只有 POST 且 is_valid 为真才写数据库,错误时原表单连同字段错误重新渲染,输入得以保留。

成功保存后重定向列表,避免刷新浏览器重复提交同一 POST;这叫 POST/Redirect/GET。浏览器仍可能重发请求,因此它不取代数据库唯一约束或业务幂等策略。列表默认显示全部,state=open/done 才附加布尔条件,未知 state 退回全部,不拼接原始 SQL。

Django任务表单怎样携带CSRF令牌 ​

每个修改表单包含 csrf_token,渲染时产生供浏览器提交的隐藏令牌,CsrfViewMiddleware 在不安全方法中检查令牌及相关来源条件。Cookie 保存 CSRF secret 的相关值,提交令牌用于证明请求与当前站点页面的关系;登录身份本身不能代替它。

本例使用同源普通 HTML 表单,不配置跨域放行,不关闭 CSRF 中间件。登录、新增、编辑、删除和退出均覆盖 token。GET 不做删除,因此链接预取、爬虫和浏览器缓存不会误删任务。未来前后端分离时需另外分析 Origin、Cookie 属性和凭证策略,不能把本例表单步骤直接等同跨域配置。

Django任务板需要哪些隔离测试 ​

测试客户端覆盖正常 CRUD 与完成状态筛选,也覆盖匿名跳转、越权 404、GET 删除 405、空POST、空白与超长标题、新增/编辑重复冲突、保留原标题、跨用户同名及伪造 owner。普通 TestCase 客户端默认放松 CSRF 检查,因此另建 Client(enforce_csrf_checks=True) 验证无 token 得到 403、有 token 成功创建。

测试不能只断言状态码,还要核对任务数量、owner、修改后的字段、删除结果及列表隔离。重复保存后再发 GET,确认异常恢复路径没有把后续数据库查询留在坏事务里。当前只检查测试源码和场景完整性,没有执行测试客户端,没有创建测试数据库。

全部工程文件与同源正文 ​

下列代码读取 examples/python/p2-django 原文件,不维护第二份副本。迁移为手写源文件,测试为未执行规格。

requirements.txt ​

ini
Django==5.2.18

.env.example ​

ini
# Settings reads process environment, not this file automatically.
DJANGO_SECRET_KEY=replace-with-a-long-local-random-value
DJANGO_DEBUG=1
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,testserver

.gitignore ​

ini
.venv/
__pycache__/
*.pyc
db.sqlite3
.env

manage.py ​

py
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "taskboard.settings")
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

taskboard/init.py ​

py
# Project configuration package.

taskboard/settings.py ​

py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")
INSTALLED_APPS = [
    "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes",
    "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles",
    "tasks.apps.TasksConfig",
]
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "taskboard.urls"
TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True,
    "OPTIONS": {"context_processors": [
        "django.template.context_processors.request",
        "django.contrib.auth.context_processors.auth",
        "django.contrib.messages.context_processors.messages",
    ]},
}]
WSGI_APPLICATION = "taskboard.wsgi.application"
ASGI_APPLICATION = "taskboard.asgi.application"
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}
AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "tasks:list"
LOGOUT_REDIRECT_URL = "login"

taskboard/urls.py ​

py
from django.contrib import admin
from django.urls import include, path
from django.contrib.auth.views import LoginView, LogoutView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/login/", LoginView.as_view(), name="login"),
    path("accounts/logout/", LogoutView.as_view(), name="logout"),
    path("", include("tasks.urls")),
]

taskboard/wsgi.py ​

py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "taskboard.settings")
application = get_wsgi_application()

taskboard/asgi.py ​

py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "taskboard.settings")
application = get_asgi_application()

tasks/init.py ​

py
# Taskboard application.

tasks/apps.py ​

py
from django.apps import AppConfig

class TasksConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "tasks"

tasks/models.py ​

py
from django.conf import settings
from django.db import models

class Task(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=120)
    done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at", "-pk"]
        constraints = [models.UniqueConstraint(fields=["owner", "title"], name="task_owner_title_unique")]

    def __str__(self):
        return self.title

tasks/forms.py ​

py
from django import forms
from .models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ["title", "done"]

    def clean_title(self):
        title = self.cleaned_data["title"].strip()
        if not title:
            raise forms.ValidationError("标题不能为空。")
        return title

tasks/views.py ​

py
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError, transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods, require_POST
from .forms import TaskForm
from .models import Task

@login_required
@require_http_methods(["GET"])
def task_list(request):
    tasks = Task.objects.filter(owner=request.user)
    state = request.GET.get("state", "all")
    if state in {"open", "done"}:
        tasks = tasks.filter(done=state == "done")
    return render(request, "tasks/list.html", {"tasks": tasks, "state": state})

@login_required
@require_http_methods(["GET", "POST"])
def task_edit(request, pk=None):
    task = get_object_or_404(Task, pk=pk, owner=request.user) if pk is not None else None
    form = TaskForm(request.POST if request.method == "POST" else None, instance=task)
    if request.method == "POST" and form.is_valid():
        saved = form.save(commit=False)
        saved.owner = request.user
        try:
            with transaction.atomic():
                saved.save()
        except IntegrityError:
            # This model has one business constraint; transaction has rolled back.
            form.add_error(None, "保存冲突,请检查是否已有同名任务,再重试。")
        else:
            return redirect("tasks:list")
    return render(request, "tasks/form.html", {"form": form, "task": task})

@login_required
@require_POST
def task_delete(request, pk):
    task = get_object_or_404(Task, pk=pk, owner=request.user)
    task.delete()
    return redirect("tasks:list")

tasks/urls.py ​

py
from django.urls import path
from . import views

app_name = "tasks"
urlpatterns = [
    path("", views.task_list, name="list"),
    path("tasks/new/", views.task_edit, name="create"),
    path("tasks/<int:pk>/edit/", views.task_edit, name="edit"),
    path("tasks/<int:pk>/delete/", views.task_delete, name="delete"),
]

继续阅读