Python學習之路18-用戶帳戶

《Python編程:從入門到實踐》筆記。
本篇記錄如何建立用戶註冊系統,如何實現用戶輸入本身的數據。

1. 前言

在本篇中,咱們將:html

  • 建立一些表單,讓用戶可以添加主題和條目,以及編輯既有的條目;
  • 實現一個身份驗證系統。

2. 讓用戶可以輸入數據

先添加幾個頁面,讓用戶可以添加新主題,新條目以及編輯條目。python

2.1 添加新主題

和以前建立網頁的步驟同樣:定義URL,編寫視圖函數,編寫模板。主要區別是,這裏須要一個包含表單的模塊forms.pyshell

2.1.1 建立forms.py模塊

用戶輸入信息時,須要進行驗證,確保提交的信息是正確的數據類型,且不是惡意信息,如中斷服務器的代碼。而後再處理信息,並保存到數據庫中。固然,這些工做不少都由Django自動完成。數據庫

models.py所在的目錄中新建forms.py模塊。建立表單的最簡單方法是繼承Django的ModelForm類:django

from django import forms
from .models import Topic

class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ["text"]
        labels = {"text": ""}

最簡單的ModelForm版本只包含一個內嵌的Meta類,它告訴Django根據哪一個模型建立表單,以及在表單中包含哪些字段。第6行,咱們根據Topic建立一個表單,該表單只包含字段text(第7行),並不爲該字段生成標籤(第8行)。編程

2.1.2 URL模式new_topic

當用戶要添加新主題時,將切換到http://localhost:8000/new_topic/ 。在learning_logs/urls.py中添加以下代碼:瀏覽器

urlpatterns = [
    -- snip --
    # 用於添加新主題的網站
    path("new_topic/", views.new_topic, name="new_topic"),
]

2.1.3 視圖函數new_topic()

該函數須要處理兩種情形:①剛進入new_topic網頁,顯示一個空表單;②對提交的表單數據進行處理,並將用戶重定向到網頁topics。修改views.py文件:bash

from django.http import HttpResponseRedirect
from django.urls import reverse
from .forms import TopicForm

def new_topic(request):
    """添加新主題"""
    if request.method != "POST":
        # 爲提價數據:建立一個新表單
        form = TopicForm()
    else:
        # POST提交的數據,對數據進行處理
        form = TopicForm(request.POST)
        if form.is_valid():
            form.save()
            # 該類將用戶重定向到網頁topics,函數reverse()根據指定的URL模型肯定URL
            return HttpResponseRedirect(reverse("learning_logs:topics"))

    context = {"form": form}
    return render(request, "learning_logs/new_topic.html", context)

2.1.4 GET請求和POST請求

建立Web應用程序時,將用到兩種主要數據請求類型:GET請求和POST請求。從這倆英文單詞能夠看出,若是隻從服務器讀取數據頁面,則使用GET請求;若是要提交用戶填寫的表單,一般使用POST請求。固然還有一些其餘的請求類型,但這個項目中沒有使用。本項目中處理表單都使用POST方法。服務器

request.method存儲了請求的類型(第7行代碼)。微信

當不是POST請求時,咱們生成一個空表單傳遞給模板new_topic.html,而後返回給用戶;當請求是POST時,咱們從request.POST這個變量中獲取用戶提交的數據,並暫存到form變量中。

經過is_valid()方法驗證表單數據是否知足要求:用戶是否填寫了全部必不可少的字段(表單字段默認都是必填的),且輸入的數據與字段類型是否一致。固然這些驗證都是Django自動進行的。若是表單有效,在經過formsave()方法存儲到數據庫,而後經過reverse()函數獲取頁面topics的URL,並將其傳遞給HTTPResponseRedirect()以重定向到topics頁面。若是表單無效,把這些數據從新傳回給用戶。

2.1.5 模板new_topic.html

{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Add a new topic:</p>

  <form action="{% url 'learning_logs:new_topic' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">add topic</button>
  </form>
  
{% endblock content %}

模板繼承了base.html,所以其基本結構和項目中的其餘頁面相同。第6行中,參數action告訴服務器將提交的表單數據送到什麼位置去處理,參數method瀏覽器POST請求的方式提交數據。

Django使用模板標籤csrf_token(第7行)來防止攻擊者利用表單得到對服務器未經受權的訪問(跨站請求僞造)。

Django顯示錶單很是方便:只須要使用模板變量form.as_p,修飾符as_p讓Django以段落格式渲染全部表單元素,這是一種整潔地顯示錶單的簡單方法。

Django不自動建立提交表單的按鈕,需自行建立。

2.1.6 連接到頁面new_topic

在頁面topics.html中添加一個到頁面new_topic的連接:

{% extends "learning_logs/base.html" %}

{% block content %}
  -- snip --
  <a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}

2.1.7 效果

如下是實際效果圖:

圖片描述

經過這個頁面,隨意添加幾個主題,以下:

圖片描述

2.2 添加新條目

和前面的步驟類似:建立條目表單,添加URL,添加視圖,添加模板,連接到頁面

2.2.1 建立條目表單

建立一個與模型Entry相關聯的表單,但這個表單的自定義程度比TopicForm要高些,依然是在剛纔建立的forms.py中添加:

from .models import Topic, Entry

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ["text"]
        labels = {"text": ""}
        widgets = {"text": forms.Textarea(attrs={"cols": 80})}

代碼中定義了屬性widgets。小部件(widget)是一個HTML表單元素,如單行文本框、多行文本框或下拉列表。經過設置屬性widgets能夠覆蓋Django選擇的默認小部件。經過Django的forms.Textarea定製字段「text"的輸入小部件,將文本框的寬度設置爲80列,而不是默認的40列。

2.2.2 添加URL模式new_entry

修改learning_logs/urls.py

urlpatterns = [
    -- snip --
    path("new_entry/<int:topic_id>/", views.new_entry, name="new_entry"),
]

該URL模式與形式爲http://localhost:8000/new_entry/topi_id/ 的URL匹配,其中topic_id是主題的ID。

2.2.3 視圖函數new_entry()

與函數new_topic()很像:

from .forms import TopicForm, EntryForm

def new_entry(request, topic_id):
    """在特定的主題中添加新條目"""
    topic = Topic.objects.get(id=topic_id)

    if request.method != "POST":
        # 未提交數據,建立一個空表單
        form = EntryForm()
    else:
        # POST提交的數據,對數據進行處理
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return HttpResponseRedirect(reverse("learning_logs:topic", args=[topic_id]))

    context = {"topic": topic, "form": form}
    return render(request, "learning_logs/new_entry.html", context)

new_entry()的定義包含形參topic_id,用於存儲從URL中得到的值。

在調用save()時傳遞了參數commit=False(第14行),它讓Django建立一個新的條目對象,但並不馬上提交數據庫,而是暫時存儲在變量new_entry中,待爲這個新條目對象添加了屬性topic以後再提交數據庫。

在重定向時,reverse()函數中傳遞了兩個參數,URL模式的名稱以及列表argsargs包含要包含在URL中的全部參數。

2.2.4 模板new_entry.html

相似於new_topic

{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>

  <p>Add a new entry:</p>
  <form action="{% url 'learning_logs:new_entry' topic.id %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">add entry</button>

  </form>
{% endblock content %}

注意第4行代碼,改行代碼返回到特定主題頁面。

2.2.5 連接到頁面new_entry

在顯示特定主題的頁面中添加到頁面new_entry的連接,修改topic.html

{% extends "learning_logs/base.html" %}

{% block content %}

  <p>Topic: {{ topic }}</p>

  <p>Entries:</p>
  <p>
    <a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
  </p>
  -- snip --

{% endblock content %}

2.2.6 效果

下圖是實際效果,請隨意添加一些條目:

圖片描述

2.3 編輯條目

建立一個頁面,讓用戶能編輯既有條目。順序是:添加URL,添加視圖,添加模板,連接到頁面。

2.3.1 URL模式edit_entry

修改learning_logs/urls.py

urlpatterns = [
    -- snip --
    path("edit_entry/<int:entry_id>/", views.edit_entry, name="edit_entry"),
]

2.3.2 視圖函數edit_entry()

from .models import Topic, Entry

def edit_entry(request, entry_id):
    """編輯既有條目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic

    if request.method != "POST":
        # 初次請求,使用當前條目填充表單
        form = EntryForm(instance=entry)
    else:
        # POST提交的數據,對數據進行處理
        form = EntryForm(instance=entry, data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))

    context = {"entry": entry, "topic": topic, "form": form}
    return render(request, "learning_logs/edit_entry.html", context)

首先獲取要被修改的entry以及與該條目相關的主題。處理GET請求時,經過參數instance=entry建立EntryForm實例,該參數讓Django建立一個表單,並使用既有條目對象中的信息填充它。處理POST請求時,還傳入了data=request.POST參數,Django根據POST中的相關數據對entry進行修改。

2.3.3 模板edit_entry.html

{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>

  <p>Edit entry:</p>

  <form action="{% url 'learning_logs:edit_entry' entry.id %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">save changes</button>
  </form>

{% endblock content %}

2.3.4 連接到頁面edit_entry.html

在顯示特定主題的頁面中,須要給每一個條目添加到頁面edit_entry.html的連接,爲此,修改topic.html

-- snip --
    {% for entry in entries %}
      <li>
        <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
        <p>{{ entry.text|linebreaks }}</p>
        <p>
          <a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a>
        </p>
      </li>
-- snip --

2.3.5 效果

如下是實際效果圖:

圖片描述

3. 建立用戶帳戶

如今開始創建一個用戶註冊和身份驗證系統。爲此將建立一個新的應用程序,其中包含處理用戶帳戶相關的全部功能。對Topic模型也要作稍許修改,讓每一個主題都歸屬於特定用戶。

3.1 建立應用程序users

但願你們還記得如何使用startapp命令還建立APP:

python manage.py startapp users

將APP添加到settings.py中

INSTALLED_APPS = [
    -- snip --
    "users.apps.UsersConfig",
]

在APP根目錄下建立urls.py文件,並添加命名空間:

app_name = "users"

爲APP定義URL,修改項目根目錄中的urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("learning_logs.urls")),
    path("users/", include("users.urls")),
]

3.2 登錄頁面

使用Django提供的默認登錄視圖,URL模式會有所不一樣。在users中的urls.py中添加以下代碼:

"""爲應用程序users定義URL模式"""
from django.contrib.auth.views import login
from django.urls import path

app_name = "users"

urlpatterns = [
    # 登錄頁面
    path("login/", login, {"template_name": "users/login.html"}, name="login"),
]

代碼中,咱們使用Django自帶的login視圖函數(注意,參數是login,而不是views.login)。從以前的例子能夠看出,咱們渲染模板的代碼都是在本身寫的視圖函數中。但這裏使用了自帶的視圖函數,沒法自行編寫進行渲染的代碼。因此,咱們還傳了一個字典給path,告訴Django到哪裏查找咱們要用到的模板。注意,該模板在users中,而不是在learning_logs中。

3.2.1 新建模板login.html

learning_log/users/templates/users中建立login.html

{% extends "learning_logs/base.html" %}

{% block content %}

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

  <form method="post" action="{% url 'users:login' %}">
    {% csrf_token %}
    {{ form.as_p }}

    <button name="submit">log in</button>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
  </form>
  
{% endblock content %}

若是表單的errors屬性被設置,則顯示一條提示帳號密碼錯誤的信息。

3.2.2 連接到登錄頁面

base.html中添加到登錄頁面的連接,讓全部頁面都包含它。將這個連接嵌套在一個 if 標籤中,用戶已登陸時隱藏掉該連接:

<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
  <a href="{% url 'learning_logs:topics' %}">Topics</a>
  {% if user.is_authenticated %}
    Hello, {{ user.username }}
  {% else %}
    <a href="{% url 'users:login' %}">log in</a>
  {% endif %}
</p>

{% block content %}{% endblock content %}

在Django身份驗證系統中,每一個模板均可使用變量user,這個變量有一個is_authenticated屬性:若是用戶已登陸,該屬性將爲True,不然爲False

3.2.3 使用登錄頁面

首先訪問localhost:8000/admin註銷超級用戶,再訪問localhost:8000/users/login/,獲得以下頁面:

圖片描述

3.3 註銷

並不爲註銷建立單獨的頁面,而是讓用戶單擊一個鏈接用於註銷並返回主頁。所以,須要作以下工做:註銷URL模式,新建視圖,連接到註銷視圖

users/urls.py中添加與http://localhost:8000/users/logout/ 匹配的URL模式:

-- snip --
urlpatterns = [
    -- snip --
    path("logout/", views.logout_view, name="logout"),
]

編寫視圖函數logout_view(),其中,直接調用Django自帶的logout()函數,該函數要求request做爲參數:

from django.contrib.auth import logout
from django.http import HttpResponseRedirect
from django.urls import reverse

def logout_view(request):
    """註銷用戶"""
    logout(request)
    return HttpResponseRedirect(reverse("learning_logs:index"))

base.html中添加註銷連接:

-- snip --
  {% if user.is_authenticated %}
    Hello, {{ user.username }}
    <a href="{% url 'users:logout' %}">log out</a>
  {% else %}
    <a href="{% url 'users:login' %}">log in</a>
  {% endif %}
-- snip --

3.4 註冊頁面

使用Django提供的表單UserCreationFrom,但編寫本身的視圖函數和模板。URL->view->template->link

首先,建立註冊頁面的URL模式,修改users/urls.py

-- snip --
urlpatterns = [
    -- snip --
    path("register/", views.register, name="register"),
]

其次,建立視圖register()

from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render
-- snip --

def register(request):
    """註冊新用戶"""
    if request.method != "POST":
        # 顯示空的註冊表單
        form = UserCreationForm()
    else:
        # 處理填寫好的表單
        form = UserCreationForm(data=request.POST)

        if form.is_valid():
            new_user = form.save()
            # 讓用戶自動登錄,再重定向到主頁
            # 註冊是要求輸入兩次密碼,因此有password1和password2
            authenticated_user = authenticate(username=new_user.username,
                                              password=request.POST["password1"])
            login(request, authenticated_user)
            return HttpResponseRedirect(reverse("learning_logs:index"))

    context = {"form": form}
    return render(request, "users/register.html", context)

以上代碼在用戶成功建立了用戶後會自動登錄,該功能由login()函數實現。該函數將會爲經過了身份驗證的用戶對象建立會話(session)。最後上述代碼重定向到主頁。

而後,編寫註冊頁面的模板register.html

{% extends "learning_logs/base.html" %}

{% block content %}
  <form method="post" action="{% url 'users:register' %}">
  {% csrf_token %}
  {{ form.as_p }}

  <button name="sumbit">register</button>
  <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>

  </form>
{% endblock content %}

最後,在頁面中顯示註冊連接,修改base.html,在用戶沒有登陸時顯示註冊連接:

-- snip --
  {% if user.is_authenticated %}
    Hello, {{ user.username }}
    <a href="{% url 'users:logout' %}">log out</a>
  {% else %}
    <a href="{% url 'users:register' %}">register</a> -
    <a href="{% url 'users:login' %}">log in</a>
  {% endif %}
-- snip --

下面是實際效果:

圖片描述

這是直接點register按鈕時的反饋,不過這裏有點疑惑,從上面的register.html中看到,其實代碼很簡單,但這裏有個浮動效果,並且在註冊模板中並無像前面那樣的form.errors模板標籤,但依然有未註冊成功時的反應,並且註冊的視圖函數也是本身寫的,並非用的自帶的註冊函數,因此不知道是否是和form.as_p有關。以後再慢慢研究吧,

4. 讓用戶擁有本身的數據

用戶應該可以輸入其專有的數據,因此應該建立一個系統,肯定各項數據所屬的用戶,再限制對頁面的訪問,使得用戶只能使用本身的數據,即訪問控制。

4.1 使用@login_required限制訪問

Django提供了裝飾器@login_required,使得能輕鬆實現用戶只能訪問本身能訪問的頁面。

限制對topics.html的訪問

每一個主題都歸特定用戶全部,因此須要加限制,修改learning_logs/views.py

from django.contrib.auth.decorators import login_required

@login_required
def topics(request):
    """顯示全部的主題"""
    topics = Topic.objects.order_by("date_added")
    # 一個上下文字典,傳遞給模板
    context = {"topics": topics}
    return render(request, "learning_logs/topics.html", context)

裝飾器也是一個函數,python在運行topics()前會先運行login_required()的代碼。

login_required()函數檢查用戶是否登陸,僅當用戶已登陸時,Django才運行topics()函數,若未登陸,就重定向到登錄界面。而爲了實現這個重定向,還須要修改項目settings.py文件,在該文件中添加這樣一個常量(其實也是變量),通常在文件末尾添加:

-- snip --
LOGIN_URL = '/users/login/'

全面限制對項目「學習筆記」的訪問

Django能輕鬆地限制對頁面的訪問,但得本身設計須要限制哪些頁面。通常先肯定哪些頁面不須要保護,再限制對其餘頁面的訪問。在該項目中,咱們不限制對主頁、註冊頁面和註銷連接的訪問,其餘頁面均限制。在learning_logs/views.py中,除了index()外,每一個視圖函數都加上@login_required

4.2 將數據關聯到用戶

爲了禁止用戶訪問其餘用戶的數據,須要將用戶與數據關聯。只須要將最高層的數據關聯到用戶,這樣更低層的數據將自動關聯到用戶。下面修改Topic模型和相關視圖:

from django.contrib.auth.models import User
from django.db import models

class Topic(models.Model):
    """用戶學習的主題"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    -- snip --

修改模型後,還須要遷移數據庫。此時,須要將主題與用戶關聯。這裏並無經過代碼進行關聯,咱們在遷移數據庫時手動進行關聯。爲此,咱們須要先知道有哪些用戶,以及這些用戶的ID。咱們經過Django shell查詢用戶信息,固然也能夠直接查看數據庫,這裏再也不演示。咱們將主題都關聯到超級用戶ll_admin上,它的ID是1。如今咱們執行數據遷移:

(venv)learning_logs$ python manage.py makeimgrations learning_logs
You are trying to add a non-nullable field 'owner' to topic without a default; 
we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for 
 this column)
 2) Quit, and let me add a default in models.py
Select an option:  1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>>  1
Migrations for 'learning_logs':
  learning_logs\migrations\0003_topic_owner.py
    - Add field owner to topic

Django指出試圖給既有模型Topic添加一個必不可少(不可爲空)的字段,而該字段沒有默認值,須要咱們採起措施:要麼如今提供默認值,要麼退出並在models.py中添加默認值。咱們選擇了直接輸入默認值。接下來,Django使用這個值來遷移數據庫,並生成了遷移文件0003_topic_owner.py,它在模型Topic中添加字段owner

如今執行遷移命令:

(venv)learning_logs$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
  Applying learning_logs.0003_topic_owner... OK

執行後能夠在Django shell中驗證是否遷移成功,這裏再也不驗證。

4.3 只容許用戶訪問本身的主題

目前無論以哪一個用戶身份登陸,都能看到全部主題。如今咱們添加限制,讓用戶只能看到本身的主題。在views.py中,對topics()作以下修改:

-- snip --
@login_required
def topics(request):
    """顯示全部的主題"""
    topics = Topic.objects.filter(owner=request.user).order_by("date_added")
    # 一個上下文字典,傳遞給模板
    context = {"topics": topics}
    return render(request, "learning_logs/topics.html", context)
-- snip --

用戶登陸後,request對象將有一個user屬性,這個屬性存儲了有關該用戶的信息。第5行代碼讓Django只從數據庫中讀取特定用戶的數據。

4.4 保護用戶的主題

上述代碼作到了登陸後只顯示相應用戶的數據,可是,若是登陸後直接經過URL訪問,如直接輸入http://localhost:8000/topics/1/ ,依然能夠訪問不屬於本身的特定主題頁面。下面修改views.py中的topic()函數來加以限制:

from django.http import HttpResponseRedirect, Http404

@login_required
def topic(request, topic_id):
    """顯示單個主題及其全部的條目"""
    topic = Topic.objects.get(id=topic_id)
    # 確認請求的主題屬於當前用戶
    if topic.owner != request.user:
        raise Http404
    -- snip --

4.5 保護頁面edit_entry

此時用戶也能夠像上面同樣,登錄後直接經過URL來訪問edit_entry.html,如今咱們對這個頁面也加以限制:

@login_required
def edit_entry(request, entry_id):
    """編輯既有條目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404
    
    -- snip --

4.6 最後一步:將新主題關聯到當前用戶

當前用於添加新主題的頁面存在問題,由於它沒有將新主題關聯到特定用戶。若是此時嘗試添加新主題,將看到錯誤信息IntegrityError,指出learning_logs_topic.user_id不能爲NULL,下面修改new_topic()函數:

@login_required
def new_topic(request):
    """添加新主題"""
    if request.method != "POST":
        # 爲提價數據:建立一個新表單
        form = TopicForm()
    else:
        # POST提交的數據,對數據進行處理
        form = TopicForm(request.POST)
        if form.is_valid():
            # 添加新主題時關聯到特定用戶
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            # 該類將用戶重定向到網頁topics,函數reverse()根據指定的URL模型肯定URL
            return HttpResponseRedirect(reverse("learning_logs:topics"))

    context = {"form": form}
    return render(request, "learning_logs/new_topic.html", context)

如今,這個項目容許任何用戶註冊,而每一個用戶想添加多少新主題均可以,每一個用戶只能訪問本身的數據,不管是查看數據、輸入新數據仍是修改舊數據時都是如此。

5. 小結

本篇主要講述瞭如何使用表單來讓用戶添加新主題、添加新條目和編輯既有條目;如何實現註冊,登陸與註銷,如何使用裝飾器來限制訪問,如何對用戶數據進行保護等。下一篇中,咱們將使這個項目更漂亮,而且部署到服務器上。


迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~

相關文章
相關標籤/搜索