DjangoでDynamoDBに移行する


CodestarでDjangoを立ちあげ、データベースをsqliteからDynamoDBに移行します。

この記事は以下の方におすすめです！

:::note info
CodestarでDjangoを立ち上る方法を軽く知りたい
Djangoでデータベースがどのように使われているのかを知りたい
DjangoでDynamoDBに移行したい
:::


#概要
この記事ではAWSのサービスの一つである`CodeStar`を使用し、`Django`で環境を構築します。そこからDjangoのデフォルトのデータベースであるsqliteからAWSのDynamoDBに移行します。
DjangoはDynamoDBに対応していないので、ある程度自分で対応させる必要があります。

何か不備があった場合はコメントの方でご指摘いただけると幸いです。

#環境
```
git
Windows
Django==2.1.15
Python==3.6.12
```


#CodeStarでDjangoを立ち上げる


:::note info
CodeStarはAWSのサービスの一つです。
AWSの様々なサービスが使いやすいように統合された形になっており、アプリケーションを素早く開発、構築できます。
あまりAWSに慣れていない方におすすめのサービスです。
:::

##CodeStarでプロジェクトの作成
それではまずCodeStarでプロジェクトの作成を行っていきます。
AWSサービスのwebページに行き、CodeStarのページに行きます。
`プロジェクト`から`プロジェクトの作成`を押します。
すると下記のようなページが出てくるので下記のようにDjangoを選択し`next`を押します。Pythonで検索すると見つけやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/bceec2ad-972e-5287-2732-b74001826bfb.png)

次はプロジェクトの設定がでてくるので必要な情報を入力します。
![Inkedqiita_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/0cfe4c62-07dd-72a6-fd02-0c4b19a6cec5.jpeg)


`プロジェクトの作成`を押したら完了です。
プロジェクトの作成には数分かかります。

##ローカルにクローンする
プロジェクトの作成に成功したら`リポジトリ`に行き、リポジトリをクローンします。
今回はHTTPSでクローンします。下記の画像のHTTPSボタンを押したらURLがコピーできるので、ローカル環境でプロジェクトを展開したい場所で
`git clone 【HTTPS_URL】`を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/e06bed73-af58-4b8a-8d53-fe9e6211a71f.png)

settings.pyの該当する部分を以下のように変更します。

```python
import os #追加
#DATABASESを以下のように変更
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

manage.pyがある場所へ移動し、
`pyhton manage.py runserver`
を実行し下記のようなページが表示されたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/57e5e2fe-5911-f4a6-abf7-bc3e2cb4da47.png)

今回はこのプロジェクトを使っていきます。
詳細を知りたい方は以下のページが参考になると思います。
- [CodeStarでDjangoやーる（Python3.6.8、Django2.2.9）](https://qiita.com/SatoshiGachiFujimoto/items/0f7929d7fe7e30b9413c)

#SQLite3からDynamoDBへの移行


:::note info
SQLite3
非常に軽量なデータベース。Pythonに標準のライブラリがあるため使いやすく、ファイルでデータを保存するため永続化が可能。
:::

:::note info
DynamoDB
AWSのデータベースで非リレーショナルデータベース。
規模が大きくても信頼性の高いパフォーマンスが期待できる。
:::


`Django`では`SQLite3`などのデータベースを使う際ユーザーの情報やセッションIDの情報などが保存されます。
`LoginView`,`CreateView`などの汎用クラスではデータベースへのアクセスが頻繁に行われます。

しかし、`DynamoDB`は`Django`にサポートされていないため、
データベースに関係する汎用クラス等が使用できなくなります。
`LoginView`などは使えなくなるので注意が必要です。
これらを`DynamoDB`で補完できるように変更していきます。

##準備

###AWS CLI
DjangoでDynamoDBを使うにはAWS　CLIが必要になります。
以下のリンクが参考になります。

- [【Windows】AWS CLI のインストールと設定手順（Python＆pip使用）
](https://qiita.com/C_HERO/items/711edd101ea79198bdfb)

###ライブラリ
必要なライブラリをインストールします。

```
pip install dynamorm
pip install marshmallow
pip install django-dynamodb-sessions 
```


##設定
DjangoはSQLite3のほかにも`PostgreSQL`や`Oracle`、`MySQL`をサポートしています。
DetaBaseの設定は`settings.py`の`DATABASES`で設定します。ここではセッションの設定も記述します。
今回は使用しないためコメントアウトしています。

```python:settings.py
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
#     }
# }

#セッションを使うための設定
INSTALLED_APPS = [
    'django.contrib.sessions',
]
MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
]
SESSION_ENGINE = 'dynamodb_sessions.backends.dynamodb'

LOGIN_URL = 'helloworld:login'
LOGIN_REDIRECT_URL = 'helloworld:top'
LOGOUT_REDIRECT_URL = 'helloworld:login'

#セッションの有効期間などを設定
SESSION_COOKIE_AGE = 3600
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True

#AWSのアクセスIDとシークレットIDを追記
DYNAMODB_SESSIONS_AWS_ACCESS_KEY_ID = 'YOUR_AWS_ACCESS_KEY_ID'
DYNAMODB_SESSIONS_AWS_SECRET_ACCESS_KEY = 'YOUR_AWS_SECRET_ACCESS_KEY_ID'
DYNAMODB_SESSIONS_AWS_REGION_NAME = 'ap-northeast-1'
#DynamORMの設定
AWS_ACCESS_KEY_ID = 'YOUR_AWS_ACCESS_KEY_ID'
AWS_SECRET_ACCESS_KEY = 'YOUR_AWS_SECRET_ACCESS_KEY_ID'

```


詳細を知りたい方はこちらのサイトがおすすめです
 ・ [[Django]既存のデータベースを利用する方法](https://qiita.com/okoppe8/items/82a9cc2bb99de05f8dbb)
・[DjangoでDynamoDB、Sessionを使いたい](https://qiita.com/saka_rinta/items/aea763fd21dfecc72c66)



##DynamoDBでデータベース作成
使いたいテーブルをまずDynamoDBで作成しましょう。
今回はユーザーデータを保存する`SampleUser`、セッションを保存する`sessions`というテーブルを作成していきます。
DynamoDBのページに行き->テーブル->テーブルの作成、でそれぞれのテーブルを作成します。

それぞれのプライマリキーは以下の通りです。

- SampleUser : `UserId`
- sessions   : `SessionId`

この画像はSampleUserを作成しています。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/e08a0aa8-d75f-cde5-1440-fd7fc0bf9e5a.png)



##モデル
モデルは以下のようにします。
nameに操作したいDynamoDBのテーブル名を、hash_keyにプライマリキーを入れます。

```python:models.py
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from dynamorm import DynaModel
from marshmallow import fields


class SampleUser(DynaModel):
    class Table:
        name = "SampleUser"
        hash_key = 'UserId'
        read = 25
        write = 5
    class Schema:
        UserId = fields.String(required=True)
        Password = fields.String()
        isActive = fields.Boolean()
        name = fields.String()
        age = fields.Number()


class Sessions(DynaModel):
    class Table:
        name = "sessions"
        hash_key = 'SessionId'
        read = 25
        write = 5
    class Schema:
        SessionId = fields.String(required=True)
        ExpireTime = fields.Decimal()
       
```

これでデータベースを使う準備ができました。

##データベース、セッションの操作,詳細【割愛】

詳しくは別の記事で説明してます。

・[DjangoでDynamoDB、Sessionを使いたい](https://qiita.com/saka_rinta/items/aea763fd21dfecc72c66)

##汎用クラスを補完してみる
ログインなど,汎用クラス等でできることをDynamoDBでもできるように`views.py`を変更します。
基本的にDjangoの公式ドキュメントを参考して、データベースが関係する部分を変えています。
間違いや冗長な部分があるかもしれないので参考程度にしていただけると幸いです。
今回実装したのは以下の通りです。

- 会員登録
- ログイン
- ログアウト
- パスワード変更

###メールを送るには
今回はメールを使って会員登録を行ってます。長くなってしまうので記事には書きません。
メールの送り方は以下のリンクが参考になると思います。

・[Python + Djangoでメールを送信する](https://qiita.com/t-iguchi/items/88c37c06a9a6e153c33a)
・[DjangoからAmazon SES経由でメール送信する](https://dev.classmethod.jp/articles/sending-mail-from-django-through-amazon-ses/)

###会員登録
会員登録は大まかに分けて、仮登録と本登録に分かれています。
仮登録が`UserCreate`,`UserCreateDone`
本登録が`UserCreateComplete`,`UserCreateComplateDone`です。
DjangoのCreateViewではパスワードの暗号化や保存をしてくれるのですがそれを今回は自分で書いています。

流れとしては
**仮登録**

1. 新規ユーザーのデータを取得
1. 同じIDの人がいないか確認
1. パスワード暗号化
1. DynamoDBに保存(仮登録：isActive = 0)
1. アクティベーションURL発行、メール送信
1. 完了

**本登録**

1. トークンが正しいか確認
1. DynamoDBを更新(本登録：isActive = 1)
1. 完了


```python:views.py
class UserCreate(TemplateView):
    """ユーザー仮登録"""
    template_name = 'helloworld:user_create.html'

    def get(self, request, **kwargs):
        form = UserCreateForm()
        context = {
            "form":form,
        }
        return render(request, 'helloworld/user_create.html', context)
    
    def post(self, request, **kwargs):

        form = UserCreateForm(request.POST)
        if form.is_valid():
            UserId = form.cleaned_data["email"]
            form.save()
            # アクティベーションURLの送付
            current_site = get_current_site(self.request)
            domain = current_site.domain
            context = {
                'protocol': 'https' if self.request.is_secure() else 'http',
                'domain': domain,
                'token': dumps(UserId),
                'username': UserId,
            }
            subject_template = get_template('helloworld/create/subject.txt')
            subject = subject_template.render(context)
            message_template = get_template('helloworld/create/message.txt')
            message = message_template.render(context)
            send_mail(subject, message, None, [UserId], html_message=None, **kwargs) #メール送信
            return redirect('helloworld:user_create_done')
        context = {
            "form":form,
        }
        return render(request, 'helloworld/user_create.html', context)

class UserCreateDone(generic.TemplateView):
    """ユーザー仮登録完了"""
    template_name = 'helloworld/user_create_done.html'

class UserCreateComplete(generic.TemplateView):
    """メール内URLアクセス後のユーザー本登録"""
    template_name = 'helloworld/user_create_complete.html'
    timeout_seconds = getattr(settings, 'ACTIVATION_TIMEOUT_SECONDS', 60*60*24)  # デフォルトでは1日以内

    def get(self, request, **kwargs):
        """tokenが正しければ本登録."""
        token = kwargs.get('token')
        try:
            UserId = loads(token, max_age=self.timeout_seconds)
        # 期限切れ
        except SignatureExpired:
            print_log('SignatureExpired')
            return HttpResponseBadRequest()
        # tokenが間違っている
        except BadSignature:
            print_log('BadSignature')
            return HttpResponseBadRequest()
        # tokenは問題なし
        else:
            u = SampleUser.get(UserId=UserId) #DynamoDBからデータを取得して更新
            u.isActive = 1
            u.save(partial=True) # partial=True がないと全て更新される
            print_log('User create done.')
            return super().get(request, **kwargs)


class UserCreateDone(generic.TemplateView):
    """ユーザー仮登録完了"""
    template_name = 'helloworld/user_create_done.html'

```

formはclean_emailでそのユーザーが存在していないかチェックし、saveでDynamoDBに保存しています。

```python:forms.py(UserCreateForm)
def clean_email(self):
        email = self.cleaned_data.get('email')
        exist = False
        try:
            if SampleUser.get(UserId=email) : #DynamoDBにUserが存在するか確認
                exist = True
        except:
           pass
        if exist:
            raise ValidationError(
                self.error_messages['alredy_exist'],
            )
        return email

def save(self):
        password = make_password(self.cleaned_data.get('password1')) #パスワード暗号化
        email = self.cleaned_data.get('email')
        user = SampleUser(UserId=email,Password=password,isActive=0) #DynamoDBに保存
        user.save()
```

###ログイン・ログアウト

`OriginalLoginRequireMixin`はユーザーがログインをしていか確認するクラスです。
```self.request.session.keys()```によってセッションが存在するか確認できます。
この機能を作っておくことで、ログインしてるかの判定をしなくてよくなります。

**ログイン**

1. セッションがすでに存在するかを確認(存在した場合トップページに移動)
1. パスワードが正しいか、本登録済か確認
1. セッション生成
1. セッションに有効期限を付与(DynamoDBのTTLを使って削除)
1. 完了

**ログアウト**

ログアウトは```logout(request)```だけで完了します。
これによってセッションが削除されます。

```python:views.py

class OriginalLoginRequiredMixin(UserPassesTestMixin):
    raise_exception = False
    def test_func(self):
        if self.request.session.keys():
            return True
        else:
            self.request.session.flush() #セッションが存在しない場合はログインページに移動
            return False


class Login(TemplateView):
    """ログインページ"""
    template_name = 'helloworld/login.html'

    def get(self, request, **kwargs):
        form = LoginForm()
        if request.session.keys(): # セッションが存在した場合、topに飛ばす
            return redirect('helloworld:top')
        context = {"form":form}
        return render(request, 'helloworld/login.html', context)
    
    def post(self, request, **kwargs):
        form = LoginForm(request.POST)
        try:
           u = SampleUser.get(UserId=request.POST["username"]) # DynamoDBからデータ取得
           if check_password(request.POST["password"],u.Password) & u.isActive: #パスワードが正しいか、本登録済か  
            request.session['UserId'] = u.UserId #セッション生成
            if not request.session.session_key:
                request.session.save() # セッション保存
            date_time = Decimal(int((datetime.now()+relativedelta(months=1)).timestamp()))
            s = Sessions.get(SessionId=request.session.session_key) #セッションデータを取得し、ExpireTime(有効期限)を付与
            s.update(ExpireTime=date_time)
            s.save(partial=True)
            return redirect('helloworld:top')
        except:
           pass
        context = {"form":form,"error_mes":"メールアドレス、パスワードが正しくありません。"}
        return render(request, 'helloworld/login.html', context)
       


class Logout(TemplateView,OriginalLoginRequiredMixin):
    """ログアウトページ"""
    template_name = 'index.html'

    def get(self, request, **kwargs):
        logout(request) #ログアウト
        return redirect("helloworld:top")
```

###パスワード変更

パスワードの変更は基本的にはデータの更新だけなので比較的簡単に済むと思います。

1. データを取得
1. パスワードがあっているか確認
1. 新しいパスワードを暗号化
1. 新しいパスワードを保存
1. 完了

```python:views.py

class PasswordChange(TemplateView):
    """パスワード変更ビュー"""
    def get(self, request, **kwargs):
        form = MyPasswordChangeForm()
        context = {
            "form":form,
            "username" : request.session.get("UserId")
        }
        return render(request, 'helloworld/password_change.html', context)
    
    def post(self, request, **kwargs):
        form = MyPasswordChangeForm(request.POST)
        if form.is_valid():
            u = SampleUser.get(UserId=self.request.session.get("UserId")) #DynamoDBからデータを取得
            if check_password(form.cleaned_data["old_password"],u.Password): #パスワードをチェック
                if 'new_password1' in form.cleaned_data:
                    Password = make_password(form.cleaned_data["new_password1"]) #新しいパスワード暗号化
                    u.Password = Password
                    u.save(partial=True) #保存
                    return redirect('helloworld:password_change_done')
            else :
                context = {
                    "form":form,
                    "error_mes":"パスワードが一致していません。",
                    "username" : request.session.get("UserId")
                     }
                return render(request, 'helloworld/password_change.html', context)
        context = {
            "form":form,
            "username" : request.session.get("UserId")
            }
        return render(request, 'helloworld/password_change.html', context)
            

class PasswordChangeDone(TemplateView):
    """パスワード変更しました"""
    template_name = 'helloworld/password_change_done.html'
    def get(self, request, **kwargs):
        form = MyPasswordChangeForm()
        context = {
            "form":form,
            "pk":request.session.get("CustomerId"),
            "username" : request.session.get("UserId")
        }
        return render(request, 'helloworld/password_change_done.html', context)
 
```


##サンプル
実際に動くサンプルを用意しました。こちららで結果を確認して行きます。

1. メールアドレスとパスワードを作成して、送信します。
![s1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/eef5677e-00b9-c3f9-6344-c56871c8d613.jpeg)


2. 仮登録が完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/24216a2a-a3a4-f270-f080-b22fcf0eba87.png)

3. DynamoDBに見てみます。ユーザーが作成されています。
![s2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/3964f4f4-1583-0ef1-c6d9-356e745a86ab.jpeg)


4. メールが届くのでそのリンクを踏めば本登録完了です。
![s3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/97d0f4de-3da1-2368-3133-9faac6f25547.jpeg)


5. 本登録ができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/b2870fe1-48fe-37a0-ec84-2b8ea023617e.png)

6. ログインしてこの画面が出たら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/2443c7ef-5540-cb10-e837-9733a87339d3.png)

7. セッションもちゃんと保存されてます。
![s4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/da8ca175-c27b-dbd7-ea67-4656e8c3f0eb.jpeg)




###views.py

```python:views.py
# helloworld/views.py
from django.views.generic import TemplateView
from django.views import generic
from django.template.loader import get_template
from django.shortcuts import  redirect, render
from django.http import  HttpResponseBadRequest
from django.core.signing import BadSignature, SignatureExpired, loads, dumps
from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.auth.hashers import make_password,check_password
from django.contrib.auth import logout
from django.conf import settings
from decimal import Decimal
from dateutil.relativedelta import relativedelta
from datetime import datetime
from .models import SampleUser, Sessions
from .forms import (
    LoginForm, UserCreateForm,  MyPasswordChangeForm
)
 

class OriginalLoginRequiredMixin(UserPassesTestMixin):
    raise_exception = False
    def test_func(self):
        if self.request.session.keys():
            return True
        else:
            self.request.session.flush()
            return False


def print_log(msg):
    try:
        print("{}: {}".format(datetime.now(), msg.encode('utf-8')))
    except Exception as e:
        print(e)
        print(msg)
class Top(OriginalLoginRequiredMixin,TemplateView):

    template_name = 'helloworld/index.html'

    def get(self, request, *args, **kwargs):
        return render(request, self.template_name, context=None)


class Login(TemplateView):
    """ログインページ"""
    template_name = 'helloworld/login.html'

    def get(self, request, **kwargs):
        form = LoginForm()
        if request.session.keys():
            return redirect('helloworld:top')
        context = {"form":form}
        return render(request, 'helloworld/login.html', context)
    
    def post(self, request, **kwargs):
        form = LoginForm(request.POST)
        if form.is_valid():
            try:
                u = SampleUser.get(UserId=request.POST["email"])
                if check_password(request.POST["password"],u.Password) & u.isActive:
                    request.session['UserId'] = u.UserId
                    if not request.session.session_key:
                        request.session.save()
                    date_time = Decimal(int((datetime.now()+relativedelta(months=1)).timestamp()))
                    s = Sessions.get(SessionId=request.session.session_key)
                    s.update(ExpireTime=date_time)
                    s.save(partial=True)
                    return redirect('helloworld:top')
            except:
                pass
        context = {"form":form,"error_mes":"メールアドレス、パスワードが正しくありません。"}
        return render(request, 'helloworld/login.html', context)
       


class Logout(TemplateView,OriginalLoginRequiredMixin):
    """ログアウトページ"""
    template_name = 'index.html'

    def get(self, request, **kwargs):
        logout(request)
        return redirect("helloworld:top")

class UserCreate(TemplateView):
    """ユーザー仮登録"""
    template_name = 'helloworld:user_create.html'

    def get(self, request, **kwargs):
        form = UserCreateForm()
        context = {
            "form":form,
        }
        return render(request, 'helloworld/user_create.html', context)
    
    def post(self, request, **kwargs):

        form = UserCreateForm(request.POST)
        if form.is_valid():
            UserId = form.cleaned_data["email"]
            form.save()
            # アクティベーションURLの送付
            current_site = get_current_site(self.request)
            domain = current_site.domain
            context = {
                'protocol': 'https' if self.request.is_secure() else 'http',
                'domain': domain,
                'token': dumps(UserId),
                'username': UserId,
            }

            subject_template = get_template('helloworld/create/subject.txt')
            subject = subject_template.render(context)
            message_template = get_template('helloworld/create/message.txt')
            message = message_template.render(context)

            send_mail(subject, message, None, [UserId], html_message=None, **kwargs)

            return redirect('helloworld:user_create_done')
        context = {
            "form":form,
        }
        return render(request, 'helloworld/user_create.html', context)


class UserCreateDone(generic.TemplateView):
    """ユーザー仮登録完了"""
    template_name = 'helloworld/user_create_done.html'

class UserCreateComplete(generic.TemplateView):
    """メール内URLアクセス後のユーザー本登録"""
    template_name = 'helloworld/user_create_complete.html'
    timeout_seconds = getattr(settings, 'ACTIVATION_TIMEOUT_SECONDS', 60*60*24)  # デフォルトでは1日以内

    def get(self, request, **kwargs):
        """tokenが正しければ本登録."""
        token = kwargs.get('token')
        try:
            UserId = loads(token, max_age=self.timeout_seconds)
        # 期限切れ
        except SignatureExpired:
            print_log('SignatureExpired')
            return HttpResponseBadRequest()
        # tokenが間違っている
        except BadSignature:
            print_log('BadSignature')
            return HttpResponseBadRequest()
        # tokenは問題なし
        else:
            u = SampleUser.get(UserId=UserId)
            u.isActive = 1
            u.save(partial=True)
            print_log("Complete")
            print_log('User create done.')
            return super().get(request, **kwargs)


class UserCreateDone(generic.TemplateView):
    """ユーザー仮登録完了"""
    template_name = 'helloworld/user_create_done.html'


class PasswordChange(TemplateView):
    """パスワード変更ビュー"""
    def get(self, request, **kwargs):
        form = MyPasswordChangeForm()
        context = {
            "form":form,
            "username" : request.session.get("UserId")
        }
        return render(request, 'helloworld/password_change.html', context)
    
    def post(self, request, **kwargs):
        form = MyPasswordChangeForm(request.POST)
        if form.is_valid():
            u = SampleUser.get(UserId=self.request.session.get("UserId"))
            if check_password(form.cleaned_data["old_password"],u.Password):
                if 'new_password1' in form.cleaned_data:
                    Password = make_password(form.cleaned_data["new_password1"])
                    u.Password = Password
                    u.save(partial=True)
                    return redirect('helloworld:password_change_done')
            else :
                context = {
                    "form":form,
                    "error_mes":"パスワードが一致していません。",
                    "username" : request.session.get("UserId")
                     }
                return render(request, 'helloworld/password_change.html', context)
        context = {
            "form":form,
            "username" : request.session.get("UserId")
            }
        return render(request, 'helloworld/password_change.html', context)
            

class PasswordChangeDone(TemplateView):
    """パスワード変更しました"""
    template_name = 'helloworld/password_change_done.html'
    def get(self, request, **kwargs):
        form = MyPasswordChangeForm()
        context = {
            "form":form,
            "username" : request.session.get("UserId")
        }
        return render(request, 'helloworld/password_change_done.html', context)
 


```

###forms.py
forms.pyは最初は作成されていないので、views.pyと同じ場所に作成してください。

```python:forms.py
from logging import fatal
from django import forms
from django.contrib.auth.forms import (
   SetPasswordForm
)
import unicodedata
from django.core.exceptions import ValidationError
from django.contrib.auth import (
    password_validation,
)
from django.utils.translation import gettext as _
from .models import SampleUser
from django.contrib.auth.hashers import make_password


class LoginForm(forms.Form):
    """ログインフォーム"""
    email = forms.EmailField(label=_("Email"), max_length=254)
    password = forms.CharField(label=_("Password"), min_length=8,widget=forms.PasswordInput)

    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 
    
    def clean_password(self):
        password = self.cleaned_data.get('password')
        for p in password:
            if unicodedata.east_asian_width(p) != 'Na':
                self.add_error('password', '半角英字、数字、記号を組み合わせて8文字以上で入力してください。')
            return password


class UserCreateForm(forms.Form):
    """
    A form that creates a user, with no privileges, from the given username and
    password.
    """
    error_messages = {
        'password_mismatch': _('パスワードが一致しません'),
         'alredy_exist': _('このユーザーはすでに存在しています。'),
    }
    email = forms.EmailField(
        label=_("Email"),
        min_length=8,
        max_length=250,
        widget=forms.EmailInput(),
        help_text=_(""),
    )
    password1 = forms.CharField(
        label=_("Password"),
        strip=False,
        min_length=8,
        max_length=50,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        help_text=password_validation.password_validators_help_text_html(),
    )
    password2 = forms.CharField(
        label=_("Password confirmation"),
        min_length=8,
        max_length=50,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        strip=False,
        help_text=_("Enter the same password as before, for verification."),
    )

    class Meta:
        fields = ("email","password1","password2")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

    def clean_email(self):
        email = self.cleaned_data.get('email')
        exist = False
        try:
            if SampleUser.get(UserId=email) :
                exist = True
        except:
           pass
        if exist:
            raise ValidationError(
                self.error_messages['alredy_exist'],
            )
        return email
    
    def clean_password1(self):
        password = self.cleaned_data.get('password1')
        for p in password:
            if unicodedata.east_asian_width(p) != 'Na':
                self.add_error('password1', '半角英字、数字、記号を組み合わせて8文字以上で入力してください。')
            return password

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
        return password2

    def save(self):
        password = make_password(self.cleaned_data.get('password1'))
        email = self.cleaned_data.get('email')
        user = SampleUser(UserId=email,Password=password,isActive=0)
        user.save()

    def _post_clean(self):
        super()._post_clean()
        email = self.cleaned_data.get('email')
        password = self.cleaned_data.get('password2')
        if password:
            try:
                password_validation.validate_password(password, email)
            except ValidationError as error:
                self.add_error('password2', error)

            
class OriginalSetPasswordForm(forms.Form):
    """
    A form that lets a user change set their password without entering the old
    password
    """
    class Meta:
        fields = ("new_password1","new_password2")
    error_messages = {
        'password_mismatch': _("二つのパスワードが一致していません。"),
    }
    new_password1 = forms.CharField(
        label=_("New password"),
        strip=False,
        min_length=8,
        max_length=50,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        help_text=password_validation.password_validators_help_text_html(),
        )
    new_password2 = forms.CharField(
        label=_("New password confirmation"),
        min_length=8,
        max_length=50,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        strip=False,
        help_text=_("Enter the same password as before, for verification."),
    )

    def clean_new_password2(self):
        password1 = self.cleaned_data.get('new_password1')
        password2 = self.cleaned_data.get('new_password2')
        if password1 and password2:
            if password1 != password2:
                raise forms.ValidationError(
                    self.error_messages['password_mismatch'],
                    code='password_mismatch',
                )
        return password2

        
class MyPasswordChangeForm(OriginalSetPasswordForm):
    """
    A form that lets a user change their password by entering their old
    password.
    """
    error_messages = dict(SetPasswordForm.error_messages, **{
        'password_incorrect': _("Your old password was entered incorrectly. "
                                "Please enter it again."),
    })
    class Meta:
        fields = ("old_password")

    old_password = forms.CharField(label=_("Old password"),
                                   widget=forms.PasswordInput)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

    def clean_new_password1(self):
        password = self.cleaned_data.get('new_password1')
        for p in password:
            if unicodedata.east_asian_width(p) != 'Na':
                self.add_error('new_password1', '半角英字、数字、記号を組み合わせて8文字以上で入力してください。')
            return password
            

    def _post_clean(self):
        super()._post_clean()
        password = self.cleaned_data.get('new_password2')
        if password:
            try:
                password_validation.validate_password(password, None)
            except ValidationError as error:
                self.add_error('new_password2', error)


```

###urls.py

```python:urls.py

from django.urls import path
from . import views

app_name = "helloworld"

urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
    path('user_create/', views.UserCreate.as_view(), name='user_create'),
    path('user_create/done/', views.UserCreateDone.as_view(), name='user_create_done'),
    path('user_create/complete/<token>/', views.UserCreateComplete.as_view(), name='user_create_complete'),
    path('password_change/', views.PasswordChange.as_view(), name='password_change'),
    path('password_change/done/', views.PasswordChangeDone.as_view(), name='password_change_done'),
]

```

###templates
構成は以下のようになってます。
templates
　└─helloworld
　│ -index.html
　│ -login.html
　│ -password_change.html
　│ -password_change_done.html
　│ -user_create.html
　│ -user_create_complete.html　
　│ -user_create_done.html
　│
　└─　create
    　 　　　　-message.txt
     　 　　　　-subject.txt

**index.html**

```html:index.html
<!DOCTYPE html>
<html lang="en">
{% load static %}
<head>
    <meta charset="utf-8">
    <title>Python Django web application</title>
    <meta name="description" content="" />
    <link href="{% static "helloworld/css/styles.css" %}" rel="stylesheet">
    <link href="{% static "helloworld/css/gradients.css" %}" rel="stylesheet">
</head>

<body class="">
    <div class="wrapper">
        <div class="graphics">
            <div class="tower">
                <svg version="1.1" id="Layer_1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" x="0px" y="0px"
                     viewBox="-11 170 1000 429" enable-background="new -11 170 1000 429" xml:space="preserve">
                    <path class="path" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-miterlimit="10" d="M989,595H712v-35c0,0,4.5-1.8,8-3
                                                                                                  c5.2-1.8,12.5,5.3,22-4c3.4-3.4-0.9-7.8-0.4-10.1c0.7-3.1,4.4-6.8,1.6-11.4c-2.6-4.2-6.9-3.6-8.2-5.5c-1.7-2.3-2.2-6.3-7-9
                                                                                                  c-5.6-3.1-9.9,0.2-13-1c-2.5-0.9-2.3-5-9-5c-4.8,0-7.2,4.8-10,5c-2.3,0.2-4.9-4.5-10-2c-5.5,2.8-4,7.3-6,9c-1.4,1.2-3.1,3.2-6,4.2
                                                                                                  c-2.6,0.9-4.9,3-4,8.8c0.6,3.7,6.8,4.1,7.5,6c1,2.9-6.4,6.2-2.5,12c3,4.5,9,1.6,12,1c1.9-0.4,7.1-0.7,8,0c3.4,2.5,9,5,9,5v35
                                                                                                  l-506-1.5L182.3,444l0.3-0.8v-69.6l-0.2-0.2l12.3-72.3c10.9-2.6,16.5-6.5,16.5-6.5l-5.2-2.9l1.1-5.8l15-3.3l-10.1-3.9l1.2-4.6
                                                                                                  c-15.8-8.3-32.2-11-32.2-11v-3.6l4.1-6.5h-4.9v-4.4h-8.3V244h-1.2l-1.7-31.2l-1-1.3l-1,1.3l-1.7,31.2h-1.2v4.8h-8.3v4.4h-4.9
                                                                                                  l4.1,6.5v3.6c0,0-16.5,2.7-32.2,11l1.2,4.6l-10.1,3.9l15,3.3l1.1,5.8l-5.1,2.9c0,0,5.5,3.9,16.4,6.5l12.3,72.4l-0.1,0.1v69.6
                                                                                                  l0.3,0.8l-14.6,149.4h-1.2H-11"/>
                    <path class="path" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-miterlimit="10" d="M8,393.7c0-13.5,12.1-10.8,15.6-14.7
                                                                                                  c2.8-3.2-1-8.8,9-13.9c7.9-4.1,9.7,1,13.1,0.8c4.5-0.3,3.3-6.7,14.7-6.6c12.7,0.2,11.2,8.4,14.7,10.6c3,1.9,7.9-2.1,13.9,4.1
                                                                                                  c3.8,4,1.3,7.4,2.5,9.8c2.2,4.4,14.7,0.9,14.7,13.9c0,12.2-13.5,8.3-17.2,10.6c-3.6,2.3-4.4,9.3-13.1,11.5c-8,2-9.5-4-13.9-4.1
                                                                                                  c-5-0.1-5.5,8.6-18.8,6.6c-12.3-1.9-12.3-9.1-16.4-12.3C21.9,406.3,8,408.6,8,393.7z"/>
                    <path class="path" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-miterlimit="10" d="M325.1,313.9c-3.9,3-3.9,10-15.7,11.8
                                                                                                  c-12.8,2-13.3-6.4-18-6.3c-4.3,0.1-5.7,5.8-13.3,3.9c-8.4-2.1-9.1-8.8-12.5-11c-3.6-2.3-16.5,1.5-16.5-10.2c0-12.4,12-9.1,14.1-13.3
                                                                                                  c1.1-2.3-1.3-5.6,2.4-9.4c5.8-6,10.4-2.1,13.3-3.9c3.4-2.1,2-10,14.1-10.2c11-0.2,9.9,6,14.1,6.3c3.3,0.2,5-4.7,12.5-0.8
                                                                                                  c9.6,4.9,6,10.3,8.6,13.3c3.3,3.7,14.9,1.2,14.9,14.1C343.1,312.5,329.8,310.3,325.1,313.9z"/>
                    <path class="path" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-miterlimit="10" d="M18.4,229.5c0-13.5,12.1-10.8,15.6-14.7
                                                                                                  c2.8-3.2-1-8.8,9-13.9c7.9-4.1,9.7,1,13.1,0.8c4.5-0.3,3.3-6.7,14.7-6.6c12.7,0.2,11.2,8.4,14.7,10.6c3,1.9,7.9-2.1,13.9,4.1
                                                                                                  c3.8,4,1.3,7.4,2.5,9.8c2.2,4.4,14.7,0.9,14.7,13.9c0,12.2-13.5,8.3-17.2,10.6c-3.6,2.3-4.4,9.3-13.1,11.5c-8,2-9.5-4-13.9-4.1
                                                                                                  c-5-0.1-5.5,8.6-18.8,6.6c-12.3-1.9-12.3-9.1-16.4-12.3C32.2,242.1,18.4,244.4,18.4,229.5z"/>
                    <path class="path" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-miterlimit="10" d="M215.8,398.8c0-13.5,12.1-10.8,15.6-14.7
                                                                                                  c2.8-3.2-1-8.8,9-13.9c7.9-4.1,9.7,1,13.1,0.8c4.5-0.3,3.3-6.7,14.7-6.6c12.7,0.2,11.2,8.4,14.7,10.6c3,1.9,7.9-2.1,13.9,4.1
                                                                                                  c3.8,4,1.3,7.4,2.5,9.8c2.2,4.4,14.7,0.9,14.7,13.9c0,12.2-13.5,8.3-17.2,10.6c-3.6,2.3-4.4,9.3-13.1,11.5c-8,2-9.5-4-13.9-4.1
                                                                                                  c-5-0.1-5.5,8.6-18.8,6.6c-12.3-1.9-12.3-9.1-16.4-12.3C229.7,411.3,215.8,413.6,215.8,398.8z"/>
                </svg>

            </div>
        </div>

        <header>
            <nav class="website-nav">
                <ul>
                    <li><a href="{% url 'helloworld:logout' %}">Logout</a></li>
                    <li><a href="{% url 'helloworld:password_change' %}">Change_Password</a></li>
                </ul>
            </nav>
        </header>

        <div class="message">
            <a class="twitter-link" href="https://twitter.com/home/?status=I%20created%20a%20project%20with%20AWS%20CodeStar!%20%23AWS%20%23AWSCodeStar%20https%3A%2F%2Faws.amazon.com%2Fcodestar"><img src="{% static "helloworld/img/tweet.svg" %}" /></a>
            <div class="text">
                <h1>Congratulations!</h1>
                <h2>You just created a Python Django web application!</h2>
            </div>
        </div>
    </div>

    <footer>
        <p class="footer-contents">Designed and developed with <a href="https://aws.amazon.com/careers/devtools-jobs/">♥</a> in Seattle.</p>
    </footer>

    <script src="{% static "helloworld/js/set-background.js" %}"></script>
</body>

</html>


```


**login.html**

```html:login.html
<div>
    <div>
      <form action="{% url 'helloworld:login' %}" method="POST">
        <h2>
        <!-- サインイン -->
        </h2>
        <div>
          {{ form.non_field_errors }}
          {% for field in form %}
            {{ field }}
            {{ field.errors }}
            <br>
          {% endfor %}
          <p style="color:red; font-size:smaller; ">{{error_mes}}</p>
          {% csrf_token %}
          <button type="submit"><i class="fa fa-lock"></i> サインイン</button>
          <hr>
          <div>
            初めてご利用の方<br>
            <a href="{% url 'helloworld:user_create' %}">
              アカウントを作成する
            </a>
          </div>
        </div>
      </form>
    </div>
  </div>



```

**password_change.html**

```html:password_change.html
<!--main content start-->
  <section >
    <section >
      <h3><i ></i> パスワードの変更</h3>
      <hr>
      <div >
        <div >
            <form action="" method="POST">
              {{ form.non_field_errors }}
              <p> {{error_mes}} </p>
              {% for field in form %}
              <div>
                <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
                {{ field }}
                {{ field.errors }}
              </div>
              {% endfor %}
              {% csrf_token %}
              <button type="submit" >送信</button>
            </form>
        </div>
      </div>
    </section>
  </section>

```

**password_change_done.html**

```html:password_change_done.html
<!--main content start-->
    <section>
      <section>
        <h3><i></i> パスワードの変更</h3>
        <hr>
        <p >パスワードの変更が完了しました。</p>
         <li><a href="{% url 'helloworld:top' %}">Top</a></li>
      </section>
    </section>

```



**user_create.html**

```html:user_create.html
<div>
    <div>
      <form action="" method="POST" >
        <h2>ユーザー登録</h2>
        <div>
           <p>以下の項目を入力してください。<br>入力したメールアドレスに本登録用のリンクが送信されます。</p>
           <p>{{error_mes}} </p>
          {{ 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 %}
          <br>
          <button type="submit">送信</button>
          <hr> 
        </div>
      </form>
    </div>
  </div>
  


```


**user_create_done.html**

```html:user_create_done.html
<div>
    <div>
      <div>
        <h2>ユーザー登録</h2>
        <div>
          <p>
            会員登録確認メールを送信しました。まだ会員登録は完了していません。<br>
            入力したメールアドレスに確認メールを送信しましたので、確認メールに記載されているリンクから本登録を行ってください。
          </p>
          <hr>
          <div>
            <a href="{% url 'helloworld:top' %}">トップページ</a>
          </div>
        </div>
      </div>
    </div>
  </div>


```



**user_create_complete.html**

```html:user_create_complete.html
<div>
    <div>
      <div>
        <h2 >ユーザー登録完了</h2>
        <div>
          <p>
            本登録が完了しました。<br>
            登録されたメールアドレスとパスワードでログインしてください。
          </p>
          <hr>
          <div>
            <a href="{% url 'helloworld:top' %}">トップページ</a>
          </div>
        </div>
      </div>
    </div>
  </div>


```

**message.txt**

```
{{ username }} 様

会員登録手続きを行っていただき、ありがとうございます。

下記の本登録用URLよりサイトにアクセスの上、引き続き会員登録をお願いいたします。
まだ会員登録手続きは完了しておりませんので、ご注意ください。

本登録用URL:
{{ protocol}}://{{ domain }}{% url 'helloworld:user_create_complete' token %}

ログインURL:
{{ protocol}}://{{ domain }}{% url 'helloworld:login' %}

```

**subject.txt**

```
Sample - 会員登録
```

#参考

- [[django]フォームのバリデーションまとめ(to_python, clean, validate)](https://dackdive.hateblo.jp/entry/2015/09/11/102446)
- [[django]raise ValidationErrorで指定したエラーメッセージを表示する](https://qiita.com/komoto2020/items/4dce277b318c11d34db7)
- [[Django]既存のデータベースを利用する方法](https://qiita.com/okoppe8/items/82a9cc2bb99de05f8dbb)
- [DjangoでDynamoDB、Sessionを使いたい](https://qiita.com/saka_rinta/items/aea763fd21dfecc72c66)
- [DjangoでDynamoDB、Sessionを使いたい](https://qiita.com/saka_rinta/items/aea763fd21dfecc72c66)
- [Python + Djangoでメールを送信する](https://qiita.com/t-iguchi/items/88c37c06a9a6e153c33a)
- [DjangoからAmazon SES経由でメール送信する](https://dev.classmethod.jp/articles/sending-mail-from-django-through-amazon-ses/)

#まとめ

SQLite3からDynamoDBへ移行するための手順を今回まとめました。DynamoDBだけで完結しようとするとDjangoの一部の機能が使えなくなるため痛いですが、その分勉強になりました。
validationを今回はviewで多少行っています。formにまとめることが可能なら試そうと思います。

ご指摘などありましたらコメントによろしくお願いします。