【Python】Djangoでユーザー認証機能を実装する【ユーザー登録、ログイン、ログアウト、情報更新】

Djangoでユーザー認証機能を実装します。具体的な項目は以下の通りです。

- 管理サイトの有効化
- ログイン
- ログアウト
- マイページ（ユーザー登録情報の閲覧）
- サインアップ（ユーザー作成）
- ユーザー登録情報の更新
- パスワードの更新

# 成果物

最終的に以下の機能が実現できました。

### Webアプリ上でのサインアップ（ユーザー登録）
![サインアップページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/53eb7ef4-9b92-d5dd-26d6-cae1f25e2c00.png)



### ログイン、ログアウト
![ログイン画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/22d54394-c8dd-b536-25e8-0f7f14260782.png)
![ログアウト完了ページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/b85e0a54-2401-e8ba-5994-6aa54402bf38.png)

### 登録情報の閲覧（ログイン必要）
![マイページに登録情報更新リンク追加.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/6c60b824-70b7-794e-b6fa-c15849ab2bae.png)

###　登録情報の更新
![登録情報更新画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/a204cff2-f28f-126f-20e6-e048569f396c.png)

### パスワードの変更
![パスワード更新画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/2b90a32f-744b-6ec9-81ff-c5c9ec7cc1c7.png)

### Djangoの管理サイト
もちろん管理サイト上でユーザーを追加、更新することも可能です。
![管理画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/9e88cd39-15fe-389e-6f7c-cbdd5d5dc6e8.png)

以上です。1つずつ順を追って実装していきたいと思います。

# 準備
djangoプロジェクトを作成します。

```
$ django-admin startproject conf .
```

ユーザー認証のためaccountというアプリケーションを作成します。

```
$ python manage.py startapp account
```

アプリケーションを登録します。あと、テンプレートファイルの場所も設定します。

```python:conf/settings.py
INSTALLED_APPS = [
    'accout.apps.AccountConfig', # ユーザー認証
    # 省略
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # 直下のtemplatesを指定
        # 省略
    },
]
```

confフォルダのurls.pyを修正します。

```python:conf/urls.py
from django.urls import path, include # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('home.urls')),  # 追加
]
```

accountフォルダにurls.pyを新規作成します。とりあえずここではトップページのみを設定します。

```python:account/urls.py
from django.urls import path, include
from . import views


app_name ='account'

urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
]
```

views.pyを修正します。

```python:accout/views.py
from django.views import generic


'''トップページ'''
class TempView(generic.TemplateView):
    template_name = 'account/top.html'
```

テンプレートファイルを作成します。まずはbase.htmlから。

```html:templates/base.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- 互換表示の解除 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>ユーザー登録・更新</title>
  </head>
  <body>
    <!-- ナビバー -->
    <nav class="navbar navbar-expand-md navbar-dark bg-primary">
      <a class="navbar-brand" href="{% url 'home:top' %}">テストサイト</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>

      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item">
            <a class="nav-link" href="{% url 'account:top' %}">Top</a>
          </li>
        </ul>
      </div>
    </nav>
    <!-- コンテンツ -->
    {% block content %}{% endblock %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>
```

次はトップページです。

```html:templates/account/top.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <h1>Top Page!!</h1>
</div>
{% endblock %}
```

サーバーを起動し、ブラウザでアクセスすれば以下のようなページが表示されます。

![topページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/5d7f3730-6a79-900d-94fc-403ebaa131ea.png)

# 管理サイトを使う

ユーザー認証機能を実装する前に管理サイトを使えるようにしておきたいと思います。具体的には、マイグレーションとスーパーユーザーの作成です。スーパーユーザーを作成する際に、ユーザー名とメールアドレスとパスワードの入力が求められるので、適宜入力してください。

```
$ python manage.py makemigrations account
```

```
$ python manage.py migrate
```

```
$ python manage.py createsuperuser
```

管理サイトアクセスして、ログインすると以下のような画面が表示されます。ユーザーを追加したい場合は、画面の「追加」ボタンをクリックします。

![管理画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/4d945568-8b92-c506-25ed-90c9dba93159.png)

# ログイン

ログイン機能を実装します。

```python:account/urls.py
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'), # 追加
]
```

ビューですが、Djangoにあらかじめ用意されているLoginViewを使用します。フォームとテンプレートファイルを指定すれば、入力データのチェックなどは勝手に実装してくれます。なんて便利。ありがとう、Django。

```python:account/views.py
from .forms import LoginForm # 追加
from django.contrib.auth.views import LoginView # 追加
from django.views import generic

'''追加'''
class Login(LoginView):
    form_class = LoginForm
    template_name = 'account/login.html'
```

LoginFormとはログインページを生成するためのフォームですが、まだそのようなフォームはありません。というわけで、forms.pyを新規作成します。DjangoにはAuthenticationFormというフォームが用意されているので、ありがたく使わせて頂きます。

```python:account/forms.py
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import get_user_model # ユーザーモデルを取得するため


# ユーザーモデル取得
User = get_user_model()


'''ログイン用フォーム'''
class LoginForm(AuthenticationForm):

    # bootstrap4対応
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['placeholder'] = field.label  # placeholderにフィールドのラベルを入れる
```

テンプレートファイルを作成します。

```html:template/account/login.html
{% extends 'base.html' %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <div class="col-md-6 offset-md-3">
    <div class="card mt-3 mb-3 bg-light">
      <!-- カードヘッダー -->
      <div class="card-header bg-primary text-light">
        <h4>Log in</h4>
      </div>
      <!-- カードボディー -->
      <div class="card-body">
        <form action="" method="POST">
          {{ form.non_field_errors }}
          {% for field in form %}
            {{ field }}
            {{ field.errors }}
            <br>
          {% endfor %}
          <button type="submit" class="btn btn-primary" >ログイン</button>
          <input type="hidden" name="next" value="{{ next }}" />
          {% csrf_token %}
        </form>
      </div>
    </div>
  </div>
</div>
{% endblock %}
```

base.htmlにログインページへのリンクを作成します。

```html:template/base.html
<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'account:top' %}">Top</a>
    </li>
    <!-- 追加 -->
    <li class="nav-item">
      <a class="nav-link" href="{% url 'account:login' %}">Log in</a>
    </li>
  </ul>
</div>
```

ログインしているか判別できるようにナビゲーションバーの下に表示を追加します。ログインしていない場合は「ゲスト」と表示され、ログインしている場合はユーザー名が表示されるようにします。

```html:template/base.html
<div class="pl-4 small" style="background-color: #e2e3e5 ; border-bottom: solid 1px #f3f3f3;">
  {% if not user.is_authenticated %}
    こんにちは！ ゲスト さん
  {% else %}
    こんにちは！ {{ user.username }} さん
  {% endif %}
</div>
```

settings.pyにログインのURLとログインが完了したときの遷移先を設定します。

```python:conf/setting.py
# 追加
LOGIN_URL = 'account:login' # ログインのURLの設定
LOGIN_REDIRECT_URL = 'account:top' #ログインが完了した後に遷移するURL
```

実装は以上です。ナビゲーションバーにログインページへのリンクが出来ており、ナビゲーションバーの下にはログイン状況が表示されています。

![topページにlog in.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/2dcb8005-78b0-fdf4-a606-91030af8a554.png)


ログイン画面はこのような感じです。

![ログイン画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/9bba6f69-18c6-5cd4-b0a9-d8f17043b8b1.png)

# ログアウト

ログアウト機能を実装します。ログインよりシンプルです。

```python:account/urls.py
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'), # 追加
]
```

ビューはログイン同様、既にあるLogoutViewを使用します。

```python:account/views.py
from django.contrib.auth.views import LoginView, LogoutView # 追加

'''追加'''
class Logout(LogoutView):
    template_name = 'account/logout_done.html'
```

テンプレートファイルを作成します。ログアウトが完了したら表示されるページです。

```html:templates/account/logout_done.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class='container'>
  <div class="col-md-6 offset-md-3">
    <div class='card mt-3 mb-3 bg-light'>
      <!-- カードヘッダー -->
      <div class='card-header bg-primary text-light'>
        <h4>Log out</h4>
      </div>
      <!-- カードボディー -->
      <div class='card-body'>
        <p>ログアウトしました。</p>
        <p>ご利用ありがとうございました。</p>
        <hr class='mt-2 mb-2'>
        <p><a href="{% url 'account:login' %}">もう一度ログインする</a></p>
      </div>
    </div>
  </div>
</div>
{% endblock %}
```

最後にナビゲーションバーにログアウトボタンを設置します。ログアウトボタンはログインしているときだけ表示するようにします。逆にログインボタンはログイン時には表示しないように変更します。

```html:templates/base.html

<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'account:top' %}">Top</a>
    </li>
    {% if not user.is_authenticated %}
      <li class="nav-item">
        <a class="nav-link" href="{% url 'account:login' %}">Log in</a>
      </li>
    {% else %}
      <li class="nav-item">
        <a class="nav-link" href="{% url 'account:logout' %}">Log out</a>
      </li>
    {% endif %}
  </ul>
</div>
```

ログアウト機能は以上です。ログインするとログアウトボタンが表示されます。

![topページにlog out追加.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/1f59ec99-ce69-cb44-b21c-535664665dcb.png)

ログアウトボタンをクリックすると、ログアウト処理が実行され、完了ページが表示されます。

![ログアウト完了ページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/365c566f-3c11-f227-13e6-0bcc074365d4.png)

# マイページ

ログインしたユーザーの登録情報を表示するページ（マイページ）を作ります。

```python:account/urls.py
urlpatterns = [
    path('', views.TopView.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'), # ログイン
    path('logout/', views.Logout.as_view(), name='logout'), # ログアウト
    path('my_page/<int:pk>/', views.MyPage.as_view(), name='my_page'), # 追加
]
```

ビューはひと工夫が必要です。なぜなら、マイページは自分以外のユーザーが閲覧できないようにするためです。ありがたいことにDjangoにはUserPassesTestMixinというミックスインが用意されているので、こちらを使います。ログインしてるユーザーのpk(主キー)と、表示するマイページのそれが同じかどうかチェックしてくれます。

```python:account/views.py
from django.contrib.auth import get_user_model # 追加
from django.contrib.auth.mixins import UserPassesTestMixin # 追加


'''自分しかアクセスできないようにするMixin(My Pageのため)'''
class OnlyYouMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        # 今ログインしてるユーザーのpkと、そのマイページのpkが同じなら許可
        user = self.request.user
        return user.pk == self.kwargs['pk']


'''マイページ'''
class MyPage(OnlyYouMixin, generic.DetailView):
    model = User
    template_name = 'account/my_page.html'
    # モデル名小文字(user)でモデルインスタンスがテンプレートファイルに渡される

```

マイページ自体はDetailViewを使います。モデルを指定すると、そのモデルのインスタンスがテンプレートファイルに渡されます。インスタンスの名前はモデル名小文字（今回の場合はuser）となります。

テンプレートファイルを作成します。もっと簡単に表示する方法もあるのですが、後で実装する登録情報更新機能やパスワード変更機能のことを考えて、このようなレイアウトにしました。

```html:templates/account/my_page.html
{% extends 'base.html' %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <h1>My page</h1>
  <hr>

  <!-- アカウント情報 -->
  <div class="row">
    <div class="col"><h4>■　登録情報</h4></div>
    <!-- 後で更新ボタン追加 -->
  </div>
  <table class="table table-bordered">
    <tbody>
      <tr>
        <td style="width: 50%;">姓</td>
        <td style="width: 50%;">{{ user.last_name }}</td>
      </tr>
      <tr>
        <td>名</td>
        <td>{{ user.first_name }}</td>
      </tr>
      <tr>
        <td>メールアドレス</td>
        <td>{{ user.email }}</td>
      </tr>
      <tr>
        <td>ユーザー名</td>
        <td>{{ user.username }}</td>
      </tr>
    </tbody>
  </table>

  <!-- パスワード -->
  <div class="row mt-4">
    <div class="col"><h4>■　パスワード</h4></div>
    <!-- 後で更新ボタン追加 -->
  </div>
  <table class="table table-bordered">
    <tbody>
      <tr>
        <td style="width: 50%;">パスワード</td>
        <td style="width: 50%;">非表示</td>
      </tr>
    </tbody>
  </table>

  <!-- 権限 -->
  <div class="row mt-4">
    <div class="col"><h4>■　権限</h4></div>
  </div>
  <table class="table table-bordered">
    <tbody>
      {% if user.is_staff %}
      <tr>
        <td style="width: 50%;">スタッフ権限</td>
        <td style="width: 50%;">{{ user.is_staff }}</td>
      </tr>
      {% endif %}
      {% if user.is_superuser %}
      <tr>
        <td>スーパーユーザー権限</td>
        <td>{{ user.is_superuser }}</td>
      </tr>
      {% endif %}
    </tbody>
  </table>


</div>
{% endblock %}
```

ナビゲーションバーにマイページへのリンクを追加します。

```html:templates/base.html
<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'account:top' %}">Top</a>
    </li>
    {% if not user.is_authenticated %}
      <li class="nav-item">
        <a class="nav-link" href="{% url 'account:login' %}">Log in</a>
      </li>
    {% else %}
      <!-- 追加 -->
      <li class="nav-item">
        <a class="nav-link" href="{% url 'account:my_page' user.pk %}">My page</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="{% url 'account:logout' %}">Log out</a>
      </li>
    {% endif %}
  </ul>
</div>
```

実装は以上です。ナビゲーションバーにマイページへのリンクが追加されています。

![My pageを追加.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/1189927f-157a-ce82-92b4-373f04090e95.png)

マイページは以下の様な画面になります。

![My_page.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/e0efa4c1-d246-e71b-c9ad-f6c67465035b.png)

マイページのURLは"localhost:8000/account/my_page/2"のようになっていると思います。2がユーザーモデルの主キーです。主キーはユーザー登録の際に自動的に採番されます。URLの数字を変えると別のユーザーの情報にアクセスできてしまうのですが、今回は対策を講じているのでアクセスが拒否されて、以下の様な画面が表示されます。（ブラウザによって表示される画面は違うかもしれません）

![403_Forbidden.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/da7da029-ce57-82d7-5ca8-0b912fbcfc4c.png)


# サインアップ
現状、ユーザーの追加は管理サイト(localhost:8000/admin)でしかできないので、ブラウザ上でも出来るようにします。


今回実装するコードは、ブラウザ上で必要事項を入力したら登録完了です。本番運用する際は、ブラウザ上でデータが入力されたら、入力されたアドレスにメールを送り、ユーザーがメール内のリンクをクリックしたら本登録される、といった処理が必要だと思いますが、私はこれから勉強します…。

```python:account/urls.py
urlpatterns = [
    # 省略
    path('signup/', views.Signup.as_view(), name='signup'), # サインアップ
    path('signup_done/', views.SignupDone.as_view(), name='signup_done'), # サインアップ完了
]
```

ビューはサインアップ用のビューとサインアップ完了用のビューを作成します。 Djangoに用意されているクラスベース汎用ビューの中からCreateViewを使ってサインアップを行います。

```python:account/views.py
from .forms import LoginForm, SignupForm # 追加
from django.shortcuts import redirect # 追加


'''サインアップ'''
class Signup(generic.CreateView):
    template_name = 'account/user_form.html'
    form_class =SignupForm

    def form_valid(self, form):
        user = form.save() # formの情報を保存
        return redirect('account:signup_done')
    
    # データ送信
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Sign up"
        return context


'''サインアップ完了'''
class SignupDone(generic.TemplateView):
    template_name = 'account/signup_done.html'
```
見慣れない処理としては、get_contex_dataメソッドを使ってテンプレートファイルにcontextを渡しています。process_nameという名前で、値は"Sign up"です。理由は、テンプレートファイルを再利用しつつ、デザインを動的に変化させるためです。

サインアップページの完成形は以下の様な画面になるのですが、上部のカードヘッダーと下部のボタンにSign up"と表示されています。これがcontext['process_name']の'Sign up'です。

![サインアップページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/4c0fe082-d636-0223-8cb6-aaef2f9dc617.png)


サインアップページではuser_form.htmlというテンプレートファイルを使うのですが、実はこれから実装する「登録情報更新ページ」と「パスワード更新ページ」でもuser_form.htmlを使います。もしuser-form.htmlにcontextデータを渡さないと三つのページは全く同じデザインになってしまい、ユーザーは自分が何の処理をしているか分かりにくくなります。そこで、テンプレートファイルにプロセスの名称(process_name)を渡すことで、Webページの表示を動的に変化できるようにしています。


ちなみに、テンプレートファイルを再利用するのは保守性向上のためです。類似のコードは出来るだけ共通化しておいた方が、デザインを変更するのが簡単になります。

長くなってしまいましたが、続いてサインアップ用のフォームを作成します。UserCreationFormを利用します。fieldsでWebページ上で入力する属性を指定します。

```python:account/forms.py
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 


'''サインアップ用フォーム'''
class SignupForm(UserCreationForm):
    
    class Meta:
        model = User
        fields = ('last_name', 'first_name', 'email','username', )

    def __init__(self, *args, **kwargs):

        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['required'] = '' # 全フィールドを入力必須
            
            # オートフォーカスとプレースホルダーの設定
            print(field.label)
            if field.label == '姓':
                field.widget.attrs['autofocus'] = '' # 入力可能状態にする
                field.widget.attrs['placeholder'] = '田中'
            elif field.label == '名':
                field.widget.attrs['placeholder'] = '一郎'
            elif field.label == 'メールアドレス':
                field.widget.attrs['placeholder'] = '***@gmail.com'
```

テンプレートファイルを作成します。まずはナビゲーションバーにサインアップページへのリンクを追加します。

```html:templates/base.html
<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" href="{% url 'account:top' %}">Top</a>
    </li>
    {% if not user.is_authenticated %}
      <li class="nav-item m-1">
        <a class="nav-link" href="{% url 'account:login' %}">Log in</a>
      </li>
      <!-- 追加 -->
      <li class="nav-item m-1">
        <a class="nav-link" href="{% url 'account:signup' %}">Sign up</a>
      </li>
    {% else %}
    # 省略
```

サインアップページのテンプレートファイルを作成します。

```html:templates/account/user_form.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <div class="col-md-6 offset-md-3">
    <div class="card mt-3 mb-3 bg-light">
      <!-- カードヘッダー -->
      <div class="card-header bg-success text-light">
        <h4>{{ process_name }}</h4>
      </div>
      <!-- カードボディー -->
      <div class="card-body">
        <form action="" method="POST">
          {{ form.non_field_errors }}
          {% for field in form %}
            <div class="form-group">
              <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
              {{ field }}
              {{ field.errors }}
            </div>
          {% endfor %}
          {% csrf_token %}
          <!-- ボタン -->
          <button type="button" class="btn btn-outline-secondary btn-lg btn-block mt-4" onclick="history.back()">キャンセル</button>
          <button type="submit" class="btn btn-success btn-lg btn-block mt-2" >{{ process_name }}</button>
        </form>
      </div>
    </div>
  </div>
</div>
{% endblock %}
```

{{ process_name }} はviews.pyにあるSignupクラスのget_context_dataメソッドで送られたデータです。contextは辞書型のデータで、キーを{{ }}で囲むことによってバリューをhtmlファイルに表示できます。

サインアップ完了ページを作成します。

```html:templates/account/sign_up_done.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <div class="col-md-6 offset-md-3">
    <div class="card mt-3 mb-3 bg-light">
      <!-- カードヘッダー -->
      <div class="card-header bg-success text-light">
        <h4>Sign up Done</h4>
      </div>
      <!-- カードボディー -->
      <div class="card-body">
        <p>ユーザー登録が完了しました。ログインページへお進みください。</p>
        <p class="text-danger">"ユーザー名"と"パスワード"は大切に管理してください。</p>
        <hr>
        <a href="{% url 'account:login' %}">Log in</a>
      </div>
    </div>
  </div>
</div>
{% endblock %}
```

サインアップページは以下の様になります。

![サインアップページ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/31200522-7278-443f-19d2-9fc6f407dfe7.png)


必要事項を入力してSign upボタンをクリックし、問題が無ければ以下の様なページが表示されます。

![サインアップ完了.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/647e6c0e-6f1b-e02a-55c7-a3b7551b17eb.png)

# ユーザー登録情報の更新

サインアップ（ユーザー登録）機能ができたので、次はユーザーの登録情報を更新するための機能を実装します。

```python:account/urls.py
urlpatterns = [
    # 省略
    path('user_update/<int:pk>', views.UserUpdate.as_view(), name='user_update'), # 登録情報の更新
]
```

サインアップでgeneric.CreateViewを使いましたが、Djangoは更新用のクラスベース汎用ビューも用意してくれています。generic.UpdateViewです。

```python:account/views.py
from .forms import LoginForm, SignupForm, UserUpdateForm # 追加
from django.shortcuts import redirect, resolve_url # 追加


'''ユーザー登録情報の更新'''
class UserUpdate(OnlyYouMixin, generic.UpdateView):
    model = User
    form_class = UserUpdateForm
    template_name = 'account/user_form.html'

    def get_success_url(self):
        return resolve_url('account:my_page', pk=self.kwargs['pk'])

    # contextデータ作成
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Update"
        return context
```

マイページで使ったOnlyYouMixinをここでも使います。自分以外が情報を書き換えられないようにするためです。また、サインアップで予告した通り、user_form.htmlを再利用します。contextデータの値は"Update"としています。

続いて、forms.pyを修正します。

```python:account/forms.py
from django import forms # 追加

'''ユーザー情報更新用フォーム'''
class UserUpdateForm(forms.ModelForm):
    
    class Meta:
        model = User
        fields = ('last_name', 'first_name', 'email', 'username',)

    # bootstrap4対応
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['required'] = '' # 全フィールドを入力必須
```

テンプレートファイルはuser_form.htmlを再利用しますが、更新ページのリンクは必要なので、my_page.htmlを修正します。

```html:templates/account/my_page.html
  <!-- アカウント情報 -->
  <div class="row">
    <div class="col"><h4>■　登録情報</h4></div>
    <!-- 追加 -->
    <div class="col text-right"><a href="{% url 'account:user_update' user.pk %}">更新</a></div>
  </div>
```

更新ページへのリンクが追加されました。

![マイページに登録情報更新リンク追加.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/7d55ff4b-caee-b5e9-34c9-f76a7f01fc1e.png)

登録情報更新画面は以下のようになります。しつこくてすみませんが、テンプレートファイルはサインアップと同じuser_form.htmlですが、Sign upと表示されていた部分がUpdateになっています。 

![登録情報更新画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/8a7b32a4-2725-a311-f617-064139c608e6.png)

地味にうれしいのが、現在の登録情報が入力欄に記入済みである点です。私は何もしていませんが、Djangoが勝手に処理してくれました。

試しにメールアドレスを変更してUpdateボタンをクリックすると、更新処理が行われ、マイページに遷移します。

![登録情報更新後.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/9ea942fb-0566-b15b-1b7f-5c6f253fd167.png)

# パスワード更新

最後の機能はパスワード更新です。早速やっていきましょう。

```python:account/urls.py
urlpatterns = [
    # 省略
    path('password_change/', views.PasswordChange.as_view(), name='password_change'), # パスワード変更
    path('password_change_done/', views.PasswordChangeDone.as_view(), name='password_change_done'), # パスワード変更完了
]
```

```python:account/views.py
from .forms import LoginForm, SignupForm, UserUpdateForm, MyPasswordChangeForm # 追加
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView # 追加
from django.urls import reverse_lazy # 追加　遅延評価用


'''パスワード変更'''
class PasswordChange(PasswordChangeView):
    form_class = MyPasswordChangeForm
    success_url = reverse_lazy('account:password_change_done')
    template_name = 'account/user_form.html'

    # contextデータ作成
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["process_name"] = "Change Password"
        return context


'''パスワード変更完了'''
class PasswordChangeDone(PasswordChangeDoneView):
    template_name = 'account/password_change_done.html'
```

サインインとユーザー登録情報の更新と同じく、user_form.htmlをテンプレートファイルとします。contextデータの値は"Change Password"としました。

フォームですが、今回はPasswordChangeFormを使います。ユーザー登録情報の更新ではModelFormを使ったので、このあたりの使い分けがややこしいです。（UserChangeFormみたいなものはないのでしょうか…）

```python:account/forms.py
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm, PasswordChangeForm


'''パスワード変更フォーム'''
class MyPasswordChangeForm(PasswordChangeForm):
    
    # bootstrap4対応で、classを指定
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
```

マイページにリンクを追加します。

```html:templates/account/my_pate.html
  <!-- パスワード -->
  <div class="row mt-4">
    <div class="col"><h4>■　パスワード</h4></div>
    <div class="col text-right"><a href="{% url 'account:password_change' %}">更新</a></div>
  </div>
```

パスワード変更完了画面を作成します。

```html:template/account/password_change_done.html
{% extends "base.html" %}

<!-- コンテンツ -->
{% block content %}
<div class="container">
  <div class="col-md-6 offset-md-3">
    <div class="card mt-3 mb-3 bg-light">
      <!-- カードヘッダー -->
      <div class="card-header bg-success text-light">
        <h4>Change Password Done</h4>
      </div>
      <!-- カードボディー -->
      <div class="card-body">
        <p>パスワードを更新しました。</p>
        <hr>
        <a href="{% url 'account:my_page' user.pk %}">My page</a>
      </div>
    </div>
  </div>
</div>
{% endblock %}
```

コーディングは以上です。お疲れ様でした。

画面を確認しましょう。マイページにパスワード更新リンクが追加されています。

![マイページにパスワード更新リンク追加.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/f80c9ada-0488-17ea-6033-3c840e285906.png)

パスワード更新画面は以下のようになります。

![パスワード更新画面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/aa6358f3-1e0f-6774-7d9c-d9fa252c6016.png)

現在のパスワードと新しいパスワード、新しいパスワード(確認用)を入力してChange Passwordボタンをクリックします。問題なければ、完了画面が表示されます。

![パスワード更新完了.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/177be282-44f9-3c4c-de48-b04ca6b6cddd.png)

# おわりに
機能別に実装してきたので、同じファイルを何度も修正することになり、記事が長くなってしまいました。

もしコードのミスやもっと良いやり方が有りましたら、ご指摘ただければ幸いです。