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で検索すると見つけやすいです。
次はプロジェクトの設定がでてくるので必要な情報を入力します。
プロジェクトの作成
を押したら完了です。
プロジェクトの作成には数分かかります。
ローカルにクローンする
プロジェクトの作成に成功したらリポジトリ
に行き、リポジトリをクローンします。
今回はHTTPSでクローンします。下記の画像のHTTPSボタンを押したらURLがコピーできるので、ローカル環境でプロジェクトを展開したい場所で
git clone 【HTTPS_URL】
を実行します。
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
を実行し下記のようなページが表示されたら成功です。
今回はこのプロジェクトを使っていきます。
詳細を知りたい方は以下のページが参考になると思います。
SQLite3からDynamoDBへの移行
SQLite3
非常に軽量なデータベース。Pythonに標準のライブラリがあるため使いやすく、ファイルでデータを保存するため永続化が可能。
DynamoDB
AWSのデータベースで非リレーショナルデータベース。
規模が大きくても信頼性の高いパフォーマンスが期待できる。
Django
ではSQLite3
などのデータベースを使う際ユーザーの情報やセッションIDの情報などが保存されます。
LoginView
,CreateView
などの汎用クラスではデータベースへのアクセスが頻繁に行われます。
しかし、DynamoDB
はDjango
にサポートされていないため、
データベースに関係する汎用クラス等が使用できなくなります。
LoginView
などは使えなくなるので注意が必要です。
これらをDynamoDB
で補完できるように変更していきます。
準備
AWS CLI
DjangoでDynamoDBを使うにはAWS CLIが必要になります。
以下のリンクが参考になります。
ライブラリ
必要なライブラリをインストールします。
pip install dynamorm
pip install marshmallow
pip install django-dynamodb-sessions
設定
DjangoはSQLite3のほかにもPostgreSQL
やOracle
、MySQL
をサポートしています。
DetaBaseの設定はsettings.py
のDATABASES
で設定します。ここではセッションの設定も記述します。
今回は使用しないためコメントアウトしています。
# 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
モデル
モデルは以下のようにします。
nameに操作したいDynamoDBのテーブル名を、hash_keyにプライマリキーを入れます。
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()
これでデータベースを使う準備ができました。
データベース、セッションの操作,詳細【割愛】
詳しくは別の記事で説明してます。
汎用クラスを補完してみる
ログインなど,汎用クラス等でできることをDynamoDBでもできるようにviews.py
を変更します。
基本的にDjangoの公式ドキュメントを参考して、データベースが関係する部分を変えています。
間違いや冗長な部分があるかもしれないので参考程度にしていただけると幸いです。
今回実装したのは以下の通りです。
- 会員登録
- ログイン
- ログアウト
- パスワード変更
メールを送るには
今回はメールを使って会員登録を行ってます。長くなってしまうので記事には書きません。
メールの送り方は以下のリンクが参考になると思います。
・Python + Djangoでメールを送信する
・DjangoからAmazon SES経由でメール送信する
会員登録
会員登録は大まかに分けて、仮登録と本登録に分かれています。
仮登録がUserCreate
,UserCreateDone
本登録がUserCreateComplete
,UserCreateComplateDone
です。
DjangoのCreateViewではパスワードの暗号化や保存をしてくれるのですがそれを今回は自分で書いています。
流れとしては
仮登録
- 新規ユーザーのデータを取得
- 同じIDの人がいないか確認
- パスワード暗号化
- DynamoDBに保存(仮登録:isActive = 0)
- アクティベーションURL発行、メール送信
- 完了
本登録
- トークンが正しいか確認
- DynamoDBを更新(本登録:isActive = 1)
- 完了
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に保存しています。
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()
によってセッションが存在するか確認できます。
この機能を作っておくことで、ログインしてるかの判定をしなくてよくなります。
ログイン
- セッションがすでに存在するかを確認(存在した場合トップページに移動)
- パスワードが正しいか、本登録済か確認
- セッション生成
- セッションに有効期限を付与(DynamoDBのTTLを使って削除)
- 完了
ログアウト
ログアウトはlogout(request)
だけで完了します。
これによってセッションが削除されます。
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")
パスワード変更
パスワードの変更は基本的にはデータの更新だけなので比較的簡単に済むと思います。
- データを取得
- パスワードがあっているか確認
- 新しいパスワードを暗号化
- 新しいパスワードを保存
- 完了
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)
サンプル
実際に動くサンプルを用意しました。こちららで結果を確認して行きます。
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と同じ場所に作成してください。
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
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
<!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
<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
<!--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
<!--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
<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
<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
<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)
- [django]raise ValidationErrorで指定したエラーメッセージを表示する
- [Django]既存のデータベースを利用する方法
- DjangoでDynamoDB、Sessionを使いたい
- DjangoでDynamoDB、Sessionを使いたい
- Python + Djangoでメールを送信する
- DjangoからAmazon SES経由でメール送信する
まとめ
SQLite3からDynamoDBへ移行するための手順を今回まとめました。DynamoDBだけで完結しようとするとDjangoの一部の機能が使えなくなるため痛いですが、その分勉強になりました。
validationを今回はviewで多少行っています。formにまとめることが可能なら試そうと思います。
ご指摘などありましたらコメントによろしくお願いします。