Django 从入门到精通:知行博客实战(6.1) · 第 17 / 36 节

文章增删改与对象权限

吴秀峰 13 次阅读 0 条评论 ↓

学习目标

构建作者工作区、新建、编辑、删除和草稿预览,理解登录检查与对象归属检查为什么缺一不可。

为什么每次写入都要检查对象

login_required 只说明请求有用户身份,并不说明这个用户拥有某篇文章。编辑和删除必须在查询时附加 author=request.user,不能相信表单传来的 author 或隐藏 id。返回 404 同时避免泄露不属于当前用户的记录是否存在。

创建使用 commit=False,在服务端设置作者;编辑使用 instance 绑定已有对象,校验失败时保留用户输入;删除 GET 只展示确认,POST 才改变数据库。保存后重定向到详情,避免浏览器刷新重复写入,这叫 POST/Redirect/GET。

具体怎么做

  1. 更新视图、路由和模板。新增文章作者来自当前会话。
  2. 我的文章列表只显示当前作者记录,草稿也包含在内。
  3. 详情查询允许“已发布,或者当前用户是作者”,使作者能够预览草稿。
  4. 编辑和删除的查询仅按作者范围执行,即使请求人为提交别人的 id 也无效。
  5. 先创建分类;没有分类时表单无法提交,应由管理员准备有效选项。

本章新增或替换的完整文件

以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。

blog/urls.py

from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
    path("", views.home, name="home"),
    path("mine/", views.mine, name="mine"),
    path("posts/new/", views.create, name="create"),
    path("posts/<int:pk>/edit/", views.edit, name="edit"),
    path("posts/<int:pk>/delete/", views.delete, name="delete"),
    path("posts/<int:pk>/", views.detail, name="detail"),
    path("about/", views.about, name="about"),
    path("old-home/", views.old_home, name="old_home"),
]

blog/views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_GET, require_http_methods
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import PostForm
from django.core.paginator import Paginator
from django.db.models import Q
from .models import Category, Post

@require_GET
def home(request):
    posts = Post.objects.filter(status=Post.Status.PUBLISHED).select_related("author", "category")
    q = request.GET.get("q", "").strip()[:100]
    category = request.GET.get("category", "").strip()[:50]
    if q:
        posts = posts.filter(Q(title__icontains=q) | Q(body__icontains=q))
    if category:
        posts = posts.filter(category__slug=category)
    page_obj = Paginator(posts, 6).get_page(request.GET.get("page"))
    return render(request, "blog/home.html", {"page_obj": page_obj, "q": q,
                  "selected_category": category, "categories": Category.objects.all()})

@require_GET
def detail(request, pk):
    visible = Q(status=Post.Status.PUBLISHED)
    if request.user.is_authenticated:
        visible |= Q(author=request.user)
    post = get_object_or_404(Post.objects.select_related("author", "category").filter(visible), pk=pk)
    return render(request, "blog/detail.html", {"post": post})

@require_GET
def about(request):
    return render(request, "blog/about.html")

@require_GET
def old_home(request):
    return redirect("blog:home")

@login_required
@require_GET
def mine(request):
    page_obj = Paginator(Post.objects.filter(author=request.user).select_related("category"), 10).get_page(request.GET.get("page"))
    return render(request, "blog/mine.html", {"page_obj": page_obj})

@login_required
@require_http_methods(["GET", "POST"])
def create(request):
    form = PostForm(request.POST if request.method == "POST" else None)
    if request.method == "POST" and form.is_valid():
        post = form.save(commit=False)
        post.author = request.user
        post.save()
        messages.success(request, "文章已保存。")
        return redirect(post)
    return render(request, "blog/form.html", {"form": form, "heading": "写文章"})

@login_required
@require_http_methods(["GET", "POST"])
def edit(request, pk):
    post = get_object_or_404(Post, pk=pk, author=request.user)
    form = PostForm(request.POST if request.method == "POST" else None, instance=post)
    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, "修改已保存。")
        return redirect(post)
    return render(request, "blog/form.html", {"form": form, "heading": "编辑文章"})

@login_required
@require_http_methods(["GET", "POST"])
def delete(request, pk):
    post = get_object_or_404(Post, pk=pk, author=request.user)
    if request.method == "POST":
        post.delete()
        messages.success(request, "文章已删除。")
        return redirect("blog:mine")
    return render(request, "blog/confirm_delete.html", {"post": post})

templates/base.html

{% load static %}
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}知行博客{% endblock %}</title>
<link rel="stylesheet" href="{% static 'blog/site.css' %}"></head>
<body>
<a href="#main">跳到正文</a>
<header><nav aria-label="主导航"><a href="{% url 'blog:home' %}">知行博客</a> <a href="{% url 'blog:about' %}">关于</a>{% if user.is_authenticated %}<a href="{% url 'blog:create' %}">写文章</a><a href="{% url 'blog:mine' %}">我的文章</a><span>{{ user.username }}</span><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">退出</button></form>
{% else %}<a href="{% url 'login' %}">登录</a><a href="{% url 'register' %}">注册</a>{% endif %}</nav></header>
<main id="main">{% if messages %}<ul class="messages" role="status">{% for message in messages %}<li>{{ message }}</li>{% endfor %}</ul>{% endif %}{% block content %}{% endblock %}</main>
<footer>知行博客 · 记录、实践、分享</footer>
</body></html>

templates/blog/confirm_delete.html

{% extends "base.html" %}
{% block content %}<h1>确认删除</h1><p>即将删除:{{ post.title }}。确认后无法通过页面撤销。</p>
<form method="post">{% csrf_token %}<button type="submit">确认删除</button> <a href="{{ post.get_absolute_url }}">保留文章</a></form>{% endblock %}

templates/blog/detail.html

{% extends "base.html" %}
{% block title %}{{ post.title }} · 知行博客{% endblock %}
{% block content %}<article>
<h1>{{ post.title }}</h1><p class="muted">{{ post.author.username }} · {{ post.category.name }} · {{ post.created_at|date:"Y-m-d H:i" }}</p>
{% if post.status == 'draft' %}<p>这是一篇草稿,只有作者可以查看。</p>{% endif %}
{% if user.pk == post.author_id %}<p><a href="{% url 'blog:edit' post.pk %}">编辑</a> · <a href="{% url 'blog:delete' post.pk %}">删除</a></p>{% endif %}
<div>{{ post.body|linebreaks }}</div>
</article>{% endblock %}

templates/blog/form.html

{% extends "base.html" %}
{% block title %}{{ heading }} · 知行博客{% endblock %}
{% block content %}<h1>{{ heading }}</h1><form method="post" class="panel">{% csrf_token %}
{{ form.as_p }}<button type="submit">保存文章</button> <a href="{% url 'blog:mine' %}">取消</a></form>{% endblock %}

templates/blog/mine.html

{% extends "base.html" %}
{% block content %}<h1>我的文章</h1><a class="button" href="{% url 'blog:create' %}">写文章</a>
{% for post in page_obj %}<article><h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2><p>{{ post.get_status_display }} · {{ post.category.name }}</p></article>{% empty %}<p>从第一篇笔记开始。</p>{% endfor %}
{% include "blog/pagination.html" %}{% endblock %}

如何确认成功

账号 A 新建草稿,详情返回 200,首页不显示;账号 B 和未登录窗口访问该地址返回 404。A 发布文章后,所有人可读,但只有 A 能编辑。B 直接访问或 POST A 的编辑地址仍返回 404。

打开删除确认页后刷新,文章仍存在;点击“保留文章”返回详情;点击确认删除才移除记录。未登录访问新建页应跳转登录,并在成功登录后回到原页面。

练习:使用两个账号完成上面的检查表。提交空标题时,页面应显示原正文和字段错误;解释为什么不能遇到校验失败就 redirect 到空表单。

常见问题

为什么作者按钮隐藏了还要查归属? 请求可以绕过页面直接发送,界面只改善体验,服务端才决定权限。后台超级用户是否也能前台编辑他人文章? 当前前台明确只允许作者;后台的运营权限单独管理,不要默认把超级用户权限扩散到所有流程。并发修改会怎样? 当前编辑是最后一次保存生效,第 23 章会为发布状态和审计建立事务;需要多人协作时还应加入版本号和冲突提示。

官方参考

把学习连起来

登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。

登录并保存学习进度 →
联系站长

微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。

章节讨论 0