第四章 创建一个社交网站

4 创建一个社交网站

在上一章中,你学习了如何创建站点地图和订阅,并且为博客应用构建了一个搜索引擎。在这一章中,你会开发一个社交应用。你会为用户创建登录,登出,编辑和修改密码的功能。你会学习如何为用户创建自定义的个人资料,并在网站中添加社交认证。

本章会涉及以下知识点:

  • 使用认证框架
  • 创建用户注册视图
  • 用自定义个人资料模型扩展User模型
  • python-social-auth添加社交认证

让我们从创建新项目开始。

4.1 创建一个社交网站项目

我们将会创建一个社交应用,让用户可以分享他们在Internet上发现的图片。我们需要为该项目构建以下元素:

  • 一个认证系统,用于用户注册,登录,编辑个人资料,修改或重置密码
  • 一个关注系统,允许用户互相关注
  • 显示分享的图片,并实现一个书签工具,让用户可以分享任何网站的图片
  • 每个用户的活动信息,让用户可以看到他关注的用户上传的内容

本章讨论第一点。

4.1.1 启动社交网站项目

打开终端,使用以下命令为项目创建一个虚拟环境,并激活:

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

终端会如下显示你激活的虚拟环境:

(bookmarks)laptop:~ zenx$

使用以下命令,在虚拟环境中安装Django:

pip install Django

执行以下命令创建一个新项目:

django-admin startproject bookmarks

创建初始项目结构之后,使用以下命令进入项目目录,并创建一个account的新应用:

cd bookmarks/
django-admin startapp account

通过把该应用添加到settings.py文件的INSTALLED_APPS中,来激活它。把它放在INSTALLED_APPS列表的最前面:

INSTALLED_APPS = (
    'account',
    # ...
)

执行下面的命令,同步INSTALLED_APPS设置中默认应用的模型到数据库中:

python manage.py migrate

接下来,我们用authentication框架在项目中构建一个认证系统。

4.2 使用Django认证框架

Django内置一个认证框架,可以处理用户认证,会话,权限和用户组。该认证系统包括常见的用户操作视图,比如登录,登出,修改密码和重置密码。

认证框架位于django.contrib.auth中,并且被其它Django contrib包使用。记住,你已经在第一章中使用过认证框架,为博客应用创建了一个超级用户,以便访问管理站点。

当你使用startproject命令创建新Django项目时,认证框架已经包括在项目的默认设置中。它由django.contrib.auth应用和以下两个中间件(middleware)类组成(这两个中间类位于项目的MIDDLEWARE_CLASSES设置中):

  • AuthenticationMiddleware:使用会话管理用户和请求
  • SessionMiddleware:跨请求处理当前会话

一个中间件是一个带有方法的类,在解析请求或响应时,这些方法在全局中执行。你会在本书的好几个地方使用中间件类。你会在第13章学习如何创建自定义的中间件。

该认证框架还包括以下模块:

  • User:一个有基础字典的用户模型;主要字段有:usernamepasswordemailfirst_namelast_nameis_active
  • Group:一个用于对用户分类的组模型。
  • Permission:执行特定操作的标识。

该框架还包括默认的认证视图和表单,我们之后会学习。

4.2.1 创建登录视图

我们从使用Django认证框架允许用户登录网站开始。我们的视图要执行以下操作来登录用户:

  1. 通过提交表单获得用户名和密码。
  2. 对比数据库中的数据,来验证用户。
  3. 检查用户是否激活。
  4. 用户登录,并开始一个认证的会话(authenticated session)。

首先,我们将创建一个登录表单。在account应用目录中创建forms.py文件,添加以下代码:

from django import forms

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

该表单用于在数据库用验证用户。注意,我们使用PasswordInput组件来渲染包括type="password"属性的HTML input元素。编辑account应用的views.py文件,添加以下代码:

from django.shortcuts import render
from django.http import 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(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请求调用user_login视图时,我们使用form = LoginForm()实例化一个新的登录表单,用于在模板中显示。当用户通过POST提交表单时,我们执行以下操作:

  1. 使用form = LoginForm(request.POST)实例化带有提交的数据的表单。
  2. 检查表单是否有效。如果无效,则在模板中显示表单错误(例如,用户没有填写某个字段)。
  3. 如果提交的数据有效,我们使用authenticate()方法,在数据库中验证用户。该方法接收usernamepassword参数,如果用户验证成功,则返回User对象,否则返回None。如果用户没有通过验证,我们返回一个原始的HttpResponse,显示一条消息。
  4. 如果用户验证成功,我们通过is_active属性检查用户是否激活。这是Django User模型的属性。如果用户没有激活,我们返回一个HttpResponse显示信息。
  5. 如果是激活的用户,我们在网站登录用户。我们调用login()方法,把用户设置在session中,并返回一条成功消息。

注意authenticatelogin之间的区别:authenticate()方法检查用户的认证信息,如果正确,则返回User对象;login()在当前session中设置用户。

现在,你需要为该视图创建URL模式。在account应用目录中创建urls.py文件,并添加以下代码:

from django.conf.urls import url
from . import views

urlpatterns = [
    # post views
    url(r'^login/$', views.user_login, name='login'),
]

编辑bookmarks项目目录中的urls.py文件,在其中包括account应用的URL模式:

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

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^account/', include('account.urls')),
]

现在可以通过URL访问登录视图了。是时候为该视图创建一个模板了。因为该项目还没有模板,所以你可以创建一个基础模板,在登录模板中扩展它。在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样式。该基础模板定义了titlecontent区域,可以被从它扩展的模板填充内容。

让我们为登录表单创建模板。打开account/login.html模板,添加以下代码:

{% extends "base.html" %}

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

{% block content %}
    <h1>Log-in</h1>
    <p>Please, user 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 %}模板标签进行CSRF保护。你在第2章学习了CSRF保护。

现在数据库中还没有用户。首先,你需要创建一个超级用户,访问管理站点来管理其他用户。打开命令行,执行python manage.py createsuperuser。填写必需的用户名,邮箱和密码。然后使用python manage.py runserver启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/。使用你刚创建的用户登录管理站点。你会看到Django管理站点中包括了Django认证框架的UserGroup模型,如下图所示:

通过管理站点创建一个新用户,并在浏览器中打开http://127.0.0.1:8000/account/login/。你会看到包括登录表单的模板:

现在,提交表单时不填其中一个字段。这时,你会看到表单是无效的,并显示错误信息,如下图所示:

如果你输入一个不存在的用户,或者错误的密码,你会看到一条Invalid login消息。

如果你输入有效的认证信息,会看到一条Authenticated successfully消息,如下图所示:

4.2.2 使用Django认证视图

Django在认证框架中包括了几个表单和视图,你可以直接使用。你已经创建的登录视图对于理解Django中的用户认证过程是一个很好的练习。然而,你在绝大部分情况下可以使用默认的Django认证视图。

Django提供了以下视图处理认证:

  • login:操作一个登录表单,并登录用户
  • logout:登出一个用户
  • logout_then_login:登出一个用户,并重定向用户到登录页面

Django提供以下视图处理修改密码:

  • password_change:操作一个修改用户密码的表单
  • password_change_done:修改密码后,显示成功页面

Django还提供以下视图用于重置密码:

  • password_reset:允许用户重置密码。它生成一个带令牌的一次性链接,并发送到用户的电子邮箱中。
  • password_reset_done:告诉用户,重置密码的邮件已经发送到他的邮箱中。
  • password_reset_confirm:让用户设置新密码。
  • password_reset_complete:用户重置密码后,显示成功页面。

创建一个带用户账户的网站时,这里列出的视图会节省你很多时间。你可以覆盖这些视图使用的默认值,比如需要渲染的模板的位置,或者视图使用的表单。

你可以在这里获得更多关于内置的认证视图的信息。

4.2.3 登录和登出视图

编辑account应用的urls.py文件,如下所示:

from django.conf.urls import url
from django.contrib.auth.views import login, logout, logout_then_login
from . import views

urlpatterns = [
    # previous login view
    # url(r'^login/$', views.user_login, name='login'),

    # login / logout urls
    url(r'^login/$', login, name='login'),
    url(r'^logout/$', logout, name='logout'),
    url(r'^logout-then-login/$', logout_then_login, name='logout_then_login'),
]

译者注:Django新版本中,URL模式使用方式跟旧版本不一样。

我们注释了之前为user_login视图创建的URL模式,使用了Django认证框架的login视图。

account应用的templates目录中创建一个registration目录。这是Django认证视图的默认路径,它期望你的认证模板在这个路径下。在新创建的目录中创建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, user 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  %}

这个login模板跟我们之前创建那个很像。Django默认使用django.contrib.auth.forms中的AuthenticationForm。该表单尝试验证用户,如果登录不成功,则抛出一个验证错误。这种情况下,如果认证信息出错,我们可以在模板中使用{% if form.errors %}查找错误。注意,我们添加了一个隐藏的HTML <input>元素,用于提交名为next的变量的值。当你在请求中传递一个next参数时(比如,http://127.0.0.1:8000/account/login/?next=/account/),这个变量首次被登录视图设置。

next参数必须是一个URL。如果指定了这个参数,Django登录视图会在用户登录后,重定义到给定的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  %}

用户登出之后,Django会显示这个模板。

为登录和登出视图添加URL模式和模板后,网站已经可以使用Django认证视图登录了。

注意,我们在urlconf中包含的logout_then_login视图不需要任何模板,因为它重定义到了登录视图。

现在我们开始创建一个新的视图,当用户登录账号时,用于显示用户的仪表盘。打开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装饰器装饰视图。该装饰器检查当前用户是否认证。如果是认证用户,它会执行被装饰的视图。如果不是认证用户,它会重定向用户到登录URL,并在登录URL中带上一个名为nextGET参数,该参数是用户试图访问的URL。通过这样的做法,当用户成功登录后,登录视图会重定向用户到用户登录之前试图访问的页面。记住,我们在登录模板的表单中添加了一个隐藏的<input>元素就是为了这个目的。

我们还定义了一个section变量。我们用这个变量跟踪用户正在查看网站的哪一部分(section)。多个视图可能对应相同的部分。这是定义每个视图对应的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 = [
    # ...
    url(r'^$', views.dashboard, name='dashboard'),
 ]

编辑项目的settings.py文件,添加以下代码:

from django.core.urlresolvers import reverse_lazy

LOGIN_REDIRECT_URL = reverse_lazy('dashboard')
LOGIN_URL = reverse_lazy('login')
LOGOUT_URL = reverse_lazy('logout')

这些设置是:

  • LOGIN_REDIRECT_URL:告诉Django,如果contrib.auth.views.login视图没有获得next参数时,登录后重定向到哪个URL
  • LOGIN_URL:重定向用户登录的URL(比如使用login_required装饰器)
  • LOGOUT_URL:重定向用户登出的URL

我们使用reverse_lazy(),通过URL的名字动态创建URL。reverse_lazy()函数跟reverse()函数一样逆向URL。当你需要在项目URL配置加载之前逆向URL时,可以使用reverse_lazy()

让我们总结一下,到现在为止,我们做了哪些工作:

  • 你在项目中添加了内置的Django认证登录和登出视图
  • 你为这两个视图创建了自定义模板,并定义了一个简单的视图,让用户登录后重定向到这个视图
  • 最后,你配置了设置,让Django默认使用这些URL

现在,我们需要把登录和登出链接到基础模板中,把所有功能串起来。

要做到这点,我们需要确定,无论当前用户是否登录,都能显示适当的链接。通过认证中间件,当前用户被设置在HttpRequest对象中。你可以通过request.user访问。即使用户没有认证,你也可以找到一个用户对象。一个未认证的用户在request中是一个AnonymousUser的实例。调用request.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 }},
            <a href="{% url "logout" %}">Logout</a>
        {% else %}
            <a href="{% url "login" %}">Log-in</a>
        {% endif %}
    </span>
</div>

正如你所看到的,我们只为认证的用户显示网站的菜单。我们还检查当前的section,通过CSS为相应的<li>项添加selected类属性来高亮显示菜单中的当前section。我们还显示用户的姓,如果是认证过的用户,还显示一个登出链接,否则显示登录链接。

现在,在浏览器中打开http://127.0.0.1:8000/account/login。你会看到登录页面。输入有效的用户名和密码,点击Log-in按钮,你会看到这样的页面:

因为My dashboardselected属性,所以你会看到它是高亮显示的。因为是认证过的用户,所以用户的姓显示在头部的右边。点击Logout链接,你会看到下面的页面:

在这个页面中,用户已经登出,所以你不能再看到网站的菜单。现在头部右边显示Log-in链接。

如果你看到的是Django管理站点的登出页面,而不是你自己的登出页面,检查项目的INSTALLED_APPS设置,确保django.contrib.adminaccount应用之后。这两个模板位于同样的相对路径中,Django目录加载器会使用第一个。

4.2.4 修改密码视图

用户登录我们的网站后,我们需要用户可以修改他们的密码。我们通过集成Django认证视图来修改密码。打开account应用的urls.py文件,添加以下URL模式:

from django.contrib.auth.views import password_change
from django.contrib.auth.views import password_change_done

# change password urls
urlpatterns = [
    url(r'^password-change/$', password_change, name='password_change'),
    url(r'^password_change/done/$', password_change_done, name='password_change_done'),
]

password_change视图会处理修改密码表单,password_change_done会在用户成功修改密码后显示一条成功消息。让我们为每个视图创建一个模板。

account应用的templates/registration/目录中创建password_change_form.html文件,添加以下代码:

{% extends "base.html" %}

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

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

该模板包括修改密码的表单。在同一个目录下创建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 %}

该模板只包括一条用户成功修改密码后显示的成功消息。

在浏览器中打开http://127.0.0.1:8000/account/password-change/。如果用户没有登录,浏览器会重定向到登录页面。当你认证成功后,你会看到下面的修改密码页面:

在表单中填写当前密码和新密码,点击Change按钮。你会看到以下成功页面:

登出后,使用新密码再次登录,确定所有功能都能正常工作。

4.2.5 重置密码视图

account应用的urls.py文件中,为重置密码添加以下URL模式:

from django.contrib.auth.views import password_reset
from django.contrib.auth.views import password_reset_done
from django.contrib.auth.views import password_rest_confirm
from django.contrib.auth.views import password_reset_complete

# restore password urls
url(r'^password-reset/$', password_reset, name='password_reset'),
url(r'^password-reset/done/$', password_reset_done, name='password_reset_done'),
url(r'^password-reset/confirm/(?P<uidb64>[-\w]+)/(?P<token>[-\w]+)/$', password_reset_confirm, name='password_reset_confirm'),
url(r'^password-reset/complete/$', password_reset_complete, 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">
        {{ form.as_p }}
        <p><input type="submit" value="Send e-mail"></p>
        {% csrf_token %}
    </form>
{% endblock %}

在同一个目录下创建password_reset_email.html文件,添加以下代码:

Someon asked for password reset for email {{ email }}. Fllow the link below:
{{ protocol }}://{{ domain }}/{% url "password_reset_form" uidb64=uid token=token %}
Your usernmae, 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">
            {{ formo.as_p }}
            {% csrf_token %}
            <p><input type="submit" value="Change my password" /></p>
        </form>
    {% else %}
        <p>The password reset link was invalid, possible because it has already been used. 
            Please request a new password reset.</p>
    {% endif %}
{% endblock  %}

我们检查提供的链接是否有效。Django重置页面视图设置该变量,并把它放在这个模板的上下文中。如果链接有效,我们显示重置密码表单。

创建另一个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 %}

最后,编辑account应用的registration/login.html模板,在<form>元素后面添加以下代码:

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

现在,在浏览器中打开htth://127.0.0.1:8000/account/login/,点击Forgotten your password?链接,你会看到以下链接:

此时,你需要在项目的settings.py文件中添加SMTP配置,让Django可以发送邮件。我们已经在第二章学习了如何添加邮件设置。但是在开发期间,你可以让Django在标准输出中写邮件,代替通过SMTP服务发送邮件。Django提供了一个邮件后台,可以把邮件输出到控制台。编辑项目的settings.py文件,添加下面这一行代码:

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

EMAIL_BACKEND设置指定用于发送邮件的类。

回到浏览器,输入已有用户的邮箱地址,点击Send a e-mail按钮。你会看到以下页面:

看一眼正在运行开发服务器的控制台,你会看到生成的邮件:

MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: lakerszhy@gmail.com
Date: Tue, 02 May 2017 03:50:20 -0000
Message-ID: <20170502035020.7440.93778@bogon>

Someon asked for password reset for email lakerszhy@gmail.com. Fllow the link below:
http://127.0.0.1:8000/account/password-reset/confirm/Mg/4lp-4b14906c833231658e9f/
Your usernmae, in case you've forgotten: antonio

邮件使用我们之间创建的password_reset_email.html模板渲染。重置密码的URL包括一个Django动态生成的令牌。在浏览器中打开连接,会看到以下页面:

设置新密码的页面对应password_reset_confirm.html模板。填写新密码并点击Change my password按钮。Django会创建一个新的加密密码,并保存到数据库中。你会看到一个成功页面:

现在你可以使用新密码再次登录。每个用于设置新密码的令牌只能使用一次。如果你再次打开收到的链接,会看到一条令牌无效的消息。

你已经在项目中集成了Django认证框架的视图。这些视图适用于大部分场景。如果需要不同的行为,你可以创建自己的视图。

4.3 用户注册和用户资料

现在,已存在的用户可以登录,登出和修改密码,如果用户忘记密码,可以重置密码。现在,我们需要创建视图,用于游客创建账户。

4.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("Passwords don't match.")
        return cd['password2']

我们为User模型创建了一个模型表单。在表单中,我们只包括了模型的usernamefirst_nameemail字段。这些字段会根据相应的模型字段验证。例如,如果用户选择了一个已存在的用户名,会得到一个验证错误。我们添加了两个额外字段:passwordpassword2,用来设置密码和确认密码。我们定义了clean_password2()方法,检查两次输入的密码是否一致,如果不一致,则让表单无效。当我们调用表单的is_valid()方法验证时,这个检查会执行。你可以为任何表单字段提供clean_<fieldname>()方法,清理特定字段的值或抛出表单验证错误。表单还包括一个通用的clean()方法验证整个表单,验证相互依赖的字段时非常有用。

Django还在django.contrib.auth.forms中提供了UserCreationForm表单供你使用,这个表单跟我们刚创建的表单类似。

编辑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():
            # Create a new user object but avoid saving it yet
            new_user = user_form.save(commit=False)
            # Set the chosen password
            new_user.set_password(user_form.cleaned_data['password'])
            # Save the User object
            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})

这个创建用户账户的视图非常简单。为了安全,我们使用User模型的set_password()方法处理加密保存,来代替保存用户输入的原始密码。

现在编辑account应用的urls.py文件,添加以下URL模式:

url(r'^register/$', views.register, name='register')

最后,我们在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">
        {{ user_form.as_p }}
        {% csrf_token %}
        <p><input type="submit" value="Create my account"></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按钮。如果所有字段都有效,则会创建用户,你会看到下面的成功消息:

点击log in链接,输入你的用户名和密码验证能否访问你的账户。

现在,你还可以在登录模板中添加注册链接。编辑registration/login.html模板,把这行代码:

<p>Please, user the following form to log-in</p>

替换为:

<p>Please, user the following form to log-in. 
If you don't have an account <a href="{% url "register" %}">register here</a></p>

我们可以通过登录页面访问注册页面了。

4.3.2 扩展User模型

当你必须处理用户账户时,你会发现Django认证框架的User模型适用于常见情况。但是User模型有非常基础的字段。你可能希望扩展User模型包含额外的数据。最好的方式是创建一个包括所有额外字段的个人资料模型,并且与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)
    date_of_birth = models.DateField(blank=True, null=True)
    photo = models.ImageField(upload_to='users/%Y/%m/%d', blank=True)

    def __str__(self):
        return 'Pofile for User {}'.format(self.user.username)

为了让代码保持通用性,请使用get_user_model()方法检索用户模型。同时,定义模型和用户模型之间的关系时,使用AUTH_USER_MODEL设置引用用户模型,而不是直接引用该用户模型。

一对一的user字段允许我们用用户关联个人资料。photo字段是一个ImageField字段。你需要安装PIL(Python Imaging Library)或Pillow(PIL的一个分支)Python包来管理图片。在终端中执行以下命令安装Pillow:

pip install Pillow

为了在Django开发服务器中提供多媒体文件上传功能,需要在项目的settings.py文件中添加以下设置:

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

MEDIA_URL是用户上传的多媒体文件的基URL,MEDIA_ROOT是多媒体文件的本地路径。我们根据项目路径动态构建该路径,让代码更通用。

现在,编辑bookmarks项目的主urls.py文件,如下所示修改代码:

from django.conf import settings
from django.conf.urls.static import static

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

这样,Django开发服务器将在开发过程中负责多媒体文件服务。

static()帮助函数只适用于开发环境,不适合生产环境。永远不要在生产环境使用Django为静态文件提供服务。

打开终端执行以下命令,为新模型创建数据库迁移:

python manage.py makemigrations

你会得到这样的输出:

Migrations for 'account':
  account/migrations/0001_initial.py
    - Create model Profile

接着使用以下命令同步数据库:

python manage.py migrate

你会看到包括下面这一样的输出:

Applying account.0001_initial... OK

编辑account应用的admin.py文件,在管理站点注册Profile模型,如下所示:

from .models import Profile

class ProfileAdmin(admin.ModelAdmin):
    list_display = ('user', 'date_of_birth', 'photo')

admin.site.register(Profile, ProfileAdmin)

使用python manage.py runserver命令运行开发服务器。现在,你会在项目的管理站点看到Profile模型,如下图所示:

现在,我们将让用户在网站上编辑个人资料。在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')

这些表单的作用是:

  • UserEditForm:允许用户编辑存在内置的User模型中的姓,名和邮箱。
  • ProfileEditForm:允许用户编辑存在自定义的Profile模型中的额外数据。用户可以编辑出生日期,并上传一张图片。

编辑account应用的views.py文件,导入Profile模型:

from .models import Profile

register视图的new_user.save()下面添加以下代码:

# Create the user profile
profile = 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存储额外的个人数据。我们检查两个表单的is_valid()方法返回True来验证提交的数据。在这里,我们保持两个表单,用来更新数据库中相应的对象。

account应用的urls.py文件中添加以下URL模式:

url(r'^edit/$', views.edit, name='edit')

最后,在templates/account/目录中,为该视图创建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">
        {{ 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 profiles</a> 
    or <a href="{% url "password_change" %}">change your password</a>. 
</p>

用户现在可以通过仪表盘访问编辑个人资料的表单。

4.3.2.1 使用自定义User模型

Django还提供了方式,可以用自定义模型代替整个User模型。你的用户类应从Django的AbstractUser类继承,它作为一个抽象模型,提供了默认用户的完整实现。你可以在这里阅读更多关于这个模型的信息。

使用自定义用户模型会有更多的灵活性,但它也可能给一些需要与User模型交互的可插拔应用应用的集成带来一定的困难。

4.3.3 使用消息框架

处理用户动作时,你可能想要通知用户动作的结果。Django内置一个消息框架,允许你显示一次性提示。该消息框架位于django.contrib.message中,当你用python manage.py startproject创建新项目时,它默认包括在settings.pyINSTALLED_APPS列表中。你注意到,设置文件的MIDDLEWARE_CLASSES设置列表中,包括一个名为django.contrib.message.middleware.MessageMiddleware的中间件。该消息框架提供了一种简单的方式来给用户添加消息。消息存储在数据库中,并会在用户下次请求时显示。你可以通过导入消息模块,使用简单的快捷方式添加新消息,来在视图中使用消息框架,如下所示:

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

你可以使用add_message()方法,或者以下任何一个快捷方法创建新消息:

  • success():动作执行成功后显示成功消息
  • info():信息消息
  • waring():还没有失败,但很可能马上失败
  • error():一个不成功的操作,或某些事情失败
  • debug():调试信息,会在生产环境移除或忽略

让我们显示消息给用户。因为消息框架对项目来说是全局的,所以我们可以在基础模板中显示消息给用户。打开base.html模板,在id为header和content的<div>元素之间添加以下代码:

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

消息框架包括一个上下文处理器(context processor),它会添加messages变量到请求上下文中。因此,你可以在模板使用该变量显示当前消息。

现在,让我们修改edit视图来使用消息框架。编辑account应用的views.py文件,如下修改edit视图:

from django.contrib import messages

@login_required
def edit(request):
    if request.method == 'POST':
    # ...
        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)
        # ...

当用户成功更新个人资料后,我们添加一条成功消息。如果任何一个表单无效,我们添加一条错误消息。

在浏览器中打开http://127.0.0.1:8000/account/edit/,并编辑你的个人资料。当个人资料更新成功后,你会看到以下消息:

当表单无效时,你会看到以下消息:

4.4 创建自定义认证后台

Django允许你针对不同来源进行身份验证。AUTHENTICATION_BACKENDS设置包括了项目的认证后台列表。默认情况下,该设置为:

('django.contrib.auth.backends.ModelBackend',)

默认的ModelBackend使用django.contrib.authUser模型,验证数据库中的用户。这适用于大部分项目。但是你可以创建自定义的后台,来验证其它来源的用户,比如一个LDAP目录或者其它系统。

你可以在这里阅读更多关于自定义认证的信息。

一旦你使用django.contrib.auth中的authenticate()函数,Django会一个接一个尝试AUTHENTICATION_BACKENDS中定义的每一个后台来验证用户,直到其中一个验证成功。只有所有后台都验证失败,才不会在站点中验证通过。

Django提供了一种简单的方式来定义自己的认证后台。一个认证后台是提供了以下两个方法的类:

  • authenticate():接收用户信息作为参数,如果用户认证成功,则返回True,否则返回False。
  • get_user():接收用户ID作为参数,并返回一个User对象。

创建一个自定义认证后台跟编写一个实现这两个方法的Python类一样简单。我们会创建一个认证后台,让用户使用邮箱地址代替用户名验证。

account应用目录中创建一个authentication.py文件,添加以下代码:

from django.contrib.auth.models import User

class EmailAuthBackend:
    """
    Authenticates using e-mail account.
    """
    def authenticate(self, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            retur None

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

这是一个很简单的认证后台。authenticate()方法接收usernamepassword作为可选参数。我们可以使用不同的参数,但我们使用usernamepassword确保后台可以立即在认证框架中工作。上面的代码完成以下工作:

  • authenticate():我们尝试使用给定的邮箱地址检索用户,并用User模型内置的check_password()方法检查密码。该方法会处理密码哈希化,并比较给定的密码和数据库中存储的密码。
  • get_user():我们通过user_id参数获得一个用户。在用户会话期间,Django使用认证用户的后台来检索User对象。

编辑项目的settings.py,添加以下设置:

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
)

我们保留了默认的ModelBackend,使用用户名和密码认证,并包括了自己的基于邮箱地址的认证后台。现在,在浏览器中打开http://127.0.0.1/8000/account/login/。记住,Django会试图使用每一个后台验证用户,所以你现在可以使用用户名或邮箱账号登录。

AUTHENTICATION_ BACKENDS设置中列出的后端顺序很重要。如果同样的信息对于多个后台都有效,Django会在第一个成功验证用户的后台停止。

4.5 为网站添加社交认证

你可能还想为网站添加社交认证,比如使用Facebook,Twitter或Google服务认证。Python-socail-auth是一个Python模块,可以简化添加社交认证过程。通过这个模块,你可以让用户使用其他服务的账户登录你的网站。

译者注:从2016年12月3日开始,这个模块迁移到了Python Social Auth。原书的内容已经过时,所以就不翻译了。

4.6 总结

在本章中,你学习了如何在网站中构建认证系统和创建自定义用户资料。你还在网站中添加了社交认证。

下一章中,你会学习如何创建一个图片书签系统,生成图片的缩略图,以及创建AJAX视图。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容