[Lightsail Django]No8 Userモデル
django-allauthを使用すると、簡単な設定でログイン画面、サインアップ画面、パスワード変更画面などの機能が利用できるようになりますが、DjangoのUserモデル周りの構成を理解しておかないと、django-allauthの設定で度々躓くことがありましたので、Userモデル周りについて整理しました。
1.Userモデル系
Userモデルは、passwordやemail、名前などのユーザー情報を保持するモデル(テーブル)です。
djangoではUserモデルをそのまま使用せずに、拡張しやすいようにカスタマイズされたUserモデル(別途自作アプリのModels.pyにカスタマイズ用としてCustomUserモデルを作るイメージ)の使用が推奨されており、django-allauthの設定手順でも、CustomUserを用意する手順がありますので、カスタマイズする際の補足についても合わせて説明して行きます。
1-1.User
UserはDjangoで標準で用意されているUserモデルで、UserはAbstractUserを継承しています。
自前で作るCustomUserには、AbstractUserまたは、AbstractBaseUserとPermissionsMixinを継承していくイメージとなります。
class User(AbstractUser):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
class Meta(AbstractUser.Meta):
swappable = "AUTH_USER_MODEL"
1-2.AbstractUser
AbstractUserはAbstractBaseUser, PermissionsMixinを継承しています。
AbstractUserにはプロフィール項目が多く用意されており、上記Userモデルから継承されています。(Userモデルと同じラインナップ)
ここから更に項目を増やしたい場合は、自前で作るCustomUserに、AbstractUserを継承させます。
class AbstractUser(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)
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"]
# ...
1-3.AbstractBaseUser
AbstractBaseUserは大元のModelで、AbstractUserと比べるとプロフィール項目は少なく、項目が少ない状態からカスタイズしたい場合、CustomUserにはAbstractBaseUserを継承させます。
class AbstractBaseUser(models.Model):
password = models.CharField(_("password"), max_length=128)
last_login = models.DateTimeField(_("last login"), blank=True, null=True)
is_active = True
REQUIRED_FIELDS = []
# Stores the raw password if set_password() is called so that it can
# be passed to password_changed() after the model is saved.
_password = None
class Meta:
abstract = True
def __str__(self):
return self.get_username()
# ...
1-4.PermissionsMixin
PermissionsMixinも大元のModeで、AbstractBaseUserと共に継承されて使用される形となります。
こちらのモデルにはis_superuserフィールドを保持しており、管理画面にログインできるユーザーの条件として、is_staff=True、is_superuser= Trueとあるため、このモデルは継承が必要となります。
class PermissionsMixin(models.Model):
"""
Add the fields and methods necessary to support the Group and Permission
models using the ModelBackend.
"""
is_superuser = models.BooleanField(
_("superuser status"),
default=False,
help_text=_(
"Designates that this user has all permissions without "
"explicitly assigning them."
),
)
groups = models.ManyToManyField(
Group,
verbose_name=_("groups"),
blank=True,
help_text=_(
"The groups this user belongs to. A user will get all permissions "
"granted to each of their groups."
),
related_name="user_set",
related_query_name="user",
)
user_permissions = models.ManyToManyField(
Permission,
verbose_name=_("user permissions"),
blank=True,
help_text=_("Specific permissions for this user."),
related_name="user_set",
related_query_name="user",
)
# ...
2.UserManagerモデル系
2-1.UserManager
UserManagerは、上記AbstractUserからコールされます。
create_userメソッドは、ユーザーを新規作成した時にコールされるイメージで、
is_staff=False、is_superuser=Falseで設定されます。
create_superuser:createsuperuser時にコールされるイメージで、
is_staff=True、is_superuser=Trueで設定されます。
管理画面にログインできるユーザーの条件として、is_staff=True、is_superuser= Trueとあるため、このようなメソッドで区分けされているかと思われます。
その他の処理は各種項目の加工処理なので、自前のCustomUserに独自項目を追加した際に、項目の加工処理などが必要であればUserManagerやBaseUserManagerを自作のCustomUserManagerから継承しつつ、独自項目の加工処理を追加していくイメージになるかと思います。
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not username:
raise ValueError("The given username must be set")
email = self.normalize_email(email)
# Lookup the real model class from the global app registry so this
# manager method can be used in migrations. This is fine because
# managers are by definition working on the real model.
GlobalUserModel = apps.get_model(
self.model._meta.app_label, self.model._meta.object_name
)
username = GlobalUserModel.normalize_username(username)
user = self.model(username=username, email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(username, email, password, **extra_fields)
2-2.BaseUserManager
class BaseUserManager(models.Manager):
@classmethod
def normalize_email(cls, email):
"""
Normalize the email address by lowercasing the domain part of it.
"""
email = email or ""
try:
email_name, domain_part = email.strip().rsplit("@", 1)
except ValueError:
pass
else:
email = email_name + "@" + domain_part.lower()
return email
def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD: username})
その他
その他、amazon LightsailにてDjangoを使用したWEBアプリ構築については以下となります。(こちらの記事は以下記事の続きとなります。)