Flask搭建博客网站之评论及回复功能实现

最近忙着搭建自己的博客网站忙的焦头烂额,其中就包括文章底下评论以及评论回复的功能实现。在网上找到很多资料并不能解决,最后通过蛛丝马迹和我自己的才华(笑)解决了这个问题,所以今天就像跟大家分享一下此部分功能的踩坑经验。

一、数据库设计

此部分是最难的地方,思考一下,一篇文章有很多评论,一个user对应一句评论,每一句评论底下对应相对的对此评论的回复,每一句回复又与回复对象user相对应。



好吧,总结一下:

  1. “评论”与“回复”两个表可以用一个,只是对象不同,评论的对象为文章(作者),回复的对象为评论的user
  2. 每一句“回复”Post对应两个user,一个user是发起者,一个user是回复的对象。
  3. 文章Articles与“回复”Post一对多关系。

画个图:


image.png

二、相应的model代码设计

除了上述问题之外,还要考虑三个基本表的基本属性。另外还需要考虑的:
1.在post表中增加一个布尔类型的字段,以便设置评论是否为私密文章(私密文章后面再考虑)。
2.post表中增加reply_id以对应回复的对象是哪一条评论(post)
3.post表增加timestamp以记录评论的时间

class Post(db.Model):#评论,一个用户对应多个评论
    __tablename__='posts'
    d = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'),index=True)#外键
    user_to_id = db.Column(db.Integer,db.ForeignKey('users.id'),index=True)#外键
    content = db.Column(db.String(400))#内容
    timestamp = db.Column(db.DateTime, index=True,default=datetime.utcnow)
    article_id = db.Column(db.Integer,db.ForeignKey('article.id'),index=True)#外键
    is_appear = db.Column(db.Boolean())    #是否显示此评论
    reply_id = db.Column(db.Integer,index=True)         #回复的是哪一条评论
    #reply_to_userid = db.Column(db.Integer,index=True)

 class User(UserMixin,db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(320), unique=True)
    password_hash = db.Column(db.String(128))
    user_to_id=db.relationship('Post',foreign_keys=[Post.user_to_id],backref=db.backref('user_to', lazy='joined'),lazy='dynamic',cascade='all, delete-orphan')
    posts=db.relationship('Post',foreign_keys=[Post.user_id],backref=db.backref('userid', lazy='joined'),lazy='dynamic',cascade='all, delete-orphan')
    address = db.Column(db.String(80))#网址
    def __repr__(self):
        return '<User{}>'.format(self.username)
    def  set_password(self,password):
        self.password_hash=generate_password_hash(password)
    def check_password(self,password):
        return check_password_hash(self.password_hash,password)

class Article(db.Model):
    __tablename__ = 'article'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128))       #标题
    body=db.Column(db.Text)     #原本text格式
    body_html = db.Column(db.Text)      #转化成html代码后的格式
    create_time = db.Column(db.DateTime, index=True, default=datetime.utcnow)       #创建时间
    seo_link = db.Column(db.String(128))        #文章原链接
    pic_path = db.Column(db.String(320))        #封面地址
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))     #文章类别
    posts = db.relationship('Post',backref='article',lazy='dynamic')    #一个评论的外键

解释一下,根据一中的图我们可以知道post与user为多对多的关系,换句话说,user有两个外键指向post,可是如果设置成两个外键的话post怎么识别对应哪个外键呢?
foreign_keys=[Post.user_to_id]和foreign_keys=[Post.user_id]两个标识就可以解决此问题。

三、命令处理

此处我们默认原本的基本表post,user,article的基本属性已经存在,现在只需要增加上述问题中的几个属性就行了。
一般的先对Post的user表部分修改后,使用下面的命令

flask db migrate -m"posts table"
flask db upgrate

然后修改user

flask db migrate -m"users table"
flask db upgrate

接着对先对Post的article表部分修改后

flask db migrate -m"posts table"
flask db upgrate

最后

flask db migrate -m"article table"
flask db upgrate

请注意以上的顺序,一个不小心就会报错!

四、表单设计

class CommentForm(FlaskForm):
    username = StringField('用户名',validators=[DataRequired()])
    post=TextAreaField('评论',validators=[DataRequired(),length(min=1,max=200)])
    email=StringField ('电子邮箱',validators=[DataRequired(),Email()])
    address=StringField('网址或博客网站')
    is_appear=BooleanField('私密评论')
    submit=SubmitField('提交')

五、路由处理

由于在文章界面增加评论以及评论回复,这里先对文章路由进行修改:

@app.route('/article/<int:id>', methods=['GET', 'POST'])
@csrf.exempt
def article(id):
        ...
    form = CommentForm()
    article=Article.query.filter_by(id=id).first_or_404()
    posts=Post.query.filter_by(article_id=id).order_by(Post.timestamp.desc())
    title=article.title
    if request.method == 'POST':
        post_username=form.username.data
        post_text=form.post.data
        post_email=form.email.data
        post_address=form.address.data
        post_is_appear=form.is_appear.data
        user1=User.query.filter_by(username=post_username).first()
        user2=User.query.filter_by(email=post_email).first()
        if user1 == user2 and user1 is not None and user2 is not None:
            post=Post(user_id=user1.id,content=post_text,is_appear=post_is_appear,article_id=article.id,reply_id=0,user_to_id=1)
            db.session.add(post)
            db.session.commit()
            flash('提交成功')
            return redirect(url_for('article',id=id))
        elif user1 is None and user2 is None:
            user=User(username=post_username,email=post_email,address=post_address)
            user.set_password("123456")
            db.session.add(user)
            db.session.commit()
            post=Post(user_id=user.id,content=post_text,is_appear=post_is_appear,article_id=article.id,reply_id=0,user_to_id=1)
            db.session.add(post)
            db.session.commit()
            flash('提交成功')
            return redirect(url_for('article',id=id))
        else:
            flash('昵称或者邮箱已经被使用!')
            return redirect(url_for('article',id=id))
    return render_template('article.html',title=title,article=article,posts=posts,form=form)

这里的思路是,首先用户评论时以昵称创造一个user(相当于注册),需要填写邮箱信息,自己的个人网站,以及评论的内容。此处要判断user昵称是否存在以及邮箱是否存在,因此需要这样的一个代码(上面提到过这里强调一下):


image.png

六、增加评论的表单

两个界面,一个是文章界面底下可以进行评论,设置一个form,一个是回复界面。
先是文章底下的form:

<form action="" method="post" class="am-center" role="form" style="width: 90%;">
                              <div class=" am-u-sm-12 am-u-md-12 am-u-lg-12  am-center">
                                <div class="am-form-group">
                                  {{ form.username(size=35,class_='am-form-field',placeholder='昵称*') }}
                                  {% for error in form.username.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
                                <div class="am-form-group">
                                  {{ form.email(size=35,class_='am-form-field',type='email',placeholder='电子邮件*') }}
                                  {% for error in form.email.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
                                <div class="am-form-group">
                                  {{ form.address(size=35,class_='am-form-field',type='text',placeholder='你的网址或者博客') }}
                                  {% for error in form.address.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
                              </div>
                              <br/>
                               <div class="am-form-group am-u-sm-12 am-u-md-12 am-u-lg-12">
                                {{ form.post(class_='am-radius',rows='7',id='',style='width:100%;') }}
                                  {% for error in form.post.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
                              <br/>
                               <div class="am-form-group am-u-sm-12 am-u-md-12 am-u-lg-12 am-text-light">
                                  {{ form.is_appear(class_='am-fr') }}
                                  {{ form.is_appear.label(class_='am-fr light-text-2') }}
                                  {% for error in form.is_appear.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
                              <div class="am-form-group am-u-sm-12 am-u-md-12 am-u-lg-12">
                                  {{ form.submit(class_='am-btn am-btn-default am-fr',type='submit',placeholder='提交') }}
                                  {% for error in form.submit.errors %}
                                  <span style="color:red;">[{{error}}]</span>
                                  {%endfor%}
                                </div>
</form>

结果为:


image.png

然后是点击回复按钮后对应的跳转界面:


image.png

七、评论显示

评论显示在文章评论表单的下方,有一下几个注意点:
1.如果评论或者回复为私密,则游客只能看到“此评论为私密,请登录相应账号”的字样(你进行评论或者回复的时候,就相当于注册了一个账号,密码默认为123456,后面会增加功能可供修改),其次,能看到此评论的人有三者:管理员,评论人,评论对象(也可能是管理员)。
2.评论界面中要有层次,评论文章的post在靠近左方的位置,而“回复”内容需要进行缩进。
3.你的post不仅要对应文章、对应对象,还要对应相应的评论id才会在相应的位置底下显示
代码:

                         <div class="am-u-sm-12 am-u-md-12 am-u-lg-12 am-center">
                          <div class="light-text am-center" style="width:90%;">
                            最新评论:
                            <hr>
                          </div>
                        </div>
                        <div class="am-u-sm-12 am-u-md-12 am-u-lg-12 am-center">
                            <div class="light-text-2 am-center" style="width:90%;">
                              {% for post in posts %}
                              {% if post.reply_id == 0 %}

                              {% if post.is_appear == True and current_user.username != 'admin' and current_user.id != post.userid.id%}
                                {{post.timestamp}}
                                <br>
                                {{post.userid.username}}评论:<a href="{{url_for('reply',article_id=post.article_id,user_id=post.user_id,user_to_id=post.user_id)}}" class="am-fr">回复</a>
                                <br>
                                &nbsp&nbsp&nbsp&nbsp此评论为私密内容,需要登录才能看见自己相应的评论
                                <br>
                                <hr>
                              {% else %}
                                {{post.timestamp}}
                                <br>
                                {{post.userid.username}}评论:<a href="{{url_for('reply',article_id=post.article_id,user_id=post.user_id,user_to_id=post.user_id)}}" class="am-fr">回复</a>
                                <br>
                                &nbsp&nbsp&nbsp&nbsp{{post.content}}
                                <br>

                                {% for reply in posts %}
                                {% if reply.reply_id == post.id %}

                                {% if reply.is_appear == True and current_user.username != 'admin' and current_user.id != post.userid.id and current_user.id != reply.userid.id and current_user.id != reply.user_to.id %}
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp{{reply.timestamp}}
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp{{reply.userid.username}}评论{{reply.user_to.username}}:<a href="{{url_for('reply',article_id=post.article_id,user_id=post.user_id,user_to_id=reply.user_id)}}" class="am-fr">回复</a>
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp此评论为私密内容,需要登录才能看见自己相应的评论
                                  <br>

                                {% else %}
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp{{reply.timestamp}}
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp{{reply.userid.username}}评论{{reply.user_to.username}}:<a href="{{url_for('reply',article_id=post.article_id,user_id=post.user_id,user_to_id=reply.user_id)}}" class="am-fr">回复</a>
                                  <br>
                                  &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp{{post.content}}
                                  <br>
                                {%endif%}
                                {%endif%}
                                {%endfor%}
                                <hr>
                              {%endif%}
                              {%endif%}
                              {%endfor%}
                            </div>
                        </div>

我这里以最简单的表现形式来写的代码可能略微复杂,但是能看懂就好。
其中for循环最外面的posts为路由传过来的一个所有的评论的值,{% if post.reply_id == 0 %}为如果post回复的对象为此文章(作者),则相当于“评论”而不是“评论回复”,不需要进行缩进,否则需要进行缩进。(下面的部分将分为“评论”和“评论回复”两方面讲)
对于一条“评论”来说,私密文章只能是评论的user和文章的作者看到,因此此处做判断 {% if post.is_appear == True and current_user.username != 'admin' and current_user.id != post.userid.id%}判断是否为私密评论。然后显示为“XX评论:XXXX”。
对于一条“评论回复”来说,首先要对应此评论,即{% if reply.reply_id == post.id %},接着判断是否为私密评论,能看到的对象为三方:管理员,评论人,评论对象—— {% if reply.is_appear == True and current_user.username != 'admin' and current_user.id != post.userid.id and current_user.id != reply.userid.id and current_user.id != reply.user_to.id %},最后显示为“XX评论了XX:XXXXX”。

做出的效果为:


image.png

(以上如有问题,不吝赐教,真心希望你的批评指正。)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容