Django 教程第八部分:使用者認證和許可權

在本教程中,我們將向您展示如何允許使用者使用自己的賬戶登入您的網站,以及如何根據他們是否登入以及他們的許可權來控制他們可以做什麼和看到什麼。作為此演示的一部分,我們將擴充套件LocalLibrary網站,新增登入和登出頁面,以及使用者和員工專屬的檢視已借圖書的頁面。

預備知識 完成所有之前的教程主題,直到幷包括Django 教程第七部分:會話框架
目標 瞭解如何設定和使用使用者認證和許可權。

概述

Django 提供了一個認證和授權(“許可權”)系統,該系統建立在上一教程中討論的會話框架之上,允許您驗證使用者憑據並定義每個使用者被允許執行的操作。該框架包括用於使用者的內建模型(一種一次性將許可權應用於多個使用者的通用方法)、指定使用者是否可以執行任務的許可權/標誌、用於使用者登入的表單和檢視,以及用於限制內容的檢視工具。

注意:根據 Django,認證系統旨在非常通用,因此不提供其他 Web 認證系統中的某些功能。某些常見問題的解決方案以第三方包的形式提供。例如,登入嘗試的節流和針對第三方的認證(例如 OAuth)。

在本教程中,我們將向您展示如何在LocalLibrary網站中啟用使用者認證,建立您自己的登入和登出頁面,向模型新增許可權,並控制頁面訪問。我們將使用認證/許可權來顯示使用者和圖書管理員借閱圖書的列表。

認證系統非常靈活,如果您願意,可以從頭開始構建您的 URL、表單、檢視和模板,只需呼叫提供的 API 即可登入使用者。但是,在本文中,我們將使用 Django 的“標準”認證檢視和表單來處理我們的登入和登出頁面。我們仍然需要建立一些模板,但這非常容易。

我們還將向您展示如何建立許可權,以及如何在檢視和模板中檢查登入狀態和許可權。

啟用認證

當我們建立骨架網站(在教程 2 中)時,認證已自動啟用,因此您此時無需做任何事情。

注意: 當我們使用 django-admin startproject 命令建立應用程式時,所有必要的配置都已為我們完成。使用者和模型許可權的資料庫表是在我們第一次呼叫 python manage.py migrate 時建立的。

配置在專案檔案(django-locallibrary-tutorial/locallibrary/settings.py)的INSTALLED_APPSMIDDLEWARE部分中設定,如下所示

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', 'myemail@crazymail.com', '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', 'myemail@crazymail.com', '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. 點選新增按鈕(Group 旁邊)建立一個新的Group;為該組輸入名稱“Library Members”。Admin site - add group
  2. 我們不需要任何組許可權,所以只需按下 SAVE(您將被帶到組列表)。

現在讓我們建立一個使用者

  1. 導航回管理站點的主頁

  2. 點選使用者旁邊的新增按鈕開啟新增使用者對話方塊。Admin site - add user pt1

  3. 為您的測試使用者輸入適當的使用者名稱密碼/密碼確認

  4. 按下儲存建立使用者。

    管理站點將建立新使用者並立即將您帶到“更改使用者”螢幕,您可以在其中更改您的使用者名稱併為使用者模型的可選欄位新增資訊。這些欄位包括名字、姓氏、電子郵件地址以及使用者的狀態和許可權(只應設定活躍標誌)。再往下您可以指定使用者的組和許可權,並檢視與使用者相關的重要日期(例如,他們的加入日期和上次登入日期)。Admin site - add user pt2

  5. 部分,從可用組列表中選擇Library Member組,然後按下框之間的右箭頭將其移到已選組框中。Admin site - add user to group

  6. 我們不需要在這裡做其他任何事情,所以再次選擇儲存,以轉到使用者列表。

就這樣!現在您有了一個“普通圖書館成員”賬戶,您將能夠用於測試(一旦我們實現了讓他們登入的頁面)。

注意:您應該嘗試建立另一個圖書館成員使用者。此外,為圖書管理員建立一個組,並向該組新增一個使用者!

設定認證檢視

Django 提供了建立認證頁面所需的一切,用於處理登入、登出和密碼管理,開箱即用。這包括 URL 對映器、檢視和表單,但不包括模板——我們需要自己建立!

在本節中,我們將展示如何將預設系統整合到 LocalLibrary 網站並建立模板。我們將它們放在主專案 URL 中。

注意:您不必使用任何這些程式碼,但您很可能希望使用它,因為它使事情變得容易得多。如果更改使用者模型,您幾乎肯定需要更改表單處理程式碼,但即使如此,您仍然可以使用庫存檢視函式。

注意:在這種情況下,我們可以合理地將認證頁面(包括 URL 和模板)放在我們的 catalog 應用程式中。但是,如果我們有多個應用程式,最好將此共享登入行為分開,並使其在整個站點中可用,所以我們在此處展示了這一點!

專案 URLs

將以下內容新增到專案 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) 的 URL 引數 next。使用者成功登入後,檢視將使用此 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 管理應用程式向我們的測試使用者借出一些圖書。

模型

首先,我們必須使書籍例項 (BookInstance) 能夠被使用者借出(我們已經有 statusdue_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),則它為空(如 [](){})、0None 或它本身是 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_displayfieldsets 中,如下所示。這將使該欄位在管理部分可見,從而允許我們在需要時將 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”是“on loan”的儲存程式碼,我們按 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 %}

此模板與我們之前為 BookAuthor 物件建立的模板非常相似。這裡唯一的“新”內容是我們檢查了模型中新增的方法 (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>

它看起來怎麼樣?

當任何使用者登入時,他們將在側邊欄中看到我的借閱連結,並且圖書列表將如下所示(第一本書沒有到期日期,這是一個我們希望在以後的教程中修復的 bug!)。

Library - borrowed books by user

Permissions

許可權與模型相關聯,並定義具有該許可權的使用者可以對模型例項執行的操作。預設情況下,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 狀態 Forbidden)。

通常您會希望 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 表單收集使用者輸入,然後開始修改一些儲存資料。

另見