naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

Djangoで、Userモデルのカスタマイズ(継承)

約459日前 2017年7月14日0:04
プログラミング関連
Django Python

概要


Djangoで、会員登録機能を自作するの1つです。

DjangoのデフォルトのUserモデルでは、usernameというフィールドがあります。
しかし、Webアプリによってはusernameとしてemailを利用することも多く、この場合はusernameというフィールドが邪魔です。
今回はusernameをなくし、emailをメインに扱うモデルを作成します。


継承を使う際、大まかに2つの方法があります。
django.contrib.auth.base_user.AbstractBaseUserを継承するか、
django.contrib.auth.models.AbstractUserを継承するか、です。

AbstractUserはデフォルトのUserモデルが持っている機能を全て持っているので、元々のUserモデルに何か修正をするというよりは、profileやphone_number、といった新しいフィールドを追加したい場合に継承するのが良さそうです。

AbstractBaseUserの継承は色々と面倒な部分が多いのですが、挙動を柔軟にカスタマイズできます。
今回はこちらを使います。

models.py


models.pyを、以下のように変更します。
https://github.com/django/django/blob/master/django/contrib/auth/models.py
AbstractUserのソースコードを殆ど借用しつつ、usernameの代わりにemailを使うようにしています。

from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):
"""ユーザーマネージャー."""

use_in_migrations = True

def _create_user(self, email, password, **extra_fields):
"""メールアドレスでの登録を必須にする"""
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)

user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user

def create_user(self, email, password=None, **extra_fields):
"""is_staff(管理サイトにログインできるか)と、is_superuer(全ての権限)をFalseに"""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)

def create_superuser(self, email, password, **extra_fields):
"""スーパーユーザーは、is_staffとis_superuserをTrueに"""
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(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
"""カスタムユーザーモデル."""

email = models.EmailField(_('email address'), unique=True)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, 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 = 'email'
REQUIRED_FIELDS = []

class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')

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

def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)

@property
def username(self):
"""username属性のゲッター

他アプリケーションが、username属性にアクセスした場合に備えて定義
メールアドレスを返す
"""
return self.email



settings.py


自分で作ったUserモデルをデフォルトで使用するように宣言する必要があります。
settings.pyに、以下を付け加えましょう。INSTALLED_APPSに、モデルを定義したアプリケーションを追加するのも忘れないでください。

# registerというアプリケーションです
AUTH_USER_MODEL = 'register.User'



自作のログイン画面等を使う場合は問題ないのですが、デフォルトの/adminでこのカスタムユーザーを使う場合は少し処理を加える必要があります。

admin.py


複雑に見えるかもしれませんが、これも
https://github.com/django/django/blob/master/django/contrib/auth/admin.py
のコードが殆どです。
元々のUserモデルが使っていたusernameをemailに置き換え、使用しているFormもカスタムユーザーに合わせたものにしているだけです。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _
from .models import User


class MyUserChangeForm(UserChangeForm):
class Meta:
model = User
fields = '__all__'


class MyUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = ('email',)


class MyUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('email', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
form = MyUserChangeForm
add_form = MyUserCreationForm
list_display = ('email', 'first_name', 'last_name', 'is_staff')
list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
search_fields = ('email', 'first_name', 'last_name')
ordering = ('email',)


admin.site.register(User, MyUserAdmin)




デフォルトUserを使うか選ばせる場合


デフォルトのUserか、このカスタムユーザーかをユーザーに選択させる場合は、settings.pyに書いたAUTH_USER_MODELの値で判別ができます。
models.pyとadmin.pyにて以下のようにするだけです。

# registerのUserモデルを使う場合だけ、登録する
if settings.AUTH_USER_MODEL == 'register.User':
# 以降、カスタムユーザーを使う際のモデル定義や、管理サイトのカスタマイズ


途中からユーザーをカスタムユーザーにしたい、といった場合は複雑になるので、どちらを使うか最初に選択してもらう必要があります。

参考
https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html
https://www.caktusgroup.com/blog/2013/08/07/migrating-custom-user-model-django/
django見習い 約172日前 2018年4月26日12:43 返信する
添付ファイルダウンロード(0FF53700-B9CC-48F8-9C18-107E0047D6C6.jpeg)
何度かデフォルトユーザーのままマイグレーションをし、途中からカスタムユーザーを使いたいなと思い、ファイルの書き換えをおこないました。
そしてマイグレーションファイルとキャッシュを全て削除したのちmigrateしようとすると次のようなエラーになるのですが、データベースのテーブルの削除か何かをしなければならないのでしょうか?
なりと 約172日前 2018年4月26日19:49
途中からカスタムユーザーに切り替えるのは中々に難しいです。手動でテーブルを変更したり、データの移行処理が必要になることもあります。ベストプラクティクスの1つとして「最初からカスタムユーザーモデルを定義だけしておく」というものがありますが、それもこのためです。
SQLに明るくない場合の楽な方法として、マイグレーションフォルダとdb.sqlite3(SQLiteの場合)を削除してしまい、初めから作り直すことです。移行するデータがあるのならば、各データの中身だけ取り出し、後でそれを入力します。入力作業はPythonスクリプトで自動で行うこともでき、個人的にはオススメな方法です。
django見習い 約171日前 2018年4月27日14:42 返信する
settings.pyの一部をコメントアウトする方法で解決しました。
最初からカスタムユーザーの定義だけしておくというやり方、とても参考になります。
現在このサイトを参考にウェブアプリケーションを作成していて、エラーと格闘しているので、近いうちにまた質問をさせていただくかもしれません。
その時はまたアドバイスを宜しくお願い致します。
名無し 約60日前 2018年8月16日1:31 返信する
質問失礼します。
webアプリについての知識が少なく、
初歩的な質問ですみません。
退会処理などをした際にアカウントの削除を行うと思うのですが、
実際に削除ではなく、is_active の要素を false にしているメリットなどがあれば教えていただきたいです。
なりと 約60日前 2018年8月16日7:01
Userモデルはその性質上、他のモデルと紐づくことが多いです。
ブログの記事ならば記事を編集したUserと紐づくかもしれませんし、会員登録性の予約アプリケーションならば予約データはUserと紐づくかもしれません。この状態でUserを削除してしまうと、それと紐付いている他モデルに影響が出る可能性があります。
ForeignKeyやOneToOneにあるon_delete引数を使うことで、Userを削除したら不味そうな場合は削除させない等の処理も今はできますが、それに頼りすぎるのも少し怖いです。
公式ドキュメントでは
「アカウントを削除するのではなく、このフラグを False に設定することをお勧めします。 そうすれば、アプリケーションに外部キーがある場合でも、外部キーが破損しません。」
と説明されています。

また、一度退会した後にもう一度登録したくなった等のケースではis_activeをTrueにするだけで全て元通りにできます。
User自体が削除されていないなら、退会したユーザーと、Userに紐付いたデータを見て色々と分析する、といったことも容易になります。
それらを考えると、is_active=Falseにしておく方が汎用的かなぁと思います。
名無し 約15日前 2018年9月30日21:19 返信する
models.pyの48-50行目について教えて下さい。
例えば
models.CharField(_('first name'), max_......
となっていますが、これと
models.CharField('first name', max_......
の違いは何でしょうか?

djangoのドキュメントを見てもよく分からず、Pythonの文法の問題なのかなと思ったのですが、これまでこのような書き方は見たことがなかったために質問させて頂きました。(udemyも受講させて頂きましたが、アンダースコアは付いていなかったように思います。)
宜しくお願い致します。
なりと 約15日前 2018年10月1日0:20
そのアンダースコアは関数です。
以下のように、gettext(又はgettext_lazy)という関数を別名でimportしています。
from django.utils.translation import gettext as _

このgettext関数が何かといいますと、多言語対応する際に使う関数です。
_('first_name') とするとその部分は翻訳されます。settings.pyに書いたLANGUAGE_CODEがjaならば、日本語に翻訳されます。'first_name'という文字列部分は翻訳する際のキーのようなものです。
名無し 約14日前 2018年10月1日21:26 返信する
有難うございます。関数に「_」という名前をつけているというのは盲点でした。。。

追加で1点教えて頂けますでしょうか?
「Djangoで、会員登録機能を自作する」の完成版をGitHubからダウンロードして、Userにlocalという名前でユーザーの居住地を持たせたいのですが、どうにもうまくいきません。

行った作業としては、
①model.pyファイルに
class Local(models.Model):
name = models.CharField('お住いの地域', max_length=20)
created_at = models.DateTimeField('日付', default=timezone.now)

def __str__(self):
return self.name
を追加。
②model.pyファイルのUserに
local = models.ForeignKey(Local, verbose_name="お住いの地域", on_delete=models.PROTECT,)
を追加。
③admin.pyファイルの最後に
admin.site.register(Local)
を追加。

の3点です。
作業の順番も、最初にマイグレーションする、上記①から③を行うまではマイグレーションしない、など色んなパターンを試したのですが、
マイグレーションの時にデフォルト値の入力を求められ、値を入れるとmigrateの時に
ValueError: invalid literal for int() with base 10: 'XXXXXXX'
という値が出たり、
migrateまではうまく言ってもpython manage.py createsuperuserのところで
django.db.utils.IntegrityError: NOT NULL constraint failed: register_user.local_id
というエラーが出るなど、どんな順番でもエラーが出ます。
(model.pyへの追記前にpython manage.py createsuperuserをすればユーザーは作成できるのですが、その後のフィールド追加で「no such column」というエラーになります。)

作業すべき順番だけでも構いませんので、お手すきの際にご対応頂けますと幸いです。宜しくお願い致します。
なりと 約12日前 2018年10月3日9:57
>>python manage.py createsuperuserのところでdjango.db.utils.IntegrityError: NOT NULL constraint failed: register_user.local_id
というエラーが

惜しいところまでいっています。
createsuperuserコマンドでのUser作成時にlocalフィールドを空欄で登録しようとし、エラーになっています。create_superuserメソッドの一番最初あたりで、localフィールドに値を入れることもできますが、ちょっとややこしくなります。

後からモデルにフィールドを加える場合の最も簡単な方法は、そのフィールドを空欄でもよい状態で一度定義してしまうことです。

①の後に②を行いますが、その際に
local = models.ForeignKey(Local, verbose_name="お住いの地域", on_delete=models.PROTECT, blank=True, null=True)
と引数を追加してください。このblank=True null=Trueはフィールドに空欄を許可するための指定です。

その後はmakemigrations、migrateを行えるはずです。
空欄の許可が嫌になったら、すべてのデータのフィールドを埋めて、blank=True, null=Trueを消し、再度makemigrationsとmigrateを行います。

blank=True null=True のかわりにdefault=...という引数でも代用できるのですが、今回のようなForeignKeyで紐づくものをデフォルト値として指定しようとすると、これも慣れないとややこしいと思うので、空欄許可の方法がシンプルで良いと思います。
名無し 約12日前 2018年10月3日22:23 返信する
有難うございます!どうしようもなく行き詰まっていたので大変有難いです。
brank=Trueは入れてみたりしていたのですが、null=Trueも必要なんですね。
改めてトライしてみます!本当に有難うございました。