最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第六章,其中我将告诉您如何创建用户个人资料页面。
供您参考,以下是本系列文章的列表。
- 第1章:Hello, World!
- 第2章:模板
- 第3章:Web表单
- 第4章:数据库
- 第5章:用户登录
- 第6章:配置文件页面和头像(本文)
- 第7章:错误处理
- 第8章:关注与被关注
- 第9章:分页
- 第10章:电子邮件支持
- 第11章:整容
- 第12章:日期和时间
- 第13章:I18n和L10n
- 第14章:Ajax
- 第15章:大型应用程序结构
- 第16章:全文搜索
- 第17章:在Linux上部署
- 第18章:在Heroku上部署
- 第19章:Docker容器上的部署
- 第20章:一些JavaScript Magic
- 第21章:用户通知
- 第22章:后台工作
- 第23章:应用程序编程接口(API)
本章将专门用于向网站添加用户个人配置页面。用户配置页面是显示关于用户信息的页面,通常由用户自己输入信息。我将向您展示如何动态生成所有用户的配置文件页面,然后我将添加一个小型配置文件编辑器,用户可以使用它来输入他们的信息。
用户资料页面
要创建用户配置文件页面,我们首先编写一个映射到/user/<username> URL 的新视图函数。
# app/routes.py: User profile view function
@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
看起来与前面的有点不同。在本例中,我有一个动态组件,它被表示为<username>( '<
URL组件>' )。当路由具有动态组件时,Flask将接受URL传递的任何文本,并将以实际文本作为参数调用视图函数。例如,如果客户端浏览器请求URL/user/susan
,则将参数username
设置为'susan'
并将调用视图函数。此视图函数只能由登录用户访问,因此我添加了Flask-Login中的@login_required
装饰器。
这个视图函数的实现非常简单。我首先尝试使用用户名查询数据库,并从数据库加载用户。您之前已经知道,通过调用来执行数据库查询时,如果您想获得所有结果就用all()
,或者您想获得第一个结果/None就使用first()
。在这个视图函数中,我使用了一个被调用first()
的变体first_or_404()
,它与first()
有返回结果的情况完全一样。但是在没有结果的情况下,会自动将404错误发送回客户端。以这种方式执行查询,我不会检查查询是否返回用户,因为当数据库中不存在用户名时,函数将不会返回,而是会引发404异常。
如果数据库查询未触发404错误,则表示找到具有给定用户名的用户。接下来,我为该用户初始化一个虚假的帖子列表,最后渲染一个新的user.html模板。
user.html模板如下所示:
app/templates/user.html: User profile template
{% 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: User profile template
<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>
这里唯一有趣的变化是用于生成配置文件页面链接调用的
url_for()
。由于用户配置文件视图函数采用动态参数,因此url_for()
函数接收其值作为关键字参数。由于这是指向登录用户配置文件的链接,因此我可以使用Flask-Login 中的current_user
来生成正确的URL。
现在试试应用程序。单击顶部的
Profile
链接可以转到您自己的用户页面。此时,没有链接可以访问其他用户的配置文件页面,但如果要访问这些页面,您可以在浏览器的地址栏中手动键入URL。例如,如果在应用程序中注册了叫“john”的用户,则可以通过在地址栏中键入http://localhost:5000/user/john来查看相应的用户配置文件。
头像
你肯定同意我们刚刚建立的个人资料页面非常Low。为了让它们更有趣,我将添加用户头像,但是我不打算在服务器中处理可能大量上传的图像,我将使用Gravatar服务为所有用户提供图像。
Gravatar服务使用起来非常简单。要为给定用户请求图像,请使用格式为https://www.gravatar.com/avatar/<hash>的URL ,其中<hash>
是用户电子邮件地址的MD5哈希值。您可以在下面看到如何通过电子邮件john@example.com
获取用户的Gravatar URL :
>>> from hashlib import md5
>>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest()
'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
如果您想查看实际示例,我自己的Gravatar URL是https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35。以下是Gravatar为此网址返回的内容:
默认情况下,返回的图像大小为80x80像素,但可以通过向URL的查询字符串添加参数s
来请求不同的大小。例如,要获取我自己的头像作为128x128像素图像,URL为https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128。
可以作为查询字符串参数传递给Gravatar的另一个有趣的参数是d
,它确定Gravatar为没有在服务中注册的头像的用户提供的图像。我最喜欢的是“identicon”,它为每个Email地址返回一个不同的几何设计。例如:
请注意,某些Web浏览器扩展程序(例如Ghostery会阻止Gravatar图像),因为他们认为Automattic(Gravatar服务的所有者)可以根据您的头像请求来确定您请求的网站。如果您在浏览器中没有看到头像,可能是由于您在浏览器中安装了这样的扩展程序。
由于头像与用户相关联,因此将生成头像URL的函数添加到User模型是有意义的。
# app/models.py: User avatar URLs
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,缩放到请求的大小(以像素为单位)。对于没有注册头像的用户,将生成“identicon”图像。要生成MD5哈希,我首先将电子邮件转换为小写,这是Gravatar服务要求的。然后,因为Python中的MD5支持工作在bytes而不是字符串,所以在将字符串传递给哈希函数之前,我将字符串编码为bytes。
如果您有兴趣了解Gravatar服务提供的其他选项,请访问他们的文档网站。
下一步是在用户个人资料模板中插入头像图像:
app/templates/user.html: User avatar in template
{% 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: User avatars in posts
{% 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子模板
我设计了用户个人资料页面,以便显示用户写的帖子及其头像。现在我希望首页页面也显示具有类似布局的帖子。我可以复制/粘贴处理帖子渲染的模板部分,但这实际上并不理想,因为以后如果我决定对这个布局进行更改,我将不得不记得更新这两个模板。
相反,我将创建一个帖子的子模板渲染它,然后我从user.html和index.html模板中引用它。首先,我创建子模板,只需要为一个帖子标记HTML。我要将此模板命名为app/templates/_post.html。 '_'
前缀只是一个命名约定,以帮助我认识的模板文件是子模板。
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: User avatars in posts
{% 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 %}
应用程序的首页页面内容还不够充实,所以我并不打算现在添加头像和帖子。
更有趣的配置文件
新用户配置文件页面还存在的一个问题是它们并没有显示更多个人信息。用户喜欢在这些页面上讲一些关于他们自己的介绍,所以我会让他们写一些关于自己的东西来展示这里。我还将跟踪每个用户最后一次访问该网站的时间,并在他们的个人资料页面上显示它。
首先,我们必须支持这些额外信息,即用两个新字段扩展数据库中的users表:
# app/models.py: New fields in user model
class User(UserMixin, db.Model):
# ...
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
每次修改数据库时,都必须生成数据库迁移。
在第4章中,我向您演示了如何通过迁移脚本跟踪数据库更改。现在我有两个要添加到数据库的新字段,因此第一步是生成迁移脚本:
(venv) $ flask db migrate -m "new fields in user model"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'user.about_me'
INFO [alembic.autogenerate.compare] Detected added column 'user.last_seen'
Generating /home/miguel/microblog/migrations/versions/37f06a334dbf_new_fields_in_user_model.py ... done
migrate
命令的输出看起来没有问题,因为它显示了User
类中的两个新字段被检测到。现在我可以将此更改应用于数据库:
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 780739b227a7 -> 37f06a334dbf, new fields in user model
我希望您意识到使用迁移框架是多么有用。数据库中之前的用户仍然存在,通过应用迁移脚本中的更改,迁移框架更新数据库但不会破坏任何数据。
下一步,我将把这两个新字段添加到用户配置文件模板中:
app/templates/user.html: Show user information in user profile template
{% 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字段
,这比上面两个字段更容易。在用户向服务器发送请求时,我们需要为给定用户的该字段写入当前时间。
从浏览器请求的每个视图函数上添加登录去设置此字段显然是不切实际的,但是可以在请求分派到视图函数之前前执行此任务,Flask中提供这种机制。看看解决方案:
app/routes.py: Record time of last visit
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
字段设置为当前时间。
之前提到过,服务器应用程序需要以一致的时间单位工作,标准做法是使用UTC时区。使用系统的本地时区并不是一个好主意,因为数据库中的时间取决于您的位置。
最后一步是提交数据库会话,以便将上面所做的更改写入数据库。
如果你想知道为什么在提交之前没有
db.session.add()
,其实是在调用current_user
时,Flask-Login将调用用户加载器中的回调函数,该函数将运行数据库查询,将此用户置于数据库会话中。因此,您也可以在此函数中再次添加用户,由于用户已经存在,所以这不是必需的。
如果您现在查看个人资料页面,您将看到“Last seen on”,其时间非常接近当前时间。如果您离开个人资料页面然后返回,您将看到时间不断更新。
译者注:国内时区是UTC+8
我将这些时间戳以UTC时区的格式存储,使得配置文件页面上显示的时间也是UTC时间。除此之外,时间格式也不是我们喜欢的,因为它实际上是Python日期时间对象的内部表示形式。现在,我不会考虑这两个问题,因为我将在后面的章节中讨论处理Web应用程序中的日期和时间。
资料编辑
我还需要向用户提供一个表单,他们可以在表单中输入一些关于自己的信息。表单允许用户更改其用户名,并编写有关他们自己的个人简介,并将其存储在about_me
字段中。让我们开始编写一个表单类:
app/forms.py: Profile editor form
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')
我将在这个表单中使用新字段类型和新的验证器。对于“About”字段,我正在使用
TextAreaField
,这是一个多行框,用户可以在其中输入文本。要验证这个字段使用Length
,这将确保输入的文本在0到140个字符之间,这是我为数据库中相应字段分配的空间。
渲染此表单的模板如下所示:
# app/templates/edit_profile.html: Profile editor form
{% 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: Edit profile view function
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)
此视图功能与处理表单的其他功能略有不同。如果
validate_on_submit()
返回True
,我将表单中的数据复制到用户对象中,然后将对象写入数据库。但是当validate_on_submit()
返回时False
,可能是由于两个不同的原因。第一,它可能是因为浏览器刚刚发送了一个GET
请求,我们需要通过提供表单模块的初始版本来响应请求。第二,当浏览器发送POST
带有表单数据的请求时,数据中的某些内容无效。
对于这个表单,我需要分别处理这两种情况。当表单是GET
请求时,我想用数据库中存储的数据预先填充字段,所以我需要将存储在用户字段中的数据复制到表单中,确保这些表单字段具有用户存储的当前数据。但是在验证错误的情况下,我不想写入任何内容到数据库,因为现在的表单已经由WTForms填充。为了区分这两种情况,我将检查request.method
请求类型,如果是GET
初始化请求表单,如果是POST则
提交验证失败。
为方便用户访问个人资料编辑器页面,我可以在他们的个人资料页面中添加一个链接:
app/templates/user.html: Edit profile link
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% endif %}
注意我正在使用的条件判断,以确保在您查看自己的个人资料时显示编辑链接,但在其他人查看时个人资料时则不会显示编辑。
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vi-profile-page-and-avatars