• トップ
  • ブログ一覧
  • 【第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...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background