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

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン