下面的内容属于:https://dormousehole.readthedocs.io/en/latest/tutorial/index.html的内容,这里做个笔记
# flask/flaskr/__init__.py import os from flask import Flask # 创建app工厂函数,并设置默认配置 def create_app(test_config=None): # instance_relative_config:Flask中的选项,用来判断是否关联自定义文件 app = Flask(__name__, instance_relative_config=True) # 配置字典 app.config.from_mapping( SECRET_KET = 'dev', # 随机字符串 DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite') # app.instance_path:获取当前文件夹的路径,构造"当前路径/instace"文件路径。这里的目的是创建数据库文件地址 ) if test_config is None: # 判断是否配置了默认配置,如果没有,就执行"config.py"默认配置文件 # from_pyfile:重载配置文件。silent参数默认为false表示如果不存在就报错 app.config.from_pyfile('config.py', silent=True) else: # 执行默认配置 app.config.from_mapping(test_config) try: # 尝试创建路径 os.makedirs(app.instance_path) except OSError: pass # 显示hello @app.route("/") def hello(): return "hello" # 调用数据库初始化 from . import db db.init_app(app) # 蓝图和视图 # 蓝图的作用:一个是用于权限认证,二适用于博客的帖子管理 from . import auth app.register_blueprint(auth.bp) # 注册蓝图 from . import blog app.register_blueprint(blog.bp) app.add_url_rule('/', endpoint='index') # 导航栏访问:http://127.0.0.1:5000/,所以必须注释上面的hello输出 return app
# flask/flaskr/db.py # 对数据库的操作 import sqlite3 import click # g:专门用来存储用户信息的对象,功能和session相似,但又不相同 # current_app:表示当前运行程序(也就是调用的app)的实例对象。 from flask import current_app, g from flask.cli import with_appcontext # 创建一个sqlite3链接,并保存到g.db中 def get_db(): if 'db' not in g: g.db = sqlite3.connect( current_app.config['DATABASE'], detect_types = sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db # 关闭g中创建的链接 def close_db(e=None): db = g.pop('db', None) if db is not None: db.close() # 如果数据库没有初始化,那么执行初始化 def init_db(): db = get_db() # open_resource:打开一个文件,该文件名是相对于 flaskr 包的 with current_app.open_resource('schema.sql') as f: # 执行sql语句创建内容 db.executescript(f.read().decode('utf8')) # click.command()定义一个名为init-db命令行 @click.command('init-db') @with_appcontext def init_db_command(): init_db() # 调用init_db click.echo('Initialized the database.') # 显示初始化信息 # 注:close_db 和 init_db_command 函数需要在应用实例中注册,否则无法使用 def init_app(app): app.teardown_appcontext(close_db) # app.teardown_appcontext()告诉Flask在返回响应后进行清理的时候调用此函数 app.cli.add_command(init_db_command) # app.cli.add_command()添加一个新的可以与flask一起工作的命令。
# flask/flaskr/auth.py import functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) from werkzeug.security import check_password_hash, generate_password_hash from flaskr.db import get_db bp = Blueprint('auth', __name__, url_prefix='/auth') # 创建一个auth的蓝图,当导航栏使用".../auth/"访问时,都会跳到该视图下面 # 蓝图内的路由 @bp.route('/register/', methods=('GET', 'POST')) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() # 调用数据库的链接 error = None # 用户和密码不能为空 if not username: error = 'Username is required.' elif not password: error = 'Password is required.' elif db.execute('SELECT id FROM user WHERE username = ?', (username,)).fetchone() is not None: # db.execute('SELECT id FROM user WHERE username = ?', (username,)).fetchone():表示查找一条username的数据 error = 'User {} is already registered.'.format(username) if error is None: # 如果没有错误执行插入操作 # generate_password_hash()生成安全的哈希值并储存到数据库 db.execute('INSERT INTO user (username, password) VALUES (?, ?)', (username, generate_password_hash(password))) db.commit() # sqlites必须进行提交 return redirect(url_for('auth.login')) # 跳转登录 # 如果有错误,错误信息闪现 flash(error) return render_template('auth/register.html') # 回转注册界面 @bp.route('/login/', methods=('GET', 'POST')) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None user = db.execute('SELECT * FROM user WHERE username = ?', (username,)).fetchone() if user is None: error = 'Incorrect username.' elif not check_password_hash(user['password'], password): # check_password_hash()将user['password']密码转换为hash值,并与password比较 error = 'Incorrect password.' if error is None: session.clear() # 清空session session['user_id'] = user['id'] # 设置session return redirect(url_for('index')) # 跳转首页 flash(error) return render_template('auth/login.html') # before_app_request:在每次请求之前执行该函数,同样的还有before_request @bp.before_app_request def load_logged_in_user(): user_id = session.get('user_id', None) # 获取session if user_id is None: g.user = None else: g.user = get_db().execute('SELECT * FROM user WHERE id = ?', (user_id,)).fetchone() # 这个是注销函数 @bp.route('/logout') def logout(): session.clear() # 清空session return redirect(url_for('index')) # 跳转到首页 # 做装饰器,用来检验用户是否登录 def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for('auth.login')) return view(**kwargs) return wrapped_view
from flask import (Blueprint, flash, g, redirect, render_template, request, url_for) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp = Blueprint('blog', __name__) @bp.route('/') def index(): db = get_db() posts = db.execute('SELECT p.id, title, body, created, author_id, username FROM post p JOIN user u ON p.author_id = u.id ORDER BY created DESC').fetchall() return render_template('blog/index.html', posts=posts) @bp.route('/create', methods=('GET', 'POST')) @login_required def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute('INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)', (title, body, g.user['id'])) db.commit() return redirect(url_for('blog.index')) return render_template('blog/create.html') def get_post(id, check_author=True): post = get_db().execute('SELECT p.id, title, body, created, author_id, username FROM post p JOIN user u ON p.author_id = u.id WHERE p.id = ?', (id,)).fetchone() if post is None: abort(404, "Post id {0} doesn't exist.".format(id)) if check_author and post['author_id'] != g.user['id']: abort(403) return post @bp.route('/<int:id>/update', methods=('GET', 'POST')) @login_required def update(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute('UPDATE post SET title = ?, body = ? WHERE id = ?', (title, body, id)) db.commit() return redirect(url_for('blog.index')) return render_template('blog/update.html', post=post) @bp.route('/<int:id>/delete', methods=('POST',)) @login_required def delete(id): get_post(id) db = get_db() db.execute('DELETE FROM post WHERE id = ?', (id,)) db.commit() return redirect(url_for('blog.index'))
flaskr/schema.sql
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;
CREATE TABLE user(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL);
CREATE TABLE post(id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, body TEXT NOT NULL, FOREIGN KEY(author_id) REFERENCES user(id));
flaskr/templates/base.html
<!doctype html> <!-- html5代码,block表示可代替的标题 --> <title>{% block title %}{% endblock %} - Flaskr</title> <!-- 调用静态文件 --> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <!-- 在html5中nav表示头部 --> <nav> <h1>Flaskr</h1> <ul> <!-- 判断用户是否存在 --> {% if g.user %} <!-- 显示用户名 --> <li><span>{{ g.user['username'] }}</span> <li><a href="{{ url_for('auth.logout') }}">Log Out</a> {% else %} <li><a href="{{ url_for('auth.register') }}">Register</a> <li><a href="{{ url_for('auth.login') }}">Log In</a> {% endif %} </ul> </nav> <!-- session表示主体 --> <section class="content"> <header> {% block header %}{% endblock %} </header> <!-- 获取消息闪现信息 --> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% block content %}{% endblock %} </section>
测试:
# tests/confest.py import os import tempfile import pytest from flaskr import create_app from flaskr.db import get_db, init_db with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: _data_sql = f.read().decode('utf8') @pytest.fixture def app(): # tempfile.mkstemp():创建并打开一个临时文件,返回该文件对象和路径。测试结束后,临时文件会被关闭并 删除。 db_fd, db_path = tempfile.mkstemp() app = create_app({ 'TESTING': True, # 表明在测试模式下 'DATABASE': db_path, }) # 创建应用情景并测试 with app.app_context(): init_db() get_db().executescript(_data_sql) yield app # 测试结束,关闭对象并删除临时文件 os.close(db_fd) os.unlink(db_path) # 客户端接口 @pytest.fixture def client(app): return app.test_client() # 测试由客户端向应用发送数据 @pytest.fixture def runner(app): return app.test_cli_runner() # 创建一个运行器,可以调用应用注册的命令。 class AuthActions(object): def __init__(self, client): self._client = client # 模拟登陆 def login(self, username='test', password='test'): return self._client.post('/auth/login', data={'username': username, 'password': password}) # 模拟推出 def logout(self): return self._client.get('/auth/logout') # 登陆/退出的接口,方便后面使用 @pytest.fixture def auth(client): return AuthActions(client)
test/data.sql
INSERT INTO user (username, password) VALUES('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); INSERT INTO post (title, body, author_id, created) VALUES('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');
# tests/auth.py import pytest from flask import g, session from flaskr.db import get_db def test_register(client, app): # 模拟用户注册 assert client.get('/auth/register').status_code == 200 # 访问 response = client.post('/auth/register', data={'username': 'a', 'password': 'a'}) # 注册 assert 'http://localhost/auth/login' == response.headers['Location'] # 响应 # 创建测试应用情景 with app.app_context(): assert get_db().execute("select * from user where username = 'a'",).fetchone() is not None # 多次测试 # pytest.mark.parametrize:表示已不同的参数运行同一个测试函数,第一个参数为测试的键,后面的参数为测试的多个值 @pytest.mark.parametrize(('username', 'password', 'message'), (('', '', b'Username is required.'), ('a', '', b'Password is required.'), ('test', 'test', b'already registered'),)) def test_register_validate_input(client, username, password, message): response = client.post('/auth/register', data={'username': username, 'password': password}) assert message in response.data # 判断响应信息是否正确 # 测试登录模块 def test_login(client, auth): assert client.get('/auth/login').status_code == 200 response = auth.login() assert response.headers['Location'] == 'http://localhost/' with client: client.get('/') assert session['user_id'] == 1 assert g.user['username'] == 'test' @pytest.mark.parametrize(('username', 'password', 'message'), ( ('a', 'test', b'Incorrect username.'), ('test', 'a', b'Incorrect password.'),)) def test_login_validate_input(auth, username, password, message): response = auth.login(username, password) assert message in response.data # 测试退出模块 def test_logout(client, auth): auth.login() with client: auth.logout() assert 'user_id' not in session
使用:
python setup.py bdist_wheel
可以打包产品