个人博客Demo: link.
GitHub项目完整链接:link
回顾上一节主要讲了以下3个方面内容:
- 基模板 base.html
- 文章列表局部模板 _post.html
- 主页index和分类页category相关模板和视图函数
3.2.1 文章详情页post
- 文章详情页除了包括文章标题,内容,分类,时间戳以外,还有一个重要内容就是文章评论板块
- 代码虽然比较长(主要是评论板块的占比),但是理解起来并不复杂,其中最重要的莫过于用form表单标签包裹开启,关闭评论按钮和删除评论按钮,form包裹能够提高安全性,防范CSRF攻击。具体查看代码注释。
- 文章详情页还涉及到很多视图函数,包括后台管理部分的视图函数如,开启关闭评论,删除评论等,视图函数代码一一列举到后面
- post.html 代码如下:
{% extends 'base.html' %}
{% from 'bootstrap/pagination.html' import render_pagination %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}详情{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm-10 mx-auto">
<div class="page-header">
<h1>{{ post.title }}</h1>
</div>
<div class="">
<span class="body-font">{{ post.body|safe }}</span>
<p><small>分类: {{ post.category.name }}</small></p>
<small>日期: {{ moment(post.timestamp).format('LLL') }}</small>
</div>
<hr>
<div class="comments" id="comments">
<h3>评论数:{{ pagination.total }}
<!-- 如果管理员已登录,显示以下按钮(表单包裹按钮旨在防范CSRF攻击) -->
{% if current_user.is_authenticated %}
<form method="post"
action="{{ url_for('admin.set_comment', post_id=post.id, next=request.full_path) }}"
class="float-right">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm">
{% if post.can_comments %}关闭{% else %}开启{% endif %}评论
</button>
</form>
{% endif %}
</h3>
<!-- 评论列表组 -->
{% if comments %}
<ul class="list-group">
{% for comment in comments %}
<!-- flex弹性盒子,flex-column子元素垂直方向显示 -->
<li class="list-group-item list-group-item-action flex-column">
<!-- w-100:width:100%,justify-content-between:内容排列方式 -->
<div class="d-flex w-100 justify-content-between">
<!-- mb-1:margin-button -->
<h5 class="mb-1 text-primary">
<!-- 这里用于判定,如果评论为管理员则admin.name作为评论者名字,否则用comment.author -->
{% if comment.from_admin %}
{{ admin.name }}
{% else %}
{{ comment.author }}
{% endif %}
</a>
<!-- 管理员评论添加Author徽章 -->
{% if comment.from_admin %}
<span class="badge badge-primary">作者</span>{% endif %}
<!-- 当评论是一个回复时,则显示一个Reply提示标签 -->
{% if comment.replied %}<span class="badge badge-light">Reply</span>{% endif %}
</h5>
<small data-toggle="tooltip" data-placement="top" data-delay="500"
data-timestamp="{{ comment.timestamp.strftime('%Y-%m-%dT%H:%M:%SZ') }}">
{{ moment(comment.timestamp).fromNow() }}
</small>
</div>
<!-- 判断是否回复;comment.replied.id表示被回复评论的作者,br表示换行 -->
{% if comment.replied %}
<p class="alert alert-dark reply-body">{{ comment.replied.author }}:
<br>{{ comment.replied.body }}
</p>
{%- endif -%}
<!-- 评论主体 -->
<p class="mb-1">{{ comment.body }}</p>
<!-- float-right:靠右悬浮 -->
<div class="float-right">
<a class="btn btn-light btn-sm"
href="{{ url_for('.reply_comment', comment_id=comment.id) }}">回复</a>
{% if current_user.is_authenticated %}
<form class="inline" method="post"
action="{{ url_for('admin.delete_comments',
comment_id=comment.id, next=request.full_path) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('确定删除该评论?');">删除
</button>
</form>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="tip"><h5>这里还没有评论...</h5></div>
{% endif %}
</div>
{% if comments %}
{{ render_pagination(pagination, fragment='#comments') }}
{% endif %}
{% if request.args.get('reply')%}
<div class="alert alert-dark ">
回复:<strong>{{ request.args.get('author') }}</strong>
<a class="float-right " href="{{ url_for('.show_post', post_id=post.id )}}">取消回复</a>
</div>
{% endif %}
{% if post.can_comments %}
{{ render_form(form, action=request.full_path) }}
{% else %}
<div class="tip">
<p>评论区已关闭</p>
</div>
{% endif %}
</div>
</div>
{% endblock content %}
- 文章详情页视图函数部分:
- blog.py/ show_post视图
blog.py导入模块及实例部分代码变化成这样:
from flask import Blueprint, request, current_app, render_template, redirect, flash, url_for
from Blog.models import Post, Category, Comment
from Blog.forms import CommentForm
from Blog.extensions import db
@blog_bm.route('/post/<int:post_id>', methods=['GET', 'POST'])
def show_post(post_id):
post = Post.query.get_or_404(post_id)
page = request.args.get('page', 1, type=int)
per_page = current_app.config['BLOG_COMMENT_PER_PAGE']
pagination = Comment.query.with_parent(post).order_by(Comment.timestamp.desc()).paginate(page, per_page=per_page)
comments = pagination.items
form = CommentForm()
if form.validate_on_submit():
author = form.name.data
body = form.comment.data
# 必须加入post=post参数,否则无法显示出对应评论,因为comment不知道自己属于哪一篇文章
comment = Comment(
author=author,
body=body,
post=post
)
replied_id = request.args.get('reply')
if replied_id:
replied_comment = Comment.query.get_or_404(replied_id)
comment.replied = replied_comment
db.session.add(comment)
db.session.commit()
flash('评论成功!', 'success')
return redirect(url_for('.show_post', post_id=post_id))
return render_template('blog/post.html', post=post, pagination=pagination, comments=comments, form=form)
- 视图函数中涉及到分页对象;实例化评论表单类: form = CommentForm();渲染文章详情页模板,传入相关参数
- form.validate_on_submit()验证表单并提交,表单name,comment字段数据传递到author,body变量中,再实例化数据模型Comment类,传入参数author,body变量,以及post对象(告知评论属于哪一篇文章)
- 在表单提交验证中,通过查询字符串reply获取replied_id,存在replied回复则将对应replied_id评论转换成replied对象,即被回复的评论对象
- 最后db.session.add()提生成临时会话,session.commit()提交会话保存到数据库,页面重定向到文章详情页,刷新评论
- blog.py /reply_comment视图函数
- 评论区回复按钮的视图函数代码:
@blog_bm.route('/reply/comment/<int:comment_id>', methods=['GET', 'POST'])
def reply_comment(comment_id):
comment = Comment.query.get_or_404(comment_id)
if not comment.post.can_comments:
flash('暂时不能回复,文章评论已关闭')
return redirect(url_for('.show_post', post_id=comment.post.id))
return redirect(url_for
('.show_post', post_id=comment.post.id, reply=comment_id, author=comment.author) + '#comment-form')
- 重定向url的时候,传入的参数post_id为回复对应的评论对象comment的外键post.id,确定文章页面不出错;reply=comment_id参数为被回复评论的id;author=comment.author则是被回复评论的作者。
- admin.py / set_comment视图函数
- 由于设置评论功能只能登录后才能看到,因此使用flask_login提供的login_required装饰器进行视图保护,代码较为简单,不赘述
- 开启/关闭按钮视图函数:
from flask import Blueprint, flash, redirect, url_for
from flask_login import login_required
from Blog.models import Post
from Blog.extensions import db
admin_bm = Blueprint('admin', __name__)
@admin_bm.route('/set-comment/<int:post_id>', methods=['POST'])
@login_required
def set_comment(post_id):
post = Post.query.get_or_404(post_id)
if post.can_comments:
post.can_comments = False
flash('评论已关闭!', 'info')
else:
post.can_comments = True
flash('评论已开启!', 'info')
db.session.commit()
return redirect(url_for('blog.show_post', post_id=post.id))
- admin.py / delete_comment视图函数
- 删除评论功能代码如下:
@admin_bm.route('/comment/<int:comment_id>/delete', methods=['POST'])
@login_required
def delete_comments(comment_id):
comment = Comment.query.get_or_404(comment_id)
db.session.delete(comment)
db.session.commit()
flash('评论已删除.', 'success')
return redirect_back()
3.2.2 登录页面login和登出logout
- 登录/登出的入口在页脚右下角(登录页面将页脚设置为空),可直接通过快速渲染表单样式render_form渲染,代码比较简单
- 用户在登录时,提交表单通过验证之后,获取表单密码,用户名进行验证,最后再调用login_user(admin, remember)方法登录用户
- 登出不需要特殊的页面,只需要调用flask_login模块提供的logout_user()函数即可
- 登录页面模板代码 login.html 如下
{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title%}登录{% endblock title %}
{% block content %}
<div class="container ">
<div class="row justify-content-center page-header">
<h1>登录</h1>
</div>
<!--
<div class="row justify-content-center">
{{ render_form(form, extra_classes='col-6') }}
</div>
-->
<div class="row">
<div class="col-sm-6 mx-auto">
<form method="post">
{{ form.csrf_token }}
<div class="form-group ">
{{ form.username.label }}
{{ form.username(class='form-control rounded-0') }}
</div>
<div class="form-group ">
{{ form.password.label }}
{{ form.password(class='form-control rounded-0') }}
</div>
<div class="form-check">
{{ form.remember(class='form-check-input') }}
{{ form.remember.label }}
</div>
{{ form.submit(class='btn btn-dark rounded-0') }}
</form>
</div>
</div>
</div>
{% endblock content %}
{% block footer %}
{% endblock footer %}
- 登录登出的视图函数 login.py
from flask import Blueprint, render_template, flash, redirect, url_for
from Blog.forms import LoginForm
from flask_login import logout_user, login_required, login_user
from Blog.helpers import redirect_back
from Blog.models import Admin
from flask_login import current_user
login_bm = Blueprint('login', __name__)
@login_bm.route('/login', methods=['GET', 'POST'])
def login():
# 如果已登录,重定向回到主页
if current_user.is_authenticated:
return redirect(url_for('blog.index'))
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
password = form.password.data
remember = form.remember.data
admin = Admin.query.first() # 返回查询的第一条记录
if admin:
if username == admin.username and admin.validate_password(password):
login_user(admin, remember) # 登入用户
flash('欢迎回来!', 'info')
return redirect_back()
flash("用户名或密码错误", 'warning')
else:
flash("没有管理员账户")
return render_template('login/login.html', form=form)
@login_bm.route('/logout')
@login_required
def logout():
logout_user()
flash('注销成功!', 'info')
return redirect_back()
# return redirect(url_for('blog.index'))
3.2.3 小结
- 文章详情页中的评论板块需要好好理解,细节较多
- 后面会介绍管理后台页面的实现