クラスベースビュー(ログイン)について（自分用メモ）

## Django自分用メモになります
今回はクラスベースビュー（ログイン）についてまとめます



## 開発環境
OS:mac
エディタ：vscode
python:3.10.9
django:4.1.0


LoginRequiredMixin
ログインしていないと動かしたくないviewに多重継承させる
またはログインが必要なViewにデコレータをつける
@method_decorator(login_required)

### 下準備（AbstractBaseUserなどの復習）

```python:models.py
from django.db import models
from django.contrib.auth.models import (
    BaseUserManager,AbstractBaseUser,PermissionsMixin
)
from django.urls import reverse_lazy


class UserManager(BaseUserManager):
    def create_user(self,username,email,password=None):
        if not email:
            raise ValueError('メールアドレスをいれてください')
        user =self.model(
            username = username,
            email = email
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
    
    def create_superuser(self,username,email,password=None):
        user = self.model(
            username=username,
            email = email,
        )
        user.set_password(password)
        user.is_staff=True
        user.is_active=True
        user.is_superuser=True
        user.save(using=self._db)
        return user


class Users(AbstractBaseUser,PermissionsMixin):
    username = models.CharField(max_length=150)
    email = models.EmailField(max_length=255,unique=True)
    is_active=models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS=['username']
    
    objects = UserManager()
    
    def __str__(self):
        return self.email+''+self.username
    
    def get_absolute_url(self):
        return reverse_lazy('accounts:home')
```
settings.pyに設定も忘れず
```python:settings.py
AUTH_USER_MODEL = 'accounts.Users'

# Application definition

INSTALLED_APPS = [
    #(略)
    'accounts',
]
```

### viewの作成　登録フォームの作成 urlの作成　遷移ができるhtmlの作成

```python:views.py
from django.shortcuts import render
from django.views.generic.base import TemplateView,View
from django.views.generic.edit import CreateView,FormView
from .forms import RegistForm
class HomeView(TemplateView):
    template_name = 'home.html'
    
    
class RegistUserView(CreateView):
    template_name = 'regist.html'
    form_class = RegistForm
    
class UserLoginView(FormView):
    pass #のちに記述

class UserLogoutView(View):
    pass #のちに記述
```
```python:forms.py
from django import forms
from django.contrib.auth import get_user_model
from .models import Users
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError

User=get_user_model

class RegistForm(forms.ModelForm):
    username=forms.CharField(label='名前')
    age = forms.IntegerField(label='年齢',min_value=0)
    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','age','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 ValidationError('パスワードが一致しません')
        
    def save(self,commit=False):
        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
```
```python:urls.py
from django.urls import path
from .views import(
    HomeView,RegistUserView,UserLoginView,UserLogoutView
)
app_name='accounts'

urlpatterns = [
    path('home/',HomeView.as_view(),name='home'),
    path('regist/',RegistUserView.as_view(),name='regist'),
    path('user_login/',UserLoginView.as_view(),name='user_login'),
    path('user_logout/',UserLogoutView.as_view(),name='user_logout'),
]

```
```python:base.html
<!DOCTYPE html>
<html lang='ja'>
    <head>
        <meta charset='utf-8'>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    </head>
    <body>
        <nav class ='navbar navbar-expand-lg navbar-light bg-light'>
            <a class ='navbar-brand' href="{%url 'accounts:home'%}">ホーム</a>
            {% if user.is_authenticated%} {#ログインしているかどうかで表示を切り替える#}
            <a class ='navbar-brand' href="{%url 'accounts:user_logout'%}">ログアウト</a>
            {% else %}
            <a class ='navbar-brand' href="{%url 'accounts:user_login'%}">ログイン</a>
            <a class ='navbar-brand' href="{%url 'accounts:regist'%}">ユーザ登録</a>
            {% endif %}

        </nav>
        {% block content%}
        {% endblock %}
    </body>
</html>
```

### ログインとログアウトの実装
:::note
views.pyで先ほど定義したUserLoginViewとUserLogoutViewの処理を書いていく。
ログインに関してはforms.pyでログインに必要な項目を記載したフォームをつくったら
FormViewを継承し、form_classで使うフォームに指定。
def postでPOSTが行われたそのemailとpasswordを取得しauthenticateで
存在するか確かめてTrueであればloginメゾットでログインし、redirectさせる
ログアウトもgetで受け取ってlogoutメゾットを使って、redirectさせるだけ


```python:forms.py
class UserLoginForm(forms.Form):
    email=forms.EmailField(label='メールアドレス')
    password =forms.CharField(label='パスワード',widget=forms.PasswordInput)
```
```python:views.py
class UserLoginView(FormView):
    template_name='user_login.html'
    form_class=UserLoginForm
    # success_url=reverse_lazy('accounts:home')　#redirectさせるのでいらない
    
    def post(self,request,*arg,**kwargs):
        #POSTが行われたらemailとpasswordを取得して
        email = request.POST['email']
        password = request.POST['password']
        #そのユーザが存在するかauthenticateで確かめる
        user = authenticate(email=email,password=password)
        #userが存在し、かつ、is_activeがTrueの場合にログインを行う
        if user is not None and user.is_active:
            login(request,user)
        return redirect('accounts:home')

class UserLogoutView(View):
    
    def get(self,request,*args,**kwargs):
        logout(request)
        return redirect('accounts:user_login')
```
:::

### ログインしていないと実行させたくないを設定する方法(LoginRequiredMixin,login_required)
やりかたは３つ
・dispatchメゾットにmethod_decorator(login_required)を定義する方法
・クラス全体にmethodo_decorator(login_required,name='dispatch')を定義する方法
・LoginRequiredMixinをクラスに多重継承させる

importするもの
```python
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
```

```python:views.py
# 2.@method_decorator(login_required,name='dispatch')
class UserView(LoginRequiredMixin,TemplateView): #3
    template_name = 'user.html'
    
    #1. @method_decorator(login_required) #ログインしていないと実行されない
    #dispatchをオーバーライド
    #dispatchとはgetリクエストならばgetをpostリクエストならpost処理を行うメゾット、だからリクエストの際必ず行われる
    def dispatch(self,*args,**kwargs):
        return super().dispatch(*args,**kwargs)
```
:::note
dispatchとはgetリクエストならばgetをpostリクエストならpost処理を行うメゾット、だからリクエストの際必ず行われる
:::
ここでLoginRequiredMixinやlogin_requiredで弾かれた場合内部的にどのurlにアクセスしていて
page not found がでているのかというとデフォルトでhttp://127.0.0.1:8000/accounts/login/
にアクセスしていて、urls.pyにそんなページは存在しないからpage not foundとなる。
もし弾かれた場合にログイン画面に移動させたい場合はsettings.pyでLOGIN_URLを設定してあげる
![スクリーンショット 2023-08-23 13.57.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627182/cb731080-3aeb-f25e-bdd2-cc2920bdb51f.png)

```python:settings.py
LOGIN_URL='/accounts/user_login' #デフォルトのaccounts/login/をにaccounts/user_loginにリダイレクト先を変更する
```

### ?next= に対応させる（よりスマートなページ移行）
login_requiredまたはLoginRequiredMixinによって弾かれてかつ
settings.pyでLOGIN_URLでログインページにリダイレクトした場合、
ログインページに普通にアクセスするときと違い、urlパラメータに?next=/accounts/user/と
表示されていることがわかる。
?next=とはこのページに行こうとして弾かれてしまったので、ログインができたらこのページに行こうとしていたよという情報を残してくれている。
もしnext_urlがあればログインした時にホーム画面などに飛ぶのではなく、その行こうとしたページに飛ぶといった処理を
1.ログインするviewに追加することと、
2.ログインのhtmlにinputからvalueで?next=のあとの情報をPOSTするように設定すると、そのように実行されるのでスマートになる

```python:views.py
class UserLoginView(FormView):
    template_name='user_login.html'
    form_class=UserLoginForm
    # success_url=reverse_lazy('accounts:home')
    
    def post(self,request,*arg,**kwargs):
        #POSTが行われたらemailとpasswordを取得して
        email = request.POST['email']
        password = request.POST['password']
        #そのユーザが存在するかauthenticateで確かめる
        user = authenticate(email=email,password=password)
        #userが存在し、かつ、is_activeがTrueの場合にログインを行う
        if user is not None and user.is_active:
            login(request,user)
        # ?nextがurlパラメータにあればそちらに移行する  #追加
        next_url =request.POST['next']
        if next_url:
            return redirect(next_url)
        return redirect('accounts:home')
```
```python:user_login.html
    <input type="hidden" name="next" value="{{request.GET.next}}">
    #request.GET.nextで?next=の後の情報をviewに投げることができる
```

### LoginView,LogoutView
```python:
from django.contrib.auth.views import LoginView #ログインするためのView

template_name= ''　#表示するテンプレートの指定
authentication_form #ログイン認証に用いられるFormを指定（デフォルトはAuthenticationForm）

from django.contrib.auth.forms import AuthenticationForm #ログイン認証に用いられるForm(forms.pyで使うフォームに継承させる)
from django.contrib.auth.views import LogoutView #ログアウトするためのView

#settings.pyに設定する内容
LOGIN_REDIRECT_URL #LoginViewで次に遷移する先が指定されていなかった場合の遷移先のURL
LOGIN_URL #ログインしていない状態でlogin_requiredが指定されているViewを表示しようとした場合にリダイレクトされるView
LOGOUT_REDIRECT_URL #LogoutViewで次に遷移する先が指定されていなかった場合の遷移先のURL

```
LoginViewを継承してUserLoginViewを作り直す。
```python:views.py
class UserLoginView(LoginView):
    template_name='user_login.html'
    authentication_form=UserLoginForm

class UserLogoutView(LogoutView):
    pass
```
:::note
ここでのusernameとはmodels.pyでのUSERNAME_FIELD = 'email'に設定したものを扱うのでEmailFieldをつかう

```python:forms.py
class UserLoginForm(AuthenticationForm):
    #ここでのusernameとはmodels.pyでのUSERNAME_FIELD = 'email'に設定したものを扱うのでEmailFieldをつかう
    username= forms.EmailField(label='メールアドレス')
    password= forms.CharField(label='パスワード',widget=forms.PasswordInput())
```
:::
ログインの遷移先を指定しないとpage not foundになる
```python:settings.py
LOGIN_REDIRECT_URL='/accounts/home'
LOGOUT_REDIRECT_URL='/accounts/user_login'
```

### 次回のログインを記憶させる（セッションの保存時間を変更する）
[公式](https://docs.djangoproject.com/en/3.1/topics/http/sessions/)

```python:settings.py
SESSION_COOKIE_AGE #セッションの保存時間(秒)　デフォルトは1209600(2週間)
```
[公式](https://docs.djangoproject.com/ja/3.1/ref/settings/#std:setting-SESSION_COOKIE_AGE)
さらにrequest.session.set_expiry(value):セッションの保存時間を引数の秒に変更する。
引数が０の場合、ブラウザを閉じるとセッションがなくなる。もしvalueがdatetimeまたはtimedeltaオブジェクトならば指定された日時に破棄される
[公式](https://docs.djangoproject.com/ja/3.1/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry)

```python:settings.py
SESSION_COOKIE_AGE=5 #5秒でセッションが解除される
```
ログイン状態を保持するボタンをforms.pyに追加
```python:forms.py
class UserLoginForm(AuthenticationForm):
    #ここでのusernameとはmodels.pyでのUSERNAME_FIELD = 'email'に設定したものを扱うのでEmailFieldをつかう
    username= forms.EmailField(label='メールアドレス')
    password= forms.CharField(label='パスワード',widget=forms.PasswordInput())
    remember= forms.BooleanField(label='ログイン状態を保持する',required=False)
```

```python:views.py
class UserLoginView(LoginView):
    template_name='user_login.html'
    authentication_form=UserLoginForm
    
    #ログイン状態を保持する(session時間を変更する)
    def form_valid(self,form):
        #rememberを取り出してTrueであれば秒数を更新
        remember=form.cleaned_data['remember']
        if remember:
            self.request.session.set_expiry(1200000)
        return super().form_valid(form)
```