Flask Mega-Tutorial 中文教程 V2.0 第6章:配置文件页面和头像

最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。

这是Flask Mega-Tutorial系列的第六章,其中我将告诉您如何创建用户个人资料页面。

供您参考,以下是本系列文章的列表。

本章将专门用于向网站添加用户个人配置页面。用户配置页面是显示关于用户信息的页面,通常由用户自己输入信息。我将向您展示如何动态生成所有用户的配置文件页面,然后我将添加一个小型配置文件编辑器,用户可以使用它来输入他们的信息。

本章的GitHub链接是:BrowseZipDiff

 


 

用户资料页面

要创建用户配置文件页面,我们首先编写一个映射到/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为此网址返回的内容:

ch06-gravatar

默认情况下,返回的图像大小为80x80像素,但可以通过向URL的查询字符串添加参数s来请求不同的大小。例如,要获取我自己的头像作为128x128像素图像,URL为https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128

可以作为查询字符串参数传递给Gravatar的另一个有趣的参数是d,它确定Gravatar为没有在服务中注册的头像的用户提供的图像。我最喜欢的是“identicon”,它为每个Email地址返回一个不同的几何设计。例如:

ch06-gravatar-identicon

请注意,某些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 %}

ch06-avatars

使用Jinja2子模板

我设计了用户个人资料页面,以便显示用户写的帖子及其头像。现在我希望首页页面也显示具有类似布局的帖子。我可以复制/粘贴处理帖子渲染的模板部分,但这实际上并不理想,因为以后如果我决定对这个布局进行更改,我将不得不记得更新这两个模板。

相反,我将创建一个帖子的子模板渲染它,然后我从user.htmlindex.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应用程序中的日期和时间。

ch06-last-seen

资料编辑

我还需要向用户提供一个表单,他们可以在表单中输入一些关于自己的信息。表单允许用户更改其用户名,并编写有关他们自己的个人简介,并将其存储在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则提交验证失败。

ch06-edit-profile

为方便用户访问个人资料编辑器页面,我可以在他们的个人资料页面中添加一个链接:

app/templates/user.html: Edit profile link

                {% if user == current_user %}
                <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
                {% endif %}

注意我正在使用的条件判断,以确保在您查看自己的个人资料时显示编辑链接,但在其他人查看时个人资料时则不会显示编辑。

ch06-user-profile-link


原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vi-profile-page-and-avatars

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值