Django Web應(yīng)用程序(六)

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é)果

無主題關(guān)聯(lián)的用戶顯示

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鸳君。


404

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ù),將被成功地保存煞茫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帕涌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子续徽,更是在濱河造成了極大的恐慌蚓曼,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钦扭,死亡現(xiàn)場離奇詭異纫版,居然都是意外死亡,警方通過查閱死者的電腦和手機客情,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門其弊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裹匙,你說我怎么就攤上這事瑞凑∧┩海” “怎么了概页?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長练慕。 經(jīng)常有香客問我惰匙,道長,這世上最難降的妖魔是什么铃将? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任项鬼,我火速辦了婚禮,結(jié)果婚禮上劲阎,老公的妹妹穿的比我還像新娘绘盟。我一直安慰自己,他們只是感情好悯仙,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布龄毡。 她就那樣靜靜地躺著,像睡著了一般锡垄。 火紅的嫁衣襯著肌膚如雪沦零。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天货岭,我揣著相機與錄音路操,去河邊找鬼疾渴。 笑死,一個胖子當(dāng)著我的面吹牛屯仗,可吹牛的內(nèi)容都是我干的搞坝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼魁袜,長吁一口氣:“原來是場噩夢啊……” “哼瞄沙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起慌核,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤距境,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后垮卓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垫桂,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年粟按,在試婚紗的時候發(fā)現(xiàn)自己被綠了诬滩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡灭将,死狀恐怖疼鸟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庙曙,我是刑警寧澤空镜,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站捌朴,受9級特大地震影響吴攒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砂蔽,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一洼怔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧左驾,春花似錦镣隶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至稻爬,卻和暖如春嗜闻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桅锄。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工琉雳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留样眠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓翠肘,卻偏偏與公主長得像檐束,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子束倍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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