【第4回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【モデルの構築】
IT技術
第4回~モダンなフレームワークの使い方を学びながらブログシステムを構築~
連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第4回目です。
前回は、「管理者ページの整備」まで行いました。
今回は、「ブログシステムのデータベース」を解説していきます。
ブログシステムのデータベースを作ろう!
今回は、いよいよ Firestore を使って、「ブログシステムのデータベース」を作ります。
以下のポイントを押さえながら解説していきます。
- Firestore と Python の連携はどうやるの?
- ブログシステムには何が必要?
それでは、さっそく見ていきましょう!
モデルの作成
まずはモデルを扱う、models.py を作成しましょう。
ここで、「記事」や「カテゴリ」といったモデルを作成し、「Firestore に追加していく機能」を実装していきます。
実際には「コントローラ側で、モデルをもとにデータを作成する」形になります。
まずは、試しに作ってみましょう。
Category
まずは、「カテゴリのモデル」作成から。
カテゴリは、以下の要素で構成されます。
- 主キーとなる ID
- 名前
- スラッグ
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 %}
44 未
45 {% 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つです。
- 公開済みの記事に直接飛べるようにする
- サイドメニューに「カテゴリ」と「タグ」の編集リンクを追加(後ほど実装します)
- テーブルのヘッダーに「公開」を追加
確認
実際に、管理者ページにアクセスしてみると、
うまく動作しています!
第5回へつづく!
今回は、データベースにおける「Firestore と Python の連携部分」を実装してみました。
管理者ページもだいぶ整いましたが、「記事の追加」「編集」などの実装はまだです。
まだまだブログシステム完全構築には程遠いですね…。
次回は「記事個別ページ」を作成していきます。
道のりは長いですが、次回も頑張って実装していきましょう!
次回の記事はこちら
第1回はこちら
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit
おすすめ記事
immichを知ってほしい
2024.10.31