Django Web应用程序(六)

19.3 让用户拥有自己的数据

这一节,我们将对一些页面进行限制,仅让已登录的用户访问它们,我们还将确保每个主题都属于特定用户。我们将创建一个系统,确保各项数据所属的用户,再限制对页面的访问,让用户只能使用自己的数据。
在本节中,我们将修改模型Topic,让每个主题都归属于特定用户。这也将影响条目,因为每个条目都属于特定的主题。我们先来限制对一些页面的访问

19.3.1 使用@login_required限制访问

Django提供了装饰器@login_required,让你能够轻松地实现这样的目标:对于某些页面,只允许已登录的用户访问它们。装饰器(decorator)是放在函数定义前面的指令,Python在函数运行前,根据它来修饰函数代码的行为。下面来看一个示例

1. 限制对topics页面的访问
每个主题都归特定用户所有,因此应只允许已登录的用户请求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':
        # 未提交数据: 创建一个新表单
        form = TopicForm()
    else:
        # POST提交的数据, 对数据进行处理
        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':
        # 未提交数据,创建一个空表单
        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)
    
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)

login_required()的代码检查用户是否已登录,仅当用户已登录时,Django才运行topics()的代码。
如果用户未登录,就重定向到登录页面。为实现这种重定向,我们需要修改settings.py,让Django知道到哪里去查找登录页面。在settings.py末尾添加如下代码:

# my settings
LOGIN_URL = '/users/login/'

现在,如果未登录的用户请求装饰器@login_required的保护页面,Django将重定向到settings.py中LOGIN_URL指定的URL。在这里单击Topics链接,未登录用户将重定向到登录页面。

2. 全面限制对项目“学习笔记”的访问
Django使我们能够轻松的限制对页面的访问,但你必须针对要保护哪些页面做出决定。最好先确定哪些页面不需要保护,再限制对其他所有页面的访问。你可以轻松地修改过于严格的访问限制,其风险比不限制对敏感页面的访问更低。
在项目“学习笔记中”,我们将不限制对主页、注册页面和注销页面的访问,并限制对其他所有页面的访问。
在下面的learning_logs/views.py中,对除index()外的每个视图都应用了装饰器@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 将数据关联到用户

现在,需要将数据关联到提交它们的用户。我们只需将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。例如,在项目“学习笔记”中,应用程序的最高层数据是主题,而所有条目都与特定主题相关联。只要每个主题都归属于特定用户,我们就能确定数据库中每个条目的所有者。
下面来修改模型Topic,在其中添加一个关联到用户的外键。这样做后,我们必须对数据库进行迁移。最后,我们必须对有些视图进行修改,使其只显示与当前登录的用户相关联的数据。
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--

我们先导入了django.contrib.auth中的模型User,然后在Topic中添加了字段owner,它建立到模型User的外键关系。

2. 确定当前有哪些用户
我们迁移数据库时,Django将对数据库进行修改,使其能够存储主题和用户之间的关联。为执行迁移,Django需要知道该将各个既有主题关联到哪些用户。最简单的办法是,将既有主题都关联到同一个用户,如超级用户。为此,我们需要知道该用户的ID。
下面来查看已创建的所有用户的ID。启用Django shell回话,并执行如下命令:

(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询问要将既有主题关联到哪个用户时,我们将指定其中的一个ID值。

3. 迁移数据库
知道用户ID后,就可以迁移数据库了。

(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

我们首先执行了命令makemigrations,在输出中,Django指出我们试图给既有模型Topic添加一个必不可少(不可为空)的字段,而该字段没有默认值。Django提供了两种选择:1. 现在提供默认值 2. 退出并在models.py中添加默认值。
为将所有既有主题都关联到管理用户ll_admin, 我输入了用户ID值为1。接下来,Django使用这个值来迁移数据库,并生成了迁移文件0003_topic_owner.py,它再模型Topic中添加字段owner。

现在可以执行迁移了。

(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

为验证迁移是否符合预期,可在shell会话中像下面这样做

>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
...   print(topic, topic.owner)
... 
Chess ll_admin
Rock Climbing ll_admin

注意:你可以重置数据库而不是迁移它,但如果这样做,既有的数据都将丢失。一种不错的做法是,学习如何在迁移数据库的同时确保用户数据的完整性。如果你确实想要一个全新的数据库,可执行命令python manage.py flush,这将重建数据库结构。如果这样做,就必须重新创建超级用户,且原来的所有数据都将丢失。

19.3.3 只允许用户访问自己的主题

当前,不管是以哪个用户的身份登录,都能看到所有的主题。接下来我们使得它只向用户显示属于自己的 主题。
在views.py中,对函数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属性,这个属性存储了有关该用户的信息。代码Topic.objects.filter(owner=request.user)让Django只从数据库中获取owner属性为当前用户的Topic对象。由于我们没有修改主题的显示方式,因此无需对页面topics的模板做任何修改。

查看结果

无主题关联的用户显示

19.3.4 保护用户的主题

我们还没有限制对显示单个主题的页面的访问,因此任何已登录的用户都可输入类似于http://localhost:8000/topics/1/的URL,来访问显示相应主题的页面。如下图

单个主题页面依然能访问

为修复这个问题,我们在视图函数topic()获取请求的条目前进行检查

# 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)
    # 确认请求的主题属于当前用户
    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--

服务器上没有请求的资源时,标准的做法是返回404响应。在这里,我们导入了异常Http404,并在用户请求它不能查看的主题时引发这个异常。收到主题请求后,我们在渲染网页前检查该主题是否属于当前登录的用户。
现在,我们再查看其他用户的主题条目时,将看到Django发送的消息Page Not Found。


404

19.3.5 保护页面edit_entry

页面edit_entry 的URL为http://localhost:8000/edit_entry/entry_id / ,其中 entry_id 是一个数字。下面来保护这个页面,禁止用户通过输入类似于前面的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':
# 初次请求,使用当前条目的内容填充表单
--snip--

19.3.6 将新主题关联到当前用户

当前,用户添加新主题的页面存在问题,因为它没有将新主题关联到特定用户。如果你尝试添加新主题,将看到错误消息IntegrityError ,指出learning_logs_topic.user_id 不能为NULL 。Django的意思是说,创建新主题时,你必须指定其owner 字段的值。
添加如下代码,将新主题关联到当前用户:

@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()
            return HttpResponseRedirect(reverse('learning_logs:topics'))
            
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

我们首先调用form.save() ,并传递实参commit=False ,这是因为我们先修改新主题,再将其保存到数据库中。接下来,将新主题的owner 属性设置为当前用户。最后,对刚定义的主题实例调用save() 。现在主题包含所有必不可少的数据,将被成功地保存。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容