【第2回】Responderを使ってDjangoチュートリアルをやってみた【データベース・モデル構築編】
IT技術
第2回~Responderを使ってDjangoチュートリアルをやってみた~
今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。
Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。
今回は、Django のチュートリアル「はじめての Django アプリ作成、その2」をご参考下さい。
【はじめての Django アプリ作成、その2】
https://docs.djangoproject.com/ja/2.2/intro/tutorial02/
第1回はこちら
データベースの構築
Responder単体では、Djangoのように単体で簡単にデータベース作成はできません。
そこで、「SQLAlchemy(エスキューエルアルケミー)」と呼ばれる、Pythonでデータベース管理ができるライブラリの力を借りることにしました。
他にも、データベース管理のためのPythonライブラリはあるようですが、情報がたくさんあるSQLAlchemyを使うことにします。
データベースを構築
では、早速データベースを構築していきましょう!
今回は、SQLite3を使ってデータベースを構築していきます。
以下は、データベースの設定ファイルdb.py です。
作成場所は、プロジェクトのルートで構いません。
1"""
2db.py
3データベース設定用
4"""
5from sqlalchemy.ext.declarative import declarative_base
6from sqlalchemy import create_engine
7from sqlalchemy.orm import sessionmaker
8
9Base = declarative_base()
10RDB_PATH = 'sqlite:///db.sqlite3'
11ECHO_LOG = True
12
13engine = create_engine(
14 RDB_PATH, echo=ECHO_LOG
15)
16
17Session = sessionmaker(bind=engine)
18session = Session()
現段階では、SQLAlchemyを触ったことのない人にとっては何が書いてあるか分からないかもしれません。
でも、今の段階では深く理解する必要はありません。
「ああ、SQLite3っていうデータベースを使うんだな」くらいに思っていただければOKです。
モデルの定義
今回、必要なテーブル(モデル)は、以下の通りとなります。
Question テーブル | id(主キー)、question_text(質問事項)、pub_date(公開日時) |
Choice テーブル | id(主キー)、question(紐づけられた質問)、choice_text(選択肢テキスト)、votes(投票数) |
User テーブル | id(主キー)、username(ユーザ名)、password(ハッシュ化されたパスワード) |
これらは、SQLAlchemyにおいてクラスで定義可能です。
models.py
これらのモデル定義をするために、models.py を作成し、以下のように記述します。
1"""
2models.py
3モデルの定義
4"""
5import os
6from datetime import datetime
7
8from db import Base
9from db import engine
10
11from sqlalchemy import Column, String, DateTime, ForeignKey
12from sqlalchemy.sql.functions import current_timestamp
13from sqlalchemy.dialects.mysql import INTEGER
14
15import hashlib
16
17SQLITE3_NAME = "./db.sqlite3"
18
19
20class Question(Base):
21 """
22 Questionテーブル
23
24 id : 主キー
25 question_text : 質問事項
26 pub_date : 公開日
27 """
28 __tablename__ = 'question'
29
30 id = Column(
31 'id',
32 INTEGER(unsigned=True),
33 primary_key=True,
34 autoincrement=True
35 )
36 question_text = Column('question_text', String(256))
37 pub_date = Column(
38 'pub_date',
39 DateTime,
40 default=datetime.now(),
41 nullable=False,
42 server_default=current_timestamp()
43 )
44
45 def __init__(self, question_text, pub_date=datetime.now()):
46 self.question_text = question_text
47 self.pub_date = pub_date
48
49 def __str__(self):
50 return str(self.id) + ':' + self.question_text + ' - ' + self.pub_date.strftime('%Y/%m/%d - %H:%M:%S')
51
52
53
54class Choice(Base):
55 """
56 Choiceテーブル
57
58 id : 主キー
59 question : 紐づけられた質問 (外部キー)
60 choice_text : 選択肢のテキスト
61 votes : 投票数
62 """
63 __tablename__ = 'choice'
64 id = Column(
65 'id',
66 INTEGER(unsigned=True),
67 primary_key=True,
68 autoincrement=True
69 )
70 question = Column('question', ForeignKey('question.id'))
71 choice_text = Column('choice_text', String(256))
72 votes = Column('votes', INTEGER(unsigned=True), nullable=False)
73
74 def __init__(self, question, choice_text, vote=0):
75 self.question = question
76 self.choice_text = choice_text
77 self.votes = vote
78
79 def __str__(self):
80 return str(self.id) + ':' + self.choice_text + ' - ' + self.votes
81
82class User(Base):
83 """
84 Userテーブル
85
86 id : 主キー
87 username : ユーザネーム
88 password : パスワード
89 """
90 __tablename__ = 'user'
91 id = Column(
92 'id',
93 INTEGER(unsigned=True),
94 primary_key=True,
95 autoincrement=True
96 )
97 username = Column('username', ForeignKey('question.id'))
98 password = Column('password', String(256))
99
100 def __init__(self, username, password):
101 self.username = username
102 # パスワードはハッシュ化して保存
103 self.password = hashlib.md5(password.encode()).hexdigest()
104
105 def __str__(self):
106 return str(self.id) + ':' + self.username
107
108
109if __name__ == "__main__":
110 path = SQLITE3_NAME
111 if not os.path.isfile(path):
112
113 # テーブルを作成する
114 Base.metadata.create_all(engine)
コードを見てわかるように、各クラスのメンバ変数が各テーブルのレコードの要素となります。
このとき、各モデルのインスタンスをprint() できるようにdef __str__() も実装しておきましょう!
実行してみよう!
さて、実際に models.py を実行してみてください。
db.sqlite3 というファイルが生成されたかと思います。
では、実際にモデルの定義ができたか確認してみましょう!
1hoge@user:responder$ sqlite3 db.sqlite3
で作成したデータベースにアクセスします。
1sqlite> .table
2choice question user
しっかりテーブルが作成できました!
サンプルのデータを挿入してみる
それでは、適当なデータを挿入してみましょう。
このとき、Userテーブルには管理者ユーザ(admin)を追加することにします。
早速、sample_insert.py (ファイル名はなんでも良い)を作成しましょう!
1"""
2sample_insert.py
3サンプルデータをデータベースに格納してみる
4"""
5import models
6import db
7
8if __name__ == '__main__':
9 # サンプル質問
10 question = models.Question('What\'s up?')
11 db.session.add(question)
12 db.session.commit()
13
14 # サンプル選択肢
15 choices = list()
16 choice_1 = models.Choice(question.id, 'Fine.')
17 choice_2 = models.Choice(question.id, 'Not bad')
18 choice_3 = models.Choice(question.id, 'Bad...')
19
20 choices.append(choice_1)
21 choices.append(choice_2)
22 choices.append(choice_3)
23
24 db.session.add_all(choices)
25 db.session.commit()
26
27 # adminユーザを作成
28 admin = models.User('admin', 'responder')
29 db.session.add(admin)
30 db.session.commit()
31
32 db.session.close()
SQLAlchemyでは、このようにデータをデータベースに追加が可能です。
データベースをいじる際には、import models とimport db を忘れないようにしましょう。
simple_insert.py を実行してみる!
では、simple_insert.py を実行してみましょう!
すると、models.py で定義された各モデルのコンストラクタ__init__()に、引数にとったデータが渡され、各テーブルにdb.session.commit() で挿入されます。
確認
うまく挿入できたか確認してみましょう!
1sqlite> select * from user;
21|admin|6295474448cef4d8b762397447b77beb
3
4sqlite> select * from question;
51|What's up?|2019.08.02 - 12:17:36
6
7sqlite> select * from choice;
81|1|Fine.|0
92|1|Not bad|0
103|1|Bad...|0
うまく挿入できていそうです!
adminサイトに入る前に
それでは、adminページに入りたいところですが、Djangoと違ってデフォルトで用意されていないので、管理者ページを作ります。
ということで、今から作成するのは、この3つになります。
- 管理者(admin)のログイン機能
- 管理者ページ(administrator.html)
- ログアウト機能
少々ややこしい部分も出てきますが頑張っていきましょう!
管理者(admin)のログイン機能
それでは、前回作成したadmin.html からpostメソッドで受け取る/ad_login を作っていきます。
ルーティング作業ですので、urls.py に加筆していきます。
ログイン機能
ログイン機能には、様々な機能が必要です。
- usernameとpasswordがデータベースに存在するか
- usernameとpasswordが一致するか
- そもそも入力フォームにそれぞれが入力されているか
- 以上3点が守られていない場合エラーメッセージを返す
- OKならば入力されたusernameでセッションを開始する
と、最低限必要な機能を挙げるだけでも、これだけあります。
また、getで/ad_login にアクセスされた場合に、余計な処理をせずにログインページへリダイレクトさせる必要もあります。
これらを実装していく前に、「1.usernameとpasswordがデータベースに存在するか」と「2.usernameとpasswordが一致するか」は、一つの関数として最初に定義してしまいましょう。
新しく認証用の関数を定義するauth.py を作成し、認証ができたか否かを返す関数is_auth() を実装します。
また、「今現在ログイン状態にあるかないかを判定し、ログインしていなければログイン画面へリダイレクトさせる」機能をもつauthorized() も一緒に実装してしまおうかと思います。
auth.py
実装例は、以下のようになります。
1"""
2auth.py
3認証系の関数群
4"""
5from models import User
6import db
7
8
9def is_auth(username, password):
10 """
11 Userテーブルに存在するか否かを返す
12 """
13 users = db.session.query(User.username, User.password)
14 db.session.close()
15
16 for user in users:
17 if user.username == username and user.password == password:
18 return True
19 return False
20
21
22def authorized(req, resp, api):
23 """
24 cookieにusernameが存在しない場合ログインページにリダイレクトする
25 (ログインしていない場合)
26 """
27 if 'username' not in req.cookies:
28 api.redirect(resp, '/admin')
is_auth() では、まずデータベースのUserテーブルから、「username」と「password」をセットでリストとして持ってきます。
users = db.session.query(User.username, User.password)
そのリスト内に、引数にとったusernameとpasswordに対して一致するものがあればTrueを返し、なければFalseを返すシンプルな関数です。
authorized() もシンプルで、cookieに「username」キーがなければリダイレクト、としているだけです。
urls.py
では、またurls.py に戻ります。
先ほど挙げた機能をurls.py に実装してみたいと思います。
新しく加筆した部分には# New! をつけていますので参考にしてください。
1"""
2urls.py
3ルーティング用ファイル
4"""
5import responder
6
7from auth import is_auth, authorized # New!
8import hashlib # New!
9
10from models import User, Question, Choice # New!
11import db # New!
12
13api = responder.API()
14
15# -- 省略-- #
16
17@api.route('/ad_login') # New!
18class AdLogin:
19 async def on_get(self, req, resp): # getならリダイレクト
20 resp.content = api.template('admin.html')
21
22 async def on_post(self, req, resp):
23 data = await req.media()
24 error_messages = []
25
26 if data.get('username') is None or data.get('password') is None:
27 error_messages.append('ユーザ名またはパスワードが入力されていません。')
28 resp.content = api.template('admin.html', error_messages=error_messages)
29 return
30
31 username = data.get('username')
32 password = hashlib.md5(data.get('password').encode()).hexdigest()
33
34 if not is_auth(username, password):
35 # 認証失敗
36 error_messages.append('ユーザ名かパスワードに誤りがあります。')
37 resp.content = api.template('admin.html', error_messages=error_messages)
38 return
39
40 # 認証成功した場合sessionにユーザを追加しリダイレクト
41 resp.set_cookie(key='username', value=username, expires=None, max_age=None)
42 api.redirect(resp, '/admin_top')
加筆したコード量は多いですが、一つ一つ丁寧にみていけば、どのような処理を行っているかはなんとなく理解できると思います。
Responderでは、今回実装した@api.route('/ad_login') のように処理をクラスでも記述することができますclass AdLogin 。
- async def on_get(self, req, resp) でget処理
- async def on_post(self, req, resp) でpost処理
そうすると、async def on_get(self, req, resp) でget処理、async def on_post(self, req, resp) でpost処理を書き分けることができますので、覚えておきましょう!
ログイン/ログアウト
また、Responderのログイン/ログアウトについては、resp.set_cookie(key='名前', value='実際の値', expires='いつ破棄されるか', max_age='何秒後に破棄されるか')を使用します。
- ログインは、expire=None max_age=None
- ログアウトは、expire=0 max_age=0
ログインでは、expire=None max_age=None としておき、ログアウトではexpire=0 max_age=0 とすれば機能が実現できます。
他にも、resp.set_cookie()には引数は存在しますが、今回は簡略化のためスルーします。
また、まだ/admin_top のルーティングは終わってないので、このログイン機能は完全には機能しません。
管理者ページ(administrator.html)
次に、先ほどの/admin_top のルーティングと、そのビューadministrator.html を作成していきます。
管理者ページでは、データベースの中身を視覚的に確認できるようにします。
最終的には、このページからデータベースへの追加や削除もできるようにしますが、とりあえずは表示のみを目指します。
/admin_top のルーティング
早速、urls.py に/admin_top のルーティングを行なっていきます。
また、その際にQuestionテーブルとChoiceテーブルのデータを取得しておき、administrator.html に渡す作業も実装していきます。
1@api.route('/admin_top')
2async def on_session(req, resp):
3 """
4 管理者ページ
5 """
6
7 authorized(req, resp, api)
8
9 # ログインユーザ名を取得
10 auth_user = req.cookies.get('username')
11
12 # データベースから質問一覧と選択肢を全て取得
13 questions = db.session.query(Question).all()
14 choices = db.session.query(Choice).all()
15 db.session.close()
16
17 # 各データを管理者ページに渡す
18 resp.content = api.template('administrator.html',
19 auth_user=auth_user,
20 questions=questions,
21 choices=choices)
やっていることは先のis_auth() と少し似ていますね。
関数の冒頭で、cookieにユーザが存在しない(=ログインしていない)ときに、ログインページへリダイレクトauthorized() させています。
こうすることで、/admin_top に直接アクセスできなくさせています。
ビューを作る
それでは、ビューを作っていきましょう。
ビューは、前回のように作っていけば良いので説明は省略しますが、受け取ったデータの展開の仕方だけコードを見て理解しておきましょう。
今更ですが、ビューでの動的ページは、Jinja2と呼ばれるエンジンに従ったコーディングを行なっています。
{% %} の中に構文、{{ }} の中には出力させたい変数を入れます。
詳細は、コードを見ながら覚えられると思います。
ビューの実装
ビューのコードは以下のように実装しました。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<div class="row">
5 <div class="col-md-10">
6 <h1>Polls Application Administrator</h1>
7 </div>
8 <div class="col-md-2">
9 <form action="/logout" method="post">
10 <input type="hidden" name="token" value="{{auth_user}}">
11 <button type="submit" class="btn btn-primary">Logout</button>
12 </form>
13 </div>
14</div>
15<hr>
16<p>Hi, {{auth_user}}.</p>
17<br>
18
19
20<br>
21<h4>Question <a href="/add_Question" class="btn-warning btn-sm">Add</a></h4>
22<table class="table">
23 <thead class="thead-dark">
24 <tr>
25 <th>id</th>
26 <th scope="col">question_text</th>
27 <th scope="col">pub_date</th>
28 <th scope="col"></th>
29 </tr>
30 </thead>
31 <tbody>
32 {% for question in questions%}
33 <tr>
34 <th scope="row">{{question['id']}}</th>
35 <td>{{question['question_text']}}</td>
36 <td>{{question['pub_date']}}</td>
37 <td>
38 <a href="/change/question/{{question['id']}}" class="btn-secondary btn-sm">Change</a>
39 <a href="/delete/question/{{question['id']}}" class="btn-danger btn-sm">Delete</a>
40 </td>
41 </tr>
42 {% endfor %}
43 </tbody>
44</table>
45
46<br>
47<h4>Choice <a href="/add_Choice" class="btn-warning btn-sm">Add</a></h4>
48<table class="table">
49 <thead class="thead-dark">
50 <tr>
51 <th>id</th>
52 <th scope="col">question</th>
53 <th scope="col">choice_text</th>
54 <th scope="col">votes</th>
55 <th scope="col"></th>
56 </tr>
57 </thead>
58 <tbody>
59 {% for choice in choices%}
60 <tr>
61 <th scope="row">{{choice['id']}}</th>
62 <td>{{choice['question']}}</td>
63 <td>{{choice['choice_text']}}</td>
64 <td>{{choice['votes']}}</td>
65 <td>
66 <a href="/change/choice/{{choice['id']}}" class="btn-secondary btn-sm">Change</a>
67 <a href="/delete/choice/{{choice['id']}}" class="btn-danger btn-sm">Delete</a>
68 </td>
69 </tr>
70 {% endfor %}
71 </tbody>
72</table>
73{% endblock %}
テーブルでデータベースの情報を表示する形をとりました。
「Add」ボタン、「Change」ボタン、「Delete」ボタンは、まだ実装していないので機能しません。
右上に表示される「ログアウトボタン」については、今から実装していきたいと思います。
ログアウト機能
さて、長くなりましたが、やっとで本記事最後のコーディングになります。
ログアウト機能は、とてもシンプルです。
セッションから抜けて、ログイン画面にリダイレクトさせるだけなので、以下のようにurls.py に加筆します。
ログアウトは、先ほど言ったようにcookieを破棄するまでの時間を0とすればOKです。
1@api.route('/logout') # New!
2async def logout(req, resp):
3 # クッキーを削除
4 resp.set_cookie(key='username', value='', expires=0, max_age=0)
5 api.redirect(resp, '/admin')
ちなみに、cookieの中身を見たい場合はprint(req.cookies) で見ることができます。
(resp ではなくreq です!)
もし、うまくいかない場合は、デバッグ用で活用してくださいね。
adminサイトに入る
ここにきて、やっとでチュートリアルに戻ってきました!
まずは、サーバを起動して、http://127.0.0.1:5042/adminにアクセスしてみます。
ログイン成功
次に、ログイン画面を確認してみたいと思います。
それぞれに「admin」「responder」と入力して、
ログインしてみましょう!
うまくデータベースの中身が表示できました!
ログイン失敗
それでは、わざとユーザ名やパスワードを間違えて、エラーメッセージが出るかも確認してみます。
【error1】
【error2】
問題なさそうですね!
あとは、管理者ページからデータベースの操作ができれば管理者ページは完成になります。
こちらは、次回に続きます!
第3回へつづく!
今回は、「データベースの構築」と「モデルの定義」と、ボリュームのある回となりました。
ですが、ここを乗り越えると、Webアプリケーション初心者でも、なんとなく勝手が分かってくるのではないでしょうか?
次回も、「データベースとの連携」を含んだ話が続きますが、あともう少し頑張っていきましょう!
第3回の記事はこちら
【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
urls.py
最後に、前回から更新(加筆)されたurls.pyの全体を載せておきます。
1"""
2urls.py
3ルーティング用ファイル
4"""
5import responder
6
7from auth import is_auth, authorized # New!
8import hashlib # New!
9
10from models import User, Question, Choice # New!
11import db # New!
12
13api = responder.API()
14
15
16@api.route('/')
17def index(req, resp):
18 resp.content = api.template("index.html")
19
20
21@api.route('/admin')
22def admin(req, resp):
23 resp.content = api.template("admin.html")
24
25
26@api.route('/ad_login') # New!
27class AdLogin:
28 async def on_get(self, req, resp): # getならリダイレクト
29 resp.content = api.template('admin.html')
30
31 async def on_post(self, req, resp):
32 data = await req.media()
33 error_messages = []
34
35 if data.get('username') is None or data.get('password') is None:
36 error_messages.append('ユーザ名またはパスワードが入力されていません。')
37 resp.content = api.template('admin.html', error_messages=error_messages)
38 return
39
40 username = data.get('username')
41 password = hashlib.md5(data.get('password').encode()).hexdigest()
42
43 if username != 'admin':
44 # 少々強引ですがadmin以外はログインできないようにする。
45 error_messages.append('管理者ユーザではありません。')
46 resp.content = api.template('admin.html', error_messages=error_messages)
47 return
48
49 if not is_auth(username, password):
50 # 認証失敗
51 error_messages.append('ユーザ名かパスワードに誤りがあります。')
52 resp.content = api.template('admin.html', error_messages=error_messages)
53 return
54
55 # 認証成功した場合cookieにユーザを追加しリダイレクト
56 resp.set_cookie(key='username', value=username, expires=None, max_age=None)
57 api.redirect(resp, '/admin_top')
58
59
60@api.route('/admin_top') # New!
61async def on_session(req, resp):
62 """
63 管理者ページ
64 """
65 authorized()
66
67 # ログインユーザ名を取得
68 auth_user = req.cookies.get('username')
69
70 # データベースから質問一覧と選択肢を全て取得
71 questions = db.session.query(Question.id, Question.question_text, Question.pub_date)
72 choices = db.session.query(Choice.id, Choice.question, Choice.choice_text, Choice.votes)
73 db.session.close()
74
75 # 各データを管理者ページに渡す
76 resp.content = api.template('administrator.html',
77 auth_user=auth_user,
78 questions=questions,
79 choices=choices)
80
81
82@api.route('/logout') # New!
83async def logout(req, resp):
84 # クッキーを削除
85 resp.set_cookie(key='username', value='', expires=0, max_age=0)
86 api.redirect(resp, '/admin')
ディレクトリ構成
あとは、現在のディレクトリ構成も載せておきます。
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│ ├── admin.html
12│ ├── administrator.html
13│ ├── index.html
14│ └── layout.html
15└── urls.py
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit