6.9 KiB
6.9 KiB
进阶任务参考解答
说明:这里是完整参考实现,按文件拆成四块。 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 views.py。 提醒:后端请务必自己手写,验收时会围绕这些代码提问。
1. blog/models.py
from django.db import models
class Member(models.Model):
"""社团成员"""
nickname = models.CharField(max_length=50)
email = models.EmailField(unique=True) # 邮箱唯一
joined_at = models.DateTimeField(auto_now_add=True) # 创建时自动记录
bio = models.TextField(blank=True, null=True) # 允许为空
def __str__(self):
return self.nickname
class Article(models.Model):
"""文章"""
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey(
Member,
on_delete=models.CASCADE, # 作者被删 => 文章一起删
related_name="articles", # member.articles.all() 能取到该作者全部文章
)
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
views = models.PositiveIntegerField(default=0) # 浏览量不会为负,用 PositiveIntegerField
class Meta:
ordering = ["-created_at"] # 新文章在前
def __str__(self):
return self.title
class Comment(models.Model):
"""评论"""
content = models.TextField()
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name="comments", # article.comments.all() 能取到该文章全部评论
)
commenter = models.ForeignKey(Member, on_delete=models.CASCADE)
class Meta:
ordering = ["id"] # 按评论先后排序
def __str__(self):
return f"评论({self.commenter}):{self.content[:20]}"
思考题答案(供对照):
on_delete=models.CASCADE:作者被删除时,他写的文章(以及文章下的评论)级联删除, 不会留下指向不存在作者的孤儿数据。若改成SET_NULL,删作者时文章的author会被置为NULL——但前提是外键字段允许为空,即author = models.ForeignKey(Member, null=True, blank=True, on_delete=models.SET_NULL)。related_name="comments":定义反向查询的名字。有了它,从文章查评论就是article.comments.all()(不需要自己再写Comment.objects.filter(article=...))。
2. blog/views.py
from django.db.models import Count, F
from django.shortcuts import render, get_object_or_404, redirect
from django.http import Http404
from .models import Article, Comment, Member
def index(request):
# 只要已发布文章,按时间倒序(Meta.ordering 已生效,order_by 可省略,写上更明确)
articles = Article.objects.filter(is_published=True).order_by("-created_at")
return render(request, "blog/index.html", {"articles": articles})
def article_detail(request, pk):
# get_object_or_404 = get() + 捕获 DoesNotExist 后抛 Http404
article = get_object_or_404(Article, pk=pk)
# 浏览量 +1。为什么不用 article.views += 1; article.save()?
# 两个用户同时点进来:两人都读到 views=10,各自 +1 后都写回 11,
# 丢了 1 次访问——这就是"并发丢失更新"。
# 用 F("views") + 1 是在数据库层完成"读旧值+1再写回",
# 原子操作,不会丢更新。
Article.objects.filter(pk=pk).update(views=F("views") + 1)
return render(request, "blog/detail.html", {"article": article})
def add_comment(request, pk):
article = get_object_or_404(Article, pk=pk)
if request.method == "POST":
content = request.POST.get("content", "").strip()
if content:
member = Member.objects.first() # 简化:还没做登录,先随便取一个成员
Comment.objects.create(
content=content,
article=article,
commenter=member,
)
return redirect("article_detail", pk=pk) # POST 后重定向,避免刷新重复提交
def author_articles(request, nickname):
author = get_object_or_404(Member, nickname=nickname)
# 进阶:annotate 在数据库层完成"按文章分组数评论数",
# 返回的每篇文章都带一个动态属性 comment_count。
articles = (
Article.objects.filter(author=author)
.annotate(comment_count=Count("comments")) # 反向查询名直接拿来用
.order_by("-created_at")
)
return render(request, "blog/author.html", {"author": author, "articles": articles})
3. 路由
项目级 mysite/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")), # 把 / 开头的请求交给 blog 应用处理
]
应用级 blog/urls.py:
from django.urls import path
from . import views
app_name = "blog" # 配合 name 使用:模板里 {% url 'blog:article_detail' a.pk %}
urlpatterns = [
path("", views.index, name="index"),
path("article/<int:pk>/", views.article_detail, name="article_detail"),
path("article/<int:pk>/comment/", views.add_comment, name="add_comment"),
path("author/<str:nickname>/", views.author_articles, name="author_articles"),
]
路由命名与反向解析:name 给路由起了名字,模板和视图里用
{% url 'blog:article_detail' a.pk %} / redirect("article_detail", pk=pk) 引用,
而不是手写死 URL 字符串——这样以后改路径,代码不用跟着改。
4. 完整链路说明(题目三第 2 问示例答案)
用户在浏览器输入
/article/3/回车后:Django 首先在 URL 路由表中逐条匹配, 命中path("article/<int:pk>/"),把pk=3提取出来并调用对应的视图函数article_detail(request, pk=3)。视图内执行 ORM 查询(get_object_or_404对应 一条 SELECT),拿到文章对象后调用render把数据填入模板、生成 HTML, 最终打包成HttpResponse返回给浏览器渲染。
5. 进阶:index.html 模板片段
{% if articles %}
<ul>
{% for a in articles %}
<li>
<a href="{% url 'blog:article_detail' a.pk %}">{{ a.title }}</a>
<span>{{ a.created_at|date:"Y-m-d H:i" }}</span>
· 作者:{{ a.author.nickname }}
</li>
{% endfor %}
</ul>
{% else %}
<p>暂无文章,快来发布第一篇吧!</p>
{% endif %}
验收清单
python manage.py makemigrations blog与migrate无报错runserver后,列表页 / 详情页 / 评论提交 / 作者页全部可访问- 删掉一个 Member 后,他的文章和评论也一起消失(验证 CASCADE)
- 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"