Flask使ってWebアプリケーション作ってみたお話

この記事は[SLP KBIT AdventCalendar2018](https://qiita.com/advent-calendar/2018/slp_kbit) 25日目の記事です。
今回が初投稿です、がっきーです。

今回はサークル活動の一環でWebアプリケーションを作成した奮闘記および備忘録をお送りします。
Webアプリケーションを作るのは今回が初めてだったので、シンプルな課題管理アプリ作成に取り組みました。

## やっていき

### 環境

- Ubuntu 16.04 LTS
- PostgreSQL 9.5.14

### 使用言語

- Python3
- JavaScript
- HTML

RDBMSにPostgreSQL、バックエンドの実装に[Flask](http://flask.pocoo.org/)をそれぞれ用いました。
またテンプレートエンジンに[Jinja2](http://jinja.pocoo.org/)、RDBMSへの接続に[psycopg2](http://initd.org/psycopg/docs/)というモジュールを用いました。

### 要件定義

- ユーザ登録制にする。
- 課題の追加、編集、削除ができる。
- 課題のステータスに応じた一覧を表示できる。

といったことを最低限の目標に進めていきました。

ここから実際の作業に移ります。
先に流れだけ示しておきます。

1. FlaskでHTTPサーバを構築する
2. ViewをJinja2を用いて作る
3. DBとの接続をしてデータの処理を実装する

一つずつさらっていきます。

## FlaskによるWebサーバの構築

Flaskのチュートリアルに則って以下の通りに実装します。

```Python:run.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def tutorial():
    return 'Complete!'

if __name__ == '__main__':
    app.run()
```

次にコンソール上で以下のようにして実行すると起動できます。

```bash
python3 run.py
```

あとはローカルで
http://127.0.0.1:5000
にアクセスすれば画面に**Complete!**と表示されます。

ちなみにapp.pyの中の**@app.route('/')**を含めた三行、ここでルーティングをしています。
雑多に説明するとhttp://***以降のパスをここで指定した場所にした時、def以下の処理をしてくれるようになります。
また、@app.route()内で、リクエストを受け付けるHTTPメソッドや、URLパラメータを指定することもできます。
受け付けるHTTPメソッドを指定してのルーティングはあとでもう少し詳しくお話します。

続いてViewの作成の話に移ります。

## Viewの作成
今回作ったページは以下の通りです。

- ログイン画面(index.html)
- ユーザ登録画面(regist.html)
- タスク一覧(list.html)
- タスク追加画面(add.html)
- タスク編集画面(edit.html)

ここに関しては現状はHTMLとJavaScriptだけで書いてます。

HTML側は登録した課題や入力フォームを表示するためのtableタグと、
課題の登録やログインためのformタグでほぼすべてを構成しています。
またJavaScriptで必須入力が不足していないかの判定、
課題の登録ステータスに応じた、表示の変更といったことを実装しています。

ページ間の遷移の概略は以下の図の通りです。
![sitemap](https://qiita-image-store.s3.amazonaws.com/0/309137/725cfb3c-80dc-a7fa-134d-ada124851444.jpeg)

最後にバックエンドの実装に移ります。

## バックエンド

この実装に一番時間がかかりました。
やったことだけ簡単にまとめると以下の通りです。

- DB作成
- DB操作
- 詳細なルーティング
- クライアントとサーバでのデータの受け渡し

### DB作成

DB周りはPostgreSQLを活用して実装しています。
PostgreSQLはRDBMSの一つでデータがまとめられてるものを管理するミドルウェアです。
他にもMySQLとかSQLiteとかOracleDatabaseとかいろんな種類があります。

DBを作るのも簡単です。
superuser権限を持ったユーザpostgresがいるはずなので、まずはこのユーザに切り替えてPostgreSQLを起動します。

```bash
$ su - postgres
$ psql
```
先ほども言ったようにこのユーザはsuperuserなのであらゆることができます。
早速DBを作ってしまいます。

```
# CREATE database todo;
```

これでtodoという名前のデータベースの出来上がりです。
ちなみにこのデータベースに対してユーザは作っていないので、
現状ここに対して操作ができるのは製作者であるpostgresだけです。
ちなみに**\l**と叩いてもらえればDBの一覧が見れるので、作れているか確認することができます。

もっともこれでは箱ができただけなのであまり何にもなりません。
中身を作っていきます。
この中身をテーブルといって、複数のカラム(列)から構成されています。
これは作りたいもの、備えたい機能に応じて考えることになります。
この時、それによって一意にレコード(行)が決まるようなカラム(主キー)を作ることを忘れてはいけません。
自分はこれを忘れて作業中に頭を抱えることになりました(結局追加しました)。

では実際にテーブルを作っていきます。
今回は実際に自分が作った課題を管理しておくtableを例に出します。
この作業はテーブルを追加したいDBにつないでから行います。

```
# \c todo;
# CREATE table task (content text, status text, deadline date, detail text, username text, id text PRIMARY KEY);
```

これでtext型のcontent、status、detail、username、idとdate型のdeadlineを持ったtaskという名前のテーブルがtodoというDBの中にできました。
idに"PRIMARY KEY"と指定していますが、このカラムが先ほど言った主キーになります。
より複雑に具体的にしようと思えば、他のテーブルのこのカラムの値しか入れられないとか、
NULLが入ることを許すかどうかとか、様々な制約をかけることも可能らしいです。
ちなみに今回はこのほかに、ユーザを管理するテーブルも準備しました。

さて、これでデータの操作・管理を行う下準備が整いました。
いよいよ動きをつけていきます。

### コネクト

データのやり取りの流れは、以下の通りです。
1. ユーザからの要求をアプリケーションの方で受ける
2. DBに対して処理を行う
3. 結果をユーザに返す

ただ標準のPythonではDBに対して操作をすることができません。
ここでpsycopg2を使っていきました。
これを使うことで、DBとアプリケーション間でコネクションを作ることができ、DBの操作をすることができるようになります。

実際にコネクションの確立だけすると一例としては以下の通りです。

```Python:connect.py
import psycopg2

conn = psycopg2.connect("host=127.0.0.1 port=5432 dbname=todo user=postgres password=postgres")
```

hostには接続先のアドレス、portに接続先のポート番号(これは省略可能で省略時に5432が使われます)、dbnameは接続するデータベースの名前、userは接続に使うデータベースユーザ(これも省略できます。省略したらOSユーザになります)、passwordにはuserのパスワードを入れるようになります。

また、実際にデータベースを操作するためにはカーソル(cursor)と言われるものが必要になります。
先ほどのconnect.pyに以下の内容を追記します。

```Python
cur = conn.cursor()
```

これをすることでいろんなメソッドを利用することができるようになります。

### 実装

ここまでできたらようやくデータの処理及び受け渡しに移っていけます。
今まで例に出してきたデータベースなどを利用して、サンプルコードを書いていきます。解説は後ほど行います。

```Python:app.py
from flask import Flask, render_template, request, redirect, url_for, session
import psycopg2

app = Flask(__name__)
conn = psycopg2.connect("host=127.0.0.1 dbname=testdb user=postgres password=postgres")
cur = conn.cursor()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/list', methods=['POST', 'GET'])
def display(): #ユーザ名とパスワードが正しいか確認
    if request.method == 'POST':
        name = request.form['username']
        password = request.form['password']
        cur.execute("SELECT 1 from users WHERE username = '" + name + "' AND id = '" + password + "'")
        result = cur.fetchall()
        if not result: #正しくなければ差し戻す
            return render_template('index.html')
        else: #正しければリスト画面に移動
            session['name'] = name
            cur.execute("SELECT * from task WHERE username = '" + name + "'")
            task = cur.fetchall()
            return render_template('list.html', name=name, task=task)
    else: #GETメソッドであればセッションからユーザ名を取得して課題をまとめてリスト画面に遷移
        name = session.get('name')
        cur.execute("SELECT * from task WHERE username = '" + name + "'")
        task = cur.fetchall()
        return render_template('list.html', name=name, task=task)

@app.route('/add', methods=['POST', 'GET'])
def add(): #課題の追加
    if request.method == 'POST': #formのvalueを追加する。
        cur.execute("SELECT COUNT(id) FROM task")
        rows = cur.fetchall()
        for row in rows:
            ids = rows[0] + 1
        cur.execute("INSERT INTO task (content, status, deadline, detail, username, id) VALUES (%s, %s, %s, %s, %s, %s)", request.form['contents'], request.form['status'], request.form['deadline'], request.form['detail'], session.get('name'), ids)
        cur.commit()
        return redirect(url_for('display'))
    else:
        return render_template('add.html')

```

ログインと課題の追加の部分だけ載せています。
実装した全容は[GitHub](https://github.com/gacky35/CompleteTasks)にあげています。

まだ解説入れてないところを簡単にお話していきます。
**render_template**
ここでは第一引数で指定した場所に遷移をしてくれます。
ちなみにこの遷移先はFlaskの場合は、実行ファイルと同じ階層に作ったtemplatesという名前のディレクトリにあるものである必要があります。
第二引数以降ではサーバ側から、クライアント側にデータを渡すということを実現しています。

**redirect(url_for())**
url_forの引数で指定した関数を呼び出すページにリダイレクトします。
url_forを書かないのであれば、redirectの引数に遷移先のパスを書けばいい。

**methods=['POST', 'GET']**
これはクライアント側からのPOSTとGETメソッドを受けて処理ができるということを記述しています。
POSTメソッドを受けたい場合これを明記しておかないとMethod not allowedとエラーを返されます。
GET単体であれば明記する必要はありませんが、POSTもGETもということになると両方明記する必要があります。
どのメソッドに対してどの処理を行うかはif文で分岐させれば大丈夫です。

**request.form**
クライアント側からのformの値を引っ張ってくることができます。(POSTメソッドの時)
request.form['status']とすると、formの中のnameがstatusの部分のvalueを返してくれます。
GETメソッドの場合はrequest.args.get('status')と書けばできます。(この場合、正確にはQueryStringから値を取得しています。)

**cur.execute, cur.fetchall, cur.commit**
executeでデータベースに対して操作を行い、データを抽出してきた場合はそれを取り出すためにfetchallをします。fetchには一つだけ取り出すfetchoneなどもあります。
またデータベースのデータに変更を加える場合は、デフォルトではトランザクションが有効になっているため、commitをしないとちゃんとやったことが反映されません。autocommitをtrueにしておけばなくても大丈夫です。

## おわりに
ここまで書いてきた内容を利活用すれば、Webアプリケーションの形はとりあえず作れるということが経験できました。
まだまだ知識として甘い部分もあるので、今後詰めていければという気持ち。叱責もいただければ幸いです。
今後はCSSをちゃんと書いて体裁を整えたいと思います。
そして機能面でも、以下のようなものを実装できればなと思っています。

- 課題の内容を確認する画面を別に作って、そこから編集・削除ができるように変更する
- パスワードの変更機能
- 通知機能
- チーム・フレンド間での課題の共有機能
- Safari及びIEへの対応(弾く方向になるかも)

また、サーバ管理も経験してみたいという気持ちもあるので学生のうちにリリースできるように頑張っていくつもりです。