Add Day_4: 设计一个真正的Django小应用
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
# Day_4 参考解答
|
||||
|
||||
> 说明:这里是完整参考实现,按文件拆成四块。
|
||||
> 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 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)
|
||||
- [ ] 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"
|
||||
Reference in New Issue
Block a user