第 13 章:文章应用
是时候构建我们的 Newspaper 应用了。我们将创建一个文章页面,让记者可以发布文章;设置权限以确保只有文章的作者才能编辑或删除它;最后添加让其他用户对每篇文章发表评论的功能。
文章应用
首先,创建一个文章应用并定义数据库模型。关于应用命名的规则没有硬性规定,唯一的限制是不能使用内置应用的名称。查看 django_project/settings.py 中的 INSTALLED_APPS 部分,你可以看到哪些应用名称是禁用的:
adminauthcontenttypessessionsmessagesstaticfiles
一般的经验法则是使用应用名称的复数形式:posts、payments、users 等。唯一的例外是当使用复数形式明显错误时,比如 blogs,这时使用单数形式 blog 更合理。
先创建新的 articles 应用。
(.venv) $ python manage.py startapp articles然后将其添加到 INSTALLED_APPS 中,并在 settings 中更新时区 TIME_ZONE(较低的位置),因为我们需要为文章添加时间戳。你可以在这个 Wikipedia 列表中找到你的时区(https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)。例如,我住在马萨诸塞州波士顿,属于美国东部时区,因此我的时区设置为 America/New_York。
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 3rd Party
"crispy_forms",
"crispy_bootstrap5",
# Local
"accounts",
"pages",
"articles", # new
]
TIME_ZONE = "America/New_York" # new接下来,定义我们的数据库模型,包含四个字段:title、body、date 和 author。我们让 Django 根据 TIME_ZONE 设置自动设置时间和日期。对于 author 字段,我们引用自定义用户模型 "accounts.CustomUser"(已在 django_project/settings.py 中通过 AUTH_USER_MODEL 设置)。我们还将实现最佳实践,定义 get_absolute_url 方法和 __str__ 方法,以便在管理界面中查看模型。
# articles/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse
class Article(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
date = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("article_detail", kwargs={"pk": self.pk})有两种方式引用自定义用户模型:AUTH_USER_MODEL 和 get_user_model()(https://docs.djangoproject.com/en/5.0/topics/auth/customizing/#django.contrib.auth.get_user_model)。一般建议如下:
AUTH_USER_MODEL适用于models.py文件中的引用get_user_model()推荐在其他地方使用,例如视图、测试等
由于我们有了新的应用和模型,是时候创建迁移文件并将其应用到数据库了。
(.venv) $ python manage.py makemigrations articles
Migrations for 'articles':
articles/migrations/0001_initial.py
- Create model Article
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: accounts, admin, articles, auth, contenttypes, sessions
Running migrations:
Applying articles.0001_initial... OK此时,我喜欢先进入管理后台试用一下模型,然后再构建显示数据所需的 URL/视图/模板。但首先,我们需要更新 articles/admin.py,让我们的新应用显示出来。
# articles/admin.py
from django.contrib import admin
from .models import Article
admin.site.register(Article)现在启动服务器。
(.venv) $ python manage.py runserver访问 http://127.0.0.1:8000/admin/ 并登录。

如果你点击顶部的”文章”旁边的”+ 添加”,可以输入一些示例数据。你可能有三个可用用户:超级用户、testuser 和 testuser2。使用你的超级用户账户作为作者创建新文章。我已经添加了三篇新文章,如下图所示。

但如果能在管理后台看到每篇文章的更多信息就更好了,对吧?我们可以通过更新 articles/admin.py 并使用 list_display(https://docs.djangoproject.com/en/5.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display)来快速实现。
# articles/admin.py
from django.contrib import admin
from .models import Article
class ArticleAdmin(admin.ModelAdmin):
list_display = [
"title",
"body",
"author",
]
admin.site.register(Article, ArticleAdmin)我们继承了 ModelAdmin(一个代表管理界面中模型的类),并通过 list_display 指定了要列出的字段。在文件底部,我们注册了 ArticleAdmin 以及顶部导入的 Article 模型。Django 管理后台还有许多可用的自定义选项,所以官方文档值得仔细阅读。

如果你点击某篇文章,会看到标题、正文和作者都已显示,但日期没有显示——尽管我们在模型中定义了 date 字段。这是因为日期是由 Django 自动添加的,因此在管理后台中无法更改。我们可以让日期可编辑——在更复杂的应用中,通常同时包含 created_at 和 updated_at 属性——但为简单起见,我们暂时让 Django 在创建时自动设置日期。虽然日期在这里没有显示,但我们仍然可以在模板中访问它,用于在网页上显示。
URL 和视图
下一步是配置 URL 和视图。让我们的文章出现在 articles/ 路径下。在 django_project/urls.py 文件中添加文章的 URL 模式。
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("accounts.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("articles/", include("articles.urls")), # new
path("", include("pages.urls")),
]接下来,在文本编辑器中创建一个新的 articles/urls.py 文件,并填入路由。让我们从 articles/ 路径下的文章列表页面开始,它将使用 ArticleListView 视图。
# articles/urls.py
from django.urls import path
from .views import ArticleListView
urlpatterns = [
path("", ArticleListView.as_view(), name="article_list"),
]现在使用 Django 内置的通用 ListView 创建视图。我们只需要指定两个属性:模型 Article 和模板名称 article_list.html。
# articles/views.py
from django.views.generic import ListView
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = "article_list.html"最后一步是在文本编辑器中创建一个名为 templates/article_list.html 的新模板文件。Bootstrap 有一个名为卡片(https://getbootstrap.com/docs/5.3/components/card/)的内置组件,我们可以为每篇文章定制它。回想一下,ListView 返回一个名为 <model_name>_list 的对象,我们可以使用 for 循环对其进行遍历。
我们显示每篇文章的标题、正文、作者和日期。甚至可以提供指向”详情”、“编辑”和”删除”页面的链接——尽管这些页面我们还没有构建。
<!-- templates/article_list.html -->
{% extends "base.html" %}
{% block title %}Articles{% endblock title %}
{% block content %}
{% for article in article_list %}
<div class="card">
<div class="card-header">
<span class="fw-bold">
<a href="#">{{ article.title }}</a>
</span> ·
<span class="text-muted">by {{ article.author }} |
{{ article.date }}</span>
</div>
<div class="card-body">
{{ article.body }}
</div>
<div class="card-footer text-center text-muted">
<a href="#">Edit</a> <a href="#">Delete</a>
</div>
</div>
<br />
{% endfor %}
{% endblock content %}重新启动服务器,访问 http://127.0.0.1:8000/articles/ 查看效果。

不错吧?如果我们想做得更花哨一些,可以创建一个自定义模板过滤器,让日期以秒、分钟或天为单位显示。这可以通过一些 if/else 逻辑和 Django 的日期选项来实现,但我们在这里不进行实现。
如果你想深入了解,可以查看 Django 文档中关于自定义模板过滤器(https://docs.djangoproject.com/en/5.0/howto/custom-template-tags/)和日期选项(https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#date)的详细说明。
详情/编辑/删除
下一步是添加文章的详情、编辑和删除功能。这意味着需要新的 URL、视图和模板。先从 URL 开始。Django ORM 会自动为每个数据库条目添加一个主键,这意味着第一篇文章的 pk 值为 1,第二篇为 2,依此类推。我们可以利用这一点来构建 URL 路径。
对于详情页面,我们希望路由为 articles/<int:pk>。这里的 int 是一个路径转换器(https://docs.djangoproject.com/en/5.0/topics/http/urls/#path-converters),告诉 Django 我们希望将这个值视为整数,而不是字符串等其他数据类型。因此,第一篇文章的 URL 路由将是 articles/1/。由于我们在 articles 应用中,所有 URL 路由都会以 articles/ 为前缀(因为我们在 django_project/urls.py 中设置了这一点)。我们只需要在这里添加 <int:pk> 部分。
接下来是编辑和删除路由,它们也使用主键。它们的 URL 路由分别是 articles/1/edit/ 和 articles/1/delete/(主键为 1 时)。以下是更新后的 articles/urls.py 文件。
# articles/urls.py
from django.urls import path
from .views import (
ArticleListView,
ArticleDetailView, # new
ArticleUpdateView, # new
ArticleDeleteView, # new
)
urlpatterns = [
path("<int:pk>/", ArticleDetailView.as_view(),
name="article_detail"), # new
path("<int:pk>/edit/", ArticleUpdateView.as_view(),
name="article_edit"), # new
path("<int:pk>/delete/", ArticleDeleteView.as_view(),
name="article_delete"), # new
path("", ArticleListView.as_view(),
name="article_list"),
]我们将使用 Django 的通用类视图:DetailView、UpdateView 和 DeleteView。详情视图只需要指定模型和模板名称。更新/编辑视图还需要添加可以修改的具体属性(title 和 body)。对于删除视图,我们必须添加一个重定向,指定删除条目后将用户发送到何处。这需要导入 reverse_lazy 并指定 success_url 及对应的命名 URL。
# articles/views.py
from django.views.generic import ListView, DetailView # new
from django.views.generic.edit import UpdateView, DeleteView # new
from django.urls import reverse_lazy # new
from .models import Article
class ArticleListView(ListView):
model = Article
template_name = "article_list.html"
class ArticleDetailView(DetailView): # new
model = Article
template_name = "article_detail.html"
class ArticleUpdateView(UpdateView): # new
model = Article
fields = (
"title",
"body",
)
template_name = "article_edit.html"
class ArticleDeleteView(DeleteView): # new
model = Article
template_name = "article_delete.html"
success_url = reverse_lazy("article_list")如果你还记得 CRUD(创建-读取-更新-删除)这个缩写,你会发现我们在这里实现了四种功能中的三种。我们将在本章后面添加第四种——创建功能。几乎所有网站都使用 CRUD,在使用 Django 或其他任何 Web 框架时,这种模式很快就会变得很自然。
URL 路径和视图已经完成,最后一步是添加模板。在你的文本编辑器中创建三个新的模板文件:
templates/article_detail.htmltemplates/article_edit.htmltemplates/article_delete.html
先从详情页面开始,它显示标题、日期、正文和作者,并提供编辑和删除的链接。它还提供了返回所有文章的链接。DetailView 会自动将上下文对象命名为 object 或模型名称的小写形式。回想一下,Django 模板语言的 url 标签需要 URL 名称,任何参数都会依次传入。
编辑路由的名称是 article_edit,需要传入主键 article.pk。删除路由的名称是 article_delete,同样需要主键 article.pk。文章页面是一个 ListView,因此不需要传入任何额外参数。
<!-- templates/article_detail.html -->
{% extends "base.html" %}
{% block content %}
<div class="article-entry">
<h2>{{ object.title }}</h2>
<p>by {{ object.author }} | {{ object.date }}</p>
<p>{{ object.body }}</p>
</div>
<div>
<p><a href="{% url 'article_edit' article.pk %}">Edit</a>
<a href="{% url 'article_delete' article.pk %}">Delete</a>
</p>
<p>Back to <a href="{% url 'article_list' %}">All Articles</a>.</p>
</div>
{% endblock content %}对于编辑和删除页面,我们可以使用 Bootstrap 的按钮样式(https://getbootstrap.com/docs/5.3/components/buttons/),让编辑按钮显示浅蓝色,删除按钮显示红色。
<!-- templates/article_edit.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<h1>Edit</h1>
<form action="" method="post">{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-info ms-2" type="submit">Update</button>
</form>
{% endblock content %}<!-- templates/article_delete.html -->
{% extends "base.html" %}
{% block content %}
<h1>Delete</h1>
<form action="" method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ article.title }}"?</p>
<button class="btn btn-danger ms-2" type="submit">Confirm</button>
</form>
{% endblock content %}最后一步,在 article_list.html 中,我们可以为详情、编辑和删除页面添加 URL 路由,替换掉现有的 <a href="#"> 占位符。我们可以使用模型中定义的 get_absolute_url 方法作为详情页面的链接。对于编辑和删除链接,我们使用 url(https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#url)模板标签、URL 名称和每篇文章的 pk。
<!-- templates/article_list.html -->
{% extends "base.html" %}
{% block title %}Articles{% endblock title %}
{% block content %}
{% for article in article_list %}
<div class="card">
<div class="card-header">
<span class="fw-bold">
<!-- add link here! -->
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</span> ·
<span class="text-muted">by {{ article.author }} |
{{ article.date }}</span>
</div>
<div class="card-body">
{{ article.body }}
</div>
<div class="card-footer text-center text-muted">
<!-- new links here! -->
<a href="{% url 'article_edit' article.pk %}">Edit</a>
<a href="{% url 'article_delete' article.pk %}">Delete</a>
</div>
</div>
<br />
{% endfor %}
{% endblock content %}好了,我们准备查看成果了。使用 python manage.py runserver 启动服务器,访问文章列表页面 http://127.0.0.1:8000/articles/。点击第一篇文章旁边的”编辑”链接,你将被重定向到 http://127.0.0.1:8000/articles/1/edit/。

如果你在标题末尾添加”(edited)“并点击”更新”,你将被重定向到详情页面,显示新的更改。

如果你点击”删除”链接,你将被重定向到删除页面。

点击可怕的红色”确认”按钮。你将被重定向到文章页面,现在只剩下两篇文章。

创建页面
最后一步是创建一个用于发布新文章的页面,我们可以使用 Django 内置的 CreateView 来实现。我们的三个步骤是创建视图、URL 和模板。这个流程现在应该已经很熟悉了。
在 articles/views.py 文件中,将 CreateView 添加到顶部的导入中,并在文件底部创建一个名为 ArticleCreateView 的新类,指定模型、模板和可用字段。
# articles/views.py
...
from django.views.generic.edit import (
UpdateView, DeleteView, CreateView # new
)
...
class ArticleCreateView(CreateView): # new
model = Article
template_name = "article_new.html"
fields = (
"title",
"body",
"author",
)注意,fields 属性中包含 author,因为我们要将新文章与作者关联;但是,一旦文章创建完成,我们不希望用户能够更改作者,这就是为什么 ArticleUpdateView 只有 ['title', 'body'] 属性。
现在,用新路由更新 articles/urls.py 文件。
# articles/urls.py
from django.urls import path
from .views import (
ArticleListView,
ArticleDetailView,
ArticleUpdateView,
ArticleDeleteView,
ArticleCreateView, # new
)
urlpatterns = [
path("<int:pk>/",
ArticleDetailView.as_view(), name="article_detail"),
path("<int:pk>/edit/",
ArticleUpdateView.as_view(), name="article_edit"),
path("<int:pk>/delete/",
ArticleDeleteView.as_view(), name="article_delete"),
path("new/", ArticleCreateView.as_view(), name="article_new"), # new
path("", ArticleListView.as_view(), name="article_list"),
]为了完成新的创建功能,添加一个名为 templates/article_new.html 的模板,并用以下 HTML 代码更新它。
<!-- templates/article_new.html -->
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<h1>New article</h1>
<form action="" method="post">{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success ms-2" type="submit">Save</button>
</form>
{% endblock content %}额外链接
我们应该在导航栏中添加创建新文章的 URL 链接,以便登录用户可以在站点的任何位置访问它。
<!-- templates/base.html -->
...
{% if user.is_authenticated %}
<li><a href="{% url 'article_new' %}"
class="nav-link px-2 link-dark">+ New</a></li>
...刷新网页并点击”+ New”将重定向到创建新文章页面。

最后一个需要添加的链接是让文章列表页面可以从首页访问。用户需要知道或猜测其地址是 http://127.0.0.1:8000/articles/,但我们可以通过在 templates/home.html 文件中添加一个按钮链接来解决这个问题。
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock title %}
{% block content %}
{% if user.is_authenticated %}
Hi {{ user.username }}! You are {{ user.age }} years old.
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit">Log Out</button>
</form>
<br/>
<p><a class="btn btn-primary" href="{% url 'article_list' %}"
role="button">View all articles</a></p> <!-- new -->
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a> |
<a href="{% url 'signup' %}">Sign Up</a>
{% endif %}
{% endblock content %}刷新首页,按钮将会出现并按预期工作。

如果你需要确认你的 HTML 文件是否准确,请参考官方源代码(https://github.com/wsvincent/djangoforbeginners/)。你也可以查看完整的代码(https://github.com/wsvincent/djangoforbeginners/)。
Git
本章添加了相当多的新代码,所以在进入下一章之前,让我们用 Git 保存进度。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "newspaper app"
(.venv) $ git push origin main小结
我们现在已经创建了一个专用的文章应用,具备 CRUD 功能。文章可以被创建、读取、更新、删除,甚至以列表形式查看。但目前还没有任何权限或授权机制,这意味着任何人都可以做任何事情!如果未登录的用户知道正确的 URL,他们就可以编辑或删除现有文章——即使那不是他们的文章!在下一章中,我们将为项目添加权限和授权功能来解决这个问题。