DjangoでWebアプリを作ってみる。【vol. 11 学生用のレポート共有Webアプリを作ってみた。】

# イントロダクション

６月あたりにDjangoもそもそもPythonも今まで触ったことないけど、どんなもんだろうと実装していました。そんな折に、ちょうど知り合いから「学生用のレポート共有Webアプリが欲しい」的な話をいただいたので、１ヶ月ほど集中して作ってみました。

リリースはまだ先でこれからもデザイン調整や項目変更はありそうなのですが、概ね完成した段階なので、ここらでまとめてみたいと思います。今回、[ReadMe](https://github.com/TsJazz27Sumin/nakamuratamara/tree/feature/qiita)に記載していますように、非常に多くの記事やブログに助けられました。感謝の意味も込めて以下記載となります。

# 機能構成

- 管理者向け機能
    - 認証
        - ログインURLリクエスト
        - ログイン
        - ログアウト
    - レポートメンテナンス
        - 検索
        - ソート
        - ページング
        - レポート登録
        - レポート削除
        - レポートダウンロード
    - ユーザーメンテナンス
        - 検索
        - ソート
        - ページング
        - ユーザー登録
        - ユーザー更新
        - ユーザー削除
    - アクセスログ閲覧
        - 一覧
        - CSVダウンロード
- 学生向け機能
    - ログイン
    - 一覧
    - 一覧切り替え
    - レポートダウンロード

# アーキテクチャ

普段、自分がJavaやC#でやっている「Layerd Architecture x CQRS」な形にしています。
[ルート階層](https://github.com/TsJazz27Sumin/nakamuratamara/tree/feature/qiita/songcycle/apps/student)のフォルダ構成から以下抜き書きになります。

- decorators
    - ログ出力と認可処理用のデコレーターを配置している。
- forms
    - HTML上のフォームに対応するクラス群を配置している。
- functions
    - 複数のクラスから共通利用されるような処理を配置している。
- models
    - DBのテーブルに対応するモデルクラスを配置している。
- queries
    - 【2層目】検索を取り扱う。
        - CQRSのQueryに対応させている。
        - 検索のDBアクセスを担当する。
- repositories
    - 【3層目】登録・更新・削除のDBアクセスを担当する。
- services
    - 【2層目】ビジネスロジックを担当する部分。登録・更新・削除を取り扱う。
        - CQRSのCommandに対応させている。
- templates
    - HTMLテンプレートを配置している。
- templatetags
    - HTMLテンプレートで使用される表示処理用の関数群を配置している。
- views
    - 【1層目】MVCで言う所のController、DjangoではViewと呼ばれる。

Dipendency Injectionについては、[ライブラリ](https://qiita.com/mkgask/items/d984f7f4d94cc39d8e3c#python%E3%81%AEdi%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)を使用すればできるようですが、今回は見送りました。理由としては、UTでモックを差し込む時ぐらいしかメリットを感じたことがないし、お約束ごとのようにinterface的なものを用意して実装するのも何か違うかなと思ったからです。

もっと大規模なアプリケーションであれば、こちらの記事にあるように

>interfaceを使ってメソッドの実装を強制し、クラスに直接依存していたのをinterfaceへの依存にする
>[Pythonのinterface代替モジュール abc と 依存性の逆転](https://qiita.com/mkgask/items/d984f7f4d94cc39d8e3c#python%E3%81%AEinterface%E4%BB%A3%E6%9B%BF%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB-abc-%E3%81%A8-%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E9%80%86%E8%BB%A2)

することが必要になると思います。

# 工夫した点
### 管理者向け機能では、ログインIDとパスワードの認証ではなく、一時的なログインURLを発行するようにした。

ログインIDとパスワードの認証ですが、昨今よく事件が起きているように漏れるといとも簡単に破られてしまいます。そのためQiitaでもGitHub等でも二要素認証を設定するようになっていますが、何か他の仕組みはないものかと検討して今回実装しました。

[loginservice.send_login_url](https://github.com/TsJazz27Sumin/nakamuratamara/blob/feature/qiita/songcycle/apps/student/services/loginservice.py#L49)
※リンク先は、SendGrid部分はコメントアウトしてあります。

```py

from django.utils.crypto import get_random_string
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
...
    def send_login_url(self, email):

        # ここでランダムな文字列をワンタイムパスワードとして発行する。
        onetime_password = get_random_string(200)
        # ログインURLに付加して送付用のURLを作る。
        login_url = self.__master_query.get_root_login_url() + onetime_password

        # メールアドレスとワンタイムパスワードの組み合わせを登録する。
        # 一定時間経ったらこの組み合わせ自体が無効になるようにしています。
        self.__temporarily_login_url_repository.insert(email, onetime_password)

        message = Mail(
            from_email=develop.SENDGRID_FROM,
            to_emails=email,
            subject='ログインURLのお知らせ',
            html_content='一時的に有効なログインURLです。<br>' + login_url
        )

        sg = SendGridAPIClient(develop.SENDGRID_APIKEY)
        sg.send(message)
```

ログインURLのリクエスト画面では、ユーザーにメールアドレスを入力してもらいます。
メールアドレスについては、事前に登録済かどうかチェックした後に、ワンタイムパスワード付きのログインURLをメールで送付するようにしています。

そのままログイン出来ないのは手間なのですが、これならメールボックスを乗っ取られない限りはとりあえず大丈夫です。

メール送信のサービスとしては、SendGridを使用しています。SendGridのPython実装は、いくつかバージョンがあるので上手く動作しなかったりしたのですが、[こちらのサイト](https://www.mathpython.com/ja/sendgrid/)などを参考にしながら、メール送信が可能なところまで実装してあります。

ワンタイムパスワードについては、`django.utils.crypto`の`get_random_string`を使用しています。なお、Djangoには元々認証用の機構が整備されていますが、今回は使用していません。関連するところとしては、セッション管理の機構は利用しています。

### なるべく安く済ませたいのでストレージとしてGoogle Driveを使うことにした。

学生のレポートとしてWordのファイルを保管する必要があるのですが、AWS・Azure・GCPいずれもお金がかかります。保管はファイルのサイズが大したことないので、微々たるものなのですが、[ダウンロードのリクエスト単体でも課金がされます。](https://qiita.com/kawaz/items/07d67a851fd49c1c183e)

計算してみれば微々たるものだとは思うのですが、やるなら0円運用を目指したい！ということで[GoogleのDriveAPI](https://developers.google.com/drive/api/v3/quickstart/python?hl=ja)を使うことにしました。保存先は、特定の個人アカウントのGoogleDriveです。

サンプルコードやドキュメントもGoogleがだいぶ整備しているので試しやすかったです。
ちょっと引っかかったのは、Wordのファイルをアップロードしたはいいが、どうやってダウンロードするの？ってところでした。

[googleapiservice.download＿file](https://github.com/TsJazz27Sumin/nakamuratamara/blob/feature/qiita/songcycle/apps/student/services/googleapiservice.py#L90)

```py

    def download＿file(self, google_file_id):

        service = self.__get_service(self.__scopes)

        file = service.files().export(
            fileId=google_file_id,
            mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        ).execute()

        return file
```

exportのメソッドを使って、wordのmimeTypeを指定して実現しています。
[Python用のAPIライブラリ](https://developers.google.com/resources/api-libraries/documentation/drive/v3/python/latest/?hl=ja)も整備されているし、ドキュメントも英語ではあるもの割と使いやすい印象でした。

### Decoratorを実装してAOPっぽいことを実現した。
Spring BootやASP.NET MVCをやってきた身からすると、Controllerのメソッドにアノテーション付けて、認証のバリデーションとかロギングとか出来るよね！という感じだったのですが、Djangoにはそんなものはありませんでした。

ただ、Decoratorなるものを使えば似たようなことは出来そうなので実装してみました。
アクセスログを書き出すのと認可処理です。

[decorator.authenticate](https://github.com/TsJazz27Sumin/nakamuratamara/blob/feature/qiita/songcycle/apps/student/decorators/decorator.py#L48)

```py
import logging

from django.shortcuts import redirect
from user_agents import parse
from django.http.response import JsonResponse

logger = logging.getLogger("student")

...

def authenticate(function_name):
    def __decorator(function):
        def wrapper(*args, **kwargs):

            # アクセスしてきたらまずログを書き出す。
            __output_ordinary_log(args, function_name)

            if 'authority' not in args[0].session:
                # 権限が不明な場合は、セッション切れ、もしくは不正アクセスと見なし、強制ログアウト
                return redirect('request_login')
            return function(*args, **kwargs)

        return wrapper
    return __decorator

...

def __output_ordinary_log(args, function_name):
    request = getattr(args[0], 'request', args[0])
    user_agent = parse(request.META['HTTP_USER_AGENT'])
    remote_addr = request.META['REMOTE_ADDR']

    user_id = ""
    if 'user_id' in request.session:
        user_id = request.session['user_id']

    logger.info(
        '{} : {} : {} : {}'.format(
            user_agent,
            remote_addr,
            function_name,
            user_id))
```

ログ出力は、ユーザーエージェントから色々情報を取るのですが、Parseの処理はライブラリを使用しました。`pip install user-agents`で使えるようになります。自前で加工する処理を書かなくて済んだので、とても便利でした。

ログとしては、こんな形で「いつ誰がどんなデバイスでどこから何のメソッドを呼んだのか」が分かるようにしています。

`【INFO 】2019-08-11 22:52:50,219 : decorator : PC / Mac OS X 10.14.6 / Chrome 76.0.3809 : 127.0.0.1 : index : U0004`

作ったdecoratorは、こういった形でメソッドの前に@を付けて使います。
JavaとかC#でWebアプリを作っている人は、馴染みのある形ではないでしょうか。

[homeview.home](https://github.com/TsJazz27Sumin/nakamuratamara/blob/feature/qiita/songcycle/apps/student/views/homeview.py#L9)

```py
@decorator.authenticate("home")
def home(request):
    context = {'authority_name': request.session['authority']}

    return render(request, 'student/home.html', context)
```

これでhomeの処理が始まる前にアクセスログが出力されて、不正アクセスであれば事前に弾かれます。

# その他

フロントエンドは、CSSでそのまま書くのはちょっと嫌だったので、stylusを導入してみたり、Ajaxで差分更新にしてSPAっぽくしてみたりしています。ただ、Ajaxでゴリゴリ書いた後に「やっぱりこういうことやるんだったら、ReactやVue.jsを使った方が楽なんだろうな」というちょっとした後悔はありました。

それ以外にも実際、一からアプリケーションを作ってみると色々気づくことが多かったです。PythonとDjango自体も一通り実装してみると、どんなことが出来て、逆に出来ないことが何かというのが身に染みて理解出来ました。

個人的には、DjangoはSpring BootやASP.NET MVCと比べると、フレームワークとして洗練されてない印象を受けました。Decoratorで実装した部分とか主にView周りです。ORMについては、[Spring Data JPAであるようなメソッド名で処理を定義できる](https://qiita.com/sndr/items/af7d12be264c2cc4b252)ものはないですが、JavaやC#のORMとあまり変わらない印象でした。

UTの機構も、JUnitやNUnitとほぼ同じ印象ですが、デフォルトでテスト用のDBに繋がる機構が用意されている点はいいと思いました。

きっとJavaやC#と比べると、PythonでWebアプリケーションを作ること自体がこれからのものだということだと思っています。

以上、PythonやDjangoを学習している方々の何か参考になれば幸いです。