浏览知识库目录

Python

CRUD、分页、搜索与消息框架

CRUD、分页、搜索与消息框架

CRUD 指创建(Create)、读取(Read)、更新(Update)和删除(Delete)。本篇把前面分散的模型、表单、视图和模板组合成一个完整业务模块,并加入分页、搜索和操作反馈。


一、路由设计

notes/urls.py

from django.urls import path

from . import views

app_name = "notes"

urlpatterns = [
    path("", views.message_list, name="list"),
    path("create/", views.message_create, name="create"),
    path("<int:pk>/", views.message_detail, name="detail"),
    path("<int:pk>/edit/", views.message_update, name="update"),
    path("<int:pk>/delete/", views.message_delete, name="delete"),
]

create/ 放在 <int:pk>/ 前面可读性更好。虽然 int 不会匹配 create,但明确的静态路由优先是一个好习惯。


二、列表与搜索

from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render

from .forms import MessageSearchForm
from .models import Message


def message_list(request):
    queryset = Message.objects.select_related("owner").prefetch_related("tags")
    form = MessageSearchForm(request.GET)

    if form.is_valid():
        keyword = form.cleaned_data["q"]
        status = form.cleaned_data["status"]

        if keyword:
            queryset = queryset.filter(
                Q(name__icontains=keyword) | Q(content__icontains=keyword)
            )
        if status:
            queryset = queryset.filter(status=status)

    paginator = Paginator(queryset, 10)
    page_obj = paginator.get_page(request.GET.get("page"))

    return render(request, "notes/message_list.html", {
        "form": form,
        "page_obj": page_obj,
    })

get_page() 会容错非法页码;若你希望非法页码直接报错,可以使用 page() 并自行处理异常。


三、详情页

from django.shortcuts import get_object_or_404, render


def message_detail(request, pk):
    message = get_object_or_404(
        Message.objects.select_related("owner").prefetch_related("tags"),
        pk=pk,
    )
    return render(request, "notes/message_detail.html", {"message": message})

四、创建与更新

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render

from .forms import MessageForm


@login_required
def message_create(request):
    form = MessageForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        message = form.save(commit=False)
        message.owner = request.user
        message.save()
        form.save_m2m()
        messages.success(request, "留言创建成功。")
        return redirect("notes:detail", pk=message.pk)

    return render(request, "notes/message_form.html", {
        "form": form,
        "page_title": "新建留言",
    })


@login_required
def message_update(request, pk):
    message = get_object_or_404(Message, pk=pk)
    form = MessageForm(request.POST or None, instance=message)

    if request.method == "POST" and form.is_valid():
        message = form.save()
        messages.success(request, "留言更新成功。")
        return redirect("notes:detail", pk=message.pk)

    return render(request, "notes/message_form.html", {
        "form": form,
        "page_title": "编辑留言",
    })

这里暂未加入登录和对象归属校验,下一篇会补上。真实项目不能只要知道主键就允许编辑。


五、删除确认

删除通常分两步:GET 展示确认页面,POST 真正删除。

@login_required
def message_delete(request, pk):
    message = get_object_or_404(Message, pk=pk)

    if request.method == "POST":
        message.delete()
        messages.success(request, "留言已删除。")
        return redirect("notes:list")

    return render(request, "notes/message_confirm_delete.html", {
        "message": message,
    })

确认模板:

{% extends "base.html" %}

{% block content %}
  <h1>确认删除</h1>
  <p>确定删除“{{ message.name }}”吗?此操作无法撤销。</p>
  <form method="post">
    {% csrf_token %}
    <button type="submit">确认删除</button>
    <a href="{% url 'notes:detail' message.pk %}">取消</a>
  </form>
{% endblock %}

六、分页模板

<nav aria-label="分页">
  {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}&q={{ request.GET.q|urlencode }}">上一页</a>
  {% endif %}

  <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span>

  {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}&q={{ request.GET.q|urlencode }}">下一页</a>
  {% endif %}
</nav>

筛选条件较多时,建议编写模板标签或在 View 中构造保留参数,避免分页链接丢失当前筛选。


七、消息框架

base.html

{% if messages %}
  <div aria-live="polite">
    {% for message in messages %}
      <div class="alert alert-{{ message.tags }}">{{ message }}</div>
    {% endfor %}
  </div>
{% endif %}

常用级别:messages.successmessages.infomessages.warningmessages.error


八、业务规则放在哪里

建议按职责划分:

  • 输入格式校验:Form。
  • 单个模型自身规则:Model 的验证或方法。
  • 跨模型业务流程:Service 函数或领域服务模块。
  • 请求和响应编排:View。
  • 展示:Template。

不要让 View 同时承担复杂计算、多个模型写入、第三方接口调用和页面渲染,否则很难测试与复用。


九、常见问题

  • 搜索后翻页条件丢失:分页链接没有保留查询字符串。
  • 更新变成新增:ModelForm 缺少 instance=message
  • 删除可被链接触发:真正删除逻辑没有限制为 POST。
  • 成功消息不显示:模板未遍历 messages,或相关中间件/上下文处理器被移除。
  • 列表查询过多:模板访问关联对象,View 没有使用 select_relatedprefetch_related

十、本篇检查清单

  • 能完成模型的增删改查页面。
  • 能实现搜索、状态筛选和分页。
  • 删除只在 POST 请求中执行。
  • 成功写操作采用 PRG 模式并显示反馈。
  • 能说清 Form、View、Model 和 Service 的职责边界。

上一篇:表单验证与文件上传 | 下一篇:用户认证与权限控制