【第4回】Responderを使ってDjangoチュートリアルをやってみた【公開ビュー作成編】
IT技術
前回、「【第3回】Responderを使ってDjangoチュートリアルをやってみた【データベース操作編】」の続きです。
今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。
Django のチュートリアル「はじめての Django アプリ作成」を、Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。
【はじめての Django アプリ作成】
https://docs.djangoproject.com/ja/2.2/intro/
第1回はこちら
オーバビュー
今記事は、Djangoチュートリアルでいうところのと、「はじめての Django アプリ作成、その 3 」と「 はじめての Django アプリ作成、その 4 」にあたります。
現在、作成しているPollsアプリケーションでは、以下のビューを公開用として作成していきます。
質問-インデックスページ | 最新エントリーをいくつか表示 |
質問-詳細ページ | 結果を表示せず、質問テキストと投票フォームを表示 |
質問-結果ページ | 特定の質問の結果を表示 |
投票ページ | 特定の質問の選択を投票として受付 |
また、ビューのURLは、前回の「/delete/question/1」のような解りやすくシンプルなURLパターンで提供できるようにします。
早速作成していきましょう。
質問-インデックスページ
インデックスページとは、http://127.0.0.1:5042で表示されるビューです。
簡単ではありますが、インデックスページを以前すでに作っています。
現在、templates/index.html は以下のようになっているはずです。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<h1>Welcome to Polls Application!</h1>
5<hr>
6<p>
7 This is a simple polls application from Django Tutorial
8 (URL: <a href="https://docs.djangoproject.com/ja/2.2/intro/tutorial01/">https://docs.djangoproject.com/ja/2.2/intro/tutorial01/</a>)
9</p>
10{% endblock %}
この章では、これを改造していきたいと思います。
インデックスページでは、質問一覧を表示させる必要があります。
しかし、このようなデータベース処理は、【第1回】から読んでいただいている方には、さほど難しくはないはずです。
urls.py
それでは、まず、urls.py の@api.route('/') を触っていきましょう。
Questionテーブルから「id」と「質問内容」と「公開日」を持ってきてビューに渡すだけですが、少し凝ってみましょう。
1@api.route('/')
2def index(req, resp):
3 resp.content = api.template("index.html")
を
1@api.route('/')
2class Index:
3 def on_get(self, req, resp):
4
5 # 最新5個の質問を降順で取得
6 questions = selfget_queryset()
7
8 # フォーマットを変更して必要なものだけ
9 pub_date = [q.pub_date.strftime('%Y-%m-%d %H:%M:%S') for q in questions]
10
11 resp.content = api.template("index.html", questions=questions, pub_date=pub_date)
12
13 def get_queryset(self, latest=5):
14 """
15 最新latest個の質問を返す
16 :param latest:
17 :return:
18 """
19 # 公開日の大きいものでソートして取得
20 questions = db.session.query(Question).order_by(Question.pub_date.desc()).all()
21 db.session.close()
22
23 return questions[:latest]
としてみました。
def get_queryset(self, latest=5) で、最新latest 個、公開日が新しい順で取得しています。
今回は、最新順で表示させる必要があるため、データベースからは降順でデータを取得する必要があります。
したがって、SQLでいう「ORDER BY DESC」でデータを取得する必要があり、SQLAlchemyではorder_by() を使います。
ビュー
それでは、次に、ビューを作ります。
こちらも、もらった質問一覧をビューで展開するだけなので、とても簡単です。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<h1>Welcome to Polls Application!</h1>
5<hr>
6<p>
7 This is a simple polls application from Django Tutorial
8 (URL: <a href="https://docs.djangoproject.com/ja/2.2/intro/tutorial01/">https://docs.djangoproject.com/ja/2.2/intro/tutorial01/</a>)
9</p>
10
11<!-- ここから追記 -->
12<br>
13<br>
14<h3>最新の質問</h3>
15
16<table class="table">
17 <thead class="thead-dark">
18 <tr>
19 <th scope="col">質問</th>
20 <th scope="col">公開日</th>
21 </tr>
22 </thead>
23 <tbody>
24 {% for question in questions %}
25 <tr>
26 <td><a href="/detail/{{question['id']}}">{{question['question_text']}}</a></td>
27 <td>{{pub_date[loop.index-1]}}</td>
28 </tr>
29 {% endfor%}
30 </tbody>
31</table>
32<!-- ここまで -->
33
34{% endblock %}
今回は、このように実装してみました。
質問の詳細は、/detail/1のようなURLパターンで処理することにしています。
質問一覧としては機能していますが、せっかくなので少し仕様を変更してみましょう。
最新(直近)の質問だけ強調する
現在は、公開日が新しい順に並んでいますが、昨日までに公開されたものを強調してみましょう。
久しぶりに、モデルをいじります。
1class Question(Base):
2
3 # 省略
4
5 def was_published_recently(self, days=1):
6 """
7 最近追加された質問に対してTrueを返す関数
8 :return:
9 """
10 return self.pub_date >= datetime.now() - timedelta(days=days)
上記のような関数を追加しました。
これは、登録されている日付が、現在からdays 日前に存在していればTrueを返す関数です。
次に、インデックスページを表示させる関数に以下の処理を追加していきます。
1@api.route('/')
2class Index:
3 def on_get(self, req, resp):
4
5 # 最新5個の質問を降順で取得
6 questions = self.get_queryset()
7
8 # New! 最新かどうか
9 emphasized = [question.was_published_recently() for question in questions]
10
11 # フォーマットを変更して必要なものだけ
12 pub_date = [q.pub_date.strftime('%Y-%m-%d %H:%M:%S') for q in questions]
13
14 resp.content = api.template("index.html", questions=questions, emphasized=emphasized, pub_date=pub_date)
Pythonの内包表記を使って、各質問の公開日が最新かどうかをlistとして取得しています。
ビュー
それでは、ビューには、以下のように追記してみましょう。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<h1>Welcome to Polls Application!</h1>
5<hr>
6<p>
7 This is a simple polls application from Django Tutorial
8 (URL: <a href="https://docs.djangoproject.com/ja/2.2/intro/tutorial01/">https://docs.djangoproject.com/ja/2.2/intro/tutorial01/</a>)
9</p>
10
11<br>
12<br>
13<h3>最新の質問</h3>
14
15<table class="table">
16 <thead class="thead-dark">
17 <tr>
18 <th scope="col">質問</th>
19 <th scope="col">公開日</th>
20 </tr>
21 </thead>
22 <tbody>
23 {% for question in questions %}
24 <tr>
25 <td>
26 <!-- ここから変更 -->
27 <a href="/detail/{{question['id']}}">{{question['question_text']}}</a>
28 {% if emphasized[loop.index-1]%}
29 <span class="badge badge-info"> new! </span>
30 {% endif %}
31 <!-- ここまで -->
32 </td>
33 <td>{{question['pub_date']}}</td>
34 </tr>
35 {% endfor%}
36 </tbody>
37</table>
38
39{% endblock %}
Jinja2では、loop.index は「1」から始まるので、配列のインデックスとして扱う場合は上記のように-1 します。
動作確認
それでは、新しく質問を追加してみて、実際に動作するか確認してみましょう!
画像のように、バッジが表示されていれば成功です!
SQLAlchemyでは、モデルクラスに関数の定義が可能で、それらをResponderとリンクさせることでさまざまな機能が実現できます。
質問-詳細ページ
それでは、詳細ページを作っていきます。
まず、 detail/{id} というURLパターンをurls.py に追記していきます。
これは、URLによって指定された「id」に対して、合致するものをデータベースから取得しビューに渡すだけなので、
1@api.route('/detail/{q_id}')
2class Detail:
3 async def on_get(self, req, resp, q_id):
4 question = db.session.query(Question).filter(Question.id == q_id).first()
5 choices = db.session.query(Choice).filter(Choice.question == q_id).all()
6 db.session.close()
7
8 resp.content = api.template('/detail.html', question=question, choices=choices)
のように記述します。
Questionテーブルからは、一つで良いのでfirst() 、Choiceテーブルからは質問idが同じものすべて取得する必要があるのでall() を使っていきます。
ビュー
次に、ビューを作って行きましょう。
ビューでは、質問内容に対して選択肢と、投票機能を備えていれば良いので以下のようなコーディングが良いでしょう。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<a href="/" class="btn btn-primary">戻る</a>
5<h2>{{question['question_text']}}</h2>
6<hr>
7<p>質問公開日:{{question['pub_date']}}</p>
8<br>
9<form action="/vote/{{question['id']}}" method="post">
10 {% for choice in choices %}
11 <p><input type="radio" name="choice" value="{{choice['id']}}" {% if loop.first %} checked {% endif %}> {{choice['choice_text']}}</p>
12 {% endfor %}
13 <button type="submit" name="submit" class="btn btn-success">投票する</button>
14</form>
15
16{% endblock %}
次に、投票機能と質問-結果ページを実装していきたいと思います。
投票機能と質問-結果ページ
投票機能では、先ほどのビューで /vote/{id} のようなURLパターンで提供するように実装しました。
したがって、いつもの如くルーティング作業をしていきます。
1@api.route('/vote/{q_id}')
2class Vote:
3 async def on_post(self, req, resp, q_id):
4 # postデータを取得
5 data = await req.media()
6
7 # 該当するchoiceを取得しvoteをインクリメント
8 choice = db.session.query(Choice).filter(Choice.id == data.get('choice')).first()
9 choice.votes += 1
10 db.session.commit()
11
12 db.session.close()
13
14 # リダイレクト
15 url_redirect = '/result/' + str(q_id)
16 api.redirect(resp, url_redirect)
実装内容の詳細な説明は、もう大丈夫だと思います。
これで、投票機能はできたので、リダイレクト先となる質問-結果ページを作成していきます。
ルーティング
まずは、ルーテイングから。
1@api.route('/result/{q_id}')
2class Result:
3 async def on_get(self, req, resp, q_id):
4
5 question = db.session.query(Question).filter(Question.id == q_id).first()
6 choices = db.session.query(Choice).filter(Choice.question == q_id).all()
7 db.session.close()
8
9 resp.content = api.template('result.html', question=question, choices=choices)
ビュー
次に、ビューは以下のように実装しました。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<a href="/" class="btn btn-primary">戻る</a>
5<h2>{{question['question_text']}}</h2>
6<hr>
7<p>質問公開日:{{question['pub_date']}}</p>
8<br>
9<h3>現在の投票結果</h3>
10{% for choice in choices %}
11 <p><span class="badge badge-info">{{choice['votes']}}</span> {{choice['choice_text']}}</p>
12{% endfor %}
13
14{% endblock %}
投票数は、bootstrap4のbadgeクラスを用いてデザインしていますが、どんな形でも構いません。
動作確認
実際に投票してみると、以下のように、うまいこと投票機能と結果のビューが実装できていることが確認できます。
404エラーの送出
Djangoチュートリアルでは、ここで404エラー、すなわち「ページが存在しないときのエラー画面」についての説明が入ります。
しかし、Responderには現在そのようなエラーハンドリングがないようです。
ネット上でも様々な人が試行錯誤していますが、既存のResponderではスマートに解決できていないようです。
力技で解決する場合
この方法は、Responderのコード自体をいじってしまうため、あまりオススメできませんが、ひとつの解決案として参考にしてください。
Responderのコードをいじると言っても1行変更するだけです。
Responderのapi.py というファイルの494行目にdefault_response() という関数があります。
その関数を、以下のように変更してみたください(変更部分は508行目)。
1 def default_response(
2 self, req=None, resp=None, websocket=False, notfound=False, error=False
3 ):
4 if websocket:
5 return
6
7 if resp.status_code is None:
8 resp.status_code = 200
9
10 if self.default_endpoint and notfound:
11 self.default_endpoint(req=req, resp=resp)
12 else:
13 if notfound:
14 resp.status_code = status_codes.HTTP_404
15 # resp.text = "Not found." ここを以下に変更
16 resp.content = self.template('404.html')
17 if error:
18 resp.status_code = status_codes.HTTP_500
19 resp.text = "Application error."
これで404エラーコードはすべてテンプレートの「404.html」にレスポンスされます。
404.html
ひとまず、404.htmlは、以下のようにしてみました。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<a href="/" class="btn btn-primary btn-sm">トップページにもどる</a>
5<br><br>
6<h2>404: Not Found.</h2>
7<hr>
8<p>お探しのページが見つかりませんでした。</p>
9<p>The page you are looking for is not found.</p>
10<p></p>
11
12{% endblock %}
これで、ルーティングしていないURLにアクセスすると、今追加した404.htmlが表示されると思います。
あまりスマートな方法ではありませんが、大きな変更ではないのでひとつの案としてご紹介しました。
第5回へつづく!
ここまでの内容で、Pollsアプリケーションは、最低限の機能の実装を終えました。
投票の一覧も確認できて、投票もできて、結果も確認できるようになりました。
そして、質問や選択肢を追加したり、変更したり、削除もできます。
次は、少し内容が変わって、自動テストについてのチュートリアルに移ります。
内容が内容なだけに距離をおきがちですが、このチュートリアルでは大したことはやらないので、これを機に自動テストについて理解を深めていきましょう。
第5回の記事はこちら
【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
1.responder/
2├── __pycache__
3├── auth.py
4├── db.py
5├── db.sqlite3
6├── models.py
7├── run.py
8├── sample_insert.py
9├── static
10├── templates
11│ ├── add_choice.html
12│ ├── add_question.html
13│ ├── admin.html
14│ ├── administrator.html
15│ ├── change.html
16│ ├── delete.html
17│ ├── detail.html
18│ ├── index.html
19│ ├── layout.html
20│ └── result.html
21└── urls.py
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit