ここでは,前のページと同じ方な方法でユーザ登録時に電子メールアドレスも設定できるようにします.ただし,電子メールは他のユーザと重複してはならないはずであるので,そのあたりの調整も同時に行いましょう.
まず,ユーザ登録時にメールアドレスも登録されるように修正します.
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>
すでに登録されているメールアドレスでユーザ登録を行おうとした場合に正しくエラーメッセージが表示されることがわかりました.