The Flask Mega-Tutorial 之 Chapter 6: Profile Page and Avatars

Objective

  • Add user profile pages to the application (generate profile pages for all users dynamically).
  • Add a small profile editor for users to enter information.

User Profile Page

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

(1) < > 用来标明 dynamic component of URL.
(2) 只对 logged 用户可见,故 @login_required
(3) first_or_404() ,如无结果,则返回 404 error

  • app / templates / user.html: User profile 模板
{% 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 %}

创建链接到 User Profile 的入口,如下:

  • app / templates / base.html: 在 base.html 中创建 User profile 的链接入口
<body>
    <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>
</body>

(1) Profile 入口只对 logged 用户可见。
(2) 通过 url_for(‘user’, username=current_user.username) 定位至 ‘user’ (view func),并传入dynamic component/user/<username> 中的 username.

如果自己已登录,则点击顶部的Profile,会跳转出现如下类似的页面;
尚无链接可至其他 user’s profile, 但可手动在 地址栏输入,如http://localhost:5000/user/john (前提是 john 已注册,可从db 提取到)。

这里写图片描述


Avatars

  • 为使 User Profile 配头像,采用 Gravatar 服务(服务器无需管理上传的图片)。
  • Gravatar URL 格式 https://www.gravatar.com/avatar/<hash> ,其中 <hash> 是 MD5 hash of user’s email。
  • Gravatar URL 生成方式如下:
>>> from hashlib import md5
>>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest()
'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
  • 配置 https://www.gravatar.com/avatar/<hash>?d=identicon&s=<size>'

d = identicon: generate an “identicon” image for users that have not registered avatar.
s = <size> : default 80x80 pixels, we can set 128 for profile with a 128x128 pixels avatar.

some web browser extensions such as Ghostery block Gravatar images.


1、app / models.py: 为 User Model 添加 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)

(1) Gravatar 要求 email 是小写,故先统一转为小写。
(2) Python 中的 MD5 支持 bytes操作(不支持 string),故在传入解析前先进行编码(‘utf-8’)。

: 在 User Model 中定义 avatar() 的好处是,一旦之后想改变(如不用 Gravatar avatars),可以重新改写 avatar() 以返回不同的 URLs。


2、app / templates / user.html: 将 avatar 添加至 user profile 模板

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

(1) 加了一个<table> 标签,其中 <tr> 为 table row, <td> 为 table data; <tr> 中的 valign= “top”,指垂直靠上。
(2) 插入一个table,包含一行,行中包括两个元素:

  • 第一个元素为 avatar,调用 User Model 中定义的 avatar(),并传入 size 参数128;
  • 第二个元素为 用户名(此user.username 已经过路由限定 为 logged)。

3、app / templates / user.html: 将缩小版的 avatar 添加到 Profile 中下方出现的 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 %}

(1) 插入第2个 <table> 标签,插入位置为 {% for post in posts %} <table></table> {% endfor %} 之间。
(2) 依然一个<tr valign="top"></tr> 标签,包括两个元素:

  • 第一个元素:post.author.avatar(36) 对应每个 post 的小头像。
  • 第二个元素:原来<p></P> 中的内容,{{ post.author.username }} says: <br> {{ post.body }},注意加重标签<b></b> 改为了换行标签 <br>

    这里写图片描述


Using Jinja2 Sub-Templates

缘起:User Profile 下方展示了对应 user 发表的 posts,若别的页面模板也有类似的 posts 展示需求,则虽然可以通过简单的copy/paste 完成,但如果之后发生变动,则需要修改多处模板。

1、 app / templates / _post.html: 创建 展示 posts 的sub_template,即 _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 中的 posts 部分单独创建为 _post.html,然后供别的需求模板调用 (include)。

2、app / templates / user.html: 在原 user profile 中 引入 _post.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 %}

注意引用语法: {% include "_post.html" %}


More Interesting Profiles

  • 可自我编辑 (about_me)
  • 可追踪最近登录时间 (last_seen)

1、这类显示信息,需要与 db 发生交互,且属于 User 的属性,所以首先更新 User Model:

app / models.py: 将这两类属性添加至 user model

class User(UserMixin, db.Model):
    # ...
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)
  • 限定 about_me 的大小为 140 字符(非字节)
  • last_seen,默认 datetime.utcnow,与时区无关(暂时不管具体时区的转换及datetime格式)

2、 更新了 User Model,则须 flask db migrate & flask db upgrade

(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
(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

注: migration framework 非常有用(useful)。 db 中原有的数据依然存在,只是通过 flask db upgradeflask db migrate 产生的 migration script 在不损坏任何数据的情况下更改 db 的结构。

I hope you realize how useful it is to work with a migration framework. Any users that were in the database are still there, the migration framework surgically applies the changes in the migration script without destroying any data.


3、将这两个新字段,添加到 User Profile (user.html )中

app / templates / user.html: 将 about_me & last_seen 添加到 user profile 模板

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

注: wrapping these two fields in Jinja2’s conditionals, because we want them to be visible if they are set.


Recording The Last Visit Time For a User

目标: 对于一个给定的 user,在其向 server 发起请求时,无论何时/无论何种路由,都希望能将current time 写入到此 user 的 last_seen 字段中。

@app.before_request: 可使被装饰的函数,在执行任何路由函数之前先执行。

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

无需 db.session.add(user)。
判断 current_user 时,已经通过 Flask-Login 的 user loader (models.py 中)将 db 中的目标 user 载入 db session,所以后面在更新此 user 的 last_seen 后,才用db.session.commit() 提交。

If you are wondering why there is no db.session.add() before the commit, consider that when you reference current_user, Flask-Login will invoke the user loader callback function, which will run a database query that will put the target user in the database session. So you can add the user again in this function, but it is not necessary because it is already there.

这里写图片描述

  • Storing timestamps in the UTC timezone makes the time displayed on the profile page also be in UTC.
  • Further, time format is not ideal, since it is the internal representation of Python datetime object.

这两个问题,稍后的章节会处理。


Profile Editor

1、创建 EditProfileForm

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')
  • wtforms引入 TextAreaField,自 wtforms.validators 引入 Length
  • about_me 非必须,所以未设置 DataRequired()
  • Length 的限制为 140 字符,和 User Model 中设置的字段大小保持一致 (about_me = db.Column(db.String(140))

2、创建 自我编辑 的模板

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


3、创建 自我编辑 的路由

app / routes.py: 自我编辑的路由 (view func for edit profile)

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)

注:设置了 @login_required(登录方可自我编辑)

(1) form.validate_on_submit() 判断为 ‘True’ 时,依据成功提交的 form 中的信息改写 current_userusernameabout_me 字段值并 commit 到 db,然后 flash 提示信息,最终重定向至‘edit_profile’。

(2) form.validate_on_submit() 判断为 ‘False’ 时,有两种情况:

  • 第一种情况:浏览器发送的是初次 ‘GET‘ 请求(通过 request.method 判断),我们应在返回样表(edit_profile.html)的基础上,先从 db中 调用 current_user 中的字段数据,并赋值 form.username.dataform.about_me.data, 最终返回 pre-populatededit_profile.html.
  • 第二种情况:浏览器发送的是失败的 ‘POST‘ 请求(a submission that failed validation),返回空白edit_profile.html

这里写图片描述

4、为使 user 便于看到 Edit Profile 入口,则在 Profile 页面添加链接入口:

app / templates / user.html: 添加 edit profile 链接入口

{% 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 %}
                {% if user == current_user %}
                <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
                {% endif %}
            </td>
        </tr>
    </table>
    <hr>
    {% for post in posts %}
        {% include '_post.html' %}
    {% endfor %}
{% endblock %}

加入第3条判断语句: 保证只有在浏览自己的 profile 时,才会出现 Edit link ; 浏览其他人的 profile 时, 链接入口不可见。

这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值