文章增删改与对象权限
学习目标
构建作者工作区、新建、编辑、删除和草稿预览,理解登录检查与对象归属检查为什么缺一不可。
为什么每次写入都要检查对象
login_required 只说明请求有用户身份,并不说明这个用户拥有某篇文章。编辑和删除必须在查询时附加 author=request.user,不能相信表单传来的 author 或隐藏 id。返回 404 同时避免泄露不属于当前用户的记录是否存在。
创建使用 commit=False,在服务端设置作者;编辑使用 instance 绑定已有对象,校验失败时保留用户输入;删除 GET 只展示确认,POST 才改变数据库。保存后重定向到详情,避免浏览器刷新重复写入,这叫 POST/Redirect/GET。
具体怎么做
- 更新视图、路由和模板。新增文章作者来自当前会话。
- 我的文章列表只显示当前作者记录,草稿也包含在内。
- 详情查询允许“已发布,或者当前用户是作者”,使作者能够预览草稿。
- 编辑和删除的查询仅按作者范围执行,即使请求人为提交别人的 id 也无效。
- 先创建分类;没有分类时表单无法提交,应由管理员准备有效选项。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
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