一个web应用的诞生(8)--博文发布

这个系统一直号称轻博客,但貌似博客的功能还没有实现,这一章将简单的实现一个博客功能,首先,当然是为数据库创建一个博文表(models\post.py):

from .. import db
from datetime import datetime

class Post(db.Model):
    __tablename__='posts'
    id=db.Column(db.Integer,primary_key=True)
    body=db.Column(db.Text)
    createtime=db.Column(db.DateTime,index=True,default=datetime.utcnow)
    author_id=db.Column(db.Integer,db.ForeignKey("users.id"))

你可能注意到了,这个博文表并没有title字段,这个是参考了微博以及目前市面上的一些轻博产品,每个人可以随心所以的发布轻博客,不限制必须发布正规的博客。

同时修改用户表与博文表关联

class User(UserMixin,db.Model):
    
    ...

    posts=db.relationship("Post",backref="author",lazy='dynamic')

然设置博文表单(forms\PostForm.py):

from flask_wtf import FlaskForm
from wtforms import TextAreaField,SubmitField
from wtforms.validators import DataRequired
class PostForm(FlaskForm):
    body=TextAreaField("分享一下现在的心情吧!",validators=[DataRequired()])
    submit=SubmitField("提交")

模仿一下微博或现有的轻博产品,首先首页会有一个发布框,用于一键发送消息,如:

当然,还有轻博的鼻祖,墙外的Tumblr都是如此设计,我们也当然要追求流行的设计趋势,利用用户的固有习惯,最求最精致的感觉(好吧,其实就是抄袭),所以首页也要有此功能,修改首页的视图方法:

@main.route("/",methods=["GET","POST"])
def index():
    form=PostForm()
    if form.validate_on_submit():
        post=Post(body=form.body.data,author_id=1) #先写死 因为没有设置权限系统
        db.session.add(post);
        return redirect(url_for(".index")) #跳回首页
    posts=Post.query.order_by(Post.createtime.desc()).all()  #首页显示已有博文 按时间排序
    return render_template("index.html",site_name='myblog',form=form,posts=posts)

然后还要对首页进行修改,同样,追求现有的流行趋势,来设计出一个原型图:

你可能已经注意到了,这个原型中的头像,用户表中是没有的,不过这个不用担心,再以后在实现,现在暂时先使用一张固定的图片,重点实现博文的功能,当然,为了能够预览首页,首先需要更新db:

python manage.py db migrate #更新迁移脚本
python manage.py db upgrade #更新db

然后修改base.html,为导航条新增搜索框:

{%extends "bootstrap/base.html "%}
{% block title%}牛博客 {% endblock %}<!--覆盖title标签-->

{% block navbar %}
<nav class="navbar navbar-inverse"><!-- 导航部分 -->
  <div class="navbar-header">
      <a class="navbar-brand" href="#">
        NBlog
      </a>
  </div>
  <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li><a href="/">首页</a></li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
        {% if current_user.is_authenticated %}
            <li><p class="navbar-text"><a href="#" class="navbar-link">{{current_user.username}}</a>  您好</p></li>
            <li><a href="{{url_for('auth.logout')}}">登出</a></li>
        {% else %}
            <li><a href="{{url_for('auth.login')}}">登录</a></li>
        {% endif %}
      </ul>
      <form class="navbar-form navbar-right">
        <div class="form-group">
          <input type="text" class="form-control" placeholder="Search">
        </div>
        <button type="submit" class="btn btn-default">搜索</button>
      </form>
    </div><!-- /.navbar-collapse -->
</nav>
{% endblock %}
{% block content %} <!--具体内容-->
{% block main %} <!--实际具体内容-->
<div class="container"></div>
{% endblock %}
{% block footer %} <!--页脚-->
<div class="container  navbar-fixed-bottom">
    <div class="center-block text-center">
        footer
    </div>
</div>
{% endblock %}
{% endblock %}

注意在content block内,新增了两个block,main和footer,即以后原有content内的内容均放到main中,使其自动加载页脚

然后修改首页内容部分,以完成静态页面(注意栅格系统):

{% extends "base.html" %}
{% block main %}
<div class="container">
<div class="row">
 <div class="col-xs-12 col-md-8 col-md-8 col-lg-8">
  <div>
  <textarea class="form-control" rows="3"></textarea>
  <br>
  <button class="btn btn-default" type="submit">分享</button>
  </div>
   <br>
  <div>
      <div class="bs-callout" >
          <div class="row">
              <div class="col-sm-2 col-md-2">
                    ![...](http://upload-images.jianshu.io/upload_images/6290181-87093f937ef232c0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
              </div>
              <div class="col-sm-10 col-md-10">
               <div>
               <p>Options for individual tooltips can alternatively be specified through the use of data attributes, as explained above.</p>
                </div>
               <div>
                <a class="text-left" href="#">李四</a>
                <span class="text-right">发表于5分钟前</span>
               </div>
              </div>
          </div>
      </div>
      <div class="bs-callout bs-callout-d bs-callout-last" >
        <div class="row">
              <div class="col-sm-2 col-md-2">
                    ![...](http://upload-images.jianshu.io/upload_images/6290181-87093f937ef232c0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
              </div>
              <div class="col-sm-10 col-md-10">
               <div>
               <p>Options for individual tooltips can alternatively be specified through the use of data attributes, as explained above.</p>
                </div>
               <div>
                <a class="text-left" href="#">李四</a>
                <span class="text-right">发表于5分钟前</span>
               </div>
              </div>
          </div>
      </div>
     </div>
 </div>
 <div class="col-md-4 col-md-4 col-lg-4">
    ![...](http://upload-images.jianshu.io/upload_images/6290181-cb75387286d97dac.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
     <br><br>
     <p class="text-muted">我已经分享<span class="text-danger">55</span>条心情</p>
     <p class="text-muted">我已经关注了<span class="text-danger">7</span>名好友</p>
     <p class="text-muted">我已经被<span class="text-danger">8</span>名好友关注</p>
 </div>
</div>
</div>
{% endblock %}

还有一个简单的css(原谅我的css渣),

.bs-callout {
    padding: 10px;
    margin: 0px 0;
    border: 1px solid #ccc;
    border-left-width: 0px;
    border-right-width: 0px;
    border-bottom-width: 0px;
    border-radius: 3px;
}
 .bs-callout-d{
  background: #f3f3f3;
 }
 .bs-callout-last{
     border-bottom-width: 1px;
 }
 .bs-callout div img{ width:100px;height:100px;}
 .headimg{width:250px;height:250px;}

好 跑起来看看现在的样子:

看上去貌似还不错,当然,也许是因为模特的颜值高,接下来就开始实现功能,首先注意一点,目前没有用户权限系统, 而是在模型中写死只要发送用户即为1,不管怎么说,先使用wtf将发送框作为表单输入,非常简单,即将文本框和按钮部分替换为:

{{ wtf.quick_form(form) }}

然后就可以直接输入信息,提交,一切就是这么简单:

点击提交,查询数据库:

一切非常完美,但是,连个富文本框都没有,怎么好意思叫博客呢,虽然是轻博客,而作为一个bigger高高的轻博客,那怕简单的富文本框也要bigger搞搞,那么bigger满满的markdown就浮出水面,包括这篇博客,都是用markdown来写,下面就开始集成markdown插件,第一步当然还是安装:

pip3.6 install flask-pagedown markdown bleach

注意这里使用了一种新的安装形式,一次性安装多个功能,他们分别为:

flask-pagedown flask插件,前端可以有通过js的方式支持markdown脚本的能力
markdown 为py提供markdown脚本与html转换的能力
html清理器

第二部也和之前一样,在系统内进行注册:

...
from flask_pagedown import PageDown
...
pagedown=PageDown();
...
def create_app():
    ...
    pagedown.init_app(app)
    ...

然后使用起来也非常简单,修改PostForm文件:

from flask_wtf import FlaskForm
from wtforms import TextAreaField,SubmitField
from flask_pagedown.fields import PageDownField
from wtforms.validators import DataRequired
class PostForm(FlaskForm):
    body=PageDownField("分享一下现在的心情吧!",validators=[DataRequired()])
    submit=SubmitField("分享")

markdown当然需要模板,PageDown同样也提供了一个模板宏,来实现这个功能,它是通过js来实现:

{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock%}

跑起来看看效果:

Perfect!!

下面来保存,db中查看:

可以看到,提交的至少文本框中的markdown的源码,这样当然非常完美,没有问题,但是返回页面中显示的情况如何呢?接下来完成博文列表功能,依然很简单,将原来的列表部分修改为:

 {% for post in posts %}
  <div class="bs-callout
          {% if loop.index % 2 ==0 %}
           bs-callout-d
          {% endif %}
          {% if loop.last %}
            bs-callout-last
          {% endif %}" >
      <div class="row">
          <div class="col-sm-2 col-md-2">
                ![...](http://upload-images.jianshu.io/upload_images/6290181-87093f937ef232c0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
          </div>
          <div class="col-sm-10 col-md-10">
           <div>
           <p>{{post.body}}</p>
            </div>
           <div>
            <a class="text-left" href="#">李四</a>
            <span class="text-right">发表于{{}}</span>
           </div>
          </div>
      </div>
  </div>
  {% endfor %}

注意两个if 的作用,第一个为当偶数行的时候增加一个背景色,第二个的作用为最后一个item增加一个下边框,具体内容看css文件。

你可能已经注意到了,至少有四个问题

  1. markdown没有转义,显示的依然是源码
  2. 时间显示的为时间的格式化字符串形式
  3. 用户头像为固定项
  4. 用户姓名为固定项

关于用户的问题留待下一章在解决,当前首先解决前两个问题,从第一个开始,这个问题的解决方式当然是将markdown转义为html代码,首先想到的是在客户端解决,因为之前既然已经可以预览,那么一定可以通过pagedown插件进行转换,这个想法是正确的,但是,这样做的一个后果就是每次渲染页面的时候都要进行转换,效率并不是很高,所以,我们可以在提交之后,存入数据库之前进行转换,即db中新增一个字段保存html源码,下面修改Post.py文件:

...
import bleach
from markdown import markdown

class Post(db.Model):
    ...
    body_html=db.Column(db.Text)
    ...
    def on_change_body(target,value,oldvalue,initiator):
        allowed_tags=['a','abbr','acronym','b','blockquote','code','em','i',                'li','ol','pre','strong','ul','h1','h2','h3','p']
        target.body_html=bleach.linkify(bleach.clean(markdown(value,output_format='html'),tags=allowed_tags,
                                                     strip=True))

db.event.listen(Post.body,"set",Post.on_change_body)

这段代码的意思是创建一个转换函数on_change_body,并且通过SqlAlchemy的set事件进行监听,即每次body的值更新的时候,HTML的值都会更新

在静态方法中allowed_tag为一个白名单,markdown方法将markdown源码转换后,bleach会清除所有不在白名单上的代码,来确保安全。

继续修改模板代码为:

  <p>
   {% if post.body_html%}
      {{post.body_html|safe}}
    {% else %}
   {{post.body}}
   {% endif %}
  </p>

即当body_html不为空的时候,显示body_html的内容,否则显示body的内容,参数safe的意思为jinja2框架不对后台传入的html标签进行转义,而是直接注入,OK,看看渲染的结果:

ok 显示的结果非常好,接下来让我们继续看看关于时间的问题吧

当想要实现一个小功能的时候,一般来讲有两种方法,第一种就是自己造一个轮子,第二种就是使用一个已有的轮子,自己造轮子的当然会有很多问题,比如圆不圆了,是不是耐用,能走什么路况等,但对于已有轮子,一般来说这些问题都经过了系统的测试,自己所需要面对的仅仅是这个轮子和这辆车是不是匹配,是不是合适的问题,而幸运的是,对于现在这个功能,正好有一个合适的轮子,那就是第一步当然还是安装:

pip3.6 install Flask-Moment

然后进行注册:

from flask_moment import Moment
...
moment=Moment()

def create_app():
    ...
    moment.init_app(app)
    ...

用法非常简单,首先修改base.html代码,引入moment库(js库,其实完全可以自己使用cdn导入):

{%  block scripts %}
{{ super() }}
{{ moment.include_moment()}}
{%  endblock %}

然后修改index.html的时间的部分:

 <span class="text-right">发表于{{ moment( post.createtime).fromNow(refresh=True)}}</span>

refresh的意思为在不刷新的页面下,可以动态变化时间,运行一下看看效果:

可以看到,时间格式化已经完成,但是显示的是英文,显然不是我们需要的,国际化的方式也非常简单,修改base.html代码为:

{%  block scripts %}
{{ super() }}
{{ moment.include_moment()}}
{{ moment.lang("zh-CN") }}
{%  endblock %}

设置一下语言即可,运行显示:

正确显示时间,现在新发布一条,并且为了美观,在时间块前增加一个空格,显示结果为:

so good!关于moment还有更多的用法,具体请参阅相关文档

这一章终于写完了,再次感到对不起语文老师,不管怎么说,总算在周日之前完成,虽然上周食言了,尽量坚持吧!

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

推荐阅读更多精彩内容