Django 教程 第 8 部分:用户认证和权限

在本教程中,我们将向您展示如何允许用户使用自己的帐户登录您的网站,以及如何根据他们是否已登录以及他们的权限来控制他们可以执行的操作和查看的内容。作为此演示的一部分,我们将扩展LocalLibrary 网站,添加登录和注销页面,以及供用户和员工查看已借书籍的特定页面。

先决条件 完成所有之前的教程主题,直至并包括Django 教程第 7 部分:会话框架
目标 了解如何设置和使用用户身份验证和权限。

概述

Django 提供了一个身份验证和授权(“权限”)系统,构建在前面教程中讨论的会话框架之上,允许您验证用户凭据并定义每个用户允许执行的操作。该框架包括用于UsersGroups 的内置模型(一种将权限一次应用于多个用户的通用方法)、用于指定用户是否可以执行任务的权限/标志、用于登录用户的表单和视图,以及用于限制内容的视图工具。

注意:根据 Django 的说法,身份验证系统旨在非常通用,因此不提供其他 Web 身份验证系统中提供的一些功能。一些常见问题的解决方案可作为第三方软件包获得。例如,登录尝试的限制和针对第三方的身份验证(例如 OAuth)。

在本教程中,我们将向您展示如何在LocalLibrary 网站中启用用户身份验证,创建您自己的登录和注销页面,向您的模型添加权限,以及控制对页面的访问。我们将使用身份验证/权限来显示用户和管理员已借书籍的列表。

身份验证系统非常灵活,如果您愿意,您可以从头开始构建您的 URL、表单、视图和模板,只需调用提供的 API 来登录用户即可。但是,在本文中,我们将使用 Django 的“默认”身份验证视图和表单来创建我们的登录和注销页面。我们仍然需要创建一些模板,但这非常简单。

我们还将向您展示如何创建权限,以及如何在视图和模板中检查登录状态和权限。

启用身份验证

当我们创建网站骨架(在教程 2 中)时,身份验证已自动启用,因此您现在无需执行任何其他操作。

注意:当我们使用django-admin startproject 命令创建应用程序时,所有必要的配置都已为我们完成。当我们第一次调用python manage.py migrate 时,创建了用户和模型权限的数据库表。

该配置设置在项目文件的INSTALLED_APPSMIDDLEWARE 部分(django-locallibrary-tutorial/locallibrary/settings.py)中,如下所示

python
INSTALLED_APPS = [
    # …
    'django.contrib.auth',  # Core authentication framework and its default models.
    'django.contrib.contenttypes',  # Django content type system (allows permissions to be associated with models).
    # …

MIDDLEWARE = [
    # …
    'django.contrib.sessions.middleware.SessionMiddleware',  # Manages sessions across requests
    # …
    'django.contrib.auth.middleware.AuthenticationMiddleware',  # Associates users with requests using sessions.
    # …

创建用户和组

当我们在教程 4 中查看Django 管理站点时,您已经创建了您的第一个用户(这是一个超级用户,使用命令python manage.py createsuperuser 创建)。我们的超级用户已通过身份验证并拥有所有权限,因此我们需要创建一个测试用户来表示普通站点用户。我们将使用管理站点来创建我们的locallibrary 组和网站登录,因为这是最快的方法之一。

注意:您也可以按如下所示以编程方式创建用户。例如,如果您正在开发一个界面以允许“普通”用户创建自己的登录信息(您不应该授予大多数用户访问管理站点的权限),则必须执行此操作。

python
from django.contrib.auth.models import User

# Create user and save to the database
user = User.objects.create_user('myusername', '[email protected]', 'mypassword')

# Update fields and then save again
user.first_name = 'Tyrone'
user.last_name = 'Citizen'
user.save()

但是,强烈建议在启动项目时设置一个自定义用户模型,因为如果需要,您可以在将来轻松地对其进行自定义。如果使用自定义用户模型,则创建相同用户的代码如下所示

python
# Get current user model from settings
from django.contrib.auth import get_user_model
User = get_user_model()

# Create user from model and save to the database
user = User.objects.create_user('myusername', '[email protected]', 'mypassword')

# Update fields and then save again
user.first_name = 'Tyrone'
user.last_name = 'Citizen'
user.save()

有关更多信息,请参阅启动项目时使用自定义用户模型(Django 文档)。

下面我们将首先创建一个组,然后创建一个用户。即使我们还没有任何要为我们的图书馆成员添加的权限,但如果我们以后需要,将其一次添加到组中而不是单独添加到每个成员中会容易得多。

启动开发服务器并在本地 Web 浏览器中导航到管理站点(http://127.0.0.1:8000/admin/)。使用超级用户帐户的凭据登录站点。管理站点的顶级显示所有模型,按“Django 应用程序”排序。在身份验证和授权部分,您可以单击用户链接以查看其现有记录。

Admin site - add groups or users

首先让我们为我们的图书馆成员创建一个新组。

  1. 单击添加按钮(在组旁边)以创建一个新的;为该组输入名称“图书馆成员”。管理站点 - 添加组
  2. 我们不需要该组的任何权限,因此只需按保存即可(您将被带到一个组列表)。

现在让我们创建一个用户

  1. 导航回管理站点的首页
  2. 单击用户旁边的添加按钮以打开添加用户对话框。管理站点 - 添加用户 pt1
  3. 为您的测试用户输入适当的用户名密码/密码确认
  4. 保存创建用户。管理站点将创建新用户并立即将您带到一个更改用户屏幕,您可以在其中更改您的用户名并为 User 模型的可选字段添加信息。这些字段包括姓氏、名字、电子邮件地址以及用户的状态和权限(仅应设置活动标志)。在下面,您可以指定用户的组和权限,并查看与用户相关的重要的日期(例如,他们的加入日期和上次登录日期)。管理站点 - 添加用户 pt2
  5. 部分,从可用组列表中选择图书馆成员组,然后按框之间的右箭头将其移动到已选组框中。管理站点 - 将用户添加到组
  6. 我们不需要在这里做任何其他事情,因此只需再次选择保存即可转到用户列表。

就是这样!现在您拥有了一个“普通图书馆成员”帐户,您将能够将其用于测试(一旦我们实现了启用他们登录的页面)。

注意:您应该尝试创建另一个图书馆成员用户。此外,为管理员创建一个组,并将用户也添加到该组中!

设置身份验证视图

Django 提供了您创建身份验证页面以处理登录、注销和密码管理“开箱即用”所需的大部分内容。这包括 URL 映射器、视图和表单,但不包括模板——我们必须自己创建!

在本节中,我们将展示如何将默认系统集成到LocalLibrary 网站并创建模板。我们将把它们放在主项目 URL 中。

注意:您不必使用任何这些代码,但您很可能希望这样做,因为它使事情变得更容易。如果您更改了用户模型,您几乎肯定需要更改表单处理代码,但即使如此,您仍然可以使用默认的视图函数。

注意:在这种情况下,我们可以合理地将身份验证页面(包括 URL 和模板)放在我们的 catalog 应用程序中。但是,如果我们有多个应用程序,最好将这种共享登录行为分离出来,并使其在整个站点中可用,因此这就是我们在这里展示的内容!

项目 URL

将以下内容添加到项目 urls.py 文件(django-locallibrary-tutorial/locallibrary/urls.py)文件的底部

python
# Add Django site authentication urls (for login, logout, password management)

urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
]

导航到http://127.0.0.1:8000/accounts/ URL(注意尾部的正斜杠!)。Django 将显示一条错误消息,指出它找不到此 URL 的映射,并列出它尝试的所有 URL。由此您可以看到创建模板后可用的 URL。

注意:如上所示添加accounts/ 路径会添加以下 URL 以及名称(方括号中给出),这些名称可用于反转 URL 映射。您无需实现任何其他内容——上述 URL 映射会自动映射下面提到的 URL。

python
accounts/ login/ [name='login']
accounts/ logout/ [name='logout']
accounts/ password_change/ [name='password_change']
accounts/ password_change/done/ [name='password_change_done']
accounts/ password_reset/ [name='password_reset']
accounts/ password_reset/done/ [name='password_reset_done']
accounts/ reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/ reset/done/ [name='password_reset_complete']

现在尝试导航到登录 URL(http://127.0.0.1:8000/accounts/login/)。这将再次失败,但会显示一条错误消息,告诉您我们在模板搜索路径上缺少所需的模板(registration/login.html)。您将在顶部的黄色部分看到以下几行列出

python
Exception Type:    TemplateDoesNotExist
Exception Value:    registration/login.html

下一步是为模板创建一个名为“registration”的目录,然后添加login.html 文件。

模板目录

我们刚刚添加的 URL(以及隐式视图)期望在模板搜索路径中的某个位置的/registration/ 目录中找到其关联的模板。

对于此站点,我们将 HTML 页面放在templates/registration/ 目录中。此目录应位于您的项目根目录中,即与cataloglocallibrary 文件夹相同的目录。请立即创建这些文件夹。

注意:您的文件夹结构现在应如下所示

django-locallibrary-tutorial/   # Django top level project folder
  catalog/
  locallibrary/
  templates/
    registration/

为了使templates 目录对模板加载器可见,我们需要将其添加到模板搜索路径中。打开项目设置(/django-locallibrary-tutorial/locallibrary/settings.py)。

然后导入os 模块(如果该文件不存在,请在文件顶部附近添加以下行)。

python
import os # needed by code below

如所示更新TEMPLATES 部分的'DIRS'

python
    # …
    TEMPLATES = [
      {
       # …
       'DIRS': [os.path.join(BASE_DIR, 'templates')],
       'APP_DIRS': True,
       # …

登录模板

警告:本文提供的身份验证模板是 Django 演示登录模板的一个非常基本/略微修改的版本。您可能需要根据自己的需要对其进行自定义!

创建一个名为/django-locallibrary-tutorial/templates/registration/login.html 的新 HTML 文件,并为其提供以下内容

django
{% extends "base_generic.html" %}

{% block content %}

  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  {% if next %}
    {% if user.is_authenticated %}
      <p>Your account doesn't have access to this page. To proceed,
      please login with an account that has access.</p>
    {% else %}
      <p>Please login to see this page.</p>
    {% endif %}
  {% endif %}

  <form method="post" action="{% url 'login' %}">
    {% csrf_token %}
    <table>
      <tr>
        <td>{{ form.username.label_tag }}</td>
        <td>{{ form.username }}</td>
      </tr>
      <tr>
        <td>{{ form.password.label_tag }}</td>
        <td>{{ form.password }}</td>
      </tr>
    </table>
    <input type="submit" value="login">
    <input type="hidden" name="next" value="{{ next }}">
  </form>

  {# Assumes you set up the password_reset view in your URLconf #}
  <p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

此模板与我们之前看到的模板有一些相似之处——它扩展了我们的基本模板并覆盖了content 块。其余代码是相当标准的表单处理代码,我们将在以后的教程中讨论。您现在只需要知道它将显示一个表单,您可以在其中输入您的用户名和密码,并且如果您输入无效的值,页面刷新时将提示您输入正确的值。

保存模板后,导航回登录页面(http://127.0.0.1:8000/accounts/login/),您应该会看到类似以下内容

Library login page v1

如果您使用有效的凭据登录,您将被重定向到另一个页面(默认情况下,这将是http://127.0.0.1:8000/accounts/profile/)。问题是,默认情况下,Django 期望登录后您希望转到个人资料页面,这可能是也可能不是这种情况。由于您尚未定义此页面,因此您将收到另一条错误消息!

打开项目设置(/django-locallibrary-tutorial/locallibrary/settings.py)并在底部添加下面的文本。现在,当您登录时,默认情况下应该会重定向到站点首页。

python
# Redirect to home URL after login (Default redirects to /accounts/profile/)
LOGIN_REDIRECT_URL = '/'

注销模板

如果您导航到注销 URL(http://127.0.0.1:8000/accounts/logout/),那么您将收到一条错误消息,因为 Django 5 不允许使用GET 注销,只允许使用POST。我们将在稍后添加一个可用于注销的表单,但首先我们将创建用户注销后转到的页面。

创建并打开/django-locallibrary-tutorial/templates/registration/logged_out.html。复制下面的文本

django
{% extends "base_generic.html" %}

{% block content %}
  <p>Logged out!</p>
  <a href="{% url 'login'%}">Click here to login again.</a>
{% endblock %}

此模板非常简单。它只显示一条消息,告知您已注销,并提供一个链接,您可以点击该链接返回登录屏幕。屏幕呈现如下(注销后)

Library logout page v1

重置密码模板

默认的密码重置系统使用电子邮件向用户发送重置链接。您需要创建表单以获取用户的电子邮件地址、发送电子邮件、允许他们输入新密码以及记录整个过程何时完成。

以下模板可用作起点。

重置密码表单

这是用于获取用户电子邮件地址(用于发送密码重置电子邮件)的表单。创建** /django-locallibrary-tutorial/templates/registration/password_reset_form.html **,并赋予其以下内容

django
{% extends "base_generic.html" %}

{% block content %}
  <form action="" method="post">
  {% csrf_token %}
  {% if form.email.errors %}
    {{ form.email.errors }}
  {% endif %}
      <p>{{ form.email }}</p>
    <input type="submit" class="btn btn-default btn-lg" value="Reset password">
  </form>
{% endblock %}

重置密码完成

收集您的电子邮件地址后,将显示此表单。创建** /django-locallibrary-tutorial/templates/registration/password_reset_done.html **,并赋予其以下内容

django
{% extends "base_generic.html" %}

{% block content %}
  <p>We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.</p>
{% endblock %}

重置密码邮件

此模板提供包含重置链接的 HTML 电子邮件的文本,我们将发送给用户。创建** /django-locallibrary-tutorial/templates/registration/password_reset_email.html **,并赋予其以下内容

django
Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

确认重置密码

点击密码重置电子邮件中的链接后,您可以在此页面输入新密码。创建** /django-locallibrary-tutorial/templates/registration/password_reset_confirm.html **,并赋予其以下内容

django
{% extends "base_generic.html" %}

{% block content %}
    {% if validlink %}
        <p>Please enter (and confirm) your new password.</p>
        <form action="" method="post">
        {% csrf_token %}
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password"></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Password reset failed</h1>
        <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
    {% endif %}
{% endblock %}

重置密码完成

这是最后一个密码重置模板,显示以通知您密码重置已成功。创建** /django-locallibrary-tutorial/templates/registration/password_reset_complete.html **,并赋予其以下内容

django
{% extends "base_generic.html" %}

{% block content %}
  <h1>The password has been changed!</h1>
  <p><a href="{% url 'login' %}">log in again?</a></p>
{% endblock %}

测试新的身份验证页面

现在您已添加 URL 配置并创建了所有这些模板,身份验证页面(注销除外)现在应该可以正常工作了!

您可以通过首先尝试使用 URL `http://127.0.0.1:8000/accounts/login/` 登录您的超级用户帐户来测试新的身份验证页面。您将能够从登录页面中的链接测试密码重置功能。**请注意,Django 仅会将重置电子邮件发送到其数据库中已存储的地址(用户)!**

请注意,您还无法测试帐户注销,因为注销请求必须作为 `POST` 而不是 `GET` 请求发送。

**注意:**密码重置系统要求您的网站支持电子邮件,这超出了本文的范围,因此此部分**尚无法使用**。为了允许测试,在 `settings.py` 文件的末尾添加以下行。这会将发送到控制台的任何电子邮件记录下来(以便您可以从控制台中复制密码重置链接)。

python
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

有关更多信息,请参阅 发送电子邮件(Django 文档)。

针对已认证的用户进行测试

本节介绍了我们可以如何根据用户是否登录来选择性地控制用户看到的内容。

在模板中测试

您可以使用 `{{ user }}` 模板变量获取有关当前登录用户的信息(当您按照我们在框架中所做的那样设置项目时,默认情况下会将此添加到模板上下文中)。

通常,您首先会针对 `{{ user.is_authenticated }}` 模板变量进行测试,以确定用户是否有资格查看特定内容。为了演示这一点,接下来我们将更新我们的侧边栏,如果用户已注销则显示“登录”链接,如果用户已登录则显示“注销”链接。

打开基本模板(** /django-locallibrary-tutorial/catalog/templates/base_generic.html **)并将以下文本复制到 `sidebar` 块中,紧接在 `endblock` 模板标记之前。

django
  <ul class="sidebar-nav">{% if user.is_authenticated %}
     <li>User: {{ user.get_username }}</li>
     <li>
       <form id="logout-form" method="post" action="{% url 'logout' %}">
         {% csrf_token %}
         <button type="submit" class="btn btn-link">Logout</button>
       </form>
     </li>
   {% else %}
     <li><a href="{% url 'login' %}?next={{ request.path }}">Login</a></li>
   {% endif %}</ul>

如您所见,我们使用 `if` / `else` / `endif` 模板标记根据 `{{ user.is_authenticated }}` 是否为真来有条件地显示文本。如果用户已通过身份验证,那么我们知道我们有一个有效的用户,因此我们调用 `{{ user.get_username }}` 来显示他们的名称。

我们使用 `url` 模板标记和 `login` URL 配置的名称创建登录链接 URL。还要注意我们如何在 URL 末尾附加了 `?next={{ request.path }}`。这样做是在链接的 URL 末尾添加一个 URL 参数 `next`,其中包含当前页面的地址(URL)。在用户成功登录后,视图将使用此“`next`”值将用户重定向回他们最初点击登录链接的页面。

注销模板代码有所不同,因为从 Django 5 开始,您必须使用带有按钮的表单 `POST` 到 `admin:logout` URL 以注销。默认情况下,这将呈现为一个按钮,但您可以将按钮样式设置为显示为链接。在此示例中,我们使用的是Bootstrap,因此我们通过应用 `class="btn btn-link"` 使按钮看起来像链接。您还需要将以下样式附加到** /django-locallibrary-tutorial/catalog/static/css/styles.css **,以便正确地将注销链接放置在所有其他侧边栏链接旁边

css
#logout-form {
  display: inline;
}
#logout-form button {
  padding: 0;
  margin: 0;
}

通过点击侧边栏中的登录/注销链接试一试。您应该会被带到您在上面的 模板目录 中定义的注销/登录页面。

在视图中测试

如果您使用的是基于函数的视图,则限制对函数访问的最简单方法是将 `login_required` 装饰器应用于您的视图函数,如下所示。如果用户已登录,则您的视图代码将照常执行。如果用户未登录,这将重定向到项目设置中定义的登录 URL(`settings.LOGIN_URL`),并将当前绝对路径作为 `next` URL 参数传递。如果用户成功登录,则他们将被返回到此页面,但这次是经过身份验证的。

python
from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    # …

**注意:**您可以通过手动测试 `request.user.is_authenticated` 来执行相同的事情,但装饰器更方便!

类似地,在基于类的视图中限制对已登录用户的访问的最简单方法是从 `LoginRequiredMixin` 派生。您需要首先在超类列表中声明此混合,然后声明主视图类。

python
from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
    # …

这与 `login_required` 装饰器具有完全相同的重定向行为。您还可以指定一个替代位置来重定向未经身份验证的用户(`login_url`),以及一个 URL 参数名称而不是“`next`”来插入当前绝对路径(`redirect_field_name`)。

python
class MyView(LoginRequiredMixin, View):
    login_url = '/login/'
    redirect_field_name = 'redirect_to'

有关其他详细信息,请查看 此处的 Django 文档

示例 - 列出当前用户的书籍

现在我们知道如何将页面限制为特定用户,让我们创建一个当前用户借阅的书籍视图。

不幸的是,我们还没有任何方法让用户借阅书籍!因此,在创建书籍列表之前,我们将首先扩展 `BookInstance` 模型以支持借阅的概念,并使用 Django Admin 应用程序将一些书籍借给我们的测试用户。

模型

首先,我们必须使用户能够借阅 `BookInstance`(我们已经有 `status` 和 `due_back` 日期,但我们还没有此模型与特定用户之间的任何关联。我们将使用 `ForeignKey`(一对多)字段创建一个。我们还需要一种简单的机制来测试借出的书籍是否已逾期。

打开** catalog/models.py **,并从 `django.conf` 中导入 `settings`(将其添加到文件顶部的上一行导入语句下方,以便后续使用它们的代码可以使用它们)

python
from django.conf import settings

接下来,将 `borrower` 字段添加到 `BookInstance` 模型中,将键的用户模型设置为设置 `AUTH_USER_MODEL` 的值。由于我们没有使用 自定义用户模型 覆盖设置,因此它映射到来自 `django.contrib.auth.models` 的默认 `User` 模型。

python
borrower = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)

**注意:**以这种方式导入模型可以减少以后发现需要自定义用户模型时所需的工作量。本教程使用默认模型,因此您可以使用以下行直接导入 `User` 模型

python
from django.contrib.auth.models import User
python
borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)

趁此机会,让我们添加一个属性,我们可以从模板中调用它来判断特定的书籍实例是否已逾期。虽然我们可以在模板本身中计算这一点,但使用如下所示的 属性 将效率更高。

将其添加到文件的顶部附近

python
from datetime import date

现在将以下属性定义添加到 `BookInstance` 类中

**注意:**以下代码使用 Python 的 `bool()` 函数,该函数评估对象或表达式的结果对象,并在结果不为“假”时返回 `True`,否则返回 `False`。在 Python 中,如果对象为(评估为 `False`),则为:空(如 `[]`、`()`、`{}`)、`0`、`None` 或 `False`。

python
@property
def is_overdue(self):
    """Determines if the book is overdue based on due date and current date."""
    return bool(self.due_back and date.today() > self.due_back)

**注意:**在进行比较之前,我们首先验证 `due_back` 是否为空。空的 `due_back` 字段会导致 Django 抛出错误而不是显示页面:空值不可比较。这不是我们希望用户体验到的!

现在我们已更新了模型,我们需要对项目进行新的迁移,然后应用这些迁移

bash
python3 manage.py makemigrations
python3 manage.py migrate

管理

现在打开** catalog/admin.py **,并将 `borrower` 字段添加到 `BookInstanceAdmin` 类中的 `list_display` 和 `fieldsets` 中,如下所示。这将使该字段在管理部分可见,允许我们在需要时将 `User` 分配给 `BookInstance`。

python
@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
    list_display = ('book', 'status', 'borrower', 'due_back', 'id')
    list_filter = ('status', 'due_back')

    fieldsets = (
        (None, {
            'fields': ('book', 'imprint', 'id')
        }),
        ('Availability', {
            'fields': ('status', 'due_back', 'borrower')
        }),
    )

借几本书

现在可以将书籍借给特定用户了,请借出一些 `BookInstance` 记录。将它们的 `borrowed` 字段设置为您的测试用户,将 `status` 设置为“已借出”,并分别在将来和过去设置到期日期。

**注意:**我们不会详细说明此过程,因为您已经知道如何使用管理站点!

已借出视图

现在,我们将添加一个视图,用于获取已借给当前用户的所有书籍列表。我们将使用我们熟悉的相同的基于泛型的列表视图,但这次我们还将导入并从 `LoginRequiredMixin` 派生,以便只有登录用户才能调用此视图。我们还将选择声明一个 `template_name`,而不是使用默认值,因为我们最终可能会有几个不同的 `BookInstance` 记录列表,具有不同的视图和模板。

将以下内容添加到 catalog/views.py 中

python
from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
    """Generic class-based view listing books on loan to current user."""
    model = BookInstance
    template_name = 'catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return (
            BookInstance.objects.filter(borrower=self.request.user)
            .filter(status__exact='o')
            .order_by('due_back')
        )

为了将我们的查询限制为仅当前用户的 `BookInstance` 对象,我们重新实现了 `get_queryset()`,如上所示。请注意,“o”是“已借出”的存储代码,我们按 `due_back` 日期排序,以便首先显示最旧的项目。

已借出书籍的 URL 配置

现在打开** /catalog/urls.py **并添加一个指向上述视图的 `path()`(您可以将下面的文本复制到文件的末尾)。

python
urlpatterns += [
    path('mybooks/', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'),
]

已借出书籍的模板

现在,我们只需要为此页面添加一个模板。首先,创建模板文件** /catalog/templates/catalog/bookinstance_list_borrowed_user.html **并赋予其以下内容

django
{% extends "base_generic.html" %}

{% block content %}
    <h1>Borrowed books</h1>

    {% if bookinstance_list %}
    <ul>

      {% for bookinst in bookinstance_list %}
      <li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
        <a href="{% url 'book-detail' bookinst.book.pk %}">{{ bookinst.book.title }}</a> ({{ bookinst.due_back }})
      </li>
      {% endfor %}
    </ul>

    {% else %}
      <p>There are no books borrowed.</p>
    {% endif %}
{% endblock %}

此模板与我们之前为 `Book` 和 `Author` 对象创建的模板非常相似。这里唯一“新”的东西是我们检查了我们在模型中添加的方法 `(bookinst.is_overdue)` 并使用它来更改逾期项目的颜色。

当开发服务器正在运行时,您现在应该能够在浏览器中以 `http://127.0.0.1:8000/catalog/mybooks/` 查看登录用户的列表。在用户登录和注销的情况下试一试(在第二种情况下,您应该会被重定向到登录页面)。

将列表添加到侧边栏

最后一步是为这个新页面在侧边栏中添加一个链接。我们将把它放在显示登录用户其他信息相同的部分。

打开基本模板(** /django-locallibrary-tutorial/catalog/templates/base_generic.html**)并在下面所示的位置将“我的借阅”行添加到侧边栏。

django
 <ul class="sidebar-nav">
   {% if user.is_authenticated %}
   <li>User: {{ user.get_username }}</li>

   <li><a href="{% url 'my-borrowed' %}">My Borrowed</a></li>

   <li>
     <form id="logout-form" method="post" action="{% url 'admin:logout' %}">
       {% csrf_token %}
       <button type="submit" class="btn btn-link">Logout</button>
     </form>
   </li>
   {% else %}
   <li><a href="{% url 'login' %}?next={{ request.path }}">Login</a></li>
   {% endif %}
 </ul>

它是什么样子的?

当任何用户登录时,他们将在侧边栏中看到“我的借阅”链接,以及如下所示的图书列表(第一本书没有到期日,这是一个我们希望在后续教程中修复的错误!)。

Library - borrowed books by user

权限

权限与模型相关联,并定义具有该权限的用户可以对模型实例执行的操作。默认情况下,Django 会自动向所有模型授予添加更改删除权限,这允许具有这些权限的用户通过管理员站点执行相关操作。您可以为模型定义自己的权限并将其授予特定用户。您还可以更改与同一模型的不同实例关联的权限。

然后,在视图和模板中测试权限与测试身份验证状态非常相似(实际上,测试权限也会测试身份验证)。

模型

权限的定义是在模型“class Meta”部分使用permissions字段完成的。您可以在元组中指定任意数量的权限,每个权限本身都在一个嵌套元组中定义,该元组包含权限名称和权限显示值。例如,我们可以定义一个权限,允许用户标记一本书已归还,如下所示

python
class BookInstance(models.Model):
    # …
    class Meta:
        # …
        permissions = (("can_mark_returned", "Set book as returned"),)

然后,我们可以在管理员站点中将权限分配给“图书管理员”组。

打开**catalog/models.py**,并按上述方式添加权限。您需要重新运行迁移(调用python3 manage.py makemigrationspython3 manage.py migrate)以适当地更新数据库。

模板

当前用户的权限存储在一个名为{{ perms }}的模板变量中。您可以使用关联的 Django “应用程序”内的特定变量名称来检查当前用户是否具有特定权限——例如,如果用户具有此权限,则{{ perms.catalog.can_mark_returned }}将为True,否则为False。我们通常使用模板{% if %}标签来测试权限,如下所示

django
{% if perms.catalog.can_mark_returned %}
    <!-- We can mark a BookInstance as returned. -->
    <!-- Perhaps add code to link to a "book return" view here. -->
{% endif %}

视图

可以在函数视图中使用permission_required装饰器或在基于类的视图中使用PermissionRequiredMixin来测试权限。模式与登录身份验证相同,当然,您可能需要添加多个权限。

函数视图装饰器

python
from django.contrib.auth.decorators import permission_required

@permission_required('catalog.can_mark_returned')
@permission_required('catalog.can_edit')
def my_view(request):
    # …

基于类的视图的权限必需混合。

python
from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'catalog.can_mark_returned'
    # Or multiple permissions
    permission_required = ('catalog.can_mark_returned', 'catalog.change_book')
    # Note that 'catalog.change_book' is permission
    # Is created automatically for the book model, along with add_book, and delete_book

注意:上述行为存在细微的默认差异。对于具有权限违规的登录用户,**默认**情况下

  • @permission_required重定向到登录屏幕(HTTP 状态 302)。
  • PermissionRequiredMixin返回 403(HTTP 状态禁止)。

通常,您需要PermissionRequiredMixin行为:如果用户已登录但没有正确的权限,则返回 403。要对函数视图执行此操作,请使用@login_required@permission_required以及raise_exception=True,如下所示

python
from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('catalog.can_mark_returned', raise_exception=True)
def my_view(request):
    # …

示例

我们不会在此处更新LocalLibrary;也许在下一个教程中!

挑战自我

在本文的前面,我们向您展示了如何为当前用户创建一个页面,列出他们借阅的图书。现在的挑战是创建一个仅对图书管理员可见的类似页面,该页面显示所有已借阅的图书,并包括每个借阅者的姓名。

您应该能够遵循与其他视图相同的模式。主要区别在于您需要将视图限制为仅图书管理员。您可以根据用户是否是工作人员(函数装饰器:staff_member_required,模板变量:user.is_staff)来执行此操作,但我们建议您改为使用can_mark_returned权限和PermissionRequiredMixin,如上一节所述。

警告:请记住不要将超级用户用于基于权限的测试(即使尚未定义权限,超级用户的权限检查也始终返回 true!)。相反,创建一个图书管理员用户,并添加所需的功能。

完成后,您的页面应类似于下面的屏幕截图。

All borrowed books, restricted to librarian

总结

出色的工作——您现在创建了一个网站,图书馆成员可以在其中登录并查看自己的内容,并且图书管理员(具有正确的权限)可以查看所有借出的图书及其借阅者。目前,我们只是在查看内容,但是当您想要开始修改和添加数据时,将使用相同的原则和技术。

在我们的下一篇文章中,我们将了解如何使用 Django 表单收集用户输入,然后开始修改我们存储的一些数据。

另请参阅