Flaskでログイン状態の時ユーザ名を常に表示する(sessionとg)


# 目次
- [はじめに](#はじめに)
- [ディレクトリ構成](#ディレクトリ構成)
- [ログイン時の設定](#ログイン時の設定)
- [全ページに表示する](#全ページに表示する)
- [余談 render_templateとg](#余談-render_templateとg)

# はじめに
Flaskでログイン機能を実装し、ログインしていることをわかりやすくするためにユーザー名を表示したいと思ったのですが、全てのページに同じ内容（今回はユーザー名）を表示する際、簡単な方法が無いか調べてみました。
結論に至るまでの試行錯誤も余談として記録しようと思います。

## 結論
ログイン時にsessionにユーザー名を格納し、base.html等全ページに適用されるhtmlで表示させます。

## 環境
| ソフトウェア | バージョン |
|:-:|:-:|
|  Python |  3.9.0 |
|  Flask | 2.2.2  |
| Flask-Login    | 0.6.2  |



# ディレクトリ構成
```
flask
├ flask_app
│ ├ models
│ ├ templates
│ │ ├ base.html
│ ├ └ ...
│ ├ views
│ │ └ auth.py
│ ├ __init__.py
│ └ ...
└ tests
```


[Flaskのflashの内容をpytestでテストする](https://qiita.com/yuri_t/items/04e8221f59da9e43673b)と同様です。今回はviewフォルダ内のauth.py、templlatesフォルダ内のbase.htmlを使用します。

# ログイン時の設定

```Python:auth.py

from flask import Blueprint, render_template, request, \
    flash, url_for, redirect,session
from flask_login import login_user
from werkzeug.security import check_password_hash
from sksk_app.models.questions import User

auth = Blueprint('auth', __name__, url_prefix='/auth')

@auth.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form['email']
        password = request.form['password']

        user = User.query.filter_by(email=email).first()
        
        if not user or not check_password_hash(user.password, password):
            flash('パスワードが異なります')
            return redirect(url_for('auth.login'))

        flash('ログインしました')
        login_user(user)
        session['user_id'] = user.id
        session['user_name'] = user.name
        return redirect(url_for('pg.toppage'))

    page_title = 'ログイン'
    return render_template('auth/login.html',page_title=page_title)
```
実際のコードではなく、importなどは今回の設定に必要なものだけに絞っています。
ログインの仕組みについては本記事では割愛します。
こちらのログイン関係のコードは [Flask-Login を使用してアプリケーションに認証を追加する方法 | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login-ja) を大いに参考にしています。

## sessionへのユーザー名の格納
```Python:auth.py
login_user(user)
session['user_id'] = user.id
session['user_name'] = user.name
```
「login_user」メソッドでログインした後に、sessionにそれぞれ値を格納します。
>session is a dict that stores data across requests. When validation succeeds, the user’s id is stored in a new session. The data is stored in a cookie that is sent to the browser, and the browser then sends it back with subsequent requests. Flask securely signs the data so that it can’t be tampered with.
[Login - Blueprints and Views — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/tutorial/views/#login)


「session['user_name']」に、ログイン時に表示したい名前を格納します。

# 全ページに表示する

1つのコードで全てのページに表示させるには、base.htmlという全てのページに共通するファイルを用意し、それぞれのページで読み込みます。

>Each page in the application will have the same basic layout around a different body. Instead of writing the entire HTML structure in each template, each template will extend a base template and override specific sections.
[Templates — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/tutorial/templates/)

このbase.htmlに「session['user_name']」を表示させれば良いだけです。

注：　Web上の記事や書籍では、このBase Layoutのbase.htmlをlayout.htmlとするところも多いです。私は公式チュートリアルに沿ってbase.htmlにしています。

## base.html
表示させたいところに「{{session['user_name']}}」を記述します。
下記の例ではpタグの中に表示、続けて「さん」を表示させる様にします。
```html:base.html
<p> {{session['user_name']}} さん</p>
```
これで全ページにユーザー名を表示することができます。


# 余談 render_templateとg

## render_templateで全ページに設定？
ページ間で値をやりとりするとしたらsessionを使用するのは何かの機会に知りました。
また、画面に変数の値を表示する方法といえば、render_template()でHTMLを生成する際に変数を設定する方法しか知りませんでした。

ということで、render_template()とsessionを組み合わせるのかなあと思ったのですが、そのやり方でやると、viewの全ての関数でsessionから情報取得→render_templateで値の受け渡し、という膨大なコードが生まれてしまいます。
世の中のアプリがこんな面倒なことをしているとは思えないので、何か良い策は無いかなあと思っていました。

sessionもbase.htmlによるhtml生成も知っていたのに、なぜか二つを組み合わせることが思いつきませんでした。この方法を教えてくれた以下のStacOverflowの解答に感謝です。
- [python - How to display username in multiple pages using flask? - Stack Overflow](https://stackoverflow.com/questions/59380641/how-to-display-username-in-multiple-pages-using-flask)

## 公式チュートリアルのやり方　Flask.g
公式ではFlask.gというものを使用して、ユーザー名の表示をしています。

```html:base.html
{% if g.user %}
  <li><span>{{ g.user['username'] }}</span>
  <li><a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
  <li><a href="{{ url_for('auth.register') }}">Register</a>
  <li><a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
```
[Templates — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/tutorial/templates/)より一部抜粋

この「g」とはなんでしょうか？
アプリケーション・コンテクストを通じてデータを格納できる場所とのことです。
>A namespace object that can store data during an application context. This is an instance of Flask.app_ctx_globals_class, which defaults to ctx._AppCtxGlobals.
This is a good place to store resources during a request. 
[flask.g - API — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/api/#flask.g)

ではアプリケーション・コンテクストとは？
アプリケーションレベルのデータをリクエストの間に記録しておくものです。

>The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.
[The Application Context — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/appcontext/)

なんだか良さそうなものの気もしますが、sessionが「複数のリクエストにまたがって」（session is a dict that stores data <font color="red">across requests</font>.）データを格納しておくのに対し、アプリケーション・コンテクストは「1つのリクエスト・CLIコマンド・その他のアクティビティの間」(The application context keeps track of the application-level data <font color="red">during a request</font>, CLI command, or other activity.)データを記録するもの、とあります。

従って、リクエストが新たに作られる度にgにデータを格納する必要があります。
公式チュートリアルのアプリではgを利用するために、以下のようなコードがあります。
```Python:auth.py
@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()
```
>bp.before_app_request() registers a function that runs before the view function, no matter what URL is requested. 
[Login - Blueprints and Views — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/tutorial/views/#login)

before_app_request()で、View機能が実行される前に、sessionからユーザーのidを取得し、そこからDBからユーザー情報を取得、gに格納、という流れです。

before_app_request()はbefore_request()のようなものだけど、Blueprintで扱うものに対して実行されるようです。

>Like before_request(), but before every request, not only those handled by the blueprint. Equivalent to Flask.before_request().
[before_app_request(f) - API — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/api/#flask.Blueprint.before_app_request)

before_request()はBlueprintを適用していないものに対し、各リクエストの前に機能を登録して実行するものです。

>Register a function to run before each request.
For example, this can be used to open a database connection, or to load the logged in user from the session.
[before_request(f) - API — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/api/#flask.Blueprint.before_request)

おそらくリクエスト毎にこの処理を行う目的として、本当にそのユーザーかどうかの確認をするものだと思うのですが、1つのリクエスト毎にDBから情報を取得することが疑問だったので、とりあえずこの処理は省くことにしました。

そもそもgを実装したのは、公式チュートリアルのテストを実行する際に、gから情報を取得することもテスト項目に入っていたからでした。とりあえず今のところはテストも含め、gは利用せず、sessionでユーザー名の表示をしておこうと思います。

# 参考
- [Flask-Login を使用してアプリケーションに認証を追加する方法 | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login-ja)
- [Blueprints and Views — Flask Documentation (2.2.x)](https://flask.palletsprojects.com/en/2.2.x/tutorial/views/)
- [python - How to display username in multiple pages using flask? - Stack Overflow](https://stackoverflow.com/questions/59380641/how-to-display-username-in-multiple-pages-using-flask)
- [python - Display username in base.html in Flask - Stack Overflow](https://stackoverflow.com/questions/64439987/display-username-in-base-html-in-flask)