第四章 创建社交网站

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

本章包含以下内容:

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

1社交网站

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

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

1.1启动社交网站项目

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

mkdir env
virtualenv env/bookmarks
source env/bookmarks/bin/activate

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

(bookmarks)laptop:~ zenx$

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

pip install Django==2.0.5
django-admin startproject bookmarks

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

cd bookmarks/
django-admin startapp account

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

INSTALLED_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设置中的如下两个中间件:

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

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

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

2.1创建登录视图

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

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

from django import forms

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

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

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

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

def user_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 is not None:
                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),使用提交的数据实例化一个表单对象。
  2. 通过调用form.is_valid()验证表单数据。如果未通过,则将当前表单对象展示在页面中。
  3. 如果表单数据通过验证,则调用内置authenticate()方法。该方法接受request对象,usernamepassword三个参数,之后到数据库中进行匹配,如果匹配成功,会返回一个User数据对象;如果未找到匹配数据,返回None。在匹配失败的情况下,视图返回一个登陆无效信息。
  4. 如果用户数据成功通过匹配,则根据is_active属性检查用户是否为活动用户,这个属性是Django内置User模型的一个字段。如果用户不是活动用户,则返回一个消息显示不活动用户。
  5. 如果用户是活动用户,则调用login()方法,在会话中设置用户信息,并且返回登录成功的消息。

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

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

from django.urls import path
from . import views

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

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

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

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

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

templates/
    account/
        login.html
    base.html

编辑base.html,添加下列代码:

{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{% endblock %}</title>
    <link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
    <div id="header">
        <span class="logo">Bookmarks</span>
    </div>
    <div id="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

{% extends 'base.html' %}

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

{% block content %}
<h1>Log-in</h1>
<p>Please, use the following form to log-in:</p>
    <form action="." method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="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中提供了如下基于类的视图供使用:

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

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

2.3登录与登出视图

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

from 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并添加如下代码:

{% 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 %}

    <div class="login-form">
        <form action="{% url 'login' %}" method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <input type="hidden" name="next" value="{{ next }}">
            <p><input type="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

{% extends 'base.html' %}

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

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

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

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

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

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

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

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

{% extends 'base.html' %}

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

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

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

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

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

LOGIN_REDIRECT_URL = 'dashboard'
LOGIN_URL = 'login'
LOGOUT_URL = 'logout'

这三个设置分别表示:

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

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

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

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

<div id="header">
<span class="logo">Bookmarks</span>
    {% if request.user.is_authenticated %}
    <ul class="menu">
        <li {% if section == 'dashboard' %}class="selected"{% endif %}><a href="{% url 'dashboard' %}">My dashboard</a></li>
        <li {% if section == 'images' %}class="selected"{% endif %}><a href="#">Images</a></li>
        <li {% if section == 'people' %}class="selected"{% endif %}><a href="#">People</a></li>
    </ul>
    {% endif %}

    <span class="user">
        {% if request.user.is_authenticated %}
        Hello {{ request.user.first_name }},{{ request.user.username }},<a href="{% url 'logout' %}">Logout</a>
            {% else %}
            <a href="{% 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:

path('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,添加如下代码:

{% 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>
    <form action="." method="post" novalidate>
    {{ form.as_p }}
    <p><input type="submit" value="Change"></p>
    {% csrf_token %}
    </form>
{% endblock %}

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

{% 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:

path('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

{% 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>
    <form action="." method="post" novalidate>
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Send e-mail"></p>
    </form>
{% endblock %}

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

Someone 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,表示成功发送邮件的页面:

{% 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,这个页面是用户从邮件中打开链接后经过视图处理后返回的页面:

{% 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>
        <form action="." method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <p><input type="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

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

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

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

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

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

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

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

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

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

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: user@domain.com
Date: Fri, 15 Dec 2017 14: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, in case you've forgotten: zenx

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

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

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

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

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

urlpatterns = [
    # ...
    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文件,添加如下代码:

from django.contrib.auth.models import User

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

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

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

这里通过用户模型建立了一个模型表单,只包含usernamefirst_nameemail字段。这些字段会根据User模型中的设置进行验证,比如如果输入了一个已经存在的用户名,则验证不会通过,因为username字段被设置了unique=True。添加了两个新的字段passwordpassword2,用于用户输入并且确认密码。定义了一个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.formsUserCreationForm表单供使用,和我们自行编写的表单非常类似。

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

from .forms import LoginForm, UserRegistrationForm

def register(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匹配:

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

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

{% 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>
    <form action="." method="post" novalidate>
    {{ user_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Register"></p>
    </form>
{% endblock %}

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

{% 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 <a href="{% 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文件,添加以下代码:

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


class Profile(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库才能使用图片文件字段,在系统命令行中输入:

pip install Pillow==5.1.0

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

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

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

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

from 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模型注册到管理后台中:

from django.contrib import admin
from .models import Profile

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

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

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

from .models import Profile

class UserEditForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileEditForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('date_of_birth', 'photo')

这两个表单解释如下:

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

from .models import Profile

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

Profile.objects.create(user=new_user)

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

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

from .forms import LoginForm, UserRegistrationForm, UserEditForm, ProfileEditForm

@login_required
def edit(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用于指定表单类实例化为某个具体的数据对象。在这个例子里,将UserEditForminstance指定为request.user表示该对象是数据库中当前登录用户那一行的数据对象,而不是一个空白的数据对象,ProfileEditForminstance属性指定为当前用户对应的Profile类中的那行数据。这里如果不指定instance参数,则变成向数据库中增加两条新记录,而不是修改原有记录。

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

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

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

{#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>
    <form action="." method="post" enctype="multipart/form-data" novalidate>
    {{ user_form.as_p }}
    {{ profile_form.as_p }}
    {% csrf_token %}
        <p><input type="submit" value="Save changes"></p>
    </form>
{% endblock %}

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

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

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

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

将其替换为:

<p>Welcome to your dashboard. You can <a href="{% url 'edit' %}">edit your profile</a> or <a href="{% 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请求的时候在对应的响应上附加该信息。导入消息模块并且在视图中使用很简单的语句就可以发送消息,例如:

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

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

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

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

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

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

from django.contrib import messages

@login_required
def edit(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设置列出了项目中可使用的验证后端。其默认是:

['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提供了一个简单的规则用于编写自定义验证后端:一个验证后端必须是一个类,至少提供如下两个方法:

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

from django.contrib.auth.models import User


class EmailAuthBackend:
    """
    Authenticate using an e-mail address.
    """

    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            return None

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

编辑settings.py文件增加:

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

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

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

5第三方认证登录

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

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

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

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

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

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

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

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

urlpatterns = [
    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加入一行:

127.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为如下:

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

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

5.1使用Google第三方认证

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

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

AUTHENTICATION_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字样右边的选择项目,会弹出项目对话框,点击右上方的新建项目,如图所示:

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

  3. 之后与步骤1中的步骤类似,点开选择项目,选中刚建立的Bookmarks项目,然后点击右下方的打开
  4. 会自动跳转到一个页面提示尚未创建API凭据,点击页面中的创建凭据按钮,并选择第二项OAuth客户端ID,如下图所示:

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

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

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

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

  9. 将API ID 和密钥填写到settings.py文件中,增加如下两行:
    SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'XXX' # API ID
    SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'XXX' # 密钥
    
  10. 点击确认关闭对话框,之后在左侧菜单的凭据菜单内可以回到此处查看ID和密钥。现在点击左侧菜单的,会跳转到欢迎使用新版API库的界面,在其中找到Google+ API,如图所示:

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

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

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

<div class="social">
    <ul>
        <li class="google"><a href="{% 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视图如下:

@login_required
def edit(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技术。