11.flask博客项目实战六之用户个人资料
Posted 豆约翰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11.flask博客项目实战六之用户个人资料相关的知识,希望对你有一定的参考价值。
配套视频教程
本章将专门用于给应用程序添加用户个人资料页面。用户个人资料页面呈现的是关于用户信息的页面,通常具有由用户自己输入的信息。接下来将展示如何动态生成用户个人资料页面,然后添加一个小型个人资料编辑器,用户可用它来输入Ta们的信息。
用户个人资料页面
要创建一个用户个人资料页面,首先编写一个映射到 /user/<username>URL
的新视图函数。
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装饰器下声明的视图函数看起来与前面的有点不同。在这个示例下,有一个动态组件,它被作为<username>
URL组件 表示,并由 <
和>
包围。当路由有动态组件时,Flask将接受URL部分中的任何文本,并将以实际文本作为参数调用视图函数。例如,如果客户端浏览器请求这个URL /user/susan
,则将调用视图函数并将参数username
设置为susan
。这个视图函数只能由登录用户访问,因此添加了Flask-Login的@login_required
装饰器。
这个视图函数的实现非常简单。首先,尝试用username
查询从数据库加载用户。在之前学过,假如想获得所有结果,可调用all()
执行数据库查询;假如想获得第一个结果 或None
(0个结果时),可调用first()
执行数据库查询。在这个视图函数中,使用了一个叫first_or_404()
的first()
变体,在有结果的情况下它与first()
完全一样,不过在没有结果的情况下,会自动将404 error
发送回客户端。以这种方式执行查询,不用检查查询是否返回一个用户,因为数据库中不存在username
时,函数将不会返回,而是会引发404异常
。
如果数据库查询没有触发404 error
,那么表示找到给定username
的用户。接下来, 为用户初始化一个“假”的帖子列表,最后渲染一个新的user.html模板,将该模板传递给用户对象、帖子列表。
app/templates/user.html:用户个人资料模板
{% extends "base.html" %}
{% blcok 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>
上述唯一有趣的变化是用于生成个人资料页面链接的url_for()
调用。由于用户个人资料视图函数采用动态参数,因此url_for()
函数接收其值作为关键字参数。因为这是指向登录用户的个人资料的链接,所以使用Flask-Login的current_user
生成正确的URL。
运行程序,效果:
(venv) D:microblog>flask run
* Serving Flask app "microblog.py"
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [10/Aug/2018 18:04:30] "GET /user/belen HTTP/1.1" 200 -
127.0.0.1 - - [10/Aug/2018 18:04:38] "GET /user/susan HTTP/1.1" 200 -
当前数据库里有两个用户:susan、belen。改变URL /user/<username>
将得到不同的页面;单击“Profile”按钮将转到登录用户的用户页面。但键入数据库中没有的username时,将得404页面。
用户头像
目前为止,建立的个人资料页面很单调。增加用户头像,但不打算在服务器中处理大量上传的图像,将使用Gravatar服务未所有用户提供图像。
Gravatar服务器使用起来很简单。要为给定用户请求图像,格式为 https://www.gravatar.com/avatar/john@example.com
的用户的Gravatar URL:
(venv) D:microblog>python
Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from hashlib import md5
>>> ‘https://www.gravatar.com/avatar/‘ + md5(b‘john@example.com‘).hexdigest()
‘22861789757368.jpg‘
浏览器输入上述URL
https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6
将显示
我的实例,Gravatar URL是
https://s.gravatar.com/avatar/9aefdb9ccdb72aa75ccbe1b921f9d9f2?s=80
返回的头像是:
默认情况下,返回的图形大小为80x80像素,但可通过向URL的查询字符串添加 s
参数来请求不同大小的尺寸。例如,要获取一个128x128像素的图像,URL为
https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=128
另一个有趣的参数是 d
,它能确定在服务中没有头像注册的用户由Gravatar提供图像。如作者喜欢的“identicon”,它为每个电子邮件返回一个不同的几何设计。如:
PS:某些Web浏览器扩展程序(如 Ghostery会阻止Gravatar图像)因为它们会认为Automattic(Gravatar服务的所有者)可以根据它们为您的头像获取的请求来确定访问的网站。如果在浏览器中没有看到头像,可考虑的问题是 可能由于在浏览器中安装了某个扩展程序。
由于头像与用户相关联,因此将生成头像的URL的逻辑添加到User 用户模型中是很重要的。
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支持字节而不是字符串,所以得在字符串传递给哈希函数之前,将字符串编码为字节。
参考:Gravatar服务文档
接下来,在用户个人资料模板中插入头像图像:
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%}
运行程序,效果:
(venv) D:microblog>flask run
* Serving Flask app "microblog.py"
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [10/Aug/2018 19:34:34] "GET /user/belen HTTP/1.1" 200 -
使用Jinja2子模板
上述设计了用户个人资料页面,以便显示用户写的帖子、头像。如今,希望/index
页面也显示具有类似布局的帖子。如果复制、粘贴处理帖子渲染的模板部分,这是不理想的,因为今后如果决定对这个布局进行更改,则一定要同时更新这两个模板。
理想方法:创建一个只渲染一个帖子的子模板,然后从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模板调用此子模板,得使用Jinja2include
语句:更新代码
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 %}
运行程序,效果是一样的:
PS:目前应用程序的 /index
页面还未真正充实,暂不添加此功能。
更多有趣的个人资料
新用户个人资料页面有一个问题是Ta们并没有真正展现出太多。用户喜欢在这些页面上讲一些关于Ta们的内容,所以讲让Ta们写一些关于Ta们自己内容展示在这里。还将跟踪每个用户最后一次访问该网站的时间,并显示在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)
#...
每次修改数据库时,都必须生成数据库迁移。在第4章,展示了如何设置应用程序以通过迁移脚本跟踪数据库更改。上述,有两个要添加到数据库的新字段,因此第一步是生成迁移脚本:
C:UsersAdministrator>d:
D:>cd D:microblogvenvScripts
D:microblogvenvScripts>activate
(venv) D:microblogvenvScripts>cd D:microblog
(venv) D:microblog>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 D:microblogmigrationsversions 0cd8a8ea68a_new_fields_in_user_model.py ... done
可注意到一个细节,-m
参数的描述内容会自动添加到/versions
下的迁移.p文件名中。
migrate
命令的打印很友好,如显示了User类
中两个新字段被检测到。现在可将这个更改运用于数据库:
(venv) D:microblog>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 c0139b2ef594 -> 00cd8a8ea68a, new fields in user model
使用迁移框架非常有用,数据库中的任何用户仍然存在,迁移框架通过“外科手术”运用迁移脚本中的更改而不会破坏任何数据。在CMD下命令行查看数据库中表结构:
(venv) D:microblog>sqlite3 app.db #进入数据库
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> .tables #查看数据库中的表
alembic_version post user
sqlite> .schema user #查看表结构
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(64),
email VARCHAR(120),
password_hash VARCHAR(128), about_me VARCHAR(140), last_seen DATETIME,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX ix_user_email ON user (email);
CREATE UNIQUE INDEX ix_user_username ON user (username);
sqlite> select * from user;#查看user表数据
1|susan|susan@example.com|pbkdf2:sha256:50000$OONOkVyy$8d008c6647ab95a5793cf60bf57eaa3bb1123d6e5b3135c5cc5e42e02eddae32||
2|belen|belen@example.com|pbkdf2:sha256:50000$PEDt5NxS$cf6c958c97b6ad28d9495d138cb5a310f6f2389534b0cafa3002dd3cec9af9d1||
sqlite> .quit #退出sqlite语句
(venv) D:microblog>
接着,将这俩个新字段添加到用户个人资料模板中:
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>
<hr>
{% for post in posts %}
{% include ‘_post.html‘ %}
{% endfor%}
{% endblock%}
注意:这两个字段包装在Jinja2的条件中,因为只希望Ta们在设置时可见。此时对于所有用户,这俩个新字段都是空的,因此,如果现在运行该应用程序,不会看到这些字段。
记录用户上次访问时间
last_seen
这个字段相对更容易。现在要做的是:在用户向服务器发送请求时,为给定用户写入此字段的当前时间。
在从浏览器请求的每一个可能的视图函数中,添加登录去设置这个字段,这明显是不切实际的。请求分派到视图函数之前执行一些通用逻辑,这在Web应用程序中是一个常见任务,Flask将它作为一个原生特征提供。解决方案:
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字段设置为当前时间。
之前有提到,服务器应用程序需要以一致的时间单位工作,标准做法是使用UTC时区。使用系统的本地时间不是一个好办法,因为数据库中的内容取决于你的位置。
最后一步是提交数据库会话,以便将上面所做的更改写入数据库。
在提交之前,想知道为什么没有db.session.add()
,得考虑什么时候引用current_user
,Flask-Login将调用用户加载器回调函数,该函数将允许数据库查询,将目标用户置于数据库会话中。因此,可在此功能中再次添加用户,但这不是必需的,因为它已经存在了。
如果在做了这个更改后查看某个用户个人资料页面,那么将看到“Last seen on
”行,其时间非常接近当前时间。而如果离开个人资料页面,再次返回,将看到时间更新。
实际上,我们将这些时间戳存储在UTC时区中,使得在个人资料页面显示的时间也是UTC。除此以外,时间格式不是所期望的,因为它实际是Python日期时间对象的内部表示。目前,不考虑这些问题,在今后的章节中讨论Web应用程序的日期和时间的话题。
运行程序,登录(用户名1+密码:belen,Abc123456;用户2:susan,cat )这个用户后,点击“Profile”
,效果:
(venv) D:microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
个人资料编辑
想用户提供一个表格,在表格中用户可输入一些关于Ta们自己的信息。这个表单允许用户修改用户名,并编写有关自己的内容,以存储在新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‘)
about_me字段
用的是TextAreaField
,是一个多行框,用户可以在这输入文本。用Length
验证这个字段,确保输入的文本在0-140个字符之间,这也是为数据库中相应字段分配的空间。
渲染上述表单的模板:
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![这里写图片描述](https://img-blog.csdn.net/20180811191336485?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODI1NjQ3NA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) = current_user.about_me
return render_template(‘edit_profile.html‘, title=‘Edit Profile‘, form=form)
这个视图函数与处理表单的其他函数略有不同。如果validate_on_submit()
返回True
,则将表单中的数据复制到用户对象中,然后将对象写入数据库;返回False
,可能是由于两个不同的原因。首先,可能是因为浏览器刚发送了一个GET
请求,需要通过提供表单模板的初始版本来响应。也可能是当浏览器发送带有表单数据的POST
请求时,该数据中的某些内容无效。对于这种形式,需要分别处理这两种情况。当第一次以GET
请求表格时,想用数据库中已存储的数据预先填充字段,所以得做与提交情况相反的操作,并将存储在用户字段中的数据移动到表单中,因为这将确保这些表单字段具有为用户存储的当前数据。但在验证错误的情况下,我们不想在表单字段中写入任何内容,因为这些已由WTForms填充。为区分这两种情况,得检查request.method
,对初始化请求这将是GET
,并对验证失败的提交将是POST
。
为方便用户访问个人资料编辑页面,可在Ta们个人资料页面中添加一个链接:
app/templates/user.html:添加个人资料编辑的链接
{% if user.last_seen %}
<p>Last seen on:{{ user.last_seen }}</p>
{% endif %}
{% if user == current_user %}
<p>
<a href="{{ url_for(‘edit_profile‘) }}">Edit your profile</a>
</p>
{% endif %}
上述代码中加了个判断:当在查看自己个人资料时,显示编辑链接;在查看其他人资料时,不显示编辑链接。
运行程序 flask run
,效果:
点击“Edit your profile”链接后,可看见 用户个人资料 编辑页面:
点击“Submit”按钮后,会提示“Your changes have been saved.
”,点击“Profile”链接后,效果:
CMD下命令行查看 数据库中 user表信息,可看到新插入的字段内容:
(venv) D:microblog>sqlite3 app.db
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from user;
1|susan|susan@example.com|pbkdf2:sha256:50000$OONOkVyy$8d008c6647ab95a5793cf60bf57eaa3bb1123d6e5b3135c5cc5e42e02eddae32||2018-08-11 11:02:54.902074
2|belen|belen@example.com|pbkdf2:sha256:50000$PEDt5NxS$cf6c958c97b6ad28d9495d138cb5a310f6f2389534b0cafa3002dd3cec9af9d1|学习Flask超级教程,Python Web开发学习,坚持!|2018-08-11 11:21:03.778628
目前为止,项目结构
microblog/
app/
templates/
_post.html
base.html
edit_profile.html
index.html
login.html
register.html
user.html
__init__.py
forms.py
models.py
routes.py
migrations/
venv/
app.db
config.py
microblog.py
参考:
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vi-profile-page-and-avatars
以上是关于11.flask博客项目实战六之用户个人资料的主要内容,如果未能解决你的问题,请参考以下文章
慕课网实战Spark Streaming实时流处理项目实战笔记六之铭文升级版
慕课网实战Spark Streaming实时流处理项目实战笔记十六之铭文升级版
Netty实战六之ChannelHandler和ChannelPipeline