3月11日
大型Flask项目目录结构
project/ # 项目根目录
app/ # 存放整个的应用程序
static/ # 静态资源
js/ # JS脚本
css/ # 层叠样式表
img/ # 图片资源
favicon.ico # 网站图标
templates/ # 模板文件
common/ # 通用模板文件
main/ # 主蓝本模板文件
errors/ # 错误模板文件
email/ # 邮件模板文件
user/ # 用户蓝本模板文件
posts/ # 博客篮板模板文件
forms/ # 存放所有表单
models/ # 存放所有模型
views/ # 存放视图函数
email.py # 邮件发送
extensions.py # 所有的扩展
config.py # 配置文件
migrations/ # 数据库迁移脚本目录
venv/ # 虚拟环境目录
tests/ # 测试单元
requirements.txt # 所有的依赖包
manage.py # 项目启动控制文件
如何快速复制一个虚拟环境:
1. 将当前环境依赖冷冻起来
pip freeze > requirements.txt
2. 创建虚拟环境
virtualenv venv
3. 安装冷冻的依赖包
pip install -r requirements.txt
代码书写步骤
1. 书写配置即使用配置文件
config.py、__ init __.py、manage.py
2. 配置相关扩展
extensions.py、__ init __.py
3. 配置相关蓝本
views目录、__ init __.py
4. 定制项目基础模板
common/base.html
5. 自定义错误页面
templates/errors/404.html、__ init __.py
6. 异步发送邮件
email.py、templates/email/
7. 用户注册登录相关知识点
密码要加密存储与校验:
from werkzeug.security importgenerate_password_hash,check_password_hash
@main.route('/jiami/')
def jiami():
return generate_password_hash('123456')
@main.route('/check/<password>')
def check(password):
# 密码校验函数:加密后的值 密码
# 正确:True,错误:False
if check_password_hash('pbkdf2:sha256:50000$8tHnM54f$c1518c6e491e0a7c5ebd90beb8b56c1d3b03cef66ad940c566578e6a5cfd62ea', password):
return '密码正确'
else:
return '密码错误'
```
用户账户激活(token)
```python
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
@main.route('/generate_token/')
def generate_token():
s = Serializer(current_app.config['SECRET_KEY'], expires_in=3600)
# 加密指定的数据,已字典的形式传入
return s.dumps({'id': 250})
@main.route('/activate/<token>')
def activate(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return 'token有误'
return str(data.get('id'))
3月12日
用户管理
# 用户管理
### 用户注册与激活
1. 在项目基础模板(common/base.html)中添加注册点击的链接
```html
<li><a href="{{url_for('user.register')}}">注册</a></li>
```
> 注意:由于所有的视图函数都是在蓝本中写的,因此在使用时要指定蓝本;否则可能会出问题,因为默认是当前蓝本的视图函数。
2. 在user蓝本中添加视图函数,如下:
```python
@user.route('/register/')
def register():
return render_template('user/register.html')
```
3. 书写模板文件(templates/user/register.html),如下:
```html
{% extends 'common/base.html' %}
{% block title %}欢迎注册{% endblock %}
{% block page_content %}
用户注册
{% endblock %}
```
4. 创建用户注册表单类
```python
# 导入表单基类
from flask_wtf import FlaskForm
# 导入相关字段
from wtforms import StringField, PasswordField, SubmitField
# 导入验证器类
from wtforms.validators import DataRequired, EqualTo, Email, Length
class RegisterForm(FlaskForm):
username = StringField('用户名',
validators=[DataRequired(),
Length(6, 18, message='用户名必须在6~18个字符之间')])
password = PasswordField('密码',
validators=[DataRequired(),
Length(6, 18, message='密码长度必须在6~18个字符之间')])
confirm = PasswordField('确认密码',
validators=[EqualTo('password',
message='两次密码不一致')])
email = StringField('邮箱',
validators=[Email(message='邮箱格式不正确')])
submit = SubmitField('立即注册')
# 自定义验证器,验证用户名
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('该用户名已存在,请选用其它用户名')
# 自定义验证器,验证邮箱
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('该邮箱已使用,请选用其它邮箱')
```
5. 创建表单对象并渲染,如下:
```python
@user.route('/register/')
def register():
form = RegisterForm()
return render_template('user/register.html', form=form)
```
模板中渲染表单
```html
{% extends 'common/base.html' %}
{% block title %}欢迎注册{% endblock %}
{% block page_content %}
{{ wtf.quick_form(form) }}
{% endblock %}
```
6. 用户的注册校验逻辑
7. 用户注册的邮件激活
```python
@user.route('/register/', methods=['GET', 'POST'])
def register():
form = RegisterForm()
if form.validate_on_submit():
# 创建对象,写入数据库
# 发送激活邮件
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=3600)
token = s.dumps({'id': 250})
send_mail(form.email.data, '账户激活',
'email/account_activate', token=token,
username=form.username.data)
flash('激活邮件已发送,请点击链接完成用户激活')
return redirect(url_for('main.index'))
return render_template('user/register.html', form=form)
@user.route('/activate/<token>')
def activate(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return 'token有误'
return '%d号账户已经激活' % data.get('id')
```
8. 用户模型的设计,如下:
```python
from flask import current_app
from app.extensions import db
# 生成token使用
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# 密码散列及校验
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
# 指定表名
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), unique=True)
password_hash = db.Column(db.String(128))
email = db.Column(db.String(64), unique=True)
confirmed = db.Column(db.Boolean, default=False)
# 保护字段
@property
def password(self):
raise AttributeError('密码是不可读属性')
# 设置密码,加密存储
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
# 密码校验
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
# 生成用户激活的token
def generate_activate_token(self, expires_in=3600):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expires_in)
return s.dumps({'id': self.id})
# 激活账户时的token校验,校验时还不知道用户信息,需要静态方法
@staticmethod
def check_activate_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
user = User.query.get(data.get('id'))
if user is None:
# 不存在此用户
return False
if not user.confirmed:
# 账户没有激活时才激活
user.confirmed = True
db.session.add(user)
return True
```
用户登录认证与退出
说明:用户登录认证及退出的逻辑可以自己实现,但是比较繁琐,推荐使用flask-login
安装:`pip install flask-login`
使用:
```python
from flask_login import LoginManager
login_manager = LoginManager()
def config_extensions(app):
...
login_manager.init_app(app)
# 会话保护级别:
# None不使用
# 'basic'基本级别,默认级别
# 'strong'用户信息更改立即退出
login_manager.session_protection = 'strong'
# 设置登录页面端点,当用户访问需要登录才能访问的页面,
# 此时还没有登录,会自动跳转到此处
login_manager.login_view = 'user.login'
# 设置提示信息,默认是英文提示信息
login_manager.login_message = '需要登录才可访问'
# 在用户的Model中添加一个回调函数
@login_manager.user_loader
def loader_user(user_id):
return User.query.get(int(user_id))
```
1. 基础模板中添加点击的链接,如下
```html
<li><a href="{{url_for('user.login')}}">登录</a></li>
```
2. 在user蓝本中添加视图函数,使用flask-login认证,如下
```python
@user.route('/login/', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
u = User.query.filter_by(username=form.username.data).first()
if u is None:
flash('无效的用户名')
elif u.verify_password(form.password.data):
# 验证通过,用户登录,顺便可以完成'记住我'的功能
login_user(u, remember=form.remember_me.data)
# 如果有下一跳转地址就跳转到指定地址,没有跳转到首页
return redirect(request.args.get('next') or
url_for('main.index'))
else:
flash('无效的密码')
return render_template('user/login.html', form=form)
```
3. 添加一个login.html的模板文件,如下:
```
{% extends 'common/base.html' %}
{% block title %}用户登录{% endblock %}
{% block page_content %}
登录页面展示
{% endblock %}
```
4. 使用flask-login退出登录
```python
@user.route('/logout/')
def logout():
logout_user()
flash('您已退出登录')
return redirect(url_for('main.index'))
```
总结:扩展库提供了很多实用的功能
login_user:可以完成用户的登录,顺便还可以完成'记住我'的功能
logout_user:退出登录
current_user:在任意的地方都可使用,表示当前登录的用户,未登录是一个匿名用户
is_authenticated:是否已登录
is_anonymous:是否是匿名用户
login_required:保护指定的路由,针对需要登录才可访问的路由
3月13日
用户管理(修改头像)
1. 配置
```python
# 最大上传文件大小
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
# 上传文件存储位置
UPLOADED_PHOTOS_DEST = os.path.join(base_dir, 'static/upload')
```
2. 添加flask-uploads扩展
```python
# 导入类库及函数
from flask_uploads import UploadSet, IMAGES
from flask_uploads import configure_uploads, patch_request_class
# 创建对象
photos = UploadSet('photos', IMAGES)
# 初始化
configure_uploads(app, photos)
patch_request_class(app, size=None)
```
3. 修改头像中使用上传文件
1. 在基础模板中添加点击链接
```html
<li><a href="{{url_for('user.change_icon')}}">修改头像</a></li>
```
2. 添加视图函数
```python
@user.route('/change_icon/')
@login_required
def change_icon():
form = IconForm()
return render_template('user/change_icon.html', form=form)
```
3. 创建模板文件
```html
{% extends 'common/base.html' %}
{% block title %}修改头像{% endblock %}
{% block page_content %}
{{ wtf.quick_form(form) }}
{% endblock %}
```
4. 设计上传文件的表单
```python
# 导入上传文件的字段及验证器
from flask_wtf.file import FileField, FileRequired, FileAllowed
from app.extensions import photos
# 修改头像表单
class IconForm(FlaskForm):
icon = FileField('头像', validators=[FileRequired('请选择上传文件'), FileAllowed(photos, '只能上传图片')])
submit = SubmitField('上传')
```
5. 修改user数据模型
```python
class User(UserMixin, db.Model):
...
# 添加头像字段
icon = db.Column(db.String(64), default='default.jpg')
```
> 记得迁移数据库,顺便将默认值也修改了
6. 修改模型后在信息展示和上传头像中测试
7. 完整的上传头像并生成缩略图
```python
import os
from PIL import Image
@user.route('/change_icon/', methods=['GET', 'POST'])
@login_required
def change_icon():
form = IconForm()
if form.validate_on_submit():
# 生成随机的文件名
suffix = os.path.splitext(form.icon.data.filename)[1]
name = rand_str() + suffix
# 保存上传头像
photos.save(form.icon.data, name=name)
# 生成缩略图
pathname = os.path.join(os.path.join(current_app.config['UPLOADED_PHOTOS_DEST'], name))
img = Image.open(pathname)
img.thumbnail((64, 64))
img.save(pathname)
# 删除原有头像
if current_user.icon != 'default.jpg':
# 第一次更换头像不删除,除此之外原来的头像都要删除
os.remove(os.path.join(current_app.config['UPLOADED_PHOTOS_DEST'], current_user.icon))
# 更新新的头像名至数据库
current_user.icon = name
db.session.add(current_user)
flash('头像已更换')
return render_template('user/change_icon.html', form=form)
# 生成随机的字符串
def rand_str(length=32):
import random
base_str = 'abcdefghijklmnopqrstuvwxyz1234567890'
return ''.join(random.choice(base_str) for i in range(length))
```
博客管理
1. 设计博客的模型
```python
from app.extensions import db
from datetime import datetime
class Posts(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
rid = db.Column(db.Integer, index=True, default=0)
content = db.Column(db.Text)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
# 指定外键(表名.字段)
uid = db.Column(db.Integer, db.ForeignKey('users.id'))
```
为了关联查询,需要修改User模型,如下:
```python
class User(UserMixin, db.Model):
...
# 添加关联模型,相当于在关联的模型中动态的添加了一个字段
# 参数说明:
# 第一个参数:唯一一个必须的参数,关联的模型类名
# backref:反向引用的字段名
# lazy:指定加载关联数据的方式,dynamic:不加载记录,但是提供关联查询
posts = db.relationship('Posts', backref='user', lazy='dynamic')
```
> 数据模型添加或修改后,即可进行数据库迁移操作
2. 准备发表博客的表单
```python
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
class PostsForm(FlaskForm):
# 如果想要设置字段的其它属性,可以通过render_kw完成
content = TextAreaField('', render_kw={'placeholder': '这一刻的想法...'}, validators=[DataRequired(), Length(1, 128, message='说话要注意影响,不多不少最好')])
submit = SubmitField('发表')
```
3. 添加视图函数
```python
@main.route('/')
def index():
form = PostsForm()
return render_template('main/index.html', form=form)
```
4. 渲染表单
```html
{% extends 'common/base.html' %}
{% block title %}首页{% endblock %}
{% block page_content %}
{{ wtf.quick_form(form) }}
{% endblock %}
```
5. 发表博客
```python
@main.route('/', methods=['GET', 'POST'])
def index():
form = PostsForm()
if form.validate_on_submit():
# 判断是否登录
if current_user.is_authenticated:
u = current_user._get_current_object()
# 根据表单提交的数据常见对象
p = Posts(content=form.content.data, user=u)
# 然后写入数据库
db.session.add(p)
return redirect(url_for('main.index'))
else:
flash('登录后才能发表博客')
return redirect(url_for('user.login'))
return render_template('main/index.html', form=form)
```
6. 展示博客
```
@main.route('/', methods=['GET', 'POST'])
def index():
form = PostsForm()
...
# 从数据库中读取博客,并分配到模板中,然后在模板中渲染
# 安装发表时间,降序排列
# 只获取发表的帖子,过滤回复的帖子
posts = Posts.query.filter_by(rid=0).order_by(Posts.timestamp.desc()).all()
return render_template('main/index.html', form=form, posts=posts)
```
模板渲染
```html
{# 展示博客内容 #}
{% for p in posts %}
<hr style="margin-top: 10px; margin-bottom: 10px;" />
<div class="media">
<div class="media-left">
<a href="#">
<img class="media-object"
src="{{url_for('static',
filename='upload/'+p.user.icon)}}"
style="width: 64px; height: 64px;" alt="icon">
</a>
</div>
<div class="media-body">
<div style="float: right;">{{moment(p.timestamp).fromNow()}}</div>
<h4 class="media-heading">{{p.user.username}}</h4>
{{p.content}}
</div>
</div>
{% endfor %}
<hr />
```
7. 分页展示
插叙数据时使用专门的分页函数:paginate,参数如下:
page:是唯一的必须参数,表示当前页数
per_page:每页显示的记录数,默认为20条
error_out:页码超出范围时是否显示404错误,默认为True
函数的返回值是一个对象(Pagination),介绍如下:
属性:
items:当前页面的所有记录
page:当前的页码
pages:总页数
total:总记录数
prev_num:上一页的页码
next_num:下一页的页码
has_prev:是否有上一页,有返回True
has_next:是否有下一页,有返回True
方法:
iter_pages:是一个迭代器,每次返回一个在分页导航条上显示的页码
prev:上一页的分页对象
next:下一页的分页对象
8. 封装一个宏,专门负责分页显示
```html
{% macro pagination_show(pagination, endpoint) %}
<nav aria-label="Page navigation">
<ul class="pagination">
{# 上一页 #}
<li {%if not pagination.has_prev %}class="disabled"{% endif %}>
<a href="{% if pagination.has_prev %}{{url_for(endpoint, page=pagination.prev_num, **kwargs)}}{% else %}#{% endif %}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{# 分页页码 #}
{% for p in pagination.iter_pages() %}
{% if p %}
<li {% if pagination.page == p %}class="active"{% endif %}><a href="{{url_for(endpoint, page=p, **kwargs)}}">{{p}}</a></li>
{% else %}
<li><a href="#">…</a></li>
{% endif %}
{% endfor %}
{# 下一页 #}
<li {% if not pagination.has_next %}class="disabled"{% endif %}>
<a href="{% if pagination.has_next %}{{url_for(endpoint, page=pagination.next_num, **kwargs)}}{% else %}#{% endif %}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
{% endmacro %}
```
9. 在视图函数中获取分页对象
```python
@main.route('/', methods=['GET', 'POST'])
def index():
form = PostsForm()
...
# 分页处理
# 获取当前页码,没有认为是第一页
page = request.args.get('page', 1, type=int)
pagination = Posts.query.filter_by(rid=0)
.order_by(Posts.timestamp.desc())
.paginate(page, per_page=3, error_out=False)
posts = pagination.items
return render_template('main/index.html', form=form,
posts=posts, pagination=pagination)
```
10. 在模板中渲染分页导航条
```html
{# 导入分页展示的宏 #}
{% from 'common/macro.html' import pagination_show %}
{# 展示分页导航条 #}
{{ pagination_show(pagination, 'main.index') }}
```
(学习自B站zhu_xuan0612老师,在此表示感谢!)