测试开发之Django实战示例 第四章 创建社交网站

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

第四章 创建社交网站

在之前的章节学习了如何创建站点地图、订阅信息和创建一个全文搜索引擎。这一章我们来开发一个社交网站。会创建用户登录、登出、修改和重置密码功能为用户创建额外的用户信息以及使用第三方身份认证登录。

本章包含以下内容

  • 使用Django内置验证模块

  • 创建用户注册视图

  • 使用自定义的用户信息表扩展用户模型

  • 添加第三方身份认证系统

我们来创建本书的第二个项目。

1社交网站

我们将创建一个社交网站让用户可以把网上看到的图片分享到网站来。这个社交网站包含如下功能

  • 一个供用户注册、登录、登出、修改和重置密码的用户身份验证系统还能够让用户自行填写用户信息

  • 关注系统让用户可以关注其他用户

  • 一个JS小书签工具让用户可以将外部的图片分享(上传到本站

  • 一个追踪系统让用户可以看到他所关注的用户的上传内容

本章涉及到其中的第一个内容用户身份验证系统。

1.1启动社交网站项目

启动系统命令行输入下列命令创建并激活一个虚拟环境

Copymkdirenv
virtualenv env/bookmarks
sourceenv/bookmarks/bin/activate

终端会显示当前的虚拟环境如下

Copy(bookmarks)laptop:~ zenx$

在终端中安装Django并启动bookmarks项目

Copypip install Django==2.0.5
django-admin startproject bookmarks

然后到项目根目录内创建account应用

Copycd bookmarks/
django-admin startapp account

然后在settings.py中的INSTALLED_APPS设置中激活该应用

CopyINSTALLED_APPS = [
    'account.apps.AccountConfig',
    # ...
]

这里将我们的应用放在应用列表的最前边原因是我们稍后会为自己的应用编写验证系统的模板Django内置的验证系统自带了一套模板如此设置可以让我们的模板覆盖其他应用中的模板设置。Django按照INSTALLED_APPS中的顺序寻找模板。

之后执行数据迁移过程。

译者注新创建的Django项目默认依然使用Python的SQLlite数据库建议读者为每个项目配置一个新创建的数据库。推荐使用上一章的PostgreSQL因为本书之后还会使用PostgreSQL。

2使用Django内置验证框架

django提供了一个验证模块框架具备用户验证会话控制(session权限和用户组功能并且自带一组视图用于控制常见的用户行为如登录、登出、修改和重置密码。

验证模块框架位于django.contrib.auth也被其他Django的contrib库所使用。在第一章里创建超级用户的时候就使用到了验证模块。

使用startproject命令创建一个新项目时验证模块默认已经被设置并启用包括INSTALLED_APPS设置中的django.contrib.auth应用和MIDDLEWARE设置中的如下两个中间件

  • AuthenticationMiddleware将用户与HTTP请求联系起来

  • SessionMiddleware处理当前HTTP请求的session

中间件是一个类在接收HTTP请求和发送HTTP响应的阶段被调用在本书的部分内容中会使用中间件第十三章上线中会学习开发自定义中间件。

验证模块还包括如下数据模型

  • User一个用户数数据表包含如下主要字段usernamepasswordemailfirst_namelast_name和is_active。

  • Group一个用户组表格

  • Permission存放用户和组的权限清单

验证框架还包括默认的验证视图以及对应表单稍后会使用到。

2.1创建登录视图

从这节开始使用Django的验证模块一个登录视图需要如下功能

  • 通过用户提交的表单获取用户名和密码

  • 将用户名和密码与数据库中的数据进行匹配

  • 检查用户是否处于活动状态

  • 通过在HTTP请求上附加session让用户进入登录状态

首先需要创建一个登录表单在account应用内创建forms.py文件添加以下内容

Copyfrom django import forms

classLoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)

这是用户输入用户名和密码的表单。由于一般密码框不会明文显示这里采用了widget=forms.PasswordInput令其在页面上显示为一个type="password"的INPUT元素。

然后编辑account应用的views.py文件添加如下代码

Copyfrom django.shortcuts import render, HttpResponse
from django.contrib.auth import authenticate, login
from .forms import LoginForm

defuser_login(request):
    if request.method == "POST":
        form = LoginForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            user = authenticate(request, username=cd['username'], password=cd['password'])
            if user isnotNone:
                if user.is_active:
                    login(request, user)
                    return HttpResponse("Authenticated successfully")
                else:
                    return HttpResponse("Disabled account")
            else:
                return HttpResponse("Invalid login")

    else:
        form = LoginForm()

    return render(request, 'account/login.html', {'form': form})

这是我们的登录视图其基本逻辑是当视图接受一个GET请求通过form = LoginForm()实例化一个空白表单;如果接收到POST请求则进行如下工作

  1. 通过form = LoginForm(request.POST)使用提交的数据实例化一个表单对象。

  1. 通过调用form.is_valid()验证表单数据。如果未通过则将当前表单对象展示在页面中。

  1. 如果表单数据通过验证则调用内置authenticate()方法。该方法接受request对象username和password三个参数之后到数据库中进行匹配如果匹配成功会返回一个User数据对象;如果未找到匹配数据返回None。在匹配失败的情况下视图返回一个登陆无效信息。

  1. 如果用户数据成功通过匹配则根据is_active属性检查用户是否为活动用户这个属性是Django内置User模型的一个字段。如果用户不是活动用户则返回一个消息显示不活动用户。

  1. 如果用户是活动用户则调用login()方法在会话中设置用户信息并且返回登录成功的消息。

注意区分内置的authenticate()和login()方法。authenticate()仅到数据库中进行匹配并且返回User数据对象其工作类似于进行数据库查询。而login()用于在当前会话中设置登录状态。二者必须搭配使用才能完成用户名和密码的数据验证和用户登录的功能。

现在需要为视图设置路由在account应用下创建urls.py添加如下代码

Copyfrom django.urls import path
from . import views

urlpatterns = [
    path('login/', views.user_login, name='login')
]

然后编辑项目的根ulrs.py文件导入include并且增加一行转发到account应用的二级路由配置

Copyfrom django.conf.urls import path, include
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
]

之后需要配置模板。由于项目还没有任何模板可以先创建一个母版在account应用下创建如下目录和文件结构

Copytemplates/
    account/
        login.html
    base.html

编辑base.html添加下列代码

Copy{% load staticfiles %}
<!DOCTYPE html><html><head><title>{% block title %}{% endblock %}</title><linkhref="{% static "css/base.css" %}" rel="stylesheet"></head><body><divid="header"><spanclass="logo">Bookmarks</span></div><divid="content">
        {% block content %}
        {% endblock %}
    </div></body></html>

这是这个项目使用的母版。和上一个项目一样使用了CSS文件你需要把static文件夹从源码复制到account应用目录下。这个母版有一个title块和一个content块用于继承。

译者注原书第一章使用了{% load static %}这里的模板使用了{% load staticfiles %}作者并没有对这两者的差异进行说明读者可以参考What is the difference between {% load staticfiles %} and {% load static %}

之后编写account/login.html

Copy{% extends 'base.html' %}

{% block title %}Log-in{% endblock %}

{% block content %}
<h1>Log-in</h1><p>Please, use the following form to log-in:</p><formaction="."method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <p><inputtype="submit"value="Log in"></p></form>
{% endblock %}

这是供用户填写登录信息的页面由于表单通过Post请求提交所以需要{% csrf_token %}。

我们的站点还没有任何用户建立一个超级用户然后使用超级用户到http://127.0.0.1:8000/admin/登录会看到默认的管理后台

使用管理后台添加一个用户然后打开http://127.0.0.1:8000/account/login/可以看到如下登录界面

填写刚创建的用户信息并故意留空表单然后提交可以看到错误信息如下

注意和第一章一样很可能一些现代浏览器会阻止表单提交修改模板关闭表单的浏览器验证即可。

再进行一些实验如果输入不存在的用户名或密码会得到无效登录的提示如果输入了正确的信息就会看到如下的登录成功信息

2.2使用内置验证视图

Django内置很多视图和表单可供直接使用上一节的登录视图就是一个很好的例子。在大多数情况下都可以使用Django内置的验证模块而无需自行编写。

Django在django.contrib.auth.views中提供了如下基于类的视图供使用

  • LoginView处理登录表单填写和登录功能(和我们写的功能类似

  • LogoutView退出登录

  • PaswordChangeView处理一个修改密码的表单然后修改密码

  • PasswordChangeDoneView成功修改密码后执行的视图

  • PasswordResetView用户选择重置密码功能执行的视图生成一个一次性重置密码链接和对应的验证token然后发送邮件给用户

  • PasswordResetDoneView通知用户已经发送给了他们一封邮件重置密码

  • PasswordResetConfirmView用户设置新密码的页面和功能控制

  • PasswordResetCompleteView成功重置密码后执行的视图

上边的视图列表按照一般处理用户相关功能的顺序列出相关视图在编写带有用户功能的站点时可以参考使用。这些内置视图的默认值可以被修改比如渲染的模板位置和使用的表单等。

可以通过官方文档https://docs.djangoproject.com/en/2.0/topics/auth/default/#all-authentication-views了解更多内置验证视图的信息。

2.3登录与登出视图

由于直接使用内置视图和内置数据模型所以不需要编写模型与视图来为内置登录和登出视图配置URL编辑account应用的urls.py文件注释掉之前的登录方法改成内置方法

Copyfrom django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # path('login/', views.user_login, name='login'),
    path('login/',auth_views.LoginView.as_view(),name='login'),
    path('logout/',auth_views.LogoutView.as_view(),name='logout'),
]

现在我们把登录和登出的URL导向了内置视图然后需要为内置视图建立模板

在templates目录下新建registration目录这个目录是内置视图默认到当前应用的模板目录里寻找具体模板的位置。

django.contrib.admin模块中自带一些验证模板用于管理后台使用。我们在INSTALLED_APPS中将account应用放到admin应用的上边令django默认使用我们编写的模板。

在templates/registration目录下创建login.html并添加如下代码

Copy{% extends 'base.html' %}

{% block title %}Log-in{% endblock %}

{% block content %}
    <h1>Log-in</h1>
    {% if form.errors %}
        <p>
        Your username and password didn't match.
        Please try again.
        </p>
    {% else %}
        <p>Please, use the following form to log-in:</p>
    {% endif %}

    <divclass="login-form"><formaction="{% url 'login' %}"method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <inputtype="hidden"name="next"value="{{ next }}"><p><inputtype="submit"value="Log-in"></p></form></div>

{% endblock %}

这个模板和刚才自行编写登录模板很类似。内置登录视图默认使用django.contrib.auth.forms里的AuthenticationForm表单通过检查{% if form.errors %}可以判断验证信息是否错误。注意我们添加了一个name属性为next的隐藏<input>元素这是内置视图通过Get请求获得并记录next参数的位置用于返回登录前的页面例如http://127.0.0.1:8000/account/login/?next=/account/

next参数必须是一个URL地址如果具有这个参数登录视图会在登录成功后将用户重定向到这个参数的URL。

在registration目录下创建logged_out.html

Copy{% extends 'base.html' %}

{% block title %}
Logged out
{% endblock %}

{% block content %}
<h1>Logged out</h1><p>You have been successfully logged out. You can <ahref="{% url 'login' %}">log-in again</a>.</p>
{% endblock %}

这是用户登出之后显示的提示页面。

现在我们的站点已经可以使用用户登录和登出的功能了。现在还需要为用户制作一个登录成功后自己的首页打开account应用的views.py文件添加如下代码

Copyfrom django.contrib.auth.decorators import login_required
@login_requireddefdashboard(request):
    return render(request, 'account/dashboard.html', {'section': 'dashboard'})

使用@login_required装饰器表示被装饰的视图只有在用户登录的情况下才会被执行如果用户未登录则会将用户重定向至Get请求附加的next参数指定的URL。这样设置之后如果用户在未登录的情况下无法看到首页。

还定义了一个参数section可以用来追踪用户当前所在的功能板块。

现在可以创建首页对应的模板在templates/account/目录下创建dashboard.html

Copy{% extends 'base.html' %}

{% block title %}
Dashboard
{% endblock %}

{% block content %}
    <h1>Dashboard</h1><p>Welcome to your dashboard.</p>
{% endblock %}

然后在account应用的urls.py里增加新视图对应的URL

Copyurlpatterns = [
    # ...
    path('', views.dashboard, name='dashboard'),
]

还需要在settings.py里增加如下设置

CopyLOGIN_REDIRECT_URL = 'dashboard'
LOGIN_URL = 'login'
LOGOUT_URL = 'logout'

这三个设置分别表示

  • 如果没有指定next参数登录成功后重定向的URL

  • 用户需要登录的情况下被重定向到的URL地址(例如@login_required重定向到的地址

  • 用户需要登出的时候被重定向到的URL地址

这里都使用了path()方法中的name属性以动态的返回链接。在这里也可以硬编码URL。

总结一下我们现在做过的工作

  • 为项目添加内置登录和登出视图

  • 为两个视图编写模板并编写了首页视图和对应模板

  • 为三个视图配置了URL

最后需要在母版上添加登录和登出相关的展示。为了实现这个功能必须根据当前用户是否登录决定模板需要展示的内容。在内置函数LoginView成功执行之后验证模块的中间件在HttpRequest对象上设置了用户对象User可以通过request.user访问用户信息。在用户未登录的情况下request.user也存在是一个AnonymousUser类的实例。判断当前用户是否登录最好的方式就是判断User对象的is_authenticated只读属性。

编辑base.html修改ID为header的<div>标签

Copy<divid="header"><spanclass="logo">Bookmarks</span>
    {% if request.user.is_authenticated %}
    <ulclass="menu"><li {% ifsection == 'dashboard' %}class="selected"{% endif %}><ahref="{% url 'dashboard' %}">My dashboard</a></li><li {% ifsection == 'images' %}class="selected"{% endif %}><ahref="#">Images</a></li><li {% ifsection == 'people' %}class="selected"{% endif %}><ahref="#">People</a></li></ul>
    {% endif %}

    <spanclass="user">
        {% if request.user.is_authenticated %}
        Hello {{ request.user.first_name }},{{ request.user.username }},<ahref="{% url 'logout' %}">Logout</a>
            {% else %}
            <ahref="{% url 'login' %}">Log-in</a>
        {% endif %}
 </span></div>

上边的视图只显示站点的菜单给已登录用户。还添加了了根据section的内容为<li>添加CSS类selected的功能用于显示高亮当前的板块。最后对登录用户显示名称和登出链接对未登录用户则显示登录链接。

现在启动项目到http://127.0.0.1:8000/account/login/会看到登录页面输入有效的用户名和密码并点击登录按钮之后会看到如下页面

可以看到当前的 My dashboard 应用了selected类的CSS样式。当前用户的信息显示在顶部的右侧点击登出链接会看到如下页面

可以看到用户已经登出顶部的菜单栏已经不再显示右侧的链接变为登录链接。

如果这里看到Django内置的管理站点样式的页面检查settings.py文件中的INSTALLED_APPS设置确保account应用在django.contrib.admin应用的上方。由于内置的视图和我们自定义的视图使用了相同的相对路径Django的模板加载器会使用先找到的模板。

2.4修改密码视图

在用户登录之后需要允许用户修改密码我们在项目中集成Django的内置修改密码相关的视图。编辑account应用的urls.py文件添加如下两行URL

Copypath('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),

asswordChangeView视图会控制渲染修改密码的页面和表单PasswordChangeDoneView视图在成功修改密码之后显示成功消息。

之后要为两个视图创建模板在templates/registration/目录下创建password_change_form.html添加如下代码

Copy{% extends 'base.html' %}

{% block title %}
Change your password
{% endblock %}

{% block content %}
<h1>Change your password</h1><p>Use the form below to change your password.</p><formaction="."method="post"novalidate>
    {{ form.as_p }}
    <p><inputtype="submit"value="Change"></p>
    {% csrf_token %}
    </form>
{% endblock %}

password_change_form.html模板包含修改密码的表单再在同一目录下创建password_change_done.html

Copy{% extends 'base.html' %}

{% block title %}
Password changed
{% endblock %}

{% block content %}
<h1>Password changed</h1>
    <p>Your password has been successfully changed.</p>
{% endblock %}

password_change_done.html模板包含成功创建密码后的提示消息。

启动服务到http://127.0.0.1:8000/account/password_change/成功登录之后可看到如下页面

填写表单并修改密码之后可以看到成功消息

之后登出再登录验证是否确实成功修改密码。

2.5重置密码视图

编辑account应用的urls.py文件添加如下对应到内置视图的URL

Copypath('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

然后在account应用的templates/registration/目录下创建password_reset_form.html

Copy{% extends 'base.html' %}

{% block title %}
Reset your password
{% endblock %}

{% block content %}
<h1>Forgotten your password?</h1><p>Enter your e-mail address to obtain a new password.</p><formaction="."method="post"novalidate>
    {{ form.as_p }}
    {% csrf_token %}
    <p><inputtype="submit"value="Send e-mail"></p></form>
{% endblock %}

在同一目录下创建发送邮件的页面password_reset_email.html添加如下代码

CopySomeone asked for password reset for email {{ email }}. Follow the link
below:
{{ protocol }}://{{ domain }}{% url "password_reset_confirm" uidb64=uid token=token %}
Your username, in case you've forgotten: {{ user.get_username }}

这个模板用来渲染向用户发送的邮件内容。

之后在同一目录再创建password_reset_done.html表示成功发送邮件的页面

Copy{% extends 'base.html' %}

{% block title %}
Reset your password
{% endblock %}

{% block content %}
<h1>Reset your password</h1><p>We've emailed you instructions for setting your password.</p><p>If you don't receive an email, please make sure you've entered the
address you registered with.</p>
{% endblock %}

然后创建重置密码的页面password_reset_confirm.html这个页面是用户从邮件中打开链接后经过视图处理后返回的页面

Copy{% extends 'base.html' %}

{% block title %}Reset your password{% endblock %}

{% block content %}
    <h1>Reset your password</h1>
    {% if validlink %}
        <p>Please enter your new password twice:</p><formaction="."method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <p><inputtype="submit"value="Change my password"/></p></form>
    {% else %}
        <p>The password reset link was invalid, possibly because it has
            already been used. Please request a new password reset.</p>
    {% endif %}
{% endblock %}

这个页面里有一个变量validlink表示用户点击的链接是否有效由PasswordResetConfirmView视图传入模板。如果有效就显示重置密码的表单如果无效就显示一段文字说明链接无效。

在同一目录内建立password_reset_complete.html

Copy{% extends "base.html" %}
{% block title %}Password reset{% endblock %}
{% block content %}
<h1>Password set</h1><p>Your password has been set. You can <ahref="{% url "login" %}">log in
now</a></p>
{% endblock %}

最后编辑registration/login.html在<form>元素之后加上如下代码为页面增加重置密码的链接

Copy<p><ahref="{% url 'password_reset' %}">Forgotten your password?</a></p>

之后在浏览器中打开http://127.0.0.1:8000/account/login/点击Forgotten your password?链接会看到如下页面

这里必须在settings.py中配置SMTP服务器在第二章中已经学习过配置STMP服务器的设置。如果确实没有SMTP服务器可以增加一行

CopyEMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

以让Django将邮件内容输出到命令行窗口中。

返回浏览器填入一个已经存在的用户的电子邮件地址之后点SEND E-MAIL按钮会看到如下页面

此时看一下启动Django站点的命令行窗口会打印如下邮件内容(或者到信箱中查看实际收到的电子邮件

CopyContent-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on127.0.0.1:8000From: webmaster@localhostTo: user@domain.com
Date: Fri, 15Dec201714:35:08-0000
Message-ID: <20150924143508.62996.55653@zenx.local>
Someone asked for password reset for email user@domain.com. Follow the link
below:
http://127.0.0.1:8000/account/reset/MQ/45f-9c3f30caafd523055fcc/
Your username, incase you've forgotten: zenx

这个邮件的内容就是password_reset_email.html经过渲染之后的实际内容。其中的URL指向视图动态生成的链接将这个URL复制到浏览器中打开会看到如下页面

这个页面使用password_reset_confirm.html模板生成填入一个新密码然后点击CHANGE MY PASSWORD按钮Django会用你输入的内容生成加密后的密码保存在数据库中然后会看到如下页面

现在就可以使用新密码登录了。这里生成的链接只能使用一次如果反复打开该链接会收到无效链接的错误。

我们现在已经集成了Django内置验证模块的主要功能在大部分情况下可以直接使用内置验证模块。也可以自行编写所有的验证程序。

在第一个项目中我们提到为应用配置单独的二级路由有助于应用的复用。现在的account应用的urls.py文件中所有配置到内置视图的URL可以用如下一行来代替

Copyurlpatterns = [
    # ...
    path('', include('django.contrib.auth.urls')),
]

可以在github上看到django.contrib.auth.urls的源代码https://github.com/django/django/blob/stable/2.0.x/django/contrib/auth/urls.py

3用户注册与用户信息

已经存在的用户现在可以登录、登出、修改和重置密码了。现在需要建立一个功能让用户注册。

3.1用户注册

为用户注册功能创建一个简单的视图先建立一个供用户输入用户名、姓名和密码的表单。编辑account应用的forms.py文件添加如下代码

Copyfrom django.contrib.auth.models import User

classuserRegistrationForm(forms.ModelForm):
    password = forms.CharField(label='password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Repeat password', widget=forms.PasswordInput)

    classMeta:
        model = User
        fields = ('username','first_name','email')

    defclean_password2(self):
        cd = self.cleaned_data
        if cd['password'] != cd['password2']:
            raise forms.ValidationError(r"Password don't match.")
        return cd['password2']

这里通过用户模型建立了一个模型表单只包含usernamefirst_name和email字段。这些字段会根据User模型中的设置进行验证比如如果输入了一个已经存在的用户名则验证不会通过因为username字段被设置了unique=True。添加了两个新的字段password和password2用于用户输入并且确认密码。定义了一个clean_password2()方法用于检查两个密码是否一致这个方法是一个验证器方法会在调用is_valid()方法的时候执行。可以对任意的字段采用clean_<fieldname>()方法名创建一个验证器。Forms类还拥有一个clean()方法用于验证整个表单可以方便的验证彼此相关的字段。

译者注这里必须了解表单的验证顺序。clean_password2()方法中使用了cd['password2'];为什么验证器还没有执行完毕的时候cleaned_data中已经存在password2数据了呢?这里有一篇介绍django验证表单顺序的文章可以看到在执行自定义验证器之前已经执行了每个字段的clean()方法这个方法仅针对字段本身的属性进行验证只要这个通过了cleaned_data中就有了数据之后才执行自定义验证器最后执行form.clean()完成验证。如果过程中任意时候抛出ValidationErrorcleaned_data里就会只剩有效的值errors属性内就有了错误信息。

关于用户注册Django提供了一个位于django.contrib.auth.forms的UserCreationForm表单供使用和我们自行编写的表单非常类似。

编辑account应用的views.py文件添加如下代码

Copyfrom .forms import LoginForm, UserRegistrationForm

defregister(request):
    if request.method == "POST":
        user_form = UserRegistrationForm(request.POST)
        if user_form.is_valid():
            # 建立新数据对象但是不写入数据库
            new_user = user_form.save(commit=False)
            # 设置密码
            new_user.set_password(user_form.cleaned_data['password'])
            # 保存User对象
            new_user.save()
            return render(request, 'account/register_done.html', {'new_user': new_user})
    else:
        user_form = UserRegistrationForm()
    return render(request, 'account/register.html', {'user_form': user_form})

这个视图逻辑很简单我们使用了set_password()方法设置加密后的密码。

再配置account应用的urls.py文件添加如下的URL匹配:

Copypath('register/', views.register, name='register'),

在templates/account/目录下创建模板register.html添加如下代码

Copy{% extends 'base.html' %}

{% block title %}
Create an account
{% endblock %}

{% block content %}
<h1>Create an account</h1><p>Please, sign up using the following form</p><formaction="."method="post"novalidate>
    {{ user_form.as_p }}
    {% csrf_token %}
    <p><inputtype="submit"value="Register"></p></form>
{% endblock %}

在同一目录下创建register_done.html模板用于显示注册成功后的信息

Copy{% extends 'base.html' %}

{% block title %}
Welcome
{% endblock %}

{% block content %}
    <h1>Welcome {{ new_user.first_name }}!</h1><p>Your account has been successfully created. Now you can <ahref="{% url 'login' %}">log in</a>.</p>
{% endblock %}

现在可以打开http://127.0.0.1:8000/account/register/看到注册界面如下

填写表单并点击CREATE MY ACCOUNT按钮如果表单正确提交会看如下成功页面

3.2扩展用户模型

Django内置验证模块的User模型只有非常基础的字段信息可能需要额外的用户信息。最好的方式是建立一个用户信息模型然后通过一对一关联字段将用户信息模型和用户模型联系起来。

编辑account应用的models.py文件添加以下代码

Copyfrom django.db import models
from django.conf import settings

classProfile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    date_of_birth = models.DateField(blank=True, null=True)
    photo = models.ImageField(upload_to='user/%Y/%m/%d/', blank=True)

    def__str__(self):
        return"Profile for user {}".format(self.user.username)

为了保持代码通用性使用get_user_model()方法来获取用户模型;当定义其他表与内置User模型的关系时使用settings.AUTH_USER_MODEL指代User模型。

这个Profile模型的user字段是一个一对一关联到用户模型的关系字段。将on_delete设置为CASCADE当用户被删除时其对应的信息也被删除。这里还有一个图片文件字段必须安装Python的Pillow库才能使用图片文件字段在系统命令行中输入

Copypip install Pillow==5.1.0

由于我们要允许用户上传图片必须配置Django让其提供媒体文件服务在settings.py中加入下列内容

CopyMEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL表示存放和提供用户上传文件的URL路径MEDIA_ROOT表示实际媒体文件的存放目录。这里都采用相对地址动态生成URL。

来编辑一下bookmarks项目的根urls.py修改其中的代码如下:

Copyfrom django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

这样设置后Django开发服务器在DEBUG=True的情况下会提供媒体文件服务。

static()方法仅用于开发环境在生产环境中不要用Django提供静态文件服务(而是用Web服务程序比如NGINX等提供静态文件服务。

建立了新的模型之后需要执行数据迁移过程。之后将新的模型加入到管理后台编辑account应用的admin.py文件将Profile模型注册到管理后台中

Copyfrom django.contrib import admin
from .models import Profile

@admin.register(Profile)classProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'date_of_birth', 'photo']

启动站点打开http://127.0.0.1:8000/admin/可以在管理后台中看到新增的模型

现在需要让用户填写额外的用户信息为此需要建立表单编辑account应用的forms.py文件

Copyfrom .models import Profile

classUserEditForm(forms.ModelForm):
    classMeta:
        model = User
        fields = ('first_name', 'last_name', 'email')

classProfileEditForm(forms.ModelForm):
    classMeta:
        model = Profile
        fields = ('date_of_birth', 'photo')

这两个表单解释如下

  • UserEditForm这个表单依据User类生成让用户输入姓名和电子邮件。

  • ProfileEditForm这个表单依据Profile类生成可以让用户输入生日和上传一个头像。

之后建立视图编辑account应用的views.py文件导入Profile模型

Copyfrom .models import Profile

然后在register视图的new_user.save()下增加一行

CopyProfile.objects.create(user=new_user)

当用户注册的时候会自动建立一个空白的用户信息关联到用户。在之前创建的用户则必须在管理后台中手工为其添加对应的Profile对象

还必须让用户可以编辑他们的信息在同一个文件内添加下列代码

Copyfrom .forms import LoginForm, UserRegistrationForm, UserEditForm, ProfileEditForm

@login_requireddefedit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

这里使用了@login_required装饰器因为用户必须登录才能编辑自己的信息。我们使用UserEditForm表单存储内置的User类的数据用ProfileEditForm存放Profile类的数据。然后调用is_valid()验证两个表单数据如果全部都通过将使用save()方法写入数据库。

译者注原书没有解释instance参数。instance用于指定表单类实例化为某个具体的数据对象。在这个例子里将UserEditForm``instance指定为request.user表示该对象是数据库中当前登录用户那一行的数据对象而不是一个空白的数据对象ProfileEditForm的instance属性指定为当前用户对应的Profile类中的那行数据。这里如果不指定instance参数则变成向数据库中增加两条新记录而不是修改原有记录。

之后编辑account应用的urls.py文件为新视图配置URL

Copypath('edit/', views.edit, name='edit'),

最后在templates/account/目录下创建edit.html添加如下代码

Copy{#edit.html#}
{% extends 'base.html' %}

{% block title %}
Edit your account
{% endblock %}

{% block content %}
<h1>Edit your account</h1><p>You can edit your account using the following form:</p><formaction="."method="post"enctype="multipart/form-data"novalidate>
    {{ user_form.as_p }}
    {{ profile_form.as_p }}
    {% csrf_token %}
        <p><inputtype="submit"value="Save changes"></p></form>
{% endblock %}

由于这个表单可能处理用户上传头像文件所以必须设置enctype="multipart/form-data。我们采用一个HTML表单同时提交user_form和profile_form表单。

启动站点注册一个新用户然后打开http://127.0.0.1:8000/account/edit/可以看到页面如下

现在可以在用户登录后的首页加上修改用户信息的链接了打开account/dashboard.html找到下边这行

Copy<p>Welcome to your dashboard.</p>

将其替换为

Copy<p>Welcome to your dashboard. You can <ahref="{% url 'edit' %}">edit your profile</a> or <ahref="{% url "password_change" %}">change your password</a>.</p>

用户现在可以通过登录后的首页修改用户信息打开http://127.0.0.1:8000/account/然后可以看到新增了修改用户信息的链接页面如下

3.2.1使用自定义的用户模型

Django提供了使用自定义的模型替代内置User模型的方法需要编写自定义的类继承AbstractUser类。这个AbstractUser类提供了默认的用户模型的完整实现作为一个抽象类供其他类继承。关于模型的继承将在本书最后一个项目中学习。可以在https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#substituting-a-custom-user-model找到关于自定义用户模型的详细信息。

使用自定义用户模型比起默认内置用户模型可以更好的满足开发需求但需要注意的是会影响一些使用Django内置用户模型的第三方应用。

3.3使用消息框架

当用户在我们的站点执行各种操作时在一些关键操作可能需要通知用户其操作是否成功。Django有一个内置消息框架可以给用户发送一次性的通知。

消息模块位于django.contrib.messages并且已经被包含在初始化的INSTALLED_APPS设置中还有一个默认启用的中间件叫做django.contrib.messages.middleware.MessageMiddleware共同构成了消息系统。

消息框架提供了非常简单的方法向用户发送通知默认在cookie中存储消息内容(根据session的存储设置然后会在下一次HTTP请求的时候在对应的响应上附加该信息。导入消息模块并且在视图中使用很简单的语句就可以发送消息例如

Copyfrom django.contrib import messages
messages.error(request, 'Something went wrong')

这样就在请求上附加了一个错误信息。可以使用add_message()或如下的方法创建消息

  • success()一个动作成功之后发送的消息

  • info()通知性质的消息

  • warning()警告性质的内容所谓警告就是还没有失败但很可能失败的情况

  • error()错误信息通知操作失败

  • debug()除错信息给开发者展示在生产环境中需要被移除

在我们的站点中增加消息内容。由于消息是贯穿整个网站的所以打算将消息显示的部分设置在母版中编辑base.html在ID为header的<div>标签和ID为content的<div>标签之间增加下列代码

Copy{% if messages %}
    <ulclass="messages">
        {% for message in messages %}
            <liclass="{{ message.tags }}">{{ message|safe }}<ahref="#"class="close">X</a></li>
        {% endfor %}
    </ul>
{% endif %}

在模板中使用了messages变量在后文可以看到视图并未向模板传入该变量。这是因为在settings.py中的TEMPLATES设置中context_processors的设置中包含django.contrib.messages.context_processors.messages这个上下文管理器从而为模板传入了messages变量而无需经过视图。默认情况下可以看到还有debugrequest和auth三个上下文处理器。其中后两个就是我们在模板中可以直接使用request.user而无需传入该变量也无需为request对象添加user属性的原因。

之后来修改account应用的views.py文件导入messages然后编辑edit视图

Copyfrom django.contrib import messages

@login_requireddefedit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

为视图增加了两条语句分别在成功登录之后显示成功信息在表单验证失败的时候显示错误信息。

浏览器中打开http://127.0.0.1:8000/account/edit/编辑用户信息之后可以看到成功信息如下

故意填写通不过验证的数据则可以看到错误信息如下

关于消息框架的更多信息可以查看官方文档https://docs.djangoproject.com/en/2.0/ref/contrib/messages/

4创建自定义验证后端

Django允许对不同的数据来源采用不同的验证方式。在settings.py里有一个AUTHENTICATION_BACKENDS设置列出了项目中可使用的验证后端。其默认是

Copy['django.contrib.auth.backends.ModelBackend']

默认的ModelBackend通过django.contrib.auth后端进行验证这对于大部分项目已经足够。然而我们也可以创建自定义的验证后端用于满足个性化需求比如LDAP目录或者来自于其他系统的验证。

关于自定义验证后端可以参考官方文档https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#other-authentication-sources

每次使用内置的authenticate()函数时Django会按照AUTHENTICATION_BACKENDS设置中列出的顺序依次执行其中的验证后端进行验证工作直到有一个验证后端返回成功为止。如果列表中的后端全部返回失败则这个用户就不会被认证通过。

Django提供了一个简单的规则用于编写自定义验证后端一个验证后端必须是一个类至少提供如下两个方法

  • authenticate()参数为request和用户验证信息如果用户验证信息有效必须返回一个user对象否则返回None。request参数必须是一个HttpRequest对象或者是None

  • get_user()参数为用户的ID返回一个user对象

我们来编写一个采用电子邮件(而不是username字段和密码登录的验证后端编写验证后端就和编写一个Python的类没有什么区别

Copyfrom django.contrib.auth.models import User

classEmailAuthBakcend:
    """
    Authenticate using an e-mail address.
    """defauthenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            returnNoneexcept User.DoesNotExist:
            returnNonedefget_user(self, user_id):
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            returnNone

以上代码是一个简单的验证后端。authenticate()方法接受request对象和username及password作为可选参数这里可以用任何自定义的参数名称我们使用username及password是为了可以与内置验证框架配合工作。两个方法工作流程如下

  • authenticate()尝试使用电子邮件和密码获取用户对象采用check_password()方法验证加密后的密码。

  • get_user()通过user_id参数获取用户ID在会话存续Django会使用内置的验证后端去验证并取得User对象。

编辑settings.py文件增加

CopyAUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
]

在上边的设置里我们将自定义验证后端加到了默认验证的后边。打开http://127.0.0.1:8000/account/login/注意Django尝试使用所有的验证后端所以我们现在可以使用用户名或者电子邮件来登录填写的信息会先交给ModelBackend进行验证如果没有得到用户对象就会使用我们的EmailAuthBackend进行验证。

AUTHENTICATION_BACKENDS中的顺序很重要如果一个用户信息对于多个验证后端都有效Django会停止在第一个成功验证的后端处。

5第三方认证登录

很多社交网站除了注册用户之外提供了链接可以快速的通过第三方平台的用户信息进行登录我们也可以为自己的站点添加例如FacebookTwitter或Google的第三方认证登录功能。Python Social Auth是一个提供第三方认证登录的模块。使用这个模块可以让用户以第三方网站的信息进行登录而无需先注册本网站的用户。这个模块的源码在https://github.com/python-social-auth

这个模块支持很多不同的Python Web框架其中也包括Django通过以下命令安装

Copypip install social-auth-app-django==2.1.0

然后将应用名social_django添加到settings.py文件的INSTALLED_APPS设置中

CopyINSTALLED_APPS = [
    #...'social_django',
]

该应用自带了数据模型所以需要执行数据迁移过程。执行之后可以在数据库中看到新增social_auth开头的一系列数据表。Python 的social auth模块具体支持的第三方验证服务可以查看官方文档https://python-social-auth.readthedocs.io/en/latest/backends/index.html#supported-backends

译者注FacebookTwitter和Google的第三方验证均通过OAuth2认证而且操作方式基本相同。以下仅以Google为例子进行翻译

需要先把第三方认证的URL添加到项目中编辑bookmarks项目的根urls.py

Copyurlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
]

一些网站的第三方验证接口不允许将验证后的地址重定向到类似127.0.0.1或者localhost这种本地地址为了正常使用第三方验证服务需要一个正式域名可以通过修改Hosts文件。如果是Linux或macOS X下可以编辑/etc/hosts加入一行

Copy127.0.0.1 mysite.com

这样会将mysite.com域名对应到本机地址。如果是Windows环境可以在C:\Windows\System32\Drivers\etc\hosts找到hosts文件。

为了测试该设置是否生效启动站点然后在浏览器中打开http://mysite.com:8000/account/login/会得到如下错误信息

这是因为Djanog在settings.py中的ALLOWED_HOSTS设置中仅允许对此处列出的域名提供服务这是为了防止HTTP请求头攻击。关于该设置可以参考官方文档https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts

编辑settings.py文件然后修改ALLOWED_HOSTS为如下

CopyALLOWED_HOSTS = ['mysite.com', 'localhost', '127.0.0.1']

mysite.com之外我们增加了localhost和127.0.0.1其中localhost是在DEBUG=True和ALLOWED_HOSTS留空情况下的默认值现在就可以通过http://mysite.com:8000/account/login/正常访问开发网站了。

5.1使用Google第三方认证

Google提供OAuth2认证详细文档可以参考https://developers.google.com/identity/protocols/OAuth2

为使用Google的第三方认证服务将以下验证后端添加到settings.py的AUTHENTICATION_BACKENDS中

CopyAUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
    'social_core.backends.google.GoogleOAuth2',
]

译者注由于Google API的界面在原书成书后已经改变以下在Google网站的操作步骤和截图来自于译者实际操作过程。

需要到Google开发者网站创建一个API key按照以下步骤操作

  1. 打开https://console.developers.google.com/apis/credentials点击屏幕左上方Google APIs字样右边的选择项目会弹出项目对话框点击右上方的新建项目如图所示

  1. 填写新建项目的信息项目名称为Bookmarks位置可以不选之后点击创建按钮如下图所示

  1. 之后与步骤1中的步骤类似点开选择项目选中刚建立的Bookmarks项目然后点击右下方的打开。

  1. 会自动跳转到一个页面提示尚未创建API凭据点击页面中的创建凭据按钮并选择第二项OAuth客户端ID如下图所示

  1. 之后会进入一个界面要求必须配置OAuth同意屏幕如下图所示

点击右侧的配置同意屏幕按钮。

  1. 之后进入到OAuth同意屏幕里边有一系列设置。在应用名称中填入Bookmarks默认支持电子邮件为你自己的电子邮件地址可以修改为其他地址在已获授权的网域中填入mysite.com之后点击保存如图所示

  1. 此时会跳转到步骤5的问题页面选择网页应用之后会被要求填写辅助信息在名称中填写Bookmarks已获授权的重定向 URI中填写http://mysite.com:8000/social-auth/complete/google-oauth2/如下图所示

  1. 点击创建按钮即可在页面中看到当前API的ID和密钥如图所示

  1. 将API ID 和密钥填写到settings.py文件中增加如下两行

CopySOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'XXX'# API ID
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'XXX'# 密钥
  1. 点击确认关闭对话框之后在左侧菜单的凭据菜单内可以回到此处查看ID和密钥。现在点击左侧菜单的库会跳转到欢迎使用新版API库的界面在其中找到Google+ API如图所示

  1. 点击Google+ API在弹出的页面中选择启用如图所示

在Google中的配置就全部结束了生成了一个OAuth2认证的ID和密钥之后我们就将采用这些信息与Google进行通信。

然后编辑account应用的registration/login.html模板在content块的内部最下方增加用于进行Google第三方认证登录的链接

Copy<divclass="social"><ul><liclass="google"><ahref="{% url 'social:begin' 'google-oauth2' %}">Log in with Google</a></li></ul></div>

打开http://mysite.com:8000/account/login/可以看到如下页面

点击Login with Google按钮使用Google账户登录后就会被重定向到我们网站的登录首页。

我们现在就为项目增加了第三方认证登录功能即使是没有在本站注册的用户也可以快捷的进行登录了。

译者注这里有一个小问题就是通过第三方登录进来的用户检查auth_user表会发现其实用户信息已经被写入到了该表里但是Profile表没有写入对应的外键字段导致第三方认证用户在修改用户信息时会报错。很多网站的做法是通过第三方验证进来的用户必须捆绑到本站已经存在的账号中。这里我们简化一下处理当用户修改字段的Get请求进来时检测Profile表中该用户的外键是不是存在如果不存在就新建对应该用户的Profile对象然后再用这个数据对象返回表单实例供填写。修改后的edit视图如下

Copy@login_requireddefedit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(instance=request.user.profile, data=request.POST, files=request.FILES)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    else:
        try:
            Profile.objects.get(user=request.user)
        except Profile.DoesNotExist:
            Profile.objects.create(user=request.user)
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)

    return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})

总结

这一章学习了使用内置框架快捷的建立用户验证系统以及建立自定义的用户信息还学习了为网站添加第三方认证。

下一章中将学习建立一个图片分享系统生成图片缩略图以及在Djanog中使用AJAX技术。

如有不懂还要咨询下方小卡片博主也希望和志同道合的测试人员一起学习进步

在适当的年龄选择适当的岗位尽量去发挥好自己的优势。

我的自动化测试开发之路一路走来都离不每个阶段的计划因为自己喜欢规划和总结

测试开发视频教程、学习笔记领取传送门

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go

“测试开发之Django实战示例 第四章 创建社交网站” 的相关文章