个人中心

在个人中心模块,可以修改用户头像、个性签名等。首先在 blueprints/user.py 中添加个人中心的视图函数,代码如下。

@bp.get("/profile/<string:user_id>")
def profile(user_id):
    user = UserModel.query.get(user_id)
    return render_template("front/profile.html", user=user)

上述代码中,把 user_id 放到 URL 中当作参数传过来。访问当前用户的个人中心和其他用户的个人中心时都需要传递这个参数。下面添加限制,当访问当前用户的个人中心时可以进行编辑,访问其他用户的个人中心则不能编辑,代码如下。

...
@bp.get("/profile/<string:user_id>")
def profile(user_id):
    user = UserModel.query.get(user_id)
    is_mine = False
    if hasattr(g, "user") and g.user.id == user_id:
        is_mine = True
    context = {
        "user": user,
        "is_mine": is_mine
    }
    return render_template("front/profile.html", **context)
...

上述代码中,为了在模板中能区分当前是否是自己的个人中心,我们通过逻辑判断后把结果赋值给 is_mine,然后再把 is_mine 传到模板中。

使用Flask-Avatars生成随机头像

用户没有上传头像时,如果显示空白会影响用户体验。可以使用 Flask-Avatars 插件来生成随机的头像。使用 Flask-Avatars 插件非常简单,先在 exts.py 中创建 avatars 对象,代码如下。

...
from flask_avatars import Avatars
...
avatars = Avatars()

在 app.py 中使用 app 对象进行初始化,代码如下。

...
from exts import avatars
...
avatars.init_app(app)
...

现在在模板中就可以使用 avatars 变量来随机生成头像了。Flask-Avatars 提供了 5 种随机或默认头像的方案,下面分别进行讲解。

Gravatar

Gravatar(globally recognized avatar)是一种全球通用的头像服务。用户只需在 Gravatar 官网( https://cn.gravatar.com/ )上使用邮箱注册一个账号,并且上传头像,那么在任何支持 Gravatar 的网站上使用相同的邮箱,都可以使用这个头像,当然如果用户没有上传过头像,则会生成一个随机的头像。在模板中使用 avatars.gravatar 函数,代码如下。

<img src="{{ avatars.gravatar(email_hash) }}">

其中 email_hash 是 email 的哈希值,可以通过以下代码获取邮箱哈希值。

import hashlib

def email_hash(email):
    return hashlib.md5(email.lower().encode("utf-8")).hexdigest()

Gravatar 生成的头像效果图,如图 9-34 所示。

Robohash

Robohash(官网 https://robohash.org/ )是专门提供机器人随机头像的服务,通过 avatars.robohash(text) 获取头像,根据 text 随机生成头像,代码如下。

<img src="{{ avatars.robohash('jack') }}">

Robohash 生成的头像效果图,如图 9-35 所示。

image 2025 01 22 16 26 53 166
Figure 1. 图9-34 Gravatar头像效果图
image 2025 01 22 16 27 12 017
Figure 2. 图9-35 Robohash头像效果图

社交网站头像

针对全球比较流行的社交平台 Twitter、Facebook、Instagram,可以使用 avatars.social_media 获取指定平台用户的头像。如使用 Twitter 上某人的头像,那么可以使用以下代码实现。

<img src="{{ avatars.social_media('Twitter上某人的头像') }}">

默认头像

Flask-Avatars 提供了一个默认的头像,可以通过设置 size 值为 s、m、l 来显示不同尺寸的头像,实现代码如下,效果如图 9-36 所示。

<img src="{{ avatars.default(size='l') }}">

Identicon哈希头像

Identicon 是一种基于用户信息的哈希值生成图像的技术,如根据 IP 地址、邮箱等,其雏形是 9 个方格的图案,现在已经发展到许多其他类型的生成模式了。Identicon 生成的头像效果如图 9-37 所示。

image 2025 01 22 16 29 33 634
Figure 3. 图9-36 默认头像效果图
image 2025 01 22 16 29 50 162
Figure 4. 图9-37 Identicon头像效果图

Identicon 会将生成的图片保存到指定位置。所以我们首先要在 config.py 中添加参数 AVATARS_SAVE_PATH,如这里在 DevelopmentConfig 中添加如下代码。

AVATARS_SAVE_PATH = os.path.join(BaseConfig.UPLOAD_IMAGE_PATH,"avatars")

上述代码中,我们将头像的存储路径设置在 BaseConfig.UPLOAD_IMAGE_PATH 的 avatars 文件夹下,然后在用户模型中实现一个生成头像的方法,示例代码如下。

class User(db.Model):
    avatar_s = db.Column(db.String(64))
    avatar_m = db.Column(db.String(64))
    avatar_l = db.Column(db.String(64))

    def __init__(self):
        self.generate_avatar()

    def generate_avatar(self):
        avatar = Identicon()
        filenames = avatar.generate(text=self.email)
        self.avatar_s = filenames[0]
        self.avatar_m = filenames[1]
        self.avatar_l = filenames[2]
        db.session.commit()

以后在创建用户对象时,会自动调用 generate_avatar 方法,并且将生成的头像分别存储到 avatar_s、avatar_m 和 avatar_l 上。

现在采取上述第一种方案 Gravatar 来生成随机头像,avatars.gravatar 需要使用邮箱的哈希值作为参数,需要在 pythonbbs 项目根路径下创建一个 filters.py 文件,用来存放过滤器。下面先创建一个 email_hash 装饰器函数,代码如下。

import hashlib

def email_hash(email):
    return hashlib.md5(email.lower().encode("utf-8")).hexdigest()

下面在 app.py 中导入 filters,通过 app.template_filter 函数添加过滤器,代码如下。

# 添加模板过滤器
app.template_filter("email_hash")(filters.email_hash)

接着在 templates/front/profile.html 中,将头像部分的代码替换为如下代码。

<tr>
  <th>头像:</th>
  <td>
    <img class="avatar" src="{{ avatars.default(size='l') }}" alt="">
  </td>
</tr>

现在重新访问个人中心页面,即可以生成随机头像了。

修改导航条上的登录状态

现在再修改一下导航条右侧展示数据的逻辑。即在没有登录的情况下显示登录、注册链接;在登录的情况下显示用户名和退出登录链接;单击用户名链接时可以跳转到个人中心页面。下面在 templates/front/base.html 中,将导航条右侧部分的代码修改如下。

...
<ul class="navbar-nav ml-4">
  {% if g.user %}
    <li class="nav-item">
      <a href="{{ url_for('user.profile', user_id=g.user.id) }}" class="nav-link">{{ g.user.username }}</a>
    </li>
    <li class="nav-item">
      <a href="{{ url_for('user.logout') }}" class="nav-link">退出登录</a>
    </li>
  {% else %}
    <li class="nav-item">
      <a class="nav-link" href="{{ url_for('user.login') }}">登录</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="{{ url_for('user.register') }}">注册</a>
    </li>
  {% endif %}
</ul>
...

上述代码中,我们通过判断对象 g 是否有 user 属性,如果有,说明已经登录,则显示用户名和退出登录链接,如果没有,则显示登录和注册链接。上面我们还添加了退出登录的链接,接下来再来到 blueprints/user.py 中,添加 logout 视图函数代码。

...
@bp.get('/logout')
def logout():
    session.clear()
    return redirect("/")
...

通过在 session 中保存 user_id 作为登录的标志,所以在上述代码中,清理 session 中的数据即可完成退出登录,在退出登录后会跳转到首页。

根据用户显示个人中心

由于本人的个人中心和别人的个人中心用的是同一个 URL 和模板,因此需要在模板中进行区分。访问本人的个人中心时,数据可以编辑;访问别人的个人中心时,则仅展示数据。图 9-38 所示为本人的个人中心效果图,图 9-39 所示为别人的个人中心效果图。

image 2025 01 22 16 38 23 620
Figure 5. 图9-38 本人的个人中心
image 2025 01 22 16 38 41 985
Figure 6. 图9-39 别人的个人中心

图9-38 和图9-39 的区别在于,图9-38 能对数据进行编辑,图9-39 只能访问不能编辑。两个效果图的 templates/front/profile.html 模板代码如下。

image 2025 01 22 16 39 23 112

上述代码中,在渲染用户个人数据时,通过判断 is_mine 是否为 True,用来决定是否需要渲染输入框。为了在 is_mine 为 True 的情况下,用户修改后的数据能进行保存,在 table 标签外套了一层 form 标签,并且因为涉及图片上传,所以又在 form 标签上添加了属性 enctype="multipart/form-data"。由于现在还没有写好修改用户信息的 URL 和视图,因此 form 标签上的 action 暂时为空。另外,还有一些小细节,就是在使用 avatars.gravatar 渲染随机头像时,默认会使用 Gravatar 官网的服务器加载图片,因为 Gravatar 服务器在国外,可能导致加载太慢或者加载失败,所以将 https://gravatar.com/avatar/ 替换为国内的镜像,如使用 https://gravatar.loli.net/avatar/

修改用户信息

在访问用户本人的个人中心,并且对数据进行修改后,单击 “保存” 按钮,把数据发送到视图函数进行修改。修改用户信息时,可以修改用户名、头像、签名,因此先在 forms/user.py 中添加一个 EditProfileForm 类,代码如下。

from wtforms import FileField
from flask_wtf.file import FileAllowed
...
class EditProfileForm(BaseForm):
    username = StringField(validators=[Length(min=2, max=20, message="请输入正确格式的用户名!")])
    avatar = FileField(validators=[FileAllowed(['jpg', 'jpeg', 'png'], message="文件类型错误!")])
    signature = StringField()

    def validate_signature(self, field):
        signature = field.data
        if signature and len(signature) > 100:
            raise ValidationError(message="签名不能超过100个字符")
...

上述代码中添加了 3 个字段,分别为 username、avatar 和 signature。其中 avatar 是文件类型,所以我们用 FileFiled 类型,为了限制文件后缀名,添加了 FileAllowed 验证器,指定只能上传 jpg、jpeg 以及 png 格式的文件。接着再把 EditProfileForm 类导入到 blueprints/user.py,然后再实现一个视图函数 edit_profile,代码如下。

image 2025 01 22 16 42 41 172

在 Flask 项目中,通过 request.files 可以获取前端上传的文件,通过 request.form 可以获取普通表单数据。为了对文件和普通表单数据都能做验证,使用 werkzeug.datastructures.CombinedMultiDict 方法将 request.files 和 request.form 合并成一个结构,再传给 EditProfileForm 进行验证。在上传了头像的情况下,把头像保存到 AVATARS_SAVE_PATH 参数配置项指定的路径下,然后使用 media.media_file 反转的 URL,赋值给当前用户的 avatar 属性,并且在后面分别指定用户名和签名,即可完成用户信息的修改。

在个人中心模板中,再将头像渲染逻辑修改一下,首先判断 user.avatar 是否有值,如果有则渲染 user.avatar,否则就渲染 gravatar 头像,代码修改后如下所示。

...
<th>头像:</th>
<td>
    {% if user.avatar %}
        <img src="{{ user.avatar }}" alt="">
    {% else %}
        <img class="avatar" src="{{ avatars.gravatar(user.email|email_hash) }}" alt="">
    {% endif %}
    {% if is_mine %}
        <input type="file" name="avatar" accept="image/jpeg, image/png" value="上传头像">
    {% endif %}
</td>
...

现在网站只有访问用户自己个人中心的入口,我们在帖子详情页中给帖子作者添加作者个人中心的入口,在 templates/front/post_detail.html 中,将显示作者部分的代码修改如下。

<span>作者:<a href="{{ url_for('user.profile', user_id=post.author.id) }}">{{ post.author.username }}</a></span>
...

至此,实现了个人中心的所有功能。