使用Flask搭建一个校园论坛7-帖子详情

71 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

1.功能简介

不管什么论坛,当用户发布了帖子之后,最原始的目的就是让更多的用户来看到帖子,同时参与到该帖子的讨论当中去。到目前为止我们还没有实现用户阅读帖子的页面,本节主要实现这个功能。

2.阅读帖子

在首页中我们显示了三个tab的帖子列表,用户点击对应的帖子标题超链接就进入对应的详情页面,但是在上一节之前还没有实现阅读帖子的视图函数,所以上一节中的点击帖子标题跳转到帖子详情页面的功能还不能使用,这里开始实现此功能。打开bbs/blueprint/frontend/post.py 模块,添加下面的代码

@post_bp.route('/read/<post_id>/', methods=['GET'])
def read(post_id):
    post = Post.query.get_or_404(post_id)
    if current_user.is_authenticated:
        c_tag = Collect.query.filter(Collect.user_id == current_user.id, Collect.post_id == post_id).first()
    else:
        c_tag = None
    post.read_times += 1
    db.session.commit()
    return render_template('frontend/post/read-post.html', post=post, c_tag=c_tag)

上面代码十分简单,就是通过请求中的参数从数据库中获取帖子的相关内容,然后渲染到页面中,但此时还没有对应的页面,因此需要创建模板文件,打开bbs/templates/frontend/post/,新建模板文件read-post.html,并添加下面的代码

{% extends "frontend/base.html" %}
{% block head %}
    {{ super() }}
    {{ ckeditor.load_code_theme() }}
    <!--suppress ALL -->
    <style>
        .post-title-h1{
            font-size: 22px;
            font-weight: bold;
        }

        .post-div{
            color: white;
            background: #303030;
            padding: 5px 15px 8px 15px;
            border-radius: 5px;
        }

        .post-content{
            color: white;
            background: #343434;
            padding: 8px;
            border-radius: 5px;
        }

        article>h1 {
            font-size: 20px;
            font-weight: bold;
            margin: 10px 0 10px 0;
            padding: 0 10px;
            border-left: 5px solid #20c997;
            line-height: 2em;
        }

        article>h2 {
            font-size: 18px;
            margin-top: 5px;
            margin-bottom: 5px;
            padding: 5px 5px 5px 5px;
            border-bottom: 1px solid #28a745;
        }

        article>h3 {
            font-size: 18px;
            margin-top: 10px;
            margin-bottom: 10px;
            padding: 5px 5px 5px 5px;
            border-bottom: 1px solid #28a745;
        }

        blockquote>p {
            font: 14px/22px normal helvetica, sans-serif;
            margin: 5px 0px 5px 0px;
            font-style:italic;
        }

        .report-textarea{
            height: 100px!important;
        }

        .preview{
            background: white;
            border-radius: 5px;
            color: black!important;
        }

        .blockquote-comment{
            margin-top: 5px;
            border-left: 6px solid #6c6c6c;
            background: #6c757d;
            color: white;
            padding: 8px;
        }

        .p-error-hint{
            color: #f94b43;
            display: none;
            font-weight: bold;
        }

        .p-reply{
            color: #80bdff;
            margin-bottom: 0px;
        }

        .div-comment-body{
            border-bottom: 1px solid #828286;
            padding-bottom: 6px;
        }

        .div-gutter20{
            height: 20px;
        }

        @media screen and (max-width: 567px){
            .div-gutter20{
                height: 5px;
            }
        }

        .hr-margin-5{
            margin: 5px 0 5px 0!important;
        }
    </style>
{% endblock %}
{% block title %}
    {{ post.title }}
{% endblock %}

{% block content %}
    {{ moment.locale(auto_detect=True) }}
    <body>
    <main>
        <div class="container mt-2">
            {% include "_flash.html" %}
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a class="text-decoration-none" href="/">主页</a></li>
                <li class="breadcrumb-item"><a class="text-decoration-none" href="#">杂谈</a></li>
                <li class="breadcrumb-item active">帖子标题</li>
            </ol>
            <!-- 帖子主要信息 -->
            <div class="post-div">
                <h1 class="post-title-h1" id="postTitle" data-id="{{ post.id }}">{{ post.title }}</h1>
                <div class="d-flex mb-0">
                    <p class="text-muted mr-2 mb-0"><i class="fa fa-thumbs-o-up mr-1"></i><span class="d-none d-lg-inline-block">点亮</span>({{ post.likes }}) </p>
                    <p class="text-muted mr-2 mb-0"><i class="fa fa-thumbs-o-down mr-1"></i><span class="d-none d-lg-inline-block">点灭</span>({{ -post.unlikes }}) </p>
                    <p class="text-muted mr-2 mb-0"><i class="fa fa-heart mr-1"></i><span class="d-none d-lg-inline-block">收藏</span>({{ post.collects }}) </p>
                    <p class="text-muted mb-0"><i class="fa fa-calendar-minus-o mr-1"></i>{{ post.create_time }}</p>
                </div>
                <div class="d-flex">
                    {% if post.is_anonymous == 1 %}
                        <img src="{{ post.user.avatar }}" class="rounded avatar-50">
                        <div class="d-flex">
                            <div>
                                <a class="ml-2 text-decoration-none" href="{{ url_for('profile.index', user_id=post.user.id) }}">{{ post.user.nickname }}</a>
                                <div class="d-flex">
                                    <a class="text-decoration-none ml-2"><span class="badge badge-pill badge-success">{{ post.user.college.name }}</span></a>
                                    <a class="text-decoration-none ml-2"><span class="badge badge-pill badge-info">{{ post.user.role.name }}</span></a>
                                </div>
                            </div>
                        </div>
                    {% else %}
                        <img src="{{ url_for('static', filename='img/anonymous.jpeg') }}" class="rounded avatar">
                        <div class="d-flex">
                            <div>
                                <a class="ml-2 text-muted text-decoration-none">匿名</a>
                                <div class="d-flex">
                                    <a class="text-decoration-none ml-2"><span class="badge badge-pill badge-success">{{ post.user.college.name }}</span></a>
                                    <a class="text-decoration-none ml-2"><span class="badge badge-pill badge-info">{{ post.user.role.name }}</span></a>
                                </div>
                            </div>
                        </div>
                    {% endif %}
                </div>
                <div class="mt-2">
                    <article>
                        {{ post.content|safe }}
                    </article>
                </div>
                {% if current_user.is_authenticated and current_user.id == post.user.id %}
                    <div class="d-flex flex-row-reverse mt-1">
                        <a class="text-decoration-none text-muted" href="/post/edit/{{ post.id }}/"><small>编辑</small></a>
                        <a class="mr-2 text-decoration-none text-muted" href="/post/delete/{{ post.id }}/"><small>删除</small></a>
                    </div>
                {% elif current_user.is_authenticated %}
                    <div class="d-flex flex-row-reverse mt-1">
                        <a class="text-decoration-none text-muted ml-2" data-toggle="modal" data-target="#exampleModal" href="#"><small>举报</small></a>
                        <a class="text-decoration-none text-muted ml-2" href="/post/collect/{{ post.id }}/">
                            <small>
                                {% if c_tag %}
                                    取消收藏
                                {% else %}
                                    收藏
                                {% endif %}
                            </small>
                        </a>
                        <a class="text-decoration-none text-muted ml-2" href="/post/unlike/{{ post.id }}/"><small>点灭</small></a>
                        <a class="text-decoration-none text-muted ml-2" href="/post/like/{{ post.id }}/"><small>点亮</small></a>
                    </div>
                {% endif %}
            </div>
            <div class="div-gutter20"></div>
        </div>
    </main>
    </body>
{% endblock %}

在该模板文件中,首先在head块中定义了一些代码样式,然后将主要是内容写在content块里面,在这个块里面做了下面几件事

  • 显示顶部面包屑导航
  • 展示帖子相关信息点亮、点灭、收藏数等等
  • 如果帖子是匿名的方式发布的则隐藏发布者的相关信息
  • 如果打开帖子的当前用户是该帖子的发帖者,则在帖子内容底部显示编辑、删除操作按钮,如果是其他用户则显示收藏、点亮、点灭按钮

 一般的,我们都会将style样式单独定义在一个css文件中,然后通过link标签来引用,由于是在开发过程中,如果样式需要频繁修改,则可以先定义在HTML文件中,因为如果定义在单独的css文件中,修改了样式需要清除浏览器缓存,不然样式不会更新。我们可以在调整好样式之后,将样式统一放入到一个css文件中去!

 接下来,就需要对阅读帖子页面上的一些按钮进行功能开发了。

3.帖子操作

在第二小节中在阅读帖子页面中加入了很多功能按钮,对于帖子所有者有删除、编辑帖子功能按钮,对于其他访问者有点亮、点灭、收藏等按钮,在这一小节中就分别实现对应按钮的功能。

a.点亮、点灭操作

打开bbs/blueprint/frontend/post.py模块,将下面的代码块添加进去

@post_bp.route('/like/<post_id>/', methods=['GET'])
@login_required
def like(post_id):
    post = Post.query.get_or_404(post_id)
    post.likes += 1
    db.session.commit()
    return redirect(url_for('.read', post_id=post_id))


@post_bp.route('/unlike/<post_id>/', methods=['GET'])
@login_required
def unlike(post_id):
    post = Post.query.get_or_404(post_id)
    post.unlikes += 1
    db.session.commit()
    return redirect(url_for('.read', post_id=post_id))

两个视图函数的功能分别是将帖子的点亮数、点灭数+1操作,其实这里做有点不严谨,因为没有绑定用户的id,只是单纯的进行+1的操作,那么用户可以无限点击,要绑定用户id还需要增加表,就交给聪明的读者来实现吧,在下一小节收藏、取消收藏操作中实现了这个功能,可以参照一下代码,如果没有实现思路,可以参照一下下面的代码。

b.收藏、取消收藏操作

跟大多数论坛一样,当用户看到一个比较感兴趣的帖子之后,可以通过收藏该帖子,如果下次用户还想去浏览该帖子,就不需要导出去找,同样的我们也可以实现此功能。打开bbs/blueprint/frontend/post.py 模块,添加进下面的代码

@post_bp.route('/collect/<post_id>/')
@login_required
def collect(post_id):
    post_collect(post_id)
    return redirect(url_for('.read', post_id=post_id))


def post_collect(post_id):
    post = Post.query.get_or_404(post_id)
    c = Collect.query.filter(Collect.user_id == current_user.id, Collect.post_id == post_id).first()
    if c:
        post.collects -= 1
        db.session.delete(c)
        flash('取消收藏帖子成功!', 'success')
    else:
        post.collects += 1
        c = Collect(user_id=current_user.id, post_id=post_id)
        db.session.add(c)
        flash('收藏帖子成功!', 'success')
        
    db.session.commit()

在上面的代码中,出现了一个新的模型类Collect,该类就是用来存储用户收藏帖子的数据库表,这张表属于多对多关系的中间表。因为一个用户可以收藏多个帖子,而一个帖子又可以被多个用户收藏,因此就需要通过一张中间表来存储用户与帖子之间的关系,ER图如下所示

er图

打开bbs/models.py模块,新建Collect模型类,代码清单如下

class Collect(db.Model):
    __tablename__ = 't_collect'

    id = db.Column(db.INTEGER, primary_key=True, autoincrement=True)
    user_id = db.Column(db.INTEGER, db.ForeignKey('t_user.id'))
    post_id = db.Column(db.INTEGER, db.ForeignKey('t_post.id'))
    timestamps = db.Column(db.DateTime, default=datetime.datetime.now)

    user = db.relationship('User', back_populates='collect', lazy='joined')
    post = db.relationship('Post', back_populates='collect', lazy='joined')

回到前一段代码,先通过传过来的post_id参数以及当前登录用户的idt_collect表中查找是否包含这个数据,如果包含,则说明用户点击的是取消收藏的按钮,反之则表明用户点击的是收藏按钮,按照对应的方式处理插入数据还是增加数据。

c.帖子编辑

帖子编辑功能是每个论坛都必须提供的功能,接下来就实现此功能,打开bbs/forms.py模块,加入下面的代码

class EditPostForm(BasePostForm):
    submit = SubmitField(u'保存编辑', render_kw={'class': 'source-button btn btn-danger btn-xs mt-2 text-right'})

同样的在bbs/blueprint/frontend/post.py模块加入下面的代码

@post_bp.route('/edit/<post_id>/', methods=['GET', 'POST'])
@login_required
def edit(post_id):
    form = EditPostForm()
    post = Post.query.get_or_404(post_id)
    if form.validate_on_submit():
        title = form.title.data
        ep = Post.query.filter_by(title=title).first()

        # 修改标题不能是其他已经存在帖子的标题
        if ep and ep.id != post.id:
            flash('该帖子标题已经存在', 'danger')
            return redirect(url_for('.edit', post_id=post_id))

        cate = form.category.data
        anonymous = form.anonymous.data
        content = form.body.data
        post.title = title
        post.cate_id = cate
        post.is_anonymous = anonymous
        post.content = content
        db.session.commit()
        flash('帖子编辑成功!', 'success')
        return redirect(url_for('.read', post_id=post_id))

    form.title.data = post.title
    form.body.data = post.content
    form.category.data = post.cate_id
    form.anonymous.data = post.is_anonymous
    return render_template('frontend/post/edit-post.html', post=post, form=form)

在上面的代码中,使用了EditPostForm这个表单类,通过继承自BasePostForm,就如同用户发表帖子那一节一样。之后我们根据请求参数中post_id来从数据库中获取对应的帖子内容,如果是提交表单请求的操作就进行帖子内容的更新,如果是编辑表单的请求操作就将该帖子的内容渲染在模板文件中,打开bbs/templates/frontend/post/目录,新建edit-post.html模板文件,嵌入下面的代码

{% extends "frontend/base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block head %}
    {{ super() }}

{% endblock %}
{% block title %}
    编辑帖子-{{ post.title }}
{% endblock %}
{% block content %}

    <body>
    <main>
        <div class="container mt-3">
            {% include "_flash.html" %}
            <h3 class="text-info"><strong>编辑帖子</strong></h3>
            <hr class="bg-secondary">
            <form action="/post/edit/{{ post.id }}/" method="post">
                {{ form.csrf_token }}
                {{ wtf.form_field(form.title) }}
                <div class="row">
                    <div class="col">
                        {{ wtf.form_field(form.category) }}
                    </div>
                    <div class="col">
                        {{ wtf.form_field(form.anonymous) }}
                    </div>
                </div>
                {{ form.body }}
                <div class="text-right">
                    {{ form.submit }}
                </div>
            </form>
        </div>
    </main>
    </body>
    {{ ckeditor.load() }}
    {{ ckeditor.config(name='body') }}
    <script>
        CKEDITOR.on( 'instanceReady', function( evt ) {
            evt.editor.dataProcessor.htmlFilter.addRules( {
                elements: {
                    img: function(el) {
                        el.addClass('img-fluid d-block mx-auto');
                    },
                    table: function (el){
                        el.addClass('table table-responsive');
                    },
                    thead: function (el){
                        el.addClass('thead-light');
                    },
                    blockquote: function (el){
                        el.addClass('m-blockquote');
                    }
                }
            });
        });
    </script>
{% endblock %}

跟发布帖子那一节差不多,只是在添加了一些JavaScript代码,这些代码的主要作用是用来定制一些CKEditor元素的样式的,具体可以去查看CKEditor4的文档,这里不进行细说。

d.删除帖子

删除帖子的功能相对来说就十分的简单了,这里的删除实际上不是在数据库中删除,而是将帖子设置为某种状态,在很多互联网企业中删除都是通过这种方式来实现的,打开bbs/blueprint/frontend/post.py模块,添加下面的代码清单

@post_bp.route('/delete/<post_id>/', methods=['GET'])
@login_required
def delete(post_id):
    post = Post.query.get_or_404(post_id)
    if post.can_delete():
        post.status_id = 2
        db.session.commit()
        flash('帖子删除成功!', 'success')
    else:
        flash('不是你的东西,你没有权限删除!', 'danger')
    return redirect(url_for('index_bp.index'))

通过请求参数post_id 从数据库中获取对应post,通过can_delete()方法来判断当前帖子是否属于当前用户,如果不是则返回提示消息,如果是则将当前帖子的status_id置为2,然后保存到数据库中,返回提示消息。

至此,本节内容就全部讲完啦,整个项目差不多已经写完了,可以到github仓库下载源码对照我博客网站上的教程阅读。读者对照教程一边看教程一边写代码可能会出错哦,因为教程是在我代码写完之后开始写的,可能有些地方会遗漏,请谅解~哈哈哈~