澳门新萄京官方网站-www.8455.com-澳门新萄京赌场网址

澳门新萄京官方网站:Django用户登录与注册系统

2019-11-24 作者:www.8455.com   |   浏览(178)

一、创建项目

澳门新萄京官方网站 1

需求:开发一个用户登录功能

【第一部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第二部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第三部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第四部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第五部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第六部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记【第七部分-django论坛从搭建到部署】一个完整的Django入门指南学习笔记

1.1.创建项目和app

django-admin startproject mysite_login

python manage.py startapp login

Part5

1、写一个登录功能

  • 译者:wangzhihao、CasualJi
  • 校稿:liuzhijun
  • 原文:
  • 侵权留言删

1.2.设置时区和语言

Django默认使用美国时间和英语,在项目的settings文件中,如下所示:

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

我们把它改为亚洲/上海时间和中文

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

教程:原文地址

前端页面

首先从前端页面开始,打开.../sign/templates/index.html,开发一个表单

<html>
<head>
    <title>Django Page</title>
</head>
<body>
<h1>发布会管理</h1>
<form method="post">
    <input name="username" type="text" placeholder="请输入用户名"><br>
    <input name="password" type="password" placeholder="请输入密码"><br>
    <button id="btn" type="submit">登录</button>
</form>
</body>
</html>  

启动Django服务,访问: 如图所示

澳门新萄京官方网站 2

登录,弹出“CSRF verification failed. Request aborted.”,这是因为CSRF(Cross-site request forgery)跨站请求伪造,Django针对CSRF的保护措施是在生成的每个表单中放置一个自动生成的令牌,通过这个令牌判断POST请求是否来自同一个网站。这里要用到Django的“标签模板”添加CSRF令牌。

......
<form method="post">
    <input name="username" type="text" placeholder="请输入用户名"><br>
    <input name="password" type="password" placeholder="请输入密码"><br>
    <button id="btn" type="submit">登录</button>
    {% csrf_token %}
</form>
......

 刷新,重新登录,错误提示消失了。

也可以注释掉CSRF请求,在../guest/settings.py中

'django.middleware.csrf.CsrfViewMiddleware',

这一章节将会全面介绍 Django 的身份认证系统,我们将实现注册、登录、注销、密码重置和密码修改的整套流程。

1.3.启动

运行测试一下工程,在本机的浏览器中访问http://127.0.0.1:8000/

澳门新萄京官方网站 3

介绍

这个教程将会谈谈和Django身份验证系统有关的一切。我们将完成一套完整的流程:注册,登录,登出,密码重置,密码修改。

你也将获知关于如何保护一些视图以防不合法的用户以及如何给已登录的提供信息的简介。

在以下部分,你将看到一些将在本教程中实现的和身份验证有关线框图。之后,你将看到一个全新Django App的初始步骤。至今为止我们在开发一个名叫boards的应用。不过所有身份认证相关的内容适用于不同的应用,这样能实现对代码更良好的组织。

澳门新萄京官方网站 4

处理登录请求

 1、form表单的action属性指定提交的路径。打开inde.html文件,添加如下:

form method="post" action="/login_action/">

2、添加路由,打开../guest/urls.py

url(r'^login_action/$', views.login_action),

3、添加视图,打开sign/views.py,添加login_action函数

from django.http import HttpResponse
from django.shortcuts import render


# Create your views here.
def index(request):
    return render(request, "index.html")


def login_action(request):
    if request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        if username == 'admin' and password == 'admin123':
            return HttpResponse('login success!')
        else:
            return render(request, 'index.html', {'error': 'username or password error!'})

4、前端添加返回错误信息的地方,打开inde.html,使用Django的模板

<form method="post" action="/login_action/">
    <input name="username" type="text" placeholder="请输入用户名"><br>
    <input name="password" type="password" placeholder="请输入密码"><br>
    {{ error }} <br>
    <button id="btn" type="submit">登录</button>
    {% csrf_token %}
</form>

{{ error }} 对应render返回的字典中的key,即“error”,登录失败页面中显示对应的value,即‘username or password error!’
5、刷新页面,登录。

同时你还会了解到如何保护某些试图以防未授权的用户访问,以及如何访问已登录用户的个人信息。

 二、设计数据模型

线框图

我们需要更新这个应用的线框图。首先,我们为头部菜单增加新的选项。如果当前用户没有经过身份认证,我们应该显示两个按钮:“注册”和“登录”。

澳门新萄京官方网站 5

Figure 1: Top menu for not authenticated users.

如果当前用户已经通过身份认证,我们应该显示他们的名字,沿着他们的名字显示带有“我的账户”,“修改密码”,“登出”这三个选项的下拉框。

澳门新萄京官方网站 6

Figure 2: Top menu for authenticated users.

在登陆页面,我们需要张带有“用户名”和“密码”的表单,一个有着主要功能的按钮(登录)和两个可选路径:“注册”和“重置密码”。

澳门新萄京官方网站 7

Figure 3: Log in page

在注册界面,我们应该有一张带有四个字段的表单:“用户名”,“电邮”,“密码”,“确认密码”。用户也应能够跳转到登陆界面。

澳门新萄京官方网站 8

Figure 4: Sign up page

在密码重置界面,我们只需要带有“电子邮箱”的表单。

澳门新萄京官方网站 9

Figure 5: Password reset

然后,在点击了一个特殊的链接后(注:指的是“send password reset email”这个按钮)用户将会重定向到一个他们可以设置新密码的页面。

澳门新萄京官方网站 10

Figure 6: Change password


将登录请求跳转到指定页面

1、创建../templates/event_manage.html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event Manage Page</title>
</head>
<body>
    <h1> Login Success! </h1>
</body>
</html>

2、修改views.py文件

from django.http import HttpResponseRedirect
from django.shortcuts import render


# Create your views here.
def index(request):
    return render(request, "index.html")


def login_action(request):
    if request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        if username == 'admin' and password == 'admin123':
            return HttpResponseRedirect('/event_manage/')
        else:
            return render(request, 'index.html', {'error': 'username or password error!'})


# 发布会管理
def event_manage(request):
    return render(request, "event_manage.html")

HttpResponseRedirect类可以对路径重定向,从而将登录请求成功的请求指向/event_manage/目录
3、添加路由

url(r'^event_manage/$', views.event_manage),

4、刷新,登录

在接下来的部分,你会看到一些和身份验证有关线框图,将在本教程中实现。之后是一个全新Django 应用的初始化设置。至今为止我们一直在一个名叫 boards 的应用中开发。不过,所有身份认证相关的内容都将在另一个应用中,这样能更良好的组织代码。

 2.1.数据库模型设计

 作为一个用户登录和注册项目,需要保存的都是各种用户的相关信息。很显然,我们至少需要一张用户表User,在用户表里需要保存下面的信息:

  • 用户名
  • 密码
  • 邮箱地址
  • 性别
  • 创建时间

 进入login/models.py,代码如下

# login/models.py

from django.db import models


class User(models.Model):
    '''用户表'''

    gender = (
        ('male','男'),
        ('female','女'),
    )

    name = models.CharField(max_length=128,unique=True)
    password = models.CharField(max_length=256)
    email = models.EmailField(unique=True)
    sex = models.CharField(max_length=32,choices=gender,default='男')
    c_time = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['c_time']
        verbose_name = '用户'
        verbose_name_plural = '用户'

各字段含义:

  • name必填,最长不超过128个字符,并且唯一,也就是不能有相同姓名;
  • password必填,最长不超过256个字符(实际可能不需要这么长);
  • email使用Django内置的邮箱类型,并且唯一;
  • 性别使用了一个choice,只能选择男或者女,默认为男;
  • 使用__str__帮助人性化显示对象信息;
  • 元数据里定义用户按创建时间的反序排列,也就是最近的最先显示;

注意:这里的用户名指的是网络上注册的用户名,不要等同于现实中的真实姓名,所以采用了唯一机制。如果是现实中可以重复的人名,那肯定是不能设置unique的。

初始化安装

为了去管理所有信息,我们可以将它分解成为一个不同的应用。在项目根目录,也就是manage.py脚本所在的界面下,运行以下命令来创建一个新的应用:

django-admin startapp accounts

这个项目结构应该是这样:

myproject/
 |-- myproject/
 |    |-- accounts/     <-- our new django app!
 |    |-- boards/
 |    |-- myproject/
 |    |-- static/
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

下一步,在settings.py中的INSTALLED_APPS中包含accounts

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'widget_tweaks',

    'accounts',
    'boards',
]

从现在开始,我们将对 accounts 应用 进行开发。


 Cookie的使用

修改.../sign/views.py文件:

def login_action(request):
    if request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        if username == 'admin' and password == 'admin123':
            response = HttpResponseRedirect('/event_manage/')
            # 添加cookie: key,value,有效期
            response.set_cookie('user', username, 3600)
            return response
        else:
            return render(request, 'index.html', {'error': 'username or password error!'})


# 发布会管理
def event_manage(request):
    username = request.COOKIES.get('user', '')  # 读取浏览器cookie
    return render(request, "event_manage.html", {"user": username})

修改.../templates/event_manage.html页面,添加<div>标签来显示用户登录的用户名:

<div style="float: right;">
        <a>嘿!{{ user }} 欢迎</a><hr/>
</div>

刷新,重新登录,如下图所示:

澳门新萄京官方网站 11

澳门新萄京官方网站 12

 2.2.设置数据库为Mysql

在settings.py修改

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django',        #数据库名字
        'USER': 'root',          #账号
        'PASSWORD': '123456',      #密码
        'HOST': '127.0.0.1',    #IP
        'PORT': '3306',                   #端口
    }
}

init.py里面导入pymysql模块

# login/init.py

import pymysql
pymysql.install_as_MySQLdb()

注册

澳门新萄京官方网站 13

让我们从创建一个注册视图着手。首先在urls.py中创建一个新的路由。

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

from accounts import views as accounts_views
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^signup/$', accounts_views.signup, name='signup'),
    url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'),
    url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'),
    url(r'^admin/', admin.site.urls),
]

注意我们使用另一种方式如何从accounts中引入views模块

from accounts import views as accounts_views

我们使用了别名,因为否则的话,他将会与boards'的视图产生冲突。我们可以改良url.py 的设计。但是现在,让我们关注一下身份验证的特征。

现在编辑在accounts应用中的views.py文件,并且创建一个新的名为signup的视图。

from django.shortcuts import render

def signup(request):
    return render(request, 'signup.html')

创建一个新视图,名为signup.html

templates/signup.html

{% extends 'base.html' %}

{% block content %}
  <h2>Sign up</h2>
{% endblock %}

在浏览器中打开链接:http://127.0.0.1:8000/signup/ ,检查它是否已经生效。

是时候写一些单元测试了:

accounts/tests.py

from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import signup

class SignUpTests(TestCase):
    def test_signup_status_code(self):
        url = reverse('signup')
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_signup_url_resolves_signup_view(self):
        view = resolve('/signup/')
        self.assertEquals(view.func, signup)

如果这个链接/signup/ 返回了正确的视图,那么代表状态码200测试成功。

python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..................
----------------------------------------------------------------------
Ran 18 tests in 0.652s

OK
Destroying test database for alias 'default'...

对于身份验证的视图(注册,登录,重置密码,等等)我们不需要使用顶部栏和面包屑。我们可以继续使用base.html模板,只是它需要一些调整。

templates/base.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Django Boards{% endblock %}</title>
    <link href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/app.css' %}">
    {% block stylesheet %}{% endblock %}  <!-- HERE -->
  </head>
  <body>
    {% block body %}  <!-- HERE -->
      <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
          <a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
        </div>
      </nav>
      <div class="container">
        <ol class="breadcrumb my-4">
          {% block breadcrumb %}
          {% endblock %}
        </ol>
        {% block content %}
        {% endblock %}
      </div>
    {% endblock body %}  <!-- AND HERE -->
  </body>
</html>

我在base.html模板中标记了新的注释。代码块{% block stylesheet %}{% endblock %}将被用来引入额外的CSS文件,这对于页面来说,更清晰明确。

代码块{% block body %}用来包含整个HTML文件,我们可以用它来使一个空白的文档借用base.html的头部。注意一下我们是如何命名代码块尾部{% endblock body %}
像在本案例中这样命名是一种合理命名结束标签的方式,所以这使人们更容易找到代码块到哪结束。

现在,在signup.html模板中,我们可以使用{% block body %}来代替{% block content %}

templates/signup.html

{% extends 'base.html' %}

{% block body %}
  <h2>Sign up</h2>
{% endblock %}

澳门新萄京官方网站 14

澳门新萄京官方网站 15

是时候创建注册表单了。Django有一种名为UserCreationForm的内置表单。让我们来使用它:

accounts/views.py

from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render

def signup(request):
    form = UserCreationForm()
    return render(request, 'signup.html', {'form': form})

templates/signup.html

{% extends 'base.html' %}

{% block body %}
  <div class="container">
    <h2>Sign up</h2>
    <form method="post" novalidate>
      {% csrf_token %}
      {{ form.as_p }}
      <button type="submit" class="btn btn-primary">Create an account</button>
    </form>
  </div>
{% endblock %}

澳门新萄京官方网站 16

看上去有点混乱,对吗?我们可以使用我们的form.html模板来使他看上去更整洁。

templates/signup.html

{% extends 'base.html' %}

{% block body %}
  <div class="container">
    <h2>Sign up</h2>
    <form method="post" novalidate>
      {% csrf_token %}
      {% include 'includes/form.html' %}
      <button type="submit" class="btn btn-primary">Create an account</button>
    </form>
  </div>
{% endblock %}

澳门新萄京官方网站 17

啊,差不多了。目前,我们的局部模板form.html 显示了一些原生的HTML。这是一种安全的特征。由于默认的Django认为所有的字符串都是不安全的,避免任何可能引起错误的字符。但是在这个案例中,我们可以信任它!

templates/includes/form.html

% load widget_tweaks %}

{% for field in form %}
  <div class="form-group">
    {{ field.label_tag }}

    <!-- code suppressed for brevity -->

    {% if field.help_text %}
      <small class="form-text text-muted">
        {{ field.help_text|safe }}  <!-- new code here -->
      </small>
    {% endif %}
  </div>
{% endfor %}

基本上,我们在之前的模板上的field.help_text增加一个选项safe:
{{ field.help_text|safe }}.

保存form.html文件,然后再次检查注册页面:

澳门新萄京官方网站 18

现在,让我们实现注册视图上的业务逻辑:

accounts/views.py

from django.contrib.auth import login as auth_login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render, redirect

def signup(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('home')
    else:
        form = UserCreationForm()
    return render(request, 'signup.html', {'form': form})

这个基本的表单处理有一个小细节:login方法(重命名为auth_login 来避免与内置login视图发生冲突)

笔记:我重命名这个 login 方法为 auth_login, 但是之后我意识到Django1.11为login 视图提供了一个基于类的视图,LoginView,所以是没有出现命名冲突的风险的。

在更老的版本中,有 auth.login 和 auth.views.login,这会引起一些混乱,因为一个是用户登录的方法,一个是视图。

长话短说:你可以使用 login 作为方法名,这不会引起任何问题。

如果这个表单是有效的,将会通过user=form.save()来创建一个用户实例。这个被创建的用户然后作为一个参数被传进auth_login方法,手动地(?)进行身份验证。之后,视图将会重定向到首页。

让我们试一试。首先,尝试提交一些无效的数据。空表单,未匹配的字段或者已存在用户名:

澳门新萄京官方网站 19

现在,填写表单然后提交,检查用户是否被创建并且重定向到首页:

澳门新萄京官方网站 20

 session的使用

1、修改一下代码

修改.../sign/views.py文件,在login_action函数中,将:
response.set_cookie('user',username,3600)
替换为:
request.session['user']=username #将session信息记录到浏览器
在event_manage函数中,将:
username=request.COOKIES.get('user','')
替换为:
username=request.session.get('user','') #读取浏览器session

2、创建一个存session的数据库sqlite

python manage.py migrate

3、刷新,登录

线框图

我们必须更新一下应用的线框图。首先,我们需要在顶部菜单添加一些新选项,如果用户未通过身份验证,应该有两个按钮:分别是注册和登录按钮。

澳门新萄京官方网站 21

如果用户已经通过身份认证,我们应该显示他们的名字,和带有“我的账户”,“修改密码”,“登出”这三个选项的下拉框

澳门新萄京官方网站 22

在登录页面,我们需要一个带有usernamepassword的表单, 一个登录的按钮和可跳转到注册页面和密码重置页面的链接。

澳门新萄京官方网站 23

在注册页面,我们应该有包含四个字段的表单:username,email address, passwordpassword confirmation。同时,也应该有一个能够访问登录页面链接。

澳门新萄京官方网站 24

在密码重置页面上,只有email address字段的表单。

澳门新萄京官方网站 25

之后,用户在点击带有特殊token的重置密码链接以后,用户将被重定向到一个页面,在那里他们可以设置新的密码。

澳门新萄京官方网站 26

要管理这些功能,我们可以在另一个应用中将其拆解。在项目根目录中的 manage.py 文件所在的同一目录下,运行以下命令以创建一个新的app:

django-admin startapp accounts

项目的目录结构应该如下:

myproject/ |-- myproject/ | |-- accounts/ <-- 新创建的app | |-- boards/ | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

下一步,在 settings.py 文件中将 accounts app 添加到INSTALLED_APPS

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'widget_tweaks', 'accounts', 'boards',]

现在开始,我们将会在 accounts 这个app下操作。

澳门新萄京官方网站 27

我们从创建注册视图开始。首先,在urls.py 文件中创建一个新的路由:

myproject/urls.py

from django.conf.urls import urlfrom django.contrib import adminfrom accounts import views as accounts_viewsfrom boards import viewsurlpatterns = [ url(r'^$', views.home, name='home'), url(r'^signup/$', accounts_views.signup, name='signup'), url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'), url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'), url(r'^admin/', admin.site.urls),]

注意,我们以不同的方式从accounts app 导入了views模块

from accounts import views as accounts_views

我们给 accounts 的 views 指定了别名,否则它会与boardsviews 模块发生冲突。稍后我们可以改进urls.py 的设计,但现在,我们只关注身份验证功能。

现在,我们在 accounts app 中编辑 澳门新萄京官方网站:Django用户登录与注册系统,django论坛从搭建到部署。views.py,新创建一个名为signup的视图函数:

accounts/views.py

from django.shortcuts import renderdef signup: return render(request, 'signup.html')

接着创建一个新的模板,取名为signup.html

templates/signup.html

{% extends 'base.html' %}{% block content %} <h2>Sign up</h2>{% endblock %}

在浏览器中打开 ,看看是否程序运行了起来:

澳门新萄京官方网站 28

接下来写点测试用例:

accounts/tests.py

from django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCasefrom .views import signupclass SignUpTests: def test_signup_status_code: url = reverse response = self.client.get self.assertEquals(response.status_code, 200) def test_signup_url_resolves_signup_view: view = resolve('/signup/') self.assertEquals(view.func, signup)

测试状态码(200=success)以及 URL /signup/ 是否返回了正确的视图函数。

python manage.py test

Creating test database for alias 'default'...System check identified no issues (0 silenced)...................----------------------------------------------------------------------Ran 18 tests in 0.652sOKDestroying test database for alias 'default'...

对于认证视图(注册、登录、密码重置等),我们不需要顶部条和breadcrumb导航栏,但仍然能够复用base.html 模板,不过我们需要对它做出一些修改,只需要微调:

templates/base.html

{% load static %}<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>{% block title %}Django Boards{% endblock %}</title> <link href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet"> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/app.css' %}"> {% block stylesheet %}{% endblock %} <!-- 这里 --> </head> <body> {% block body %} <!-- 这里 --> <nav > <div > <a href="{% url 'home' %}">Django Boards</a> </div> </nav> <div > <ol > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div> {% endblock body %} <!-- 这里 --> </body></html>

我在 base.html 模板中标注了注释,表示新加的代码。块代码{% block stylesheet %}{% endblock %} 表示添加一些额外的CSS,用于某些特定的页面。

代码块{% block body %} 包装了整个HTML文档。我们可以只有一个空的文档结构,以充分利用base.html头部。注意,还有一个结束的代码块{% endblock body %},在这种情况下,命名结束标签是一种很好的实践方法,这样更容易确定结束标记的位置。

现在,在signup.html模板中,我们使用{% block body %}代替了 {% block content %}

templates/signup.html

{% extends 'base.html' %}{% block body %} <h2>Sign up</h2>{% endblock %}

澳门新萄京官方网站 29澳门新萄京官方网站 30

是时候创建注册表单了。Django有一个名为 UserCreationForm的内置表单,我们就使用它吧:

accounts/views.py

from django.contrib.auth.forms import UserCreationFormfrom django.shortcuts import renderdef signup: form = UserCreationForm() return render(request, 'signup.html', {'form': form})

templates/signup.html

{% extends 'base.html' %}{% block body %} <div > <h2>Sign up</h2> <form method="post" novalidate> {% csrf_token %} {{ form.as_p }} <button type="submit" >Create an account</button> </form> </div>{% endblock %}

澳门新萄京官方网站 31

看起来有一点乱糟糟,是吧?我们可以使用form.html模板使它看起来更好:

templates/signup.html

{% extends 'base.html' %}{% block body %} <div > <h2>Sign up</h2> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Create an account</button> </form> </div>{% endblock %}

澳门新萄京官方网站 32

哈?非常接近目标了,目前,我们的form.html部分模板显示了一些原生的HTML代码。这是django出于安全考虑的特性。在默认的情况下,Django将所有字符串视为不安全的,会转义所有可能导致问题的特殊字符。但在这种情况下,我们可以信任它。

templates/includes/form.html

{% load widget_tweaks %}{% for field in form %} <div > {{ field.label_tag }} <!-- code suppressed for brevity --> {% if field.help_text %} <small > {{ field.help_text|safe }} <!-- 新的代码 --> </small> {% endif %} </div>{% endfor %}

我们主要在之前的模板中,将选项safe 添加到field.help_text: {{ field.help_text|safe }}.

保存form.html文件,然后再次检测注册页面:

澳门新萄京官方网站 33

现在,让我们在signup视图中实现业务逻辑:

accounts/views.py

from django.contrib.auth import login as auth_loginfrom django.contrib.auth.forms import UserCreationFormfrom django.shortcuts import render, redirectdef signup: if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() auth_login(request, user) return redirect else: form = UserCreationForm() return render(request, 'signup.html', {'form': form})

表单处理有一个小细节:login函数重命名为auth_login以避免与内置login视图冲突)。

(编者注:我重命名了login 函数重命名为auth_login ,但后来我意识到Django1.11对登录视图LoginView具有基于类的视图,因此不存在与名称冲突的风险。在比较旧的版本中,有一个auth.loginauth.view.login ,这会导致一些混淆,因为一个是用户登录的功能,另一个是视图。

简单来说:如果你愿意,你可以像login 一样导入它,这样做不会造成任何问题。)

如果表单是有效的,那么我们通过user=form.save()创建一个User实例。然后将创建的用户作为参数传递给auth_login函数,手动验证用户。之后,视图将用户重定向到主页,保持应用程序的流程。

让我们来试试吧,首先,提交一些无效数据,无论是空表单,不匹配的字段还是已有的用户名。

澳门新萄京官方网站 34

现在填写表单并提交,检查用户是否已创建并重定向到主页。

澳门新萄京官方网站 35

  2.3.数据库迁移

注册app

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'login',
]

迁移到数据库

python manage.py makemigrations

python manage.py migrate

在模板中引用被验证的用户

我们怎么知道它已经生效了呢?我们可以编辑base.html模板来将用户名添加到顶部栏:

templates/base.html

{% block body %}
  <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
    <div class="container">
      <a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">

      </button>
      <div class="collapse navbar-collapse" id="mainMenu">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item">
            <a class="nav-link" href="#">{{ user.username }}</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>

  <div class="container">
    <ol class="breadcrumb my-4">
      {% block breadcrumb %}
      {% endblock %}
    </ol>
    {% block content %}
    {% endblock %}
  </div>
{% endblock body %}

澳门新萄京官方网站 36

2、Django认证系统

在模板中引用已认证的用户

我们要怎么才能知道上述操作是否有效呢?我们可以编辑base.html模板来在顶部栏上添加用户名称:

templates/base.html

{% block body %} <nav > <div > <a href="{% url 'home' %}">Django Boards</a> <button type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">  </button> <div > <ul > <li > <a href="#">{{ user.username }}</a> </li> </ul> </div> </div> </nav> <div > <ol > {% block breadcrumb %} {% endblock %} </ol> {% block content %} {% endblock %} </div>{% endblock body %}

澳门新萄京官方网站 37

三、admin后台

测试注册视图

让我们来改善一下单元测试:

accounts/tests.py

from django.contrib.auth.forms import UserCreationForm
from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import signup

class SignUpTests(TestCase):
    def setUp(self):
        url = reverse('signup')
        self.response = self.client.get(url)

    def test_signup_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_signup_url_resolves_signup_view(self):
        view = resolve('/signup/')
        self.assertEquals(view.func, signup)

    def test_csrf(self):
        self.assertContains(self.response, 'csrfmiddlewaretoken')

    def test_contains_form(self):
        form = self.response.context.get('form')
        self.assertIsInstance(form, UserCreationForm)

我们略微修改了SignUpTests这个类。定义了setUp这个方法,将response实体移植到这儿。然后,我们也可以测试response中是否含有表单和CSRF token。

现在,我们将测试一个成功的注册。让我们新建一个类来更好地组织代码:

accounts/test.py

from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import signup

class SignUpTests(TestCase):
    # code suppressed...

class SuccessfulSignUpTests(TestCase):
    def setUp(self):
        url = reverse('signup')
        data = {
            'username': 'john',
            'password1': 'abcdef123456',
            'password2': 'abcdef123456'
        }
        self.response = self.client.post(url, data)
        self.home_url = reverse('home')

    def test_redirection(self):
        '''
        A valid form submission should redirect the user to the home page
        '''
        self.assertRedirects(self.response, self.home_url)

    def test_user_creation(self):
        self.assertTrue(User.objects.exists())

    def test_user_authentication(self):
        '''
        Create a new request to an arbitrary page.
        The resulting response should now have a `user` to its context,
        after a successful sign up.
        '''
        response = self.client.get(self.home_url)
        user = response.context.get('user')
        self.assertTrue(user.is_authenticated)

运行这个测试。

使用相似地方法,现在让我们创建一个当注册数据无效时的测试:

from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import signup

class SignUpTests(TestCase):
    # code suppressed...

class SuccessfulSignUpTests(TestCase):
    # code suppressed...

class InvalidSignUpTests(TestCase):
    def setUp(self):
        url = reverse('signup')
        self.response = self.client.post(url, {})  # submit an empty dictionary

    def test_signup_status_code(self):
        '''
        An invalid form submission should return to the same page
        '''
        self.assertEquals(self.response.status_code, 200)

    def test_form_errors(self):
        form = self.response.context.get('form')
        self.assertTrue(form.errors)

    def test_dont_create_user(self):
        self.assertFalse(User.objects.exists())

登录admin后台

1、创建admin后台用户名密码:python manage.py createsuperuser

设置用户名/密码:admin / zlm111222

2、通过URL地址: 来访问Django自带的Admin管理后台

测试注册视图

我们来改进测试用例:

accounts/tests.py

from django.contrib.auth.forms import UserCreationFormfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCasefrom .views import signupclass SignUpTests: def setUp: url = reverse self.response = self.client.get def test_signup_status_code: self.assertEquals(self.response.status_code, 200) def test_signup_url_resolves_signup_view: view = resolve('/signup/') self.assertEquals(view.func, signup) def test_csrf: self.assertContains(self.response, 'csrfmiddlewaretoken') def test_contains_form: form = self.response.context.get self.assertIsInstance(form, UserCreationForm)

我们稍微改变了SighUpTests类,定义了一个setUp方法,将response对象移到那里,现在我们测试响应中是否有表单和CSRF token。

现在我们要测试一个成功的注册功能。这次,让我们来创建一个新类,以便于更好地组织测试。

accounts/tests.py

from django.contrib.auth.models import Userfrom django.contrib.auth.forms import UserCreationFormfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCasefrom .views import signupclass SignUpTests: # code suppressed...class SuccessfulSignUpTests: def setUp: url = reverse data = { 'username': 'john', 'password1': 'abcdef123456', 'password2': 'abcdef123456' } self.response = self.client.post(url, data) self.home_url = reverse def test_redirection: ''' A valid form submission should redirect the user to the home page ''' self.assertRedirects(self.response, self.home_url) def test_user_creation: self.assertTrue(User.objects.exists def test_user_authentication: ''' Create a new request to an arbitrary page. The resulting response should now have a `user` to its context, after a successful sign up. ''' response = self.client.get(self.home_url) user = response.context.get self.assertTrue(user.is_authenticated)

运行这个测试用例。

使用类似地策略,创建一个新的类,用于数据无效的注册用例

from django.contrib.auth.models import Userfrom django.contrib.auth.forms import UserCreationFormfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCasefrom .views import signupclass SignUpTests: # code suppressed...class SuccessfulSignUpTests: # code suppressed...class InvalidSignUpTests: def setUp: url = reverse self.response = self.client.post # submit an empty dictionary def test_signup_status_code: ''' An invalid form submission should return to the same page ''' self.assertEquals(self.response.status_code, 200) def test_form_errors: form = self.response.context.get self.assertTrue(form.errors) def test_dont_create_user: self.assertFalse(User.objects.exists

3.1.在admin中注册模型

# login/admin.py

from django.contrib import admin
from . import models

admin.site.register(models.User)

在表单中增加邮箱字段

一切都生效了,但是...没有Email address字段。呃,UserCreationForm 没有提供一个email 字段,但是我们可以继承他。

accounts文件夹里创建一个forms.py的文件:

accounts/forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class SignUpForm(UserCreationForm):
    email = forms.CharField(max_length=254, required=True, widget=forms.EmailInput())
    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')

现在,在views.py中,让我们引入一个新的表单,SignUpForm,并用它来代替UserCreationForm

accounts/views.py

from django.contrib.auth import login as auth_login
from django.shortcuts import render, redirect

from .forms import SignUpForm

def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('home')
    else:
        form = SignUpForm()
    return render(request, 'signup.html', {'form': form})

只是做出了一点小小改变,一切都已生效:

澳门新萄京官方网站 38

别忘了将测试单元中的UserCreationForm修改为SignUpForm

from .forms import SignUpForm

class SignUpTests(TestCase):
    # ...

    def test_contains_form(self):
        form = self.response.context.get('form')
        self.assertIsInstance(form, SignUpForm)

class SuccessfulSignUpTests(TestCase):
    def setUp(self):
        url = reverse('signup')
        data = {
            'username': 'john',
            'email': 'john@doe.com',
            'password1': 'abcdef123456',
            'password2': 'abcdef123456'
        }
        self.response = self.client.post(url, data)
        self.home_url = reverse('home')

    # ...

之前的测试单元依然能够通过,因为SignUpForm继承了UserCreationForm,这是一个UserCreationForm的实例。

现在让我们想一会发生了什么。我们增加一个新的表单域:

fields = ('username', 'email', 'password1', 'password2')

它将自动反射到HTML模板。这很棒,对吧?是的,很可靠。如果将来一名新的开发者想要复用SignUpForm用于其他用途,或者想增加一些额外字段。那么这些新的字段也将会显示在signup.html中。这些改变是不容易被察觉的,我们不希望发生意外情况。

让我们新建一个测试,它将验证模板中来自HTML的输入:

**accounts/test.py

class SignUpTests(TestCase):
    # ...

    def test_form_inputs(self):
        '''
        The view must contain five inputs: csrf, username, email,
        password1, password2
        '''
        self.assertContains(self.response, '<input', 5)
        self.assertContains(self.response, 'type="text"', 1)
        self.assertContains(self.response, 'type="email"', 1)
        self.assertContains(self.response, 'type="password"', 2)

 引用Django认证登录

打开.../sign/views.py文件修改login_action函数

def login_action(request):
    if request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        # 使用authenticate()函数认证给出的用户名和密码。它接受两个参数,用户名username和密码password
        # 并在用户名密码正确的情况下返回一个user对象。如果用户名密码不正确,则authenticate()返回None
        user = auth.authenticate(username=username, password=password)
        if user is not None:
            auth.login(request, user)  # 登录
            request.session['user'] = username  # 将session信息记录到浏览器
            response = HttpResponseRedirect('/event_manage/')
            return response
        else:
            return render(request, 'index.html', {'error': 'username or password error!'})

将Email字段添加到表单

一切都正常,但还缺失 email address字段。UserCreationForm不提供 email 字段,但是我们可以对它进行扩展。

accounts 文件夹中创建一个名为forms.py的文件:

accounts/forms.py

from django import formsfrom django.contrib.auth.forms import UserCreationFormfrom django.contrib.auth.models import Userclass SignUpForm(UserCreationForm): email = forms.CharField(max_length=254, required=True, widget=forms.EmailInput class Meta: model = User fields = ('username', 'email', 'password1', 'password2')

现在,我们不需要在views.py 中使用UserCreationForm,而是导入新的表单SignUpForm,然后使用它:

accounts/views.py

from django.contrib.auth import login as auth_loginfrom django.shortcuts import render, redirectfrom .forms import SignUpFormdef signup: if request.method == 'POST': form = SignUpForm(request.POST) if form.is_valid(): user = form.save() auth_login(request, user) return redirect else: form = SignUpForm() return render(request, 'signup.html', {'form': form})

只用这个小小的改变,可以运作了:

澳门新萄京官方网站 39

请记住更改测试用例以使用SignUpForm而不是UserCreationForm:

from .forms import SignUpFormclass SignUpTests: # ... def test_contains_form: form = self.response.context.get self.assertIsInstance(form, SignUpForm)class SuccessfulSignUpTests: def setUp: url = reverse data = { 'username': 'john', 'email': 'john@doe.com', 'password1': 'abcdef123456', 'password2': 'abcdef123456' } self.response = self.client.post(url, data) self.home_url = reverse # ...

之前的测试用例仍然会通过,因为SignUpForm扩展了UserCreationForm,它是UserCreationForm的一个实例。

添加了新的表单后,让我们想想发生了什么:

fields = ('username', 'email', 'password1', 'password2')

它会自动映射到HTML模板中。这很好吗?这要视情况而定。如果将来会有新的开发人员想要重新使用SignUpForm来做其他事情,并为其添加一些额外的字段。那么这些新的字段也会出现在signup.html中,这可能不是所期望的行为。这种改变可能会被忽略,我们不希望有任何意外。

那么让我们来创建一个新的测试,验证模板中的HTML输入:

accounts/tests.py

class SignUpTests: # ... def test_form_inputs: ''' The view must contain five inputs: csrf, username, email, password1, password2 ''' self.assertContains(self.response, '<input', 5) self.assertContains(self.response, 'type="text"', 1) self.assertContains(self.response, 'type="email"', 1) self.assertContains(self.response, 'type="password"', 2)

 3.2.创建超级管理员

python manage.py createsuperuser

然后再增加几个测试用户

 

澳门新萄京官方网站 40

 

改善单元测试的布局

好吧,我们正在测试输入,不过我们也必须去测试表单本身。让我们稍稍改善一下项目的设计,不再只将单元测试添加到accounts/tests.py

创建一个带有accounts子文件夹的名为tests的文件夹。然后,在test文件夹中,创建一个名为**init.py**的空文件。

现在,移动tests.py文件到这个test文件夹,然后将他重命名为test_view_signup.py

最终结果如下所示:

myproject/
 |-- myproject/
 |    |-- accounts/
 |    |    |-- migrations/
 |    |    |-- tests/
 |    |    |    |-- __init__.py
 |    |    |     -- test_view_signup.py
 |    |    |-- __init__.py
 |    |    |-- admin.py
 |    |    |-- apps.py
 |    |    |-- models.py
 |    |     -- views.py
 |    |-- boards/
 |    |-- myproject/
 |    |-- static/
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

记住,由于我们相对地引入了应用的上下文,我们需要修改引入路径:

accounts/tests/test_view_signup.py

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase

from ..views import signup
from ..forms import SignUpForm

我们在应用模块中使用相对地引入,所以我们稍后能自由地重命名Django应用,不需要修改所有的绝对引入。

现在让我们创建一个新的测试文件去测试SignUpForm。增加一个名为test_form_signup.py的测试文件:

accounts/tests/test_form_signup.py

from django.test import TestCase
from ..forms import SignUpForm

class SignUpFormTest(TestCase):
    def test_form_has_fields(self):
        form = SignUpForm()
        expected = ['username', 'email', 'password1', 'password2',]
        actual = list(form.fields)
        self.assertSequenceEqual(expected, actual)

这看上去是非常严谨的,对吗?举个例子,如果在将来我们需要包含用户的姓和用户的名(注:两个字段)不得不修改 SignUpForm,我们不再需要修改一些测试单元。

澳门新萄京官方网站 41

这些提示是很有用的,因为他们给开发者带来意识,尤其是对于首次接触代码的小白。这将有助于他们培养编码的信心。

关上窗户

1、 可以直接打开登录成功页,我们需要把这个窗户关上,只需要如下修改:

from django.contrib import auth
.....

# 发布会管理
@login_required
def event_manage(request):
    username = request.session.get('user', '')
    return render(request, "event_manage.html", {"user": username})

再次尝试访问/event_manage/目录(千万不要忘记清理浏览器缓存再试!),Django会告诉访问的路径并不存在(404)。

2、修改.../urls.py文件,添加以下路径

  url(r'^accounts/login/$', views.index),

 

现在访问以下url都能跳转到登录页面了



改进测试代码的组织结构

好的,现在我们正在测试输入和所有的功能,但是我们仍然必须测试表单本身。不要只是继续向accounts/tests.py 文件添加测试,我们稍微改进一下项目设计。

accounts文件夹下创建一个名为tests的新文件夹。然后在tests文件夹中,创建一个名为init.py 的空文件。

现在,将test.py 文件移动到tests文件夹中,并将其重命名为test_view_signup.py

最终的结果应该如下:

myproject/ |-- myproject/ | |-- accounts/ | | |-- migrations/ | | |-- tests/ | | | |-- __init__.py | | |  -- test_view_signup.py | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | |  -- views.py | |-- boards/ | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

注意到,因为我们在应用程序的上下文使用了相对导入,所以我们需要在 test_view_signup.py中修复导入:

accounts/tests/test_view_signup.py

from django.contrib.auth.models import Userfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCasefrom ..views import signupfrom ..forms import SignUpForm

我们在应用程序模块内部使用相对导入,以便我们可以自由地重新命名Django应用程序,而无需修复所有绝对导入。

现在让我们创建一个新的测试文件来测试SignUpForm,添加一个名为test_form_signup.py的新测试文件:

accounts/tests/test_form_signup.py

from django.test import TestCasefrom ..forms import SignUpFormclass SignUpFormTest: def test_form_has_fields: form = SignUpForm() expected = ['username', 'email', 'password1', 'password2',] actual = list(form.fields) self.assertSequenceEqual(expected, actual)

它看起来非常严格对吧,例如,如果将来我们必须更改SignUpForm,以包含用户的名字和姓氏,那么即使我们没有破坏任何东西,我们也可能最终不得不修复一些测试用例。

澳门新萄京官方网站 42

这些警报很有用,因为它们有助于提高认识,特别是新手第一次接触代码,它可以帮助他们自信地编码。

 四、url路由和视图

 前面我们已经创建好数据模型了,并且在admin后台中添加了一些测试用户。下面我们就要设计好站点的url路由、对应的处理视图函数以及使用的前端模板了。

改善注册模板

让我们一起做起来吧。我们可以使用Bootstrap 4 卡片组件来使页面更美观。

访问https://www.toptal.com/designers/subtlepatterns/
寻找一个美观的背景模型来作为账号页面。下载它,在static文件夹中创建名为img的文件夹,然后将图片保存在这儿。

之后,新建一个名为accounts.css的CSS文件。结果如下:

myproject/
 |-- myproject/
 |    |-- accounts/
 |    |-- boards/
 |    |-- myproject/
 |    |-- static/
 |    |    |-- css/
 |    |    |    |-- accounts.css  <-- here
 |    |    |    |-- app.css
 |    |    |     -- bootstrap.min.css
 |    |     -- img/
 |    |    |     -- shattered.png  <-- here (the name may be different, depending on the patter you downloaded)
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

现在来编辑这个accounts.css文件

static/css/accounts.css

body {
  background-image: url(../img/shattered.png);
}

.logo {
  font-family: 'Peralta', cursive;
}

.logo a {
  color: rgba(0,0,0,.9);
}

.logo a:hover,
.logo a:active {
  text-decoration: none;
}

signup.html模板中,我们可以使用新的CSS和Bootstrap 4卡片组件。

templates/signup.html

{% extends 'base.html' %}

{% load static %}

{% block stylesheet %}
  <link rel="stylesheet" href="{% static 'css/accounts.css' %}">
{% endblock %}

{% block body %}
  <div class="container">
    <h1 class="text-center logo my-4">
      <a href="{% url 'home' %}">Django Boards</a>
    </h1>
    <div class="row justify-content-center">
      <div class="col-lg-8 col-md-10 col-sm-12">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Sign up</h3>
            <form method="post" novalidate>
              {% csrf_token %}
              {% include 'includes/form.html' %}
              <button type="submit" class="btn btn-primary btn-block">Create an account</button>
            </form>
          </div>
          <div class="card-footer text-muted text-center">
            Already have an account? <a href="#">Log in</a>
          </div>
        </div>
      </div>
    </div>
  </div>
{% endblock %}

有了这些,我们的注册页面现在变成了这样:

澳门新萄京官方网站 43


改进注册模板

让我们稍微讨论一下,在这里,我们可以使用Bootstrap4 组件来使它看起来不错。

访问: 并找到一个很好地背景图案作为账户页面的背景,下载下来再静态文件夹中创建一个名为img的新文件夹,并将图像放置再那里。

之后,再static/css中创建一个名为accounts.css的新CSS文件。结果应该如下:

myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | |-- myproject/ | |-- static/ | | |-- css/ | | | |-- accounts.css <-- here | | | |-- app.css | | |  -- bootstrap.min.css | |  -- img/ | | |  -- shattered.png <-- here (the name may be different, depending on the patter you downloaded) | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

现在编辑accounts.css这个文件:

static/css/accounts.css

body { background-image: url(../img/shattered.png);}.logo { font-family: 'Peralta', cursive;}.logo a { color: rgba;}.logo a:hover,.logo a:active { text-decoration: none;}

在signup.html模板中,我们可以将其改为使用新的CSS,并使用Bootstrap4组件:

templates/signup.html

{% extends 'base.html' %}{% load static %}{% block stylesheet %} <link rel="stylesheet" href="{% static 'css/accounts.css' %}">{% endblock %}{% block body %} <div > <h1 > <a href="{% url 'home' %}">Django Boards</a> </h1> <div > <div > <div > <div > <h3 >Sign up</h3> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Create an account</button> </form> </div> <div > Already have an account? <a href="#">Log in</a> </div> </div> </div> </div> </div>{% endblock %}

这就是我们现在的注册页面:

澳门新萄京官方网站 44

为了在实现过程保持完整自然流畅的功能,我们还添加注销视图,编辑urls.py以添加新的路由:

myproject/urls.py

from django.conf.urls import urlfrom django.contrib import adminfrom django.contrib.auth import views as auth_viewsfrom accounts import views as accounts_viewsfrom boards import viewsurlpatterns = [ url(r'^$', views.home, name='home'), url(r'^signup/$', accounts_views.signup, name='signup'), url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'), url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'), url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'), url(r'^admin/', admin.site.urls),]

我们从Django的contrib模块导入了views ,我们将其更名为auth_views 以避免与boards.views发生冲突。注意这个视图有点不同: LogoutView.as_view()。这是一个Django的“基于类”的视图,到目前为止,我们只将类实现为python函数。基于类的视图提供了一种更加灵活的方式来扩展和重用视图。稍后我们将讨论更多这个主题。

打开settings.py文件,然后添加LOGOUT_REDIRECT_URL变量到文件的底部:

myproject/settings.py

LOGOUT_REDIRECT_URL = 'home'

在这里我们给变量指定了一个URL模型的名称,以告诉Django当用户退出登录之后跳转的地址。

在这之后,这次重定向就算完成了。只需要访问URL 127.0.0.1:8000/logout/ 然后您就将被注销。但是再等一下,在你注销之前,让我们为登录用户创建下拉菜单。

 4.1.路由设计

初步设想需要下面的四个URL:

 澳门新萄京官方网站 45

考虑到登录系统属于站点的一级功能,为了直观和更易于接受,这里没有采用二级路由的方式,而是在根路由下直接编写路由条目,同样也没有使用反向解析名(name参数)。

# mysite_login/urls.py

from django.conf.urls import url
from django.contrib import admin
from login import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index),
    url(r'^login/', views.login),
    url(r'^register/', views.register),
    url(r'^logout/', views.logout),
]

登出

为了形成一个完整的流程,我们加入一个登出的视图。首先,在urls.py中加入一个新的路由:
myproject/urls.py

from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth import views as auth_views

from accounts import views as accounts_views
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^signup/$', accounts_views.signup, name='signup'),
    url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'),
    url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'),
    url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'),
    url(r'^admin/', admin.site.urls),
]

我们从Django的contrib 模块中引入views。我们将他重命名为:auth_views来避免与boards.views发生冲突。注意这个视图会有些许不同:LogoutView.as_view()。这是一个Django基类视图,至今我们只把类实现为Python方法。这些基类视图提供了更多的灵活的方式去继承和复用视图。之后关于这点,我们将会讨论更多。

打开settings.py,在文件底部添加LOGOUT_REDIRECT_URL变量:

myproject/settings.py

LOGOUT_REDIRECT_URL = 'home'

在这里,我们将URL的值设置为想让用户登出后重定向到的页面。

完成这步,一切就绪。访问**127.0.0.1:8000/logout/ **,你将会登出。不过稍等一下,在你登出之前,让我们为登陆用户创建一个下拉菜单。


为登录用户显示菜单

现在我们需要在 base.html模板中进行一些调整。我们必须添加一个带注销链接的下拉菜单。

Bootstrap 4 下拉组件需要jQuery才能工作。

首先,我们前往 jquery.com/download/,然后下载压缩的 jQuery 3.2.1版本。

澳门新萄京官方网站 46

在静态文件夹中,创建一个名为js的新文件夹。将jquery-3.2.1.min.js文件复制到那里。

Bootstrap4还需要一个名为Popper 的库才能工作,前往 popper.js.org 下载它的最新版本。

popper.js-1.12.5文件夹中,转到dist/umd并将文件popper.min.js 复制到我们的js 文件夹。这里注意,敲黑板!Bootstrap 4只能与 umd/popper.min.js协同工作。所以请确保你正在复制正确的文件。

如果您不再拥有 Bootstrap 4文件,请从getbootstrap.com.再次下载它。

同样,将 bootstrap.min.js文件复制到我们的js文件夹中。最终的结果应该是:

myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | |-- myproject/ | |-- static/ | | |-- css/ | |  -- js/ | | |-- bootstrap.min.js | | |-- jquery-3.2.1.min.js | |  -- popper.min.js | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

base.html文件底部,在{% endblock body %}后面添加脚本:

templates/base.html

{% load static %}<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>{% block title %}Django Boards{% endblock %}</title> <link href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet"> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/app.css' %}"> {% block stylesheet %}{% endblock %} </head> <body> {% block body %} <!-- code suppressed for brevity --> {% endblock body %} <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script> <script src="{% static 'js/popper.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script> </body></html>

如果你发现上面的说明很模糊,只需要直接在下面的链接下载文件

打开链接,右键另存为

现在我们可以添加Bootstrap4下拉菜单了:

templates/base.html

<nav > <div > <a href="{% url 'home' %}">Django Boards</a> <button type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">  </button> <div > <ul > <li > <a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {{ user.username }} </a> <div aria-labelledby="userMenu"> <a href="#">My account</a> <a href="#">Change password</a> <div ></div> <a href="{% url 'logout' %}">Log out</a> </div> </li> </ul> </div> </div></nav>

澳门新萄京官方网站 47

我们来试试吧,点击注销:

澳门新萄京官方网站 48

现在已经成功显示出来了,但是无论用户登录与否,下拉菜单都会显示。不同的是在未登录时用户名显示是空的,我们只能看到一个箭头。

我们可以改进一点:

<nav > <div > <a href="{% url 'home' %}">Django Boards</a> <button type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">  </button> <div > {% if user.is_authenticated %} <ul > <li > <a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {{ user.username }} </a> <div aria-labelledby="userMenu"> <a href="#">My account</a> <a href="#">Change password</a> <div ></div> <a href="{% url 'logout' %}">Log out</a> </div> </li> </ul> {% else %} <form > <a href="#" >Log in</a> <a href="{% url 'signup' %}" >Sign up</a> </form> {% endif %} </div> </div></nav>

现在,我们告诉Django程序,要在用户登录时显示下拉菜单,如果没有,则显示登录并注册按钮:

澳门新萄京官方网站 49

首先,添加一个新的URL路径:

myproject/urls.py

from django.conf.urls import urlfrom django.contrib import adminfrom django.contrib.auth import views as auth_viewsfrom accounts import views as accounts_viewsfrom boards import viewsurlpatterns = [ url(r'^$', views.home, name='home'), url(r'^signup/$', accounts_views.signup, name='signup'), url(r'^login/$', auth_views.LoginView.as_view(template_name='login.html'), name='login'), url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'), url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'), url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'), url(r'^admin/', admin.site.urls),]

as_view()中,我们可以传递一些额外的参数,以覆盖默认值。在这种情况下,我们让LoginView 使用login.html模板。

编辑settings.py然后添加

myproject/settings.py

LOGIN_REDIRECT_URL = 'home'

这个配置信息告诉Django在成功登录后将用户重定向到哪里。

最后,将登录URL添加到 base.html模板中:

templates/base.html

<a href="{% url 'login' %}" >Log in</a>

我们可以创建一个类似于注册页面的模板。创建一个名为 login.html 的新文件:

templates/login.html

{% extends 'base.html' %}{% load static %}{% block stylesheet %} <link rel="stylesheet" href="{% static 'css/accounts.css' %}">{% endblock %}{% block body %} <div > <h1 > <a href="{% url 'home' %}">Django Boards</a> </h1> <div > <div > <div > <div > <h3 >Log in</h3> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Log in</button> </form> </div> <div > New to Django Boards? <a href="{% url 'signup' %}">Sign up</a> </div> </div> <div > <small> <a href="#" >Forgot your password?</a> </small> </div> </div> </div> </div>{% endblock %}

澳门新萄京官方网站 50

我们看到HTML模板中的内容重复了,现在来重构一下它。

创建一个名为base_accounts.html的新模板:

templates/base_accounts.html

{% extends 'base.html' %}{% load static %}{% block stylesheet %} <link rel="stylesheet" href="{% static 'css/accounts.css' %}">{% endblock %}{% block body %} <div > <h1 > <a href="{% url 'home' %}">Django Boards</a> </h1> {% block content %} {% endblock %} </div>{% endblock %}

现在在signup.htmllogin.html中使用它:

templates/login.html

{% extends 'base_accounts.html' %}{% block title %}Log in to Django Boards{% endblock %}{% block content %} <div > <div > <div > <div > <h3 >Log in</h3> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Log in</button> </form> </div> <div > New to Django Boards? <a href="{% url 'signup' %}">Sign up</a> </div> </div> <div > <small> <a href="#" >Forgot your password?</a> </small> </div> </div> </div>{% endblock %}

我们有密码重置的功能,因此现在让我们将其暂时保留为#

templates/signup.html

{% extends 'base_accounts.html' %}{% block title %}Sign up to Django Boards{% endblock %}{% block content %} <div > <div > <div > <div > <h3 >Sign up</h3> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Create an account</button> </form> </div> <div > Already have an account? <a href="{% url 'login' %}">Log in</a> </div> </div> </div> </div>{% endblock %}

请注意,我们添加了登录链接: <a href="{% url 'login' %}">Log in</a>.

4.2.架构初步视图

路由写好了,就进入login/views.py文件编写视图的框架,代码如下:

# login/views.py

from django.shortcuts import render,redirect

def index(request):
    pass
    return render(request,'login/index.html')

def login(request):
    pass
    return render(request,'login/login.html')

def register(request):
    pass
    return render(request,'login/register.html')

def logout(request):
    pass
    return redirect('/index/')

我们先不着急完成视图内部的具体细节,而是把框架先搭建起来。

为通过身份验证的用户显示菜单

现在,我们需要在base.html中做一些调整。我们需要加入一个带有登出链接的下拉菜单。

Bootstrap 4 下拉组件需要JQuery的支持。

首先,访问 jquery.com/download/压缩的发行版本JQuery3.2.1

澳门新萄京官方网站 51

static文件夹中,创建一个js文件夹,并将jquery-3.2.1.min.js复制粘贴进去。

Bootstrap4 还需要一个叫做Popper的库支持。访问 popper.js.org
下载最新版本。

popper.js-1.12.5文件夹中,去dist/umd目录下寻找并且复制popper.min.jsjs文件夹。注意,Bootstrap4只需要和umd/popper.min.js配合生效,所以确保你复制了正确的文件。

如果你没有Bootstrap4的完整文件,访问 getbootstrap.com
重新下载。

同样的,复制bootstrap.min.js文件到js文件夹。

最后结果如下:

myproject/
 |-- myproject/
 |    |-- accounts/
 |    |-- boards/
 |    |-- myproject/
 |    |-- static/
 |    |    |-- css/
 |    |     -- js/
 |    |         |-- bootstrap.min.js
 |    |         |-- jquery-3.2.1.min.js
 |    |          -- popper.min.js
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

base.html文件底部,在{% endblock body %}后加入脚本:

templates/base.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Django Boards{% endblock %}</title>
    <link href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/app.css' %}">
    {% block stylesheet %}{% endblock %}
  </head>
  <body>
    {% block body %}
    <!-- code suppressed for brevity -->
    {% endblock body %}
    <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
    <script src="{% static 'js/popper.min.js' %}"></script>
    <script src="{% static 'js/bootstrap.min.js' %}"></script>
  </body>
</html>

如果你觉得说明混乱,只要通过以下链接下载文件:

  • https://code.jquery.com/jquery-3.2.1.min.js
  • https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js
  • https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js

右击将链接另存为...

现在,我们能够添加Bootstrap4的下拉菜单:

templates/base.html

<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">

    </button>
    <div class="collapse navbar-collapse" id="mainMenu">
      <ul class="navbar-nav ml-auto">
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ user.username }}
          </a>
          <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
            <a class="dropdown-item" href="#">My account</a>
            <a class="dropdown-item" href="#">Change password</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
          </div>
        </li>
      </ul>
    </div>
  </div>
</nav>

澳门新萄京官方网站 52

让我们试一试,点击登出:

澳门新萄京官方网站 53

它生效了。但是下拉菜单只是显示缺不管用户是否已经登出,不同的是,现在用户名是空的,我们只能看到一个小箭头。

我们需要稍稍改善一下:

<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
  <div class="container">
    <a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainMenu" aria-controls="mainMenu" aria-expanded="false" aria-label="Toggle navigation">

    </button>
    <div class="collapse navbar-collapse" id="mainMenu">
      {% if user.is_authenticated %}
        <ul class="navbar-nav ml-auto">
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
              {{ user.username }}
            </a>
            <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
              <a class="dropdown-item" href="#">My account</a>
              <a class="dropdown-item" href="#">Change password</a>
              <div class="dropdown-divider"></div>
              <a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
            </div>
          </li>
        </ul>
      {% else %}
        <form class="form-inline ml-auto">
          <a href="#" class="btn btn-outline-secondary">Log in</a>
          <a href="{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>
        </form>
      {% endif %}
    </div>
  </div>
</nav>

现在,我们告知Django,当用户已登录时显示下拉菜单,如果未登录,下是登录和注册按钮:

澳门新萄京官方网站 54


无登录信息错误

如果我们提交空白的登录信息,我们会得到一些友好的错误提示信息:

澳门新萄京官方网站 55

但是,如果我们提交一个不存在的用户名或一个无效的密码,现在就会发生这种情况:

澳门新萄京官方网站 56

有点误导,这个区域是绿色的,表明它们是良好运行的,此外,没有其他额外的信息。

这是因为表单有一种特殊类型的错误,叫做 non-field errors。这是一组与特定字段无关的错误。让我们重构form.html部分模板以显示这些错误:

templates/includes/form.html

{% load widget_tweaks %}{% if form.non_field_errors %} <div role="alert"> {% for error in form.non_field_errors %} <p{% if forloop.last %} {% endif %}>{{ error }}</p> {% endfor %} </div>{% endif %}{% for field in form %} <!-- code suppressed -->{% endfor %}

{% if forloop.last %}只是一个小事情,因为p标签有一个空白的margin-bottom.一个表单可能有几个non-field error,我们呈现了一个带有错误的p标签。然后我要检查它是否是最后一次渲染的错误。如果是这样的,我们就添加一个 Bootstrap 4 CSS类 mb-0 ,它的作用是代表了“margin bottom = 0”。这样的话警告看起来就不那么奇怪了并且多了一些额外的空间。这只是一个非常小的细节。我这么做的原因只是为了保持间距的一致性。

澳门新萄京官方网站 57

尽管如此,我们仍然需要处理密码字段。问题在于,Django从不将密码字段的数据返回给客户端。因此,在某些情况下,不要试图做一次自作聪明的事情,我们可以直接忽略is-validis-invalid 的CSS类。但是我们的表单模板看起来十分的复杂,我们可以将一些代码移动到模板标记中去。

4.3.创建HTML页面文件

在项目根路径的login目录中创建一个templates目录,再在templates目录里创建一个login目录

login/templates/login目录中创建三个文件index.htmllogin.html以及register.html ,并写入如下的代码:

index.html

{#login/templates/login/index.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>

login.html

{#login/templates/login/login.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>

register.html

{#login/templates/login/register.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>
<body>
<h1>注册页面</h1>
</body>
</html>

登录

首先,增加一个新的URL路由:
myproject/urls.py

from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth import views as auth_views

from accounts import views as accounts_views
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^signup/$', accounts_views.signup, name='signup'),
    url(r'^login/$', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
    url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'),
    url(r'^boards/(?P<pk>d )/$', views.board_topics, name='board_topics'),
    url(r'^boards/(?P<pk>d )/new/$', views.new_topic, name='new_topic'),
    url(r'^admin/', admin.site.urls),
]

as_view()中,我们可以传递一些额外参数,这样可以重写默认方法。在这个案例中,我们将控制LoginViewlogin.html中去寻找模板。

编辑settings.py 然后添加如下配置:

myproject/settings.py

LOGIN_REDIRECT_URL = 'home'

这个配置告诉Django在用户登陆成功后的重定向位置。

最后,在base.html模板中添加登录URL:

templates/base.html

<a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>

我们可以创建一个类似于注册页面的模板。新建一个名为login.html的文件:

templates/login.html

{% extends 'base.html' %}

{% load static %}

{% block stylesheet %}
  <link rel="stylesheet" href="{% static 'css/accounts.css' %}">
{% endblock %}

{% block body %}
  <div class="container">
    <h1 class="text-center logo my-4">
      <a href="{% url 'home' %}">Django Boards</a>
    </h1>
    <div class="row justify-content-center">
      <div class="col-lg-4 col-md-6 col-sm-8">
        <div class="card">
          <div class="card-body">
            <h3 class="card-title">Log in</h3>
            <form method="post" novalidate>
              {% csrf_token %}
              {% include 'includes/form.html' %}
              <button type="submit" class="btn btn-primary btn-block">Log in</button>
            </form>
          </div>
          <div class="card-footer text-muted text-center">
            New to Django Boards? <a href="{% url 'signup' %}">Sign up</a>
          </div>
        </div>
        <div class="text-center py-2">
          <small>
            <a href="#" class="text-muted">Forgot your password?</a>
          </small>
        </div>
      </div>
    </div>
  </div>
{% endblock %}

澳门新萄京官方网站 58

HTML模板中有写代码重复了,让我们来重构他。

新建一个名为:** base_accounts.html**的模板:

{% extends 'base.html' %}

{% load static %}

{% block stylesheet %}
  <link rel="stylesheet" href="{% static 'css/accounts.css' %}">
{% endblock %}

{% block body %}
  <div class="container">
    <h1 class="text-center logo my-4">
      <a href="{% url 'home' %}">Django Boards</a>
    </h1>
    {% block content %}
    {% endblock %}
  </div>
{% endblock %}

现在把他添加到signup.htmllogin.html

templates/login.html

{% extends 'base_accounts.html' %}

{% block title %}Log in to Django Boards{% endblock %}

{% block content %}
  <div class="row justify-content-center">
    <div class="col-lg-4 col-md-6 col-sm-8">
      <div class="card">
        <div class="card-body">
          <h3 class="card-title">Log in</h3>
          <form method="post" novalidate>
            {% csrf_token %}
            {% include 'includes/form.html' %}
            <button type="submit" class="btn btn-primary btn-block">Log in</button>
          </form>
        </div>
        <div class="card-footer text-muted text-center">
          New to Django Boards? <a href="{% url 'signup' %}">Sign up</a>
        </div>
      </div>
      <div class="text-center py-2">
        <small>
          <a href="#" class="text-muted">Forgot your password?</a>
        </small>
      </div>
    </div>
  </div>
{% endblock %}

我们还没有密码重置URL,所以我们现在先用#搁置它。

templates/signup.html

{% extends 'base_accounts.html' %}

{% block title %}Sign up to Django Boards{% endblock %}

{% block content %}
  <div class="row justify-content-center">
    <div class="col-lg-8 col-md-10 col-sm-12">
      <div class="card">
        <div class="card-body">
          <h3 class="card-title">Sign up</h3>
          <form method="post" novalidate>
            {% csrf_token %}
            {% include 'includes/form.html' %}
            <button type="submit" class="btn btn-primary btn-block">Create an account</button>
          </form>
        </div>
        <div class="card-footer text-muted text-center">
          Already have an account? <a href="{% url 'login' %}">Log in</a>
        </div>
      </div>
    </div>
  </div>
{% endblock %}

请注意我们添加了登录URL:<a href="{% url 'login' %}">Log in</a>

创建自定义模板标签

boards应用中,创建一个名为templatetags的新文件夹。然后在该文件夹内创建两个名为 ****init.pyform_tags.py的空文件。

文件结构应该如下:

myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | | |-- migrations/ | | |-- templatetags/ <-- here | | | |-- __init__.py | | |  -- form_tags.py | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | | |-- tests.py | |  -- views.py | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

form_tags.py文件中,我们创建两个模板标签:

boards/templatetags/form_tags.py

from django import templateregister = template.Library()@register.filterdef field_type(bound_field): return bound_field.field.widget.__class__.__name__@register.filterdef input_class(bound_field): css_class = '' if bound_field.form.is_bound: if bound_field.errors: css_class = 'is-invalid' elif field_type(bound_field) != 'PasswordInput': css_class = 'is-valid' return 'form-control {}'.format(css_class)

这些是模板过滤器,他们的工作方式是这样的:

首先,我们将它加载到模板中,就像我们使用 widget_tweaksstatic 模板标签一样。请注意,在创建这个文件后,你将不得不手动停止开发服务器并重启它,以便Django可以识别新的模板标签。

{% load form_tags %}

之后,我们就可以在模板中使用它们了。

{{ form.username|field_type }}

返回:

'TextInput'

或者在 input_class的情况下:

{{ form.username|input_class }}<!-- if the form is not bound, it will simply return: -->'form-control '<!-- if the form is bound and valid: -->'form-control is-valid'<!-- if the form is bound and invalid: -->'form-control is-invalid'

现在更新 form.html以使用新的模板标签:

templates/includes/form.html

{% load form_tags widget_tweaks %}{% if form.non_field_errors %} <div role="alert"> {% for error in form.non_field_errors %} <p{% if forloop.last %} {% endif %}>{{ error }}</p> {% endfor %} </div>{% endif %}{% for field in form %} <div > {{ field.label_tag }} {% render_field field class=field|input_class %} {% for error in field.errors %} <div > {{ error }} </div> {% endfor %} {% if field.help_text %} <small > {{ field.help_text|safe }} </small> {% endif %} </div>{% endfor %}

这样的话就好多了是吧?这样做降低了模板的复杂性,它现在看起来更加整洁。并且它还解决了密码字段显示绿色边框的问题:

澳门新萄京官方网站 59

 五、前端页面设计

登录空字段错误

如果我们提交一张空的登录表单,我们会得到一些很优雅的报错信息:

澳门新萄京官方网站 60

但如果我们提交一个不存在的用户名或者一个非法密码时,将会发生这种情况:

澳门新萄京官方网站 61

这有点误导人。字段域显示绿色,仿佛在提示他们是没有问题的。而且也没有任何其他说明信息。

这是因为表单有一种叫空字段错误的特殊错误,它包含了所有和特定字段不相关的错误。让我们来重构form.html的局部来显示错误信息:

templates/includes/form.html

{% load widget_tweaks %}

{% if form.non_field_errors %}
  <div class="alert alert-danger" role="alert">
    {% for error in form.non_field_errors %}
      <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
    {% endfor %}
  </div>
{% endif %}

{% for field in form %}
  <!-- code suppressed -->
{% endfor %}

{% if forloop.last %}是次要的。p标签有margin-bottom样式。一张表单可能含有多个空字段错误,对于每个错误,我们都要呈现一个带有错误信息的p标签。然后我将测试呈现这是否一个持续性的错误。如果是的话,我们引入Bootstrap4的样式类mb-0,这个类用于替代margin bottom = 0。有了更多的额外空间,这时警告信息看上去没那么奇怪了。还有一个很次要的细节,保持他们的间距一致。

澳门新萄京官方网站 62

尽管我们还需要处理密码字段。但事实是,Django永远不会将密码字段返回给用户。所以我们只需忽略is-validis-invalid样式,而不用去做额外的加工了。此时我们表单模板看上去很复杂,我们可以移植一些代码到template tag

测试模板标签

首先,让我们稍微组织一下boards的测试。就像我们对account app 所做的那样。创建一个新的文件夹名为tests,添加一个init.py,复制test.py并且将其重命名为test_views.py

添加一个名为 test_templatetags.py的新空文件。

myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | | |-- migrations/ | | |-- templatetags/ | | |-- tests/ | | | |-- __init__.py | | | |-- test_templatetags.py <-- new file, empty for now | | |  -- test_views.py <-- our old file with all the tests | | |-- __init__.py | | |-- admin.py | | |-- apps.py | | |-- models.py | |  -- views.py | |-- myproject/ | |-- static/ | |-- templates/ | |-- db.sqlite3 |  -- manage.py  -- venv/

修复test_views.py的导入问题:

boards/tests/test_views.py

from ..views import home, board_topics, new_topicfrom ..models import Board, Topic, Postfrom ..forms import NewTopicForm

执行测试来确保一切都正常。

boards/tests/test_templatetags.py

from django import formsfrom django.test import TestCasefrom ..templatetags.form_tags import field_type, input_classclass ExampleForm(forms.Form): name = forms.CharField() password = forms.CharField(widget=forms.PasswordInput class Meta: fields = ('name', 'password')class FieldTypeTests: def test_field_widget_type: form = ExampleForm() self.assertEquals('TextInput', field_type(form['name'])) self.assertEquals('PasswordInput', field_type(form['password']))class InputClassTests: def test_unbound_field_initial_state: form = ExampleForm() # unbound form self.assertEquals('form-control ', input_class(form['name'])) def test_valid_bound_field: form = ExampleForm({'name': 'john', 'password': '123'}) # bound form (field   data) self.assertEquals('form-control is-valid', input_class(form['name'])) self.assertEquals('form-control ', input_class(form['password'])) def test_invalid_bound_field: form = ExampleForm({'name': '', 'password': '123'}) # bound form (field   data) self.assertEquals('form-control is-invalid', input_class(form['name']))

我们创建了一个用于测试的表单类,然后添加了覆盖两个模板标记中可能出现的场景的测试用例。

python manage.py test

Creating test database for alias 'default'...System check identified no issues (0 silenced).................................----------------------------------------------------------------------Ran 32 tests in 0.846sOKDestroying test database for alias 'default'...

密码重置过程中涉及一些不友好的 URL 模式。但正如我们在前面的教程中讨论的那样,我们并不需要成为正则表达式专家。我们只需要了解常见问题和它们的解决办法。

在我们开始之前另一件重要的事情是,对于密码重置过程,我们需要发送电子邮件。一开始有点复杂,因为我们需要外部服务。目前,我们不会配置生产环境使用的电子邮件服务。实际上,在开发阶段,我们可以使用Django的调试工具检查电子邮件是否正确发送。

澳门新萄京官方网站 63

 5.1.原生HTML页面

login.html文件中的内容,写入下面的代码:

{#login/templates/login/login.html#}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
     <div style="margin: 15% 40%;">
        <h1>欢迎登录!</h1>
       <form action="/login/" method="post">
            <p>
                <label for="id_username">用户名:</label>
                <input type="text" id="id_username" name="username" placeholder="用户名" autofocus required />
            </p>

            <p>
                <label for="id_password">密码:</label>
                <input type="password" id="id_password" placeholder="密码" name="password" required >
            </p>
            <input type="submit" value="确定">
        </form>
    </div>



</body>
</html>

可以看到如下图的页面:

澳门新萄京官方网站 64

创建自定义Template Tags(模板标签)

boards应用中,新建一个名为templatetags的文件夹。在这个文件夹里,新建名为** init.pyform_tags.py**的空文件。

目录结构如下所示:

myproject/
 |-- myproject/
 |    |-- accounts/
 |    |-- boards/
 |    |    |-- migrations/
 |    |    |-- templatetags/        <-- here
 |    |    |    |-- __init__.py
 |    |    |     -- form_tags.py
 |    |    |-- __init__.py
 |    |    |-- admin.py
 |    |    |-- apps.py
 |    |    |-- models.py
 |    |    |-- tests.py
 |    |     -- views.py
 |    |-- myproject/
 |    |-- static/
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

form_tags.py文件中,让我们新建两个模板标签:

boards/templatetags/form_tags.py

from django import template

register = template.Library()

@register.filter
def field_type(bound_field):
    return bound_field.field.widget.__class__.__name__

@register.filter
def input_class(bound_field):
    css_class = ''
    if bound_field.form.is_bound:
        if bound_field.errors:
            css_class = 'is-invalid'
        elif field_type(bound_field) != 'PasswordInput':
            css_class = 'is-valid'
    return 'form-control {}'.format(css_class)

这些是模板过滤器,工作原理如下:

首先,当我们使用了widget_tweaks或**static **模板标签时,他们会在模板中被加载。注意在创建了这些文件后,你需要手动重启服务器,这样Django才能识别到新的模板标签。

{% load form_tags %}

在这之后,我们可以在模板中使用它们:

{{ form.username|field_type }}

这将会返回:

'TextInput'

或者在使用了input_class的情况下:

{{ form.username|input_class }}

<!-- if the form is not bound, it will simply return: -->
'form-control '

<!-- if the form is bound and valid: -->
'form-control is-valid'

<!-- if the form is bound and invalid: -->
'form-control is-invalid'

现在让我们用新的模板标签来更新form.html
templates/includes/form.html

{% load form_tags widget_tweaks %}

{% if form.non_field_errors %}
  <div class="alert alert-danger" role="alert">
    {% for error in form.non_field_errors %}
      <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
    {% endfor %}
  </div>
{% endif %}

{% for field in form %}
  <div class="form-group">
    {{ field.label_tag }}
    {% render_field field class=field|input_class %}
    {% for error in field.errors %}
      <div class="invalid-feedback">
        {{ error }}
      </div>
    {% endfor %}
    {% if field.help_text %}
      <small class="form-text text-muted">
        {{ field.help_text|safe }}
      </small>
    {% endif %}
  </div>
{% endfor %}

好多了不是吗?减小了模板的复杂度,看上去更整洁了。这同时也解决了密码框显示绿色的问题:

澳门新萄京官方网站 65

image.png

控制台收发Email

这个主意来自于项目开发过程中,而不是发送真实的电子邮件,我们只需要记录它们。我们有两种选择:将所有电子邮件写入文本文件或仅将其显示在控制台中。我发现第二个方式更加方便,因为我们已经在使用控制台来运行开发服务器,并且设置更容易一些。

编辑 settings.py模块并将EMAIL_BACKEND变量添加到文件的末尾。

myproject/settings.py

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

5.2.引入Bootstrap

Bootstrap3.3.7下载地址

根目录下新建一个static目录,并将解压后的bootstrap-3.3.7-dist目录,整体拷贝到static目录中,如下图所示:

 澳门新萄京官方网站 66

由于Bootstrap依赖JQuery,所以我们需要提前下载并引入JQuery:下载地址

在static目录下,新建一个css和js目录,作为以后的样式文件和js文件的存放地,将我们的jquery文件拷贝到static/js目录下。

 澳门新萄京官方网站 67

然后打开项目的settings文件,在最下面添加配置,用于指定静态文件的搜索目录:

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

测试模板标签

首先,我们来稍稍组织一下boards'的单元测试。类似于我们在accounts应用中所做的一样,新建名为tests的文件夹,然后增加一个** init.py文件,复制tests.py并将他重命名为 test_views.py**。

新建一个名为test_templatetags.py的空文件:

myproject/
 |-- myproject/
 |    |-- accounts/
 |    |-- boards/
 |    |    |-- migrations/
 |    |    |-- templatetags/
 |    |    |-- tests/
 |    |    |    |-- __init__.py
 |    |    |    |-- test_templatetags.py  <-- new file, empty for now
 |    |    |     -- test_views.py  <-- our old file with all the tests
 |    |    |-- __init__.py
 |    |    |-- admin.py
 |    |    |-- apps.py
 |    |    |-- models.py
 |    |     -- views.py
 |    |-- myproject/
 |    |-- static/
 |    |-- templates/
 |    |-- db.sqlite3
 |     -- manage.py
  -- venv/

修改test_views.py的引入路径:

boards/tests/test_views.py

from ..views import home, board_topics, new_topic
from ..models import Board, Topic, Post
from ..forms import NewTopicForm

执行测试文件来确保一切都生效了。

boards/tests/test_templatetags.py

from django import forms
from django.test import TestCase
from ..templatetags.form_tags import field_type, input_class

class ExampleForm(forms.Form):
    name = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput())
    class Meta:
        fields = ('name', 'password')

class FieldTypeTests(TestCase):
    def test_field_widget_type(self):
        form = ExampleForm()
        self.assertEquals('TextInput', field_type(form['name']))
        self.assertEquals('PasswordInput', field_type(form['password']))

class InputClassTests(TestCase):
    def test_unbound_field_initial_state(self):
        form = ExampleForm()  # unbound form
        self.assertEquals('form-control ', input_class(form['name']))

    def test_valid_bound_field(self):
        form = ExampleForm({'name': 'john', 'password': '123'})  # bound form (field   data)
        self.assertEquals('form-control is-valid', input_class(form['name']))
        self.assertEquals('form-control ', input_class(form['password']))

    def test_invalid_bound_field(self):
        form = ExampleForm({'name': '', 'password': '123'})  # bound form (field   data)
        self.assertEquals('form-control is-invalid', input_class(form['name']))

我们创建了一个表单测试类,并在两个模板标签中添加针对可能出现的错误的测试方法。

python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
................................
----------------------------------------------------------------------
Ran 32 tests in 0.846s

OK
Destroying test database for alias 'default'...

配置路由

密码重置过程需要四个视图:

  • 带有表单的页面,用于启动重置过程;
  • 一个成功的页面,表示该过程已启动,指示用户检查其邮件文件夹等;
  • 检查通过电子邮件发送token的页面
  • 一个告诉用户重置是否成功的页面

这些视图是内置的,我们不需要执行任何操作,我们所需要做的就是将路径添加到 urls.py并且创建模板。

myproject/urls.py

url(r'^reset/$', auth_views.PasswordResetView.as_view( template_name='password_reset.html', email_template_name='password_reset_email.html', subject_template_name='password_reset_subject.txt' ), name='password_reset'),url(r'^reset/done/$', auth_views.PasswordResetDoneView.as_view(template_name='password_reset_done.html'), name='password_reset_done'),url(r'^reset/(?P<uidb64>[0-9A-Za-z_-] )/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', auth_views.PasswordResetConfirmView.as_view(template_name='password_reset_confirm.html'), name='password_reset_confirm'),url(r'^reset/complete/$', auth_views.PasswordResetCompleteView.as_view(template_name='password_reset_complete.html'), name='password_reset_complete'),]

在密码重置视图中,template_name参数是可选的。但我认为重新定义它是个好主意,因此视图和模板之间的链接比仅使用默认值更加明显。

templates文件夹中,新增如下模板文件

  • password_reset.html
  • password_reset_email.html:这个模板是发送给用户的电子邮件正文
  • password_reset_subject.txt:这个模板是电子邮件的主题行,它应该是单行文件
  • password_reset_done.html
  • password_reset_confirm.html
  • password_reset_complete.html

在我们开始实现模板之前,让我们准备一个新的测试文件。

我们可以添加一些基本的测试,因为这些视图和表单已经在Django代码中进行了测试。我们将只测试我们应用程序的细节。

accounts/tests 文件夹中创建一个名为 test_view_password_reset.py 的新测试文件。

5.3.创建base.html模板

既然要将前端页面做得像个样子,那么就不能和前面一样,每个页面都各写各的,单打独斗。一个网站有自己的统一风格和公用部分,可以把这部分内容集中到一个基础模板base.html中。现在,在根目录下的templates中新建一个base.html文件用作站点的基础模板。

在Bootstrap文档中,为我们提供了一个非常简单而又实用的基本模板,代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link href="css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="http://www.cruity.com/uploads/allimg/191124/062ZC3A-67.jpg"></script>
      <script src="http://www.cruity.com/uploads/allimg/191124/062ZC632-68.jpg"></script>
    <![endif]-->
  </head>
  <body>
    <h1>你好,世界!</h1>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="http://www.cruity.com/uploads/allimg/191124/062Z61941-69.jpg"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="js/bootstrap.min.js"></script>
  </body>
</html>

将它整体拷贝到base.html文件中。

密码重置

密码重置视图

templates/password_reset.html

{% extends 'base_accounts.html' %}{% block title %}Reset your password{% endblock %}{% block content %} <div > <div > <div > <div > <h3 >Reset your password</h3> <p>Enter your email address and we will send you a link to reset your password.</p> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Send password reset email</button> </form> </div> </div> </div> </div>{% endblock %}

澳门新萄京官方网站 68

accounts/tests/test_view_password_reset.py

from django.contrib.auth import views as auth_viewsfrom django.contrib.auth.forms import PasswordResetFormfrom django.contrib.auth.models import Userfrom django.core import mailfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCaseclass PasswordResetTests: def setUp: url = reverse('password_reset') self.response = self.client.get def test_status_code: self.assertEquals(self.response.status_code, 200) def test_view_function: view = resolve('/reset/') self.assertEquals(view.func.view_class, auth_views.PasswordResetView) def test_csrf: self.assertContains(self.response, 'csrfmiddlewaretoken') def test_contains_form: form = self.response.context.get self.assertIsInstance(form, PasswordResetForm) def test_form_inputs: ''' The view must contain two inputs: csrf and email ''' self.assertContains(self.response, '<input', 2) self.assertContains(self.response, 'type="email"', 1)class SuccessfulPasswordResetTests: def setUp: email = 'john@doe.com' User.objects.create_user(username='john', email=email, password='123abcdef') url = reverse('password_reset') self.response = self.client.post(url, {'email': email}) def test_redirection: ''' A valid form submission should redirect the user to `password_reset_done` view ''' url = reverse('password_reset_done') self.assertRedirects(self.response, url) def test_send_password_reset_email: self.assertEqual(1, len(mail.outbox))class InvalidPasswordResetTests: def setUp: url = reverse('password_reset') self.response = self.client.post(url, {'email': 'donotexist@email.com'}) def test_redirection: ''' Even invalid emails in the database should redirect the user to `password_reset_done` view ''' url = reverse('password_reset_done') self.assertRedirects(self.response, url) def test_no_reset_email_sent: self.assertEqual(0, len(mail.outbox))

templates/password_reset_subject.txt

[Django Boards] Please reset your password

templates/password_reset_email.html

Hi there,Someone asked for a password reset for the email address {{ email }}.Follow the link below:{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}In case you forgot your Django Boards username: {{ user.username }}If clicking the link above doesn't work, please copy and paste the URLin a new browser window instead.If you've received this mail in error, it's likely that another user enteredyour email address by mistake while trying to reset a password. If you didn'tinitiate the request, you don't need to take any further action and can safelydisregard this email.Thanks,The Django Boards Team

澳门新萄京官方网站 69

我们可以创建一个特定的文件来测试电子邮件。在accounts/tests 文件夹中创建一个名为test_mail_password_reset.py的新文件:

accounts/tests/test_mail_password_reset.py

from django.core import mailfrom django.contrib.auth.models import Userfrom django.urls import reversefrom django.test import TestCaseclass PasswordResetMailTests: def setUp: User.objects.create_user(username='john', email='john@doe.com', password='123') self.response = self.client.post(reverse('password_reset'), { 'email': 'john@doe.com' }) self.email = mail.outbox[0] def test_email_subject: self.assertEqual('[Django Boards] Please reset your password', self.email.subject) def test_email_body: context = self.response.context token = context.get uid = context.get password_reset_token_url = reverse('password_reset_confirm', kwargs={ 'uidb64': uid, 'token': token }) self.assertIn(password_reset_token_url, self.email.body) self.assertIn('john', self.email.body) self.assertIn('john@doe.com', self.email.body) def test_email_to: self.assertEqual(['john@doe.com',], self.email.to)

此测试用例抓取应用程序发送的电子邮件,并检查主题行,正文内容以及发送给谁。

 5.4.创建页面导航条

Bootstrap提供了现成的导航条组件

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        Toggle navigation



      </button>
      <a class="navbar-brand" href="#">Brand</a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link (current)</a></li>
        <li><a href="#">Link</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown </a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">One more separated link</a></li>
          </ul>
        </li>
      </ul>
      <form class="navbar-form navbar-left">
        <div class="form-group">
          <input type="text" class="form-control" placeholder="Search">
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>
      <ul class="nav navbar-nav navbar-right">
        <li><a href="#">Link</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown </a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Separated link</a></li>
          </ul>
        </li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

澳门新萄京官方网站 70

其中有一些部分,比如搜索框是我们目前还不需要的,需要将多余的内容裁剪掉。同时,有一些名称和url地址等需要按我们的实际内容修改。最终导航条的代码如下:

<nav class="navbar navbar-default">
      <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav" aria-expanded="false">
            切换导航条



          </button>
          <a class="navbar-brand" href="#">Mysite</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="my-nav">
          <ul class="nav navbar-nav">
            <li class="active"><a href="/index/">主页</a></li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            <li><a href="/login/">登录</a></li>
            <li><a href="/register/">注册</a></li>
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->
    </nav>

密码重置完成视图

templates/password_reset_done.html

{% extends 'base_accounts.html' %}{% block title %}Reset your password{% endblock %}{% block content %} <div > <div > <div > <div > <h3 >Reset your password</h3> <p>Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.</p> <a href="{% url 'login' %}" >Return to log in</a> </div> </div> </div> </div>{% endblock %}

澳门新萄京官方网站 71

accounts/tests/test_view_password_reset.py

from django.contrib.auth import views as auth_viewsfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCaseclass PasswordResetDoneTests: def setUp: url = reverse('password_reset_done') self.response = self.client.get def test_status_code: self.assertEquals(self.response.status_code, 200) def test_view_function: view = resolve('/reset/done/') self.assertEquals(view.func.view_class, auth_views.PasswordResetDoneView)

5.5.使用Bootstrap静态文件

{% static '相对路径' %}这个Django为我们提供的静态文件加载方法,可以将页面与静态文件链接起来

 最后,base.html内容如下:

{% load staticfiles %}

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>{% block title %}base{% endblock %}</title>

    <!-- Bootstrap -->
    <link href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="http://www.cruity.com/uploads/allimg/191124/062ZC3A-67.jpg"></script>
      <script src="http://www.cruity.com/uploads/allimg/191124/062ZC632-68.jpg"></script>
    <![endif]-->
    {% block css %}{% endblock %}
  </head>
  <body>
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav" aria-expanded="false">
            切换导航条



          </button>
          <a class="navbar-brand" href="#">Mysite</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="my-nav">
          <ul class="nav navbar-nav">
            <li class="active"><a href="/index/">主页</a></li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            <li><a href="/login/">登录</a></li>
            <li><a href="/register/">注册</a></li>
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->
    </nav>

    {% block content %}{% endblock %}


    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="{% static 'js/jquery-3.2.1.js' %}"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
  </body>
</html>

简要说明:

  • 通过页面顶端的{% load staticfiles %}加载后,才可以使用static方法;
  • 通过{% block title %}base{% endblock %},设置了一个动态的页面title块;
  • 通过{% block css %}{% endblock %},设置了一个动态的css加载块;
  • 通过{% block content %}{% endblock %},为具体页面的主体内容留下接口;
  • 通过{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}将样式文件指向了我们的实际静态文件,下面的js脚本也是同样的道理。

 看下效果

澳门新萄京官方网站 72

密码重置确认视图

templates/password_reset_confirm.html

{% extends 'base_accounts.html' %}{% block title %} {% if validlink %} Change password for {{ form.user.username }} {% else %} Reset your password {% endif %}{% endblock %}{% block content %} <div > <div > <div > <div > {% if validlink %} <h3 >Change password for @{{ form.user.username }}</h3> <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Change password</button> </form> {% else %} <h3 >Reset your password</h3> <div role="alert"> It looks like you clicked on an invalid password reset link. Please try again. </div> <a href="{% url 'password_reset' %}" >Request a new password reset link</a> {% endif %} </div> </div> </div> </div>{% endblock %}

这个页面只能通过电子邮件访问,它看起来像这样:

在开发阶段,从控制台中的电子邮件获取此链接。

如果链接是有效的:

澳门新萄京官方网站 73

倘若链接已经被使用:

澳门新萄京官方网站 74

accounts/tests/test_view_password_reset.py

from django.contrib.auth.tokens import default_token_generatorfrom django.utils.encoding import force_bytesfrom django.utils.http import urlsafe_base64_encodefrom django.contrib.auth import views as auth_viewsfrom django.contrib.auth.forms import SetPasswordFormfrom django.contrib.auth.models import Userfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCaseclass PasswordResetConfirmTests: def setUp: user = User.objects.create_user(username='john', email='john@doe.com', password='123abcdef') ''' create a valid password reset token based on how django creates the token internally: https://github.com/django/django/blob/1.11.5/django/contrib/auth/forms.py#L280 ''' self.uid = urlsafe_base64_encode(force_bytes.decode() self.token = default_token_generator.make_token url = reverse('password_reset_confirm', kwargs={'uidb64': self.uid, 'token': self.token}) self.response = self.client.get(url, follow=True) def test_status_code: self.assertEquals(self.response.status_code, 200) def test_view_function: view = resolve('/reset/{uidb64}/{token}/'.format(uidb64=self.uid, token=self.token)) self.assertEquals(view.func.view_class, auth_views.PasswordResetConfirmView) def test_csrf: self.assertContains(self.response, 'csrfmiddlewaretoken') def test_contains_form: form = self.response.context.get self.assertIsInstance(form, SetPasswordForm) def test_form_inputs: ''' The view must contain two inputs: csrf and two password fields ''' self.assertContains(self.response, '<input', 3) self.assertContains(self.response, 'type="password"', 2)class InvalidPasswordResetConfirmTests: def setUp: user = User.objects.create_user(username='john', email='john@doe.com', password='123abcdef') uid = urlsafe_base64_encode(force_bytes.decode() token = default_token_generator.make_token ''' invalidate the token by changing the password ''' user.set_password('abcdef123') user.save() url = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token}) self.response = self.client.get def test_status_code: self.assertEquals(self.response.status_code, 200) def test_html: password_reset_url = reverse('password_reset') self.assertContains(self.response, 'invalid password reset link') self.assertContains(self.response, 'href="{0}"'.format(password_reset_url))

5.6.设计登录页面

Bootstarp提供了一个基本的表单样式,代码如下:

<form>
  <div class="form-group">
    <label for="exampleInputEmail1">Email address</label>
    <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Email">
  </div>
  <div class="form-group">
    <label for="exampleInputPassword1">Password</label>
    <input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
  </div>
  <div class="form-group">
    <label for="exampleInputFile">File input</label>
    <input type="file" id="exampleInputFile">
    <p class="help-block">Example block-level help text here.</p>
  </div>
  <div class="checkbox">
    <label>
      <input type="checkbox"> Check me out
    </label>
  </div>
  <button type="submit" class="btn btn-default">Submit</button>
</form>

如下:

澳门新萄京官方网站 75

我们结合Bootstrap和前面自己写的form表单,修改login/templates/login/login.html成符合项目要求的样子:

{% extends 'login/base.html' %}
{% load staticfiles %}
{% block title %}登录{% endblock %}
{% block css %}
    <link rel="stylesheet" href="{% static 'css/login.css' %}">
{% endblock %}


{% block content %}
    <div class="container">
        <div class="col-md-4 col-md-offset-4">
          <form class='form-login' action="/login/" method="post">
              <h2 class="text-center">欢迎登录</h2>
              <div class="form-group">
                <label for="id_username">用户名:</label>
                <input type="text" name='username' class="form-control" id="id_username" placeholder="Username" autofocus required>
              </div>
              <div class="form-group">
                <label for="id_password">密码:</label>
                <input type="password" name='password' class="form-control" id="id_password" placeholder="Password" required>
              </div>
              <button type="reset" class="btn btn-default pull-left">重置</button>
              <button type="submit" class="btn btn-primary pull-right">提交</button>
          </form>
        </div>
    </div> <!-- /container -->
{% endblock %}

说明:

  • 通过{% extends 'base.html' %}继承了‘base.html’模板的内容;
  • 通过{% block title %}登录{% endblock %}设置了专门的title;
  • 通过block css引入了针对性的login.css样式文件;
  • 主体内容定义在block content内部
  • 添加了一个重置按钮。

static/css目录中新建一个login.css样式文件,这里简单地写了点样式,

body {
  background-color: #eee;
}
.form-login {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}
.form-login .form-control {
  position: relative;
  height: auto;
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
          box-sizing: border-box;
  padding: 10px;
  font-size: 16px;
}
.form-login .form-control:focus {
  z-index: 2;
}
.form-login input[type="text"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-login input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

最后效果

澳门新萄京官方网站 76

密码重置完成视图

templates/password_reset_complete.html

{% extends 'base_accounts.html' %}{% block title %}Password changed!{% endblock %}{% block content %} <div > <div > <div > <div > <h3 >Password changed!</h3> <div role="alert"> You have successfully changed your password! You may now proceed to log in. </div> <a href="{% url 'login' %}" >Return to log in</a> </div> </div> </div> </div>{% endblock %}

澳门新萄京官方网站 77

accounts/tests/test_view_password_reset.py (view complete file contents)

from django.contrib.auth import views as auth_viewsfrom django.core.urlresolvers import reversefrom django.urls import resolvefrom django.test import TestCaseclass PasswordResetCompleteTests: def setUp: url = reverse('password_reset_complete') self.response = self.client.get def test_status_code: self.assertEquals(self.response.status_code, 200) def test_view_function: view = resolve('/reset/complete/') self.assertEquals(view.func.view_class, auth_views.PasswordResetCompleteView)

 六、登录视图

密码更改视图

此视图旨在提供给希望更改其密码的登录用户使用。通常,这些表单由三个字段组成:旧密码、新密码、新密码确认。

myproject/urls.py (view complete file contents)

url(r'^settings/password/$', auth_views.PasswordChangeView.as_view(template_name='password_change.html'), name='password_change'),url(r'^settings/password/done/$', auth_views.PasswordChangeDoneView.as_view(template_name='password_change_done.html'), name='password_change_done'),

这些视图仅适合登录用户,他们使用名为 @login_required的装饰器,此装饰器可防止非授权用户访问此页面。如果用户没有登录,Django会将他们重定向到登录页面。

现在我们必须在settings.py中定义我们应用程序的登录URL:

myproject/settings.py (view complete file contents)

LOGIN_URL = 'login'

templates/password_change.html

{% extends 'base.html' %}{% block title %}Change password{% endblock %}{% block breadcrumb %} <li >Change password</li>{% endblock %}{% block content %} <div > <div > <form method="post" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" >Change password</button> </form> </div> </div>{% endblock %}

澳门新萄京官方网站 78

templates/password_change_done.html

{% extends 'base.html' %}{% block title %}Change password successful{% endblock %}{% block breadcrumb %} <li ><a href="{% url 'password_change' %}">Change password</a></li> <li >Success</li>{% endblock %}{% block content %} <div role="alert"> <strong>Success!</strong> Your password has been changed! </div> <a href="{% url 'home' %}" >Return to home page</a>{% endblock %}

澳门新萄京官方网站 79

关于密码更改视图,我们可以执行类似的测试用例,就像我们迄今为止所做的那样。创建一个名为test_view_password_change.py的新测试文件。

我将在下面列出新的测试类型。你可以检查我为密码更改视图编写的所有测试,然后单击代码段旁边的查看文正文件内容链接。大部分测试与我们迄今为止所做的相似。我转移到一个外部文件以避免太过于复杂。

accounts/tests/test_view_password_change.py (view complete file contents)

class LoginRequiredPasswordChangeTests: def test_redirection: url = reverse('password_change') login_url = reverse response = self.client.get self.assertRedirects(response, f'{login_url}?next={url}')

上面的测试尝试访问password_change视图而不登录。预期的行为是将用户重定向到登录页面。

accounts/tests/test_view_password_change.py (view complete file contents)

class PasswordChangeTestCase: def setUp(self, data={}): self.user = User.objects.create_user(username='john', email='john@doe.com', password='old_password') self.url = reverse('password_change') self.client.login(username='john', password='old_password') self.response = self.client.post(self.url, data)

在这里我们定义了一个名为PasswordChangeTestCase的新类。它将进行基本的设置,创建用户并向 password_change视图发送一个POST 请求。在下一组测试用例中,我们将使用这个类而不是 TestCase类来测试成功请求和无效请求:

accounts/tests/test_view_password_change.py (view complete file contents)

class SuccessfulPasswordChangeTests(PasswordChangeTestCase): def setUp: super().setUp({ 'old_password': 'old_password', 'new_password1': 'new_password', 'new_password2': 'new_password', }) def test_redirection: ''' A valid form submission should redirect the user ''' self.assertRedirects(self.response, reverse('password_change_done')) def test_password_changed: ''' refresh the user instance from database to get the new password hash updated by the change password view. ''' self.user.refresh_from_db() self.assertTrue(self.user.check_password('new_password')) def test_user_authentication: ''' Create a new request to an arbitrary page. The resulting response should now have an `user` to its context, after a successful sign up. ''' response = self.client.get(reverse user = response.context.get self.assertTrue(user.is_authenticated)class InvalidPasswordChangeTests(PasswordChangeTestCase): def test_status_code: ''' An invalid form submission should return to the same page ''' self.assertEquals(self.response.status_code, 200) def test_form_errors: form = self.response.context.get self.assertTrue(form.errors) def test_didnt_change_password: ''' refresh the user instance from the database to make sure we have the latest data. ''' self.user.refresh_from_db() self.assertTrue(self.user.check_password('old_password'))

refresh_from_db()方法确保我们拥有最新的数据状态。它强制Django再次查询数据库以更新数据。考虑到change_password视图会更新数据库中的密码,我们必须这样做。为了查看测试密码是否真的改变了,我们必须从数据库中获取最新的数据。

对于大多数Django应用程序,身份验证是一种非常常见的用例。在本教程中,我们实现了所有重要视图:注册、登录、注销、密码重置和更改密码。现在我们有了一种方法来创建用户并进行身份验证,我们将能够继续开发应用程序和其他视图。

我们仍然需要改进很多关于代码设计的问题:模板文件夹开始变得乱七八糟。 boards 应用测试仍然是混乱的。此外,我们必须开始重构新的主题视图,因为现在我们可以检索登录的用户。我们很快就将做到这一点。

我希望你喜欢本教程系列的第四部分!第五部分将于2017年10月2日下周发布,如果您希望在第五部分结束的时候收到通过,请您订阅我们的邮件列表。

该项目的源代码在GitHub上面可用,项目的当前状态在发布标签v0.4-lw下可以找到。链接如下:

6.1.登录视图

根据我们在路由中的设计,用户通过login.html中的表单填写用户名和密码,并以POST的方式发送到服务器的/login/地址。服务器通过login/views.py中的login()视图函数,接收并处理这一请求。

我们可以通过下面的方法接收和处理请求:

def login(request):
    if request.method == "POST":
        username = request.POST.get('username')
        password = request.POST.get('password')        return redirect('/index/')
    return render(request, 'login/login.html')

还需要在前端页面的form表单内添加一个{% csrf_token %}标签:

<form class='form-login' action="/login/" method="post">
  {% csrf_token %}
  <h2 class="text-center">欢迎登录</h2>
  <div class="form-group">
  ......
</form>

进入登录页面,输入用户名,密码然后跳转到index页面。

6.2.数据验证

通过唯一的用户名,使用Django的ORM去数据库中查询用户数据,如果有匹配项,则进行密码对比,如果没有匹配项,说明用户名不存在。如果密码对比错误,说明密码不正确。

def login(request):
    if request.method == "POST":
        username = request.POST.get('username', None)
        password = request.POST.get('password', None)
        if username and password:  # 确保用户名和密码都不为空
            username = username.strip()
            # 用户名字符合法性验证
            # 密码长度验证
            # 更多的其它验证.....
            try:
                user = models.User.objects.get(name=username)
            except:
                return render(request, 'login/login.html')
            if user.password == password:
                return redirect('/index/')
    return render(request, 'login/login.html')

6.3.添加提示信息

上面的代码还缺少很重要的一部分内容,提示信息!无论是登录成功还是失败,用户都没有得到任何提示信息,这显然是不行的。

修改一下login视图:

def login(request):
    if request.method == "POST":
        username = request.POST.get('username', None)
        password = request.POST.get('password', None)
        message = "所有字段都必须填写!"
        if username and password:  # 确保用户名和密码都不为空
            username = username.strip()
            # 用户名字符合法性验证
            # 密码长度验证
            # 更多的其它验证.....
            try:
                user = models.User.objects.get(name=username)
                if user.password == password:
                    return redirect('/index/')
                else:
                    message = "密码不正确!"
            except:
                message = "用户名不存在!"
        return render(request, 'login/login.html', {"message": message})
    return render(request, 'login/login.html')

增加了message变量,用于保存提示信息。当有错误信息的时候,将错误信息打包成一个字典,然后作为第三个参数提供给render()方法。这个数据字典在渲染模板的时候会传递到模板里供你调用。

为了在前端页面显示信息,还需要对login.html进行修改:

{% extends 'login/base.html' %}
{% load staticfiles %}
{% block title %}登录{% endblock %}
{% block css %}
    <link rel="stylesheet" href="{% static 'css/login.css' %}">
{% endblock %}


{% block content %}
    <div class="container">
        <div class="col-md-4 col-md-offset-4">
            <form class='form-login' action="/login/" method="post">

                {% if message %}
                    <div class="alert alert-warning">{{ message }}</div>
                {% endif %}

                {% csrf_token %}
                <h2 class="text-center">欢迎登录</h2>
                <div class="form-group">
                    <label for="id_username">用户名:</label>
                    <input type="text" name='username' class="form-control" id="id_username" placeholder="Username"
                           autofocus required>
                </div>
                <div class="form-group">
                    <label for="id_password">密码:</label>
                    <input type="password" name='password' class="form-control" id="id_password" placeholder="Password"
                           required>
                </div>
                <button type="reset" class="btn btn-default pull-left">重置</button>
                <button type="submit" class="btn btn-primary pull-right">提交</button>
            </form>
        </div>
    </div> <!-- /container -->
{% endblock %}

index.html主页模板也修改一下,删除原有内容,添加下面的代码:

{#login/templates/login/index.html#}

{% extends 'login/base.html' %}
{% block title %}主页{% endblock %}
{% block content %}
    <h1>欢迎回来!</h1>
{% endblock %}

 七、Django表单

Django的表单给我们提供了下面三个主要功能:

  • 准备和重构数据用于页面渲染;
  • 为数据创建HTML表单元素;
  • 接收和处理用户从表单发送过来的数据

 7.1.创建表单模型

from django import forms


class UserForm(forms.Form):
    username = forms.CharField(label="用户名", max_length=128)
    password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput)

说明:

  • 要先导入forms模块
  • 所有的表单类都要继承forms.Form类
  • 每个表单字段都有自己的字段类型比如CharField,它们分别对应一种HTML语言中<form>内的一个input元素。这一点和Django模型系统的设计非常相似。
  • label参数用于设置<label>标签
  • max_length限制字段输入的最大长度。它同时起到两个作用,一是在浏览器页面限制用户输入不可超过字符数,二是在后端服务器验证用户输入的长度也不可超过。
  • widget=forms.PasswordInput用于指定该字段在form表单里表现为<input type='password' />,也就是密码输入框。

 7.2.修改视图

使用了Django的表单后,就要在视图中进行相应的修改:

# login/views.py

from django.shortcuts import render,redirect
from . import models
from .forms import UserForm

def index(request):
    pass
    return render(request,'login/index.html')

def login(request):
    if request.method == "POST":
        login_form = UserForm(request.POST)
        message = "请检查填写的内容!"
        if login_form.is_valid():
            username = login_form.cleaned_data['username']
            password = login_form.cleaned_data['password']
            try:
                user = models.User.objects.get(name=username)
                if user.password == password:
                    return redirect('/index/')
                else:
                    message = "密码不正确!"
            except:
                message = "用户不存在!"
        return render(request, 'login/login.html', locals())

    login_form = UserForm()
    return render(request, 'login/login.html', locals())

说明:

  • 对于非POST方法发送数据时,比如GET方法请求页面,返回空的表单,让用户可以填入数据;
  • 对于POST方法,接收表单数据,并验证;
  • 使用表单类自带的is_valid()方法一步完成数据验证工作;
  • 验证成功后可以从表单对象的cleaned_data数据字典中获取表单的具体值;
  • 如果验证不通过,则返回一个包含先前数据的表单给前端页面,方便用户修改。也就是说,它会帮你保留先前填写的数据内容,而不是返回一个空表!

另外,这里使用了一个小技巧,Python内置了一个locals()函数,它返回当前所有的本地变量字典,我们可以偷懒的将这作为render函数的数据字典参数值,就不用费劲去构造一个形如{'message':message, 'login_form':login_form}的字典了。这样做的好处当然是大大方便了我们,但是同时也可能往模板传入了一些多余的变量数据,造成数据冗余降低效率。

7.3.修改login界面

Django的表单很重要的一个功能就是自动生成HTML的form表单内容。现在,我们需要修改一下原来的login.html文件:

{% extends 'base.html' %}
{% load staticfiles %}
{% block title %}登录{% endblock %}
{% block css %}<link href="{% static 'css/login.css' %}" rel="stylesheet"/>{% endblock %}


{% block content %}
    <div class="container">
        <div class="col-md-4 col-md-offset-4">
          <form class='form-login' action="/login/" method="post">

              {% if message %}
                  <div class="alert alert-warning">{{ message }}</div>
              {% endif %}
              {% csrf_token %}
              <h2 class="text-center">欢迎登录</h2>

              {{ login_form }}

              <button type="reset" class="btn btn-default pull-left">重置</button>
              <button type="submit" class="btn btn-primary pull-right">提交</button>

          </form>
        </div>
    </div> <!-- /container -->
{% endblock %}

浏览器生成的HTML源码

重新启动服务器,刷新页面,如下图所示:澳门新萄京官方网站 80

澳门新萄京官方网站 81

7.4.手动渲染表单

直接{{ login_form }}虽然好,啥都不用操心,但是界面真的很丑,往往并不是你想要的,如果你要使用CSS和JS,比如你要引入Bootstarps框架,这些都需要对表单内的input元素进行额外控制,那怎么办呢?手动渲染字段就可以了。

可以通过{{ login_form.name_of_field }}获取每一个字段,然后分别渲染,如下例所示:

<div class="form-group">
  {{ login_form.username.label_tag }}
  {{ login_form.username}}
</div>
<div class="form-group">
  {{ login_form.password.label_tag }}
  {{ login_form.password }}
</div>

然后,在form类里添加attr属性即可,如下所示修改login/forms.py

from django import forms

class UserForm(forms.Form):
    username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))
    password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))

再次刷新页面,就显示正常了!

 八、图片验证码

为了防止机器人频繁登录网站或者破坏分子恶意登录,很多用户登录和注册系统都提供了图形验证码功能。

验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试。

图形验证码的历史比较悠久,到现在已经有点英雄末路的味道了。因为机器学习、图像识别的存在,机器人已经可以比较正确的识别图像内的字符了。但不管怎么说,作为一种防御手段,至少还是可以抵挡一些低级入门的攻击手段,抬高了攻击者的门槛。

在Django中实现图片验证码功能非常简单,有现成的第三方库可以使用,我们不必自己开发(也要能开发得出来,囧)。这个库叫做django-simple-captcha

8.1.安装captcha

直接安装:pip install django-simple-captcha

Django自动帮我们安装了相关的依赖库sixolefilePillow,其中的Pillow是大名鼎鼎的绘图模块。

注册captcha

在settings中,将‘captcha’注册到app列表里:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'login',
    'captcha',
]

captcha需要在数据库中建立自己的数据表,所以需要执行migrate命令生成数据表:

python manage.py migrate

8.2.添加url路由

根目录下的urls.py文件中增加captcha对应的网址:

from django.conf.urls import url
from django.conf.urls import include
from django.contrib import admin
from login import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index),
    url(r'^login/', views.login),
    url(r'^register/', views.register),
    url(r'^logout/', views.logout),
    url(r'^captcha', include('captcha.urls'))  # 增加这一行
]

8.3.修改forms.py

如果上面都OK了,就可以直接在我们的forms.py文件中添加CaptchaField了。

from django import forms
from captcha.fields import CaptchaField

class UserForm(forms.Form):
    username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))
    password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))
    captcha = CaptchaField(label='验证码')

需要提前导入from captcha.fields import CaptchaField,然后就像写普通的form字段一样添加一个captcha字段就可以了!

 8.4.修改login.html

 由于我们前面是手动生成的form表单,所以还要修改一下,添加captcha的相关内容,如下所示:

{% extends 'login/base.html' %}
{% load staticfiles %}
{% block title %}登录{% endblock %}
{% block css %}
    <link rel="stylesheet" href="{% static 'css/login.css' %}">
{% endblock %}


{% block content %}
    <div class="container">
        <div class="col-md-4 col-md-offset-4">
          <form class='form-login' action="/login/" method="post">

              {% if message %}
                  <div class="alert alert-warning">{{ message }}</div>
              {% endif %}
              {% csrf_token %}
              <h2 class="text-center">欢迎登录</h2>
              <div class="form-group">
                  {{ login_form.username.label_tag }}
                  {{ login_form.username}}
              </div>
              <div class="form-group">
                  {{ login_form.password.label_tag }}
                  {{ login_form.password }}
              </div>

              <div class="form-group">
                  {{ login_form.captcha.errors }}
                  {{ login_form.captcha.label_tag }}
                  {{ login_form.captcha }}
              </div>

              <button type="reset" class="btn btn-default pull-left">重置</button>
              <button type="submit" class="btn btn-primary pull-right">提交</button>

          </form>
        </div>
    </div> <!-- /container -->
{% endblock %}

这里额外增加了一条{{ login_form.captcha.errors }}用于明确指示用户,你的验证码不正确

 查看效果:

澳门新萄京官方网站 82

其中验证图形码是否正确的工作都是在后台自动完成的,只需要使用is_valid()这个forms内置的验证方法就一起进行了,完全不需要在视图函数中添加任何的验证代码,非常方便快捷!

 九、session会话

        因为因特网HTTP协议的特性,每一次来自于用户浏览器的请求(request)都是无状态的、独立的。通俗地说,就是无法保存用户状态,后台服务器根本就不知道当前请求和以前及以后请求是否来自同一用户。对于静态网站,这可能不是个问题,而对于动态网站,尤其是京东、天猫、银行等购物或金融网站,无法识别用户并保持用户状态是致命的,根本就无法提供服务。你可以尝试将浏览器的cookie功能关闭,你会发现将无法在京东登录和购物。

为了实现连接状态的保持功能,网站会通过用户的浏览器在用户机器内被限定的硬盘位置中写入一些数据,也就是所谓的Cookie。通过Cookie可以保存一些诸如用户名、浏览记录、表单记录、登录和注销等各种数据。但是这种方式非常不安全,因为Cookie保存在用户的机器上,如果Cookie被伪造、篡改或删除,就会造成极大的安全威胁,因此,现代网站设计通常将Cookie用来保存一些不重要的内容,实际的用户数据和状态还是以Session会话的方式保存在服务器端。

Session依赖Cookie!但与Cookie不同的地方在于Session将所有的数据都放在服务器端,用户浏览器的Cookie中只会保存一个非明文的识别信息,比如哈希值。

Django提供了一个通用的Session框架,并且可以使用多种session数据的保存方式:

  • 保存在数据库内
  • 保存到缓存
  • 保存到文件内
  • 保存到cookie内

通常情况,没有特别需求的话,请使用保存在数据库内的方式,尽量不要保存到Cookie内。

Django的session框架默认启用,并已经注册在app设置内,如果真的没有启用,那么参考下面的内容添加有说明的那两行,再执行migrate命令创建数据表,就可以使用session了。

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',    # 这一行
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',  # 这一行
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

当session启用后,传递给视图request参数的HttpRequest对象将包含一个session属性,就像一个字典对象一样。你可以在Django的任何地方读写request.session属性,或者多次编辑使用它。

下面是session使用参考:

class backends.base.SessionBase
        # 这是所有会话对象的基类,包含标准的字典方法:
        __getitem__(key)
            Example: fav_color = request.session['fav_color']
        __setitem__(key, value)
            Example: request.session['fav_color'] = 'blue'
        __delitem__(key)
            Example: del request.session['fav_color']  # 如果不存在会抛出异常
        __contains__(key)
            Example: 'fav_color' in request.session
        get(key, default=None)
            Example: fav_color = request.session.get('fav_color', 'red')
        pop(key, default=__not_given)
            Example: fav_color = request.session.pop('fav_color', 'blue')

 # 类似字典数据类型的内置方法
        keys()
        items()
        setdefault()
        clear()


        # 它还有下面的方法:
        flush()
            # 删除当前的会话数据和会话cookie。经常用在用户退出后,删除会话。

        set_test_cookie()
            # 设置一个测试cookie,用于探测用户浏览器是否支持cookies。由于cookie的工作机制,你只有在下次用户请求的时候才可以测试。
        test_cookie_worked()
            # 返回True或者False,取决于用户的浏览器是否接受测试cookie。你必须在之前先调用set_test_cookie()方法。
        delete_test_cookie()
            # 删除测试cookie。
        set_expiry(value)
            # 设置cookie的有效期。可以传递不同类型的参数值:
        • 如果值是一个整数,session将在对应的秒数后失效。例如request.session.set_expiry(300) 将在300秒后失效.
        • 如果值是一个datetime或者timedelta对象, 会话将在指定的日期失效
        • 如果为0,在用户关闭浏览器后失效
        • 如果为None,则将使用全局会话失效策略
        失效时间从上一次会话被修改的时刻开始计时。

        get_expiry_age()
            # 返回多少秒后失效的秒数。对于没有自定义失效时间的会话,这等同于SESSION_COOKIE_AGE.
            # 这个方法接受2个可选的关键字参数
        • modification:会话的最后修改时间(datetime对象)。默认是当前时间。
        •expiry: 会话失效信息,可以是datetime对象,也可以是int或None

        get_expiry_date()
            # 和上面的方法类似,只是返回的是日期

        get_expire_at_browser_close()
            # 返回True或False,根据用户会话是否是浏览器关闭后就结束。

        clear_expired()
            # 删除已经失效的会话数据。
        cycle_key()
            # 创建一个新的会话秘钥用于保持当前的会话数据。django.contrib.auth.login() 会调用这个方法。

9.1.使用session

首先,修改login/views.py中的login()视图函数:

def login(request):
    if request.session.get('is_login',None):
        return redirect('/index')

    if request.method == "POST":
        login_form = UserForm(request.POST)
        message = "请检查填写的内容!"
        if login_form.is_valid():
            username = login_form.cleaned_data['username']
            password = login_form.cleaned_data['password']
            try:
                user = models.User.objects.get(name=username)
                if user.password == password:
                    request.session['is_login'] = True
                    request.session['user_id'] = user.id
                    request.session['user_name'] = user.name
                    return redirect('/index/')
                else:
                    message = "密码不正确!"
            except:
                message = "用户不存在!"
        return render(request, 'login/login.html', locals())

    login_form = UserForm()
    return render(request, 'login/login.html', locals())

通过下面的if语句,我们不允许重复登录:

if request.session.get('is_login',None):
    return redirect("/index/")

通过下面的语句,我们往session字典内写入用户状态和数据:

request.session['is_login'] = True
request.session['user_id'] = user.id
request.session['user_name'] = user.name

你完全可以往里面写任何数据,不仅仅限于用户相关!

既然有了session记录用户登录状态,那么就可以完善我们的登出视图函数了:

def logout(request):
    if not request.session.get('is_login', None):
        # 如果本来就未登录,也就没有登出一说
        return redirect("/index/")
    request.session.flush()
    # 或者使用下面的方法
    # del request.session['is_login']
    # del request.session['user_id']
    # del request.session['user_name']
    return redirect("/index/")

flush()方法是比较安全的一种做法,而且一次性将session中的所有内容全部清空,确保不留后患。但也有不好的地方,那就是如果你在session中夹带了一点‘私货’,会被一并删除,这一点一定要注意。

 9.2.完善页面

有了用户状态,就可以根据用户登录与否,展示不同的页面,比如导航条内容:

首先,修改base.html文件:

 <div class="collapse navbar-collapse" id="my-nav">
          <ul class="nav navbar-nav">
            <li class="active"><a href="/index/">主页</a></li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
              {% if request.session.is_login %}
                  <li><a href="#">当前在线:{{ request.session.user_name }}</a></li>
                  <li><a href="/logout/">登出</a></li>
              {% else %}
                  <li><a href="/login/">登录</a></li>
                  <li><a href="/register/">注册</a></li>
              {% endif %}
          </ul>
        </div><!-- /.navbar-collapse -->
      </div><!-- /.container-fluid -->

通过if判断,当登录时,显示当前用户名和登出按钮。未登录时,显示登录和注册按钮。

注意其中的模板语言,{{ request }}这个变量会被默认传入模板中,可以通过圆点的调用方式,获取它内部的{{ request.session }},再进一步的获取session中的内容。其实{{ request }}中的数据远不止此,例如{{ request.path }}就可以获取先前的url地址。

再修改一下index.html页面,根据登录与否的不同,显示不同的内容:

{% extends 'base.html' %}
{% block title %}主页{% endblock %}
{% block content %}
    {% if request.session.is_login %}
    <h1>你好,{{ request.session.user_name }}!欢迎回来!</h1>
    {% else %}
    <h1>你尚未登录,只能访问公开内容!</h1>
    {% endif %}
{% endblock %}

看下效果:

澳门新萄京官方网站 83

澳门新萄京官方网站 84

 

 十、注册视图

 10.1.创建forms

/login/forms.py中添加一个新的表单类:

class RegisterForm(forms.Form):
    gender = (
        ('male', "男"),
        ('female', "女"),
    )
    username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))
    password1 = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))
    password2 = forms.CharField(label="确认密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))
    email = forms.EmailField(label="邮箱地址", widget=forms.EmailInput(attrs={'class': 'form-control'}))
    sex = forms.ChoiceField(label='性别', choices=gender)
    captcha = CaptchaField(label='验证码')

说明:

  • gender和User模型中的一样,其实可以拉出来作为常量共用,为了直观,特意重写一遍;
  • password1和password2,用于输入两遍密码,并进行比较,防止误输密码;
  • email是一个邮箱输入框;
  • sex是一个select下拉框;

 10.2.完善register.html

同样地,类似login.html文件,我们在register.html中编写forms相关条目:

{% extends 'login/base.html' %}

{% block title %}注册{% endblock %}
{% block content %}
    <div class="container">
        <div class="col-md-4 col-md-offset-4">
          <form class='form-register' action="/register/" method="post">

              {% if message %}
                  <div class="alert alert-warning">{{ message }}</div>
              {% endif %}

              {% csrf_token %}

              <h2 class="text-center">欢迎注册</h2>
              <div class="form-group">
                  {{ register_form.username.label_tag }}
                  {{ register_form.username}}
              </div>
              <div class="form-group">
                  {{ register_form.password1.label_tag }}
                  {{ register_form.password1 }}
              </div>
              <div class="form-group">
                  {{ register_form.password2.label_tag }}
                  {{ register_form.password2 }}
              </div>
              <div class="form-group">
                  {{ register_form.email.label_tag }}
                  {{ register_form.email }}
              </div>
              <div class="form-group">
                  {{ register_form.sex.label_tag }}
                  {{ register_form.sex }}
              </div>
              <div class="form-group">
                  {{ register_form.captcha.errors }}
                  {{ register_form.captcha.label_tag }}
                  {{ register_form.captcha }}
              </div>

              <button type="reset" class="btn btn-default pull-left">重置</button>
              <button type="submit" class="btn btn-primary pull-right">提交</button>

          </form>
        </div>
    </div> <!-- /container -->
{% endblock %}

10.3.注册视图

进入/login/views.py文件,现在来完善我们的register()视图:

def register(request):
    if request.session.get('is_login', None):
        # 登录状态不允许注册。你可以修改这条原则!
        return redirect("/index/")
    if request.method == "POST":
        register_form = RegisterForm(request.POST)
        message = "请检查填写的内容!"
        if register_form.is_valid():  # 获取数据
            username = register_form.cleaned_data['username']
            password1 = register_form.cleaned_data['password1']
            password2 = register_form.cleaned_data['password2']
            email = register_form.cleaned_data['email']
            sex = register_form.cleaned_data['sex']
            if password1 != password2:  # 判断两次密码是否相同
                message = "两次输入的密码不同!"
                return render(request, 'login/register.html', locals())
            else:
                same_name_user = models.User.objects.filter(name=username)
                if same_name_user:  # 用户名唯一
                    message = '用户已经存在,请重新选择用户名!'
                    return render(request, 'login/register.html', locals())
                same_email_user = models.User.objects.filter(email=email)
                if same_email_user:  # 邮箱地址唯一
                    message = '该邮箱地址已被注册,请使用别的邮箱!'
                    return render(request, 'login/register.html', locals())

                # 当一切都OK的情况下,创建新用户

                new_user = models.User.objects.create()
                new_user.name = username
                new_user.password = password1
                new_user.email = email
                new_user.sex = sex
                new_user.save()
                return redirect('/login/')  # 自动跳转到登录页面
    register_form = RegisterForm()
    return render(request, 'login/register.html', locals())

从大体逻辑上,也是先实例化一个RegisterForm的对象,然后使用is_valide()验证数据,再从cleaned_data中获取数据。

重点在于注册逻辑,首先两次输入的密码必须相同,其次不能存在相同用户名和邮箱,最后如果条件都满足,利用ORM的API,创建一个用户实例,然后保存到数据库内。

看一下注册的页面:

澳门新萄京官方网站 85

注册成功在admin后台可以看到注册的用户

澳门新萄京官方网站 86

10.4.密码加密

用户注册的密码应该加密才对

对于如何加密密码,有很多不同的途径,其安全程度也高低不等。这里我们使用Python内置的hashlib库,使用哈希值的方式加密密码,可能安全等级不够高,但足够简单,方便使用,不是么?

首先在login/views.py中编写一个hash函数:

import hashlib

def hash_code(s, salt='mysite'):# 加点盐
    h = hashlib.sha256()
    s  = salt
    h.update(s.encode())  # update方法只接收bytes类型
    return h.hexdigest()

然后,我们还要对login()和register()视图进行一下修改:

#login.html

if user.password == hash_code(password):  # 哈希值和数据库内的值进行比对

#register.html

new_user.password = hash_code(password1)  # 使用加密密码

澳门新萄京官方网站 87澳门新萄京官方网站 88

# login/views.py

from django.shortcuts import render,redirect
from . import models
from .forms import UserForm,RegisterForm
import hashlib

def index(request):
    pass
    return render(request,'login/index.html')

def login(request):
    if request.session.get('is_login', None):
        return redirect("/index/")
    if request.method == "POST":
        login_form = UserForm(request.POST)
        message = "请检查填写的内容!"
        if login_form.is_valid():
            username = login_form.cleaned_data['username']
            password = login_form.cleaned_data['password']
            try:
                user = models.User.objects.get(name=username)
                if user.password == hash_code(password):  # 哈希值和数据库内的值进行比对
                    request.session['is_login'] = True
                    request.session['user_id'] = user.id
                    request.session['user_name'] = user.name
                    return redirect('/index/')
                else:
                    message = "密码不正确!"
            except:
                message = "用户不存在!"
        return render(request, 'login/login.html', locals())

    login_form = UserForm()
    return render(request, 'login/login.html', locals())


def register(request):
    if request.session.get('is_login', None):
        # 登录状态不允许注册。你可以修改这条原则!
        return redirect("/index/")
    if request.method == "POST":
        register_form = RegisterForm(request.POST)
        message = "请检查填写的内容!"
        if register_form.is_valid():  # 获取数据
            username = register_form.cleaned_data['username']
            password1 = register_form.cleaned_data['password1']
            password2 = register_form.cleaned_data['password2']
            email = register_form.cleaned_data['email']
            sex = register_form.cleaned_data['sex']
            if password1 != password2:  # 判断两次密码是否相同
                message = "两次输入的密码不同!"
                return render(request, 'login/register.html', locals())
            else:
                same_name_user = models.User.objects.filter(name=username)
                if same_name_user:  # 用户名唯一
                    message = '用户已经存在,请重新选择用户名!'
                    return render(request, 'login/register.html', locals())
                same_email_user = models.User.objects.filter(email=email)
                if same_email_user:  # 邮箱地址唯一
                    message = '该邮箱地址已被注册,请使用别的邮箱!'
                    return render(request, 'login/register.html', locals())

                # 当一切都OK的情况下,创建新用户

                new_user = models.User.objects.create()
                new_user.name = username
                new_user.password = hash_code(password1)  # 使用加密密码
                new_user.email = email
                new_user.sex = sex
                new_user.save()
                return redirect('/login/')  # 自动跳转到登录页面
    register_form = RegisterForm()
    return render(request, 'login/register.html', locals())

def logout(request):
    if not request.session.get('is_login',None):
        return redirect('/index/')
    request.session.flush()

    return redirect('/index/')

def hash_code(s, salt='mysite_login'):
    h = hashlib.sha256()
    s  = salt
    h.update(s.encode())  # update方法只接收bytes类型
    return h.hexdigest()

views.py全部代码

重启服务器,进入注册页面,新建一个用户,然后进入admin后台,查看用户的密码情况:

澳门新萄京官方网站 89

 再使用该用户登录一下,大功告成!

可以看到密码长度根据你哈希算法的不同,已经变得很长了,所以前面model中设置password字段时,不要想当然的将max_length设置为16这么小的数字。

 

本文由澳门新萄京官方网站发布于www.8455.com,转载请注明出处:澳门新萄京官方网站:Django用户登录与注册系统

关键词: