原文: http://www.catonlinepy.tech/
声明:原创不易,未经许可,不得转载
1. 你将学会什么
今天的教程主要给大家介绍,如何在 Flask 应用中添加个人主页以及在个人主页中如何上传用户头像。教程中的代码都会托管到 github 上,猫姐一如既往的强调,在学习本课内容时一定要亲自动手实现代码,遇到问题再到 github 上查看代码,如果实在不知道如何解决,可以在日志下方留言。
2. 个人主页的实现
2.1 项目目录的创建
在创建个人主页之前,先来创建今天的项目目录,猫姐直接将第 5 天的 day5 目录复制后改成 day6,然后程序里面的 userauth_demo 目录改成 userprofile_demo 目录,并将代码中的 userauth_demo 改为 userprofile_demo。在此基础上,我们还需要创建如下文件和目录:
# 注意:以下所有的操作都必须在虚拟环境中进行
# 在 userprofile_demo 新建文件 utils.py 文件,此文件用来保存一些功能独立的小函数
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6/userprofile_demo$ touch utils.py
# 在 userprofile_demo 目录下新建 static 目录,此目录用来保存 css,js 及图片文件
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6/userprofile_demo$ mkdir static
# cd 到 static 目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6/userprofile_demo$ cd static
# 在 static 目录中新建 profile 目录,用户上传的头像图片将保存到该目录
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6/userprofile_demo/static$ mkdir profile
# 在 templates 目录中新建 account.html 文件
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6/userprofile_demo/templates$ touch account.html
最终,我们得到今天项目的目录结构如下 (使用 tree 命令得到):
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary$ tree day6
day6
├── run.py
└── userprofile_demo
├── config.py
├── database.db
├── forms.py
├── __init__.py
├── models.py
├── routes.py
├── static
│ └── profile
│ └── default.jpg
├── templates
│ ├── account.html
│ ├── index.html
│ ├── layout.html
│ ├── login.html
│ └── register.html
└── utils.py
2.2 为个人主页添加入口链接
通常,在用户登录后,在主页导航栏中会有一个用户名的超链接,当用户点击这个超链接时,就会跳转到用户的个人主页。下面,我们在 layout.html 文件的导航栏中添加个人主页的入口:
<html>
<head>
{% if title %}
<title>{{title}}- 喵星在线 </title>
{% else %}
<title> 喵星在线 </title>
{% endif %}
{% block js %}
{% endblock js%}
</head>
<header>
<div>
<a href="{{url_for('index') }}"> 主页 </a>
{% if current_user.is_authenticated %}
<a href="{{url_for('logout') }}"> 注销 </a>
<a href="#">{{current_user.username}}</a>
{% else %}
<a href="{{url_for('login') }}"> 登陆 </a>
{% endif %}
</div>
</header>
<body>
<!-- 渲染 flash 消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{category}}">
{{message}}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}
{% endblock %}
</body>
</html>
此时用户在登录状态下,访问 http://127.0.0.1:5005/ 时,可以得到如下效果:
2.3 开始创建个人主页
完成了导航中个人主页的入口后,我们需要完成个人主页中呈现的内容。这里 account.html 同样需要继承 layout.html 的导航栏。在 account.html 文件中添加如下代码:
{% extends "layout.html" %}
{% block content %}
<hr>
<div>
<h1> 用户:{{html_user.username}}</h1>
</div>
{% endblock %}
当点击顶部的 miaojie 时,它会将用户带到个人主页页面,服务器为了响应这一请求,还需要添加相应的路由函数,在 routes.py 文件中添加如下代码:
#...
@app.route("/account")
def account():
if not current_user.is_authenticated:
return redirect(url_for('index'))
return render_template("account.html", title="第六天", html_user=curent_user)
#...
此时,还需要修改基模板 layout.html 文件中个人主页的入口链接,才能实现正常的 url 跳转,如下使用 url_for 函数完成个人主页 url 的渲染:
#..
<header>
<div>
<a href="{{url_for('index') }}"> 主页 </a>
{% if current_user.is_authenticated %}
<a href="{{url_for('logout') }}"> 注销 </a>
<a href="{{url_for('account') }}">{{current_user.username}}</a>
{% else %}
<a href="{{url_for('login') }}"> 登陆 </a>
{% endif %}
</div>
</header>
此时刷新主页后,在主页中点击 miaojie,就可以跳转到用户个人页面了,效果如下:
3. 更新用户头像
3.1 在主页中显示用户的头像
上面只是在个人主页中显示了用户的用户名,这里我们再添加显示用户头像的代码,只需在 account.html 中增加如下 img 标签即可:
<!-- 继承基模板 -->
{% extends "layout.html" %}
{% block content %}
<hr>
<div>
<h1> 用户:{{html_user.username}}</h1>
<img alt=""style="border-radius: 50%;"src='/static/profile/default.jpg'width="60"height="60">
</div>
{% endblock %}
同时在 img 标签中增加了一点 css 效果,style=”border-radius:50%”,长和高都为 60px,radius 会将图片渲染成圆形,src 是图片所在的位置。此时我们只需要在 static/profile 目录下放置一张用户头像的图片,然后刷新个人主页,就能看到用户的头像了:
3.2 在个人主页中增加上传头像表单
上文中只是让图像简单的显示在主页中,我们并不能对其进行编辑,为了实现改换用户头像的功能,我们还需要增加图片文件的上传功能。在 models.py 文件中的 user 数据库中增加 profile_image 字段,该字段用来保存用户头像图片的文件名:
#..
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
profile_image = db.Column(db.String(20), nullable=True)
def __repr__(self):
return f"User('{self.username}','{self.email}','{self.password}')"
由于代表数据库中表的 models.py 文件发生了变化,所以需要进行数据库的迁移,但是在进行迁移之前,我们先将 config.py 文件的位置放到与 run.py 同级目录中,第 4 课中已经讲了数据库的迁移,大家可以直接按照第 4 课的 5.3 小结内容进行操作即可,同样在操作之前,需要设置环境变量,环境变量在第 1 课的 3 节最后已经讲过如何设置了。迁移完成后 database.db 会在 run.py 同级目录中生成。
数据库的用户表更新后,需要在 forms.py 文件中增加更新用户信息使用的表单 UpdateAccountForm:
#..
from flask_wtf.file import FileField, FileAllowed
#..
class UpdateAccountForm(FlaskForm):
username = StringField(u'用户名',
validators=[DataRequired(), Length(min=2,max=20)])
email = EmailField(u"邮箱",
validators=[DataRequired()])
password = StringField(u'密码', validators=[DataRequired()])
profile_image = FileField(u"更新头像", validators=[FileAllowed(["png", "jpg"])])
submit = SubmitField(u'更新')
在表单中 FileField 字段使用了 flask_wtf 提供的 FileAllowed 验证函数,它确保上传的图像只能是 png 和 jpg 两种格式,FileField 字段会被 Jinja2 渲染生成 type=”file” 的 <input> 标签。
在模板 account.html 中添加渲染前端表单的内容 (html_form 对象是通过路由函数传到这里的):
<!-- 继承基模板 -->
{% extends "layout.html" %}
{% block content %}
<hr>
<div>
<h1> 用户:{{html_user.username}}</h1>
<img alt=""style="border-radius: 50%;"src='/static/profile/default.jpg'width="60"height="60">
</div>
<hr>
<div>
<form method="POST" action="{{url_for('account') }}" enctype="multipart/form-data">
{{html_form.hidden_tag() }}
<fieldset>
<div class="form-group">
{{html_form.username.label(class="form-control-label") }} <br>
{{html_form.username(class="form-control form-control-lg") }}
</div>
<div class="form-group">
{{html_form.email.label(class="form-control-label") }} <br>
{{html_form.email(class="form-control form-control-lg") }}
</div>
<div class="form-group">
{{html_form.profile_image.label() }} <br>
{{html_form.profile_image(class="form-control-file") }}
</div>
</fieldset>
<div class="form-group">
{{html_form.submit(class="btn btn-outline-info") }}
</div>
</form>
</div>
{% endblock %}
上面已经完成 account.html 页面前台 form 表单的显示(渲染)工作,这时就需要在视图函数中(python 文件)将代表表单的类传递到前端模板文件(html 文件)中,下面在 routes.py 中完成视图函数的编写:
#..
# 从 userprofile_demo.forms 中导入 UpdateAccountForm
from userprofile_demo.forms import LoginForm, UpdateAccountForm
# ..
@app.route("/account")
def account():
if not current_user.is_authenticated:
return redirect(url_for('index'))
form = UpdateAccountForm()
form.email.data = user.email
form.username.data = user.username
return render_template("account.html", html_user=current_user, html_form=form)
#..
这里,视图函数默认收到的是 get 请求,并且将当前登录用户的信息传递到 form 表单,最终用户的信息将会在前端显示出来(user.email 是从数据库中读取 email 的值显示在邮箱输入框中,user.username 表示从数据库中读取 username 的值显示在用户名输入框中)。再次刷新个人主页页面,效果如下:
3.3 在 config.py 文件中配置上传头像目录
在上面的 account.html 文件中,img 标签中的 src 显示的是固定的图像,如果我们需要显示其它的用户头像,每次都需要修改 account.html 文件中的 img 标签,但是我们并不希望这么做。为了能够方便的更新用户的头像信息,我们需要使用更加聪明的方法。我们首先需要在 config.py 文件中配置用户头像保存的目录,如下:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'database.db')
# 使用表单, 对 Flask_WTF 进行配置
SECRET_KEY = 'miaojie is great!'
# 配置上传用户头像的目录
PROFILE_PATH = "profile/"
然后我们修改模板文件 account.html 中的 img 标签,将 src 属性的值更改为视图函数传过来的变量,如下:
<!-- 继承基模板 -->
{% extends "layout.html" %}
{% block content %}
<hr>
<div>
<h1> 用户:{{html_user.username}}</h1>
<img alt=""style="border-radius: 50%;"src='{{url_for("static", filename = html_user_image)}}'width="60"height="60">
</div>
#..
{% endblock %}
下面我们在 routes.py 文件中,对用户的头像图片变量 profile_image 进行处理,使其能向前端传入正确的用户头像文件:
#..
# 从 flask 中导入 current_app
from flask import current_app
#..
@app.route("/account")
def account():
if not current_user.is_authenticated:
return redirect(url_for('index'))
form = UpdateAccountForm()
profile_image = current_user.profile_image
if profile_image:
profile_image = current_app.config["PROFILE_PATH"] + profile_image
else:
profile_image = current_app.config["PROFILE_PATH"] + "default.jpg"
form.email.data = current_user.email
form.username.data = current_user.username
return render_template("account.html", title="第六天", html_user=current_user, html_form=form, html_user_image=profile_image)
#..
当我们还没有上传新的用户头像文件时,profile_image 变量为 None 值,所以会使用 default.jpg 图片文件作为用户的默认头像。
3.4 更新用户头像的 post 请求
上面只是实现了用户头像的显示,这里将介绍如何对用户的头像进行更新。当用户选择了新的头像文件后,点击“更新”按钮时,会触发 post 请求,此时 account 视图函数需要对该 post 进行正确的处理,才能实现用户头像文件的上传:
#..
@app.route("/account", methods=["GET", "POST"])
def account():
if not current_user.is_authenticated:
return redirect(url_for('index'))
form = UpdateAccountForm()
profile_image = current_user.profile_image
if profile_image:
profile_image = current_app.config["PROFILE_PATH"] + profile_image
else:
profile_image = current_app.config["PROFILE_PATH"] + "default.jpg"
if request.method == "POST":
username = form.username.data
email = form.email.data
if form.profile_image.data:
picture_file = save_user_face_image(form.profile_image.data)
current_user.profile_image = picture_file
current_user.username = username
current_user.email = email
db.session.commit()
return redirect(url_for("account"))
form.email.data = current_user.email
form.username.data = current_user.username
return render_template("account.html", title="第六天", html_user=current_user, html_form=form, html_user_image=profile_image)
#..
上面代码中,当 account 视图函数拿到 post 请求后,从表单中获取用户输入的用户名和邮箱,并保存到 username 和 email 变量中。这里,如果 form.profile_image.data 为 True,则代表用户上传了新的头像文件,此时我们需要将用户上传的文件保存至 profile 目录,并使用新的用户头像文件名更新数据库中的 profile_image 字段。
在上面的 post 请求中,还没有实现 save_user_face_image() 函数,该函数的定义,我们将它单独放在 utils.py 文件中。由于该函数在处理图片文件时,需要用到 PIL 模块,所以我们先使用 pip 命令来安装 pillow 库:
(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day6$ pip install pillow
现在,在 utils.py 文件中实现 save_user_face_image() 函数:
import os,secrets
from flask import current_app
# 从 PIL 中导入 Image 类
from PIL import Image
def save_user_face_image(image_data):
random_hex = secrets.token_hex(8)
f_name, f_ext = os.path.splitext(image_data.filename)
new_image_name = random_hex + f_ext
new_image_path = os.path.join(current_app.root_path, "static/profile", new_image_name)
output_size = (125,125)
i = Image.open(image_data)
i.thumbnail(output_size)
i.save(new_image_path)
return new_image_name
在 route.py 文件中还需要导入 save_user_face_image:
#..
from userprofile_demo.utils import save_user_face_image
#..
OK!大功告成,此时,我们浏览 http://127.0.0.1:5005/account 页面,点击“Browse”按钮,选择图片后点击“更新”按钮,就可以实现用户头像的更新了,效果如下:
4. 总结
学习完今天的内容,我们实现了如下功能:
- 在导航栏中添加了个人主页的入口
- 实现了用户个人主页的展示
- 实现了用户个人头像的更新
下一课的教程,猫姐将带领大家一起学习创建、更新和删除日志。今天的内容就到这里,喜欢的同学们可以在下面点赞留言,或是访问我的博客地址:http://www.catonlinepy.tech/ 加入我们的 QQ 群进一步交流学习!
5. 代码的获取
大家可以到 github 上获取今天教程的所有代码:https://github.com/miaojie19/…
具体下载代码的命令如下:
# 使用 git 命令下载 flask-course-primary 仓库所有的代码
git clone https://github.com/miaojie19/flask-course-primary.git
# 下载完成后,进入 day6 目录下面,即可看到今天的代码
cd flask-course-primary
cd day6