跟着学习(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vi-profile-page-and-avatars
跟着学习(旧版):http://www.pythondoc.com/flask-mega-tutorial/profile.html
回顾上一章:https://blog.csdn.net/weixin_41263513/article/details/85013477
本章内容
- 个人页面
- 头像
- 用Jinja2的子模版
- 个人时间,访问痕迹
- 记录用户的上次访问时间
- 编辑个人资料
个人页面
要创建一个个人资料页面,先写一个新的视图函数,映射到/user/
文件:/app/routes.py
@app.route('/user/<username>')
@login_required
def user(username):
user = User.query.filter_by(username=username).first_or_404()
posts = [
{'author': user, 'body': 'Test post #1'},
{'author': user, 'body': 'Test post #2'}
]
return render_template('user.html', user=user, posts=posts)
这个@app.route装饰器看起来与之前的有点不通过,因为这里多了一个的动态URL组件。当路由具有动态组件时,Flask将接受URL部分中的任何文本,并将以实际文本作为参数调用视图函数,如:如果客户端浏览器请求URL / user / susan,则将使用参数username设置为“susan”来调用视图函数,此视图功能只能由登录用户访问,因此我添加了Flask-Login中的@login_required装饰器。
在这个视图函数中,我使用名为first_or_404()的first()变体,当有结果时,它与first()完全相同,但是如果没有结果,则自动将404错误发送给请求的人看到
如果数据库查询未触发404错误,则表示找到具有给定用户名的用户。接下来,我为该用户初始化一个虚假的帖子列表,最后呈现一个新的user.html模板,我将该模板传递给用户对象和帖子列表。
文件:/app/templates/user.html
{% extends "base.html" %}
{% block content %}
<h1>User: {{ user.username }}</h1>
<hr>
{% for post in posts %}
<p>
{{ post.author.username }} says: <b>{{ post.body }}</b>
</p>
{% endfor %}
{% endblock %}
个人页面已经完成,现在需要在顶部的导航栏中添加一个链接
文件:/app/templates/base.html
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
我们可以使用Flask-Login的current_user来生成正确的URL
头像
为了进一步改进资料页面的外观,可以在页面中显示用户的头像,我们将会用Gravatar提供用户头像。Gravatar是一个行业领先的头像服务,能把头像和电子邮箱关联起来,用户要先到该网站上注册账户,然后上传图像。这个服务通过一个特殊的URL对外开放用户的头像,这个URL中包含用户电子邮箱地址的MD5散列值(说实话,在学习之前,我根本不知道Gravata这东西,不过跟着学习嘛,就当是跟着了解一下,扩充眼界,以后再慢慢细改)
由于头像跟用户关联,因此需要将头像URL的逻辑添加到用户模型
文件:/app/models.py
from hashlib import md5
# ...
class User(UserMixin, db.Model):
# ...
def avatar(self, size):
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
digest, size)
User类的新avatar()方法返回用户头像图像的URL,缩放到请求的大小(以像素为单位)。 对于没有注册化身的用户,将生成“同心”图像。要生成MD5哈希,我首先将电子邮件转换为小写,因为Gravatar服务需要这样做。 然后,因为Python中的MD5支持在字节而不是字符串上工作,所以在将字符串传递给哈希函数之前,我将字符串编码为字节。
下一步是将头像图像插入用户资料模板中
文件:/app/templates/user.html
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
<p>
{{ post.author.username }} says: <b>{{ post.body }}</b>
</p>
{% endfor %}
{% endblock %}
让User类负责返回头像URL的好处是,如果有一天我不想用Gravatar头像了,我可以重写avatar()方法返回不同的URL,所有模板将自动显示新的头像。
在用户个人资料页面的顶部有一个漂亮的大头像,但还有一些来自底部用户的帖子,每个帖子都应该有一个小头像。
要显示各个帖子的头像,我只需要在模板中进行一个小的更改:
文件:/app/templates/user.html
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
{% endfor %}
{% endblock %}
用Jinja2的子模版
我们已经实现了用户信息页,它能够显示用户的 blog。我们的首页也应该显示任何一个用户这个时候的 blog 。这样我们有两个页需要显示用户的 blog。当然我们可以直接拷贝和复制处理渲染 blog 的模板,但这不是最理想的。因为当我们决定要修改 blog 的布局的时候,我们要更新所有使用它的模板。
相反,我们将要制作一个渲染 blog 的子模板,我们在使用它的模板中包含这个子模板。
我们创建一个 blog 的子模板,这是一个再普通不过的模板, _前缀只是一个命名约定,可以帮助我识别哪些模板文件是子模板。
文件:/app/templates/_post.html
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
要从user.html模板调用此子模板,我使用Jinja2的include语句
文件:/app/templates/user.html
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
{% endblock %}
个人时间,访问痕迹
用户除了头像,还少不了个人简介,让别人更好的认识ta,除此之外,我们还可以跟踪每个用户最后一次访问该网站的时间,并在他们的个人资料页面上显示时间
为此,我们需要用两个新字段扩展数据库中的users表:
文件:/app/models.py
class User(UserMixin, db.Model):
# ...
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
每次修改数据库时,都必须生成数据库迁移,详情请看数据库这一章节,迁移数据库这一操作十分重要,数据库中的任何用户仍然存在,迁移数据库不会破坏任何数据
改了users模型,就必须要再对应的模板html中添加新字段
文件:/app/templates/user.html
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td>
<h1>User: {{ user.username }}</h1>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
</td>
</tr>
</table>
...
{% endblock %}
记录用户的上次访问时间
让我们先从last_seen字段开始,想做的是在用户向服务器发送请求时为用户写入此字段的当前时间:
文件:/app/routes.py
from datetime import datetime
@app.before_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
Flask的@before_request装饰器作用时用来注册一个函数,在每次请求之前运行,该实现只是检查current_user是否已登录,并且在这种情况下将last_seen字段设置为当前时间。该实现只是检查current_user是否已登录,并且在这种情况下将last_seen字段设置为当前时间。现在将这些时间戳存储在UTC时区中使得user.html上显示的时间也是UTC。除此之外,时间格式不是您所期望的,因为它实际上是Python日期时间对象的内部表示。现在,不用担心这两个问题,因为在后面的章节中将会讨论处理Web应用程序中的日期和时间的主题。
编辑个人资料
现在还需要向用户提供一个表格,在表格中他们可以输入一些关于他们自己的信息。 该表单将允许用户更改其用户名,并编写有关他们自己的内容,存储在新的about_me字段中。 让我们开始为它编写一个表单类
文件:/app/forms.py
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
# ...
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
submit = SubmitField('Submit')
有了新的表单类,就需要创建新的html模板来接收用户填写的表单了:
文件:/app/templates/edit_profile.html
{% extends "base.html" %}
{% block content %}
<h1>Edit Profile</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.about_me.label }}<br>
{{ form.about_me(cols=50, rows=4) }}<br>
{% for error in form.about_me.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
最后,在视图函数中把一切内容给连接起来
文件:/app/routes.py
from app.forms import EditProfileForm
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
db.session.commit()
flash('Your changes have been saved.')
return redirect(url_for('edit_profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title='Edit Profile',
form=form)
可能有些朋友看不懂elif之后的内容,其实吧,我也不太懂蛤蛤,大概来说就是为保万无一失,某些原因会让validate_on_submit()返回false,导致提交失败,不过这种情况还是非常少的,到后面我们再来慢慢修改精简
那么为了让用户能更好,更快的编辑自己的资料,需要在顶部的导航栏多加一个链接
文件:/app/templates/user.html
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% endif %}
激动人心效果图:
这一章节会拉出一个非常非常非常大的bug,下一章混合着解决办法一起说了