• トップ
  • ブログ一覧
  • 【第4回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【モデルの構築】
  • 【第4回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【モデルの構築】

    広告メディア事業部広告メディア事業部
    2020.07.30

    IT技術

    第4回~モダンなフレームワークの使い方を学びながらブログシステムを構築~

    連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第4回目です。

    前回は、「管理者ページの整備」まで行いました。

    featureImg2020.07.22【第3回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【管理者ページ整備】~第3回~モダンなフレームワークの使い方を学びながらブログシステムを構築連載「Python Responder + F...

    今回は、「ブログシステムのデータベース」を解説していきます。

    ブログシステムのデータベースを作ろう!

    今回は、いよいよ Firestore を使って、「ブログシステムのデータベース」を作ります。

    以下のポイントを押さえながら解説していきます。

    1. Firestore と Python の連携はどうやるの?
    2. ブログシステムには何が必要?

    それでは、さっそく見ていきましょう!

    モデルの作成

    まずはモデルを扱う、models.py を作成しましょう。

    ここで、「記事」や「カテゴリ」といったモデルを作成し、「Firestore に追加していく機能」を実装していきます。

    実際には「コントローラ側で、モデルをもとにデータを作成する」形になります。

    まずは、試しに作ってみましょう。

    Category

    まずは、「カテゴリのモデル」作成から。

    カテゴリは、以下の要素で構成されます。

    1. 主キーとなる ID
    2. 名前
    3. スラッグ

    Firestore と Python の連携はどうやる?

    Firestore と Python の連携は、一般的に「構築したクラスを Dict に変換して追加」という方法で行います。

    したがって、この様な実装をしてみました。

    1# models.py
    2from datetime import datetime
    3from init_db import db
    4
    5
    6class Category(object):
    7    def __init__(self, name: str, slug: str):
    8        """
    9        Create Category
    10        :param name:  category name
    11        :param slug:  category slug
    12        """
    13        # slugの被りがあるか調査
    14        try:
    15            if slug in get_category_slugs():
    16                raise ValueError('Error: The slug you gave has already been existed.')
    17        except ValueError as e:
    18            print(e)
    19            exit(-1)
    20
    21        self.name = name
    22        self.slug = slug
    23
    24    def to_dict(self):
    25        data = self.__dict__
    26        return data
    27
    28    def add(self):
    29        """ Add data to Firestore """
    30        db.collection('categories').add(self.to_dict())

    ID の設定

    ID は、動的に被りがない様にしましょう。

    Cloud Firestore は、データを追加する際、「指定しなければ、ランダムに ID を割り当ててくれる」ので、これを使います。

    スラッグの設定

    また、ID 同様、スラッグも被りがない様にします。

    以下のように、「Firestore 内データのスラッグ一覧を取得する関数」を用意しておきます。

    1# models.py
    2def get_category_slugs():
    3    return [cat.to_dict()['slug'] for cat in db.collection('categories').stream()]
    4
    5def get_tag_slugs():
    6    return [cat.to_dict()['slug'] for cat in db.collection('tags').stream()]
    7
    8
    9def get_article_slugs():
    10    return [cat.to_dict()['slug'] for cat in db.collection('articles').stream()]

    コレクションからstream() で得たデータは、そのままでは使い勝手が悪いです。

    そのため、「辞書型変数」に直してから、スラッグだけのリストを作りましょう!

    Tag

    タグも同様です。

    1# models.py
    2class Tag(object):
    3    def __init__(self, name: str, slug: str):
    4        """
    5        Create Tag
    6        :param name:
    7        :param slug:
    8        """
    9        # slugの被りがあるか調査
    10        try:
    11            if slug in get_category_slugs():
    12                raise ValueError('Error: The slug you gave has already been existed.')
    13        except ValueError as e:
    14            print(e)
    15            exit(-1)
    16
    17        self.name = name
    18        self.slug = slug
    19
    20    def to_dict(self):
    21        data = self.__dict__
    22        return data
    23
    24    def add(self):
    25        db.collection('tags').add(self.to_dict())

    Article

    記事は、以下のフィールドを持たせます。

    id一位に定まるキー
    title記事タイトル
    thumbnailサムネイル画像
    contents記事内容
    description詳細・記事抜粋
    author著者
    slugスラッグ (これも一意に定まるもの)
    categoryカテゴリ (1つ)
    tagsタグ (複数個可)
    released公開状態

    今回、「カテゴリは1つ」「タグは複数個」持てるようにします。

    その他は、先ほどの2つと大きな違いはありません。

    1# models.py
    2class Article(object):
    3    def __init__(self,
    4                 title: str,
    5                 thumbnail: str,
    6                 contents: str,
    7                 description: str,
    8                 author: str,
    9                 slug: str,
    10                 category: str,
    11                 tags: list,
    12                 released: bool = False
    13                 ):
    14        """
    15        Create article
    16        :param title:       タイトル
    17        :param thumbnail:    サムネイル
    18        :param contents:    記事内容
    19        :param description: 詳細・抜粋
    20        :param author:      著者
    21        :param slug:        スラッグ
    22        :param category:    カテゴリ
    23        :param tags:        タグ
    24        :param released:    公開設定
    25        """
    26        # slugの被りがあるか調査
    27        try:
    28            if slug in get_article_slugs():
    29                raise ValueError('Error: The slug you gave has already been existed.')
    30        except ValueError as e:
    31            print(e)
    32            exit(-1)
    33
    34        self.title = title
    35        self.thumbnail = thumbnail
    36        self.contents = contents
    37        self.description = description
    38        self.author = author
    39        self.slug = slug
    40        self.category = category
    41        self.tags = tags
    42        self.last_update = datetime.now()
    43        self.released = released
    44
    45    def to_dict(self):
    46        data = self.__dict__
    47        return data
    48
    49    def add(self):
    50        db.collection('articles').add(self.to_dict())

    サンプルデータを追加してみる

    それでは、動作確認がてらデータを追加してみましょう!

    1from datetime import datetime
    2from init_db import db
    3
    4
    5class Category(object):
    6    # ~~~省略~~~ #
    7
    8class Tag(object):
    9    # ~~~省略~~~ #
    10
    11class Article(object):
    12    # ~~~省略~~~ #
    13
    14if __name__ == '__main__':
    15    # テストコード
    16
    17    cat1 = Category('お知らせ', 'news')
    18    cat2 = Category('技術', 'technology')
    19    cat1.add()
    20    cat2.add()
    21
    22    tag1 = Tag('tag1', 'tag1')
    23    tag2 = Tag('tag2', 'tag2')
    24    tag1.add()
    25    tag2.add()
    26
    27    art = Article(
    28        title='ブログを開設しました!',
    29        thumbnail='![画像の代替テキスト](画像パス)',
    30        contents='## マークダウン形式のコンテンツ',
    31        description='ブログを開設しました。ここは記事の詳細で、抜粋としても使われます。',
    32        author='rightcode',
    33        slug='create-blog',
    34        category='news',
    35        tags=['tag1', 'tag2'],
    36    )
    37    art.add()

    マークダウン形式で書いておこう

    執筆はマークダウン形式にしたいので、ここでも、マークダウン形式で記事内容を書いておきます。

    今は、とりあえず適当なデータを格納しておきましょう。

    実行!

    実行してみると、以下の様にデータが追加されました!

    データが追加できた

    管理者ページに反映させる

    これで、データベースに記事ができました。

    次は、「管理者ページ」もとい「投稿一覧ページ」に、反映させましょう。

    関数を作成

    まずは、models.py に、記事をList[Dict] で取得できる関数を作っておきます。

    1# models.py
    2from google.cloud.firestore import Query
    3
    4
    5def get_articles(unreleased: bool = True):
    6    """
    7    Get articles
    8    :param unreleased: True => all articles   False => released articles
    9    :return:
    10    """
    11    if unreleased:
    12        articles = [art.to_dict() 
    13                    for art in 
    14                    db.collection('articles').order_by(
    15                        'last_update', 
    16                        direction=Query.DESCENDING
    17                    ).stream()
    18                    ]
    19    else:  # 実は以下のコードはダメ : 理由は連載後半で解説
    20        articles = [art.to_dict() 
    21                    for art in 
    22                    db.collection('articles').where('released', '==', True).order_by(
    23                        'last_update', 
    24                        direction=Query.DESCENDING
    25                    ).stream()
    26                    ]
    27
    28    return articles

    諸々の仕様を設定

    あとで使い回せるよう、公開済みか否か」によって、取得する記事を変更する様にしました。

    また、order_by('last_update', direction=Query.DESCENDING) で日付降順で取り出すようにしています。

    ビューに投げる処理を追記

    そうしたら、controllers.py 側からこれを呼び出し、ビューに投げる処理を追記します。

    1# controllers.py
    2from models import *
    3
    4# ~~~省略~~~ #
    5
    6@api.route('/admin')
    7class Admin:
    8    async def on_get(self, req, resp):
    9        # ログインしてなければ
    10        if req.cookies.get('session') is None:
    11            api.redirect(resp, '/login')
    12
    13        # ログイン済み
    14        else:
    15            articles = get_articles()  # [New!]
    16            resp.html = api.template('admin.html',
    17                                     title='管理者ページ',
    18                                     name=req.cookies.get('username'),
    19                                     articles=articles)
    20
    21    async def on_post(self, req, resp):
    22        # POSTデータを取得
    23        data = await req.media()
    24        email = data['email']
    25        password = data['password']
    26
    27        # 認証
    28        res, session = _login(email, password, COOKIE_EXPIRES)
    29
    30        if 'error' not in res:
    31            # クッキーにログイン情報をセット
    32            expires = datetime.now() + timedelta(COOKIE_EXPIRES)
    33            resp.set_cookie(key='session', value=session, expires=expires)
    34            resp.set_cookie(key='username', value=res['displayName'], expires=expires)
    35            resp.set_cookie(key='email', value=email, expires=expires)
    36
    37            # 認証成功ならば管理者ページへ
    38            articles = get_articles()  # New
    39            resp.html = api.template('admin.html',
    40                                     title='管理者ページ',
    41                                     name=res['displayName'],
    42                                     articles=articles)
    43        else:
    44            # 認証失敗ならばエラーメッセージをログイン画面に渡してリダイレクト
    45            api.redirect(resp, '/login?error={}'.format(res['error']['errors'][0]['message']))

    ビューを変更

    最後に、ビューも変更しましょう。

    1<!--
    2templates/admin.html
    3管理者ページ
    4-->
    5
    6{% extends "layout.html" %}
    7{% block content %}
    8
    9<br>
    10<h1>My Blog Name | 管理者ページ</h1>
    11<p>こんにちは,{{ name }} さん</p>
    12
    13<div class="main-container">
    14    <div class="admin-main-menu">
    15        <h2>投稿一覧</h2>
    16        <table>
    17            <tr>
    18                <th style="width:  5%;">#</th>
    19                <th style="width: 35%;">タイトル</th>
    20                <th style="width: 10%;">作成者</th>
    21                <th style="width: 10%;">スラッグ</th>
    22                <th style="width: 10%;">カテゴリ</th>
    23                <th style="width: 10%;">タグ</th>
    24                <th style="width: 15%;">最終更新日</th>
    25                <th style="width:  5%;">公開</th>
    26            </tr>
    27            {% for art in articles %}
    28            <tr>
    29                <td>{{ loop.index-1 }}</td>
    30                <td>{{ art['title'] }}</td>
    31                <td>{{ art['author'] }}</td>
    32                <td>{{ art['slug'] }}</td>
    33                <td>{{ art['category'] }}</td>
    34                <td>
    35                    {% for tag in art['tags'] %}
    36                    {{ tag }},
    37                    {% endfor %}
    38                </td>
    39                <td>{{ art['last_update'].strftime('%Y.%m.%d') }}</td>
    40                <td>
    41                    {% if art['released'] %}
    42                    <a href="/category/{{ art['category'] }}/{{ art['slug'] }}"></a>
    43                    {% else %}
    4445                    {% endif %}
    46                </td>
    47            </tr>
    48            {% endfor %}
    49
    50        </table>
    51    </div>
    52    <div class="admin-side-menu">
    53        <h2>Menu</h2>
    54        <ul>
    55            <li><a href="/admin/profile">プロフィール確認・編集</a></li>
    56            <li><a href="/admin/new">新規追加</a></li>
    57            <li><a href="/admin">投稿一覧</a></li>
    58            <li><a href="/admin/category">カテゴリ</a></li>
    59            <li><a href="/admin/tag">タグ</a></li>
    60            <li><a href="/logout">ログアウト</a></li>
    61        </ul>
    62    </div>
    63</div>
    64
    65{% endblock %}

    ポイントは、以下の3つです。

    1. 公開済みの記事に直接飛べるようにする
    2. サイドメニューに「カテゴリ」と「タグ」の編集リンクを追加(後ほど実装します)
    3. テーブルのヘッダーに「公開」を追加

    確認

    実際に、管理者ページにアクセスしてみると、

    管理者ページにデータが反映された

    うまく動作しています!

    第5回へつづく!

    今回は、データベースにおける「Firestore と Python の連携部分」を実装してみました。

    管理者ページもだいぶ整いましたが、「記事の追加」「編集」などの実装はまだです。

    まだまだブログシステム完全構築には程遠いですね…。

    次回は「記事個別ページ」を作成していきます。

    道のりは長いですが、次回も頑張って実装していきましょう!

    次回の記事はこちら

    featureImg2020.07.30【第5回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事個別ページ作成】第5回~モダンなフレームワークの使い方を学びながらブログシステムを構築~連載「Python Responder + F...

    第1回はこちら

    featureImg2020.07.17【第1回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【初期セットアップ編】モダンなフレームワークの使い方を学びながらブログシステムを構築今回より、また新たに Python WebAPI 関連の...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事