Python編程從入門到實踐: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編程從入門到實踐》書籍

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

GitHub:

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

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

http://www.lxweimin.com/p/b3267d16c245

2. 用戶賬戶

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

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

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

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

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

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

2.1.1 添加新主題

urls -> views -> html

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

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

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

models.py

from django import forms
from .models import Topic

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

2. URL模式new_topic
這個新網(wǎng)頁的URL應(yīng)簡短而具有描述性, 因此當(dāng)用戶要添加新主題時, 將切換到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'),
]

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

3. 視圖函數(shù)new_topic()
函數(shù)new_topic() 需要處理兩種情形: 剛進入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個空表單) ; 對提交的表單數(shù)據(jù)進行處理, 并將用戶重定向到網(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() 需要處理兩種情形: 剛進入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個空表單) ; 對提交的表單數(shù)據(jù)進行處理, 并將用戶重定向到網(wǎng)頁topics
def new_topic(request):
    """添加新主題"""
    if request.method != 'POST':
        # 未提交數(shù)據(jù): 創(chuàng)建一個新表單
        form = TopicForm()  # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個空表單
    else:
        if form.is_valid(): # 必須先通過檢查確定它們是有效的
            form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
            # 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時生成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)用程序時, 將用到的兩種主要請求類型是GET請求和POST請求。 對于只是從服務(wù)器讀取數(shù)據(jù)的頁面, 使用GET請求; 在用戶需要通過表單提交信息時, 通常使用POST
    請求。 處理所有表單時, 我們都將指定使用POST方法。 還有一些其他類型的請求, 但這個項目沒有使用。
    函數(shù)new_topic() 將請求對象作為參數(shù)。 用戶初次請求該網(wǎng)頁時, 其瀏覽器將發(fā)送GET請求; 用戶填寫并提交表單時, 其瀏覽器將發(fā)送POST請求。 根據(jù)請求的類型, 我們可以
    確定用戶請求的是空表單(GET請求) 還是要求對填寫好的表單進行處理(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>
<!--定義了一個HTML表單-->
<!--實參action 告訴服務(wù)器將提交的表單數(shù)據(jù)發(fā)送到哪里, 這里我們將它發(fā)回給視圖函數(shù)new_topic() 。 實參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)建提交按鈕, 因此定義了一個這樣的按鈕-->
</form>
{% endblock content %}

5. 鏈接到頁面new_topic
在頁面topics 中添加一個到頁面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中再添加一個類。

1. 用于添加新條目的表單
創(chuàng)建一個與模型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) 是一個HTML表單元素, 如單行文本框、 多行文本區(qū)域或下拉列表。 通過設(shè)置屬性widgets , 可覆蓋Django選擇的默認小
        部件。 通過讓Django使用forms.Textarea , 我們定制了字段'text' 的輸入小部件, 將文本區(qū)域的寬度設(shè)置為80列, 而不是默認的40列。 這給用戶提供了足夠的空間, 可以
        編寫有意義的條目。
        '''

2. URL模式new_entry
添加新條目的頁面的URL模式中, 需要包含實參topic_id , 因為條目必須與特定的主題相關(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)建一個空表單
        form = EntryForm()
    else:
        # POST提交的數(shù)據(jù),對數(shù)據(jù)進行處理
        form = EntryForm(data=request.POST)
        if form.is_valid(): 
            # 調(diào)用save() 時, 傳遞了實參commit=False , 讓Django創(chuàng)建一個新的條目對象, 并將其存儲到new_entry 中, 但不將它保存到數(shù)據(jù)庫中。
            new_entry = form.save(commit=False)
            new_entry.topic = topic # 將new_entry的屬性topic 設(shè)置為在這個函數(shù)開頭從數(shù)據(jù)庫中獲取的主題
            new_entry.save()    # 調(diào)用save() , 且不指定任何實參。 這將把條目保存到數(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)建一個頁面, 讓用戶能夠編輯既有的條目。
1. URL模式edit_entry
這個頁面的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'),

    # 特定主題的詳細頁面
    # 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+ 與包含在兩個斜桿內(nèi)的任何數(shù)字都匹配, 不管這個數(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 中。 這個URL模式將預(yù)期匹配的請求發(fā)送給視圖函數(shù)edit_entry()

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

頁面edit_entry 收到GET請求時, edit_entry() 將返回一個表單, 讓用戶能夠?qū)l目進行編輯。 該頁面收到POST請求(條目文本經(jīng)過修訂) 時, 它將修改后的文本保存到數(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ù)進行處理
        # 讓Django根據(jù)既有條目對象創(chuàng)建一個表單實例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進行修改
        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. 模板edit_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'> <!--實參action 將表單發(fā)回給函數(shù)edit_entry() 進行處理-->
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">save changes</button>
</form>
{% endblock content %}

4. 鏈接到頁面edit_entry

在顯示特定主題的頁面中, 需要給每個條目添加到頁面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--

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


2.1.3 edit entry.png

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

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

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

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

2.2.1 應(yīng)用程序users

步驟:

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

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

python manage.py startapp users

2.2.1 startapp.png

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

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

settings.py

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

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

需要修改項目根目錄中的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')), 
    # 代碼包含實參namespace , 讓我們能夠?qū)earning_logs 的URL同項目中的其他URL區(qū)分開來
    path('users/',include('users.urls', namespace='users')), # 這行代碼與任何以單詞users打頭的URL(如http://localhost:8000/users/login/) 都匹配
]

2.2.2 登錄頁面

首先來實現(xiàn)登錄頁面的功能。 為此, 將使用Django提供的默認登錄視圖, 因此URL模式會稍有不同。 在目錄learning_log/users/中, 新建一個名為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
用戶請求登錄頁面時, Django將使用其默認視圖login , 但依然需要為這個頁面提供模板。 為此, 在目錄learning_log/users/中, 創(chuàng)建一個名為templates的目錄, 并在其中創(chuàng)建一個名為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' %}" />
    <!--包含了一個隱藏的表單元素——'next' , 其中的實參value 告訴Django在用戶成功登錄后將其重定向到什么地方——在這里是主頁。-->
</form>

{% endblock content %}

2. 鏈接到登錄頁面

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

base.html

<p>
    <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
    <a href="{% url 'learning_logs:topics' %}">Topics</a>
    <!--在Django身份驗證系統(tǒng)中, 每個模板都可使用變量user , 這個變量有一個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. 使用登錄頁面
前面建立了一個用戶賬戶, 下面來登錄一下, 看看登錄頁面是否管用。 請訪問http://localhost:8000/admin/, 如果你依然是以管理員的身份登錄的, 請在頁眉上找到注銷鏈接并單擊它。

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

2.2.2 login.png

2.2.3 注銷

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

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'),
]


個URL模式將請求發(fā)送給函數(shù)logout_view() 。 這樣給這個函數(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. 鏈接到注銷視圖

需要添加一個注銷鏈接。 我們在base.html中添加這種鏈接, 讓每個頁面都包含它; 將它放在標(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)建一個讓新用戶能夠注冊的頁面。 將使用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()
]

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

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

在注冊頁面首次被請求時, 視圖函數(shù)register() 需要顯示一個空的注冊表單, 并在用戶提交填寫好的注冊表單時對其進行處理。 如果注冊成功, 這個函數(shù)還需讓用戶自動登錄。 請在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 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在表單中正確地顯示所有的字段, 包括錯誤消息——如果用戶沒有正確地填寫表單。

4. 鏈接到注冊頁面

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

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)建一個系統(tǒng), 確定各項數(shù)據(jù)所屬的用戶, 再限制對頁面的訪問, 讓用戶只能使用自己的數(shù)據(jù)

將修改模型Topic , 讓每個主題都歸屬于特定用戶。 這也將影響條目, 因為每個條目都屬于特定的主題。 先來限制對一些頁面的訪問。

2.3.1 使用@login_required 限制訪問

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

1. 限制對topics 頁面的訪問

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

views.py

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

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

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

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

settings.py


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

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

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

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

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

在下面的learning_logs/views.py中, 對除index() 外的每個視圖都應(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)到用戶。 例如, 在項目“學(xué)習(xí)筆記”中, 應(yīng)用程序的最高層數(shù)據(jù)是主題, 而所有條目都與特定主題相關(guān)聯(lián)。 只要每個主題都歸屬于特定用戶, 我們就能確定數(shù)據(jù)庫中每個條目的所有者。

修改模型Topic , 在其中添加一個關(guān)聯(lián)到用戶的外鍵。 這樣做后, 我們必須對數(shù)據(jù)庫進行遷移。 最后, 我們必須對有些視圖進行修改, 使其只顯示與當(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是一個CharField——由字符或文本組成的數(shù)據(jù)
    # https://docs.djangoproject.com/en/2.2/ref/models/fields/#datetimefield
    date_added = models.DateTimeField(auto_now_add=True) # 實參auto_add_now=True 讓Django將這個屬性自動設(shè)置成當(dāng)前日期和時間。
    owner = models.ForeignKey(User,on_delete=models.CASCADE) # 
    def __str__(self):
        """返回模型的字符串表示"""
        return self.text

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

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

查看已創(chuàng)建的所有用戶的ID。 為此, 啟動一個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使用這個值來遷移數(shù)據(jù)庫, 并生成了遷移文件0003_topic_owner.py, 它在模型Topic 中添加字段owner 。

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

python manage.py migrate

2.3.2 migrate.png

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

2.3.2 shell.png

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

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

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

views.py

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

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

2.3.4 保護用戶的主題

還沒有限制對顯示單個主題的頁面的訪問, 因此任何已登錄的用戶都可輸入類似于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):
    """顯示單個主題及其所有的條目"""
    topic = Topic.objects.get(id=topic_id)
    # 確認請求的主題屬于當(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,并在用戶請求它不能查看的主題時引發(fā)這個異常

2.3.4 Http404.png

2.3.5 保護頁面edit_entry

頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / , 其中 entry_id 是一個數(shù)字。 下面來保護這個頁面, 禁止用戶通過輸入類似于前面的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: # 保護頁面edit_entry
        raise Http404

    if request.method != 'POST':
        # 初次請求, 使用當(dāng)前條目填充表單
        form = EntryForm(instance=entry)
    else:
        # POST提交的數(shù)據(jù), 對數(shù)據(jù)進行處理
        # 讓Django根據(jù)既有條目對象創(chuàng)建一個表單實例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進行修改
        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)到特定用戶。 如果你嘗試添加新主題, 將看到錯誤消息IntegrityError , 指出learning_logs_topic.user_id 不能為NULL 。 Django的意思是說, 創(chuàng)建新主題時, 你必須指定其owner 字段的值。

可以通過request 對象獲悉當(dāng)前用戶, 因此存在一個修復(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)建一個新表單
        form = TopicForm()  # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個空表單
    else:
        '''
        if form.is_valid(): # 必須先通過檢查確定它們是有效的
            form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
            # 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時生成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)在, 這個項目允許任何用戶注冊, 而每個用戶想添加多少新主題都可以。 每個用戶都只能訪問自己的數(shù)據(jù), 無論是查看數(shù)據(jù)、 輸入新數(shù)據(jù)還是修改舊數(shù)據(jù)時都如此。


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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容