Files
DailyChallenge_ProgrammingD…/进阶_Django/参考解答.md
T

6.9 KiB
Raw Blame History

进阶任务参考解答

说明:这里是完整参考实现,按文件拆成四块。 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 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]}"

思考题答案(供对照):

  1. on_delete=models.CASCADE:作者被删除时,他写的文章(以及文章下的评论)级联删除, 不会留下指向不存在作者的孤儿数据。若改成 SET_NULL,删作者时文章的 author 会被置为 NULL——但前提是外键字段允许为空,即 author = models.ForeignKey(Member, null=True, blank=True, on_delete=models.SET_NULL)。
  2. 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)
  • 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"