19.3 讓用戶擁有自己的數(shù)據(jù)
這一節(jié),我們將對一些頁面進(jìn)行限制,僅讓已登錄的用戶訪問它們远搪,我們還將確保每個主題都屬于特定用戶。我們將創(chuàng)建一個系統(tǒng)么鹤,確保各項數(shù)據(jù)所屬的用戶终娃,再限制對頁面的訪問味廊,讓用戶只能使用自己的數(shù)據(jù)蒸甜。
在本節(jié)中,我們將修改模型Topic余佛,讓每個主題都?xì)w屬于特定用戶柠新。這也將影響條目,因為每個條目都屬于特定的主題伸刃。我們先來限制對一些頁面的訪問
19.3.1 使用@login_required限制訪問
Django提供了裝飾器@login_required拍顷,讓你能夠輕松地實現(xiàn)這樣的目標(biāo):對于某些頁面吟榴,只允許已登錄的用戶訪問它們。裝飾器(decorator)是放在函數(shù)定義前面的指令憔恳,Python在函數(shù)運行前,根據(jù)它來修飾函數(shù)代碼的行為净蚤。下面來看一個示例
1. 限制對topics頁面的訪問
每個主題都?xì)w特定用戶所有钥组,因此應(yīng)只允許已登錄的用戶請求topics頁面。為此今瀑,修改learning_logs/views.py 為如下代碼
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
# Create your views here.
def index(request):
return render(request, 'learning_logs/index.html')
@login_required
def topics(request):
"""show all topics"""
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
entries = topic.entry_set.order_by('date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 未提交數(shù)據(jù): 創(chuàng)建一個新表單
form = TopicForm()
else:
# POST提交的數(shù)據(jù)程梦, 對數(shù)據(jù)進(jìn)行處理
form = TopicForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
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ù)進(jìn)行處理
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)
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ù)進(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)
login_required()的代碼檢查用戶是否已登錄哥童,僅當(dāng)用戶已登錄時挺份,Django才運行topics()的代碼。
如果用戶未登錄贮懈,就重定向到登錄頁面压恒。為實現(xiàn)這種重定向,我們需要修改settings.py错邦,讓Django知道到哪里去查找登錄頁面探赫。在settings.py末尾添加如下代碼:
# my settings
LOGIN_URL = '/users/login/'
現(xiàn)在,如果未登錄的用戶請求裝飾器@login_required的保護(hù)頁面撬呢,Django將重定向到settings.py中LOGIN_URL指定的URL伦吠。在這里單擊Topics鏈接,未登錄用戶將重定向到登錄頁面。
2. 全面限制對項目“學(xué)習(xí)筆記”的訪問
Django使我們能夠輕松的限制對頁面的訪問毛仪,但你必須針對要保護(hù)哪些頁面做出決定搁嗓。最好先確定哪些頁面不需要保護(hù),再限制對其他所有頁面的訪問箱靴。你可以輕松地修改過于嚴(yán)格的訪問限制腺逛,其風(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--
19.3.2 將數(shù)據(jù)關(guān)聯(lián)到用戶
現(xià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)铁追。只要每個主題都?xì)w屬于特定用戶,我們就能確定數(shù)據(jù)庫中每個條目的所有者讨韭。
下面來修改模型Topic脂信,在其中添加一個關(guān)聯(lián)到用戶的外鍵。這樣做后透硝,我們必須對數(shù)據(jù)庫進(jìn)行遷移狰闪。最后,我們必須對有些視圖進(jìn)行修改濒生,使其只顯示與當(dāng)前登錄的用戶相關(guān)聯(lián)的數(shù)據(jù)埋泵。
1. 修改模型Topic
對models.py的修改只涉及兩行代碼:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
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)
def __str__(self):
return self.text
class Entry(models.Model):
--snip--
我們先導(dǎo)入了django.contrib.auth中的模型User,然后在Topic中添加了字段owner罪治,它建立到模型User的外鍵關(guān)系丽声。
2. 確定當(dāng)前有哪些用戶
我們遷移數(shù)據(jù)庫時觉义,Django將對數(shù)據(jù)庫進(jìn)行修改雁社,使其能夠存儲主題和用戶之間的關(guān)聯(lián)。為執(zhí)行遷移晒骇,Django需要知道該將各個既有主題關(guān)聯(lián)到哪些用戶霉撵。最簡單的辦法是磺浙,將既有主題都關(guān)聯(lián)到同一個用戶,如超級用戶徒坡。為此撕氧,我們需要知道該用戶的ID。
下面來查看已創(chuàng)建的所有用戶的ID喇完。啟用Django shell回話伦泥,并執(zhí)行如下命令:
(ll_env) c5220056@GMPTIC:~/myworkplace$ python manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: testuser>, <User: Yolanda>]>
>>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
testuser 2
Yolanda 3
Django詢問要將既有主題關(guān)聯(lián)到哪個用戶時,我們將指定其中的一個ID值锦溪。
3. 遷移數(shù)據(jù)庫
知道用戶ID后不脯,就可以遷移數(shù)據(jù)庫了。
(ll_env) c5220056@GMPTIC:~/myworkplace$ python manage.py makemigrations 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
我們首先執(zhí)行了命令makemigrations海洼,在輸出中跨新,Django指出我們試圖給既有模型Topic添加一個必不可少(不可為空)的字段富腊,而該字段沒有默認(rèn)值坏逢。Django提供了兩種選擇:1. 現(xiàn)在提供默認(rèn)值 2. 退出并在models.py中添加默認(rèn)值。
為將所有既有主題都關(guān)聯(lián)到管理用戶ll_admin赘被, 我輸入了用戶ID值為1是整。接下來,Django使用這個值來遷移數(shù)據(jù)庫民假,并生成了遷移文件0003_topic_owner.py浮入,它再模型Topic中添加字段owner。
現(xiàn)在可以執(zhí)行遷移了羊异。
(ll_env) c5220056@GMPTIC:~/myworkplace$ 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
為驗證遷移是否符合預(yù)期事秀,可在shell會話中像下面這樣做
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
注意:你可以重置數(shù)據(jù)庫而不是遷移它,但如果這樣做野舶,既有的數(shù)據(jù)都將丟失易迹。一種不錯的做法是,學(xué)習(xí)如何在遷移數(shù)據(jù)庫的同時確保用戶數(shù)據(jù)的完整性平道。如果你確實想要一個全新的數(shù)據(jù)庫睹欲,可執(zhí)行命令python manage.py flush,這將重建數(shù)據(jù)庫結(jié)構(gòu)一屋。如果這樣做窘疮,就必須重新創(chuàng)建超級用戶,且原來的所有數(shù)據(jù)都將丟失冀墨。
19.3.3 只允許用戶訪問自己的主題
當(dāng)前闸衫,不管是以哪個用戶的身份登錄,都能看到所有的主題诽嘉。接下來我們使得它只向用戶顯示屬于自己的 主題蔚出。
在views.py中疫蔓,對函數(shù)topics()做修改
@login_required
def topics(request):
"""show all topics"""
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
用戶登錄后,request對象將由一個user屬性身冬,這個屬性存儲了有關(guān)該用戶的信息衅胀。代碼Topic.objects.filter(owner=request.user)讓Django只從數(shù)據(jù)庫中獲取owner屬性為當(dāng)前用戶的Topic對象。由于我們沒有修改主題的顯示方式酥筝,因此無需對頁面topics的模板做任何修改滚躯。
查看結(jié)果
19.3.4 保護(hù)用戶的主題
我們還沒有限制對顯示單個主題的頁面的訪問,因此任何已登錄的用戶都可輸入類似于http://localhost:8000/topics/1/的URL嘿歌,來訪問顯示相應(yīng)主題的頁面掸掏。如下圖
為修復(fù)這個問題,我們在視圖函數(shù)topic()獲取請求的條目前進(jìn)行檢查
# views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.urls import reverse
--snip--
@login_required
def topic(request, topic_id):
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--
服務(wù)器上沒有請求的資源時宙帝,標(biāo)準(zhǔn)的做法是返回404響應(yīng)丧凤。在這里,我們導(dǎo)入了異常Http404步脓,并在用戶請求它不能查看的主題時引發(fā)這個異常愿待。收到主題請求后,我們在渲染網(wǎng)頁前檢查該主題是否屬于當(dāng)前登錄的用戶靴患。
現(xiàn)在仍侥,我們再查看其他用戶的主題條目時,將看到Django發(fā)送的消息Page Not Found鸳君。
19.3.5 保護(hù)頁面edit_entry
頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / 农渊,其中 entry_id 是一個數(shù)字。下面來保護(hù)這個頁面或颊,禁止用戶通過輸入類似于前面的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:
raise Http404
if request.method != 'POST':
# 初次請求砸紊,使用當(dāng)前條目的內(nèi)容填充表單
--snip--
19.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 字段的值否淤。
添加如下代碼,將新主題關(guān)聯(lián)到當(dāng)前用戶:
@login_required
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 未提交數(shù)據(jù): 創(chuàng)建一個新表單
form = TopicForm()
else:
# POST提交的數(shù)據(jù)棠隐, 對數(shù)據(jù)進(jìn)行處理
form = TopicForm(request.POST)
if form.is_valid():
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)
我們首先調(diào)用form.save() 石抡,并傳遞實參commit=False ,這是因為我們先修改新主題助泽,再將其保存到數(shù)據(jù)庫中啰扛。接下來嚎京,將新主題的owner 屬性設(shè)置為當(dāng)前用戶。最后隐解,對剛定義的主題實例調(diào)用save() “暗郏現(xiàn)在主題包含所有必不可少的數(shù)據(jù),將被成功地保存煞茫。