1
2

More than 3 years have passed since last update.

DjangoでDynamoDBに移行する

Last updated at Posted at 2021-08-19

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

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

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を立ち上げる

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

CodeStarでプロジェクトの作成

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

image.png

次はプロジェクトの設定がでてくるので必要な情報を入力します。
Inkedqiita_LI.jpg

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

ローカルにクローンする

プロジェクトの作成に成功したらリポジトリに行き、リポジトリをクローンします。
今回はHTTPSでクローンします。下記の画像のHTTPSボタンを押したらURLがコピーできるので、ローカル環境でプロジェクトを展開したい場所で
git clone 【HTTPS_URL】を実行します。
image.png

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

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

今回はこのプロジェクトを使っていきます。
詳細を知りたい方は以下のページが参考になると思います。
- CodeStarでDjangoやーる(Python3.6.8、Django2.2.9)

SQLite3からDynamoDBへの移行

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

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

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

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

準備

AWS CLI

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

ライブラリ

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

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

設定

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

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]既存のデータベースを利用する方法
DjangoでDynamoDB、Sessionを使いたい

DynamoDBでデータベース作成

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

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

  • SampleUser : UserId
  • sessions : SessionId

この画像はSampleUserを作成しています。
image.png

モデル

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

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を使いたい

汎用クラスを補完してみる

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

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

メールを送るには

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

Python + Djangoでメールを送信する
DjangoからAmazon SES経由でメール送信する

会員登録

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

流れとしては
仮登録

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

本登録

  1. トークンが正しいか確認
  2. DynamoDBを更新(本登録:isActive = 1)
  3. 完了
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に保存しています。

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

ログアウト

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

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. データを取得
  2. パスワードがあっているか確認
  3. 新しいパスワードを暗号化
  4. 新しいパスワードを保存
  5. 完了
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

  2. 仮登録が完了しました。
    image.png

  3. DynamoDBに見てみます。ユーザーが作成されています。
    s2.jpg

  4. メールが届くのでそのリンクを踏めば本登録完了です。
    s3.jpg

  5. 本登録ができました。
    image.png

  6. ログインしてこの画面が出たら成功です。
    image.png

  7. セッションもちゃんと保存されてます。
    s4.jpg

views.py

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と同じ場所に作成してください。

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

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

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

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

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

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

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

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

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 - 会員登録

参考

まとめ

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2