跳转至

第 13 章:Articles 应用

是时候来完善我们的 Newspaper 应用了。我们将创建一个文章页面,让记者可以发布文章,设置权限使得只有文章作者才能编辑或删除文章,最后添加让其他用户可以对每篇文章发表评论的功能。

Articles 应用

首先,创建一个 articles 应用并定义数据库模型。关于应用命名没有硬性规定,只是不能使用内置应用的名称。如果你查看 django_project/settings.py 中的 INSTALLED_APPS 部分,可以看到哪些应用名称是不可用的:

  • admin
  • auth
  • contenttypes
  • sessions
  • messages
  • staticfiles

一个通用的经验法则是使用应用名称的复数形式:posts、payments、users 等。一个例外是当使用复数明显不合适时,比如 blogs。在这种情况下,使用单数形式 blog 更合理。

首先创建我们的新 articles 应用。

(.venv) $ python manage.py startapp articles

然后将其添加到 INSTALLED_APPS 中,并在设置的下方更新时区 TIME_ZONE,因为我们需要为文章添加时间戳。你可以在这个 维基百科时区列表 中找到你的时区。例如,我住在美国马萨诸塞州波士顿,属于美国东部时区;因此,我的条目是 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_MODELget_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/ 并登录。

管理后台页面

如果你点击页面顶部"Articles"旁边的"+ Add",我们可以输入一些示例数据。你可能有三个可用的用户:你的超级用户、testuser 和 testuser2 账户。使用你的超级用户账户作为作者创建新文章。我已经添加了三篇新文章,你可以在更新后的 Articles 页面中看到。

管理后台三篇文章

但是,如果能在管理后台中看到每篇文章的更多信息岂不是更好?我们可以通过使用 list_display 更新 articles/admin.py 来快速实现。

# 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_atupdated_at 属性——但为了保持简单,我们暂时让日期由 Django 在创建时自动设置。虽然日期没有在这里显示,但我们仍然可以在模板中访问它以在网页上显示。

URL 和视图

下一步是配置我们的 URL 和视图。让我们的文章显示在 articles/ 路径下。在 django_project/urls.py 文件中为 articles 添加 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 有一个名为 Cards 的内置组件,我们可以为每篇文章进行自定义。回想一下,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> &middot;
    <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 的日期选项来实现,但我们不会在这里实现它。

详情/编辑/删除

下一步是为文章添加详情、编辑和删除功能。这意味着需要新的 URL、视图和模板。让我们从 URL 开始。Django ORM 会自动为每条数据库记录添加一个主键,这意味着第一篇文章的 pk 值为 1,第二篇为 2,依此类推。我们可以利用这一点来设计 URL 路径。

对于详情页面,我们希望路由位于 articles/<int:pk>。这里的 int 是一个路径转换器,它告诉 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 的通用基于类的视图 DetailViewUpdateViewDeleteView。详情视图只需要列出模型和模板名称。对于更新/编辑视图,我们还需要添加可以更改的特定属性——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(Create-Read-Update-Delete,即创建-读取-更新-删除)这个缩写,你会看到我们在这里实现了四个功能中的三个。我们将在本章后面添加第四个,即创建功能。几乎所有网站都使用 CRUD,当你使用 Django 或其他任何 Web 框架时,这种模式很快就会变得很自然。

URL 路径和视图已经完成,最后一步是添加模板。在你的文本编辑器中创建三个新的模板文件:

  • templates/article_detail.html
  • templates/article_edit.html
  • templates/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 的按钮样式,使编辑按钮为浅蓝色,删除按钮为红色。

<!-- 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 模板标签、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> &middot;
    <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/ 的文章列表页面。点击第一篇文章旁边的"Edit"链接,你将被重定向到 http://127.0.0.1:8000/articles/1/edit/。

编辑页面

如果你在"Title"属性末尾添加"(edited)"并点击"Update",你将被重定向到详情页面,显示新的更改。

详情页面

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

删除页面

按下那个可怕的红色"Confirm"按钮。你将被重定向到文章页面,现在只有两条记录。

两条记录的文章页面

创建页面

最后一步是为新文章创建一个页面,我们可以使用 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 文件现在是准确的,请参考官方源代码

Git

在本章中我们添加了很多新代码,所以在进入下一章之前,让我们用 Git 保存一下。

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "newspaper app"
(.venv) $ git push origin main

结论

我们现在已经创建了一个具有 CRUD 功能的专用 articles 应用。文章可以被创建、读取、更新、删除,甚至可以作为完整列表查看。但目前还没有权限或授权,这意味着任何人都可以做任何事情!如果未登录的用户知道正确的 URL,他们可以编辑现有文章或删除文章,甚至不是他们自己的文章!在下一章中,我们将为项目添加权限和授权来解决这个问题。