码迷,mamicode.com
首页 > 其他好文 > 详细

Flask之十——博客文章

时间:2015-05-27 00:36:55      阅读:612      评论:0      收藏:0      [点我收藏+]

标签:

1. 提交和显示博客文章

app/models.py: 博客文章模型

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

class User(UserMixin, db.Model):
    # ...
    posts = db.relationship(Post, backref=author, lazy=dynamic)

 

app/main/forms.py:提交博客文章表单

class PostForm(Form):
    body = TextAreaField("What‘s on your mind?", validators=[Required()])
    submit = SubmitField(Submit)

 

app/main/views.py: 博客文章路由

# 提交和显示博客文章的首页路由
@main.route(/, methods=[GET, POST])
def index():
    form = PostForm()
    if current_user.can(Permission.WRITE_ARTICLES) and form.validate_on_submit():
        post = Post(body=form.body.data, uthor=current_user._get_current_object())
        db.session.add(post)
        return redirect(url_for(.index))
    posts = Post.query.order_by(Post.timestamp.desc()).all()
    return render_template(index.html, form=form, posts=posts)

# 在用户信息页显示博客的路由
@main.route(/user/<username>)
def user(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        abort(404)
    posts = user.posts.order_by(Post.timestamp.desc()).all()
    return render_template(user.html, user=user, posts=posts)

author 属性值为表达式 current_user._get_current_object()。
变量current_user 由 Flask-Login 提供,和所有上下文变量一样,也是通过线程内的代理对象实现。
这个对象的表现类似用户对象,但实际上却是一个轻度包装,包含真正的用户对象。
数据库需要真正的用户对象,因此要调用 _get_current_object() 方法。

 

app/templates/_posts.html: 显示博客文章的通用模板

<ul class="posts">
{% for post in posts %}
    <li class="post">
        <div class="profile-thumbnail">
            <a href="{{ url_for(‘.user‘, username=post.author.username) }}">
                <img class="img-rounded profile-thumbnail"
                    src="{{ post.author.gravatar(size=40) }}">
            </a>
        </div>
            <div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
            <div class="post-author">
            <a href="{{ url_for(‘.user‘, username=post.author.username) }}">
                {{ post.author.username }}
            </a>
        </div>
        <div class="post-body">{{ post.body }}</div>
    </li>
{% endfor %}
</ul>

 

app/templates/index.html: 显示博客文章的首页模板

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
...
<div>
{% if current_user.can(Permission.WRITE_ARTICLES) %}
    {{ wtf.quick_form(form) }}
{% endif %}
</div>

{% include ‘_posts.html‘ %}
...

 

app/templates/user.html: 显示博客文章的用户信息页模板

...
<h3>Posts by {{ user.username }}</h3>
{% include ‘_posts.html‘ %}
...

 

 

 

 

2. 分页显示长博客文章列表

app/main/views.py: 分页显示文章列表路由

@main.route(/, methods=[GET, POST])
def index():
    # ...
    page = request.args.get(page, 1, type=int)
    pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
            page, per_page=current_app.config[FLASK_POSTS_PER_PAGE],
            error_out = False)
    posts = pagination.items
    return render_template(index.html, form=form, posts=posts,
                            pagination=pagination)

渲染的页数从请求的查询字符串(request.args)中获取,如果没有明确指定,则默认渲染第一页。参数 type=int 保证参数无法转换成整数时,返回默认值。

为了显示某页中的记录,要把 all() 换成 Flask-SQLAlchemy 提供的 paginate() 方法。
页数是 paginate() 方法的第一个参数,也是唯一必需的参数。可选参数 per_page 用来指定每页显示的记录数量;如果没有指定,则默认显示 20 个记录。
另一个可选参数为 error_out,当其设为 True 时(默认值),如果请求的页数超出了范围,则会返回 404 错误;如果设为 False,页数超出范围时会返回一个空列表。
为了能够很便利地配置每页显示的记录数量,参数 per_page 的值从程序的环境变量 FLASKY_POSTS_PER_PAGE 中读取。

这样修改之后,首页中的文章列表只会显示有限数量的文章。若想查看第 2 页中的文章,要在浏览器地址栏中的 URL 后加上查询字符串 ?page=2。

 

添加分页导航

paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。这个对象包含很多属性,用于在模板中生成分页链接,因此将其作为参数传入了模板。

技术分享

 技术分享

 

app/templates/_macros.html: 分页模板宏

{% macro pagination_widget(pagination, endpoint) %}
<ul class="pagination">
    <li{% if not pagination.has_prev %} class="disabled"{% endif %}>
        <a href="{% if pagination.has_prev %}{{ url_for(endpoint, page = pagination.page - 1, **kwargs) }}{% else %}#{% endif %}">
            &laquo;
        </a>
    </li>
    {% for p in pagination.iter_pages() %}
        {% if p %}
            {% if p == pagination.page %}
            <li class="active">
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
            </li>
            {% else %}
            <li>
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
            </li>
            {% endif %}
        {% else %}
            <li class="disabled"><a href="#">&hellip;</a></li>
        {% endif %}
    {% endfor %}
    <li{% if not pagination.has_next %} class="disabled"{% endif %}>
        <a href="{% if pagination.has_next %}{{ url_for(endpoint, page = pagination.page + 1, **kwargs) }}{% else %}#{% endif %}">
            &raquo;
        </a>
    </li>
</ul>
{% endmacro %}

 

app/templates/index.html:在文章列表下面添加分页导航

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
...
{% include ‘_posts.html‘ %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, ‘.index‘) }}
</div>
{% endif %}

 

 

 

 

3. 使用 Markdown 和 Flask-PageDown 支持富文本文章

  • PageDown:使用 JavaScript 实现的客户端 Markdown 到 HTML 的转换程序
  • Flask-PageDown:为 Flask 包装的 PageDown,把 PageDown 集成到 Flask-WTF 表单中
  • Markdown:使用 Python 实现的服务器端 Markdown 到 HTML 的转换程序
  • Bleach:使用 Python 实现的 HTML 清理器

 

3.1 使用 Flask-PageDown

app/__init__.py: 初始化Flask-PageDown在客户端预览富文本

from flask.ext.pagedown import PageDown
# ...
pagedown = PageDown()
# ...
def create_app(config_name):
    # ...
    pagedown.init_app(app)
    # ...

若想把首页中的多行文本控件转换成 Markdown 富文本编辑器,PostForm 表单中的 body 字段要进行修改。

Flask-PageDown 定义了一个 PageDownField 类, 这个类和WTForms 的TextAreaField 接口一致。

 

app/main/forms.py: 启用 Markdown 的文章表单

from flask.ext.pagedown.fields import PageDownField

class PostForm(Form):
    body = PageDownField("What‘s on your mind", validators=[Required()])
    submit = SubmitField(Submit)

Markdown 预览使用 PageDown 库生成,因此要在模板中修改。Flask-PageDown 简化了这提供了一个模板宏,从 CDN 中加载所需文件。

 

app/index.html: Flask-PageDown 模板声明

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

 

3.2 在服务器上处理富文本

提交表单后,POST 请求只会发送纯 Markdown 文本,页面中显示的 HTML 预览会被丢掉。因为和表单一起发送生成的 HTML 预览有安全隐患,攻击者轻易就能修改 HTML 代码,让其和 Markdown 源不匹配,然后再提交表单。
为安全起见,只提交 Markdown 源文本,在服务器上使用 Markdown(使用 Python 编写的 Markdown 到 HTML 转换程序)将其转换成 HTML。得到 HTML 后,再使用 Bleach 进行清理,确保其中只包含几个允许使用的HTML 标签。

把 Markdown 格式的博客文章转换成 HTML 的过程可以在 _posts.html 模板中完成,但这么做效率不高,因为每次渲染页面时都要转换一次。
为了避免重复工作,我们可在创建博客文章时做一次性转换。转换后的博客文章 HTML 代码缓存在 Post 模型的一个新字段中,在模板中可以直接调用。
文章的 Markdown 源文本还要保存在数据库中,以防需要编辑。

 

app/models.py: 在Post模型中处理Markdown文本

from markdown import markdown
import bleach

class Post(db.Model):
    # ...
    body_html = db.Column(db.Text)

    # ...
    @staticmethod
    def on_changed_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_changed_body)

on_changed_body 函数注册在 body 字段上,是 SQLAlchemy“set”事件的监听程序,这意味着只要这个类实例的 body 字段设了新值,函数就会自动被调用。
on_changed_body 函数把 body 字段中的文本渲染成 HTML 格式,结果保存在 body_html 中,自动且高效地完成Markdown 文本到 HTML 的转换。

真正的转换过程分三步完成。
首先,markdown() 函数初步把 Markdown 文本转换成 HTML。
然后,把得到的结果和允许使用的 HTML 标签列表传给 clean() 函数。clean() 函数删除所有不在白名单中的标签。
转换的最后一步由 linkify() 函数完成,这个函数由 Bleach 提供,把纯文本中的 URL 转换成适当的 <a> 链接。
最后一步是很有必要的,因为 Markdown规范没有为自动生成链接提供官方支持。PageDown 以扩展的形式实现了这个功能,因此在服务器上要调用 linkify() 函数。

最后还要把博客模板上的post.body 替换成 post.body.html


app/templates/_posts.html: 显示富文本的文章显示模板

...
<div class="post-body">
    {% if post.body_html %}
        {{ post.body_html | safe }}
    {% else %}
        {{ post.body }}
    {% endif %}
</div>
...

 

 

 

 

4. 博客文章的固定链接

app/main/views.py: 文章的固定链接路由

@main.route(/post/<int:id>)
def post(id):
    post = Post.query.get_or_404(id)
    return render_template(post.html, posts=[post])

注意这个posts传入列表,因为 _posts.html模板要求posts为列表

 

app/templates/post.html: 文章固定链接模板

{% extends "base.html" %}

{% block title %}Flasky - Post{% endblock %}

{% block page_content %}
{% include ‘_posts.html‘ %}
{% endblock %}

 

app/templates/_posts.html: 在_posts.html下方添加固定链接

<ul class="posts">
    {% for post in posts %}
    <li class="post">
        ...
        <div class="post-content">
            ...
            <div class="post-footer">
                <a href="{{ url_for(‘.post‘, id=post.id) }}">
                    <span class="label label-default">Permalink</span>
                </a>
            </div>
        </div>
    </li>
    {% endfor %}
</ul>

 

 

 

 

5. 博客文章编辑器

app/main/views.py: 编辑文章的路由

@main.route(/edit/<int:id>, methods=[GET, POST])
@login_required
def edit(id):
    post = Post.query.get_or_404(id)
    if current_user != post.author and not current_user.can(Permission.ADMINISTER):
        abort(403)
    form = PostForm()
    if form.validate_on_submit():
        post.body = form.body.data
        db.session.add(post)
        flash(The post has been updated)
        return redirect(url_for(post, id=post.id))
    form.body.data = post.body
    return render_template(edit_post.html, form=form)

 

app/templates/edit_post.html:编辑文章的模板

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

{% block title %}Flasky - Edit Post{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Edit Post</h1>
</div>
<div>
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

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

 

在每篇文章的下面、固定链接的旁边添加一个指向编辑页面的链接

app/templates/_posts.html: 编辑文章的链接

<ul class="posts">
    {% for post in posts %}
    <li class="post">
    ...
        <div class="post-content">
        ...
            <div class="post-footer">
                ...
                {% if current_user == post.author %}
                <a href="{{ url_for(‘.edit‘, id=post.id) }}">
                    <span class="label label-primary">Edit</span>
                </a>
                {% elif current_user.is_administrator() %}
                <a href="{{ url_for(‘.edit‘, id=post.id) }}">
                    <span class="label label-danger">Edit [Admin]</span>
                </a>
                {% endif %}
            </div>
        </div>
    </li>
    {% endfor %}
</ul>

 

 

 

 

补充:创建虚拟博客文章数据

为了区分生产环境的依赖和开发环境的依赖,我们可以把文件 requirements.txt 换成 requirements 文件夹,它们分别保存不同环境中的依赖。
在 requirements 中,可以创建一个 dev.txt文件,列出开发过程中所需的依赖,再创建一个 prod.txt 文件,列出生产环境所需的依赖。
由于两个环境所需的依赖大部分是相同的,因此可以创建一个 common.txt 文件,在 dev.txt和 prod.txt 中使用 -r 参数导入。
requirements/dev.txt:开发所需的依赖文件

-r common.txt
ForgeryPy==0.1

 

app/models.py: 生成虚拟用户和博客文章

class User(UserMixin, db.Model):
    # ...
    @staticmethod
    def generate_fake(count=100):
        form sqlalchemy.exc import IntegrityError
        from random import seed
        import forgery_py

        seed()
        for i in range(count):
            u = User(email=forgery_py.internet.email_address(),
                    username=forgery_py.internet.user_name(True),
                    password=forgery_py.lorem_ipsum.word(),
                    confirmed=True,
                    name=forgery_py.name.full_name(),
                    location=forgery_py.email_address.city(),
                    about_me=forgery_py.lorem_ipsum.sentence(),
                    member_since=forgery_py.data.data(True))
            db.session.add(u)
            try:
                db.session.commit()
            except IntegrityError:
                db.session.rollback()

class Post(db.Model):
    # ...
    @staticmethod
    def generate_fake(count=100):
        from random import seed, randint
        import forgery_py

        seed()
        user_count = User.query.count()
        for i in range(count):
            u = User.query.offset(randint(0, user_count-1)).first()
            p = Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),
                    timestamp=forgery_py.date.date(True),
                    author=u)
            db.session.add(p)
            ab.session.commit()

用户的电子邮件地址和用户名必须是唯一的,但 ForgeryPy 随机生成这些信息,因此有重复的风险。如果发生了这种不太可能出现的情况,提交数据库会话时会抛出IntegrityError 异常。
这个异常的处理方式是,在继续操作之前回滚会话。在循环中生成重复内容时不会把用户写入数据库,因此生成的虚拟用户总数可能会比预期少。

随机生成文章时要为每篇文章随机指定一个用户。为此,使用 offset() 查询过滤器。这个过滤器会跳过参数中指定的记录数量。通过设定一个随机的偏移值,再调用 first()方法,就能每次都获得一个不同的随机用户。

在shell创建虚拟数据

(venv) $ python manage.py shell
>>> User.generate_fake(100)
>>> Post.generate_fake(100)

 

 

 

2015-05-26

Flask之十——博客文章

标签:

原文地址:http://www.cnblogs.com/whuyt/p/4532016.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!