前言
Windows下利用Django框架进行Web应用开发
Python三剑客第一本踩坑,学习笔记接上一节
如果寻找错误解决办法可以直接跳到总结查看错误类型再查看相应小节即可
一、普通用户输入
当下我们的网页只能让管理员添加数据,接下来我们来让普通用户能够输入数据
1.添加新主题
首先让用户能够添加新主题,创建一个基于表单的页面
1.1 表单模型
在learning_logs文件夹下创建forms.py文件,输入:
from django import forms
from .models import Topic
class TopicForm(forms.ModelForm):
class Meta:
model = Topic
fields = ['text']
labels = {'text': ''}
我们首先导入模块forms以及要使用的模型Topic,再定义一个TopicForm的类继承forms.ModelForm,最简单的ModelForm版本只包含一个内嵌的Meta类,它告诉Django根据哪个模型创建表单,以及表单中包含哪些字段。最后一行让Django不要为字段text添加标签。
1.2 新网页URl设定
在urls.py中添加新主题的网页
# 用于添加新主题的网页
path('topics/new_topic/', views.new_topic, name = 'new_topic'),
1.3 修改views.py
函数new_topic()需要处理两种情形:刚进入new_topic网页(在这种情况下应显示一个空表单);对提交的表单数据进行处理并将用户重定向到网页topics。添加代码:
from django.http import HttpResponseRedirect
from django.urls import reverse
from .forms import TopicForm
--snip--
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)
django2.0后版本把原来的 django.core.urlresolvers 包 更改为了 django.urls包
1.4 新建网页
在templates/learning_logs下创建new_topic.html,添加代码:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>
Add a new topic:
</p>
<form action="{% url 'learning_logs:new_topic' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">add topic</button>
</form>
{% endblock content %}
action告诉服务器将提交的表单数据发送到哪里,method让浏览器以POST请求的方式提交数据。
Django使用{% csrf_token %}来防止攻击者利用表单来获得对服务器未经授权的访问,然后我们显示表单并创建提交按钮。
1.5 topics链接
我们在打开页面topics,添加一个到new_topic的链接,添加代码:
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
全部保存,进入网页出现bug:ValueError: Field ‘id’ expected a number but got ‘new_topic’.
找到问题出在urls.py中,网页的网址不应该是topics/new_topic/而应该直接new_topic/,修改后访问成功
2.添加新条目
步骤与添加主题类似
2.1 条目模型
打开forms.py,添加如下代码:
from .models import Entry
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
labels = {'text': ''}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
小部件(widget)是一个HTML表单元素,我们订制了text的输入文本区域的宽度为80列(默认40列)使用户有足够的输入空间。
2.2 URL设定
打开urls.py,添加条目链接:
# 用于添加新条目的页面
path('new_entry/<topic_id>/', views.new_entry, name = 'new_entry'),
2.3 修改views.py
添加代码:
from .forms import EntryForm
--snip--
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)
2.4 新建网页
在templates/learning_logs下创建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 %}
我们在顶端显示主题让用户知道是在哪个主题下添加条目;该主题名也是链接可以返回到主题页面。
2.5 topic链接
我们在打开页面topic,添加一个到new_entry的链接,添加代码:
<p>
<a href="{% url 'learning_logs:new_entry' topic_id %}">add new entry</a>
</p>
全部保存,进入网页可以看到bug:Reverse for ‘new_entry’ with arguments ‘(’’,)’ not found. 1 pattern(s) tried: [‘new_entry/(?P<topic_id>[^/]+)/$’]
经过查询问题出在topic_id中,修改topic.html和new_entry.html中topic_id为topic.id即可(entry中没有topic_id参数)
再次进入网页可以看到添加条目功能
3.编辑条目
下面再创建一个页面让用户能够编辑现有条目
3.1 编辑条目URL
特定主题下编辑的条目ID,修改urls.py,添加代码:
# 用于编辑条目的页面
path('edit_entry/<entry_id>/', views.edit_entry, name = 'edit_entry'),
3.2 修改views.py
页面edit_entry收到GET请求时edit_entry()返回一个表单,让用户能够对其进行编辑,该页面收到POST请求后,再将修改后的文本保存到数据库中,添加代码:
from .models import Entry
--snip--
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)
instance=entry创建实例过后会使用现有数据填充表单,用户将看到既有数据并能够编辑它们。
3.3 新建网页
在templates/learning_logs下创建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">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">save changes</button>
</form>
{% endblock content %}
显示主题,并显示主题下要编辑的条目。
3.4 topic链接
我们在打开页面topic,添加一个到edit_entry的链接,添加代码:
<p>
<a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a>
</p>
全部保存,再次进入网页,单击edit entry
save changes后出现bug:name ‘topic_id’ is not defined
重新打开网页可以看到数据已修改成功,问题应该出在页面返回上
和2.5的问题类似,没有传递topic_id参数,把topic_id改为topic.id即可,测试已可重新返回
二、创建用户账户
在这一节我们将建立一个用户注册和身份验证的系统,让用户能够注册账户,进而登录和注销。
1.新建应用程序users
1.1 创建app
我们将创建一个新的应用程序users来完成一系列的操作。创建详细步骤可见第一讲。
(p_env)...\practice python manage.py startapp users
将该程序加入到settings.py我们的应用程序当中
1.2 修改urls.py
修改项目根目录的urls.py,将其包含我们将为应用程序users定义的URL,添加代码:
path('users/', include(('users.urls','users'), namespace = 'users')),
1.3 创建登录页面
在user目录下新建urls.py文件,添加代码:
"""为应用程序users定义URL模式"""
from django.urls import path, include
from django.contrib.auth.views import login
from . import views
urlpatterns = [
# 登录页面
path('login/', login, {'template_name': 'users/login.html'}, name = 'login'),
]
1.4 创建页面模板
在如下目录中新建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 'user:login' %}">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">log in</button>
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
实参value告诉Django在用户成功登录后将其重定向到什么地方——在这里是主页。
1.5 base中添加链接
下面在base.html中添加登录链接使所有页面都包含它,用户登录成功后隐藏,修改代码如下:
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a> -
{% if user.is_authenticated %}
Hello,{{ user.username}}.
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
进入页面看是否能够登录,网络服务错误,显示error:ImportError: cannot import name ‘login’ from ‘django.contrib.auth.views’ (C:\Users\DELL\Desktop\four\Python\Web\practice\p_env\lib\site-packages\django\contrib\auth\views.py)
打开urls.py,推测是Django更新换了包名称,百度之后果然如此,名称改为LoginView,代码修改如下
(有兴趣的朋友也可以把.views去掉来试试导入login包,亲测一样有效)
"""为应用程序users定义URL模式"""
from django.urls import path, include
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
# 登录页面
path('login/', LoginView.as_view(template_name= 'users/login.html'), name = 'login'),
]
再次进入页面,还是有bug:‘utf-8’ codec can’t decode byte 0xa3 in position 172: invalid start byte
定位后问题出再index.html中,重定位到base.html中,发现是Hello后面的逗号为中文逗号,将其改为英文逗号后可以正常访问(原代码已修改,因此直接复制不会出现此错误)
2.注销
我们不创建用户注销的页面,而让用户只需单击一个链接就能注销并返回主页。
2.1 注销URL
定义注销URL的模式,修改users/urls.py,添加代码如下:
# 注销页面
path('logout/', views.logout_view, name = 'logout'),
2.2 修改views.py
添加视图函数logout_view(),修改views.py,添加代码如下:
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout
def logout_view(request):
"""注销用户"""
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
2.3 链接视图
在base.html中添加注销链接,不登录则隐藏,添加代码:
<a href="{% url 'users:logout' %}">log out</a>
全部保存,进入页面查看,出现log out链接,单击
成功注销
登录,输入之前我们注册的管理员账号(见上一节)
出现bug,没有返回主页,好烦,找了半天错,发现在login.html中%}分隔开了,导致无法定向,把空格删掉就好了(原码已修改,可直接复制)
再次进入,输入错误密码,再输入正确密码,登录成功
3.注册
我们使用Django提供的表单UserCreationForm来让用户注册
3.1 定义URL
定义注册URL的模式,修改users/urls.py,添加代码如下:
# 注册页面
path('register/', views.register, name = 'register'),
3.2 修改views.py
添加视图函数register(),修改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
def logout_view(request):
"""注销用户"""
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
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.3 添加模板
添加注册模板register.html,保存到login.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 %}
3.4 链接视图
在base.html中添加注册链接,登录则隐藏,添加代码:
<a href="{% url 'users:register' %}">register</a> -
三、让用户有自己的数据
用户应该能够输入其专有的数据,因此我们将创建一个系统确定各项数据所属的用户,再限制对页面的访问让用户只能使用自己的数据。在本节中我们将修改模型Topic使每个主题都归于特定的用户。
1.限制对页面的访问
我们使用@login_required来限制用户的行为,对于某些页面只允许登录用户访问
1.1 限制对topics页面的访问
每个主题都归特定用户所有,因此应只允许登录的用户请求topics页面,打开learning_logs/views.py,添加如下代码:
from django.contrib.auth.decorators import login_required
--snip--
@login_required
只有用户已登录,Django才会运行topics()的代码,如果未登录就重定向到登录页面。
1.2 修改settings.py
在settings.py文件末尾添加:
# 我的设置
LOGIN_URL = '/users/login/'
进入页面,可以看到点击Topics弹出登录界面:
1.3 全面限制访问
在项目“学习笔记”中,我们不限制对主页,注册页面和注销页面的访问,限制对其他所有页面的访问,打开learning_logs/views.py,对除index()外的每个视图前都增加@login_required(感觉并无必要,因为一般不至于直接链接进主题和条目页面,但书上加了就加吧)
2.将数据关联到用户
现在我们将数据关联到提交它们的用户,我们只需将最高层的数据关联到用户,底层的数据将自动关联。例如在该项目中,应用程序的最高层数据是主题,而所有条目都与主题相关联,
2.1 修改模型Topic
打开models.py文件,建立Topic与用户的外键关系,添加如下代码并保存:
from django.contrib.auth.models import User
owner = models.ForeignKey(User)
2.2 确定用户
迁移数据库时,Django将对数据库进行更改,为执行迁移,Django需要知道该将各个既有主题关联到哪个用户。
启动shell会话来查看所有用户ID
(p_env) ···\practice>python manage.py shell
报错TypeError: init() missing 1 required positional argument: ‘on_delete’,Django2.0以上版本在定义外键时一定要加上on_delete=models.CASCADE否则会报错,修改models.py代码如下:
再次启动shell会话,可以看到我们只有一个管理员用户
2.3 迁移数据库
知道用户ID后我们就可以迁移数据库了
(p_env) ···\practice>python manage.py makemigrations learning_logs
(p_env) ···\practice>python manage.py migrate
数据迁移成功,生成迁移文件0003_topic_owner.py
再次启动shell会话,查看topic外键,可以看到主题都关联到了管理员用户
你也可以使用命令python manage.py flush,这将重置整个数据库,如果这样做就得重新设置用户并重新输入数据
3.只允许用户访问自己的主题
当前不管以哪个用户身份登录都能够看到所有的主题,这节我们实现只向用户显示自己的主题
3.1 修改views.py
打开views.py,修改函数topics()如下
3.2 保护用户主题
我们现在登录之后能够输入链接直接访问到任一主题,为修复该问题,在视图函数topic()获取请求条目前进行检查,添加代码:
from django.http import Http404
# 确认请求的主题属于当前用户
if topic.owner != request.user:
raise Http404
用户访问不属于自己的主题时将调起404页面,在后续我们会显示更合适的错误页面。
3.3 保护页面edit_entry
禁止用户通过直接输入URL来访问其他用户的条目,修改edit_entry,添加代码:
if topic.owner != request.user:
raise Http404
3.4 保护页面new_entry
禁止用户通过直接输入URL来访问其他用户的条目,修改new_entry,添加代码:
if topic.owner != request.user:
raise Http404
3.5 将新主题关联到当前用户
修改new_topic.py,如下:
if form.is_valid():
new_topic = form.save(commit = False)
new_topic.owner = request.user
new_topic.save()
总结错误
输入代码需要非常小心,很多时候是一些很小的错误造成的bug,需要仔细敲
总结一下错误:
一、
1.5 ValueError: Field ‘id’ expected a number but got ‘new_topic’.
2.5 Reverse for ‘new_entry’ with arguments ‘(’’,)’ not found. 1 pattern(s) tried: [‘new_entry/(?P<topic_id>[^/]+)/$’]
3.4 name ‘topic_id’ is not defined
二、
1.5 ‘utf-8’ codec can’t decode byte 0xa3 in position 172: invalid start byte
ImportError: cannot import name ‘login’ from ‘django.contrib.auth.views’
2.3 Page not found
三、
2.2 TypeError: init() missing 1 required positional argument: ‘on_delete’
注册这一块看书时候看漏了,补回去之后登陆页面发现bug
发现views.py处少给user打了个s。。。原代码已修改,试着注册结果又报错:NameError: name ‘authenticated_user’ is not defined,发现还是views处问题,authenticated_user多打了一个e,原码已修改。
修改后可以正常注册了,本节结束。