Djangoカスタムユーザーの作成・登録・ログイン・ログアウト　関数ベース

# 目次
[Usersモデル作成](#Usersモデルの作成)
[ユーザー登録](#ユーザー登録)
[ログイン](#ログイン)
[ログアウト](#ログアウト)

# Usersモデルの作成
「accounts」アプリを作成し、「models.py」にUsersモデルを定義します。
`from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin`
`from django.contrib.auth.models import UserManager`の二つをインポートします。
Usersモデルには`AbstractBaseUser`と`PermissionsMixin`を継承させます。
<dl>
  <dt>AbstractBaseUser</dt>
  <dd>ユーザーの認証機能のみが定義されたクラス。</dd>
  <dt>PermissionsMixin</dt>
  <dd>スーパーユーザーの権限などが定義されたクラス</dd>
</dl>
これらを継承することで認証やスーパーユーザーの管理のみを継承し、ユーザークラスに持たせる名前やメールアドレス、ユーザーアイコンなどを作成するアプリに合わせてフィールドを独自に設定することができます。

~~~python:models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.models import UserManager


class Users(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=255)
    email = models.EmailField(max_length=255, unique=True)
    icon = models.FileField(null=True, upload_to='icon/')
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email' #ユーザーを一意に識別する
    REQUIRED_FIELDS = ['username'] # スーパーユーザーに名前を持たせる

    class Meta:
        db_table = 'users' #テーブル名を指定
~~~
`USERNAME_FIELD = 'email'`とする事で認証にemailを使う事ができる。

デフォルトではDjangoのユーザーモデルが利用されてしまうので、カスタムユーザーを利用するには「settings.py」に`AUTH_USER_MODEL`を定義し、作成したUsersモデルを指定する必要があります。
~~~python:settings.py
AUTH_USER_MODEL = 'accounts.Users'
~~~
`AUTH_USER_MODEL`の設定はマイグレーションを行う前にするようにしましょう。
設定できたらマイグレーションします。
~~~python:ターミナル
python manage.py makemigrations accounts

python manage.py migrate
~~~

# ユーザー登録
### ユーザー登録用のフォームを作成します。
accountsフォルダーに新しく「forms.py」を作成します。
パスワードのバリデーションを行うための`validate_password`と「models.py」で作成した`Users`をインポートします。
~~~python:forms.py
from django import forms
from django.contrib.auth.password_validation import validate_password
from .models import Users


class SignInForm(forms.ModelForm):
    username = forms.CharField(label="名前")
    email = forms.EmailField(label='メールアドレス')
    password = forms.CharField(label='パスワード', widget=forms.PasswordInput)
    confirm_password = forms.CharField(label='パスワード再入力', widget=forms.PasswordInput)

    class Meta:
        model = Users
        fields = ('username', 'email', 'password')
    
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        if password != confirm_password:
            raise forms.ValidationError('パスワードが一致しません。')
    
    def save(self):
        user = super().save(commit=False)
        validate_password(self.cleaned_data.get('password'), user)
        user.set_password(self.cleaned_data.get('password'))
        user.save()
        return user
~~~
`forms.ModelForm`を継承する事でフォームの内容を`save()`メソッドでデータベースに登録する事ができる。
`password`と`confirm_password`に`widget=forms.PasswordInput`を指定する事でパスワード入力用のフォームにする事ができる。また、`comfirm_password`によりパスワードをもう一度入力してもらう事で、パスワードの入力ミスを予防するようにしています。

#### class Meta
`class Meta`の`model=Users`でフォーム内容を登録するモデルを指定し、`fields`で登録するフィールドを指定する。(confirm_passwordはデータベースには登録しないため省く)

#### cleanメソッド
`clean()`メソッドは`password`と`confirm_password`が一致するか判定し、一致しない場合はバリデーションエラーを返す。
`cleaned_data = super().clean()`でフォームに入力された値を取り出し`cleaned_data`に格納。(super()は親クラスを指し、この場合はSignInFormを指している)
~~~python:ターミナル
# super().clean()により取得した値
{'username': 'test', 'email': 'test@example.com', 'password': 'noteapppass', 'confirm_password': 'noteapppass'}
~~~
passwordとconfirm_passwordを`cleaned_data.get('フィールド名')`としてそれぞれ取り出す。

#### saveメソッド
`save()`メソッドはデフォルトのsaveメソッドでは上記のようにパスワードが平文のままになってしまうので、saveメソッドをオーバーライドしてパスワードを暗号化して保存するようにします。
`user = super().save(commit=False)`とすることでフォームの値を取得する。(commit=Falseでデータベースに保存せず値だけ取り出す事が可能)
`validate_password`によりパスワードのバリデーションを行う。
`user.set_password`メソッドでパスワードの暗号化を行い、`user().save()`でデータベースに保存する。

### ユーザー登録のビューを作成
「accounts」フォルダの「views.py」にユーザー登録の処理を書きます。
`from django.shortcuts import render, redirect` ⬅︎ redirectを追加
`from django.core.exceptions import ValidationError`
`from . import forms`
renderの後に`redirect`を追加と例外処理用の`ValidationError`最後に`form`をインポートします。
~~~python:views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.core.exceptions import ValidationError
from . import forms


def signin(request):
    signin_form = forms.SignInForm(request.POST or None)
    if signin_form.is_valid():
        try:
            signin_form.save()
            return redirect('accounts:home')
        except ValidationError as e:
            signin_form.add_error('password', e)
    return render(request,'accounts/signin.html', context={
        'signin_form': signin_form,
    })
~~~
`forms.SignInForm(request.POST or None)`でPOSTメソッドで送られてきたデータを`request.POST`で受け取り、GETメソッドの場合はNoneを返します。
`signin_form.is_valid()`でバリデーションのチェックとformで定義したcleanメソッドも実行されます。

`try`により例外処理を行い、`signin_form.save()`でformで定義したsaveメソッドを実行します。この時、`SignInForm`で定義した`save`メソッドの`validate_password`でバリデーションに引っかかった場合は、`except`が呼ばれ`signin_form.add_error('password', e)`でフォームのpasswordフィールドにエラーが渡されます。

例外なく実装された場合は`redirect`で遷移させます。

# ログイン
### ログイン用のフォームを作成
「accounts」の「forms.py」にログイン用のフォームを追加します。
~~~python:forms.py
class LoginForm(forms.Form):
    email = forms.EmailField(label="メールアドレス")
    password = forms.CharField(label="パスワード", widget=forms.PasswordInput)
~~~
ログインフォームではデータベースに保存することはないので`forms.Form`を継承します。
今回はログインにメールアドレスを使うので、メールアドレスとパスワードのフィールドを定義します。

### ログイン用のビューを作成
`from django.contrib.auth import authenticate, login`
`from django.contrib import messages`をインポートします。
`authenticate`はユーザーの認証を行い、`login`でログインの処理を行います。
`messages`はログイン後やログインに失敗した場合にメッセージを表示するために使います。
~~~python:views.py
from django.contrib.auth import authenticate, login


def user_login(request):
    login_form = forms.LoginForm(request.POST or None)
    if login_form.is_valid():
        email = login_form.cleaned_data.get('email')
        password = login_form.cleaned_data.get('password')
        user = authenticate(email=email, password=password)
        if user:
            login(request, user)
            messages.success(request, 'ログインしました。')
            return redirect('accounts:home')
        else:
            message.warning(request, 'メールアドレスかパスワードが間違っています。')
        return render(request, 'accounts/login.html', context={
            'login_form': login_form,
        })
~~~
`user = authenticate(email=email, password=password)`でメールアドレスとパスワードが一致したユーザーを取得する。
`user`を取得できた場合は`login(request, user)`でユーザーのログイン処理を行う。
`messages.success(request, 'ログインしました。')`によりmessagesのsuccessタグでメッセージを渡す。タグをつけることでテンプレート側での表示に使う事ができる。
例：
~~~html:index.html
<div class="message">
    {% if messages %}
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">
                {{ message.message }}
            </div>
        {% endfor %}
    {% endif %}
</div>
~~~~
テンプレート側でmessagesをfor文で取得し`{{ message.message }}`でメッセージの内容を表示する。
Bootstrapを使用する場合は`class="alert alert-{{ message.tags }}"`でタグを取得し(`messages.success`の場合successを取得できる)アラートの色をタグの種類により変える事ができる。
![スクリーンショット 2023-02-13 13.22.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2492098/b84bf08b-03a1-9f90-ff7a-56cecf647cc9.png)
![スクリーンショット 2023-02-13 13.28.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2492098/64458e3c-383b-049e-b9bf-f05873ab1666.png)


# ログアウト
### ログアウト用のビューを作成
`from django.contrib.auth import authenticate, login, logout` ⬅︎ logoutを追加
`from django.contrib.auth.decorators import login_required`
`login_required`はユーザーがログインしているかを判定してくれます。ログアウト関数のデコレータとしてログインしていない場合は関数を実行されないようにします。
~~~python:views.py
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required


@login_required
def user_logout(request):
    logout(request)
    messages.success(request, 'ログアウトしました。')
    return redirect('accounts:home')
~~~
`logout(request)`によりログアウトの処理を行ます。