本文介绍 Django 的身份验证系统。 负责管理用户数据,并将实现身份验证的整个过程:注册、登录、注销、密码重置和密码更改,以及其它个人资料更新。
如何保护某些视图免受非授权用户的访问以及如何访问已登录用户的信息。
本文还是采用"城市景点"
的项目做进一步开发,在项目根路径下建立身份验证的应用,命令如下:
django-admin startapp accounts
项目结构现在应该是这样的:
scenic_spot/
| -- accounts/ <-- 新创建的django app!
| | -- migrations/
| | + -- __init__.py
| | -- __init__.py
| | -- admin.py
| | -- apps.py
| | -- models.py
| | -- tests.py
| | -- views.py
| + -- urls.py <-- 新加的文件!
| -- cityspot/
| | -- migrations/
| | + -- __init__.py
| | -- __init__.py
| | -- apps.py
| | -- models.py
| | -- forms.py
| | -- views.py
| + -- urls.py
| -- static/
| | -- css/
| | -- img/
| | -- js/
| -- templates/
| | -- includes/
| | + -- form.html
| | -- base.html
| | -- home.html
| | -- new_city.html
| | -- new_spot.html
| | -- city_spots.html
| -- scenic_spot.py
+ -- urls.py
下一步,将accounts
应用程序包含到 INSTALLED_APPS 的配置中:
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'widget_tweaks',
'accounts',
'cityspot',
],
...
从现在开始,我们将致力于accounts
应用程序。
需要编写一些代码开始行动!还要设计注册页面,由于Django并没有提供内置的注册视图,但提供了表单类UserCreationForm
,可以重用其快速实现设计。让我们从创建注册视图开始。 首先,在 urls.py
文件中创建一个新的映射路由:
scenic_spot/urls.py
from django.urls import path, include
from django.contrib import admin
urlpatterns = [
path('admin/', admin.site.urls),
path('cityspot/',include(('cityspot.urls','cityspot'),namespace='cityspot')),
path('accounts/',include(('accounts.urls','accounts'),namespace='accounts')),
]
scenic_spot/accounts/urls.py
from django.urls import re_path
from . import views as accounts_views
urlpatterns = [
re_path(r'^signup$', accounts_views.signup,name='signup'),
]
请注意如何以不同的方式从accounts
应用程序中导入视图模块:
from . import views as accounts_views
圆点.
表示urls文件与视图模块处于同一个当前路径中。在此提供别名,以避免发生视图名冲突。 可以稍后再改进 urls的设计。
现在编辑accounts
应用程序中的 views.py
, 并创建一个名为 signup 的视图函数:
from django.shortcuts import render,redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login as auth_login
def signup(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.save()
auth_login(request, user)
return redirect('cityspot:home')
else:
form = UserCreationForm()
return render(request, 'signup.html', {'form': form})
这里使用了Django 的一个名为 UserCreationForm
的内置表单。 并且使用了登录功能(重命名为 auth_login
以避免与内置登录视图冲突)。如果表单有效,则创建一个用户实例。 然后将创建的用户作为参数传递给 auth_login
函数,以验证用户。 之后,视图将用户重定向到主页,保持应用程序的流程。
创建名为 signup.html
的新模板:
{% extends 'base.html' %}
{% block title %}注册 - {{ block.super }} {% endblock %}
{% block body %}
<div class="container">
<h2>注册</h2>
<form method="post" novalidate>
{% csrf_token %}
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-success">创建一个帐户</button>
</form>
</div>
{% endblock %}
一切就绪了,可以开始工作了。启动服务器,在浏览器访问注册页面:
自动化测试(单元测试),是用来检查代码正确性的一些简单的程序。有些测试只关注某个很小的细节(某个模型的某个方法的返回值是否满足预期?),而另一些测试可能检查对某个软件的一系列操作(某一用户输入序列是否造成了预期的结果?)。自动化测试
是由测试程序帮你自动完成的。当你创建好了一系列测试,每次修改应用代码后,就可以自动检查出修改后的代码是否还像你曾经预期的那样正常工作。而不需要花费大量时间来进行手动测试。
开发者遵循 "测试驱动" 的开发原则,在写代码之前先写测试。对于一个用例,先描述其功能,然后写代码来解决它。「测试驱动」的开发方法只是将用例的描述抽象为 Python 的测试用例。
更普遍的情况是,一个新手(初始学习者)更倾向于先写代码,然后再写测试。虽然提前写测试可能更好,但是晚点写起码也比没有强。
有时候很难决定从哪里开始下手写测试。如果你才写了几千行 Python 代码,选择从哪里开始写测试确实不怎么简单。如果是这种情况,那么在你下次修改代码(比如加新功能,或者修复 Bug)之前写个测试是比较合理且有效的。
对于大项目,对每个单元进行测试可以快速定位错误,确保项目质量,Django单元测试使用Unittest
测试框架,在 Django 中编写测试的首选方式是使用 Python 标准库中内置的 unittest 模块。先构建 unittest.TestCase
的子类django.test.TestCase
。
下面的注册功能测试案例,它是 django.test.TestCase 的子类,同时父类也是 unittest.TestCase 的子类,在事务内部运行每个测试以提供隔离。
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from django.urls import resolve
from .views import signup
from django.contrib.auth.forms import UserCreationForm
class SignUpTests(TestCase):
def setUp(self):
url = reverse('accounts:signup')
self.response = self.client.get(url)
def test_signup_status_code(self):
self.assertEquals(self.response.status_code, 200)
def test_signup_url_resolves_signup_view(self):
view = resolve('/accounts/signup')
self.assertEquals(view.func, signup)
def test_csrf(self):
self.assertContains(self.response, 'csrfmiddlewaretoken')
def test_contains_form(self):
form = self.response.context.get('form')
self.assertIsInstance(form, UserCreationForm)
在 setUp
方法中,我们准备了运行测试的环境,从而模拟一个场景,并将响应对象移动到那里。因为在Django 测试套件中不会针对当前数据库运行您的测试。 为了运行测试,Django 动态创建一个新数据库,应用所有模型迁移,运行测试,完成后销毁测试数据库。
现在我们要测试一个成功的注册。 这一次,让我们创建一个新类来更好地组织测试:
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from django.urls import resolve
from .views import signup
from django.contrib.auth.forms import UserCreationForm
class SignUpTests(TestCase):
# 代码被隐藏...
class SuccessfulSignUpTests(TestCase):
def setUp(self):
url = reverse('accounts:signup')
data = {
'username': 'zcj',
'password1': 'zcj123456',
'password2': 'zcj123456'
}
self.response = self.client.post(url, data)
self.home_url = reverse('cityspot:home')
def test_redirection(self):
'''
有效的表单提交应将用户重定向到主页
'''
self.assertRedirects(self.response, self.home_url)
def test_user_creation(self):
self.assertTrue(User.objects.exists())
def test_user_authentication(self):
'''
创建对任意页面的新请求。 成功注册后,生成的响应现在应该在其上下文中包含一个“用户”。
'''
response = self.client.get(self.home_url)
user = response.context.get('user')
self.assertTrue(user.is_authenticated)
使用类似的方案,创建一个新类,用于在数据无效时进行注册测试:
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from django.urls import resolve
from .views import signup
from django.contrib.auth.forms import UserCreationForm
class SignUpTests(TestCase):
# 代码被隐藏...
class SuccessfulSignUpTests(TestCase):
# 代码被隐藏...
class InvalidSignUpTests(TestCase):
def setUp(self):
url = reverse('accounts:signup')
self.response = self.client.post(url, {}) # 提交空字典
def test_signup_status_code(self):
'''
无效的表单提交应返回同一页面
'''
self.assertEquals(self.response.status_code, 200)
def test_form_errors(self):
form = self.response.context.get('form')
self.assertTrue(form.errors)
def test_dont_create_user(self):
self.assertFalse(User.objects.exists())
执行测试:
python scenic_spot.py test
测试结果如下,OK
表示全部通过测试。
Found 10 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 1.302s
OK
Destroying test database for alias 'default'...
UserCreationForm
不提供电子邮件字段,但我们可以扩展其设计。在 accounts 文件夹中创建一个名为 forms.py 的文件:
accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.utils.html import format_html
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.validators import UnicodeUsernameValidator
class SignUpForm(UserCreationForm):
username = forms.CharField(
label=_('用户名'),
max_length=150,
required=True,
help_text=_('必填项。 最多150个字符。 仅限字母、数字和@/./+/-/_。'),
validators=[UnicodeUsernameValidator()],
error_messages={'unique': _("具有该用户名的用户已存在。")},
widget=forms.TextInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(
label=_('电子邮件'),
max_length=254,
required=True,
help_text='必填项。 提供有效的电子邮件地址.',
widget=forms.EmailInput(attrs={'class': 'form-control'}))
password1 = forms.CharField(
label=_('密码'),
widget=(forms.PasswordInput(attrs={'class': 'form-control'})),
help_text= format_html(
'<ul><li>您的密码不能与您的其他个人信息过于相似。</li>'
+'<li>您的密码必须至少包含 8 个字符。</li><li>您的密码不能是常用密码。</li>'
+'<li>您的密码不能完全是数字。</li></ul>'))
password2 = forms.CharField(
label=_('确认密码'),
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
help_text=_('只需输入相同的密码,进行确认'))
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2',)
现在,我们不使用 views.py 中的 UserCreationForm,而是导入新表单 SignUpForm 并使用它:
accounts/views.py
from django.shortcuts import render,redirect
from django.contrib.auth import login as auth_login
from .forms import SignUpForm
def signup(request):
if request.method == 'POST':
form = SignUpForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.save()
auth_login(request, user)
return redirect('cityspot:home')
else:
form = SignUpForm()
return render(request, 'signup.html', {'form': form})
只是这个小小的改变,一切都已经照常开始工作了:
请记住:
每次再小的更改必须要进行测试, 此时将测试用例更改为使用 SignUpForm 而不是 UserCreationForm:
from .forms import SignUpForm
class SignUpTests(TestCase):
# 代码被隐藏 ...
def test_contains_form(self):
form = self.response.context.get('form')
self.assertIsInstance(form, SignUpForm)
class SuccessfulSignUpTests(TestCase):
def setUp(self):
url = reverse('accounts:signup')
data = {
'username': 'zcj',
'email': '3261524748@qq.com',
'password1': 'zcj123456',
'password2': 'zcj123456'
}
self.response = self.client.post(url, data)
self.home_url = reverse('cityspot:home')
前面的测试用例仍然可以通过,因为由于 SignUpForm 扩展了 UserCreationForm,它是 UserCreationForm 的一个实例。
现在添加了一个新的表单域:
fields = ('username', 'email', 'password1', 'password2')
将 SignUpForm
表单重新用于其他项目,并为其添加一些额外的字段。然后这些新字段也将显示在 signup.html
模板中,这种变化可能会因为需求而不同,这种变化需要创建新的测试来验证。
accounts/tests.py
class SignUpTests(TestCase):
# 代码被隐藏 ...
def test_form_inputs(self):
'''
视图必须包含五个input标记元素:csrf、用户名、电子邮件、
密码1,密码2
'''
self.assertContains(self.response, '<input', 5)
self.assertContains(self.response, 'type="text"', 1)
self.assertContains(self.response, 'type="email"', 1)
self.assertContains(self.response, 'type="password"', 2)
博文最后更新时间: