俺的Djangoベストプラクティス雛形までの一連の流れ

# 俺的Djangoベストプラクティス雛形
Djangoプロジェクト開発において、先人の知恵に大いに助けられた。
まとまりつつある俺的Djangoベストプラクティス雛形を構築するまでの手順を記録しておく。

2019年10月01日　最新版に更新
2020年09月01日　いまだ現役のプラクティス
2023年01月06日　Django 4.1対応版に更新

# 大まかなキーワード
* python環境
 * pipenv
* Django環境
 * 開発版/テスト版/デプロイ版の切り替え, 保守性, git管理
* アプリケーション開発
 * パッケージ化
* デプロイ　
 * pipenvの準備, スクリプトで一括処理
 
# python環境の準備 pipenv
python周りの管理はpipenvを使う。
バージョン管理、開発環境の分離、スクリプトの一元管理を叶えてくれる便利な奴。
ハングアップを疑うくらい遅い時がある。

## pyenv インストール
```shell:pyenvのインストール
$ sudo yum -y install gcc make zlib zlib-devel openssl openssl-devel
$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
```

## pipenv インストール
pipenvはbrewやpipでインストールできる。
便利なので仮想環境でなくグローバルにインストールしていいと思う。

```shell:pipenvのインストール
$ pip install pipenv
```
諸設定を環境変数に登録する。

```shell:諸設定を環境変数に設定するために、シェルのプロファイル系ファイルに追記
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bash_profile　# パス通し
$ echo 'export PIPENV_VENV_IN_PROJECT=1' >> ~/.bash_profile # スクリプトでvenvを使いやすくするために、venvをプロジェクトディレクトリ内に作ってもらう設定
```

## pipenvの初期設定
pipenvでpythonを始める。

```shell:プロジェクトで使うバージョンの指定
$ pyenv install --list # インストール可能なバージョンの一覧
$ pipenv --python <バージョン>
```
始めて起動するとPipfileとPipfile.lockができる。
Pipfileのscriptsブロックに任意のスクリプトを記述できる。

```shell:Pipfile
[scripts]
init = "pipenv install"
init_dev = "pipenv install --dev"
server = "python manage.py runserver 0.0.0.0:8000"
```
```shell:Pipfileのscriptsの使い方
$ pipenv run スクリプト名
```

## pipenvでpython
次に、仮想環境のON/OFF。

```shell:pipenvで仮想環境の有効化
$ pipenv shell # 仮想環境のON　未作成であればこの時に作成
$ exit # 仮想環境のOFF　Ctrl + dも可
```

スクリプトにこれを書くと、仮想環境に入って止まるので、
スクリプト内で仮想環境に入りたい場合は次のように書く

```shell:pipenvで仮想環境の有効化(スクリプト)
source .venv/bin/activate # 仮想環境のON
deactivate # 仮想環境のOFF
```

## pipenvでパッケージ管理
基本的な使い方はpipと同じ。
ただし、開発環境にだけインストールしたいものは分離できる。

```shell:パッケージインストール
(project)$ pipenv install パッケージ名
(project)$ pipenv --dev install パッケージ名 # 開発環境only
```

別環境でパッケージを復元するとき、開発用を含めるかどうか選択できる。

```shell:パッケージの復元
(project)$ pipenv install
(project)$ pipenv install --dev # 開発環境用パッケージもインストールする
```

# 仮想python環境でDjangoプロジェクトを立ち上げる
```shell:Djangoをインストール
(project)$ pipenv install django
```

```shell:Djangoプロジェクトを立ち上げる
(project)$ django-admin startproject config .
```
```shell:プロジェクトディレクトリの構造
testDjango
│── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── .venv
└── manage.py
```

# settings.pyを実行環境に応じて変更できるようにする
セキュリティやパフォーマンスの観点から、開発版、テスト版、デプロイ版それぞれでALLOWED_HOSTS、データベースの設定、DEBUGなどを切り替えたほうが良い。
環境に適した設定で動作させるために、settings.pyの内容を分割する。
実行時にどの設定ファイルを読み込むかを指定できる。
ファイル名や分割の粒度は任意。

```shell:settings.pyの分割
(project)$ mkdir config/settings
(project)$ mv config/settings.py config/settings/base.py # 既存のsettings.pyを分割後の汎用ファイルとする
(project)$ vi config/settings/base.py # 汎用設定ファイルから分割する項目を削除する
(project)$ vi config/settings/develop.py # 開発版だけに適用したい設定を記述
(project)$ vi config/settings/test.py # テスト版だけに適用したい設定を記述
(project)$ vi config/settings/deploy.py # デプロイ版だけに適用したい設定を記述
(project)$ touch config/settings/__init__.py # config/settingsディレクトリをパッケージ化
```
```shell:プロジェクトディレクトリの構造
testDjango
├── config
│   ├── __init__.py
│   ├── settings
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── deploy.py
│   │   ├── develop.py
│   │   └── test.py
│   ├── urls.py
│   └── wsgi.py
├── .venv
└── manage.py
```

設定ファイルのディレクトリ階層が一つ深くなったのでbase.pyを修正する。

```diff:base.py
- BASE_DIR = Path(__file__).resolve().parent.parent
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
```

各分割ファイルでbase.pyを読み込むことで、それぞれが一つの完結したsettings.py系ファイルになる。

```python:分割ファイルの書き方(例：deploy.py)
from .base import * # base.pyを読み込む 

# base.pyにない分を補完

DEBUG = False

ALLOWED_HOSTS = ['公開するホスト']
```
実行時にどの設定ファイルを読み込むか指定すれば環境ごとに異なる設定で実行できる。
デプロイ版ではuwsgiやgunicornで設定ファイルを指定すればいい。

```shell:seetingsファイルを指定して実行
(project)$ python manage.py runserver 0.0.0.0:8000 --settings=config.settings.develop
```
> ファイルの指定はパスをドット区切りで記述する。

Pipfileに書いておくと楽できる。

```shell:Pipfile
[scripts]
server = "python manage.py runserver 0.0.0.0:8000 --settings=config.settings.develop"
```
```shell:Pipfileのスクリプトで起動
(project)$ pipenv run server
```

ちなみに、設定ファイル無指定の場合のデフォルト値はmanage.pyに記述してある。
ここを書き換えておくと便利だけど、実行時の指定をサボらないなら必須ではない。

```python:manage.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.develop')
```

# static, templateをプロジェクトディレクトリ直下に配置する
djangoで静的ファイルやテンプレートを呼び出すと、settings系で設定されたBASE_DIR直下にあるstatic,templatesディレクトリとアプリケーションディレクトリ以下にあるstatic,templatesディレクトリを検索し、**最初にヒットしたもの**を使用する。
複数アプリケーションを開発しているとき、静的ファイルやテンプレートの名前が被ると意図しないものを使用してしまう可能性がある。
それを避けるために名前空間を与えておき、指定するデータを確実に特定できるようにするのを通例とするのが[Django公式ドキュメントのチュートリアル](https://docs.djangoproject.com/ja/3.1/intro/tutorial03/#id4)。
![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/227222/d9757d3e-6028-1364-ec72-8b1b78453676.png)

仕様も言いたいこともアプリケーション毎にまとめたい気持ちも分かるけど、冗長すぎる。
名前空間つけるなら、元より一箇所にまとめればいい。
![Untitled (2).png](https://qiita-image-store.s3.amazonaws.com/0/227222/1ea8eda1-1d55-729c-0014-a0dff57f908c.png)

仕組みは静的ファイルも同じ。
デザイナーがいる場合は切り分けたstatic, templatesだけを渡せばいいから衛生的。

これを実現するための設定は以下のとおり。
ついでに、デプロイ時に静的ファイルを集めるディレクトリも作っておく。(staticsディレクトリ)

```python:settings.pyまたはsettings/base.py
TEMPLATES = [
    {
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
    },
]

STATICFILES_DIRS = [os.path.join(BASE_DIR, 'statics')]
# デプロイ用の設定
STATIC_ROOT = os.path.join(BASE_DIR, 'static') # STATICFILES_DIRSで指定されたディレクトリからSTATIC_ROOTにファイルを集めて
STATIC_URL = '/static/' # STATIC_URL上で配信する
```
```shell:プロジェクトディレクトリの構造
testDjango
├── config
├── .venv
├── manage.py
├── static
├── statics
└── manage.py
```

# git管理から除外するファイル
あとはアプリケーションを追加して開発を進めるだけ。
そこで、個人開発でもチーム開発でももはやgit管理は避けられない。
チームで共有すること、デプロイのためのファイル管理をすることを念頭に.gitignoreファイルを作成する。
といっても、除外するのは仮想python環境(.venv)と(もしあるなら)ローカル開発環境用settings系ファイルのみ。
理由は、それぞれ(人、マシンともに)が個別に準備すべきものだから。

```shell:.gitignore
.venv/
config/settings/local.py #あれば
```
~~あと、後からアプリケーションを追加するとできるmigrationsも追記すべきだと思う。
開発で試行錯誤したmigrationファイルがデプロイ環境に行って実行されてしまうのがよくないかもしれない。~~
migrationsを.gitignoreに入れてたら、`makemigrations`と`migration`でアプリケーションを認識しなくなった。
開発環境で試行錯誤したとしても、成功までの履歴をなぞることで最終的にはきれいになるのがmigration機能のいいところなので、
その恩恵を賜る。

```shell:プロジェクトディレクトリの構造
testDjango
├── .gitignore
├── config
├── .venv
├── manage.py
├── templates
├── static
├── statics
└── manage.py
```

# アプリケーション models, viewsをパッケージ化する
アプリケーションを作成する。

```shell:Djangoアプリケーションの作成
(project)$ python manage.py startapp testApp
```
```shell:プロジェクトディレクトリの構造
testDjango
├── .gitignore
├── config
├── .venv
├── templates
├── static
├── statics
├── manage.py
└── testApp
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    ├── models.py
    ├── tests.py
    └── views.py
```
ここで気に入らないのがmodels.pyとviews.py。
保守性を高めるためにも、modelごと、viewごとにファイルを分割したい。
そこで、それぞれをパッケージ化する。

```shell:models,viewsのパッケージ化
(project)$ mkdir testApp/models
(project)$ touch testApp/models/__init__.py
(project)$ rm testApp/models.py
(project)$ mkdir testApp/views
(project)$ touch testApp/views/__init__.py
(project)$ rm testApp/views.py
```
```shell:プロジェクトディレクトリの構造
testDjango
├── .gitignore
├── config
├── .venv
├── templates
├── static
├── statics
├── manage.py
└── testApp
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    ├── models
    │   └── __init__.py
    ├── tests.py
    └── views
        └── __init__.py
```

以降、modelはmodelsディレクトリ、viewはviewsディレクトリに追加すればいい。
追加時、\_\_init__.pyにimportすること。
(import必須？詳しい方教えてください。)

# デプロイ
いつもの構成は```nginx <-> gunicorn <-> Django```。
デプロイ環境の構築・設定は[こちら](https://qiita.com/xKxAxKx/items/da9dcc8caa36e1c303c7)、Djangoアプリケーション with gunicornのサービス化は[こちら](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)を参考にする。
やるべきことは以下4点。

* git clone(初回以降fetch, mergeでもいいと思う)
* pipenvでライブラリパッケージのインストール
* 静的ファイル配信の準備
* デプロイ環境用DBのmigration

定型なので、スクリプトにしちゃいましょう。

```shell:update.sh：デプロイ用スクリプト(デプロイ環境のプロジェクトディレクトリ直下にある想定)
#!/usr/bin/bash

git fetch
git merge origin/master
source .venv/bin/activate
pipenv install
python manage.py collectstatic --noinput
python manage.py makemigrations
python manage.py migrate
deactivate
systemctl restart testDjango
```
デプロイ環境を更新したい場合は、このスクリプトを実行するだけでOK。

# 参考
[現場で使える Django の教科書《基礎編》](https://www.amazon.co.jp/dp/B07GK7BWB7/ref=cm_sw_em_r_mt_dp_U_0FMjCb3V62AAY)
[現場で使える Django の教科書《実践編》](https://www.amazon.co.jp/dp/B07L3DRGBT/ref=cm_sw_em_r_mt_dp_U_2GMjCb9E2VTTH)
[EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイする](https://qiita.com/xKxAxKx/items/da9dcc8caa36e1c303c7)
[gunicorn + Flask + nginx + Systemdで動かしてみた](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)
[2018年のPythonプロジェクトのはじめかた](https://qiita.com/sl2/items/1e503952b9506a0539ea)