[Flask] 02 - Build a New Project.

一、路由配置

Ref: https://read.helloflask.com/c1-ready

Ref: https://read.helloflask.com/c2-hello

路由页面: 

# -*- coding: utf-8 -*-
from flask import Flask
app = Flask(__name__)


@app.route('/')
def hello():
    return 'Welcome to My Watchlist!

运行时,记得指定入口:

$ export FLASK_APP=hello.py

$ flask run
Running on http://127.0.0.1:5000/

管理环境变量,goto : https://read.helloflask.com/c2-hello

 

使用模板写HTML页面内容。 

在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的模板:
Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些常见的用法。在模板里,你需要添加特定的定界符将 Jinja2 语句和变量标记出来,下面是三种常用的定界符:
1. {{ ... }} 用来标记变量。
2. {% ... %} 用来标记语句,比如 if 语句,for 语句等。
3. {# ... #} 用来写注释。
模板中使用的变量需要在渲染的时候传递进去。

 

 

二、前后端分离

原生地调用前端的方法。但未来还是推荐vue.js的策略。

# -*- coding: utf-8 -*-
from flask import Flask, render_template
app = Flask(__name__)

name = 'Grey Li'
movies = [
    {'title': 'My Neighbor Totoro', 'year': '1988'},
    {'title': 'Dead Poets Society', 'year': '1989'},
    {'title': 'A Perfect World', 'year': '1993'},
    {'title': 'Leon', 'year': '1994'},
    {'title': 'Mahjong', 'year': '1996'},
    {'title': 'Swallowtail Butterfly', 'year': '1996'},
    {'title': 'King of Comedy', 'year': '1999'},
    {'title': 'Devils on the Doorstep', 'year': '1999'},
    {'title': 'WALL-E', 'year': '2008'},
    {'title': 'The Pork of Music', 'year': '2012'},
]


@app.route('/')
def index():
    return render_template('index.html', name=name, movies=movies)

 

 

三、连接数据库

选择了属于关系型数据库管理系统(RDBMS)的 SQLite,它基于文件,不需要单独启动数据库服务器,适合在开发时使用,或是在数据库操作简单、访问量低的程序中使用。

使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为模型类,类中的属性我们将称之为字段

 

数据表的创建,可以通过:

(1) flask shell

(env) $ flask shell
>>> from app import db
>>> db.create_all() 

(2) 也可写在代码内,然后通过@click.option赋予flask shell直接运行函数的能力,如下所示。

执行 flask initdb 命令就可以创建数据库表

(env) $ flask initdb

(env) $ flask initdb --drop

 

  • 代码:app.py

如果使用Click的command()装饰器添加命令,执行时不会自动推入应用上下文。

# -*- coding: utf-8 -*-
import os
import sys

import click
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

# SQLite URI compatible WIN = sys.platform.startswith('win') if WIN: prefix = 'sqlite:///' else: prefix = 'sqlite:////' app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db')  # 告诉 SQLAlchemy 数据库连接地址 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False   # 关闭对模型修改的监控 db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app
#################################################
# 编写一个自定义命令来自动执行创建数据库表操作
#################################################
@app.cli.command() @click.option(
'--drop', is_flag=True, help='Create after drop.') def initdb(drop): """Initialize the database.""" if drop: db.drop_all() db.create_all() click.echo('Initialized database.')
# 因为有了数据库,我们可以编写一个命令函数把虚拟数据添加到数据库里。 @app.cli.command()
def forge(): """Generate fake data.""" db.create_all() name = 'Grey Li' movies = [ {'title': 'My Neighbor Totoro', 'year': '1988'}, {'title': 'Dead Poets Society', 'year': '1989'}, {'title': 'A Perfect World', 'year': '1993'}, {'title': 'Leon', 'year': '1994'}, {'title': 'Mahjong', 'year': '1996'}, {'title': 'Swallowtail Butterfly', 'year': '1996'}, {'title': 'King of Comedy', 'year': '1999'}, {'title': 'Devils on the Doorstep', 'year': '1999'}, {'title': 'WALL-E', 'year': '2008'}, {'title': 'The Pork of Music', 'year': '2012'}, ] user = User(name=name) db.session.add(user) for m in movies: movie = Movie(title=m['title'], year=m['year']) db.session.add(movie) db.session.commit() click.echo('Done.')
####################################
# 有两类数据要保存:用户信息、电影条目信息 ####################################

class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20)) class Movie(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(60)) year = db.Column(db.String(4))
@app.route(
'/') def index(): user = User.query.first() movies = Movie.query.all() return render_template('index.html', user=user, movies=movies)

 

  • 添加记录

>>> from app import User, Movie  # 导入模型类
>>> user = User(name='Grey Li') # 创建一个 User 记录 >>> m1 = Movie(title='Leon', year='1994') # 创建一个 Movie 记录 >>> m2 = Movie(title='Mahjong', year='1996') # 再创建一个 Movie 记录
>>> db.session.add(user) # 把新创建的记录添加到数据库会话 >>> db.session.add(m1) >>> db.session.add(m2)
>>> db.session.commit() # 提交数据库会话,只需要在最后调用一次即可

 

  • 读取记录

<模型类>.query.<过滤方法(可选)>.<查询方法>
>>> from app import Movie  # 导入模型类
>>> movie = Movie.query.first() # 获取 Movie 模型的第一个记录(返回模型类实例) >>> movie.title # 对返回的模型类实例调用属性即可获取记录的各字段数据 'Leon' >>> movie.year '1994' >>> Movie.query.all() # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表 [<Movie 1>, <Movie 2>] >>> Movie.query.count() # 获取 Movie 模型所有记录的数量 2 >>> Movie.query.get(1) # 获取主键值为 1 的记录 <Movie 1> >>> Movie.query.filter_by(title='Mahjong').first() # 获取 title 字段值为 Mahjong 的记录 <Movie 2> >>> Movie.query.filter(Movie.title=='Mahjong').first() # 等同于上面的查询,但使用不同的过滤方法 <Movie 2>

 

  • 更新记录

>>> movie = Movie.query.get(2)
>>> movie.title = 'WALL-E' # 直接对实例属性赋予新的值即可 >>> movie.year = '2008'
>>> db.session.commit() # 注意仍然需要调用这一行来提交改动

 

  • 删除记录

>>> movie = Movie.query.get(1)
>>> db.session.delete(movie) # 使用 db.session.delete() 方法删除记录,传入模型实例
>>> db.session.commit() # 提交改动

 

 

四、提交表单

<form method="post">                                             <!-- 指定提交方法为 POST -->
    <label for="name">名字</label>
    <input type="text" name="name" id="name"><br>                <!-- 文本输入框 -->
    <label for="occupation">职业</label>
    <input type="text" name="occupation" id="occupation"><br>    <!-- 文本输入框 -->
    <input type="submit" name="submit" value="登录">              <!-- 提交按钮 -->
</form>

 

  • POST与GET 

<form> 标签里使用 method 属性将提交表单数据的 HTTP 请求方法指定为 POST。如果不指定,则会默认使用 GET 方法,这会将表单数据通过 URL 提交,容易导致数据泄露,而且不适用于包含大量数据的情况。

Goto: 都 2019 年了,还问 GET 和 POST 的区别

 

(a) 如何获取提交的表单数据?

为了能够处理 POST 请求,我们需要修改一下视图函数,确保服务端能接收POST请求。

@app.route('/', methods=['GET', 'POST'])

 

(b) 两种方法的请求有不同的处理逻辑:

  1) 对于 GET 请求,返回渲染后的页面;

  2) 对于 POST 请求,则获取提交的表单数据并保存。

 

(c) 代码实践:为了在函数内加以区分,我们添加一个 if 判断。 

from flask import request, url_for, redirect, flash

# ...

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':               # 判断是否是 POST 请求
        # 获取表单数据
        title = request.form.get('title')      # 传入表单对应输入字段的 name 值
        year  = request.form.get('year')
        # 验证数据
        if not title or not year or len(year) > 4 or len(title) > 60:
            flash('Invalid input.')            # 显示错误提示
            return redirect(url_for('index'))  # 重定向回主页
# 保存表单数据到数据库 movie = Movie(title=title, year=year) # 创建记录 db.session.add(movie) # 添加到数据库会话 db.session.commit() # 提交数据库会话
flash('Item created.') # 显示成功创建的提示 return redirect(url_for('index')) # 重定向回主页 movies = Movie.query.all() return render_template('index.html', movies=movies)

编辑:

@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST'])
def edit(movie_id):
    movie = Movie.query.get_or_404(movie_id)

    if request.method == 'POST':                      # 处理编辑表单的提交请求
        title = request.form['title']
        year  = request.form['year']

        if not title or not year or len(year) > 4 or len(title) > 60:
            flash('Invalid input.')
            return redirect(url_for('edit', movie_id=movie_id))  # 重定向回对应的编辑页面

        movie.title = title                           # 更新标题
        movie.year  = year                            # 更新年份
        db.session.commit()                           # 提交数据库会话
flash('Item updated.') return redirect(url_for('index')) # 重定向回主页 return render_template('edit.html', movie=movie) # 传入被编辑的电影记录

删除:

@app.route('/movie/delete/<int:movie_id>', methods=['POST'])  # 限定只接受 POST 请求
def delete(movie_id):
    movie = Movie.query.get_or_404(movie_id)  # 获取电影记录
    db.session.delete(movie)                  # 删除对应的记录
    db.session.commit()                       # 提交数据库会话
    flash('Item deleted.')
    return redirect(url_for('index'))         # 重定向回主页

 

 

五、用户认证

  • 安全存储密码

继承这个类会让 User 类拥有几个用于 判断认证状态的属性和方法

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    id            = db.Column(db.Integer, primary_key=True)
    name          = db.Column(db.String(20))
    username      = db.Column(db.String(20))   # 用户名
    password_hash = db.Column(db.String(128))  # 密码散列值

    def set_password(self, password):                             # 用来设置密码的方法,接受密码作为参数
        self.password_hash = generate_password_hash(password)     # 将生成的密码保持到对应字段

    def validate_password(self, password):                        # 用于验证密码的方法,接受密码作为参数
        return check_password_hash(self.password_hash, password)  # 返回布尔值

 

  • 生成管理员账户

这里,直接编写一个命令来创建管理员账户。

import click

@app.cli.command()
@click.option('--username', prompt=True, help='The username used to login.')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.')
def admin(username, password):
    """Create user."""
    db.create_all()

    user = User.query.first()
    if user is not None:
        click.echo('Updating user...')
        user.username = username
        user.set_password(password)  # 设置密码
    else:
        click.echo('Creating user...')
        user = User(username=username, name='Admin')
        user.set_password(password)  # 设置密码
        db.session.add(user)

    db.session.commit()  # 提交数据库会话
    click.echo('Done.')

 

  • 实现用户认证

扩展 Flask-Login 提供了实现用户认证需要的各类功能函数,需要独立安装: 

(env) $ pip install flask-login

 

实现一个“用户加载回调函数”,初始化 Flask-Login。

from flask_login import LoginManager

login_manager = LoginManager(app)        # 实例化扩展类

@login_manager.user_loader
def load_user(user_id):                  # 创建用户加载回调函数,接受用户 ID 作为参数
    user = User.query.get(int(user_id))  # 用 ID 作为 User 模型的主键查询对应的用户
    return user                          # 返回用户对象

其中最常用的是 is_authenticated 属性:如果当前用户已经登录,那么 current_user.is_authenticated 会返回 True, 否则返回 False。有了 current_user 变量和这几个验证方法和属性,我们可以很轻松的判断当前用户的认证状态。

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        if not current_user.is_authenticated:  # <---- 判断 “当前用户已经登录?”
            return redirect(url_for('index'))

        title = request.form['title']
        year = request.form['year']

        if not title or not year or len(year) > 4 or len(title) > 60:
            flash('Invalid input.')
            return redirect(url_for('index'))

        movie = Movie(title=title, year=year)
        db.session.add(movie)
        db.session.commit()
        flash('Item created.')
        return redirect(url_for('index'))

    movies = Movie.query.all()
    return render_template('index.html', movies=movies)

 

  • 用户登录、登出

from flask_login import login_user

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if not username or not password:
            flash('Invalid input.')
            return redirect(url_for('login'))

        user = User.query.first()
        # 验证用户名和密码是否一致
        if username == user.username and user.validate_password(password):
            login_user(user)                    # 登入用户
            flash('Login success.')
            return redirect(url_for('index'))   # 登陆成功后,则重定向到主页

        flash('Invalid username or password.')  # 如果验证失败,显示错误消息
        return redirect(url_for('login'))       # 重定向回登录页面

    return render_template('login.html')
from flask_login import login_required, logout_user

# ...

@app.route('/logout')
@login_required                         # 用于视图保护,后面会详细介绍
def logout():
    logout_user()                       # 登出用户
    flash('Goodbye.')
    return redirect(url_for('index'))   # 重定向回首页

 

 

六、认证保护

  • 视图保护

未登录用户不能执行下面的操作:访问编辑页面、访问设置页面、执行注销操作、执行删除操作、执行添加新条目操作。

对于不允许未登录用户访问的视图,只需要为视图函数 附加一个 login_required 装饰器 就可以将未登录用户拒之门外。

添加了这个装饰器后,如果未登录的用户访问对应的 URL,Flask-Login 会把用户重定向到登录页面,并显示一个错误提示。

@app.route('/movie/delete/<int:movie_id>', methods=['POST'])
@login_required  # 登录保护
def delete(movie_id):
    movie = Movie.query.get_or_404(movie_id)
    db.session.delete(movie)
    db.session.commit()
    flash('Item deleted.')
return redirect(url_for('index'))

 

本篇章的大概内容,局限于了解。更多内容还需要系统学习。

剩余内容:

 

End.

posted @ 2020-08-18 19:25  郝壹贰叁  阅读(158)  评论(0编辑  收藏  举报