美文网首页
06.个人主页和头像

06.个人主页和头像

作者: SingleDiego | 来源:发表于2021-07-08 09:42 被阅读0次

    本章将为应用添加个人主页。个人主页用来展示用户的相关信息,其个人信息可由本用户编辑。我将为你展示如何动态地生成每个用户的主页,并提供一个编辑页面给他们来更新个人信息。






    个人主页

    作为创建个人主页的第一步,先要编写一个与个人主页对应的视图函数。

    # app\routes.py
    
    from flask_login import login_required
    
    # ...
    
    @main_routes.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> 是动态的,Flask 将接受该部分 URL 中的任何文本,并以 username 作为参数名传递给视图函数。

    例如,如果浏览器请求 URL /user/susan,则 user(username) 视图函数将被调用,其参数 username 被设置为 'susan'

    另外这个视图函数只能被已登录的用户访问,所以添加了 @login_required 装饰器。

    个人主页的视图函数当然需要查先找用户实例再返回其个人资料,这里我们使用 username 作为唯一标识符来获得相应的用户对象。这里我们使用 first_or_404() 方法,在没有与 URL 中传来的 username 相匹配的用户实例时,它将自动发送 404 error 给客户端。

    以这种方式执行查询,省去了检查用户是否处在的逻辑,因为当用户名不存在于数据库中时,函数将不会返回,而是会引发 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('main.index') }}">Home</a>
      {% if current_user.is_anonymous %}
        <a href="{{ url_for('main.login') }}">Login</a>
      {% else %}
        <a href="{{ url_for('main.user', username=current_user.username) }}">
          Profile
        </a>
        <a href="{{ url_for('main.logout') }}">Logout</a>
      {% endif %}
    </div>
    

    注意这里生成个人主页的 url_for() 函数,它接受一个动态参数,所以它可以根据当前登录用户 current_user 对象的 username 动态生成个人主页链接。

    现在我们的个人主页页面如下:






    静态文件

    我们的网站并不仅仅有来自数据库中存储的数据记录,它也包含各种静态文件,可以是图片、视频,也可以是日后要用到的 Javascript、CSS 文件等。在这里我们先使用静态文件的方法管理用户头像的图片。

    Flask 会在 static 文件夹里寻找静态文件,现在我们在 app 文件夹内创建名为 static 的文件夹,静态文件相关的内容放置在这里。为了更进一步细化静态文件的分类管理,我们继续创建 upload/avatar 路径。

    文件组织结构如下:

    microblog/
      app/
        static/
            upload/
                avatar/
        templates/
        routes.py
        # ...
      microblog.py
      # ...
    

    我们先在 app\static\upload\avatar 文件夹内放置喜欢的图片作为头像使用,为了方便查找到对应的头像,我们用用户名来为图片重命名。为了符合审美,最好使用正方形的图片且文件后缀名为 .png

    那么怎么访问静态文件呢?假设现在有 diego.png 的图片文件,那么访问它的路径就是 /static/upload/avatar/diego.png

    当然更好的方法是使用 url_for() 方法:

    url_for("static", filename="/upload/avatar/diego.png")
    






    用静态文件构建头像

    为了践行低耦合原则,我们先在 config.py 中增添一些配置:

    # config.py
    
    import os
    basedir = os.path.abspath(os.path.dirname(__file__))
    
    class Config(object):
        # ...
        
        AVATAR_ROOT = os.path.join(basedir, 'app', 'static', 'upload', 'avatar')
        AVATAR_URL = '/upload/avatar/'
        AVATAR_EXTENSION = 'png'
    

    这两条并非 Flask 官方或者任何第三方库所必须的配置项,只是我为了方便个人添加上去的,可以在其他模块引入后作为常量使用。

    AVATAR_ROOT 记录头像文件夹的绝对路径;AVATAR_URL 记录了头像文件夹相对于 static 文件夹的路径;如果以后位置变化了,直接修改配置文件的相应配置即可,不需要在代码里每一处再修改 ;AVATAR_EXTENSION 记录了头像图片文件的后缀名。

    现在在数据库模型的 User 类中添加 avatar() 方法用于生产用户头像的静态文件路径:

    import os
    from flask import url_for
    from config import Config
    
    class User(db.Model, UserMixin):
    
    # ...
    
    def avatar(self):
            root = Config.AVATAR_ROOT
            url = Config.AVATAR_URL
            extension = Config.AVATAR_EXTENSION
    
            filename = "{}/{}.{}".format(url, self.username, extension)
            avatar_path = os.path.join(root, '{}.{}'.format(self.username, extension))
    
            if os.path.exists(avatar_path):
                return url_for("static", filename=filename)
            else:
                filename = "{}{}.{}".format(url, 'default', extension)
                return url_for("static", filename=filenam
    

    现在我们把之前在配置中设置的 AVATAR_ROOTAVATAR_URL 等配置项引入进了作为常量来使用了。本函数我们通过用户名来构建用户对应的头像文件路径,如果对应的文件不存在,则返回一个默认头像。

    下一步我们改写个人主页的 HTML 模板文件,来显示用户的头像:

    # app\templates\user.html
    
    {% extends "base.html" %}
    
    {% block content %}
      <h1>User: {{ user.username }}</h1>
      <img src="{{ user.avatar() }}" weight="80" height="80">
      <hr>
      {% for post in posts %}
      <p>
        {{ post.author.username }} says: <b>{{ post.body }}</b>
      </p>
      {% endfor %}
    {% endblock %}
    

    Jinja2 模板语法里除了可以调用对象的属性外,还能直接调用对象的方法,这里我们直接使用了 user 对象的 avatar() 方法。

    现在我们的个人主页是这个样子:






    使用 Gravatar 构建头像

    上一节我们尝试用本地的静态文件来实现头像系统,这意味着我们需要把服务器空间里相当一部分空间用于存放图片,在本节,我们使用一个第三方服务 Gravatar 来实现头像模块,上一节的代码我们暂且清空。

    Gravatar(https://en.gravatar.com/)是一项全球通用头像服务,它允许我们把头像文件存储到 Gravatar 服务器中并在其他网站或应用里使用,只要提供你与这个头像关联的 email 地址,就能够显示出你的 Gravatar 头像来。

    Gravatar 头像的基础用法是 https://www.gravatar.com/avatar/ + 电子邮箱的 MD5 哈希值。URL 查询字符串 s 定义图片大小,查询字符串 d 定义默认头像。

    以我的头像为例:https://www.gravatar.com/avatar/222db16df13a040d59400787573725bb?s=128&d=robohash

    更多用法,我们参见:Gravatar 文档

    现在在数据库模型的 User 类中编写 avatar() 方法,用来生成 Gravatar 头像 URL。

    # app\models.py
    
    from hashlib import md5
    
    # ...
    
    class User(db.Model, UserMixin):
        # ...
        def avatar(self, size):
            digest = md5(self.email.lower().encode('utf-8')).hexdigest()
            return 'https://www.gravatar.com/avatar/{}?s={}&d=robohash' \
                .format(digest, size)
    

    我们先把 email 转化为小写字母,再把字符串编码为字节后生成 MD5 哈希值,用拼接字符串的方式生成头像的 URL。这里用 size 参数设置头像大小,如果头像不存在则随机生成一个机器人图片来做头像,这是 URL 查询字符串 &d=robohash 定义的。

    下一步把头像图片插入到个人主页的模板中:

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

    使用 User 类来返回头像 URL 的好处是,如果有一天我不想继续使用 Gravatar 头像了,我可以重写 avatar() 方法来返回其他头像服务网站的 URL。

    现在用户个人主页构建完成:






    使用 Jinja2 子模板

    在个人主页,我们使用头像和文字组合的方式来展示用户动态。如果我想在主页也使用一样的风格来布局,简单直接的做法就是把 user.html 的相关代码复制粘贴到 index.html,但如果以后需要修改用户动态的布局,就必须同时修改 user.htmlindex.html 两个模板文件。

    聪明的做法是把可以重用的用户动态部分作为子模板,然后在 user.htmlindex.html 模板中引用它。

    首先,创建这个只有一条用户动态 HTML 元素的子模板。 我将其命名为 app/templates/_post.html_ 前缀只是一个命名约定,可以帮助我标记哪些模板文件是子模板。

    # app/templates/_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 %}
    

    user.html 模板中使用了 Jinja2include 语句来调用该子模板:

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

    应用的主页还没有完善,所以现在我不打算在其中添加这个功能。






    更多有趣的个人资料

    我们继续丰富用户个人主页的内容,我会新增用户的自我介绍并在个人主页。同时也将跟踪每个用户最后一次访问该网站的时间,并显示在他们的个人主页上。

    为了支持所有这些额外的信息,首先需要做的是用两个新的字段扩展数据库中的用户表:

    # 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 ...\migrations\versions\cf1611920c49_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 03f1db8e73ea -> cf1611920c49,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>
      <hr>
      {% for post in posts %}
        {% include '_post.html' %}
      {% endfor %}
    {% endblock %}
    

    新增的两个字段使用了 Jinja2 的 if 语句,设置了字段存在才渲染出来,现在字段为空,所以暂时看不见它。






    记录用户的最后访问时间

    现在我们实现 last_seen 字段。容易想到这样的逻辑:一旦某个用户向服务器发送请求,就将当前时间写入到这个字段。

    为每个视图函数都添加 last_seen 字段的逻辑,显然这不是合理的做法。在视图函数处理请求之前执行一段代码逻辑在 Web 应用中十分常见, 我们利用 Flask 一个内置功能钩子函数来实现它。

    我们在 app\routes.py 添加钩子函数 before_request

    # app\routes.py
    
    from datetime import datetime
    
    @main_routes.before_request
    def before_request():
        if current_user.is_authenticated:
            current_user.last_seen = datetime.utcnow()
            db.session.commit()
    

    使用了 before_request 装饰器之后,每一次请求前 before_request 函数内的代码都会执行。这里会先检查当前是否有登录的用户,如果当前为已登录用户,则更新它的 last_seen 字段为当前 UTC 时间。

    现在可以在个人主页看见最后访问时间了,它可能和你所在时区的实际时间有所不同,我们可以很容易地用 Python 做时区转换,让在不同地区的用户以当地时间格式来显示。现在我们暂且不实现这个功能。






    个人资料编辑器

    这一节的操作和前面很类似,需要给用户一个表单,让他们通过表单更改个人介绍或用户名。并存储在新的 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
    
    @main_routes.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('main.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 请求,但该数据中的某些内容无效。这个时候我们只要留在原地不动,让验证器的提示信息出现在网页上即可。

    最后将个人资料编辑页面的链接添加到个人主页,以便用户使用:

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

    请注意这里使用的 if 语句,它确保编辑个人资料的链接只在浏览自己主页时候才出现。






    优化应用结构

    回顾我们现在编写的网站应用,现在已经实现了比较多的功能,随着开发的深入,其结构也必将变得臃肿,现在有必要对其进行优化。

    我们现在可以按照功能把应用拆成两个子模块:一部分称之为 main,也就是应用的主要功能模块,我会把跟用户动态相关的功能逻辑放在这个模块里,如当前的 index 主页和以后实现的用户发布动态的功能。

    另一个部分叫 auth,我会把跟用户相关的功能,比如用户注册、用户登录、用户个人主页等功能逻辑放到该模块。

    两个不同子模块都会放在 app 路径下,包装成 Python 的包,我会分别用不同的蓝图来对两个子模块进行组织。

    拆分后文档结构如下:

    app/
      auth/ # 与用户相关的逻辑
        __init__.py
        forms.py
        routes.py
      main/ # 与动态相关的逻辑
        __init__.py
        routes.py
      templates/
        auth\ # 与用户相关的模板文件
        base.html
        # ...
      __init__.py
      models.py
    migrations/
    venv/
    app.db
    config.py
    microblog.py  
    






    Blueprints

    在 Flask 中,blueprint 是代表应用子集的逻辑结构。blueprint 可以包括路由,视图函数,表单,模板和静态文件等元素。如果在单独的 Python 包中编写 blueprint,那么你将拥有一个封装了应用特定功能的组件。Blueprint 的内容最初处于休眠状态。为了关联这些元素,blueprint 需要在应用中注册。

    我们会现在子模块的 __init__.py 中定义该模块的 blueprint__init__.py 能让一个文件夹变成一个 Python 的包,从而用 import 语句调用。比如在 app\main\__init__.py 内定义的代码,我们用 from app.main import xxx 就可引入。

    现在按照这个原则来拆分改写我们的应用。

    编写 app\main\__init__.py

    # app\main\__init__.py
    
    from flask import Blueprint
    main_routes = Blueprint('main', __name__)
    

    这里我们在定义一个 blueprint 名为 main_routesmain 子模块下的路由都注册在该蓝图下。

    编写 app\main\routes.py

    # app\main\routes.py
    
    from datetime import datetime
    from flask import (
        render_template, 
    )
    from flask_login import current_user
    from app import db
    from app.models import User, Post
    from app.main import main_routes
    
    
    @main_routes.before_request
    def before_request():
        if current_user.is_authenticated:
            current_user.last_seen = datetime.utcnow()
            db.session.commit()
    
    @main_routes.route('/')
    @main_routes.route('/index')
    def index():
        posts = Post.query.all()
        return render_template(
            'index.html', 
            title='Home Page', 
            posts=posts
        )
    

    我们只是把跟用户动态相关的视图函数移动到这个文件下,然后把刚才创建的 main_routes 引入,再把视图函数注册到该蓝图之下。

    按照同样的逻辑,我们完成 auth 子模块的拆分。

    # app\auth\__init__.py
    
    from flask import Blueprint
    auth_routes = Blueprint('auth', __name__)
    

    app\auth\routes.py 里的逻辑参照 main 的部分,把视图函数注册到 auth_routes 中,由于代码过于冗长,就不一一列出,参见源码即可。forms 里的表单类由于全部都是与用户相关的,不做拆分直接移动到 app\auth\forms.py

    接下来我们也按照功能分类的原则,对 templates 中的模板文件进行分类。新建一个 app\templates\auth 文件夹,edit_profile.htmllogin.htmlregister.htmluser.html 这些和用户功能相关的模板文件放置其中。其它的模板文件仍然保留在 templates 目录。

    修改为成后使用 render_template 调用模板文件时候就要加上其相对路径,如:'auth/login.html'

    下一个任务就是改写 HTML 模板文件的 url_for(),由于我们把用户相关的视图函数注册到 auth_routes 并命名为 'auth';我们需要把原来 url_for('main.login') 改为 url_for('auth.login');同样地 logoutregister 等路由也同样处理。

    最后一步,在工厂函数引入新的蓝图并注册到 app 中:

    # app\__init__.py
    
    from flask import Flask
    from config import Config
    
    from flask_sqlalchemy import SQLAlchemy
    from flask_migrate import Migrate
    from flask_login import LoginManager
    
    db = SQLAlchemy()
    migrate = Migrate()
    login = LoginManager()
    
    def create_app():
        app = Flask(__name__)
    
        # 加载配置
        app.config.from_object(Config)
        
        # 初始化各种扩展库
        db.init_app(app)
        migrate.init_app(app, db)
        login.init_app(app)
    
        # 引入蓝图并注册
        from app.main.routes import main_routes
        app.register_blueprint(main_routes)
    
        from app.auth.routes import auth_routes
        app.register_blueprint(auth_routes)
    
        return app
    
    from app import models
    

    现在应用结构的修改已经完成,接下来我会按照这个框架继续为网站增添新功能,如果按照功能对其拆分,如果有比较独立的新功能需要开发,还将增添新的子模块。






    本章源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-06

    相关文章

      网友评论

          本文标题:06.个人主页和头像

          本文链接:https://www.haomeiwen.com/subject/qzdiultx.html