使用 Python 和 Flask 构建 CRUD Web 应用程序 – 第一部分

在这个由三部分组成的教程中,我们将使用Flask(一个 Python 微框架)构建一个 CRUD(创建、读取、更新、删除)员工管理 Web 应用程序我将应用程序命名为 Project Dream Team,它将具有以下功能:

  1. 用户将能够以员工身份注册和登录
  2. 管理员将能够创建、更新和删除部门和角色
  3. 管理员将能够将员工分配到部门并为其分配角色
  4. 管理员将能够查看所有员工及其详细信息

第一部分将涵盖:

  1. 数据库设置
  2. 楷模
  3. 移民
  4. 主页
  5. 验证

准备好?开始了!

先决条件

本教程建立在我的介绍性教程Flask 入门的基础上,它停止的地方开始。它假设您首先安装了以下依赖项:

  1. 蟒蛇 2.7
  2. 烧瓶
  3. virtualenv(和,可选,virtualenvwrapper

您应该设置并激活了一个虚拟环境。您还应该具有以下文件和目录结构:

├── dream-team
       ├── app
       │   ├── __init__.py
       │   ├── templates
       │   ├── models.py
       │   └── views.py
       ├── config.py
       ├── requirements.txt
       └── run.py

此项目结构将应用程序的相似组件组合在一起。dream-team目录包含所有项目文件。app目录是应用程序包,包含应用程序的不同但相互关联的模块。所有模板都存储在templates目录中,所有模型都在models.py文件中,所有路由都在views.py文件中。run.py文件是应用程序的入口点,该config.py文件包含应用程序配置,该requirements.txt文件包含应用程序的软件依赖项。

如果您没有这些设置,请访问介绍性教程并赶上!

数据库设置

Flask 支持多种关系数据库管理系统,包括SQLiteMySQLPostgreSQL在本教程中,我们将使用 MySQL。它很受欢迎,因此除了具有可扩展性、安全性和丰富的功能之外,还有很多支持。

我们将安装以下(记得激活你的虚拟环境):

  1. Flask-SQLAlchemy:这将允许我们使用SQLAlchemy,这是一个用于 SQL 与 Python一起使用的有用工具。SQLAlchemy 是一个对象关系映射器 (ORM),这意味着它将应用程序的对象连接到关系数据库管理系统中的表。这些对象可以存储在数据库中,无需编写原始 SQL 即可访问。这很方便,因为它简化了用原始 SQL 编写可能很复杂的查询。此外,它降低了SQL 注入攻击的风险,因为我们不处理原始 SQL 的输入。

  2. MySQL-Python:这是 MySQL 的 Python 接口。它将帮助我们将 MySQL 数据库连接到应用程序。

$ pip install flask-sqlalchemy mysql-python

然后我们将创建 MySQL 数据库。确保您已安装并运行 MySQL,然后以 root 用户身份登录:

$ mysql -u root

mysql> CREATE USER 'dt_admin'@'localhost' IDENTIFIED BY 'dt2016';
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE DATABASE dreamteam_db;
Query OK, 1 row affected (0.00 sec)

mysql> GRANT ALL PRIVILEGES ON dreamteam_db . * TO 'dt_admin'@'localhost';
Query OK, 0 rows affected (0.00 sec)

我们现在dt_admin使用密码创建了一个新用户dt2016,创建了一个新数据库dreamteam_db,并授予新用户所有数据库权限。

接下来,让我们编辑config.py. 删除任何现有代码并添加以下内容:

# config.py

class Config(object):
    """
    Common configurations
    """

    # Put any configurations here that are common across all environments


class DevelopmentConfig(Config):
    """
    Development configurations
    """

    DEBUG = True
    SQLALCHEMY_ECHO = True


class ProductionConfig(Config):
    """
    Production configurations
    """

    DEBUG = False

app_config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig
}

为不同的环境指定配置是一种很好的做法。在上面的文件中,我们指定了开发配置,我们将在构建应用程序并在本地运行时使用这些配置,以及在部署应用程序时将使用的生产配置。

一些有用的配置变量是:

  1. TESTING: 设置此项以True激活 Flask 扩展的测试模式。这允许我们使用可能会增加运行时成本的测试属性,例如单元测试助手。应该True在测试配置中设置为。它默认为False.
  2. DEBUG:设置此项以True激活应用程序上的调试模式。这允许我们在出现未处理的异常时使用 Flask 调试器,并在应用程序更新时自动重新加载应用程序。但是,它应该始终False在生产中设置它默认为False.
  3. SQLALCHEMY_ECHOTrue通过允许 SQLAlchemy 记录错误帮助我们进行调试。

你可以找到更多瓶配置变量在这里和SQLAlchemy的配置变量在这里

接下来,在instance目录中创建一个目录dream-team,然后config.py在里面创建一个文件。我们将在这里放置配置变量,由于它们的敏感性,它们不会被推送到版本控制。在这种情况下,我们放置密钥以及包含数据库用户密码的数据库 URI。

# instance/config.py

SECRET_KEY = 'p9Bv<3Eid9%$i01'
SQLALCHEMY_DATABASE_URI = 'mysql://dt_admin:dt2016@localhost/dreamteam_db'

现在,让我们编辑app/__init__.py文件。删除任何现有代码并添加以下内容:

# app/__init__.py

# third-party imports
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# local imports
from config import app_config

# db variable initialization
db = SQLAlchemy()


def create_app(config_name):
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_object(app_config[config_name])
    app.config.from_pyfile('config.py')
    db.init_app(app)

    return app

我们已经创建了一个函数, create_app给定一个配置名称,它从config.py文件中加载正确的配置,以及文件中的配置instance/config.py我们还创建了一个db用于与数据库交互对象。

接下来,让我们编辑run.py文件:

# run.py

import os

from app import create_app

config_name = os.getenv('FLASK_CONFIG')
app = create_app(config_name)


if __name__ == '__main__':
    app.run()

我们通过运行create_app函数并传入配置名称来创建应用程序我们从操作系统环境变量中得到它FLASK_CONFIG因为我们在开发中,我们应该将环境变量设置为development.

让我们运行应用程序以确保一切都按预期工作。首先,删除app/views.py文件和app/templates目录,因为我们将不再需要它们。接下来,将临时路由添加到app/__init__.py文件中,如下所示:

# app/__init__.py

# existing code remains


def create_app(config_name):
    # existing code remains

    # temporary route
    @app.route('/')
    def hello_world():
        return 'Hello, World!'

    return app

确保在运行应用程序之前设置FLASK_CONFIGFLASK_APP环境变量:

$ export FLASK_CONFIG=development
$ export FLASK_APP=run.py
$ flask run
 * Serving Flask app "run"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

我们可以看到我们在路由中设置的“Hello, World”字符串。到目前为止,该应用程序运行良好。

楷模

现在开始处理模型。请记住,模型是代码中数据库表的表示。我们需要三种型号:EmployeeDepartment,和Role

但首先,让我们安装Flask-Login,它将帮助我们管理用户并处理登录、注销和用户会话。Employee模型将从 Flask-Login 的UserMixin继承,这将使我们更容易使用它的属性和方法。

$ pip install flask-login

要使用 Flask-Login,我们需要创建一个 LoginManager 对象并在app/__init__.py文件中对其进行初始化首先,删除我们之前添加的路由,然后添加以下内容:

# app/__init__.py

# after existing third-party imports
from flask_login import LoginManager

# after the db variable initialization
login_manager = LoginManager()


def create_app(config_name):
    # existing code remains

    login_manager.init_app(app)
    login_manager.login_message = "You must be logged in to access this page."
    login_manager.login_view = "auth.login"

    return app

除了初始化 LoginManager 对象之外,我们还向它添加了一个login_viewlogin_message这样,如果用户尝试访问他们未获授权的页面,它将重定向到指定的视图并显示指定的消息。我们还没有创建auth.login视图,但我们会在进行身份验证时创建。

现在将以下代码添加到app/models.py文件中:

# app/models.py

from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

from app import db, login_manager


class Employee(UserMixin, db.Model):
    """
    Create an Employee table
    """

    # Ensures table will be named in plural and not in singular
    # as is the name of the model
    __tablename__ = 'employees'

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(60), index=True, unique=True)
    username = db.Column(db.String(60), index=True, unique=True)
    first_name = db.Column(db.String(60), index=True)
    last_name = db.Column(db.String(60), index=True)
    password_hash = db.Column(db.String(128))
    department_id = db.Column(db.Integer, db.ForeignKey('departments.id'))
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    is_admin = db.Column(db.Boolean, default=False)

    @property
    def password(self):
        """
        Prevent pasword from being accessed
        """
        raise AttributeError('password is not a readable attribute.')

    @password.setter
    def password(self, password):
        """
        Set password to a hashed password
        """
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        """
        Check if hashed password matches actual password
        """
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return '<Employee: {}>'.format(self.username)


# Set up user_loader
@login_manager.user_loader
def load_user(user_id):
    return Employee.query.get(int(user_id))


class Department(db.Model):
    """
    Create a Department table
    """

    __tablename__ = 'departments'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(60), unique=True)
    description = db.Column(db.String(200))
    employees = db.relationship('Employee', backref='department',
                                lazy='dynamic')

    def __repr__(self):
        return '<Department: {}>'.format(self.name)


class Role(db.Model):
    """
    Create a Role table
    """

    __tablename__ = 'roles'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(60), unique=True)
    description = db.Column(db.String(200))
    employees = db.relationship('Employee', backref='role',
                                lazy='dynamic')

    def __repr__(self):
        return '<Role: {}>'.format(self.name)

Employee模型中,我们使用了 Werkzeug 的一些方便的安全辅助方法,generate_password_hash它允许我们对密码进行散列,而check_password_hash确保散列的密码与密码匹配。为了增强安全性,我们有一种password方法可以确保永远无法访问密码;相反,将引发错误。我们还有两个外键字段department_idrole_id,它们指的是分配给员工的部门和角色的 ID。

请注意,我们有一个默认is_admin设置为字段False我们将在创建管理员用户时覆盖它。Employee模型之后,我们有一个user_loader回调,Flask-Login 使用它从会话中存储的用户 ID 重新加载用户对象。

DepartmentRole模式十分相似。两者都有namedescription字段。此外,两者都与Employee模型具有一对多关系(一个部门或角色可以有多个员工)。我们在两个模型中都使用employees字段来定义它backref允许我们在Employee模型上创建一个新属性,以便我们可以使用employee.departmentemployee.role 获取分配给该员工的部门或角色。lazy定义如何从数据库加载数据;在这种情况下,它将动态加载,这是管理大型集合的理想选择。

移民

迁移允许我们管理对模型所做的更改,并在数据库中传播这些更改。例如,如果稍后我们对其中一个模型中的字段进行更改,我们需要做的就是创建并应用迁移,数据库将反映更改。

我们将从安装Flask-Migrate开始,它将使用轻量级数据库迁移工具 Alembic 处理数据库迁移。AlembicALTER向数据库发出语句,从而实现对模型所做的更改。它还自动生成简约的迁移脚本,编写起来可能很复杂。

$ pip install flask-migrate

我们需要编辑app/__init__.py文件:

# app/__init__.py

# after existing third-party imports
from flask_migrate import Migrate

# existing code remains


def create_app(config_name):
    # existing code remains

    migrate = Migrate(app, db)

    from app import models

    return app

我们已经创建了一个migrate对象,它允许我们使用 Flask-Migrate 运行迁移。我们还从app包中导入了模型接下来,我们将运行以下命令来创建迁移存储库:

$ flask db init

这将在migrations目录中创建一个dream-team目录:

└── migrations
    ├── README
    ├── alembic.ini
    ├── env.py
    ├── script.py.mako
    └── versions

接下来,我们将创建第一个迁移:

$ flask db migrate

最后,我们将应用迁移:

$ flask db upgrade

我们已经根据我们编写的模型成功创建了表!让我们检查 MySQL 数据库以确认这一点:

$ mysql -u root

mysql> use dreamteam_db;

mysql> show tables;
+------------------------+
| Tables_in_dreamteam_db |
+------------------------+
| alembic_version        |
| departments            |
| employees              |
| roles                  |
+------------------------+
4 rows in set (0.00 sec)

蓝图

蓝图非常适合将 Flask 应用程序组织成组件,每个组件都有自己的视图和表单。我发现蓝图使项目结构更清晰、更有条理,因为每个蓝图都是一个独特的组件,用于解决应用程序的特定功能。每个蓝图甚至可以有自己的 cutom URL 前缀或子域。蓝图对于大型应用程序特别方便。

我们将在这个应用程序中拥有三个蓝图:

  1. 主页 – 这将有主页和仪表板视图
  2. 管理员 – 这将包含所有管理员(部门和角色)表单和视图
  3. 身份验证 – 这将包含所有身份验证(注册和登录)表单和视图

创建相关文件和目录,使您的目录结构类似于:

└── dream-team
    ├── app
    │   ├── __init__.py
    │   ├── admin
    │   │   ├── __init__.py
    │   │   ├── forms.py
    │   │   └── views.py
    │   ├── auth
    │   │   ├── __init__.py
    │   │   ├── forms.py
    │   │   └── views.py
    │   ├── home
    │   │   ├── __init__.py
    │   │   └── views.py
    │   ├── models.py
    │   ├── static
    │   └── templates
    ├── config.py
    ├── instance
    │   └── config.py
    ├── migrations
    │   ├── README
    │   ├── alembic.ini
    │   ├── env.py
    │   ├── script.py.mako
    │   └── versions
    │       └── a1a1d8b30202_.py
    ├── requirements.txt
    └── run.py

我选择不为每个蓝图设置statictemplates目录,因为所有应用程序模板都将从相同的基本模板继承并使用相同的 CSS 文件。相反,该templates目录将为每个蓝图提供子目录,以便蓝图模板可以组合在一起。

在每个蓝图的__init__.py文件中,我们需要创建一个蓝图对象并使用名称对其进行初始化。我们还需要导入视图。

# app/admin/__init__.py

from flask import Blueprint

admin = Blueprint('admin', __name__)

from . import views
# app/auth/__init__.py

from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views
# app/home/__init__.py

from flask import Blueprint

home = Blueprint('home', __name__)

from . import views

然后,我们可以在app/__init__.py文件中注册应用程序上的蓝图,如下所示:

# app/__init__.py

# existing code remains


def create_app(config_name):
    # existing code remains

    from app import models

    from .admin import admin as admin_blueprint
    app.register_blueprint(admin_blueprint, url_prefix='/admin')

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    from .home import home as home_blueprint
    app.register_blueprint(home_blueprint)

    return app

我们已经导入了每个蓝图对象并注册了它。对于admin蓝图,我们添加了 url 前缀/admin. 这意味着该蓝图的所有视图都将在浏览器中使用 url 前缀访问admin

家庭蓝图

是时候充实蓝图了!我们将从home蓝图开始,其中包含主页和仪表板。

# app/home/views.py

from flask import render_template
from flask_login import login_required

from . import home


@home.route('/')
def homepage():
    """
    Render the homepage template on the / route
    """
    return render_template('home/index.html', title="Welcome")


@home.route('/dashboard')
@login_required
def dashboard():
    """
    Render the dashboard template on the /dashboard route
    """
    return render_template('home/dashboard.html', title="Dashboard")

每个视图函数都有一个装饰器,home.route,它有一个 URL 路由作为参数(记住这homeapp/home/__init__.py文件中指定的蓝图的名称)。每个视图处理对指定 URL 的请求。

homepage视图呈现家里模板,而dashboard视图呈现仪表板模板。请注意,该dashboard视图有一个login_required装饰器,这意味着用户必须登录才能访问它。

现在处理基本模板,所有其他模板都将从该模板继承。base.htmlapp/templates目录中创建一个文件并添加以下代码:

<!-- app/templates/base.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{ title }} | Project Dream Team</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
    <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
</head>
<body>
    <nav class="navbar navbar-default navbar-fixed-top topnav" role="navigation">
        <div class="container topnav">
          <div class="navbar-header">
              <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                  <span class="sr-only">Toggle navigation</span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
                  <span class="icon-bar"></span>
              </button>
              <a class="navbar-brand topnav" href="{{ url_for('home.homepage') }}">Project Dream Team</a>
          </div>
          <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
              <ul class="nav navbar-nav navbar-right">
                  <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
                  <li><a href="#">Register</a></li>
                  <li><a href="#">Login</a></li>
              </ul>
          </div>
        </div>
    </nav>
    <div class="wrapper">
      {% block body %}
      {% endblock %}
      <div class="push"></div>
    </div>
    <footer>
        <div class="container">
            <div class="row">
                <div class="col-lg-12">
                    <ul class="list-inline">
                        <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
                        <li class="footer-menu-divider">⋅</li>
                        <li><a href="#">Register</a></li>
                        <li class="footer-menu-divider">⋅</li>
                        <li><a href="#">Login</a></li>
                    </ul>
                    <p class="copyright text-muted small">Copyright © 2016. All Rights Reserved</p>
                </div>
            </div>
        </div>
    </footer>
</body>
</html>

请注意,我们#用于注册和登录链接。我们将在制定auth蓝图时对此进行更新

接下来,在home目录中创建一个app/templates目录。主页模板 ,index.html将进入其中:

<!-- app/templates/home/index.html -->

{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block body %}
<div class="intro-header">
    <div class="container">
        <div class="row">
            <div class="col-lg-12">
                <div class="intro-message">
                    <h1>Project Dream Team</h1>
                    <h3>The best company in the world!</h3>
                    <hr class="intro-divider">
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

static目录中,添加cssimg目录。将以下 CSS 文件 , 添加style.css到您的static/css目录中(请注意,您将需要一个背景图片 ,intro-bg.jpg以及您static/img目录中的网站图标):

/* app/static/css/style.css */

body, html {
    width: 100%;
    height: 100%;
}

body, h1, h2, h3 {
    font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 700;
}

a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a {
  color: #aec251;
}

a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover {
  color: #687430;
}

footer {
    padding: 50px 0;
    background-color: #f8f8f8;
}

p.copyright {
    margin: 15px 0 0;
}

.alert-info {
    width: 50%;
    margin: auto;
    color: #687430;
    background-color: #e6ecca;
    border-color: #aec251;
}

.btn-default {
    border-color: #aec251;
    color: #aec251;
}

.btn-default:hover {
    background-color: #aec251;
}

.center {
    margin: auto;
    width: 50%;
    padding: 10px;
}

.content-section {
    padding: 50px 0;
    border-top: 1px solid #e7e7e7;
}

.footer, .push {
  clear: both;
  height: 4em;
}

.intro-divider {
    width: 400px;
    border-top: 1px solid #f8f8f8;
    border-bottom: 1px solid rgba(0,0,0,0.2);
}

.intro-header {
    padding-top: 50px;
    padding-bottom: 50px;
    text-align: center;
    color: #f8f8f8;
    background: url(../img/intro-bg.jpg) no-repeat center center;
    background-size: cover;
    height: 100%;
}

.intro-message {
    position: relative;
    padding-top: 20%;
    padding-bottom: 20%;
}

.intro-message > h1 {
    margin: 0;
    text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
    font-size: 5em;
}

.intro-message > h3 {
    text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
}

.lead {
    font-size: 18px;
    font-weight: 400;
}

.topnav {
    font-size: 14px;
}

.wrapper {
  min-height: 100%;
  height: auto !important;
  height: 100%;
  margin: 0 auto -4em;
}

运行应用程序;您现在应该可以看到主页了。

授权蓝图

对于 auth蓝图,我们将首先创建注册和登录表单。我们将使用Flask-WTF,这将允许我们创建安全的表单(感谢 CSRF 保护和 reCAPTCHA 支持)。

pip install Flask-WTF

现在编写表单的代码:

# app/auth/forms.py

from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField, ValidationError
from wtforms.validators import DataRequired, Email, EqualTo

from ..models import Employee


class RegistrationForm(FlaskForm):
    """
    Form for users to create new account
    """
    email = StringField('Email', validators=[DataRequired(), Email()])
    username = StringField('Username', validators=[DataRequired()])
    first_name = StringField('First Name', validators=[DataRequired()])
    last_name = StringField('Last Name', validators=[DataRequired()])
    password = PasswordField('Password', validators=[
                                        DataRequired(),
                                        EqualTo('confirm_password')
                                        ])
    confirm_password = PasswordField('Confirm Password')
    submit = SubmitField('Register')

    def validate_email(self, field):
        if Employee.query.filter_by(email=field.data).first():
            raise ValidationError('Email is already in use.')

    def validate_username(self, field):
        if Employee.query.filter_by(username=field.data).first():
            raise ValidationError('Username is already in use.')


class LoginForm(FlaskForm):
    """
    Form for users to login
    """
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Login')

Flask-WTF 有许多验证器,使编写表单变得更加容易。模型中的所有字段都有DataRequired验证器,这意味着用户需要填写所有字段才能注册或登录。

对于注册表,我们要求用户填写他们的电子邮件地址、用户名、名字、姓氏和密码两次。我们使用Email验证器来确保使用有效的电子邮件格式(例如[email protected]。)我们使用EqualTo验证器来确认匹配中的passwordconfirm_password字段RegistrationForm我们还创建了方法 (validate_emailvalidate_username) 以确保之前没有使用过输入的电子邮件和用户名。

submit两种表单中字段都将表示为一个按钮,用户可以点击该按钮分别进行注册和登录。

有了表格,我们可以编写视图:

# app/auth/views.py

from flask import flash, redirect, render_template, url_for
from flask_login import login_required, login_user, logout_user

from . import auth
from forms import LoginForm, RegistrationForm
from .. import db
from ..models import Employee


@auth.route('/register', methods=['GET', 'POST'])
def register():
    """
    Handle requests to the /register route
    Add an employee to the database through the registration form
    """
    form = RegistrationForm()
    if form.validate_on_submit():
        employee = Employee(email=form.email.data,
                            username=form.username.data,
                            first_name=form.first_name.data,
                            last_name=form.last_name.data,
                            password=form.password.data)

        # add employee to the database
        db.session.add(employee)
        db.session.commit()
        flash('You have successfully registered! You may now login.')

        # redirect to the login page
        return redirect(url_for('auth.login'))

    # load registration template
    return render_template('auth/register.html', form=form, title='Register')


@auth.route('/login', methods=['GET', 'POST'])
def login():
    """
    Handle requests to the /login route
    Log an employee in through the login form
    """
    form = LoginForm()
    if form.validate_on_submit():

        # check whether employee exists in the database and whether
        # the password entered matches the password in the database
        employee = Employee.query.filter_by(email=form.email.data).first()
        if employee is not None and employee.verify_password(
                form.password.data):
            # log employee in
            login_user(employee)

            # redirect to the dashboard page after login
            return redirect(url_for('home.dashboard'))

        # when login details are incorrect
        else:
            flash('Invalid email or password.')

    # load login template
    return render_template('auth/login.html', form=form, title='Login')


@auth.route('/logout')
@login_required
def logout():
    """
    Handle requests to the /logout route
    Log an employee out through the logout link
    """
    logout_user()
    flash('You have successfully been logged out.')

    # redirect to the login page
    return redirect(url_for('auth.login'))

就像在home蓝图中一样,这里的每个视图都处理对指定 URL 的请求。register视图Employee使用注册表单数据创建模型类的实例以填充字段,然后将其添加到数据库中。这基本上注册了一个新员工。

login视图查询数据库以检查是否存在具有与登录表单数据中提供的电子邮件地址匹配的电子邮件地址的员工。然后使用该verify_password方法检查数据库中员工的密码是否与登录表单数据中提供的密码匹配。如果这两个都是真的,它会继续使用login_userFlask-Login 提供方法登录用户。

logout视图具有login_required装饰,这意味着用户必须访问它被记录。它调用logout_userFlask-Login 提供方法将用户注销。

注意flash方法的使用,它允许我们使用 Flask 的消息闪烁功能。这使我们能够向用户传达反馈,例如通知他们注册成功或登录失败。

最后,让我们处理模板。首先,我们将安装Flask-Bootstrap以便我们可以使用它的wtfutils库。wtf库将允许我们根据forms.py文件中的表单快速生成模板中的表单utils库将允许我们显示我们之前设置的 Flash 消息以向用户提供反馈。

pip install flask-bootstrap

我们需要编辑app/__init__.py文件以使用 Flask-Bootstrap:

# app/__init__.py

# after existing third-party imports
from flask_bootstrap import Bootstrap

# existing code remains


def create_app(config_name):
    # existing code remains

    Bootstrap(app)

    from app import models

    # blueprint registration remains here

    return app

我们对该app/__init__.py文件进行了大量编辑这是文件的最终版本以及它在这一点上的外观(请注意,我已按字母顺序重新排列了导入和变量):

# app/__init__.py

# third-party imports
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

# local imports
from config import app_config

db = SQLAlchemy()
login_manager = LoginManager()


def create_app(config_name):
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_object(app_config[config_name])
    app.config.from_pyfile('config.py')

    Bootstrap(app)
    db.init_app(app)
    login_manager.init_app(app)
    login_manager.login_message = "You must be logged in to access this page."
    login_manager.login_view = "auth.login"
    migrate = Migrate(app, db)

    from app import models

    from .admin import admin as admin_blueprint
    app.register_blueprint(admin_blueprint, url_prefix='/admin')

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    from .home import home as home_blueprint
    app.register_blueprint(home_blueprint)

    return app

我们需要两个auth蓝图模板register.htmllogin.html,我们将在auth目录中的一个目录中创建它们templates

<!-- app/templates/auth/register.html -->

{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block body %}
<div class="content-section">
  <div class="center">
    <h1>Register for an account</h1>
    <br/>
    {{ wtf.quick_form(form) }}
  </div>
</div>
{% endblock %}
<!-- app/templates/auth/login.html -->

{% import "bootstrap/utils.html" as utils %}
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
<div class="content-section">
  <br/>
  {{ utils.flashed_messages() }}
  <br/>
  <div class="center">
    <h1>Login to your account</h1>
    <br/>
    {{ wtf.quick_form(form) }}
  </div>
</div>
{% endblock %}

表单是从app/auth/views.py文件加载的,我们文件中指定了要为每个视图显示的模板文件。还记得基本模板中的注册和登录链接吗?现在让我们更新它们,以便我们可以从菜单访问页面:

<!-- app/templates/base.html -->

<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
    <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
    <li><a href="{{ url_for('auth.register') }}">Register</a></li>
    <li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>

<!-- Modify footer menu -->
<ul class="list-inline">
    <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
    <li class="footer-menu-divider">⋅</li>
    <li><a href="{{ url_for('auth.register') }}">Register</a></li>
    <li class="footer-menu-divider">⋅</li>
    <li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>

再次运行应用程序并单击注册和登录菜单链接。您应该会看到加载了相应表单的模板。

尝试填写注册表;您应该能够注册新员工。注册后,您应该被重定向到登录页面,在那里您将看到我们在app/auth/views.py文件中配置的 flash 消息,邀请您登录。

登录应该是成功的;但是Template Not Found登录后应该会出现错误,因为dashboard.html模板尚未创建。现在让我们这样做:

<!-- app/templates/home/dashboard.html -->

{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<div class="intro-header">
    <div class="container">
        <div class="row">
            <div class="col-lg-12">
                <div class="intro-message">
                    <h1>The Dashboard</h1>
                    <h3>We made it here!</h3>
                    <hr class="intro-divider">
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

刷新页面。您会注意到导航菜单仍然有注册和登录链接,即使我们已经登录。我们需要修改它以在用户已经通过身份验证时显示注销链接。我们还将Hi, username!在导航栏中包含一条消息:

<!-- app/templates/base.html -->

<!-- In the head tag, include link to Font Awesome CSS so we can use icons -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
    {% if current_user.is_authenticated %}
      <li><a href="{{ url_for('home.dashboard') }}">Dashboard</a></li>
      <li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
      <li><a><i class="fa fa-user"></i>  Hi, {{ current_user.username }}!</a></li>
    {% else %}
      <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
      <li><a href="{{ url_for('auth.register') }}">Register</a></li>
      <li><a href="{{ url_for('auth.login') }}">Login</a></li>
    {% endif %}
</ul>

<!-- Modify footer menu -->
<ul class="list-inline">
    <li><a href="{{ url_for('home.homepage') }}">Home</a></li>
    <li class="footer-menu-divider">⋅</li>
    {% if current_user.is_authenticated %}
      <li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
    {% else %}
      <li><a href="{{ url_for('auth.register') }}">Register</a></li>
      <li class="footer-menu-divider">⋅</li>
      <li><a href="{{ url_for('auth.login') }}">Login</a></li>
    {% endif %}
</ul>

Note how we use if-else statements in the templates. Also, take note of the current_user proxy provided by Flask-Login, which allows us to check whether the user is authenticated and to get the user’s username.

Logging out will take you back to the login page:

Attempting to access the dashboard page without logging in will redirect you to the login page and display the message we set in the app/__init__.py file:

Notice that the URL is configured such that once you log in, you will be redirected to the page you initially attempted to access, which in this case is the dashboard.

Conclusion

That’s it for Part One! We’ve covered quite a lot: setting up a MySQL database, creating models, migrating the database, and handling registration, login, and logout. Good job for making it this far!

Watch this space for Part Two, which will cover the CRUD functionality of the app, allowing admin users to add, list, edit, and delete departments and roles, as well as assign them to employees.

觉得文章有用?

点个广告表达一下你的爱意吧 !😁