Masahiro Okubo

初心者向け : Flaskログイン機能をつけてQAサイトを作る 1 -ログイン機能+質問機能-

初心者向け : Flaskログイン機能をつけてQAサイトを作る 1 -ログイン機能+質問機能-

※記事自体が長いですがコピペは全く勉強にならないので絶対にやめてください。
最後にBitbucketのリポジトリのリンクを記載しているので、
そちらを参考にしてください。
Railsに比べてコードが長くなる+設定などが面倒臭いなどの理由で

コードの中にコメントしたりしています。
ブログ記事だけでなく両方見ながら進めてください。
時々説明が足りない部分もあるかと思いますが、その際は直接かコメントで質問を
Flaskを利用してQAサイトを作っていきます

このチュートリアルを通して

  • flask_login
  • qaサイトの基礎

などを学ぶことができます

複数回に分けて実装していきます

今回は回答者がログインして質問を作成することろまでやります

ユーザーについて
※質問者と回答者は同じユーザータイプで扱います。

  • 質問者 → 何か質問したい人
  • 回答者 → 回答できる人
  • 管理者 → システムを管理する人

 

Flaskプロジェクトの作成

FlaskはRailsと異なりルールがほとんどないので開発者や企業によってルールがかなり異なります
このチュートリアルでは以下のフォルダ構成で構築を行います

かなりざっくりとした説明ですが、初めから頭に叩き込むのではなく
開発しながら覚えましょう

  • app :Flaskプロジェクトの最上層
  • models:sqlalchemyを通してデータベースを作成するために作成するclass
  • services:modelを通してデータを登録したり取得したりするためのメソッド群
  • views:serviceを通してデータを取得したりしてそれをhtmlへ反映させるだけのところ
  • __init__.py:全てのファイルをここに取り込み、Flaskを起動させるファイル
  • create_db.py:modelを追加しデータベースを追加するときに使うファイル
  • run.py:Flaskサーバーを起動するときに使うファイル

 

それではプロジェクトを作ります

# プロジェクトの大枠作成
$ mkdir qa-site

# 移動
$ cd qa-site

# flaskのまとまり作成
$ mkdir app

# 移動
$ cd app

# touch __init__.py

# フォルダをそれぞれ作成
$ mkdir models
$ mkdir services
$ mkdir views
$ mkdir templates

# 一階層上がる
$ cd ..

# データベースを作るファイルの作成
$ touch create_db.py

# Flaskサーバーを起動するためのファイル作成
$ touch run.py

 

Flaskサーバーの設定

Flaskサーバーを起動させるために__init__.pyとrun.pyにこのような記述を加えます

__init__.py

# モジュールインポート
from flask import Flask

# Flaskアプリの生成
app = Flask(__name__)


# indexのルート設定
@app.route('/')
def index():
    return '<h1>Hello world</h1>'

run.py

# appディレクトリにある__init__.pyの中で設定したflaskアプリであるappをimportしている
from app import app


# flaskサーバーを起動
if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True)

これで最低限の設定は完了なので、ターミナルでこちらのコマンドを走らせて実行します

$ python run.py

「http://localhost:5000/」にアクセス

 

このような画面が見えると思います

これでflaskサーバーの設定は完了です。

 

SQLAlchemyの追加と設定

flaskにはsqlalchemyというデータベースとflaskを繋いでくれる便利なライブラリがあるので
こちらを利用します。ちなみにほとんどの場合このsqlalchemyが利用されております

__init__.pyを修正

# モジュールインポート
from flask import Flask
# データベースを利用するために追加
from flask_sqlalchemy import SQLAlchemy

# Flaskアプリの生成
app = Flask(__name__)

# ここから /// データベースの設定
app.secret_key = "super secret key"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///qa-site.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# ここまで /// データベースの設定

# sqlalchemyを通してflaskからdbアクセスをするための入り口
db = SQLAlchemy(app)


# indexのルート設定
@app.route('/')
def index():
    return '<h1>Hello world</h1>'

create_db.pyを修正

# appディレクトリの__init__.pyからdbをimport
from app import db

# データベースを作成するためのコード。pythonコンソールで行うことが多いですが面倒臭いのでターミナルから実行できるようにしました。
db.create_all()

 

設定が完了したので次はモデルを作成していきます

Modelを追加

ログイン機能を実装していくのでUserに関するモデルを作ります
flask-loginというログイン機能を提供しているライブラリに関連するところも
入っています

modelはmodelsに追加しますが、ついでにmodelsの中に__init__.pyを作成します
ここでは詳細を話しませんが、きになる方は調べてみてください

user.py

from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

from app import db


# モデルに関する設定
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(100), unique=True)
    name = db.Column(db.String(1000))
    password = db.Column(db.String(100))

    # モデルからインスタンスを生成するときに使います。(利便性を高めるため)
    # passwordの暗号化も自動で行うことができるので、安全性も高めることができます。
    @classmethod
    def from_args(cls, name: str, email: str, password: str):
        instance = cls()
        instance.name = name
        instance.email = email
        if password is not None:
            # passwordがあれば暗号化します。
            instance.hash_password(password)
        return instance

    # 暗号化するためのメソッド。
    def hash_password(self, clean_password):
        self.password = generate_password_hash(str(clean_password), method='sha256')

    # 登録したpasswordとユーザーがログインフォームで入力したパスワードが正しいかどうかのチェックを行うメソッド
    def check_password(self, clean_password):
        return check_password_hash(self.password, clean_password)

 

次はこのデータベースを反映させていきます

まずはapp/__init__.pyでuserモデルをimportします
importしないとデータベースに反映されません

# モジュールインポート
from flask import Flask
# データベースを利用するために追加
from flask_sqlalchemy import SQLAlchemy

# Flaskアプリの生成
app = Flask(__name__)

# ここから /// データベースの設定
app.secret_key = "super secret key"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///qa-site.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# ここまで /// データベースの設定

# sqlalchemyを通してflaskからdbアクセスをするための入り口
db = SQLAlchemy(app)

# データベースのimport
from app.models.user import User


# indexのルート設定
@app.route('/')
def index():
    return '<h1>Hello world</h1>'

次はcreate_db.pyを利用してデータベースを作成します。

ターミナルで以下を実行

$ python create_db.py

問題なく作成されればこのようにデータベースが生成されます

これでmodelとデータベースの設定は完了です

Flask-Loginを追加

次はログイン機能を構築していきます。

ここからは複数ファイルの作業になるので注意しながら開発してください

templates/index.html
templates/layout.html
templates/auth/signup.html
templates/auth/login.html
を作成

templates/index.html

{% extends "layout.html" %}

{% block content %}
    <h1>
        Index page
    </h1>
{% endblock %}

 

templates/layout.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>QA-Site</title>
</head>

<body>
<ul>
    <li><a href="{{ url_for('index') }}">Index</a></li>
    <li><a href="{{ url_for('auth.signup') }}">新規登録</a></li>
    <li><a href="{{ url_for('auth.login') }}">ログイン</a></li>
</ul>
{# flaskのメッセージを表示 #}
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <div>
            {{ messages[0] }}
        </div>
    {% endif %}
{% endwith %}
{% block content %}
{% endblock %}


</body>

</html>

 

templates/auth/signup.html

{% extends "layout.html" %}

{% block content %}
    <div>
        <h3>Sign Up</h3>
        <div>
            <form action="/signup" method="POST">
                <div>
                    <input type="text" name="name" placeholder="Name" autofocus="">
                </div>
                <div>
                    <input type="email" name="email" placeholder="Email" autofocus="">
                </div>
                <div>
                    <input type="password" name="password" placeholder="Password">
                </div>
                <button>Sign Up</button>
            </form>
        </div>
    </div>
{% endblock %}l

templates/auth/login.html

{% extends "layout.html" %}

{% block content %}
    <div>
        <h3>Login</h3>
        <form method="POST" action="/login">
            <div>
                <input type="email" name="email" placeholder="Your Email" autofocus="">
            </div>
            <div>
                <input type="password" name="password" placeholder="Your Password">
            </div>
            <div>
                <label>
                    <input type="checkbox">
                    Remember me
                </label>
            </div>
            <button>Login</button>
        </form>
    </div>
{% endblock %}

 

次はservicesを追加します

services/__init__.py
services/auth_service.py

services/__init__.pyは空のファイルを作成します

services/auth_service.pyはこのように作成します

from flask_login import login_user
from sqlalchemy.exc import SQLAlchemyError
from app import db
from app.models.user import User

"""
servicesのファイルは全てデータベースにデータを送るための準備をしたり、
データに何かしらの処理を与えるコードを記述する場所です。
"""


# 新規登録を行うためのメソッドです。引数にはviewsで取得するformデータが送られてきます。
def signup(data: {}) -> User:  # -> User:これはreturnする値の型を指定しています。Userはオブジェクトとして出力します。
    try:
        name = data.get('name')
        email = data.get('email')
        password = data.get('password')
        # ユーザーがすでに登録されているかどうかを確認します
        user = User.query.filter_by(email=email).first()
        if user:
            # 同じメールアドレスでユーザーが登録されているのであればユーザーをリターンします
            return user
        # ユーザーがいなければ作成します
        new_user = User.from_args(name, email, password)
        # データベースに追加するところ
        db.session.add(new_user)
        db.session.commit()
        return user
    except SQLAlchemyError:
        raise SQLAlchemyError


def login(data: {}) -> User:
    try:
        email = data.get('email')
        password = data.get('password')
        remember = True if data.get('remember') else False
        user = User.query.filter_by(email=email).first()
        # ユーザーとパスワードの確認
        if not user and not user.check_password(user.password, password):
            raise SQLAlchemyError

        # ログイン。rememberにチェックを入れていればログインが維持される
        login_user(user, remember=remember)
        return user
    except SQLAlchemyError:
        raise SQLAlchemyError

 

次はviewsを追加していきます。
viewsはhtmlを表示したりservicesを経由してデータを登録したり取得します
あくまでも呼び出したりするだけで具体的な処理は与えることはありません

views/__init__.py
views/auth.py

views/auth.py

from flask import Blueprint, render_template, redirect, url_for, request, flash
from app.services import auth_service

auth = Blueprint('auth', __name__)


# signupページと、postするページを共通化。
@auth.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'GET':
        return render_template('auth/signup.html')
    else:
        user = auth_service.signup(request.form)
        if user:
            flash('メールアドレスは既に登録されています。')
            return redirect(url_for('index'))
        flash('新規登録に成功しました。')
        return redirect(url_for('index'))


@auth.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('auth/login.html')
    else:
        user = auth_service.login(request.form)
        if not user:
            flash('メールアドレスもしくはパスワードに誤りがあります。')
            return render_template('auth.login')
        flash('ログインしました。')
        return redirect(url_for('index'))

 

次はapp/__init__.pyにviewsを取り込みます。

# モジュールインポート
from flask import Flask, render_template
# データベースを利用するために追加
from flask_sqlalchemy import SQLAlchemy
# flaks-loginのライブラリ追加
from flask_login import LoginManager

# Flaskアプリの生成
app = Flask(__name__)

# ここから /// データベースの設定
app.secret_key = "super secret key"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///qa-site.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# ここまで /// データベースの設定

# sqlalchemyを通してflaskからdbアクセスをするための入り口
db = SQLAlchemy(app)

# flask-loginに関する設定
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

# データベースのimport
from app.models.user import User


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

# authに関するルーティングを追加
from app.views.auth import auth

# authに関するルートをflaskアプリであるappに追加
app.register_blueprint(auth)


# indexのルート設定
@app.route('/')
def index():
    return render_template('index.html')

 

ここまでの修正をhttp://localhost:5000で確認します
こんな感じでひとまずページが作成されています。

新規登録
ページが作成されたので、新規登録ができるか試してみます
必要な項目よ入力して、「Sign Up」をクリックすると、

indexにリダイレクトされflashメッセージが表示されました

ログイン

先ほど登録したユーザー情報を入力し「Login」ボタンをクリック

無事にログインできました

 

ログイン制限の追加

次はログインしているときのみindex.htmlにアクセスすることができるように
修正を加えていきます
ログイン制限は下記のコード追加することで実装できます

# htmlに追加
{% if current_user.is_authenticated %}
 ログインしているときに表示していい項目を記載
{% endif %}

#ルーティングに追加
@app.route('/')
@login_required # login_requiredを追加するとログインしていないとアクセスができないようになる
def index():

 

app/__init__.pyを下記のように修正します
index.htmlへはログインしていないとアクセスできない修正を加えました

# モジュールインポート
from flask import Flask, render_template
# データベースを利用するために追加
from flask_sqlalchemy import SQLAlchemy
# flaks-loginのライブラリ追加
from flask_login import LoginManager, login_required

# Flaskアプリの生成
app = Flask(__name__)

# ここから /// データベースの設定
app.secret_key = "super secret key"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///qa-site.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# ここまで /// データベースの設定

# sqlalchemyを通してflaskからdbアクセスをするための入り口
db = SQLAlchemy(app)

# flask-loginに関する設定
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

# データベースのimport
from app.models.user import User


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


# authに関するルーティングを追加
from app.views.auth import auth

# authに関するルートをflaskアプリであるappに追加
app.register_blueprint(auth)


# indexのルート設定
@app.route('/')
@login_required  # ここを追加
def index():
    return render_template('index.html')

 

templates/layout.htmlを下記のように修正
新規登録とログインはログインしていない時のみ表示されます

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>QA-Site</title>
</head>

<body>
<ul>
    {# ログインしていれば表示 #}
    {% if  current_user.is_authenticated %}
        <li><a href="{{ url_for('index') }}">Index</a></li>
    {% endif %}

    {#ログインしていなければ表示#}
    {% if not current_user.is_authenticated %}
        <li><a href="{{ url_for('auth.signup') }}">新規登録</a></li>
        <li><a href="{{ url_for('auth.login') }}">ログイン</a></li>
    {% endif %}
</ul>
{# flaskのメッセージを表示 #}
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <div>
            {{ messages[0] }}
        </div>
    {% endif %}
{% endwith %}
{% block content %}
{% endblock %}


</body>

</html>

 

http://localhost:5000にアクセスするとこのような画面になっています

最後にログアウト機能を追加します

services/auth_service.py

from flask_login import login_user, logout_user
from sqlalchemy.exc import SQLAlchemyError
from app import db
from app.models.user import User

"""
servicesのファイルは全てデータベースにデータを送るための準備をしたり、
データに何かしらの処理を与えるコードを記述する場所です。
"""


# 新規登録を行うためのメソッドです。引数にはviewsで取得するformデータが送られてきます。
def signup(data: {}) -> User:  # -> User:これはreturnする値の型を指定しています。Userはオブジェクトとして出力します。
    try:
        name = data.get('name')
        email = data.get('email')
        password = data.get('password')
        # ユーザーがすでに登録されているかどうかを確認します
        user = User.query.filter_by(email=email).first()
        if user:
            # 同じメールアドレスでユーザーが登録されているのであればユーザーをリターンします
            return user
        # ユーザーがいなければ作成します
        new_user = User.from_args(name, email, password)
        # データベースに追加するところ
        db.session.add(new_user)
        db.session.commit()
        return user
    except SQLAlchemyError:
        raise SQLAlchemyError


def login(data: {}) -> User:
    try:
        email = data.get('email')
        password = data.get('password')
        remember = True if data.get('remember') else False
        user = User.query.filter_by(email=email).first()
        # ユーザーとパスワードの確認
        if not user and not user.check_password(user.password, password):
            raise SQLAlchemyError

        # ログイン。rememberにチェックを入れていればログインが維持される
        login_user(user, remember=remember)
        return user
    except SQLAlchemyError:
        raise SQLAlchemyError


def logout():
    logout_user()
    return True

 

views/auth.py

from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required

from app.services import auth_service

auth = Blueprint('auth', __name__)


# signupページと、postするページを共通化。
@auth.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'GET':
        return render_template('auth/signup.html')
    else:
        user = auth_service.signup(request.form)
        if user:
            flash('メールアドレスは既に登録されています。')
            return redirect(url_for('index'))
        flash('新規登録に成功しました。')
        return redirect(url_for('index'))


@auth.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('auth/login.html')
    else:
        user = auth_service.login(request.form)
        if not user:
            flash('メールアドレスもしくはパスワードに誤りがあります。')
            return render_template('auth.login')
        flash('ログインしました。')
        return redirect(url_for('index'))


@auth.route('/logout')
@login_required
def logout():
    auth_service.logout()
    flash('ログアウトしました。')
    return redirect(url_for('auth.login'))

 

templates/layout.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>QA-Site</title>
</head>

<body>
<ul>
    {# ログインしていれば表示 #}
    {% if  current_user.is_authenticated %}
        <li><a href="{{ url_for('index') }}">Index</a></li>
        <li><a href="{{ url_for('auth.logout') }}">ログアウト</a></li>
    {% endif %}

    {#ログインしていなければ表示#}
    {% if not current_user.is_authenticated %}
        <li><a href="{{ url_for('auth.signup') }}">新規登録</a></li>
        <li><a href="{{ url_for('auth.login') }}">ログイン</a></li>
    {% endif %}
</ul>
{# flaskのメッセージを表示 #}
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <div>
            {{ messages[0] }}
        </div>
    {% endif %}
{% endwith %}
{% block content %}
{% endblock %}


</body>

</html>

 

ログインした状態で画面を確認します
ログアウトが追加されていることが確認できました。
それでは「ログアウト」ボタンを押してみます

 

問題ないログインされ、ログインページへリダイレクトされました

 

これでログイン関連は完了です。

Question modelとAnswer modelの関係性について

今回作成するアプリは

  • 質問して
  • 質問に対しての回答をして
  • さらに回答に対して反応を示す

ことができるアプリを作るのでこのようなデータベース設計となります

Userは複数のQuestion(質問)を持つことができ、
Question(質問)は複数のAnswer(回答)を持つことができ
Answer(回答)は複数のReaction(反応)を持つことができます。

ここから先はQeustionモデルをベースに

  • model
  • service
  • template
  • view

を全て一気に作成して行きます
最後にBitbucketのリポジトリのリンクを記載しますので動かない場合は確認してください

ユーザーは先ほど作成したのでQuestionを作成していきます

Model

app/models/question.pyを作成

from app import db


# モデルに関する設定
class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(1000))
    body = db.Column(db.String(255))
    # Userに所有されている状態
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE', name='user_id__question_id_fk'))

    @classmethod
    def from_args(cls, title: str, body: str, user_id: int):
        instance = cls()
        instance.title = title
        instance.body = body
        instance.user_id = user_id
        return instance

 

app/models/user.pyを修正

from flask_login import UserMixin
# 追加
from sqlalchemy.orm import relationship
from werkzeug.security import generate_password_hash, check_password_hash

from app import db


# モデルに関する設定
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(1000))
    email = db.Column(db.String(100), unique=True)
    password = db.Column(db.String(100))

    # 追加
    # user.questions と記述することでhas_manyなquestionを取得することができます
    questions = relationship('Question')

    # モデルからインスタンスを生成するときに使います。(利便性を高めるため)
    # passwordの暗号化も自動で行うことができるので、安全性も高めることができます。
    @classmethod
    def from_args(cls, name: str, email: str, password: str):
        instance = cls()
        instance.name = name
        instance.email = email
        instance.email = email
        if password is not None:
            # passwordがあれば暗号化します。
            instance.hash_password(password)
        return instance

    # 暗号化するためのメソッド。
    def hash_password(self, clean_password):
        self.password = generate_password_hash(str(clean_password), method='sha256')

    # 登録したpasswordとユーザーがログインフォームで入力したパスワードが正しいかどうかのチェックを行うメソッド
    def check_password(self, clean_password):
        return check_password_hash(self.password, clean_password)

 

Service

app/services/question_service.py

データベースにデータを保存したり、取得してくるためのメソッドをまとめています

 

from app import db

from app.models.question import Question

from sqlalchemy.exc import SQLAlchemyError


def find_all() -> [Question]:
    return Question.query.all()


def find_one(question_id: int) -> Question:
    if question_id is None:
        raise Exception
    return Question.query.filter_by(id=question_id).first()


def save(question_id: int, user_id: int, data: {}) -> Question:
    try:
        if question_id is None:
            question = Question.from_args(
                data.get('title'),
                data.get('body'),
                user_id
            )
            db.session.add(question)
        else:
            question = find_one(question_id)
            question.title = data.get('title')
            question.body = data.get('body')
            question.user_id = user_id
        db.session.commit()
        return question
    except SQLAlchemyError:
        raise Exception


def delete(question_id: int) -> bool:
    if question_id is None:
        raise Exception
    try:
        question = find_one(question_id)
        db.session.delete(question)
        db.session.commit()
        return True
    except SQLAlchemyError:
        raise Exception

View

app/views/questions.py

http://localhost:5000/questions
http://localhost:5000/questions/add
などquestionに関わる特定のリンクにアクセスした際に発火するルーティングの設定です

from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import login_required, current_user

from app.services import question_service

questions = Blueprint('questions', __name__)


@questions.route('/')
@login_required  # ログインしていないと表示できないようにする
def find_all():
    questions = question_service.find_all()
    return render_template('questions/index.html', questions=questions)


@questions.route('/<question_id>')
@login_required  # ログインしていないと表示できないようにする
def find_one(question_id: int):
    question = question_service.find_one(question_id)
    return render_template('questions/show.html', question=question)


@questions.route('/add', methods=['GET', 'POST'])
@login_required  # ログインしていないと表示できないようにする
def add():
    try:
        if request.method == 'GET':
            return render_template('questions/post.html')
        else:
            # postとputを一つのメソッドでできるようにquestion_idを入れてあるが、
            # 新規作成時はNoneにしておく。二つ目のrequet.formはformから送られてくる情報をそのままserviceに渡す
            # current_userはflask_loginの機能で、現在ログインしているユーザーの情報を取得することができる。
            question = question_service.save(None, current_user.id, request.form)
            if question is None:
                flash('Questionを追加することができませんでした。')
                return redirect(url_for('questions.add'))
            flash('Questionを追加しました。')
            return redirect(url_for('questions.find_all'))
    except Exception:
        flash('Questionを追加することができませんでした。')
        return redirect(url_for('questions.add'))


@questions.route('/update/<question_id>', methods=['GET', 'POST'])
@login_required  # ログインしていないと表示できないようにする
def update(question_id: int):
    try:
        if request.method == 'GET':
            question = question_service.find_one(question_id)
            return render_template('questions/update.html', question=question)
        else:
            question = question_service.save(question_id, current_user.id, request.form)
            if question is None:
                flash('Questionを修正することができませんでした。')
                return redirect(url_for('questions.update', question_id=question_id))
            flash('Questionを修正しました。')
            return redirect(url_for('questions.find_all'))
    except Exception:
        flash('Questionを修正することができませんでした。')
        return redirect(url_for('questions.update', question_id=question_id))


@questions.route('/delete/<question_id>', methods=['POST'])
@login_required  # ログインしていないと表示できないようにする
def delete(question_id: int):
    try:
        question_service.delete(question_id)
        flash('Questionを削除しました。')
        return redirect(url_for('questions.find_all'))
    except Exception:
        flash('Questionを削除することができませんでした。')
        return redirect(url_for('questions.find_all'))

 

Template

登録フォームや個別ページなどのhtmlを作成します

app/templates/questions/index.html
questionの一覧ページです
templatesの中にさらにquestionsというフォルダを作っているので注意してください

{% extends "layout.html" %}

{% block content %}
    <h1>Questions</h1>
    <ul>
        <li><a href="{{ url_for('questions.find_all') }}">Index</a></li>
        <li><a href="{{ url_for('questions.add') }}">追加</a></li>
    </ul>

    {% for question in questions %}
        <p>{{ question.title }} // {{ question.body }}
            <span><button><a href="{{ url_for('questions.find_one', question_id=question.id) }}">detail</a></button></span>
            <span><button><a href="{{ url_for('questions.update', question_id=question.id) }}">update</a></button></span>
        <form action="{{ url_for('questions.delete', question_id=question.id) }}" method="POST">
            <button type="submit"><a>delete</a></button>
        </form>
        </p>
    {% endfor %}

{% endblock %}

 

app/templates/questions/post.html
questionの登録ページです
templatesの中にさらにquestionsというフォルダを作っているので注意してください

{% extends "layout.html" %}

{% block content %}
    <div>
        <h3>Questionの追加</h3>
        <div>
            <form action="/questions/add" method="POST">
                <div>
                    <div>
                        <input type="text" name="title" placeholder="Title" autofocus="">
                    </div>
                </div>

                <div>
                    <div>
                        <input type="text" name="body" placeholder="Body" autofocus="">
                    </div>
                </div>
                <button>追加する</button>
            </form>
        </div>
    </div>
{% endblock %}l

app/templates/questions/show.html
questionの個別ページです
templatesの中にさらにquestionsというフォルダを作っているので注意してください

{% extends "layout.html" %}

{% block content %}
    <h1>Questions</h1>
    <ul>
        <li><a href="{{ url_for('questions.find_all') }}">Index</a></li>
        <li><a href="{{ url_for('questions.add') }}">追加</a></li>
    </ul>
    <ul>
        <p>Title:{{ question.title }}</p>
        <p>Body:{{ question.body }}</p>
    </ul>
{% endblock %}

app/templates/questions/update.html
questionの編集ページです
templatesの中にさらにquestionsというフォルダを作っているので注意してください

{% extends "layout.html" %}

{% block content %}
    <div>
        <h3>Questionの追加</h3>
        <div>
            <form action="/questions/update/{{ question.id }}" method="POST">
                <div>
                    <div>
                        <input type="text" name="title" placeholder="Title" value="{{ question.title }}">
                    </div>
                </div>

                <div>
                    <div>
                        <input type="text" name="body" placeholder="Body" value="{{ question.body }}">
                    </div>
                </div>
                <button>修正する</button>
            </form>
        </div>
    </div>
{% endblock %}l

app/templates/layout.html
questionsへリンクを飛ばせるように、ログインしているときにリンクを表示します

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>QA-Site</title>
</head>

<body>
<ul>
    {# ログインしていれば表示 #}
    {% if  current_user.is_authenticated %}
        <li><a href="{{ url_for('index') }}">Index</a></li>
        {#ここに追加#}
        <li><a href="{{ url_for('questions.find_all') }}">Questions</a></li>
        <li><a href="{{ url_for('auth.logout') }}">ログアウト</a></li>
    {% endif %}

    {#ログインしていなければ表示#}
    {% if not current_user.is_authenticated %}
        <li><a href="{{ url_for('auth.signup') }}">新規登録</a></li>
        <li><a href="{{ url_for('auth.login') }}">ログイン</a></li>
    {% endif %}
</ul>
{# flaskのメッセージを表示 #}
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <div>
            {{ messages[0] }}
        </div>
    {% endif %}
{% endwith %}
{% block content %}
{% endblock %}


</body>

</html>

 

__init__.py

flaskアプリのペースファイルです
__init__.pyに作成したMoldeとviewを取り込んでflaskアプリから使えるようにします

# モジュールインポート
from flask import Flask, render_template
# データベースを利用するために追加
from flask_sqlalchemy import SQLAlchemy
# flaks-loginのライブラリ追加
from flask_login import LoginManager, login_required

# Flaskアプリの生成
app = Flask(__name__)

# ここから /// データベースの設定
app.secret_key = "super secret key"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///qa-site.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# ここまで /// データベースの設定

# sqlalchemyを通してflaskからdbアクセスをするための入り口
db = SQLAlchemy(app)

# flask-loginに関する設定
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

# データベースのimport
from app.models.user import User
from app.models.question import Question


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


# authに関するルーティングを追加
from app.views.auth import auth
from app.views.questions import questions

# authに関するルートをflaskアプリであるappに追加
app.register_blueprint(auth)
# url_prefixに「/questions」を入れると
# http://localhost:5000/questions/ というリンクになりわかりやすくなるので設定を追加しました
app.register_blueprint(questions, url_prefix='/questions')


# indexのルート設定
@app.route('/')
@login_required  # ここを追加
def index():
    return render_template('index.html')

 

データベースをターミナルからアップデート

追加したQuesetionを適用するためにこちらをターミナルで実行

$ python create_db.py

それでは画面をみていきましょう

http://localhost:5000

Questionsが追加され表示されました

ちなみにリンクはこんな感じです

 

「Questions」をクリックすると、、
無事に表示されました
ただしデータは一つもいれていないので空のままです

ちなみにquestionsのリンクはこんな感じになっています

複数のurlを作成するときはこのリンクが非常に重要なので
綺麗に分けていきましょう
今回はanswerやreactionもこんな感じで分けていきます

 

それでは

  • 追加
  • 個別ページ
  • 編集
  • 削除

ができるかどうかを確認していきます

 

追加

「追加」ボタンを押してpost.indexページに飛びます

適当にTitleとBodyを入力して「追加する」ボタンをクリック

 

questinosの一覧へ遷移し先ほど追加したデータが表示されております
UIは汚いですがひとまず追加と一覧表示は問題なさそうです

個別ページ

一覧の「detail」ボタンをクリックします
こちらも問題なく表示されました

 

編集

「update」ボタンをクリックして内容を変更してみます
適当にわかりやすい編集を加えて「修正する」ボタンをクリック

編集後は一覧画面へ戻り
問題なく編集が完了しました

 

削除

「delete」ボタンをクリックします
こちらも問題なく削除されました

 

まとめ

今回はログイン機能とQuestionを作成しました
templateやservice、viewはAnswerやReactionでもコピペして使えるベースなので
完全に理解できていないという方はなんども復習して理解を深めましょう

Bitbucketのリンク:https://bitbucket.org/Masahiro_Okubo/qa-site/src/phase1/
ブランチは「phase1」で指定してください。

参考リンク

FlaskでhtmlからDELETEやPUTなどのhttpメソッドを扱う方法メモ

How do I get my HTML button to delete the right list item from a SQLite database?

Buy Me A Coffee

関連記事

copyright© 2016-2021 Masahiro Okubo