1. 程式人生 > >記錄一次完整的flask小型應用開發(4)

記錄一次完整的flask小型應用開發(4)

建立部落格列表

現在所有的東西都準備好了,可以開始建立部落格引擎。
首先我們需要建立一個新的部落格模型Post:

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'))
並且在user模型中建立relationship

我們準備在首頁直接展示使用者的部落格,所以也在首頁頁面顯示一個表單,用於寫部落格,還有一個提交按鈕:
我們先建立表單:main/forms.py

class PostForm(FlaskForm):
    # 用於首頁寫部落格的表單
    body = TextAreaField('wirte your blog here !', validators=[DataRequired()])
    submit = SubmitField('submit')

接下來修改主頁面的路由,之前寫的程式碼是測試小功能用的,所以可以直接替換為:

# 這個路由用於主頁顯示部落格列表,並且在列表上方顯示一個寫部落格表單
@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,
                    author=current_user._get_current_object())
        # current_user由flask login提供,通過執行緒內的代理物件實現
        # 資料庫需要真正的使用者物件,所以使用current_user._get_current_object()
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('.index'))
    posts = Post.query.order_by(Post.timestamp.desc()).all()
    return render_template('index.html', form=form, posts=posts)

老樣子,我們實現了路由功能,還需要渲染模板:

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1>
</div>

<div>
    {% if current_user.can(Permission.WRITE_ARTICLES) %}
        {{ wtf.quick_form(form) }}
    {% endif %}
</div>

<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-content">
            # 部落格釋出日期,計算的是釋出日期距離今天有幾天
            <div class="post-date">{{ moment(post.timestamp).formNow() }}</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>
        </div>
    </li>
    {% endfor %}
</ul>
{% endblock %}

然後為了讓頁面更美化,我們用上了css來佈局:

.profile-thumbnail {
    position: absolute;
}
.profile-header {
    min-height: 260px;
    margin-left: 280px;
}
ul.posts {
    list-style-type: none;
    padding: 0px;
    margin: 16px 0px 0px 0px;
    border-top: 1px solid #e0e0e0;
}
ul.posts li.post {
    padding: 8px;
    border-bottom: 1px solid #e0e0e0;
}
ul.posts li.post:hover {
    background-color: #f0f0f0;
}
div.post-date {
    float: right;
}
div.post-author {
    font-weight: bold;
}
div.post-thumbnail {
    position: absolute;
}
div.post-content {
    margin-left: 48px;
    min-height: 48px;
}

在base.html中加上關聯css:

<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">

改變各種小bug後,我們可以看到正常的頁面了!!!
現在我們成功地在主頁上顯示了所有的部落格,下面我們實現在使用者個人主頁頁面上顯示所有的部落格文章,所以我們在渲染user.html模板的時候,我們多傳入一個引數即user.posts,找到這個使用者的所有部落格,然後在user.html加上我們之前在主頁面的部落格列表即可:

# 為每個使用者定義個人資料頁面路由
@main.route('/user/<username>')
def user(username):
    user = User.query.filter_by(username=username).first()
    # 在資料庫中搜索URL指定的使用者名稱
    if user is None:
        abort(404)
    posts = user.posts.order_by(Post.timestamp.desc()).all()
    return render_template('user.html', user=user, posts=posts)

部落格列表分頁

現在我們考慮到部落格列表可能會非常長,所以一個頁面顯示所有的部落格是不現實也不美觀的,為了方便測試,我們需要建立虛擬部落格文章資料,這樣資料庫內就有大量資料供我們測試:這裡我們使用ForgeryPy或者faker第三方庫。
所以我們在app下面建立fake.py檔案定義函式用於生成虛擬資料:然後發現報錯,cannot import name current_app!!!!!

嘗試另一種方法:在models的類裡面寫入靜態方法:

@staticmethod
    # 這是靜態方法,即此類不需要例項化就可以呼叫這個方法
    def generate_fake_user(count=100):
        fake = Faker()
        i = 0
        while i < count:
            u = User(email=fake.email(),
                     username=fake.user_name(),
                     password='password',
                     confirmed=True,
                     name=fake.name(),
                     location=fake.city(),
                     about_me=fake.text(),
                     member_since=fake.past_date())
            db.session.add(u)
            try:
                db.session.commit()
                i += 1
            except IntegrityError:
                db.session.rollback()
@staticmethod
    def generate_fake_post(count=100):
        fake = Faker()
        user_count = User.query.count()
        for i in range(count):
            u = User.query.offset(randint(0, user_count - 1)).first()
            # 隨機文章生成的時候,我們需要隨機指定一個使用者來擁有這篇文章
            # 使用offset()查詢過濾器,會跳過引數中指定的記錄數量,所以會獲得隨機的使用者
            p = Post(body=fake.text(),
                     timestamp=fake.past_date(),
                     author=u)
            db.session.add(p)
        db.session.commit()

然後執行 python manage.py shell
shell內執行:Post.generate_fake_post(100)
發現報錯說Post是undefined,然後修改manage.py檔案加上Post:

def make_shell_context():
    return dict(db=db, User=User, Role=Role, Post=Post)

因為現在是分頁部落格列表,所以我們需要修改首頁的路由:

# 這個路由用於主頁顯示部落格列表,並且在列表上方顯示一個寫部落格表單
@main.route('/', methods=['GET', 'POST'])
def index():
    form = PostForm()
    if current_user.can(Permission.WRITE) and form.validate_on_submit():
        post = Post(body=form.body.data,
                    author=current_user._get_current_object())
        # current_user由flask login提供,通過執行緒內的代理物件實現
        # 資料庫需要真正的使用者物件,所以使用current_user._get_current_object()
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('.index'))
    # posts = Post.query.order_by(Post.timestamp.desc()).all()
    # 下面將上一行修改為分頁顯示所有部落格
    page = request.args.get('page', 1, type=int)
    # 渲染的頁數從請求的查詢字串request.args中獲取,預設渲染第一頁
    pagination = Post.query.order_by(Post.timestamp.desc()).\
        paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    # 這裡把all()換成了sqlalchemy中的paginate()方法
    # 頁數是第一個引數,也是唯一必須的引數
    # per_page顯示一頁顯示的個數,預設是顯示20個記錄
    # 最後一個引數用於如果請求的頁數超出了請求範圍,那麼404
    posts = pagination.items
    return render_template('index.html', form=form, posts=posts, Permission=Permission, pagination=pagination)

現在成功分頁,如果要訪問第二頁則URL加上?page=2
但是我們肯定是需要一個底部的分頁導航來跳轉到不同的頁面。
paginate()方法返回一個Pagination物件,裡面有很多屬性,用於生成分頁連結。

所有這個強大的物件以及分頁css類,可以讓我們完成一個分頁模板巨集:

{% 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>
    
    # 這裡中間部分用於顯示所有的頁面數字
    # iter_pages()迭代器返回所有的頁面連結
    {% 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 %}

然後我們將這個模板放在index.html和user.html的後面來渲染出這個分頁導航:

{% import "_macros.html" as macros %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.index')}}
</div>

成功,下面正確顯示出分頁導航欄!!!!

使用MarkDown編輯器

要想實現這個功能我們需要安裝一些額外的包
pip install flask-pagedown markdown bleach
首先我們初始化這個拓展:

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

然後我們需要把首頁中的編輯器轉換成MarkDown富文字編輯器:
body = PageDownField('wirte your blog here !', validators=[DataRequired()])

最後,我們需要使用預覽功能,因此我們需要在模板中修改,拓展直接提供了一個模板巨集,直接CDN載入即可。

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

給每一個文章新增單獨URL

首先製作單獨連結的路由功能:

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

老樣子,單獨頁面需要模板:

{% extends "base.html" %}
{% block page_content %}
<ul class='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-content">
            <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>
        </div>
    </li>
</ul>
{% endblock %}

那怎麼樣才會使用這個功能呢,如何進入這個模板頁面呢,我們在index.html的部落格列表裡面,將部落格文字部分套上a標籤,然後使用url_for()方法呼叫這個路由就可以啦!

部落格文章編輯器

首先我們來實現這個功能的路由:

@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:
        abort(403)
    form = PostForm()
    if form.validate_on_submit():
        post.body = form.body.data
        db.session.add(post)
        db.session.commit()
        flash('you have updated your blog')
        return redirect(url_for('main.post', id=post.id))
    form.body.data = post.body
    return render_template('edit_post.html', form=form)

展示頁面的模板:edit_post.html

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

{% block title %}Edit your blog{% endblock %}

{% block page_content %}
<div>
    {% if current_user.can(Permission.WRITE) %}
        {{ wtf.quick_form(form) }}
    {% endif %}
</div>
{% endblock %}

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

然後在主頁面的每一個部落格條目上新增一個edit按鈕,即可以呼叫和這個路由跳轉到編輯頁面上!