展开

文章目录

修改历史

修改历史记录

  1. 2022-04-10 22:57:00

使用Flask搭建一个校园论坛10-个人中心(1)

2022-04-10 22:49:28 系列教程 661

简介

在上一节教程中,通过markdown库实现了论坛评论内容支持markdown语法,本节中讲解如何实现论坛的个人中心。

1. 功能介绍

论坛这种内容管理系统必不可少的一个功能模块就是个人中心,在个人中心中可以查看自己的账号基本信息、以及个人在社区的一些操作记录,比如发帖、评论、收藏等等,本论坛的个人中心功能如下图所示

个人中心功能脑图

2. 页面布局

个人中心的页面布局如下图所示,上面个人基本信息栏为个人中心所有页面通用部分,不管在个人中心的哪个栏目,上面通用部分的信息内容是不会改变的,下方的四个tab分别对应不同的功能模块,是差异性内容。

布局

2.1 通用部分

Jinja2的模板语法中,我们可以通过include关键字来将其他的模板文件引入到我们目标模板文件中,因为个人中心的上半部分的信息是通用,因此我们可以将其抽离出来,放入单独的html模板文件中去。除此之外Jinja2还支持macro块,macro的语法如下所示

# macro.html
{% macro sample() %}
   <p>I am macro function!</p>
{% endmacro %}

# other.html

{% from  macro.html import sample with context %}
<div>
    {{ sample() }}
</div>

为了方便管理,我们把所有的macro相关的都放到了templates/macro.html文件中,在文件中嵌入下面的代码

{% macro profile_header() %}
    <div class="row">
        <div class="col-md-3 col-lg-3 text-center">
            <img class="img-profile-bg" alt="{{ user.nickname }}" src="{{ user.avatar }}">
        </div>
        <div class="col-md-9 col-lg-9 div-center-md">
            <h3 class="mb-0">{{ user.nickname }}</h3>
            <p class="text-muted mb-2">@{{ user.username }}</p>
            {% if user.slogan %}
                <p class="mb-2">{{ user.slogan }}</p>
            {% endif %}
            {% if user.website or user.location %}
                <p class="mb-2">
                    {% if user.website %}
                        <a class="text-decoration-none social-label" href="{{ user.website }}" target="_blank"
                           title="{{ user.nickname }}的个人主页"><i class="fa fa-home fa-fw mr-1"></i>{{ user.website }}</a>
                    {% endif %}
                    {% if user.location %}
                        <span class="social-label d-lg-inline d-none"><i
                                class="fa fa-map-marker fa-fw mr-1"></i>{{ user.location }}
                        </span>
                        <span class="social-label d-lg-none d-md-block" title="{{ user.location }}"><i
                                class="fa fa-map-marker fa-fw mr-1"></i>{{ user.location|truncate(8) }}
                        </span>
                    {% endif %}
                </p>
            {% endif %}
            <p>
                <span class="mr-2">{{ user.post|length }}篇帖子</span>
                <a href="{{ url_for('profile.follower', user_id=user.id) }}"
                   class="mr-2 span-hand a-link text-decoration-none">{{ user.followers.count() }}位粉丝</a>
                <a href="{{ url_for('profile.following', user_id=user.id) }}"
                   class="mr-2 span-hand a-link text-decoration-none">{{ user.following.count() }}个关注</a>
            </p>
            {% if current_user.id == user.id %}
                <a class="btn btn-outline-success" href="{{ url_for('user.index', user_id=current_user.id) }}">个人主页</a>
            {% else %}
                {% if current_user.is_following(user) %}
                    <a class="btn btn-outline-info" href="/profile/unfollow/{{ user.id }}/">取消关注</a>
                {% else %}
                    <a class="btn btn-outline-info" href="/profile/follow/{{ user.id }}/">关注TA</a>
                {% endif %}
                <!-- 弹出私信窗口 -->
                <a class="btn btn-outline-primary" data-toggle="modal" data-userid="{{ user.id }}"
                   data-target="#privacyChat">私信</a>
                {% if not blocked %}
                    <a class="btn btn-outline-danger" data-toggle="modal" data-userid="{{ user.id }}"
                       data-target="#confirmBlockUser">Block</a>
                {% else %}
                    <a class="btn btn-primary disabled" data-toggle="modal" data-userid="{{ user.id }}"
                       data-target="#confirmBlockUser">Blocked</a>
                {% endif %}
            {% endif %}

        </div>
    </div>
{% endmacro %}

个人中心除了用户自己的,用户同样也可以访问其他用户的个人中心主页,因此在上面的代码中加入了判断,当前个人中心主页是否为当前用户个人的主页,如果不是,则在下方添加不同的功能按钮。

2.2 TAB部分

上面的通用部分内容既可以通过macro实现也可以通过include去实现,tab部分的内容就通过macro实现就比较方便了,在macro中我们也可以传入参数,同样的在macro.html中增加下列代码

{% macro profile_moment(tabName) %}
    <div class="mt-4">
        <ul class="nav nav-tabs nav-justified">
            <li class="nav-item">
                <a class="nav-link {% if tabName == '帖子' %}active{% endif %}" href="/profile/user/{{ user.id }}/">帖子</a>
            </li>
            <li class="nav-item">
                <a class="nav-link {% if tabName == '评论' %}active{% endif %}"
                   href="/profile/comment/{{ user.id }}/">评论</a>
            </li>
            <li class="nav-item">
                <a class="nav-link {% if tabName == '收藏' %}active{% endif %}"
                   href="/profile/collect/{{ user.id }}/">收藏</a>
            </li>
            <li class="nav-item">
                <a class="nav-link {% if tabName == '社交' %}active{% endif %}"
                   href="/profile/social/{{ user.id }}/">社交</a>
            </li>
        </ul>
    </div>
{% endmacro %}

这里与上面稍微有些不同,这里的macro函数带有参数输入,我们在函数体中通过判断参数的不同实现对应ab的被选中,当然这里也可以通过include去实现,可以通过endpoint来设置对应tab被选中。

3. 关注、取消关注用户

很多社交网站都会给用户提供关注的功能,论坛也提供了这个功能,用户既可以被别人关注,同时用户也可以关注他们,所以是多对多的关系,Follow表结构如下

class Follow(db.Model):
    __tablename__ = 't_follow'

    id = db.Column(db.INTEGER, primary_key=True, autoincrement=True)
    follower_id = db.Column(db.INTEGER, db.ForeignKey('t_user.id'))
    followed_id = db.Column(db.INTEGER, db.ForeignKey('t_user.id'))

    # 正在关注用户的人
    follower = db.relationship('User', foreign_keys=[follower_id], back_populates='following', lazy='joined')
    # 用户自己正在关注的人
    followed = db.relationship('User', foreign_keys=[followed_id], back_populates='followers', lazy='joined')


class User(db.Model, db.UserMixin):
    ...
    following = db.relationship('Follow', foreign_keys=[Follow.follower_id], back_populates='follower',
                                lazy='dynamic', cascade='all')
    followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], back_populates='followed',
                                lazy='dynamic', cascade='all')

这里需要注意的是,由于Follow表中的两个字段follower_id, followed_id都是user表的外键,因此我们在定义的时候需要指定每个关系所对应的字段,不然ORM框架会识别不了,follower指定为followe_id,followed指定为followed_id,与此同时,在user表中同样也是指定。

创建好Follow表之后,关注的逻辑就比较简单了,在blueprint/profile.py文件中增加下面的代码。代码逻辑十分简单,就是通过向Follow表中新增或者删除相应的数据,实现关注或者取消关注功能,同时还增加了自己不能关注自己的逻辑。

@profile_bp.route('/follow/<user_id>/', methods=['GET', 'POST'])
@login_required
def follow_user(user_id):
    user = User.query.get_or_404(user_id)
    if user.id == current_user.id:
        flash('我关注我自己?禁止套娃!', 'info')
        return redirect(request.referrer)
    if current_user.is_following(user):
        flash('你已经关注TA了!', 'info')
        return redirect(request.referrer)
    current_user.follow(user)
    flash('关注成功!', 'success')
    return redirect(request.referrer)


@profile_bp.route('/unfollow/<user_id>/', methods=['GET', 'POST'])
@login_required
def unfollow_user(user_id):
    user = User.query.get_or_404(user_id)
    if current_user.is_following(user):
        current_user.unfollow(user)
    if request.method == 'POST':
        return jsonify({'tag': 1})
    flash('取关成功!', 'success')
    return redirect(request.referrer)

4. 用户帖子

如上面所说,个人中心用户即可以访问自己的也可以访问他人,因此需要在帖子列表上做区分,在frontend/profile.py文件中增加下面的代码

@profile_bp.route('/user/<user_id>/')
@login_required
def index(user_id):
    page = request.args.get('page', default=1, type=int)
    user = User.query.get_or_404(user_id)
    per_page = current_app.config['BBS_PER_PAGE']
    # 其他人查看用户信息时候屏蔽用户匿名发表的帖子
    if current_user.id == int(user_id):
        pagination = Post.query.filter(Post.author_id == user_id, Post.status_id == 1).order_by(
            Post.create_time.desc()).paginate(page=page, per_page=per_page)
        posts = pagination.items
    else:
        pagination, posts = get_range_post(user.id,
                                           page=page,
                                           per_page=per_page,
                                           range_day=TIME_RANGE.get(user.range_post.name))
    blocked = BlockUser.query.filter(BlockUser.user_id == current_user.id,
                                     BlockUser.block_user_id == user_id).all()
    return render_template('frontend/profile/profile.html', user=user, tag=pagination.total > per_page,
                           pagination=pagination, posts=posts, blocked=blocked)

代码的逻辑很简单,如下

  1. 通过路由参数中的user_id来获取对应的user,如果没有找到user则返回404;
  2. 判断当前user是否为路由参数中的user,如果不是,则只获取该用户状态为正常的帖子;
  3. 如果是则获取所有的状态的帖子;

上面还出现了BlockUser的相关代码,这个代码在后续用户主页的相关文章中进行讲解,这里暂时不做讲解!

后端后去数据后,需要通过Jinja2模板进行渲染了,在frontend/profile/profile.html中新增下面的代码

{% extends "frontend/base.html" %}
{% from "macro.html" import profile_header, profile_moment, render_pagination, post_list with context %}
{% block head %}
    {{ super() }}
    <style>

        .a-title {
            font-size: 22px;
            font-weight: bold;
        }

        .a-title-sm {
            font-size: 16px;
            font-weight: bold;
        }

        .f-12-b {
            font-weight: bold;
            font-size: 12px;
        }

        .span-hand {
            cursor: pointer;
        }

    </style>
{% endblock %}

{% block title %}
    {{ user.nickname }}的个人主页
{% endblock %}
{% block content %}
    {{ moment.locale(auto_detect=True) }}
    <body>
    <main>
        <div class="container mt-2">
            {% include "_flash.html" %}
            {{ profile_header() }}
            {{ profile_moment('帖子') }}
            {% if posts %}
                <div class="mt-2">
                    {{ post_list(posts) }}
                    {% if user.range_post.name != '全部' and current_user.id != user.id %}
                        <div class="mt-2 text-center">
                            <p class="text-muted"><b>该用户仅展示最近{{ user.range_post.name }}的帖子</b></p>
                        </div>
                    {% endif %}
                </div>
            {% else %}
                {% if current_user.id == user.id %}
                    <div class="text-center mt-5">
                        <p class="text-muted">你还没有发送过帖子!</p>
                    </div>
                {% else %}
                    <div class="text-center mt-5">
                        {% if user.available_post_counts()|length %}
                            {% if user.range_post.name == '隐藏' %}
                                <p class="text-muted"><b>该用户{{ user.range_post.name }}了帖子</b></p>
                            {% else %}
                                <p class="text-muted"><b>该用户仅展示最近{{ user.range_post.name }}的帖子</b></p>
                            {% endif %}
                        {% else %}
                            <p class="text-muted"><b>他还没有发送过帖子!</b></p>
                        {% endif %}
                    </div>
                {% endif %}
            {% endif %}
            {% include "follow-modal.html" %}
        </div>
    </main>
    </body>

    {% include "frontend/profile/contact-modal.html" %}
    {% include "frontend/profile/block-user-modal.html" %}
{% endblock %}

博客文章中的代码只截取了部分关键代码,很多细节代码没有贴出来,如果想要在本地机器上运行调试可以访问我的github仓库或者gitee仓库仓库,如果想要看到实际效果请访问二狗学院

1条评论


Coder_Liuu 用户

顶一顶!博主好强!

Macv 博主 回复:Coder_Liuu

谢谢~~~拖延症晚期,很久没写这个教程了额