• トップ
  • ブログ一覧
  • 【第2回】Responderを使ってDjangoチュートリアルをやってみた【データベース・モデル構築編】
  • 【第2回】Responderを使ってDjangoチュートリアルをやってみた【データベース・モデル構築編】

    広告メディア事業部広告メディア事業部
    2019.08.22

    IT技術

    第2回~Responderを使ってDjangoチュートリアルをやってみた~

    今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。

    Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。

    今回は、Django のチュートリアル「はじめての Django アプリ作成、その2」をご参考下さい。

    【はじめての Django アプリ作成、その2】
    https://docs.djangoproject.com/ja/2.2/intro/tutorial02/

    第1回はこちら

    featureImg2019.08.20【第1回】Responderを使ってDjangoチュートリアルをやってみた【プロジェクト作成編】第1回~Responderを使ってDjangoチュートリアルをやってみた~初期セットアップが、まだお済みでない方は前回...

    データベースの構築

    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つになります。

    1. 管理者(admin)のログイン機能
    2. 管理者ページ(administrator.html)
    3. ログアウト機能

    少々ややこしい部分も出てきますが頑張っていきましょう!

    管理者(admin)のログイン機能

    それでは、前回作成したadmin.html からpostメソッドで受け取る/ad_login を作っていきます。

    ルーティング作業ですので、urls.py に加筆していきます。

    ログイン機能

    ログイン機能には、様々な機能が必要です。

    1. usernameとpasswordがデータベースに存在するか
    2. usernameとpasswordが一致するか
    3. そもそも入力フォームにそれぞれが入力されているか
    4. 以上3点が守られていない場合エラーメッセージを返す
    5. 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 

    1. async def on_get(self, req, resp) でget処理
    2. 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='何秒後に破棄されるか')を使用します。

    1. ログインは、expire=None max_age=None
    2. ログアウトは、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 &nbsp; <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 &nbsp; <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回の記事はこちら

    featureImg2019.08.27【第3回】Responderを使ってDjangoチュートリアルをやってみた【データベース操作編】第3回~Responderを使ってDjangoチュートリアルをやってみた~&nbsp;前回「【第2回】Responde...

    【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた

    featureImg2019.10.25【まとめ編】Responderを使ってDjangoチュートリアルをやってみたResponderを使ってDjangoチュートリアルをやってみた~まとめ~ライトコード社長も今、イチオシのWEBフレー...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.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

     

    広告メディア事業部

    広告メディア事業部

    おすすめ記事