ここでは,前のページと同じ方な方法でユーザ登録時に電子メールアドレスも設定できるようにします.ただし,電子メールは他のユーザと重複してはならないはずであるので,そのあたりの調整も同時に行いましょう.
まず,ユーザ登録時にメールアドレスも登録されるように修正します.
users/forms.py
class UserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
"""
error_messages = {
"password_mismatch": _("The two password fields didn’t match."),
}
password1 = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
strip=False,
help_text=_("Enter the same password as before, for verification."),
)
class Meta:
model = UserModel
fields = ("username", 'first_name', 'last_name', 'email')
field_classes = {"username": UsernameField}
widgets = {
'first_name': forms.TextInput(attrs={
'class': 'form-control',
'required' : True
}),
'last_name': forms.TextInput(attrs={
'class': 'form-control',
'required' : True
}),
'email': forms.EmailInput(attrs={
"autocomplete": "email",
'class': 'form-control',
'required' : True
}),
}
labels = {
'first_name': 'First Name',
'last_name': 'Last Name',
'email': 'Email',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._meta.model.USERNAME_FIELD in self.fields:
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
"autofocus"
] = True
def clean(self):
data = super().clean()
first_name = data.get('first_name')
last_name = data.get('last_name')
if len(first_name) == 0:
msg = "First Name が入力されていません"
self.add_error('first_name', msg)
elif len(first_name) > 20:
msg = "First Name の最大文字数は20文字です"
self.add_error('first_name', msg)
if len(last_name) == 0:
msg = "Last Name が入力されていません"
self.add_error('last_name', msg)
elif len(last_name) > 20:
msg = "Last Name の最大文字数は20文字です"
self.add_error('last_name', msg)
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 _post_clean(self):
super()._post_clean()
# Validate the password after self.instance is updated with form data
# by super().
password = self.cleaned_data.get("password2")
if password:
try:
password_validation.validate_password(password, self.instance)
except ValidationError as error:
self.add_error("password2", error)
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
この修正によって,メールアドレスもユーザ登録時に設定できるようになりました.しかしながら,他のユーザと同じメールアドレスを登録できてしまうという問題が残っています.
データベースでのユーザのテーブルを修正する前に,追加したユーザを一旦削除しておきます.とくに,メールアドレスが重複しているユーザがあればそれは削除しておくべきです.なお,削除するときには id
だけでなく,username
などでも削除するレコード(行)を指定できます.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>sqlite3 db.sqlite3 ⏎ SQLite version 3.38.2 2022-03-26 13:51:10 Enter ".help" for usage hints. sqlite> .headers ON ⏎ sqlite> select id, username, email from users_user; ⏎ id|username|email 1|user_a|a@sample.com 2|user_b|b@sample.com 3|user_c|c@sample.com 4|root|root@example.com 7|user_e| 8|user_f|a@sample.com sqlite> delete from users_user where id=7; ⏎ sqlite> delete from users_user where username="user_f"; ⏎ sqlite> select id, username, email from users_user; ⏎ id|username|email 1|user_a|a@sample.com 2|user_b|b@sample.com 3|user_c|c@sample.com 4|root|root@example.com sqlite>
次に,メールアドレスの重複を避けるために,データベースのテーブルの設計を変更します.現時点では email
フィールド(列)に UNIQUE が設定されていないことを確認します.なお,username
フィールドには UNIQUE が設定されていることも確認します.
sqlite> .schema users_user ⏎ CREATE TABLE IF NOT EXISTS "users_user" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "password" varchar(128) NOT NULL, "last_login" datetime NULL, "is_superuser" bool NOT NULL, "username" varchar(150) NOT NULL UNIQUE, "first_name" varchar(150) NOT NULL, "last_name" varchar(150) NOT NULL, "email" varchar(254) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "date_joined" datetime NOT NULL ); sqlite>
テーブルの設計を変更するために models.py を修正します.
users/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
class User(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True)
# email = models.EmailField(_("email address"), blank=True)
email = models.EmailField(
_("email address"),
# blank=True,
unique=True,
error_messages={
"unique": _("A user with that email already exists."),
},
)
is_staff = models.BooleanField(
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_active = models.BooleanField(
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
# abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
マイグレーションの実行状況を確認します.まだモデルの変更を行うためのマイグレーションが生成されていないこともわかります.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>python manage.py showmigrations ⏎ admin [X] 0001_initial [X] 0002_logentry_remove_auto_add [X] 0003_logentry_add_action_flag_choices auth [X] 0001_initial [X] 0002_alter_permission_name_max_length [X] 0003_alter_user_email_max_length [X] 0004_alter_user_username_opts [X] 0005_alter_user_last_login_null [X] 0006_require_contenttypes_0002 [X] 0007_alter_validators_add_error_messages [X] 0008_alter_user_username_max_length [X] 0009_alter_user_last_name_max_length [X] 0010_alter_group_name_max_length [X] 0011_update_proxy_permissions [X] 0012_alter_user_first_name_max_length comments (no migrations) contenttypes [X] 0001_initial [X] 0002_remove_content_type_name sessions [X] 0001_initial users [X] 0001_initial (py39) C:\Users\lecture\Documents\django\custom_auth_project>
マイグレーションファイルを生成します.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>python manage.py makemigrations ⏎ Migrations for 'users': users\migrations\0002_alter_user_email.py - Alter field email on user (py39) C:\Users\lecture\Documents\django\custom_auth_project>
いま生成されたマイグレーションファイルの中身を確認します.
users/migrations/0002_alter_user_email.py
# Generated by Django 4.0.6 on 2022-08-14 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True, verbose_name='email address'),
),
]
もう一度マイグレーションの実行状況を確認します.いま生成したマイグレーションがまだ実行されていない(つまり,データベースの設計変更が反映されていない)ことがわかります.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>python manage.py showmigrations ⏎ admin [X] 0001_initial [X] 0002_logentry_remove_auto_add [X] 0003_logentry_add_action_flag_choices auth [X] 0001_initial [X] 0002_alter_permission_name_max_length [X] 0003_alter_user_email_max_length [X] 0004_alter_user_username_opts [X] 0005_alter_user_last_login_null [X] 0006_require_contenttypes_0002 [X] 0007_alter_validators_add_error_messages [X] 0008_alter_user_username_max_length [X] 0009_alter_user_last_name_max_length [X] 0010_alter_group_name_max_length [X] 0011_update_proxy_permissions [X] 0012_alter_user_first_name_max_length comments (no migrations) contenttypes [X] 0001_initial [X] 0002_remove_content_type_name sessions [X] 0001_initial users [X] 0001_initial [ ] 0002_alter_user_email (py39) C:\Users\lecture\Documents\django\custom_auth_project>
マイグレーションを実行します.この作業によって実際にデータベースの設計が変更されます.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>python manage.py migrate ⏎
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
Applying users.0002_alter_user_email... OK
(py39) C:\Users\lecture\Documents\django\custom_auth_project>
マイグレーションの実行状況をもう一度確認します.
(py39) C:\Users\lecture\Documents\django\custom_auth_project>python manage.py showmigrations ⏎ admin [X] 0001_initial [X] 0002_logentry_remove_auto_add [X] 0003_logentry_add_action_flag_choices auth [X] 0001_initial [X] 0002_alter_permission_name_max_length [X] 0003_alter_user_email_max_length [X] 0004_alter_user_username_opts [X] 0005_alter_user_last_login_null [X] 0006_require_contenttypes_0002 [X] 0007_alter_validators_add_error_messages [X] 0008_alter_user_username_max_length [X] 0009_alter_user_last_name_max_length [X] 0010_alter_group_name_max_length [X] 0011_update_proxy_permissions [X] 0012_alter_user_first_name_max_length comments (no migrations) contenttypes [X] 0001_initial [X] 0002_remove_content_type_name sessions [X] 0001_initial users [X] 0001_initial [X] 0002_alter_user_email (py39) C:\Users\lecture\Documents\django\custom_auth_project>
データベースのテーブル設計を確認すると,email フィールドに UNIQUE 特性が追加されていることがわかります.また,データベースのロールバックを行っていないので,以前のデータはそのまま残っていることも確認してください.
sqlite> .schema users_user ⏎ CREATE TABLE IF NOT EXISTS "users_user" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "password" varchar(128) NOT NULL, "last_login" datetime NULL, "is_superuser" bool NOT NULL, "username" varchar(150) NOT NULL UNIQUE, "first_name" varchar(150) NOT NULL, "last_name" varchar(150) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "date_joined" datetime NOT NULL, "email" varchar(254) NOT NULL UNIQUE ); sqlite>
すでに登録されているメールアドレスでユーザ登録を行おうとした場合に正しくエラーメッセージが表示されることがわかりました.