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

205 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 进阶任务参考解答
> 说明:这里是完整参考实现,按文件拆成四块。
> 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 views.py。
> 提醒:后端请务必自己手写,验收时会围绕这些代码提问。
---
## 1. blog/models.py
```python
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
```python
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**:
```python
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**:
```python
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 模板片段
```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)
- [ ] 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"