Python編程從入門到實(shí)踐:Web應(yīng)用程序 - 用戶賬戶

開發(fā)系統(tǒng)和開發(fā)IDE

開發(fā)系統(tǒng): Ubuntu 16.0.4 LTS
開發(fā)IDE: Visual Studio Code 版本: 1.32.3
Python版本: Python3
依賴: Django 2.2

資料《Python編程從入門到實(shí)踐》書籍

鏈接:https://pan.baidu.com/s/1USkqvL2dLU3Q9XplVaGQJg
提取碼:zoyc

GitHub:

https://github.com/lichangke/Python3_Project/tree/master/learning_log

Web應(yīng)用程序 - Django入門

http://www.itdecent.cn/p/b3267d16c245

2. 用戶賬戶

Web應(yīng)用程序的核心是讓任何用戶都能夠注冊賬戶并能夠使用它。將創(chuàng)建一些表單, 讓用戶能夠添加主題和條目, 以及編輯既有的條目。

然后, 將實(shí)現(xiàn)一個(gè)用戶身份驗(yàn)證系統(tǒng)。 你將創(chuàng)建一個(gè)注冊頁面, 供用戶創(chuàng)建賬戶, 并讓有些頁面只能供已登錄的用戶訪問。 接下來, 將修改一些視圖函數(shù),使得用戶只能看到自己的數(shù)據(jù)。

2.1 讓用戶能夠輸入數(shù)據(jù)

階段代碼:GitHub learning_log_2.1_讓用戶能夠輸入數(shù)據(jù)

不包括虛擬環(huán)境ll_env文件夾下文件

建立用于創(chuàng)建用戶賬戶的身份驗(yàn)證系統(tǒng)之前, 先來添加幾個(gè)頁面, 讓用戶能夠輸入數(shù)據(jù)。 將讓用戶能夠添加新主題、 添加新條目以及編輯既有條目。

2.1.1 添加新主題

urls -> views -> html

首先來讓用戶能夠添加新主題。 創(chuàng)建基于表單的頁面的方法幾乎與前面創(chuàng)建網(wǎng)頁一樣: 定義一個(gè)URL, 編寫一個(gè)視圖函數(shù)并編寫一個(gè)模板。 一個(gè)主要差別是, 需要導(dǎo)入包含表單的模塊forms.py。

1. 用于添加主題的表單
讓用戶輸入并提交信息的頁面都是表單, 那怕它看起來不像表單。 用戶輸入信息時(shí), 需要進(jìn)行驗(yàn)證, 確認(rèn)提供的信息是正確的數(shù)據(jù)類型。然后, 再對這些有效信息進(jìn)行處理, 并將其保存到數(shù)據(jù)庫的合適地方。 這些工作很多都是由Django自動完成的。

創(chuàng)建一個(gè)名為forms.py的文件, 將其存儲到models.py所在的目錄中

models.py

from django import forms
from .models import Topic

# 讓用戶輸入并提交信息的頁面都是表單, 那怕它看起來不像表單。
# 創(chuàng)建表單的最簡單方式是使用ModelForm, 它根據(jù)在模型中的信息自動創(chuàng)建表單。
class TopicForm(forms.ModelForm): # 定義了一個(gè)名為TopicForm 的類, 它繼承了forms.ModelForm 。
    class Meta:
        model = Topic # 根據(jù)模型Topic 創(chuàng)建一個(gè)表單
        fields = ['text'] # 該表單只包含字段text 
        labels = {'text': ''} # 讓Django不要為字段text 生成標(biāo)簽。

2. URL模式new_topic
這個(gè)新網(wǎng)頁的URL應(yīng)簡短而具有描述性, 因此當(dāng)用戶要添加新主題時(shí), 將切換到http://localhost:8000/new_topic/。 下面是網(wǎng)頁new_topic 的URL模式, 將其添加到learning_logs/urls.py中:

urls.py

--snip--
urlpatterns = [
    --snip--
    # 用于添加新主題的網(wǎng)頁
    url(r'^new_topic/$', views.new_topic, name='new_topic'),
]

這個(gè)URL模式將請求交給視圖函數(shù)new_topic() , 接下來將編寫這個(gè)函數(shù)。

3. 視圖函數(shù)new_topic()
函數(shù)new_topic() 需要處理兩種情形: 剛進(jìn)入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個(gè)空表單) ; 對提交的表單數(shù)據(jù)進(jìn)行處理, 并將用戶重定向到網(wǎng)頁topics

views.py

from django.shortcuts import render
from django.http import HttpResponseRedirect
from .models import Topic
from .forms import TopicForm
from django.urls import reverse
# from django.core.urlresolvers import reverse
'''
https://stackoverflow.com/questions/43139081/importerror-no-module-named-django-core-urlresolvers
Django 2.0 removes the django.core.urlresolvers module, which was moved to django.urls in version 1.10.
You should change any import to use django.urls instead, like this:
from django.urls import reverse
'''
--snip--
# 函數(shù)new_topic() 需要處理兩種情形: 剛進(jìn)入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個(gè)空表單) ; 對提交的表單數(shù)據(jù)進(jìn)行處理, 并將用戶重定向到網(wǎng)頁topics
def new_topic(request):
    """添加新主題"""
    if request.method != 'POST':
        # 未提交數(shù)據(jù): 創(chuàng)建一個(gè)新表單
        form = TopicForm()  # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個(gè)空表單
    else:
        if form.is_valid(): # 必須先通過檢查確定它們是有效的
            form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
            # 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時(shí)生成URL。 
            # 調(diào)用HttpResponseRedirect() 將用戶重定向到顯示新增條目所屬主題的頁面
            return HttpResponseRedirect(reverse('learning_logs:topics'))
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)
    '''
    創(chuàng)建Web應(yīng)用程序時(shí), 將用到的兩種主要請求類型是GET請求和POST請求。 對于只是從服務(wù)器讀取數(shù)據(jù)的頁面, 使用GET請求; 在用戶需要通過表單提交信息時(shí), 通常使用POST
    請求。 處理所有表單時(shí), 我們都將指定使用POST方法。 還有一些其他類型的請求, 但這個(gè)項(xiàng)目沒有使用。
    函數(shù)new_topic() 將請求對象作為參數(shù)。 用戶初次請求該網(wǎng)頁時(shí), 其瀏覽器將發(fā)送GET請求; 用戶填寫并提交表單時(shí), 其瀏覽器將發(fā)送POST請求。 根據(jù)請求的類型, 我們可以
    確定用戶請求的是空表單(GET請求) 還是要求對填寫好的表單進(jìn)行處理(POST請求) 。
    '''

4. 模板new_topic
創(chuàng)建新模板new_topic.html, 用于顯示剛創(chuàng)建的表單

new_topic.html

{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
<!--定義了一個(gè)HTML表單-->
<!--實(shí)參action 告訴服務(wù)器將提交的表單數(shù)據(jù)發(fā)送到哪里, 這里我們將它發(fā)回給視圖函數(shù)new_topic() 。 實(shí)參method 讓瀏覽器以POST請求的方式提交數(shù)據(jù)。-->
<form action="{% url 'learning_logs:new_topic' %}" method='post'>
    {% csrf_token %} <!--防止攻擊者利用表單來獲得對服務(wù)器未經(jīng)授權(quán)的訪問-->
    {{ form.as_p }} <!--顯示表單修飾符as_p 讓Django以段落格式渲染所有表單元素, 這是一種整潔地顯示表單的簡單方式-->
    <button name="submit">add topic</button> <!--Django不會為表單創(chuàng)建提交按鈕, 因此定義了一個(gè)這樣的按鈕-->
</form>
{% endblock content %}

5. 鏈接到頁面new_topic
在頁面topics 中添加一個(gè)到頁面new_topic 的鏈接:
topics.html

{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
    {% for topic in topics %}
    <li>
        <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
    </li>
    {% empty %}
    <li>No topics have been added yet.</li>
    {% endfor %}
</ul>
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}

鏈接放在了既有主題列表的后面。 下圖顯示了生成的表單。

2.1.1 add new topic.png

2.1.2 添加新條目

urls -> views -> html 添加網(wǎng)頁步驟

用戶可以添加新主題了, 但他們還想添加新條目。 將再次定義URL, 編寫視圖函數(shù)和模板, 并鏈接到添加新條目的網(wǎng)頁。 但在此之前, 需要在forms.py中再添加一個(gè)類。

1. 用于添加新條目的表單
創(chuàng)建一個(gè)與模型Entry 相關(guān)聯(lián)的表單

forms.py

from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
--snip--
class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text': ''}
        widgets = {'text': forms.Textarea(attrs={'cols': 80})}
        '''
        定義了屬性widgets 。 小部件 (widget) 是一個(gè)HTML表單元素, 如單行文本框、 多行文本區(qū)域或下拉列表。 通過設(shè)置屬性widgets , 可覆蓋Django選擇的默認(rèn)小
        部件。 通過讓Django使用forms.Textarea , 我們定制了字段'text' 的輸入小部件, 將文本區(qū)域的寬度設(shè)置為80列, 而不是默認(rèn)的40列。 這給用戶提供了足夠的空間, 可以
        編寫有意義的條目。
        '''

2. URL模式new_entry
添加新條目的頁面的URL模式中, 需要包含實(shí)參topic_id , 因?yàn)闂l目必須與特定的主題相關(guān)聯(lián)。 該URL模式如下, 將它添加到了learning_logs/urls.py中

urls.py

--snip--
urlpatterns = [
    --snip--
    # 用于添加新條目的頁面
    re_path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
]

3. 視圖函數(shù)new_entry()

views.py

--snip--
from .models import Topic
from .forms import TopicForm,EntryForm
from django.urls import reverse
--snip--

def new_entry(request, topic_id):
    """在特定的主題中添加新條目"""
    topic = Topic.objects.get(id=topic_id)
    if request.method != 'POST':
        # 未提交數(shù)據(jù),創(chuàng)建一個(gè)空表單
        form = EntryForm()
    else:
        # POST提交的數(shù)據(jù),對數(shù)據(jù)進(jìn)行處理
        form = EntryForm(data=request.POST)
        if form.is_valid(): 
            # 調(diào)用save() 時(shí), 傳遞了實(shí)參commit=False , 讓Django創(chuàng)建一個(gè)新的條目對象, 并將其存儲到new_entry 中, 但不將它保存到數(shù)據(jù)庫中。
            new_entry = form.save(commit=False)
            new_entry.topic = topic # 將new_entry的屬性topic 設(shè)置為在這個(gè)函數(shù)開頭從數(shù)據(jù)庫中獲取的主題
            new_entry.save()    # 調(diào)用save() , 且不指定任何實(shí)參。 這將把條目保存到數(shù)據(jù)庫, 并將其與正確的主題相關(guān)聯(lián)。
            return HttpResponseRedirect(reverse('learning_logs:topic',args=[topic_id]))
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)

4. 模板new_entry

new_entry.html

{% 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 %}

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>
<ul>
--snip—
</ul>
{% endblock content %}

下圖顯示了頁面new_entry

2.1.2 new_entry.png

2.1.3 編輯條目

urls -> views -> html 添加網(wǎng)頁步驟

創(chuàng)建一個(gè)頁面, 讓用戶能夠編輯既有的條目。
1. URL模式edit_entry
這個(gè)頁面的URL需要傳遞要編輯的條目的ID。 修改后的learning_logs/urls.py如下

urls.py

--snip--
# https://docs.djangoproject.com/en/2.2/ref/urls/#module-django.urls.conf
urlpatterns = [
    # 主頁
    path('', views.index, name='index'),    # Django將在文件views.py中查找函數(shù)index()

    # 顯示所有的主題
    path('topics/',views.topics,name = 'topics'),

    # 特定主題的詳細(xì)頁面
    # use a regular expression, you can use re_path(). https://stackoverflow.com/questions/47661536/django-2-0-path-error-2-0-w001-has-a-route-that-contains-p-begins-wit
    re_path(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'), # ?P<topic_id> 將匹配的值存儲到topic_id 中; 而表達(dá)式\d+ 與包含在兩個(gè)斜桿內(nèi)的任何數(shù)字都匹配, 不管這個(gè)數(shù)字為多少位。

    # 用于添加新主題的網(wǎng)頁    
    path('new_topic/', views.new_topic, name = 'new_topic'),

    # 用于添加新條目的頁面
    re_path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),

    # 用于編輯條目的頁面
    re_path(r'^edit_entry/(?P<entry_id>\d+)/$',views.edit_entry,name='edit_entry')
]

在URL(如http://localhost:8000/edit_entry/1/) 中傳遞的ID存儲在形參entry_id 中。 這個(gè)URL模式將預(yù)期匹配的請求發(fā)送給視圖函數(shù)edit_entry()

2. 視圖函數(shù)edit_entry()

頁面edit_entry 收到GET請求時(shí), edit_entry() 將返回一個(gè)表單, 讓用戶能夠?qū)l目進(jìn)行編輯。 該頁面收到POST請求(條目文本經(jīng)過修訂) 時(shí), 它將修改后的文本保存到數(shù)據(jù)庫中:

views.py

--snip--
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
--snip--
def edit_entry(request, entry_id):
    """編輯既有條目"""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic

    if request.method != 'POST':
        # 初次請求, 使用當(dāng)前條目填充表單
        form = EntryForm(instance=entry)
    else:
        # POST提交的數(shù)據(jù), 對數(shù)據(jù)進(jìn)行處理
        # 讓Django根據(jù)既有條目對象創(chuàng)建一個(gè)表單實(shí)例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進(jìn)行修改
        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)

3. 模板e(cuò)dit_entry
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 %}" method='post'> <!--實(shí)參action 將表單發(fā)回給函數(shù)edit_entry() 進(jìn)行處理-->
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">save changes</button>
</form>
{% endblock content %}

4. 鏈接到頁面edit_entry

在顯示特定主題的頁面中, 需要給每個(gè)條目添加到頁面edit_entry 的鏈接:

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--

下圖顯示了包含這些鏈接時(shí), 顯示特定主題的頁面是什么樣的


2.1.3 edit entry.png

2.2 創(chuàng)建用戶賬戶

階段代碼:GitHub learning_log_2.2_創(chuàng)建用戶賬戶

不包括虛擬環(huán)境ll_env文件夾下文件

將建立一個(gè)用戶注冊和身份驗(yàn)證系統(tǒng), 讓用戶能夠注冊賬戶, 進(jìn)而登錄和注銷。 將創(chuàng)建一個(gè)新的應(yīng)用程序, 其中包含與處理用戶賬戶相關(guān)的所有功能。 還將對模型Topic 稍做修改, 讓每個(gè)主題都?xì)w屬于特定用戶。

2.2.1 應(yīng)用程序users

步驟:

startapp創(chuàng)建應(yīng)用程序 -> 將應(yīng)用程序添加到settings.py -> 包含應(yīng)用程序users 的URL

先使用命令startapp 來創(chuàng)建一個(gè)名為users 的應(yīng)用程序

python manage.py startapp users

2.2.1 startapp.png

1. 將應(yīng)用程序users 添加到settings.py中

在settings.py中, 我們需要將這個(gè)新的應(yīng)用程序添加到INSTALLED_APPS 中

settings.py

--snip--
INSTALLED_APPS = (
--snip--
# 我的應(yīng)用程序
'learning_logs',
'users',
)-
-snip

2. 包含應(yīng)用程序users 的URL

需要修改項(xiàng)目根目錄中的urls.py, 使其包含為應(yīng)用程序users 定義的URL:

urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),# 該模塊定義了可在管理網(wǎng)站中請求的所有URL
    path('', include('learning_logs.urls', namespace='learning_logs')), 
    # 代碼包含實(shí)參namespace , 讓我們能夠?qū)earning_logs 的URL同項(xiàng)目中的其他URL區(qū)分開來
    path('users/',include('users.urls', namespace='users')), # 這行代碼與任何以單詞users打頭的URL(如http://localhost:8000/users/login/) 都匹配
]

2.2.2 登錄頁面

首先來實(shí)現(xiàn)登錄頁面的功能。 為此, 將使用Django提供的默認(rèn)登錄視圖, 因此URL模式會稍有不同。 在目錄learning_log/users/中, 新建一個(gè)名為urls.py的文件, 并在其中添加如下代碼:

urls.py

"""為應(yīng)用程序users定義URL模式"""

# 非 from django.conf.urls import url
from django.urls import path,re_path
from django.contrib.auth.views import LoginView
# from django.contrib.auth.views import login  In django-2.1, the old function-based views have been removed,

from . import views

app_name= 'users'# 不能少

urlpatterns = [
    # 登錄頁面
    # re_path(r'^login/$', login, {'template_name': 'users/login.html'},name='login'),
    re_path(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login')
]

注意 注釋部分為原書中代碼,Django版本不同需使用新的方式。app_name= 'users'# 不能少

1. 模板login.html
用戶請求登錄頁面時(shí), Django將使用其默認(rèn)視圖login , 但依然需要為這個(gè)頁面提供模板。 為此, 在目錄learning_log/users/中, 創(chuàng)建一個(gè)名為templates的目錄, 并在其中創(chuàng)建一個(gè)名為users的目錄。 以下是模板login.html, 你應(yīng)將其存儲到目錄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' %}" />
    <!--包含了一個(gè)隱藏的表單元素——'next' , 其中的實(shí)參value 告訴Django在用戶成功登錄后將其重定向到什么地方——在這里是主頁。-->
</form>

{% endblock content %}

2. 鏈接到登錄頁面

在base.html中添加到登錄頁面的鏈接, 讓所有頁面都包含它。 用戶已登錄時(shí), 我們不想顯示這個(gè)鏈接, 因此將它嵌套在一個(gè){% if %} 標(biāo)簽中

base.html

<p>
    <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
    <a href="{% url 'learning_logs:topics' %}">Topics</a>
    <!--在Django身份驗(yàn)證系統(tǒng)中, 每個(gè)模板都可使用變量user , 這個(gè)變量有一個(gè)is_authenticated 屬性: 如果用戶已登錄, 該屬性將為True , 否則為False 。-->
    {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    {% else %}
    <a href="{% url 'users:login' %}">log in</a>
    {% endif %}
</p>

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

3. 使用登錄頁面
前面建立了一個(gè)用戶賬戶, 下面來登錄一下, 看看登錄頁面是否管用。 請?jiān)L問http://localhost:8000/admin/, 如果你依然是以管理員的身份登錄的, 請?jiān)陧撁忌险业阶N鏈接并單擊它。

訪問http://localhost:8000/users/login/, 你將看到類似于下圖所示的登錄頁面。

2.2.2 login.png

2.2.3 注銷

需要提供一個(gè)讓用戶注銷的途徑。 我們不創(chuàng)建用于注銷的頁面, 而讓用戶只需單擊一個(gè)鏈接就能注銷并返回到主頁。 為此, 將為注銷鏈接定義一個(gè)URL模式, 編寫一個(gè)視圖函數(shù), 并在base.html中添加一個(gè)注銷鏈接

1. 注銷URL
下面的代碼為注銷定義了URL模式, 該模式與URL http://locallwst:8000/users/logout/匹配。 修改后的users/urls.py如下

urls.py

--snip--
urlpatterns = [
    # 登錄頁面
    # re_path(r'^login/$', login, {'template_name': 'users/login.html'},name='login'),
    re_path(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login'),
    # 注銷
    re_path(r'^logout/$', views.logout_view, name='logout'),
]


個(gè)URL模式將請求發(fā)送給函數(shù)logout_view() 。 這樣給這個(gè)函數(shù)命名, 旨在將其與我們將在其中調(diào)用的函數(shù)logout() 區(qū)分開來(請確保你修改的是users/urls.py, 而不是learning_log/ urls.py) 。

2. 視圖函數(shù)logout_view()

函數(shù)logout_view() 很簡單: 只是導(dǎo)入Django函數(shù)logout() , 并調(diào)用它, 再重定向到主頁。 請打開users/views.py, 并輸入下面的代碼

views.py

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

# Create your views here.
def logout_view(request):
    """注銷用戶"""
    logout(request)
    return HttpResponseRedirect(reverse('learning_logs:index'))

3. 鏈接到注銷視圖

需要添加一個(gè)注銷鏈接。 我們在base.html中添加這種鏈接, 讓每個(gè)頁面都包含它; 將它放在標(biāo)簽{% if user.is_authenticated %} 中, 使得僅當(dāng)用戶登錄后才能看到它:

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--

下圖顯示了用戶登錄后看到的主頁

2.2.3 logout.png

2.2.4 注冊頁面

下面來創(chuàng)建一個(gè)讓新用戶能夠注冊的頁面。 將使用Django提供的表單UserCreationForm , 但編寫自己的視圖函數(shù)和模板。

1. 注冊頁面的URL模式

下面的代碼定義了注冊頁面的URL模式, 它也包含在users/urls.py中

urls.py

--snip--
urlpatterns = [
    # 登錄頁面
    --snip--
    # 注冊頁面
    re_path(r'^register/$', views.register, name='register'), # 與URL http://localhost:8000/users/register/匹配, 并將請求發(fā)送給我們即將編寫的函數(shù)register()
]

這個(gè)模式與URL http://localhost:8000/users/register/匹配, 并將請求發(fā)送給我們即將編寫的函數(shù)register() 。

2. 視圖函數(shù)register()

在注冊頁面首次被請求時(shí), 視圖函數(shù)register() 需要顯示一個(gè)空的注冊表單, 并在用戶提交填寫好的注冊表單時(shí)對其進(jìn)行處理。 如果注冊成功, 這個(gè)函數(shù)還需讓用戶自動登錄。 請?jiān)趗sers/views.py中添加如下代碼

views.py

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import login,logout,authenticate
from django.contrib.auth.forms import UserCreationForm


# Create your views here.
def logout_view(request):
    --snip--
def register(request):
    """注冊新用戶"""
    if request.method != 'POST':
        # 顯示空的注冊表單
        form = UserCreationForm()
    else:
        # 處理填寫好的表單   
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            # 讓用戶自動登錄, 再重定向到主頁
            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)   

3. 注冊模板

注冊頁面的模板與登錄頁面的模板類似, 請務(wù)必將其保存到login.html所在的目錄中

register.html

{% extends "learning_logs/base.html" %}
{% block content %}
<form method="post" action="{% url 'users:register' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">register</button>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}

使用了方法as_p , 讓Django在表單中正確地顯示所有的字段, 包括錯(cuò)誤消息——如果用戶沒有正確地填寫表單。

4. 鏈接到注冊頁面

添加這樣的代碼, 即在用戶沒有登錄時(shí)顯示到注冊頁面的鏈接

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--

如下圖所示


2.2.4 register.png

2.3 讓用戶擁有自己的數(shù)據(jù)

階段代碼:GitHub learning_log_2.3_讓用戶擁有自己的數(shù)據(jù)

不包括虛擬環(huán)境ll_env文件夾下文件

用戶應(yīng)該能夠輸入其專有的數(shù)據(jù), 因此將創(chuàng)建一個(gè)系統(tǒng), 確定各項(xiàng)數(shù)據(jù)所屬的用戶, 再限制對頁面的訪問, 讓用戶只能使用自己的數(shù)據(jù)

將修改模型Topic , 讓每個(gè)主題都?xì)w屬于特定用戶。 這也將影響條目, 因?yàn)槊總€(gè)條目都屬于特定的主題。 先來限制對一些頁面的訪問。

2.3.1 使用@login_required 限制訪問

Django提供了裝飾器@login_required , 讓你能夠輕松地實(shí)現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們。 裝飾器 (decorator) 是放在函數(shù)定義前面的指令, Python在函數(shù)運(yùn)行前, 根據(jù)它來修改函數(shù)代碼的行為。

1. 限制對topics 頁面的訪問

每個(gè)主題都?xì)w特定用戶所有, 因此應(yīng)只允許已登錄的用戶請求topics 頁面。 為此, 在learning_logs/views.py中添加如下代碼

views.py

--snip--
from django.contrib.auth.decorators import login_required
--snip--
#Django提供了裝飾器@login_required , 讓你能夠輕松地實(shí)現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們
@login_required  
def topics(request):
    """顯示所有的主題"""
--snip--

導(dǎo)入了函數(shù)login_required() 。 我們將login_required() 作為裝飾器用于視圖函數(shù)topics() ——在它前面加上符號@ 和login_required , 讓Python在運(yùn)行topics() 的代碼前先運(yùn)行l(wèi)ogin_required() 的代碼。

login_required() 的代碼檢查用戶是否已登錄, 僅當(dāng)用戶已登錄時(shí), Django才運(yùn)行topics() 的代碼。 如果用戶未登錄, 就重定向到登錄頁面。

為實(shí)現(xiàn)這種重定向, 需要修改settings.py, 讓Django知道到哪里去查找登錄頁面。 請?jiān)趕ettings.py末尾添加如下代碼

settings.py


--snip--
# 我的設(shè)置
LOGIN_URL = '/users/login/'

如果未登錄的用戶請求裝飾器@login_required 的保護(hù)頁面, Django將重定向到settings.py中的LOGIN_URL 指定的URL

2. 全面限制對項(xiàng)目“學(xué)習(xí)筆記”的訪問

Django讓你能夠輕松地限制對頁面的訪問, 但你必須針對要保護(hù)哪些頁面做出決定。 最好先確定項(xiàng)目的哪些頁面不需要保護(hù), 再限制對其他所有頁面的訪問。 可以輕松地修改過于嚴(yán)格的訪問限制, 其風(fēng)險(xiǎn)比不限制對敏感頁面的訪問更低。

在項(xiàng)目“學(xué)習(xí)筆記”中, 將不限制對主頁、 注冊頁面和注銷頁面的訪問, 并限制對其他所有頁面的訪問。

在下面的learning_logs/views.py中, 對除index() 外的每個(gè)視圖都應(yīng)用了裝飾器@login_required

views.py

--snip--
@login_required
def topics(request):
--snip--
@login_required
def topic(request, topic_id):
--snip--
@login_required
def new_topic(request):
--snip--
@login_required
def new_entry(request, topic_id):
--snip--
@login_required
def edit_entry(request, entry_id):
--snip

2.3.2 將數(shù)據(jù)關(guān)聯(lián)到用戶

需要將數(shù)據(jù)關(guān)聯(lián)到提交它們的用戶。 我們只需將最高層的數(shù)據(jù)關(guān)聯(lián)到用戶, 這樣更低層的數(shù)據(jù)將自動關(guān)聯(lián)到用戶。 例如, 在項(xiàng)目“學(xué)習(xí)筆記”中, 應(yīng)用程序的最高層數(shù)據(jù)是主題, 而所有條目都與特定主題相關(guān)聯(lián)。 只要每個(gè)主題都?xì)w屬于特定用戶, 我們就能確定數(shù)據(jù)庫中每個(gè)條目的所有者。

修改模型Topic , 在其中添加一個(gè)關(guān)聯(lián)到用戶的外鍵。 這樣做后, 我們必須對數(shù)據(jù)庫進(jìn)行遷移。 最后, 我們必須對有些視圖進(jìn)行修改, 使其只顯示與當(dāng)前登錄的用戶相關(guān)
聯(lián)的數(shù)據(jù)

1. 修改模型Topic

對models.py的修改只涉及兩行代碼:

models.py

from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
    ''' 用戶學(xué)習(xí)的主題'''
    # https://docs.djangoproject.com/en/2.2/ref/models/fields/#charfield
    text = models.CharField(max_length = 200) # 屬性text是一個(gè)CharField——由字符或文本組成的數(shù)據(jù)
    # https://docs.djangoproject.com/en/2.2/ref/models/fields/#datetimefield
    date_added = models.DateTimeField(auto_now_add=True) # 實(shí)參auto_add_now=True 讓Django將這個(gè)屬性自動設(shè)置成當(dāng)前日期和時(shí)間。
    owner = models.ForeignKey(User,on_delete=models.CASCADE) # 
    def __str__(self):
        """返回模型的字符串表示"""
        return self.text

class Entry(models.Model):
    --snip--

2. 確定當(dāng)前有哪些用戶
遷移數(shù)據(jù)庫時(shí), Django將對數(shù)據(jù)庫進(jìn)行修改, 使其能夠存儲主題和用戶之間的關(guān)聯(lián)。 為執(zhí)行遷移, Django需要知道該將各個(gè)既有主題關(guān)聯(lián)到哪個(gè)用戶。 最簡單的辦法是, 將既有主題都關(guān)聯(lián)到同一個(gè)用戶, 如超級用戶。 為此, 我們需要知道該用戶的ID。

查看已創(chuàng)建的所有用戶的ID。 為此, 啟動一個(gè)Django shell會話, 并執(zhí)行如下命令:

python manage.py shell

2.3.2 shell.png

3. 遷移數(shù)據(jù)庫
知道用戶ID后, 就可以遷移數(shù)據(jù)庫了

python manage.py makemigrations learning_logs

2.3.2 makemigrations .png

為將所有既有主題都關(guān)聯(lián)到管理用戶ll_admin, 我輸入了用戶ID值2。并非必須使用超級用戶, 而可使用已創(chuàng)建的任何用戶的ID。 接下來, Django使用這個(gè)值來遷移數(shù)據(jù)庫, 并生成了遷移文件0003_topic_owner.py, 它在模型Topic 中添加字段owner 。

現(xiàn)在可以執(zhí)行遷移了。 為此, 在活動的虛擬環(huán)境中執(zhí)行下面的命令

python manage.py migrate

2.3.2 migrate.png

為驗(yàn)證遷移符合預(yù)期, 可在shell會話中像下面這樣做:

2.3.2 shell.png

2.3.3 只允許用戶訪問自己的主題

當(dāng)前, 不管你以哪個(gè)用戶的身份登錄, 都能夠看到所有的主題。 改變這種情況, 只向用戶顯示屬于自己的主題

在views.py中, 對函數(shù)topics() 做如下修改

views.py

--snip--
 # Django提供了裝飾器@login_required , 讓你能夠輕松地實(shí)現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們
@login_required
def topics(request):
    """顯示所有的主題"""
    # topics = Topic.objects.order_by('date_added') # 查詢數(shù)據(jù)庫——請求提供Topic 對象, 并按屬性date_added 對它們進(jìn)行排序
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics': topics} # 一個(gè)將要發(fā)送給模板的上下文。 上下文是一個(gè)字典, 其中的鍵是我們將在模板中用來訪問數(shù)據(jù)的名稱, 而值是我們要發(fā)送給模板的數(shù)據(jù)。 
    return render(request, 'learning_logs/topics.html', context)
--snip--

要查看結(jié)果, 以所有既有主題關(guān)聯(lián)到的用戶的身份登錄, 并訪問topics頁面, 你將看到所有的主題。 然后, 注銷并以另一個(gè)用戶的身份登錄, topics頁面將不會列出任何主題。

2.3.4 保護(hù)用戶的主題

還沒有限制對顯示單個(gè)主題的頁面的訪問, 因此任何已登錄的用戶都可輸入類似于http://localhost:8000/topics/1/的URL, 來訪問顯示相應(yīng)主題的頁面

為修復(fù)這種問題, 我們在視圖函數(shù)topic() 獲取請求的條目前執(zhí)行檢查:

views.py

--snip--
from django.http import HttpResponseRedirect, Http404
--snip--
@login_required
def topic(request, topic_id):
    """顯示單個(gè)主題及其所有的條目"""
    topic = Topic.objects.get(id=topic_id)
    # 確認(rèn)請求的主題屬于當(dāng)前用戶
    if topic.owner != request.user:
        raise Http404
    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)
--snip--

導(dǎo)入了異常Http404,并在用戶請求它不能查看的主題時(shí)引發(fā)這個(gè)異常

2.3.4 Http404.png

2.3.5 保護(hù)頁面edit_entry

頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / , 其中 entry_id 是一個(gè)數(shù)字。 下面來保護(hù)這個(gè)頁面, 禁止用戶通過輸入類似于前面的URL來訪問其他用戶的條目

views.py

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

    if topic.owner != request.user: # 保護(hù)頁面edit_entry
        raise Http404

    if request.method != 'POST':
        # 初次請求, 使用當(dāng)前條目填充表單
        form = EntryForm(instance=entry)
    else:
        # POST提交的數(shù)據(jù), 對數(shù)據(jù)進(jìn)行處理
        # 讓Django根據(jù)既有條目對象創(chuàng)建一個(gè)表單實(shí)例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進(jìn)行修改
        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)

獲取指定的條目以及與之相關(guān)聯(lián)的主題, 然后檢查主題的所有者是否是當(dāng)前登錄的用戶, 如果不是, 就引發(fā)Http404 異常

2.3.6 將新主題關(guān)聯(lián)到當(dāng)前用戶

當(dāng)前, 用于添加新主題的頁面存在問題, 因此它沒有將新主題關(guān)聯(lián)到特定用戶。 如果你嘗試添加新主題, 將看到錯(cuò)誤消息IntegrityError , 指出learning_logs_topic.user_id 不能為NULL 。 Django的意思是說, 創(chuàng)建新主題時(shí), 你必須指定其owner 字段的值。

可以通過request 對象獲悉當(dāng)前用戶, 因此存在一個(gè)修復(fù)這種問題的簡單方案。 請?zhí)砑酉旅娴拇a, 將新主題關(guān)聯(lián)到當(dāng)前用戶:

views.py

--snip--
@login_required
def new_topic(request):
    """添加新主題"""
    if request.method != 'POST':
        # 未提交數(shù)據(jù): 創(chuàng)建一個(gè)新表單
        form = TopicForm()  # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個(gè)空表單
    else:
        '''
        if form.is_valid(): # 必須先通過檢查確定它們是有效的
            form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
            # 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時(shí)生成URL。 
            # 調(diào)用HttpResponseRedirect() 將用戶重定向到顯示新增條目所屬主題的頁面
            return HttpResponseRedirect(reverse('learning_logs:topics'))
        '''
        if form.is_valid(): # 將新主題關(guān)聯(lián)到當(dāng)前用戶
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            return HttpResponseRedirect(reverse('learning_logs:topics'))
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)
--snip--

現(xiàn)在, 這個(gè)項(xiàng)目允許任何用戶注冊, 而每個(gè)用戶想添加多少新主題都可以。 每個(gè)用戶都只能訪問自己的數(shù)據(jù), 無論是查看數(shù)據(jù)、 輸入新數(shù)據(jù)還是修改舊數(shù)據(jù)時(shí)都如此。


GitHub鏈接:
https://github.com/lichangke/LeetCode
知乎個(gè)人首頁:
https://www.zhihu.com/people/lichangke/
簡書個(gè)人首頁:
http://www.itdecent.cn/u/3e95c7555dc7
個(gè)人Blog:
https://lichangke.github.io/
歡迎大家來一起交流學(xué)習(xí)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容