跳转至

第 9 章:报纸项目

本章和本书的剩余部分将专注于构建一个可用于生产环境的报纸网站。选择这个项目是为了向 Django 作为报纸 CRM 的起源致敬。它提供了引入更多功能的机会,包括高级用户身份验证和样式设计、复杂的数据模型、权限、部署等。

初始设置

第一步是从命令行创建一个新的 Django 项目。我们需要执行熟悉的步骤:创建并进入一个名为 news 的新目录,安装并激活一个名为 .venv 的新虚拟环境。

# Windows
$ cd onedrive\desktop\code
$ mkdir news
$ cd news
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $

# macOS
$ cd ~/desktop/code
$ mkdir news
$ cd news
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $

接下来,安装 Django 和 Black,创建一个名为 django_project 的新 Django 项目,并创建一个名为 accounts 的新应用。

(.venv) $ python -m pip install django~=5.0.0
(.venv) $ python -m pip install black
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py startapp accounts

请注意,我们没有运行 migrate 来配置数据库。鉴于用户模型与 Django 其余部分的紧密联系,在创建新的自定义用户模型之前等待是很重要的。

在网页浏览器中,导航到 http://127.0.0.1:8000,你会看到熟悉的 Django 欢迎页面。

Git

新项目的开始是初始化 Git 并在 GitHub 上创建仓库的绝佳时机。我们之前已经做过好几次了,所以可以使用相同的命令来初始化一个新的本地 Git 仓库并检查其状态。

(.venv) $ git init
(.venv) $ git status

.venv 目录、__pycache__ 目录和 SQLite 数据库不应包含在 Git 中,所以在你的文本编辑器中创建一个项目级别的 .gitignore 文件。

# .gitignore
.venv/
__pycache__/
db.sqlite3

再次运行 git status 以确认 .venv 目录和 SQLite 数据库未被包含。然后,添加我们其余的工作并提交一条消息。

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "initial commit"

在 GitHub 上创建一个新仓库并提供一个名称。我选择了 news;我的用户名是 wsvincent。请确保在下面的命令中使用你自己的仓库名称和用户名。

(.venv) $ git remote add origin https://github.com/wsvincent/news.git
(.venv) $ git branch -M main
(.venv) $ git push -u origin main

全部搞定!

用户资料 vs 自定义用户模型

Django 的内置 User 模型允许我们立即开始使用用户,就像我们在前面章节中的博客应用所做的那样。然而,大多数大型项目需要一种方式来添加与用户相关的信息,例如年龄或任意数量的其他字段。有两种流行的方法。

第一种被称为"用户资料"方法,通过创建一个 OneToOneField扩展现有的 User 模型,指向一个包含额外信息字段的单独模型。其理念是将身份验证保留给 User,而不与非身份验证相关的用户信息捆绑在一起。

第二种方法是创建一个自定义用户模型,它扩展 User 但允许添加额外的用户信息。Django 文档建议在开始新项目时使用自定义用户模型,因为与使用默认 User 模型相比,它使后续的自定义工作容易得多。可以使用 AbstractUser 来创建自定义用户模型,其行为与默认 User 模型完全相同,但允许自定义。

也可以实现一种将自定义用户模型和用户资料模型结合的混合方法。但对于这个项目,我们将坚持使用基于 AbstractUser 的基本自定义用户模型。

AbstractUser

我们可以通过四个步骤创建自定义用户模型:

  • 更新 django_project/settings.py
  • 添加新的 CustomUser 模型
  • UserCreationFormUserChangeForm 添加新表单
  • 更新 accounts/admin.py

django_project/settings.py 中,我们将 accounts 应用添加到 INSTALLED_APPS。然后在文件底部,使用 AUTH_USER_MODEL 配置告诉 Django 使用我们新的自定义用户模型,而不是内置的 User 模型。我们将自定义用户模型命名为 CustomUser。由于它将存在于我们的 accounts 应用中,我们应该将其引用为 accounts.CustomUser

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "accounts",  # new
]
...
AUTH_USER_MODEL = "accounts.CustomUser"  # new

接下来更新 accounts/models.py,添加一个名为 CustomUser 的新 User 模型,它扩展现有的 AbstractUser。我们还将在此处包含一个自定义的 age 字段。

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    age = models.PositiveIntegerField(null=True, blank=True)

如果你阅读了关于自定义用户模型的文档,你会看到它推荐使用 AbstractBaseUser 而不是 AbstractUser,这对初学者来说会使事情变得复杂。如果我们使用 AbstractUser,使用 Django 会更简单,并且仍然可以自定义。

那为什么还要使用 AbstractBaseUser 呢?如果你想要精细的控制和自定义,AbstractBaseUser 是合理的。但它需要重写 Django 的核心部分。如果我们想要一个可以用额外字段更新的自定义用户模型,更好的选择是 AbstractUser,它是 AbstractBaseUser 的子类。换句话说,我们编写的代码更少,出错的机会也更少。除非你真的知道自己在用 Django 做什么,否则它是更好的选择!

请注意,我们在 age 字段中同时使用了 nullblank。这两个术语容易混淆,但有明显的区别:

  • null 与数据库相关。当一个字段设置了 null=True 时,它可以将数据库条目存储为 NULL,表示没有值。
  • blank 与验证相关。如果 blank=True,则表单将允许空值;如果 blank=False,则该字段为必填项。

在实践中,nullblank 通常以这种方式一起使用,以便表单允许空值,并且数据库将该值存储为 NULL。

一个常见的错误是字段类型决定了如何使用这些值。当你有一个基于字符串的字段(如 CharFieldTextField)时,像我们一样同时设置 nullblank 会导致数据库中出现两种"无数据"的可能值,这不是一个好主意。相反,Django 的惯例是使用空字符串 "",而不是 NULL。

表单

如果我们退一步思考,我们通常会如何与新的 CustomUser 模型交互?一种情况是用户在我们的网站上注册新账户。另一种是在 admin 应用中,它允许我们作为超级用户修改现有用户。所以我们需要更新两个内置表单来实现这些功能:UserCreationFormUserChangeForm

创建一个名为 accounts/forms.py 的新文件,并用以下代码更新它,以扩展现有的 UserCreationFormUserChangeForm 表单。

# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = CustomUser
        fields = UserCreationForm.Meta.fields + ("age",)

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields

对于这两个新表单,我们使用 Meta 类来覆盖默认字段,通过将 model 设置为我们的 CustomUser 并使用 Meta.fields 中的默认字段(包含所有默认字段)。要添加我们自定义的 age 字段,我们只需将其追加到末尾,它将自动显示在我们未来的注册页面上。很酷,不是吗?

表单中字段的概念一开始可能会让人困惑,让我们花点时间进一步探索。我们的 CustomUser 模型包含默认 User 模型的所有字段以及我们设置的额外 age 字段。

但这些默认字段是什么呢?事实证明有很多,包括 usernamefirst_namelast_nameemailpasswordgroups 等等。然而,当用户在 Django 上注册新账户时,默认表单只要求提供 username、email 和 password,这告诉我们 UserCreationFormfields 的默认设置只是 usernameemailpassword,尽管还有更多字段可用。

理解 Django 中表单和模型如何交互需要时间和反复实践。如果你现在感到有些困惑,不要气馁!在下一章中,我们将创建注册、登录和注销页面,以更清晰地将我们的 CustomUser 模型和表单联系在一起。

最后一步是更新我们的 admin.py 文件,因为 admin 与默认 User 模型紧密耦合。我们将扩展现有的 UserAdmin 类来使用我们新的 CustomUser 模型。要控制列出哪些字段,我们使用 list_display。但要编辑新的自定义字段(如 age),我们必须添加 fieldsets。而要将新的自定义字段包含在创建新用户的部分中,我们依赖 add_fieldsets

完整代码如下所示:

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = [
        "email",
        "username",
        "age",
        "is_staff",
    ]
    fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("age",)}),)
    add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("age",)}),)

admin.site.register(CustomUser, CustomUserAdmin)

自定义用户 admin 的方法有很多,一些开发者喜欢添加额外的选项,如 list_filtersearch_fieldsordering

但对于这个项目,我们现在已经完成了。输入 Control+c 停止本地服务器,然后运行 makemigrationsmigrate,第一次创建一个使用自定义用户模型的新数据库。

(.venv) $ python manage.py makemigrations accounts
Migrations for 'accounts':
  accounts/migrations/0001_initial.py
    - Create model CustomUser
(.venv) $ python manage.py migrate
Operations to perform:
  Apply all migrations: accounts, admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying accounts.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK

超级用户

让我们创建一个超级用户账户来确认一切正常运行。在命令行中输入以下命令并按照提示操作。

(.venv) $ python manage.py createsuperuser

确保你的超级用户邮箱是一个真正可用的邮箱。我们稍后会用它来验证邮件集成。但这个流程能够正常运行本身就是我们的自定义用户模型设置正确的第一个证明。让我们在 admin 中也查看一下,以进一步确认。启动 Web 服务器。

(.venv) $ python manage.py runserver

然后导航到 http://127.0.0.1:8000/admin 的 admin 页面并登录。如果你点击"Users"链接,你应该能看到你的超级用户账户和默认字段:Email Address、Username、Age 和 Staff Status。这些是在我们 admin.py 文件中的 list_display 中设置的。

Admin 选择用户更改

age 字段是空的,因为我们还没有设置它。但是,你现在可以设置你的年龄,因为我们设置了 fieldsets 部分。点击你的超级用户邮箱地址的高亮链接,打开编辑用户界面。如果你滚动到底部,你会看到我们添加的 age 字段。继续输入你的年龄,然后点击"Save"。

Admin 编辑年龄

它将重定向回列出我们超级用户的主 Users 页面。请注意,age 字段现在已经更新。

Admin 更新后的年龄

测试

每当我们进行更改核心功能的代码修改时,添加测试是一个好主意。虽然我们刚才手动尝试自定义用户的所有操作都成功了,但我们将来可能会破坏某些东西。为新代码添加测试并定期运行整个测试套件有助于尽早发现错误。

从高层次来看,我们希望确保普通用户和超级用户都可以被创建,并且具有正确的字段权限。假设你查看了官方文档中关于 models.User(我们的自定义用户模型继承自它)的内容。在这种情况下,它附带了几个内置字段:usernamefirst_namelast_nameemailpasswordgroupsuser_permissionsis_staffis_activeis_superuserlast_logindate_joined。也可以添加任意数量的自定义字段,正如我们通过添加 age 字段所看到的那样。

成为"staff"意味着用户可以访问 admin 站点并查看他们被授予权限的模型;"superuser"拥有对 admin 及其所有模型的完全访问权限。普通用户应该将 is_active 设置为 Trueis_staff 设置为 Falseis_superuser 设置为 False。超级用户应该将所有内容都设置为 True

以下是为我们的自定义用户模型添加测试的一种方法:

# accounts/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase

class UsersManagersTests(TestCase):
    def test_create_user(self):
        User = get_user_model()
        user = User.objects.create_user(
            username="testuser",
            email="testuser@example.com",
            password="testpass1234",
        )
        self.assertEqual(user.username, "testuser")
        self.assertEqual(user.email, "testuser@example.com")
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)

    def test_create_superuser(self):
        User = get_user_model()
        admin_user = User.objects.create_superuser(
            username="testsuperuser",
            email="testsuperuser@example.com",
            password="testpass1234",
        )
        self.assertEqual(admin_user.username, "testsuperuser")
        self.assertEqual(admin_user.email, "testsuperuser@example.com")
        self.assertTrue(admin_user.is_active)
        self.assertTrue(admin_user.is_staff)
        self.assertTrue(admin_user.is_superuser)

在顶部,我们导入了 get_user_model(),以便测试我们的用户注册。我们还导入了 TestCase,因为这些测试涉及数据库操作。

我们的测试类名为 UsersManagersTests,继承自 TestCase。第一个单元测试 test_create_user 检查普通用户是否表现出预期的行为。第二个单元测试 test_create_superuser 做同样的事情,但针对超级用户账户。

现在运行测试,它们应该顺利通过。

(.venv) $ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.114s

OK
Destroying test database for alias 'default'...

Git

我们已经完成了大量新工作,是时候添加一个 Git 提交了。

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "custom user model"

结论

我们通过添加自定义用户模型和 age 字段来启动我们的新项目。我们还了解了在更改 Django 核心功能时如何添加测试,现在可以专注于构建我们报纸网站的其余部分。在下一章中,我们将通过自定义注册、登录和注销页面来实现高级身份验证和注册功能。