文章目录
Web 应用程序的核心是让任何用户都能够注册账户并且能够使用它,不论用户身在何方。
本篇中,将通过创建一些数据库表单,使得用户能够添加主题和条目、编辑已有条目,并且防范基于表单的网页发起的常见攻击。然后,将实现一个用户身份验证系统,创建一个注册页面,方便用户创建账户,并且让有些页面仅仅提供已登录的用户访问。(确保用户数据安全)
项目前身参考 构建简单的Django项目
让用户输入数据
建立用于创建用户账户的身份验证系统前,首先需要添加几个页面以方便用户输入数据。目前,仅有超级用户能够管理网站输入数据,但是不能让用户去使用管理网站,所以需要使用 Django 的表单创建工具来创建让用户能够输入数据的页面。
添加新主题
首先让用户能够添加新主题。创建基于表单的页面方法与之前新建网页的方式一样:定义 URL、编写视图函数、编写对应模版。(这里需要导入包含表单的模块 forms.py
)
- 用于添加主题的表单
让用户输入并提交信息的页面都是表单(即使看起来不像)。用户输入信息时,需要进行验证,确认提供的信息是正确的数据类型,并且不是恶意的信息,如中断服务器的代码。然后,我们再对这些有效信息进行处理,并将其保存到数据库的合适地方。(这些工作都是由 Django 自动完成的)
在 Django 中,创建表单的最简单的方式就是使用ModelForm
,它根据项目中定义的模型信息自动创建表单。创建一个名为forms.py
文件,将其放置到models.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
。
在 TopicForm
类中仅仅包含一个内嵌的 Meta
类,告诉 Django 根据哪个模型创建表单,以及在表单中包含哪些字段。在这里的表单中只包含字段 text
,并且让 Django 不要为字段 text
生成对应的标签。
- URL模式
new_topic
新建的网页的 URL 应该尖端且具有描述性,所以当用户需要添加新主题时,将切换到http://localhost:8000/new_topic/
这个 URL 下。在urls.py
中添加:
'''定义 learning_logs 的 URL 模式'''
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
#主页
#url(r'^$', views.index, name='index'),
path('', views.index, name='index'),
path(r'^topics/$', views.topics, name='topics'),
path(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'),
#用于添加新主题的网页
path(r'^new_topic/$', views.new_topic, name='new_tipic'),
]
- 添加视图函数
new_topic()
函数new_topic()
需要处理两种情况:- 刚刚进入
new_topic
网页,这时候应该单纯显示一个空表单。 - 对提交的表单数据进行处理,并使用户重定向到网页
topics
中。
- 刚刚进入
修改 views.py
中的内容
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from .models import Topic
from .forms import TopicForm
# Create your views here.
def index(request):
'''学习笔记的主页'''
return render(request, 'learning_logs/index.html')
def topics(request):
'''显示所有的主题'''
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)
这里使用 HttpResponseRedirect
类将用户重定向到网页 topics
中。
-
GET请求和POST请求
创建 Web 应用程序时,将用到两种主要请求类型:GET 请求、POST 请求。对于只是从服务器读取数据的页面,只需要用到 GET 请求;在用户需要通过表单提交信息时,通常用到 POST 请求。处理所有表单时,都将指定 POST 方法。(此外还有其他类型的请求方式,这里先不说)
函数new_topic()
将请求对象作为参数。用户初次请求该网页时,浏览器将发送 GET 请求,而当用户填写完信息提交表单时,浏览器会发送 POST 请求。根据请求的类型,我们可以确定用户请求的是空表单还是填写好的表单数据。 -
模版
new_topic
下面创建新模版文件new_topic.html
存放到base.html
的同级目录下。
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
<form action="{% url 'new_topic' %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit">add topic</button>
</form>
{% endblock content %}
这个模版基础了 base.html
内容。这里顶一个一个 HTML
表单。实参 action
告诉服务器将要提交的表单数据发送到哪里,这里将数据发回到视图函数 new_topic()
。实参 method
让浏览器以 POST
方式请求提交数据。
Django 使用模版标签 {% csrf_token %}
来防止攻击者利用表单来获取服务器未经授权的访问。后面,显示表单,从中能够知道 Django 使得完成显示表单等任务优化地更简单了:我们只需要包含模版变量 {{ form.as_p }}
,就可以让 Django 自动创建显示表单所需要的全部字段。修饰符 as_p
让 Django 以段落格式渲染所有表单元素,这是一种整洁地显示表单的简单方式。
Django 不会自动为表单创建提交按钮,因此后面又创建了提交按钮 <button name="submit">add topic</button>
。
- 链接到页面
new_topic
接下来,将新建的页面链接到页面topics
中,修改topics.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
{% for topic in topics %}
<a href="{% url 'topic' topic.id %}">{{ topic }}</a>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
<a href="{% url 'new_topic' %}">Add a new topic:</a>
{% endblock content %}
完成后,运行服务,访问页面:
添加新条目
现在用户可以添加新主题了,但是如果还想要添加新条目应该怎么办呢?所以这里将再次定义 URL,编写视图函数和模版链接到添加新条目的页面。但在此之前,还需要在 forms.py
中添加一个类
- 用于添加新条目的表单
创建一个与模型Entry
相关联的表单,但是这个表单的定制程度要比TopicForm
要高级一些:
forms.py
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
class Meta:
model = Topic
fields = {'text'}
labels = {'text':''}
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = {'text'}
labels = {'text':''}
widgets = {'text': forms.Textarea(attrs={'cols':80})}
这里导入了 Entry
类,并且创建了新的 EntryForm
表单,定义了 widgets
属性,widget
是一个 HTML
表单元素(单行文本框、多行文本区域、下拉列表等)。通过设置属性 widgets
,可以覆盖 Django 选择的默认小部件。通过让 Django 使用 forms.Textarea
,定制了字段 ‘text’ 的输入小部件,将文本区域的宽度设置为 80 ,给用户提供了足够的空间用来编写有意义的条目。
- URL模式 new_entry
在urls.py
中添加新的 URLnew_entry
,如下:
'''定义 learning_logs 的 URL 模式'''
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
#主页
#url(r'^$', views.index, name='index'),
path('', views.index, name='index'),
path(r'^topics/$', views.topics, name='topics'),
path(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'),
#用于添加新主题的网页
path(r'^new_topic/$', views.new_topic, name='new_topic'),
#用于添加新的条目的网页
path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
]
- 视图函数
new_entry()
同之前方式类似,创建new_entry()
视图函数在views.py
中:
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)
- 创建模版
new_entry.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'topic' topic_id %}">{{ topic }}</a></p>
<p>Add a new entry:</p>
<form action="{% url 'new_entry' topic_id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit">add entry</button>
</form>
{% endblock content %}
同时修改 topic.html
:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<p>Entries:</p>
<p>
<a href="{% url 'new_entry' topic_id %}">add new entry</a>
</p>
<ul>
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}</p>
</li>
{% empty %}
<li>
There are no entries for this topic yet.
</li>
{% endfor %}
</ul>
{% endblock content %}
运行:
编辑目录
下面创建一个页面,使得用户能够编辑既有的条目。
- URL 模式
edit_entry
这个页面的 URL 需要传递要编辑的条目的 ID。修改后的learning_logs/urls.py
如下:
'''定义 learning_logs 的 URL 模式'''
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
#主页
#url(r'^$', views.index, name='index'),
path('', views.index, name='index'),
path(r'^topics/$', views.topics, name='topics'),
path(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'),
#用于添加新主题的网页
path(r'^new_topic/$', views.new_topic, name='new_topic'),
#用于添加新的条目的网页
path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
#用于编辑条目的页面
path(r'^edit_entry/(?P<entry_id>\d+)/$', views.edit_entry, name='edit_entry'),
]
在 URL (http://localhost:8000/edit_entry/1/
)中传递的 ID 存储在形参 entry_id
中。
- 视图函数
edit_entry()
在views.py
中添加视图函数edit_entry()
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)
- 模版
edit_entry.html
新建edit_entry.html
文件:
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'topic' topic.id %}">{{ topic }}</a></p>
<p>Add a new entry:</p>
<form action="{% url 'edit_entry' entry.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit">add entry</button>
</form>
{% endblock content %}
- 链接到页面
edit_entry
现在,在显示特定主题的页面中,添加edit_entry
的链接:
topic.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<p>Entries:</p>
<p>
<a href="{% url 'new_entry' topic.id %}">add new entry</a>
</p>
<ul>
{% 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 'edit_entry' entry.id %}">edit entry</a>
</p>
</li>
{% empty %}
<li>
There are no entries for this topic yet.
</li>
{% endfor %}
</ul>
{% endblock content %}
- 运行:
创建用户账户
接下来,就需要建立一个用户注册和身份验证系统,使得用户能够注册账户并且登录和注销。这里将针对这一功能模块创建新的应用程序,其中包含与处理用户账户相关的所有功能,同时还需要对模型 Topic
进行修改,使得每个主题归属于特定的用户。
创建应用程序 users
首先使用命令 startapp
创建一个名为 users
的应用程序:
<...\Scripts> py.exe .\manage.py startapp users
- 将应用程序
users
添加到settings.py
中
在settings.py
中,需要将这个新的应用添加进来。如下所示:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 自定义应用
'learning_logs',
# 账户管理应用
'users',
]
- 包含应用程序
users
的 URL
在项目根目录中的urls.py
中添加匹配 URL:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path(r'admin/', admin.site.urls),
path(r'users/', include(('users.urls', 'users'), namespace='users')),
path(r'', include(('learning_logs.urls', 'learning_logs'), namespace='learning_logs')),
]
登录页面
首先需要实现登录页面的功能。为此,将使用 Django 提供的默认登录视图,所以这里的 URL 模式会稍微不一样。在目录 users/
中,新建一个名为 urls.py
的文件,其中添加如下内容:
'''为应用 users 定义 URL 模式'''
from django.conf.urls import url
from django.urls import path
from django.contrib.auth import login
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
#主页
#url(r'^$', views.index, name='index'),
#登陆页面
#path(r'^login/$', login, {'template_name':'users/login.html'}, name='login'),
url(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login'),
]
这里使用 login
告诉 Django 使用默认视图 login
,并且传递了一个字典告诉 Django 去哪里能够查找我们编写的模版 users/login.html
。
- 模版
login.html
用户请求登录页面时,Django 将使用默认视图login
,但是我们依然需要为这个页面提供模版,所以在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' %}" />
</form>
{% endblock content %}
- 链接到登陆页面
下面在base.html
中添加到登录页面的链接,让所有页面都包含它。用户已登录时,不需要显示这个链接,所以需要将它嵌套到一个{% if %}
标签中。
<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 %}
在 Django 身份验证系统中,每个模版都可以使用变量 user
,这个变量有一个 is_authenticated
属性:如果用户已登录,该属性为 True
,否则为 False
.
在这里,我们向已登录的用户显示一条问候语。对于已投过身份验证的用户,还设置了属性 username
,我们使用这个属性来个性化问候语,让用户知道已登录了,而未登录用户则显示一个登陆页面的链接。
- 使用登陆页面
前面建立了一个用户账户,下面来登陆一下,看看登陆页面是否管用,访问http://localhost:8000/admin/
,如果依然是以管理员身份登陆,请在最上面找到注销链接并点击它。
注销
现在需要提供一个让用户注销的途径。我们不创建用于注销的页面,而让用户只需要单击一个链接就能够注销并返回主页,为此,将为注销链接定义一个 URL 模式,编写一个视图函数,并在 base.html
中添加一共注销链接。
- 注销 URL
下面的代码为注销定义了 URL 模式,该模式与 URLhttp://localhost:8000/users/login
匹配,修改后的user/urls.py
如下:
'''为应用 users 定义 URL 模式'''
from django.conf.urls import url
from django.urls import path
from django.contrib.auth import login
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
#主页
#url(r'^$', views.index, name='index'),
#登陆页面
#path(r'^login/$', login, {'template_name':'users/login.html'}, name='login'),
url(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login'),
url(r'^logout/$', views.logout_view, name='logout'),
]
- 视图函数
logout_view()
函数logout_view()
很简单:只是导入 Django 函数logout_view()
,并调用它,再重定向到主页。打开users/views.py
添加如下代码:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.contrib.auth import logout
# Create your views here.
def logout_view(request):
'''注销用户'''
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index')
- 链接到注销视图
现在回到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 }}.
<a href="{% url 'users:logout' %}">log out</a>
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
- 完成运行:
注册页面
下面就需要创建一个让新用户能够注册的页面。接下来将使用 Django 提供的表单 UserCreationForm
,并且需要编写自己的视图函数和模版。
- 注册页面的 URL 模式
在users/urls.py
中添加定义了注册页面的 URL 模式,如下:
'''为应用 users 定义 URL 模式'''
from django.conf.urls import url
from django.urls import path
from django.contrib.auth import login
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
#登陆页面
url(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login'),
#注销页面
url(r'^logout/$', views.logout_view, name='logout'),
#注册页面
url(r'^register/$', views.register, name='register'),
]
这个添加的模式与 URL http://localhost:8000/users/register/
匹配。
- 视图函数
register()
在users/views.py
中添加对应的视图函数register()
,如下:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.forms import UserCreationForm
# Create your views here.
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 = authenticated(username=new_user.username, password=request.POST['password'])
login(request, authenticated_user)
return HttpResponseRedirect(reverse('learning_logs:index'))
context = {'form':form}
return render(request, 'users/register.html', context)
- 注册模版
对应就需要在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 %}
- 链接到注册页面
接下来就需要在用户没有登陆时提供新用户注册的链接,所以在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 }}.
<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 %}
</p>
{% block content %}{% endblock content %}
- 运行
让用户拥有自己的数据
用户应该能够输入其专用的数据,所以需要创建一个系统,确定各项数据所属的用户,然后限制对页面的访问,让用户只能使用自己的数据。在这里,将修改模型 Topic
,使得每个主题都归属于特定用户。这也将影响条目,所以每个条目都属于特定的主题,因此,需要先限制一些页面的访问。
使用 @login_required
限制访问
Django 提供了装饰器 @login_required
,能够轻松实现这样的目的:对于某些页面,只允许已登录的用户访问它们。
装饰器(decorator
)是放在函数定义前面的指令,Python 在函数运行钱,根据它来修改函数代码的行为。
- 限制对
topics
页面的访问
每个主题都归特定用户所有,因此应只允许已登录的用户请求topics
页面。为此,在learning_logs/views.py
中添加如下代码:
from django.contrib.auth.decorators import login_required
...
@login_required
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.order_by("date_added")
context = {'topics':topics}
return render(request, 'learning_logs/topics.html', context)
...
首先导入函数 login_required()
。将该函数作为装饰器用于视图函数 topics()
中。在 login_required
前面加上 @
符号后放置到函数声明前面,会使得 Python 在运行 topics()
的代码前先运行 login_required()
的代码。而 login_required()
的代码是用于检查用户是否处于登录状态的。
login_required()
将检查用户是否已登录,仅当用户已登录时,Django 才运行 topics()
的代码。如果用户未登录,则会重定向到登录页面。
为了实现这种重定向,我们需要修改 settings.py
,让 Django 知道到哪里去查找登录页面。所以可以在 settings.py
末尾添加如下:
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
LOGIN_URL = '/users/login/'
- 全面限制对项目 “学习笔记” 的访问
Django 使得能够轻松地限制对页面的访问,但是必须针对要保护哪些页面做出决定。最好先确定项目的哪些页面不需要保护,再限制对其他所有页面的访问。这样可以轻松修改过于严格的访问限制,其风险比不限制对敏感页面的访问更低。
在项目“学习笔记”中,不需要限制访问的页面有:主页、注册页面、注销页面、
所以在下面的 learning_logs/views.py
中,对除了 index()
外的每个视图都应用了装饰器 @login_required
.
@login_required
def topics(request):
...
@login_required
def topic(request, topic_id):
...
@login_required
def new_topic(request):
...
@login_required
def new_entry(request, topic_id):
...
@login_required
def edit_entry(request, entry_id):
...
将数据关联到用户
现在,需要将数据关联到提交它们的用户。我们只需要将最高层的数据关联到用户,这样更底层的数据将自动关联到用户。所以,在项目“学习笔记”中,应用程序的最高层数据是主题,而所有条目都与特定主题相关联。只要每个主题都归属于特定用户,我们能够确定数据库中每个条目的所有者。
下面来修改模型 Topic
,在其中添加一个关联到用户的外键。这样修改后,我们需要修改数据库并进行迁移,并且还需要使用视图进行修改,使其只显示与当前登录的用户相关联的数据。
- 修改模型
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):
'''学到的有关主题的具体知识'''
topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
text = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = 'entries'
def __str__(self):
'''返回模型的字符串表示'''
return self.text[:50] + "..."
首先,导入 django.contrib.auth.models
中的模型 User
,然后在 Topic
中添加字段 owner
,建立到模型 User
的外键关系。
- 确定当前有哪些用户
需要迁移数据库,Django 将对数据库进行修改,使得其能够存储主题和用户之间的关联。为执行迁移,Django 需要知道该将各个既有主题关联到哪个用户。最简单的办法就是将既有主题都关联到同一用户(如:超级用户)。为此,需要知道该用户的 ID。
查看已创建的所有用户 ID,启动 Django shell ,如下:
<...>py.exe .\manage.py shell
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: 11_admin>, <User: new_user>, <User: tt_user>, <User: zy_user>]>
>>> for user in User.objects.all():
... print(user.username, user.id)
...
11_admin 1
new_user 2
tt_user 3
zy_user 4
>>>
这里,我们遍历打印了 User模型中的用户名和ID,其中得到4个用户,其中 11_admin 1 是超级用户。
- 迁移数据库
当知道用户 ID 后,就可以执行操作迁移数据库了:
<.../Scripts> py.exe .\manage.py makemigrations learning_logs
System check identified some issues:
WARNINGS:
?: (2_0.W001) Your URL pattern '^edit_entry/(?P<entry_id>\d+)/$' [name='edit_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_entry/(?P<topic_id>\d+)/$' [name='new_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_topic/$' [name='new_topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/$' [name='topics'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/(?P<topic_id>\d+)/$' [name='topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
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
现在可以执行迁移,如下:
<...\Scripts> py.exe .\manage.py migrate
System check identified some issues:
WARNINGS:
?: (2_0.W001) Your URL pattern '^edit_entry/(?P<entry_id>\d+)/$' [name='edit_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_entry/(?P<topic_id>\d+)/$' [name='new_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_topic/$' [name='new_topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/$' [name='topics'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/(?P<topic_id>\d+)/$' [name='topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
Operations to perform:
Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
Applying learning_logs.0003_topic_owner... OK
Django 应用新的迁移,结果一切顺利。为了验证迁移符合预期,可以使用 Django shell 打印如下输入:
<...\Scripts> py.exe .\manage.py shell
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess 11_admin
Rock Climbing 11_admin
Sports 11_admin
Sports 11_admin
从 learning_logs.models
中导入 Topic
,再遍历所有的既有主题,并打印每个主题及其所属的用户。所以之前的所有主题现在都属于了 11_admin
这个用户了。
只允许用户访问自己的主题
当前,不管你以哪个用户的身份登陆,都能够看到所有的主题。现在就来改变这样的情况,只允许用户访问并显示属于自己的主题。
在 views.py
中,对函数 topics()
做如下修改:
@login_required
def topics(request):
'''显示所有的主题'''
#topics = Topic.objects.order_by("date_added")
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics':topics}
return render(request, 'learning_logs/topics.html', context)
保护用户的主题
我们还没有限制对显示单个主题的页面的访问,因此,目前所有的已登录的用户都可以输入类似于 http://localhost:8000/topics/1/
的 URL ,来访问显示相应主题的页面。
为了修复这个问题,在视图函数 topic()
获取请求的条目前执行检查:
from django.http import HttpResponseRedirect, Http404
@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)
服务器上没用请求的资源时,标准的做法就是返回 404 响应。所以,在这里,如果当前用户不是这个主题的所属用户,则返回404页面。
保护页面 edit_entry
页面 edit_entry
的 URL 为 http://localhost:8000/edit_entry/entry_id/
,其中 entry_id
是一个数字,下面需要来保护这个页面,禁止用户通过输入类似前面的特定 URL 访问其他用户的条目:
@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':
'''初次请求,使用当前条目填充表单'''
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)
将新主题关联到当前用户
当前,用于添加新主题的页面是存在问题的,因为新创建的新主题是无法关联到自己的。所以需要修改 new_topic()
视图函数:
@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()
#form.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()
。修改后,这个问题就得到了解决。
最终运行效果
<...\Scripts> py.exe .\manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified some issues:
WARNINGS:
?: (2_0.W001) Your URL pattern '^edit_entry/(?P<entry_id>\d+)/$' [name='edit_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_entry/(?P<topic_id>\d+)/$' [name='new_entry'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^new_topic/$' [name='new_topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/$' [name='topics'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
?: (2_0.W001) Your URL pattern '^topics/(?P<topic_id>\d+)/$' [name='topic'] has a route that contains '(?P<', begins with a '^', or ends with a '$'. This was likely an oversight when migrating to django.urls.path().
System check identified 5 issues (0 silenced).
December 03, 2020 - 22:48:58
Django version 3.1.3, using settings 'learning_log.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
小结
通过本篇内容的记录和学习,现在总结一下本篇的主要内容:
- 使用 Django 管理用户账户
- 简历简单用户身份验证和账户注册系统
- Django URL 模式创建自己的Web应用程序