Flask学习笔记:资料页面和Avatars

1. 做好准备工作
  • 进入项目主目录
  • 激活虚拟环境

2. 用户资料页面

首先创建一个映射到 /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中的文本作为参数调用视图函数。 比如说,如果浏览器请求 /user/susan 这个URL,视图函数会将参数 username 设置为 'susan'。 这个视图函数只接受已登录用户的访问,所以我用 @login_required 装饰器装饰了这个视图函数。

这个视图函数的实现相当简单。首先我尝试通过从数据库查询用户名来加载用户。之前说过,要获取全部查询结果,就用all(),如果只想要第一个结果,就用first(),如果没有查询到,就返回None。在这个视图函数中我使用的是first()的变体——first_or_404(),当能查询到结果时它的工作原理和 first() 差不多,但当没有查询到结果时,它会自动将404错误发回到客户端。用这种方式执行查询可以免去检查查询会不会返回用户,因为如果数据库里没有某个用户名,函数会引发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>

由于用户资料视图函数接受动态参数, url_for()函数以关键字参数的形式接收它的值。由于这个链接指向已登录用户自己的资料页面,所以我可以用 current_user来生成URL。


3. Avatars

处理图像时间很麻烦的事,所以头像部分我们使用 Gravatar 服务。

Gravatar用起来很简单。以 https://www.gravatar.com/avatar/<hash> 格式的URL就能获取到给定用户的图片。<hash> 是用户 email 地址的 MD5 哈希值,下面这个例子演示如何获取 email 地址为 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'

图片的默认尺寸是 80x80 像素,但也可以在URL查询字符串加入 s 参数获取其他尺寸的图片。比如,要获取 128x128 像素的图片,URL就是 https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128 。

另外一个可以传递给 Gravatar 的查询字符串参数是 d ,这是为没有注册 gravatar 的用户提供的默认图像。

注意一些web浏览器屏蔽Gravatar图像,比如 Ghostery。

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() 方法返回用户的avatar图像的URL,并缩放到指定的尺寸。为没有注册avatar的用户生成“identicon”图像。然后,因为Python中的 MD5 支持 bytes 而不是字符串, 所以我在邮箱参数传给hash函数前把字符串编码成字节。

下一步是把 avatar 图像插入到用户资料模板中:

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 %}

用用户类来返回 avatar 图像的好处是如果某天我决定修改 Gravatar avatars了,我可以直接重写 avatar() 方法,然后返回别的URL就行。


4. 使用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>

用 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 %}


5. 更有意思的资料页面

首先,修改用户模型,添加两个新字段。

app/models.py

class User(UserMixin, db.Model):
    # ...
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)

生成新的迁移脚本:

(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/username/projectname/migrations/versions/37f06a334dbf_new_fields_in_user_model.py ... done

然后向数据库应用更改:

(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

{% 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 %}

由于现在这两个字段还没有值,所以现在还看不到。


6. 记录用户最后一次访问的时间

要实现这个功能,要做的就是每当用户向服务器发送请求时,向该用户的此字段写入当前时间。

直接向每个可能被访问的视图函数添加这个功能很麻烦,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()

被@before_request 装饰器装饰的函数会在视图函数请求之前执行,也就是说我现在可以在任何视图函数被请求之前插入代码了。实现很简单,只需要检查 current_user 有没有登录,然后将 last_seen 字段设置为当前时间。就像之前提到的,要想应用程序的时间一致,那就要使用UTC时区,所以用用户系统的本地时间不是个好主意,因为数据库中存的数据会取决于你的位置。最后一步是提交数据库事务,这样上面的这些更改才能写入数据库,考虑到在引用 current_user 时, Flask-Login会调用用户加载回调函数,运行数据库查询并将目标用户添加到数据库会话中,所以没有必要再重复在函数里加上 db.session.add() 了。


7. 资料编辑器

现在开始编写资料编辑表单类:

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')

资料编辑器的模板如下:

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)

如果validate_on_submit() 返回 True,我就将表单中的数据写入用户的数据库。如果返回False,原因可能有两个,第一个,可能是因为浏览器发送的是GET请求,应用会响应原先的表单数据,也可能是因为浏览器发送的是 POST 请求,但是某些数据没有通过验证。当表单第一次以GET来请求表单的时候,我会用数据库中原先储存着的数据来预先填充这些字段。但是如果验证错误了,我不希望这些向表单字段写入任何数据。可以提供检查 request.method 来区分这两种情况, GET 代表第一次请求, POST 代表提交数据。

现在在用户页面添加修改资料的链接:

app/templates/user.html

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值