Django Sprint #3 ユーザーモデルのカスタマイズ (Docker Toolbox向け)

この記事はDocker Toolbox (≒ Windows 10 Home) ユーザー向けです。Docker (Mac OS, Windows 10 Professional / Enterprise) ユーザーの方は[Django Sprint #3 ユーザーモデルのカスタマイズ](https://qiita.com/tmgauss/private/b8edaf0da8cb8c22b1b5)へ。

## 目次

1. [Gitの運用](#gitの運用)
2. [各種設定](#各種設定)
3. [モデル概論](#モデル概論)
4. [ユーザーモデルと認証、管理者](#ユーザーモデルと認証管理者)

## Gitの運用
*Gitは初めて使う人にとっては少し難しく感じるかもしれません。その際は、言葉を追うより、「習うより慣れろ」的に実際に使ってみる方が分かりやすいでしょう。もし難しいようならGitを使わなくても開発はできます。*

本チュートリアルは GitHub Flow を少し変更して行います。もしチームの別の運用ルールがあるのなら、そちらを優先させてください。

- [GitHub Flow](https://gist.github.com/Gab-km/3705015)
- [『GitHubの機能を活用したGitHub Flowによる開発の進め方』](https://www.slideshare.net/takemikami/githubgithub-flow)
- [『A successful Git branching model （日本語訳）』](http://keijinsonyaban.blogspot.com/2010/10/a-successful-git-branching-model.html)

また、GitをGUI上で（コマンドラインからではなく、視覚的に）操作するには

- [GitHub Desktop](https://desktop.github.com/)
- [『GitHub Desktopの使い方 – ブランチの作成・削除と切り替える方法』](https://howpon.com/7268)

をおすすめします。

試しに、READMEファイル（このプロジェクトの説明などを記入するファイル）を追加してみましょう。

#### branchとは
ブランチとは作業履歴を枝分かれさせて記録していくためのものです。試しに、以下のコマンドを叩いてみましょう。

```terminal
> git branch
* master
```
現在はmasterブランチしかないはずです。

#### 2つのブランチ： master と develop
先ほど見たmasterブランチはプロジェクトのメインのブランチです。これを「常にデプロイ（公開）可能なもの」としましょう。つまり、テストも終わった「完成品」のみが許される神聖な場所です。
一方、普段の開発用のブランチをdevelopとします。これは開発用のメインのブランチで、公開はされません。

#### developブランチを切る
このdevelopブランチを作成しましょう。ブランチを切るのは次のコマンドです。

```terminal
> git checkout -b develop
Switched to a new branch 'develop'
```
一応ブランチの一覧を見ると、やはり

```terminal
> git branch
* develop
  master
```
このようになっています。*は今いるブランチを指します。
これをリモートに反映させましょう。

```terminal
> git push -u origin develop
```
オプション```-u```により、ローカルリポジトリの現在のブランチの上流をorigin developに規定します。これを一度設定すると以後は

```terminal
> git push
```
のみでリモートのdevelopブランチにプッシュされることになります。
一度GitHubのページでdevelopブランチが正しく生成されているかどうか確認してみましょう。

#### 実際の開発フロー
実際の開発はほとんど以下の繰り返しです。

1. ```git pull```でリモートの変更をローカルに反映
1. ```git checkout -b```でローカルに機能ブランチを作成
1. ```git add``` と ```git commit``` で変更をローカルの機能ブランチに反映
1. ```git push``` でローカル機能ブランチをリモートにアップ
1. pull request を作成し、機能ブランチをdevelopブランチにmergeして良いかレビュー&修正
1. GitHub上でmerge

参照 : [『プルリクエストを使った開発プロセス』](https://backlog.com/ja/git-tutorial/pull-request/03/)

#### 機能ブランチの作成
チームメンバーでまだGitに関する何のセットアップもしていない方は

- [『2.1 Git の基本 - Git リポジトリの取得』](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E5%9F%BA%E6%9C%AC-Git-%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E5%8F%96%E5%BE%97)

を参照してください。

まず、リモートの変更をローカルに反映させます。

```terminal
> git checkout develop
> git pull origin develop
```
次に、機能ブランチを切ります（作成します）。今回はブランチ名を「add-readme」とします。できるだけ、説明的なブランチ名にすると良いでしょう。

```terminal
> git checkout -b add-readme
```

#### add & commit
まず、機能を追加・変更・削除します。ここでは、codeディレクトリ直下に、README.mdというファイルを作ります。READMEとはプロジェクトの説明などを書く場所で、主に外部の人がこのプロジェクトがどのようなものなのか理解するために使うものです。ちなみに```.md```はマークダウン記法を表す拡張子です。ファイルを作成し、ここでは内容を以下のように書き換えます。

```.md:/README.md
# Django Sprint
This is a tutorial for UTokyo Project Sprint.
```
各自好きなように書いてください。

次に、変更を保存、反映させます。

```terminal
> git add -A
> git commit -m "Add README"
```

#### pull request
作業を完了したら、まずpushします。

```terminal
> git push origin add-readme
```
次に、pull request（プルリク）を送り、チームメンバーの確認段階に入ります。詳しい内容や手順は、

- [『プルリクエストとは？』](https://backlog.com/ja/git-tutorial/pull-request/01/)
- [『開発ブランチで修正』](https://backlog.com/ja/git-tutorial/pull-request/06/)
- [『レビューとマージ』](https://backlog.com/ja/git-tutorial/pull-request/07/)
- [『画面上からマージ』](https://backlog.com/ja/git-tutorial/pull-request/08/)

を参照してください。

#### これから
今後、細かいGit関連のコマンドは省略したいと思います。Git関連は慣れが重要なので、README.mdや次の設定変更などで一度使ってみることをおすすめします。

厄介なのは、コンフリクトを起こしたときです。まずGoogleで調べてみて、類似の事例がない場合は遠慮なくご相談ください。


## 各種設定

#### settings.py
一度settings.pyを開いてみてみましょう！
settings.pyはこのプロジェクトの「設定」を記述したものです。ここで各種設定を変更することができます。よく使うので、ざっと見ておくと良いでしょう。

#### タイムゾーンの変更
試しにタイムゾーン（時刻帯）を変更してみましょう。まず、settings.pyから以下の記述を探し出してください！

```python:/config/settings.py
...
TIME_ZONE = 'UTC'
...
```
デフォルトではタイムゾーンは「UTC」、つまりロンドンのそれに設定されています。これを東京のものに変更するには以下のように書き換えてみれば良いです。

```python:/config/settings.py
...
TIME_ZONE = 'Asia/Tokyo'
...
```

#### 言語の変更
続いて言語を変更してみましょう。デフォルトでは「en-us」、つまり、英語（アメリカ）に設定されています。Djangoはたくさんの言語をサポートしており、幸いにも日本語にも対応しています。日本語に変更するには以下のように書き換えれば良いです。

```python:/config/settings.py
...
LANGUAGE_CODE = 'ja'
...
```


## モデル概論

#### MVCモデルとは
MVCモデルとは

- モデル（Model）: データベースとコントローラーの間で、データ処理を行うファイル群
- ビュー（View）: ブラウザとコントローラーの間で、リクエストの取得やHTMLなどのファイルの出力を担うファイル群
- コントローラー（Controller）: 中枢でモデルとビューからデータを受け取り、処理して返すファイル群

詳しくは

- [『MVCモデルについてわかりやすく解説します！【初心者向け】』](https://umaroidblog.com/what_mvcmodel#Model)

を参照してください。

#### DjangoにおけるMVCモデル
かなりややこしい表記の違いがあります。

- モデル（Model）: Djangoではモデル（Model）と呼ばれる
- ビュー（View）: Djangoでは**テンプレート（Template）**と呼ばれる
- コントローラー（Controller）: Djangoでは**ビュー（View）**と呼ばれる

以後、ビューと書けば「コントローラー」を意味していると考えてください。

#### 具体的なモデルファイル
具体的なモデルのファイルを見てみましょう。ここでは、サンプルとして他のプロジェクトから持ってきたものを見てみましょう。

```python:
...
class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)
```
ブログアプリの記事のモデルをイメージしてください。この```class Post```はExcelのシートのようなものです。一方、```author```や```title```などはExcelのシートの各カラム（列）に対応します。```CharField```などはそのデータの型を表します。もしもモデルと別のモデルを組み合わせたいときは```ForeignKey```などを使います。上の例では```author```はPostモデルとユーザーモデルを繋ぐものです。このようなフィールドの種類とオプションは以下のドキュメントを参考にしてください。

- [Django docs | モデルフィールドリファレンス](https://docs.djangoproject.com/ja/3.0/ref/models/fields/)

## ユーザーモデルと認証、管理者
Djangoにはユーザーモデルとその認証機能（ユーザー名、メールアドレス、パスワード）がデフォルトで付属しています。（パスワードはハッシュ化されて保存されます。）その点他のフレームワークよりも楽で、堅牢です。

ただし、注意しなければならないのは**最初にユーザーモデルのカスタマイズを行わないと後から変更するのが難しくなる点**です。不可能ではないのですが、面倒です。そこで、本チュートリアルでは最初に少しだけカスタマイズしておき、その後のカスタマイズを容易にしておきます。（実際に公式ドキュメントでも強く推奨されています。）

#### 新しいアプリケーションの作成
まず、アプリケーションを作成します。Djangoにおけるアプリケーションとは、プロジェクトを構成する機能的な単位の一つです。再利用を簡単にするために機能ごとに分けることができます。しかし、本チュートリアルでは「cms」（Contents Management System）というアプリケーションにひとまず全てまとめようと思います。

```terminal
> docker-compose run --rm web django-admin startapp cms
```
これで、次のようなディレクトリ構造になったか確認してください。

```
code
├─ requirements.txt
├─ README.md
├─ manage.py
├─ config
│  ├─ __init__.py
│  ├─ asgi.py
│  ├─ settings.py
│  ├─ urls.py
│  └─ wsgi.py
├─ cms
│  ├─ migrations
│  ├─ __init__.py
│  ├─ admin.py
│  ├─ apps.py
│  ├─ models.py
│  ├─ tests.py
│  └─ views.py
├─ docker-compose.yml
└─ Dockerfile
```
先ほどみたような ```models.py``` や ```views.py``` が見えていますね！実際にはこれらのファイルを書き換えることでWebを構築します。（ファイルを追加する場合もあります。）

ここで忘れずにアプリケーションを追加したという事実をDjangoに伝えましょう。

```/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'cms.apps.CmsConfig', #Added
]
...
```
**tips** : プロジェクト名を「config」にしたのは、このプロジェクトファイル全体を一つのアプリケーション（Djangoのアプリケーションとは別）と見たときに、「config」ディレクトリに設定に関する情報が含まれているからです。


#### models.pyの変更
モデルのファイルを書き換えますが、現時点ではコピペで構わないと思います。（もう少し簡潔な方法があるかもしれませんが、動作が確認されているのでひとまずこれで...）

```/cms/models.py
from django.contrib.auth.base_user import (
    AbstractBaseUser, BaseUserManager,
)
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _


# User-related
class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Create and save a user with the given email and password.
        """
        if not email:
            raise ValueError('Users must have an email address')
        user = self.model(
            email=self.normalize_email(email),
            **extra_fields,
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)


class AbstractUser(AbstractBaseUser, PermissionsMixin):

    username_validator = UnicodeUsernameValidator()
    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )

    first_name = models.CharField(_('first name'), max_length=150, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)

    email = models.EmailField(_('email address'), unique=True)

    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        abstract = True

    def __str__(self):
        return self.email

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)


class User(AbstractUser):
    class Meta(AbstractUser.Meta):
        swappable = "AUTH_USER_MODEL"
```

実際にはこれが裏で記述されています。それを上書きしたような形です。具体的には、

- ```username``` : ユーザー名
- ```first_name``` : 名
- ```last_name``` : 姓
- ```email``` : メールアドレス
- ```date_joined``` : 参加した日時
- ```is_staff``` : 管理者権限を持つか？
- ```is_active``` : 有効なアカウントか？

のカラムが設定されました。

最後に、「これをユーザーモデルとする」ことをDjangoに伝えます。settings.pyの最後の部分に

```/settings.py
...
# Custom
AUTH_USER_MODEL = 'cms.User'
```
を付け足せば完了です。

#### admin.py
Djangoには管理者用ページが予め用意されています。これのおかげで、実際にコンソールをいじったり、SQL文を書くことなく、データを追加したり、更新したり、削除したりできます。

とりあえず、admin.pyを以下のように書き換えて、ユーザーモデルの変更を反映させましょう。

```/cms/admin.py
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField

from .models import (
    User,
)


class UserCreationForm(forms.ModelForm):
    """A form for creating new users. Includes all the required
    fields, plus a repeated password."""
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)

    class Meta:
        model = User
        fields = ('email',)

    def clean_password2(self):
        # Check that the two password entries match
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError("Passwords don't match")
        return password2

    def save(self, commit=True):
        # Save the provided password in hashed format
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


class UserChangeForm(forms.ModelForm):
    """A form for updating users. Includes all the fields on
    the user, but replaces the password field with admin's
    password hash display field.
    """
    password = ReadOnlyPasswordHashField()

    class Meta:
        model = User
        fields = ('email', 'password', 'is_active', 'is_staff',)

    def clean_password(self):
        # Regardless of what the user provides, return the initial value.
        # This is done here, rather than on the field, because the
        # field does not have access to the initial value
        return self.initial["password"]


class UserAdmin(BaseUserAdmin):
    # The forms to add and change user instances
    form = UserChangeForm
    add_form = UserCreationForm

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference specific fields on auth.User.
    list_display = ('email', 'is_staff')
    list_filter = ('is_staff',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('username', 'last_name', 'first_name')}),
        ('Permissions', {'fields': ('is_staff',)}),
    )
    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
    # overrides get_fieldsets to use this attribute when creating a user.
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'username', 'password1', 'password2'),
        }),
    )
    search_fields = ('email',)
    ordering = ('email',)
    filter_horizontal = ()


# Now register the new UserAdmin...
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)
```

#### マイグレーション
次のステップはモデルが書き換わったことをデータベースに伝えることです。これをマイグレーションと良い、Djangoでは通常

1. マイグレーションファイルの作成
2. マイグレーションファイルの反映

の2ステップからなります。

```terminal
> docker-compose run --rm web python manage.py makemigrations 
> docker-compose run --rm web python manage.py migrate
```
#### 管理者を作成する
最後に管理者を作成します。管理者は次のコマンドで生成できます。

```terminal
$ docker-compose run --rm web python manage.py createsuperuser
```
その後にユーザー名、メールアドレス（架空で良い）、パスワードを要求されるので、指示通りに設定します。

**ただし、ここで設定したユーザー名とパスワードは覚えておいてください。**

以上で、初期設定は完了です！

#### 管理者画面
http://192.168.99.100:8000/admin/ にアクセスすると、管理者のログイン画面になると思います。そこで、先ほど設定した「ユーザー名」と「パスワード」を入力すると、このような画面になるはずです。

<img width="1280" alt="スクリーンショット 2020-05-02 10.17.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489338/17b927f9-67dc-6656-0816-1cb446ca5c98.png">

さて、このユーザー「追加」をクリックすると、簡単にユーザーが追加できます。一度、サンプルユーザーを追加してみましょう。

## 参照
#### 次の記事
[Django Sprint #4 トップページの作成 (Docker Toolbox向け)](https://qiita.com/Chrg/private/4ef90a9fb3f17b209635)
#### 前の記事
[Django Sprint #2 新規プロジェクトのスタート (Docker Toolbox向け)](https://qiita.com/Chrg/private/4c008ca01bc5341408d7)