Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する

## 環境
Windows 11 Home
Python 3.10.2
Django 4.0.2
venv利用あり
※関連記事 第1回が完了していること

## 関連記事
Django 第1回：[Django Custom User Model の作成](https://qiita.com/startours777/items/706d38e712b0c737a16a)
Django 第2回：[Django 初回ログイン時にパスワード変更を強制する](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)
Django 第3回：[Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する](https://qiita.com/startours777/items/d550ca9b67cea4408a44)
Django 第4回：[Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する](https://qiita.com/startours777/items/529e74062e7054e79517)　今回
Django 第5回：[Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除](https://qiita.com/startours777/items/5b1fc415e047a2044129)



## 背景
アカウント作成をする際、管理者が1件ずつ処理するのは現実的ではない。
そこでユーザからのリクエストを本Appで受け付け、上位者がそれを承認することでアカウントを自動作成することを想定する。
上長はアカウント不要とし、その代わりに上長向けにランダムなURLを発行し、そこで承認か非承認かを選択いただく。
またURLには有効期間を設け、長時間放置されるような申請については自動的にURLを無効化とする。
今回は1次承認者は申請者が自由に記載できるようにするが、システム開発責任者等に固定してもよい。
（申請者→申請者上長→システム開発責任者）

## 手順1：App作成
以下コマンドで`create_user`Appを作成する。
`python manage.py startapp create_user`

settings.pyに作成したAppを追加する

```python:mysite\settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'create_user', # 追加
]
```

## 手順2：models.py
```python:create_user\models.py
from django.db import models
import uuid as uuid_lib

class ApproveUser(models.Model):
    class Meta:
        verbose_name = 'アカウント申請ユーザ'
        verbose_name_plural = 'アカウント申請ユーザ'
    
    # ユーザ申請時に作成・更新
    uuid = models.UUIDField(default=uuid_lib.uuid4, primary_key=True, editable=False) # 管理ID
    username = models.CharField(unique=False, max_length=30) # ユーザ氏名、同姓同名があるためunique=False
    email = models.EmailField(unique=False) # ユーザメールアドレス、何度か申請することを考えunique=False
    application_date = models.DateTimeField() # 申請日時
    first_approver_name = models.CharField(max_length=30, unique=False) # 1次承認者氏名
    first_approver_email = models.EmailField(unique=False) # 1次承認者メールアドレス
    first_approver_token = models.CharField(max_length=50) # 1次承認者用URLトークン
    first_approver_expired_seconds = models.IntegerField() # 1次承認者用トークン有効時間
    first_approver_url = models.URLField() # 1次承認用URL

    # 1次承認時に更新
    first_approver_time = models.DateTimeField(blank=True, null=True) # 1次承認日時
    first_approver_result = models.BooleanField(blank=True, null=True) # 1次承認結果

```

以下2つのコマンドでDBに反映をする。
`python manage.py makemigrations`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/d9c57c44-392f-d360-f18c-6a179a23b2ec.png)
(`Aprooveuser`になっていますが、正しくは記載のコードのとおり`Approveuser`です)

`python manage.py migrate`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/e5f17f8a-a978-69e7-400a-b674bc3e5566.png)

## 手順2：forms.py
次に作成したモデルをベースにフォームを作成する。
`create_user` 配下に `forms.py`を作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/8268c276-f665-3b20-81bb-935fcdf0f29f.png)

以下のように記載する。

```python:create_user\forms.py
from django import forms
from .models import ApproveUser # models.pyで作成したmodel

class AppricationForm(forms.ModelForm):
    """アカウント申請フォーム"""

    class Meta:
        model = ApproveUser
        fields = ['username', 'email', 'first_approver_name', 'first_approver_email',] # Formに表示される項目
        # Formに実際に表示される文字
        labels = {
            'username': '申請者 氏名',
            'email': '申請者 メールアドレス',
            'first_approver_name': '1次承認者 氏名',
            'first_approver_email': '1次承認者 メールアドレス',
        }
        Widgets = {
            'username': forms.TextInput(attrs={'placeholder': '山田 太郎'}),
            'email': forms.TextInput(attrs={'placeholder': 'xxxx.yyyy@gmail.com'}),
            'first_approver_name': forms.TextInput(attrs={'placeholder': '日本 花子'}),
            'first_approver_email': forms.TextInput(attrs={'placeholder': 'zzzz.zzzz@gmail.com'}),
        }
    def clean(self):
        """申請者と1次承認が同じメールアドレスは不可"""
        cleaned_data = super().clean()
        email = cleaned_data.get('email')
        first_approver_email = cleaned_data.get('first_approver_email')
        if email == first_approver_email:
            raise forms.ValidationError('申請者と1次承認者のメールアドレスを同じにはできません')
        return cleaned_data

class FirstApproveForm(forms.ModelForm):
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる（表示のみ）
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['readonly'] = 'readonly'
        self.fields['email'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_name'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_email'].widget.attrs['readonly'] = 'readonly'


    class Meta:
        model = ApproveUser
        fields = ['username', 'email', 'first_approver_name', 'first_approver_email', 'first_approver_result',] # Formに表示される項目
        labels = {
            'username': '申請者 氏名',
            'email': '申請者 メールアドレス',
            'first_approver_name': '1次承認者 氏名',
            'first_approver_email': '1次承認者 メールアドレス',
            'first_approver_result': '1次承認結果',
        }

        first_approver_result = forms.BooleanField() # 1次承認の結果（True/False）

```

## 手順3：views.py
次に作成したViewで動作を決めていく。

```python:create_user\views.py
from django.shortcuts import render
from datetime import timedelta
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from .forms import AppricationForm
from .models import ApproveUser
from users.models import User
import string
import random
import math
import secrets

Token_LENGTH = 30 # ランダムURLを作成するためのTOKEN
PASSWORD_LENGTH = 12 # パスワードの長さ
EXPIRE_SECONDS = 60 #ランダムURLの有効期限(秒)

def pass_gen(size=PASSWORD_LENGTH):
    """パスワードジェネレータ"""
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return "".join(secrets.choice(chars) for x in range(size))

def create_account(username, email):
    """アカウント作成"""

    # passoword生成
    def_pass = pass_gen(PASSWORD_LENGTH)

    try:
        # すでにパスワードがある場合
        us = User.objects.get(email=email)

        # password設定
        us.set_password(def_pass)
        us.password_changed = False # Falseであれば次回ログイン時にパスワード変更画面に強制リダイレクト

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass
    
    except:
        # Userの作成
        us = User.objects.create_user(username=username, email=email, password=def_pass)

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass

def get_random_chars(char_num=Token_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])


def create(request):
    """アカウント申請画面"""
    if request.method == 'POST':
        # POSTであればフォームからデータを受け取る
        form = AppricationForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            try:
                # アカウントがすでにあるかどうか
                # ある場合も受け付けたように処理する（悪意のあるユーザにアカウント有無を判別されないようにする）
                queryset = User.objects.get(email=form.cleaned_data['email'])
                email = queryset.email
                is_active = queryset.is_active
                is_staff = queryset.is_staff

                if is_active == True and is_staff == True:
                    # すでに有効なアカウントがある場合
                    message = """
                    titte:アカウント申請
                    message:有効なアカウントが存在します。
                    """
                    print(message)
            except:
                # 有効なアカウントがないので処理を続ける
                pass

            # 期限付きURLの作成
            Timestamp_signer = TimestampSigner()
            context = {}
            context["expired_seconds"] = EXPIRE_SECONDS # URLの有効期限
            token = get_random_chars()
            token_signed = Timestamp_signer.sign(token) # ランダムURLの生成
            context["token_signed"] = token_signed

            obj.application_date = timezone.now() # 申請日時
            obj.first_approver_token = token
            obj.first_approver_url = f"http://127.0.0.1:8000/create_user/{token_signed}"
            obj.first_approver_expired_seconds = EXPIRE_SECONDS

            first_approver_name = form.cleaned_data['first_approver_name']
            first_approver_email = form.cleaned_data['first_approver_email']
            username = form.cleaned_data['username']
            email = form.cleaned_data['email']

            message =f"""
            title:アカウント1次承認依頼
            message:{first_approver_name} ({first_approver_email})さん 

            {username} ({email})さんよりアカウント承認依頼が届いています。
            以下のURLより承認を行ってください。

            {obj.first_approver_url}

            以上
            """
            print(message)
            context = {
                'title': 'アカウント申請完了',
                'message': 'アカウントを申請しました',
            }
            return render(request, 'create_user/create_request_done.html', context)
        else:
            # データが不正であればフォームを再描画する
            context = {'form': form}
            return render(request, 'create_user/create.html', context)

    else:
        # RequestがGET
        context = {
            'title': 'アカウント申請',
            'form': AppricationForm(),
        }
        return render(request, 'create_user/create.html', context)

def create_request_done(request):
    """アカウント申請完了画面"""
    context = {
        'title': 'アカウント申請完了',
        'message': 'アカウントを申請しました',
    }
    return render(request, 'create_user/create_request_done.html', context)

```

## 手順4：htmlの作成
描画するHTMLがないので作成する。
`/mysite/create_user`配下に`templates`フォルダを作り、さらにその配下に`create_user`フォルダを作成する。
`/mysite/create_user/templates/create_user`配下に以下の二つの新規ファイルを作成する
+ `create_request_done.html`
+ `create.html`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/1137547c-ef04-2292-729c-72b80cbc1f57.png)


```html:/mysite/create_user/templates/create_user/create_request_done.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
</body>
</html>
```

```html:/mysite/create_user/templates/create_user/create.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="{% url 'create_user:create' %}"method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <tr><th><td>
                        <input type="submit" value="送信" class="btn">
                    </td></th></tr>

            </table>
    </form>
</body>
</html>
```

## 手順5：urls.py
ViewとURLを紐づける。
`/mysite/create_user`配下に`urls.py`を作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/f0d1fa4d-e244-ee7d-a0c3-92a83551b410.png)

```python:/mysite/create_user/urls.py
from django.urls import path
from . import views

app_name = "create_user" # URLパターンを逆引きできるように名前を付ける

urlpatterns = [
    path('create/', views.create, name = 'create'), # アカウント申請画面
    path('create/create_request_done', views.create_request_done, name="create_request_done"),
]
```


## 手順5：mysiteのurls.py
`mysite`配下の`urls.py`を更新し、`create_user`配下の`urls.py`を読み込むようにする。

```python:/mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('create_user/', include('create_user.urls'))
]
```

## 手順6：動作の確認（途中まで）
この時点で、申請のページまでは作成できたので動作を確認する。
`python manage.py runserver`でサーバを立ち上げ、
`http://127.0.0.1:8000/create_user/create/`　にアクセスする。
CSSを設定していないので簡素だがフォームが表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/4379f9bc-1187-a46d-b86f-7b1ad69ab921.png)

適当に入力して送信を押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/4fca5a85-ef15-a8d8-d2a6-2b756f54d6ef.png)

申請完了ページが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/f22bbd55-7679-ddcb-2e52-c4f04134b89b.png)

一方、申請した内容は1次承認者に送られるが、ここではPrint文で出力しているのでそれを確認する。
実際にはここでメールを1次承認者に送信する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/ac038ea7-1042-9e6e-38d1-7360fb1104f7.png)


申請まではできたので、次はこのランダムURLから1次承認者が承認し、アカウントを払い出す部分を追加していく。

## 手順7：views.pyの更新
ランダムURLにアクセスしたさいのViewを作成していく

```python:create_user\views.py
from django.shortcuts import render
from datetime import timedelta
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from django.views.generic.base import TemplateView
from .forms import AppricationForm, FirstApproveForm
from .models import ApproveUser
from users.models import User
import string
import random
import math
import secrets

Token_LENGTH = 30 # ランダムURLを作成するためのTOKEN
PASSWORD_LENGTH = 12 # パスワードの長さ
EXPIRE_SECONDS = 60 #ランダムURLの有効期限(秒)

def pass_gen(size=PASSWORD_LENGTH):
    """パスワードジェネレータ"""
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return "".join(secrets.choice(chars) for x in range(size))

def create_account(username, email):
    """アカウント作成"""

    # passoword生成
    def_pass = pass_gen(PASSWORD_LENGTH)

    try:
        # すでにパスワードがある場合
        us = User.objects.get(email=email)

        # password設定
        us.set_password(def_pass)
        us.password_changed = False # Falseであれば次回ログイン時にパスワード変更画面に強制リダイレクト

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass
    
    except:
        # Userの作成
        us = User.objects.create_user(username=username, email=email, password=def_pass)

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass

def get_random_chars(char_num=Token_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])


def create(request):
    """アカウント申請画面"""
    if request.method == 'POST':
        # POSTであればフォームからデータを受け取る
        form = AppricationForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            try:
                # アカウントがすでにあるかどうか
                # ある場合も受け付けたように処理する（悪意のあるユーザにアカウント有無を判別されないようにする）
                queryset = User.objects.get(email=form.cleaned_data['email'])
                email = queryset.email
                is_active = queryset.is_active
                is_staff = queryset.is_staff

                if is_active == True and is_staff == True:
                    # すでに有効なアカウントがある場合
                    message = """
                    titte:アカウント申請
                    message:有効なアカウントが存在します。
                    """
                    print(message)
            except:
                # 有効なアカウントがないので処理を続ける
                pass

            # 期限付きURLの作成
            Timestamp_signer = TimestampSigner()
            context = {}
            context["expired_seconds"] = EXPIRE_SECONDS # URLの有効期限
            token = get_random_chars()
            token_signed = Timestamp_signer.sign(token) # ランダムURLの生成
            context["token_signed"] = token_signed

            obj.application_date = timezone.now() # 申請日時
            obj.first_approver_token = token
            obj.first_approver_url = f"http://127.0.0.1:8000/create_user/{token_signed}"
            obj.first_approver_expired_seconds = EXPIRE_SECONDS

            first_approver_name = form.cleaned_data['first_approver_name']
            first_approver_email = form.cleaned_data['first_approver_email']
            username = form.cleaned_data['username']
            email = form.cleaned_data['email']

            message =f"""
            title:アカウント1次承認依頼
            message:{first_approver_name} ({first_approver_email})さん 

            {username} ({email})さんよりアカウント承認依頼が届いています。
            以下のURLより承認を行ってください。

            {obj.first_approver_url}

            以上
            """
            print(message)
            context = {
                'title': 'アカウント申請完了',
                'message': 'アカウントを申請しました',
            }
            return render(request, 'create_user/create_request_done.html', context)
        else:
            # データが不正であればフォームを再描画する
            context = {'form': form}
            return render(request, 'create_user/create.html', context)

    else:
        # RequestがGET
        context = {
            'title': 'アカウント申請',
            'form': AppricationForm(),
        }
        return render(request, 'create_user/create.html', context)

def create_request_done(request):
    """アカウント申請完了画面"""
    context = {
        'title': 'アカウント申請完了',
        'message': 'アカウントを申請しました',
    }
    return render(request, 'create_user/create_request_done.html', context)

class ApproveView(TemplateView):
    template_name = 'create_users/approve.html'

    def get(self, request, token):
        if request.method == "GET":
            # GET = ランダムURLにアクセスしてきた際
            timestamp_signer = TimestampSigner()
            if token:
                try:
                    # TOKENが有効なら
                    unsigned_token = timestamp_signer.unsign(token, max_age=timedelta(seconds=EXPIRE_SECONDS))

                    user = ApproveUser.objects.get(first_approver_token=unsigned_token)

                    if user.first_approver_time == None:
                        # TOKEN未使用ならば
                        initial_dict = {
                            'username': user.username,
                            'email': user.email,
                        }
                        params = {
                            'title': '承認ページ',
                            'form': FirstApproveForm(initial=initial_dict)
                        }
                        return render(request, 'create_user/approve.html', params)
                    else:
                        params = {
                            'title': '承認ページ',
                            'message': 'すでに承認あるいは否認済みです'
                        }
                        return render(request, 'create_user/approve_done.html', params)
                
                except SignatureExpired:
                    # TOKENが期限切れならば
                    params = {
                        'title': '承認ページ',
                        'message': '有効期限切れにより自動的に否認されました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
    
                except BadSignature:
                    # TOKENが不正ならば（URLが間違っていたら）
                    params = {
                        'title': '承認ページ',
                        'message': 'URLが正しくありません'
                    }
                    return render(request, 'create_user/approve_done.html', params)

            else:
                # TOKENがない
                params = {
                    'title': '承認ページ',
                    'message': 'トークンが見つかりません'
                }
                return render(request, 'create_user/approve_done.html', params)
    
    def post(self, request, token):

        if request.method == "POST":
        # 承認・否認をする際
            timestamp_signer = TimestampSigner()
            unsigned_token = timestamp_signer.unsign(token, max_age=timedelta(seconds=EXPIRE_SECONDS))

            try:
                obj = ApproveUser.objects.get(first_approver_token=unsigned_token)
                form = FirstApproveForm(request.POST)

                if form.is_valid():
                    # パラメータ更新
                    obj.first_approver_time = timezone.now()
                    obj.first_approver_result = form.cleaned_data['first_approver_result']
                    obj.save()

                    # アカウント作成
                    username = obj.username
                    email = obj.email
                    password = create_account(username, email)

                    message = """
                    title:アカウント払い出し完了
                    message:{username} ({email})さん 

                    アカウントの払い出しが完了しました。

                    id : {email}
                    password : {password}
                    
                    以上
                    """
                    print(message)

                    params = {
                        'title': '承認ページ',
                        'message': '処理が完了しました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
                
                else:
                    params = {
                        'title': '承認ページ',
                        'message': 'エラーが発生しました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
            except:
                params = {
                    'title': '承認ページ',
                    'message': 'エラーが発生しました'
                }
                return render(request, 'create_user/approve_done.html', params)
```

## 手順8：承認用htmlの作成
`/mysite/create_user/templates/create_user`配下に以下の新規ファイルを作成する
+ `approve_done.html`
+ `approve.html`

actionの部分が先ほどと異なる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/05f0e601-150c-72d9-461d-ab9c786852f3.png)

```html:/mysite/create_user/templates/create_user/approve_done.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
</body>
</html>
```

```html:/mysite/create_user/templates/create_user/approve.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="" method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <tr><th><td>
                        <input type="submit" value="送信" class="btn">
                    </td></th></tr>

            </table>
    </form>
</body>
</html>
```

## 手順9：urls.pyの更新
ViewとランダムURLを紐づける。

```python:/mysite/create_user/urls.py
from django.urls import path
from . import views

app_name = "create_user" # URLパターンを逆引きできるように名前を付ける

urlpatterns = [
    path('create/', views.create, name = 'create'), # アカウント申請画面
    path('create/create_request_done', views.create_request_done, name="create_request_done"),
    path('<str:token>/', views.ApproveView.as_view(), name = 'ApproveView'), # 追加
]
```

## 手順10：動作確認
アクセスして必要事項を入力
http://127.0.0.1:8000/create_user/create/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/405d86c0-3ea7-5426-61a9-76e2e2845e6c.png)

コンソールに（メールで送るべき）内容が表示されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/1570ff91-d039-4b8e-053f-9765b12e8226.png)


記載のURLにアクセスする。
1次承認結果以外の欄はReadonly属性を指定しているので文字の変更はできない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/d0fdfd41-1df0-c1aa-984e-820b09fd3a44.png)


承認するかを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/c8d5ebe0-aab3-6dfa-0864-f483d98fa608.png)

承認完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/1e3b259c-5091-c027-6a99-67caf938602a.png)

コンソールにはPrint文でアカウントの払い出し完了が出力されている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/a143f580-cc2f-d4e1-510b-b54be6b0972a.png)


承認(否認)後に再度アクセスすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/6600586f-8e6d-b2ca-5b95-43e5aa575805.png)


期間経過後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/3e8b64a0-513d-8e4b-c287-3d899017e5da.png)

## 手順11：admin.py
管理画面で申請履歴を確認できるようにするには以下の手順を行う。
管理画面で見えなくていい場合は実施する必要はない。
なおアカウント申請履歴＝重要な履歴として、アカウント申請履歴データは削除させないものとする。

```python:/mysite/create_user/admin.py
from django.contrib import admin
from django.utils.translation import gettext, gettext_lazy as _
from .models import ApproveUser

@admin.register(ApproveUser)
class ApproveUserAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {'fields': ('username',)}),
        (_('Personal info'), {'fields': ('email', 'application_date',)}),
        (_('First Approver info'), {'fields': ('first_approver_name', 'first_approver_email', 'first_approver_url', 'first_approver_token', 'first_approver_expired_seconds',)}),
        (_('First Approver Result'), {'fields': ('first_approver_time', 'first_approver_result',)}),
    )
    list_display = ('username', 'email', 'application_date', 'first_approver_name', 'first_approver_email', 'first_approver_time', 'first_approver_result',)
    search_field = ('username', 'email',)
    ordering = ('-application_date',)

    def get_actions(self, request):
        # 一覧画面から削除できないようにする
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        # 詳細画面から削除できないようにする
        return False
```

http://127.0.0.1:8000/admin/ にアクセスする。
CREATE_USER アカウント申請ユーザ が追加されているのが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/d3df5f61-a5a3-1c94-cea0-0a3c9b39f6c0.png)

クリックすると申請一覧が確認できる。（削除できないことも確認）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/55c8da46-2dd0-7f23-d947-9107e6da7ed8.png)

USERNAMEをクリックして詳細を確認できる。（詳細でも削除できないことも確認）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/57a25dee-7bb5-5d0e-ef6c-1b3fe46cebb9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/9bb48c65-89d6-53ba-f9ef-8ee83a7e8a4e.png)



## 今後の改善点

```python:create_user\forms.py

class FirstApproveForm(forms.ModelForm):
...
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる（表示のみ）
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['readonly'] = 'readonly'
        self.fields['email'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_name'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_email'].widget.attrs['readonly'] = 'readonly'
...
```
の部分を

```python:create_user\forms.py

class FirstApproveForm(forms.ModelForm):
...
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる（表示のみ）
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['disabled'] = 'disabled'
        self.fields['email'].widget.attrs['disabled'] = 'disabled'
        self.fields['first_approver_name'].widget.attrs['disabled'] = 'disabled'
        self.fields['first_approver_email'].widget.attrs['disabled'] = 'disabled'
...
```
とすることで、承認者に選択できない個所を明確にすることができる。(グレーアウトできる)
しかしながらその状態で`はい` `いいえ`を選択するとエラーになってしまったことから`disabled`は不採用とした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/86adf766-d592-8d81-3546-2461460c5295.png)

選択肢・ボタンも冴えない。CSS、Bootstrapなどを使用することで、雰囲気を変えることができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/5873db39-2bd8-c694-52b2-32f61280d5d2.png)


## 参考
[Django ランダムなURLをUUIDで作成する方法](https://chuna.tech/detail/55/)
[Djangoの標準機能だけで期限つきリンクを作ってみる。](https://qiita.com/kyhei_0727/items/2bac77aaf84195510fc8)
Django 第1回：[Django Custom User Model の作成](https://qiita.com/startours777/items/706d38e712b0c737a16a)
Django 第2回：[Django 初回ログイン時にパスワード変更を強制する](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)
Django 第3回：[Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する](https://qiita.com/startours777/items/d550ca9b67cea4408a44)
Django 第4回：[Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する](https://qiita.com/startours777/items/529e74062e7054e79517)　
Django 第5回：[Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除](https://qiita.com/startours777/items/5b1fc415e047a2044129)