使用Python和Flask构建CRUD Web应用-第一部分

In this three-part tutorial, we'll build a CRUD (Create, Read, Update, Delete) employee management web app using Flask, a microframework for Python. I've named the app Project Dream Team, and it will have the following features:

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

  1. Users will be able to register and login as employees

    用户将能够注册和登录为员工
  2. The administrator will be able to create, update, and delete departments and roles

    管理员将能够创建,更新和删除部门和角色
  3. The administrator will be able to assign employees to a department and assign them roles

    管理员将能够将员工分配到部门并为其分配角色
  4. The administrator will be able to view all employees and their details

    管理员将能够查看所有员工及其详细信息

Part One will cover:

第一部分将涵盖:

  1. Database setup

    数据库设置
  2. Models

    楷模
  3. Migration

    移民
  4. Homepage

    主页
  5. Authentication

    认证方式

Ready? Here we go!

准备? 开始了!

先决条件 ( Prerequisites )

This tutorial builds on my introductory tutorial, Getting Started With Flask, picking up where it left off. It assumes you have, to begin with, the following dependencies installed:

本教程以我的入门教程Flask入门为基础。 首先,假定您已安装以下依赖项:

  1. Python 2.7

    Python 2.7
  2. Flask

    烧瓶
  3. virtualenv (and, optionally, virtualenvwrapper)

    virtualenv (以及可选的virtualenvwrapper

You should have a virtual environment set up and activated. You should also have the following file and directory structure:

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

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

This project structure groups the similar components of the application together. The dream-team directory houses all the project files. The app directory is the application package, and houses different but interlinked modules of the application. All templates are stored in the templates directory, all models are in the models.py file, and all routes are in the views.py file. The run.py file is the application's entry point, the config.py file contains the application configurations, and the requirements.txt file contains the software dependencies for the application.

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

If you don't have these set up, please visit the introductory tutorial and catch up!

如果您没有这些设置,请访问入门教程并赶上来!

数据库设置 ( Database Setup )

Flask has support for several relational database management systems, including SQLite, MySQL, and PostgreSQL. For this tutorial, we will be using MySQL. It's popular and therefore has a lot of support, in addition to being scalable, secure, and rich in features.

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

We will install the following (remember to activate your virtual environment):

我们将安装以下内容(请记住要激活您的虚拟环境):

  1. Flask-SQLAlchemy: This will allow us to use SQLAlchemy, a useful tool for SQL use with Python. SQLAlchemy is an Object Relational Mapper (ORM), which means that it connects the objects of an application to tables in a relational database management system. These objects can be stored in the database and accessed without the need to write raw SQL. This is convenient because it simplifies queries that may have been complex if written in raw SQL. Additionally, it reduces the risk of SQL injection attacks since we are not dealing with the input of raw SQL.

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

  2. MySQL-Python: This is a Python interface to MySQL. It will help us connect the MySQL database to the app.

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

$ pipinstall flask-sqlalchemy mysql-python

We'll then create the MySQL database. Ensure you have MySQL installed and running, and then log in as the root user:

然后,我们将创建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)

We have now created a new user dt_admin with the password dt2016, created a new database dreamteam_db, and granted the new user all database privileges.

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

Next, let's edit the config.py. Remove any exisiting code and add the following:

接下来,让我们编辑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
}

It is good practice to specify configurations for different environments. In the file above, we have specifed configurations for development, which we will use while building the app and running it locally, as well as production, which we will use when the app is deployed.

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

Some useful configuration variables are:

一些有用的配置变量是:

  1. TESTING: setting this to True activates the testing mode of Flask extensions. This allows us to use testing properties that could for instance have an increased runtime cost, such as unittest helpers. It should be set to True in the configurations for testing. It defaults to False.

    TESTING :将其设置为True激活Flask扩展的测试模式。 这使我们能够使用测试属性,例如,可能会增加运行时成本,例如单元测试助手。 在测试配置中应将其设置为True 。 默认为False
  2. DEBUG: setting this to True activates the debug mode on the app. This allows us to use the Flask debugger in case of an unhandled exception, and also automatically reloads the application when it is updated. It should however always be set to False in production. It defaults to False.

    DEBUG :将其设置为True激活应用程序上的调试模式。 这使我们可以在未处理的异常的情况下使用Flask调试器,并在更新应用程序时自动重新加载它。 但是,在生产中应始终将其设置为False 。 默认为False
  3. SQLALCHEMY_ECHO: setting this to True helps us with debugging by allowing SQLAlchemy to log errors.

    SQLALCHEMY_ECHO :将此设置为True可以允许SQLAlchemy记录错误,从而帮助我们进行调试。

You can find more Flask configuration variables here and SQLAlchemy configuration variables here.

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

Next, create an instance directory in the dream-team directory, and then create a config.py file inside it. We will put configuration variables here that will not be pushed to version control due to their sensitive nature. In this case, we put the secret key as well as the database URI which contains the database user password.

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

# instance/config.py

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

Now, let's edit the app/__init__.py file. Remove any existing code and add the following:

现在,让我们编辑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

We've created a function, create_app that, given a configuration name, loads the correct configuration from the config.py file, as well as the configurations from the instance/config.py file. We have also created a db object which we will use to interact with the database.

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

Next, let's edit the run.py file:

接下来,让我们编辑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()

We create the app by running the create_app function and passing in the configuration name. We get this from the OS environment variable FLASK_CONFIG. Because we are in development, we should set the environment variable to development.

我们通过运行create_app函数并传入配置名称来创建应用。 我们是从OS环境变量FLASK_CONFIG 。 因为我们在开发中,所以应该将环境变量设置为development

Let's run the app to ensure everything is working as expected. First, delete the app/views.py file as well as the app/templates directory as we will not be needing them going forward. Next, add a temporary route to the app/__init__.py file as follows:

让我们运行该应用程序以确保一切正常。 首先,删除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

Make sure you set the FLASK_CONFIG and FLASK_APP environment variables before running the 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)

We can see the "Hello, World" string we set in the route. The app is working well so far.

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

楷模 ( Models )

Now to work on the models. Remember that a model is a representation of a database table in code. We'll need three models: Employee, Department, and Role.

现在开始处理模型。 请记住,模型是代码中数据库表的表示。 我们将需要三个模型: EmployeeDepartmentRole

But first, let's install Flask-Login, which will help us with user management and handle logging in, logging out, and user sessions. The Employee model will inherit from Flask-Login's UserMixin class which will make it easier for us to make use of its properties and methods.

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

$ pipinstall flask-login

To use Flask-Login, we need to create a LoginManager object and initialize it in the app/__init__.py file. First, remove the route we added earlier, and then add the following:

要使用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

In addition to initializing the LoginManager object, we've also added a login_view and login_message to it. This way, if a user tries to access a page that they are not authorized to, it will redirect to the specified view and display the specified message. We haven't created the auth.login view yet, but we will when we get to authentication.

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

Now add the following code to the app/models.py file:

现在,将以下代码添加到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)

In the Employee model, we make use of some of Werkzeug's handy security helper methods, generate_password_hash, which allows us to hash passwords, and check_password_hash, which allows us ensure the hashed password matches the password. To enhance security, we have a password method which ensures that the password can never be accessed; instead an error will be raised. We also have two foreign key fields, department_id and role_id, which refer to the ID's of the department and role assigned to the employee.

Employee模型中,我们使用了Werkzeug的一些方便的安全帮助器方法, generate_password_hash (允许我们对密码进行哈希处理)和check_password_hash (允许我们确保哈希化的密码与密码匹配)。 为了提高安全性,我们提供了一种password方法,该方法可确保永远无法访问该密码。 而是会引发错误。 我们还有两个外键字段department_idrole_id ,它们分别指部门的ID和分配给员工的角色。

Note that we have an is_admin field which is set to False by default. We will override this when creating the admin user. Just after the Employee model, we have a user_loader callback, which Flask-Login uses to reload the user object from the user ID stored in the session.

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

The Department and Role models are quite similar. Both have name and description fields. Additionally, both have a one-to-many relationship with the Employee model (one department or role can have many employees). We define this in both models using the employees field. backref allows us to create a new property on the Employee model such that we can use employee.department or employee.role to get the department or role assigned to that employee. lazy defines how the data will be loaded from the database; in this case it will be loaded dynamically, which is ideal for managing large collections.

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

移民 ( Migration )

Migrations allow us to manage changes we make to the models, and propagate these changes in the database. For example, if later on we make a change to a field in one of the models, all we will need to do is create and apply a migration, and the database will reflect the change.

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

We'll begin by installing Flask-Migrate, which will handle the database migrations using Alembic, a lightweight database migration tool. Alembic emits ALTER statements to a database thus implememting changes made to the models. It also auto-generates minimalistic migration scripts, which may be complex to write.

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

$ pipinstall flask-migrate

We'll need to edit the app/__init__.py file:

我们需要编辑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

We have created a migrate object which will allow us to run migrations using Flask-Migrate. We have also imported the models from the app package. Next, we'll run the following command to create a migration repository:

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

$ flask db init

This creates a migrations directory in the dream-team directory:

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

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

Next, we will create the first migration:

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

$ flask db migrate

Finally, we'll apply the migration:

最后,我们将应用迁移:

$ flask db upgrade

We've sucessfully created tables based on the models we wrote! Let's check the MySQL database to confirm this:

我们已经成功地根据编写的模型创建了表格! 让我们检查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)

蓝图 ( Blueprints )

Blueprints are great for organising a flask app into components, each with its own views and forms. I find that blueprints make for a cleaner and more organised project structure because each blueprint is a distinct component that addresses a specific functionality of the app. Each blueprint can even have its own cutsom URL prefix or subdomain. Blueprints are particularly convenient for large applications.

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

We're going to have three blueprints in this app:

我们将在此应用程序中创建三个蓝图:

  1. Home - this will have the homepage and dashboard views

    主页-这将具有主页和仪表板视图
  2. Admin - this will have all administrator (department and role) forms and views

    管理员-这将具有所有管理员(部门和角色)的表格和视图
  3. Auth - this will have all authentication (registration and login) forms and views

    身份验证-这将具有所有身份验证(注册和登录)表格和视图

Create the relevant files and directories so that your directory structure resembles this:

创建相关的文件和目录,以便您的目录结构类似于以下内容:

└── 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

I chose not to have static and templates directories for each blueprint, because all the application templates will inherit from the same base template and use the same CSS file. Instead, the templates directory will have sub-directories for each blueprint so that blueprint templates can be grouped together.

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

In each blueprint's __init__.py file, we need to create a Blueprint object and initialize it with a name. We also need to import the views.

在每个蓝图的__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

Then, we can register the blueprints on the app in the app/__init__.py file, like so:

然后,我们可以在app/__init__.py文件中在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

We have imported each blueprint object and registered it. For the admin blueprint, we have added a url prefix, /admin. This means that all the views for this blueprint will be accessed in the browser with the url prefix admin.

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

家庭蓝图 ( Home Blueprint )

Time to work on fleshing out the blueprints! We'll start with the home blueprint, which will have the homepage as well as the dashboard.

是时候充实设计图了! 我们将从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")

Each view function has a decorator, home.route, which has a URL route as a parameter (remember that home is the name of the blueprint as specified in the app/home/__init__.py file). Each view handles requests to the specified URL.

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

The homepage view renders the home template, while the dashboard view renders the dashboard template. Note that the dashboard view has a login_required decorator, meaning that users must be logged in to access it.

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

Now to work on the base template, which all other templates will inherit from. Create a base.html file in the app/templates directory and add the following code:

现在开始处理所有其他模板都将继承的基础模板。 在app/templates目录中创建一个base.html文件,并添加以下代码:

<!-- 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>

Note that we use # for the Register and Login links. We will update this when we are working on the auth blueprint.

请注意,我们在注册和登录链接中使用# 。 当我们处理auth蓝图时,我们将对其进行更新。

Next, create a home directory inside the app/templates directory. The homepage template, index.html, will go inside it:

接下来,在app/templates目录中创建一个home目录。 主页模板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 %}

Inside the static directory, add css and img directories. Add the following CSS file, style.css, to your static/css directory (note that you will need a background image, intro-bg.jpg, as well as a favicon in your static/img directory):

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

/* 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;
}

Run the app; you should be able to see the homepage now.

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

认证蓝图 ( Auth Blueprint )

For the auth blueprint, we'll begin by creating the registration and login forms. We'll use Flask-WTF, which will allow us to create forms that are secure (thanks to CSRF protection and reCAPTCHA support).

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

pipinstall Flask-WTF

Now to write the code for the forms:

现在为表单编写代码:

# 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 has a number of validators that make writing forms much easier. All the fields in the models have the DataRequired validator, which means that users will be required to fill all of them in order to register or login.

Flask-WTF有许多验证器,可简化编写表格的过程。 模型中的所有字段都具有DataRequired验证器,这意味着需要用户填写所有用户才能注册或登录。

For the registration form, we require users to fill in their email address, username, first name, last name, and their password twice. We use the Email validator to ensure valid email formats are used (e.g some-name@some-domain.com.) We use the EqualTo validator to confirm that the password and confirm_password fields in the RegistrationForm match. We also create methods (validate_email and validate_username) to ensure that the email and username entered have not been used before.

对于注册表格,我们要求用户两次填写其电子邮件地址,用户名,名字,姓氏和密码。 我们使用Email验证器来确保使用有效的电子邮件格式(例如some-name@some-domain.com 。我们使用EqualTo验证器来确认RegistrationForm中的passwordconfirm_password字段匹配。 我们还创建方法( validate_emailvalidate_username )以确保之前未使用输入的电子邮件和用户名。

The submit field in both forms will be represented as a button that users will be able to click to register and login respectively.

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

With the forms in place, we can write the views:

放置好表单后,我们可以编写视图:

# 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'))

Just like in the home blueprint, each view here handles requests to the specified URL. The register view creates an instance of the Employee model class using the registration form data to populate the fields, and then adds it to the database. This esentially registers a new employee.

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

The login view queries the database to check whether an employee exists with an email address that matches the email provided in the login form data. It then uses the verify_password method to check that the password in the database for the employee matches the password provided in the login form data. If both of these are true, it proceeds to log the user in using the login_user method provided by Flask-Login.

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

The logout view has the login_required decorator, which means that a user must be logged in to access it. It calles the logout_user method provided by Flask-Login to log the user out.

logout视图具有login_required装饰器,这意味着用户必须登录才能访问它。 它调用Flask-Login提供的logout_user方法以注销用户。

Note the use of flash method, which allows us to use Flask's message flashing feature. This allows us to communicate feedback to the user, such as informing them of successful registration or unsuccessful login.

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

Finally, let's work on the templates. First, we'll install Flask-Bootstrap so we can use its wtf and utils libraries. The wtf library will allow us to quickly generate forms in the templates based on the forms in the forms.py file. The utils library will allow us to display the flash messages we set earlier to give feedback to the user.

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

pipinstall flask-bootstrap

We need to edit the app/__init__.py file to use 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

We've made quite a number of edits to the app/__init__.py file. This is the final version of the file and how it should look at this point (note that I have re-arranged the imports and variables in alphabetical order):

我们已经对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

We need two templates for the auth blueprint: register.html and login.html, which we'll create in an auth directory inside the templates directory.

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

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

The forms are loaded from the app/auth/views.py file, where we specified which template files to display for each view. Remember the Register and Login links in the base template? Let's update them now so we can access the pages from the menus:

表单是从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>

Run the app again and click on the Register and Login menu links. You should see the templates loaded with the appropriate form.

再次运行该应用程序,然后单击“注册和登录”菜单链接。 您应该看到带有适当表格的模板。

Try to fill out the registration form; you should be able to register a new employee. After registration, you should be redirected to the login page, where you will see the flash message we configured in the app/auth/views.py file, inviting you to login.

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

Logging in should be successful; however you should get a Template Not Found error after logging in, because the dashboard.html template has not been created yet. Let's do that now:

登录应该成功; 但是,登录后应该会收到“ 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 %}

Refresh the page. You'll notice that the navigation menu still has the register and login links, even though we are already logged in. We'll need to modify it to show a logout link when a user is already authenticated. We will also include a Hi, username! message in the nav bar:

刷新页面。 您会注意到,即使我们已经登录,导航菜单仍然具有注册和登录链接。我们需要对其进行修改,以在用户通过身份验证后显示注销链接。 我们还将包括一个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.

注意我们如何在模板中使用if-else语句。 另外,请注意Flask-Login提供的current_user代理,该代理使我们可以检查用户是否已通过身份验证并获取用户的用户名。

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:

尝试不登录即访问仪表板页面会将您重定向到登录页面,并显示我们在app/__init__.py文件中设置的消息:

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.

请注意,URL已配置为登录后将被重定向到最初尝试访问的页面,在本例中为仪表板。

结论 ( 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!

就是第一部分了! 我们已经介绍了很多内容:设置MySQL数据库,创建模型,迁移数据库以及处理注册,登录和注销。 到现在为止做得很好!

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.

在此空间中观看第二部分,该部分将涵盖应用程序的CRUD功能,允许管理员用户添加,列出,编辑和删除部门和角色,以及将其分配给员工。

翻译自: https://scotch.io/tutorials/build-a-crud-web-app-with-python-and-flask-part-one

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值