【第7回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【メディア管理と非同期処理】
IT技術
第7回~モダンなフレームワークの使い方を学びながらブログシステムを構築~
連載「Python Responder + Firestoreでモダンかつサーバーレスなブログシステムを作ってみる」第7回目です。
前回は、記事作成部分を作りました。
メディア管理の整備をしよう
今回はひきつづき、管理者ページの整備をしていきます。
具体的には、「メディアのアップロード部分」を実装しようと思います!
メディアのアップロードページ
まずは、メディアのアップロードページ「/admin/media」を作りましょう。
アップロードに必要な機能
メディアをアップロードするには、以下の機能が必要です。
- ファイルアップロード
- 今あるメディア一覧
- 画像タグ (マークダウン形式) のコピー
画像パスを取得する関数を作る
ビューを作る前に、あらかじめ「画像一覧を取得する関数」を作っておきましょう。
やることは、いたって簡単です。
「/static/images/」のファイルを全て取得し、そのパスをリストで返すだけです。
降順でソートする
ゆくゆくは、日付が新しい順で表示させたいので、降順でソートしておきます。
(ファイル接頭辞に日付を置くためです)
1# controllers.py
2def get_images(image_path='static/images/'):
3 """ Get images existing in the directory, [image_path] """
4 files = os.listdir(image_path)
5 extensions = ['.png', '.jpg', '.bmp', 'gif'] # 画像拡張子
6 images = [f'{image_path}{f}'
7 for f in files
8 if os.path.splitext(f)[-1] in extensions # 画像拡張子のみ取得
9 ]
10 # 降順 = 日付が新しい順
11 images.sort(reverse=True)
12
13 return images
ひとまず、png や jpg などの画像拡張子だけにしました。
ルーティングとビューの作成
ルーティング
ルーティングは、以下の様な感じです。
1# controllers.py
2@api.route('/admin/media')
3def media(req, resp):
4 if req.cookies.get('session') is None:
5 api.redirect(resp, '/login')
6
7 # ログイン済み
8 else:
9 # 今ある画像パスを全て取得
10 imgs = get_images()
11 resp.html = api.template('media.html',
12 name=req.cookies.get('username'),
13 imgs=imgs)
ビュー
次に、ビューを作ります。
1<!--
2templates/media.html
3-->
4
5{% extends "layout.html" %}
6{% block content %}
7
8<br>
9<h1>My Blog Name | 管理者ページ</h1>
10<p>こんにちは,{{ name }} さん</p>
11
12<div class="main-container">
13 <div class="admin-main-menu">
14 <h2>メディアのアップロード</h2>
15
16 <form id="fileupload" action="/admin/upload" method="post" enctype="multipart/form-data">
17 <input name="file" type="file"/>
18 <button type="submit">アップロード</button>
19 </form>
20
21 <h2>メディア一覧</h2>
22 <div class="media">
23 {% for img in imgs %}
24 <a onclick="copyToClipboard({{loop.index}})">
25 <img src="/{{img}}" alt="{{img}}">
26 </a>
27 <input type="text" id="copy-img-path{{loop.index}}" value="![image_name](/{{img}})" readonly>
28 {% endfor %}
29 </div>
30 </div>
31
32 {% include 'admin-side.html'%}
33
34</div>
35<script>
36 function copyToClipboard(num) {
37 let copyTarget = document.getElementById("copy-img-path"+num);
38 copyTarget.select();
39 document.execCommand("Copy");
40 alert("以下の画像タグをコピーしました!\n" +
41 "このまま記事にCtrl+Vしてください。\n" +
42 copyTarget.value +
43 "\nもし,代替テキストを変更する場合は [] 内を変更してください!");
44 }
45</script>
46{% endblock %}
1.media{
2 width: 100%;
3 background-color: #fff;
4 border: 2px solid #555555;
5 border-radius: 5px;
6}
7.media img{
8 height: 150px;
9 width: 150px;
10 object-fit: contain;
11 border: 1px solid gray;
12 background-color: white;
13 margin: 10px;
14}
15.media img:hover{
16 opacity: 0.7;
17}
18.media input{
19 opacity: 0;
20 position: absolute;
21}
Javascript でタグをワンクリックでコピー
ここで、「マークダウン形式のタグを、ワンクリックでクリップボードにコピー」するため、Javascript を使います。
jQuery などは使いません。
記述方法に注意!
その際、<input type="text" id="copy-img-path{{loop.index}}" value="![image_name](/{{img}})" readonly> の value をコピー対象にしています。
これを、単純にtype="hidden" や CSS でdisplay: none; と記述すると、上手く動作しません。
透明化でコード量を減らす
そのため、少々力技ですが、今回は「透明化」でコード量を少なくします。
1.media input{
2 opacity: 0;
3 position: absolute;
4}
上記のように、透明化して、重複を許可しました。
これでもしっかり動作してくれます。
サイドバーにページを追加
そうしたら、このページを、管理者ページのサイドバーに追加しておきましょう。
admin-side.html にまとめてしまい、必要なページにはインクルードする形にしておきます。
1<!-- admin-side.html -->
2<div class="admin-side-menu">
3 <h2>Menu</h2>
4 <ul>
5 <li><a href="/admin/profile">プロフィール確認・編集</a></li>
6 <li><a href="/admin/new">新規追加</a></li>
7 <li><a href="/admin">投稿一覧</a></li>
8 <li><a href="/admin/category">カテゴリ</a></li>
9 <li><a href="/admin/tag">タグ</a></li>
10 <li><a href="/admin/media" target="_blank">メディア</a></li>
11 <li><a href="/logout">ログアウト</a></li>
12 </ul>
13</div>
動作確認
それでは、動作するか確認してみましょう。
まずは、「/admin/media」にアクセス。
試しに、画像をクリックしてみます。
うまく動作しているようですね!
パスに関する注意点
本システムでは、パスの表記に「static/images/***」と「/static/images/***」の2種類を使い分けています。
これは、それぞれ Python 側と HTML / CSS 側で使い分けていますので、ご注意ください!
Python 側で「/static」と表記すると、サーバ側のファイルパスになってしまいます。
アップロード処理の実装
次に、「ファイルアップロード」について実装していきます。
先ほど説明した通り、ビュー側で「/admin/upload」に取得したファイルを投げるのです。
ポイント
押さえるべきポイントは、以下の2点です。
- ファイル名の重複を避けた名前づけ
- ファイルのアップロード機構
日付は接頭辞
1つ目のポイントは、ひとまず「日付(秒まで)を接頭辞」にすれば解決します。
ちなみに、以下のような手段もあるので、完璧なものを作りたい場合は参考にしてください。
- 日付の後ろに、ランダムな文字列を散りばめる
- 投降ユーザ名を入れる
アップロードはバックグラウンド処理
また、せっかくなので、ファイルアップロードはバックグラウンドで処理させましょう。
いざ、Responder で実装
これらを Responder で実装すると、以下のようになります。
1# controllers.py
2@api.route('/admin/upload')
3async def upload(req, resp):
4 if req.cookies.get('token') is None:
5 api.redirect(resp, '/login')
6
7 # ログイン済み
8 else:
9 # アップロードはバックグラウンドで
10 @api.background.task
11 def _upload(data, filename):
12 file = data['file']
13 f = open('static/images/{}'.format(filename), 'wb')
14 f.write(file['content'])
15 f.close()
16
17 data = await req.media(format='files')
18
19 # 画像の名前を被らない名前にする
20 # 今回は日付 + filename
21 # この接頭辞を使って最新の画像は一番上にする
22 now = datetime.today().strftime('%Y%m%d%H%M%S')
23
24 filename = data['file']['filename']
25 filename = f'{now}_{filename}'
26
27 # Upload
28 _upload(data, filename)
29
30 # media にリダイレクト
31 api.redirect(resp, '/admin/media')
このように@api.background.task というデコレータをつけるだけで、バックグラウンドで処理させることができます。
これが Responder の良いところですね!
動作確認
それでは、実際に動作を確認してみます!
適当に画像を選択して、アップロードしてみます。
良い感じですね!
新規記事投稿にサムネイルを追加できるようにする
これで、メディアアップロードが可能になりました。
最後に、記事のサムネイルを追加できるようにしましょう。
サムネイル画像も、同様にマークダウン形式で指定するようにします。
ビューにコード追加
まずは、ビューに以下のコードを追加します。
1<!-- new.html -->
2
3<!-- 省略 -->
4<div class="main-container">
5 <div class="admin-main-menu">
6 <h2>新規投稿</h2>
7 <div class="edit">
8 <form action="/admin/add" method="post">
9 <h3>サムネイル</h3>
10 <input type="text" name="thumbnail" value="" placeholder="![画像の代替テキスト](画像パス)">
11 <h3>タイトル</h3>
12 <input type="text" name="title" value="">
13
14<!-- 省略 -->
コントローラ修正
そして、コントローラを修正します。
1# controllers.py
2@api.route('/admin/add')
3class Add:
4 async def on_get(self, req, resp):
5 pass
6
7 async def on_post(self, req, resp):
8 data = await req.media()
9 title = data.get('title', '')
10 thumbnail = data.get('thumbnail', '') # 変更
11 description = data.get('description', '')
12 slug = data.get('slug', '').lower().replace(' ', '-') # 全て小文字かつ空白は置換
13 contents = data.get('contents', '')
14 category = data.get('category', '')
15 tags = data.get_list('tags')
16
17 # プレビュー の場合
18 if data.get('preview', None) is not None:
19 # マークダウンからHTMLへ
20 # tableはでデフォルトでは変換してくれないのでここで指定する
21 md = markdown.Markdown(extensions=['tables'])
22 html = md.convert(contents)
23
24 thumbnail = md.convert(thumbnail) # 追加 md => html
25
26 whats_new = get_whats_new()
27 categories = get_categories()
28 resp.html = api.template('preview.html',
29 title=title,
30 contents=html,
31 thumbnail=thumbnail,
32 category=category,
33 tags=tags,
34 whats_new=whats_new,
35 categories=categories
36 )
37 return
38
39 # ~~ 省略 ~~ #
プレビュー用のビューを修正
最後に、プレビュー用のビューを修正しましょう。
HTML 形式でサムネイルを得るので、オートエスケープを「無効」にして、展開します。
1<!-- preview.html 一部抜粋 -->
2<div class="article-main">
3
4 <!-- メイン -->
5 <div class="title"><h2>{{ title }}</h2></div>
6 <div class="contents">
7 <div class="info">
8 YYYY.mm.dd<br>
9 Author: {{ author }}
10 </div>
11 <a href="/category/{{category}}"><span class="category">🏷 {{ category }}</span></a>
12 {% autoescape false%}
13 <div class="thumbnail">{{ thumbnail }}</div>
14 </div>
15
16 <div class="contents">
17
18 {{ contents }}
19 {% endautoescape%}
20 </div>
21 </div>
これで完了です。
ここまでで、記事の新規追加機能は全て実装できました!
試しに記事を追加してみよう
次に、記事を追加してみましょう。
このとき、いろんなタグを織り交ぜてテスト投稿してみてください。
あとで記事のデザインを調節するのに役立ちます。
サンプル
筆者は、とりあえずこんな感じにしてみました。
1## 新しいブログ「My Blog Name」
2この度、ブログを開設しました。
3なんと、PythonとFirestoreで動作しています。
4
5### Responder
6ResponderはPythonのWebAPIです。
7バックエンドで処理され**Webページを動的に、高速に生成**してくれています。
8主な特徴は
9
10* 非同期処理が簡単に書ける
11* GraphQLをサポート
12* 扱いやすい!!
13
14インストールは
15
16```
17$ pip install responder
18```
19
20### Firestore
21FirestoreはGoogleが提供するアプリ向けプラットフォーム。
22公式によると、
23
24> Google の柔軟でスケーラブルな NoSQL クラウド データベースを使用して、クライアント側開発とサーバー側開発のデータを保存、同期します。
25
26だそう。
27
28ちなみにバージョンは
29
30| Tool | Version |
31|:-----------|:------------|
32| Responder | 2.0.5 |
33| Firestore | v1 beta|
34
35です。
あとで「ブログ一覧取得」や「編集画面実装」にも用いるテスト投稿なので、公開しておきます。
第8回へつづく!
今回は、「メディアアップロードの実装」を行いました。
着々とブログシステムが出来上がってきていますね!
あとは、もう少し管理者ページを充実させていきます。
次回もお楽しみに!
次回の記事はこちら
第1回はこちら
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit