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,它将具有以下功能:
- Users will be able to register and login as employees 用户将能够注册和登录为员工
- The administrator will be able to create, update, and delete departments and roles 管理员将能够创建,更新和删除部门和角色
- The administrator will be able to assign employees to a department and assign them roles 管理员将能够将员工分配到部门并为其分配角色
- The administrator will be able to view all employees and their details 管理员将能够查看所有员工及其详细信息
Part One will cover:
第一部分将涵盖:
- Database setup 数据库设置
- Models 楷模
- Migration 移民
- Homepage 主页
- 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入门为基础。 首先,假定您已安装以下依赖项:
- Python 2.7 Python 2.7
- Flask 烧瓶
- 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支持多种关系数据库管理系统,包括SQLite , MySQL和PostgreSQL 。 对于本教程,我们将使用MySQL。 它很受欢迎,因此除了具有可伸缩性,安全性和丰富的功能外,还具有很多支持。
We will install the following (remember to activate your virtual environment):
我们将安装以下内容(请记住要激活您的虚拟环境):
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注入攻击的风险。
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:
一些有用的配置变量是:
TESTING
: setting this toTrue
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 toTrue
in the configurations for testing. It defaults toFalse
.TESTING
:将其设置为True
激活Flask扩展的测试模式。 这使我们能够使用测试属性,例如,可能会增加运行时成本,例如单元测试助手。 在测试配置中应将其设置为True
。 默认为False
。DEBUG
: setting this toTrue
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 toFalse
in production. It defaults toFalse
.DEBUG
:将其设置为True
激活应用程序上的调试模式。 这使我们可以在未处理的异常的情况下使用Flask调试器,并在更新应用程序时自动重新加载它。 但是,在生产中应始终将其设置为False
。 默认为False
。SQLALCHEMY_ECHO
: setting this toTrue
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_CONFIG
和FLASK_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
.
现在开始处理模型。 请记住,模型是代码中数据库表的表示。 我们将需要三个模型: Employee
, Department
和Role
。
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_view
和login_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_id
和role_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.
Department
和Role
模型非常相似。 两者都有name
和description
字段。 此外,两者都与Employee
模型具有一对多关系(一个部门或角色可以有很多雇员)。 我们使用employees
字段在两个模型中对此进行定义。 backref
允许我们在Employee
模型上创建一个新属性,以便我们可以使用employee.department
或employee.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:
我们将在此应用程序中创建三个蓝图:
- Home - this will have the homepage and dashboard views 主页-这将具有主页和仪表板视图
- Admin - this will have all administrator (department and role) forms and views 管理员-这将具有所有管理员(部门和角色)的表格和视图
- 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
目录内,添加css
和img
目录。 将以下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
中的password
和confirm_password
字段匹配。 我们还创建方法( validate_email
和validate_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,以便可以使用其wtf
和utils
库。 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.html
和login.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