素人基盤エンジニアがDockerでDjangoを触るシリーズ③：Django admin

# TL;DR
[素人基盤エンジニアがDockerでDjangoを触るシリーズ②：モデルの作成](https://qiita.com/LittleGreenMen/items/604f5689a506e67e6eb1)の続き。
１から読みたい場合は↓こちら。
[素人基盤エンジニアがDockerでDjangoを触るシリーズ①：DockerでDjangoを作る](https://qiita.com/LittleGreenMen/items/660c27669214da1c9613)
今回も[Django Girls](https://tutorial.djangogirls.org/ja/)のサンプルを作りながらdjangoと戯れる。
前回はモデルを作成したので今回はViewとTemplateと行きたいところだが、先にDjangoモデルを管理するための管理サイト(admin)を学ぶ。
docker-composeで管理サイトのユーザ作成までを実行するには、作成コマンドであるcreatesuperuserをワンライナーで実行できるように、拡張したコマンドを作る必要があるので、それについても記載している。

# Django admin
作成したモデルをGUIで管理するために、Djangoはデフォルトでadminというページを持っている。
今回はその有効化と使い方を見ていく。

### ①admin.pyの編集
#### admin.py
下記の通り編集する。

```python:blog/admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)
```
一行目は最初から入っているので、それ以外を追記する形。
#### よくわかる解説
```python
from django.contrib import admin
```
adminサイトのアプリケーションモジュール。前回は特に触れなかったが、`django.contrib.admin`は自分で作成したアプリケーションを追記した`settings.py`内の`INSTALLED_APPS`にも、デフォルトで記載されており、最初から利用できるようになっている。

```python
from .models import Post
```
同じディレクトリに格納されているmodels.pyから、Postモデルを呼び出している。

```python
admin.site.register(Post)
```
`admin.site.register(Model)`で、Modelをadminサイトの管理下に加えることができる。

ではいざ、起動しているコンテナでadminサイトをのぞいてみる。
(`docker-compose up`は前回から起動しっぱなしの人は不要。)

```shell
docker-compose up
```
下記は別ターミナルから実行。前回は、`python manage.py migrate blog`を実行してblogディレクトリ下のmigrationsのみマイグレーションしたが、今回は引数を与えずに実行することでadminのモデルもマイグレーションする。

```shell
docker-compose exec web bash
python manage.py migrate
exit
```

`http://IPADDRESS:8000/admin`にアクセス
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/db2504a4-8c73-1fa9-9dda-821c995e6169.png)
ログイン画面が開いた。ここに入るためのログインユーザは、DBのマイグレーションと同じように`migrate.py`でコマンドを実行して作る必要がある。

### ②adminサイトのスーパーユーザの作成

```shell
docker-compose exec web bash
python manage.py createsuperuser
exit
```
`createsuperuser`を実行すると、プロンプトが返ってくる。ユーザ名とメールアドレス、パスワードを入力すると、`Superuser created successfully.`という出力があるので、ここで入力した情報を先ほどのログイン画面で入力する。

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/fb79ae65-dc13-a1d7-15e7-53c99104fc20.png)
ログインできた。

### ③レコードの操作
Postsというところをクリックしてみる。
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/51adf7b6-890c-1574-cb7a-dac3675ebee7.png)
POSTモデルは前回定義してテーブルを作成しただけなので、当然 0 postsと表示されている。ADD POSTというボタンでデータの挿入ができそうなので、押してみる。
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/faad2b24-8da9-0372-e667-120d31366033.png)
前回models.pyで定義した通りに、入力用のテキストボックスやプルダウンリストが並んでいる。ためしに一つ投稿してみる。
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/046d263e-81a7-50ac-fb20-6bb416330acf.png)
無事データを追加できた。dbのコンテナに入って実際にレコードが入っているか見てみる。

```shell
docker-compose exec db bash
psql -U postgres
SELECT * FROM blog_post;
```
```:SELECT実行結果
 id | title |        text        |      created_date      |     published_date     | author_id
----+-------+--------------------+------------------------+------------------------+-----------
  1 | TEST  | This is test post. | 2020-05-02 07:47:14+00 | 2020-05-02 07:47:26+00 |         2
(1 row)
```
ちゃんとレコードが格納されていることを確認できた。レコードの作成、更新、削除も同じ画面で可能。

# DB migrateやcreatesuperuser処理を含んだdocker-composeを作成する
これまで、あくまで検証のためということで`docker-compose exec`を使ってコンテナにログインして処理を行ってきたが、できればここまでをdocker-compose upで一気に構築できるほうがスマートだろう。

### ①DB migrate
migrateに関してはコマンドを打つのみなので、下記のようにdocker-compose.yamlの`command:`を書き換えることで処理が可能である。(makemigrationはmigrationファイルを作成するだけなので、すでに作成済みのファイルがあれば必要ない。)

```yaml:docker-compose.yaml
command: sh -c "sleep 5; python manage.py migrate; python manage.py runserver 0.0.0.0:8000"
```
migrateを実行する前に5秒sleepしているが、たまにPostgresの起動タイミングの問題でエラーが出るため追記している。

### ②createsuperuser
createsuperuserは実行時に対話的に処理が走るため、これをワンライナーで実行できるようにしないとdocker-composeでは扱えない。
ググると、manage.pyは`management/commands`というフォルダを作成してpythonファイルを追加してやれば、コマンドが拡張できるようだ。
https://stackoverflow.com/questions/6244382/how-to-automate-createsuperuser-on-django
https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/
これを使ってユーザ名やパスワードをオプションで受け取ってワンライナーで実行可能なカスタムコマンドを作成する。

```shell
cd blog
mkdir -p management/commands
touch management/__init__.py
touch management/commands/__init__.py
```

#### createcustomsuperuser.py
```python:management/commands/createcustomsuperuser.py
from django.contrib.auth.management.commands import createsuperuser
from django.core.management import CommandError


class Command(createsuperuser.Command):
    help = 'Crate a superuser, and allow password to be provided'

    def add_arguments(self, parser):
        super().add_arguments(parser)
        parser.add_argument(
            '--password', dest='password', default=None,
            help='Specifies the password for the superuser.',
        )

    def handle(self, *args, **options):
        password = options.get('password')
        username = options.get('username')
        database = options.get('database')

        if password and not username:
            raise CommandError("--username is required if specifying --password")

        super().handle(*args, **options)

        if password:
            user = self.UserModel._default_manager.db_manager(database).get(username=username)
            user.set_password(password)
            user.save()
```

#### よくわかる解説
```python
from django.contrib.auth.management.commands import createsuperuser
```
今回は、createsuperuserのコマンドを拡張するため、importする。

```python
from django.core.management import CommandError
```
djangoが持っているコマンドエラー。今回は引数が足りないときにエラーを返すために利用。

```python
class Command(createsuperuser.Command):
    help = 'Crate a superuser, and allow password to be provided'
```
`--help`もしくは`-h`でヘルプを呼び出したときに表示される文言を追加。

```python
    def add_arguments(self, parser):
        super().add_arguments(parser)
        parser.add_argument(
            '--password', dest='password', default=None,
            help='Specifies the password for the superuser.',
        )
```
コマンドの引数を追加する関数。`super().add_arguments(parser)`は、元の`createsuperuser`の引数も追加するために実行している。元のcreatesuperuserでは受け付けなかった、passwordという引数を追加。

```python
    def handle(self, *args, **options):
        password = options.get('password')
        username = options.get('username')
        database = options.get('database')

        if password and not username:
            raise CommandError("--username is required if specifying --password")

        super().handle(*args, **options)

        if password:
            user = self.UserModel._default_manager.db_manager(database).get(username=username)
            user.set_password(password)
            user.save()
```
実際の処理部分。従来のcreatesuperuserの処理により作成されたuserをdbから探してきて、passwordをセットして保存している。

このファイルを作成した上で、docker-compose.yamlを更に下記のように書き換える。

```yaml:docker-compose.yaml
    command: sh -c "sleep 5; python manage.py migrate; python manage.py createcustomsuperuser --username root --password 123456 --noinput --email 'blank@email.com'; python manage.py runserver 0.0.0.0:8000"
```
(パスワードは例）
このように、docker-compose.yamlに書けるということは、Kubernetesに移行するときもマニフェスト内でSecretで指定すればよさそうだ。

ここまでの処理が、`docker-compose up`一行で完了することをぜひ各自確かめてほしい。

```shell
docker-compose up
```

次回は、urls.pyの書き方を学ぶ。（いつになったらViewを書くのか。。）

[つづく](https://qiita.com/LittleGreenMen/items/5f3325b94ceef48b7270)