第13章:用户评论-Flask Web 开发(第2版)

社交型网站获得成功的关键在于它允许用户交互。本章将实现用户评论功能。本章所介绍的通用性技术足以在各社交应用中使用。

用户评论的数据库表示

评论和博文很相似。都有一个正文(body)、作者(author)和时间戳(timestamp),同时,都支持通过 Markdown 语法来实现富文本的正文。

评论作用于特定的博文,所以从 posts 表中定义了一对多关系。这一关系可用于从博文中轻松获取所有评论。comments(评论)表里还有另外一个指向 users 表的一对多关系。这个关系可用于获取某用户发表的所有评论,间接地将某用户发表的评论条数获取到了,这一信息可以显示在用户资料页面里。 Comment 模型的定义如示例 13-1 所示。

# 示例 13-1. app/models.py: 评论模型
class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    body_html = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    disabled = db.Column(db.Boolean)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
                        'strong']
        target.body_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),
            tags=allowed_tags, strip=True))

db.event.listen(Comment.body, 'set', Comment.on_changed_body)

Comment 模型类里的属性和 Post 模型类很相似。增加的 disabled (是否禁用)属性用于防止不良评论。由于评论内容比博文内容要短,所以在清理 HTML 的时候,保留了更少的标签,比如表示段落的 <p> 标签就被移除了。

为了完成数据库更改,UserPost 两个模型类需要实现与 Comment 的一对多关系。如示例 13-2 所示。

# 示例 13-2. app/models.py: 在 User 和 Post 模型类中定义和 Comment 的一对多关系
class User(db.Model):
    # ...
    comments = db.relationship('Comment', backref='author', lazy='dynamic')

class Post(db.Model):
    # ...
    comments = db.relationship('Comment', backref='post', lazy='dynamic')

评论的提交和显示

在本应用中,评论显示在第11章介绍的博文固定链接页面中。提交评论的表单也将同时将显示在该页面里。示例 13-3 显示了提交评论的表单类。

# 示例 13-3. app/main/forms.py: 提交评论的表单类
class CommentForm(FlaskForm):
    body = StringField('', validators=[DataRequired()])
    submit = SubmitField('提交')

示例13-4显示的是更新后的 /post/<int:id> 路由,它现在支持评论了。

# 示例 13-4. app/main/views.py: 博文支持评论
@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
    post = Post.query.get_or_404(id)
    form = CommentForm()
    if form.validate_on_submit():
        comment = Comment(body=form.body.data,
                          post=post,
                          author=current_user._get_current_object())
        db.session.add(comment)
        db.session.commit()
        flash('评论发布成功.')
        return redirect(url_for('.post', id=post.id, page=-1))
    page = request.args.get('page', 1, type=int)
    if page == -1:
        page = (post.comments.count() - 1) // \
               current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    return render_template('post.html', posts=[post], form=form,
                           comments=comments, pagination=pagination)

注意,url_for() 里的 page 之所以设置为 -1 这个特殊值,是为了让用户提交评论后,可以马上在评论列表中看到此次提交的评论。

评论是通过一个新模板进行渲染的:_comments.html。它和 _posts.html 类似,但使用不同的 CSS 样式。这个模板文件被包含进 _posts.html 模板中,你可以在git仓库看到这些模板的更改。

为了完成这一功能,需要在首页和用户资料页面中显示的博文列表里加上“评论”的链接。

<!--示例 13-5. app/templates/_posts.html: 给博文加上评论链接-->
<a href="{{ url_for('.post', id=post.id) }}#comments">
    <span class="label label-primary">
        {{ post.comments.count() }} 条评论
    </span>
</a>

我们使用了 #comments 锚点来定位博文固定链接页面中,评论的位置。评论的翻页链接也需要加上这个锚点。

如果你从 Github 克隆了示例代码,可以使用 git checkout 13a 签出本示例。签出之后,别忘了运行 flask db upgrade 命令。

审核评论

在第9章,我们定义了一些角色和权限的列表。其中有一个用于审核其他用户发表的评论的 Permission.MODERATE 权限。

审核评论功能将作为导航栏中的链接显示,该链接仅显示给具有 Permission.MODERATE 权限的用户。

<!-- 示例 13-6. app/templates/base.html:  在导航栏显示管理评论链接-->
...
{% if current_user.can(Permission.MODERATE) %}
<li><a href="{{ url_for('main.moderate') }}">审核评论</a></li>
{% endif %}
...

审核评论页面将所有博文的评论显示在一个列表中,最新的评论首先显示。在每个评论下面有一个按钮,可以切换 disabled 属性。

# 示例 13-7. app/main/views.py: 审核评论的路由
@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def moderate():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    return render_template('moderate.html', comments=comments, pagination=pagination, page=page)

审核评论的模板 moderate.html 非常简单,因为它通过 _comments.html 子模板来渲染评论列表。

<!--示例 13-8. app/templates/moderate.html: 审核评论模板-->
{% extends "base.html" %}
{% import "_macros.html" as macros %}

{% block title %}Flasky - 审核评论{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>审核评论</h1>
</div>
{% set moderate = True %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.moderate') }}
</div>
{% endif %}
{% endblock %}

此模板在将 _comments.html 子模板包含进来之前,通过 Jinja2 的 set 指令设置了一个名 moderate 值为 True 的模板变量。这个模板变量用于通知 _comments.html,将评论列表渲染为审核模式。_comments.html 需要根据不同的情况来对评论列表进行渲染:对于普通用户(未设置 moderatemoderate = False)按常规方式渲染;对于审核员(moderate = True)要以审核模式渲染,即为每个评论加上审核按钮。

现在,我们要对 _comments.html 模板进行修改。

<!--示例 13-9. app/templates/_comments.html: 渲染评论正文-->
...
<div class="comment-body">
    {% if comment.disabled %}
    <p><i>该评论已被删除。</i></p>
    {% endif %}
    {% if moderate or not comment.disabled %}
        {% if comment.body_html %}
            {{ comment.body_html | safe }}
        {% else %}
            {{ comment.body }}
        {% endif %}
    {% endif %}
</div>
{% if moderate %}
    <br>
    {% if comment.disabled %}
    <a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable',
        id=comment.id, page=page) }}">通过审核</a>
    {% else %}
    <a class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable',
        id=comment.id, page=page) }}">取消审核</a>
    {% endif %}
{% endif %}
...

审核评论的相关路由和视图函数,如下所示:

# 示例 13-10. app/main/views.py: 审核评论的相关路由和视图函数
@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_enable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = False
    db.session.add(comment)
    return redirect(url_for('.moderate',
                            page=request.args.get('page', 1, type=int)))

@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_disable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = True
    db.session.add(comment)
    return redirect(url_for('.moderate',
                            page=request.args.get('page', 1, type=int)))

如果你从 Github 克隆了示例代码,可以使用 git checkout 13b 签出本示例。

社交功能的相关主题到本章已经全部完成了。在下一章中,您将学习如何将应用程序功能公开为客户端(如智能手机应用程序)可以使用的 API。

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-29 09:48 1188703