--Host environment - macOS Catalina 10.15.7 - VirtualBox 6.1.14 --Guest environment - Ubuntu Server 20.04.1 LTS - Docker 19.03.13 - Docker Compose 1.27.3 --Package - Django 3.1.4 - uwsgi 2.0.18 - mysqlclient 1.4.6
Last time, I tried to create a production environment, but it failed for some reason like a fool, ** I didn't write a single line of code, so I don't think I need a production environment **. Since I noticed something, I would like to proceed with coding in Development environment built with Docker Compose two times before.
Start with the first migration in [https://qiita.com/hajime-f/items/c40571647135df327aa7] and see the Django Start screen with `` `$ docker-compose up```.
What I want to do this time is --Create a custom user class --Allows you to log in with your email address Two of.
Official site has an example of inheriting AbstractUser, but since I want to mess with the contents of User, I will inherit AbstractBaseUser this time.
users/models.py
. users/admin.py
Hit the following command to create the "users" app.
$ docker-compose -f docker-compose.dev.yml run python ./manage.py startapp users
In the future, I didn't want to type long commands every time I developed various apps, so I added it to `Makefile```. Read the previous article (https://qiita.com/hajime-f/items/4511f2aa036380249a3f#makefile-%E3%81%AE%E4%BD%9C%E6%88%90) for the full contents of
Makefile
``.
Makefile (excerpt)
app:
docker-compose -f docker-compose.dev.yml run python ./manage.py startapp $(APP_NAME)
You can now create a new application with $ make app APP_NAME = app
.
Don't forget to install the "users" app in settings.py
.
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#My applications
'users', #add to
]
As a result of trial and error while referring to the official AbstructUser, AbstructBaseUser, UserManager, and "Complete example", this is the final result. ..
users/models.py
import uuid
from django.db import models
from django.utils import timezone
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.validators import UnicodeUsernameValidator
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
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):
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):
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):
uuid = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
blank=True,
null=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."),
},
)
last_name = models.CharField(_('Surname'), max_length=150)
first_name = models.CharField(_('Name'), max_length=150)
last_name_kana = models.CharField(_('Last name (kana)'), max_length=150)
first_name_kana = models.CharField(_('First name (kana)'), max_length=150)
old_last_name = models.CharField(_('maiden name'), max_length=150, blank=True, null=True)
old_last_name_kana = models.CharField(_('Maiden name (Kana)'), max_length=150, blank=True, null=True)
email = models.EmailField(_('mail address'), unique=True)
sex = models.CharField(_('sex'), max_length=4, choices=(('male','male'), ('Female','Female')))
birthday = models.DateField(_('Birthday'), blank=True, null=True)
country = models.CharField(_('Country'), default='Japan', max_length=15, editable=False)
postal_code = models.CharField(_('Zip code (without hyphens)'), max_length=7, blank=True, null=True)
prefecture = models.CharField(_('Prefectures'), max_length=5, blank=True, null=True)
address = models.CharField(_('City address'), max_length=50, blank=True, null=True)
building = models.CharField(_('Building name'), max_length=30, blank=True, null=True)
tel = models.CharField(_('Phone number (no hyphen)'), max_length=11, blank=True, null=True)
url = models.URLField(_('URL'), max_length=300, blank=True, null=True)
photo = models.ImageField(_('Photo'), blank=True, null=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()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
db_table = 'User'
verbose_name = _('user')
verbose_name_plural = _('user')
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
full_name = '%s %s' % (self.last_name, self.first_name)
return full_name.strip()
def get_full_name_kana(self):
full_name_kana = '%s %s' % (self.last_name_kana, self.first_name_kana)
return full_name_kana.strip()
def get_short_name(self):
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
send_mail(subject, message, from_email, [self.email], **kwargs)
Originally, as described in this article It is necessary to have proper multilingual support, but each of them is based on the idea that you should think about such small difficulties when you need them. Japanese is written as it is in the field. ** It ’s good! !! ** **
Well, there are two points.
uuid is set to primary key This is because I'm thinking of making Django REST later. If REST is used, the user to be operated will be specified by the URL, so setting the id to the primary key is not good for security. So it's a best practice to make the uuid the primary key when designing RESTful.
Removing username from UserManager This is because I want to be able to log in with my email address. Even in the User model, "USERNAME_FIELD" is set to email and "REQUIRED_FIELDS" is set to empty.
As a result of various trials and errors, this is also as follows.
users/admin.py
from .models import User
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
@admin.register(User)
class AdminUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('last_name', 'first_name', 'last_name_kana', 'first_name_kana', 'old_last_name', 'old_last_name_kana', 'email', 'sex', 'birthday', 'postal_code', 'prefecture', 'address', 'building', 'tel', 'url', 'photo')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
list_display = ('get_full_name', 'get_full_name_kana', 'email', 'sex', 'birthday', 'postal_code', 'prefecture', 'address', 'building', 'tel', 'is_staff',)
search_fields = ('username', 'email',)
ordering = ('date_joined',)
filter_horizontal = ('groups', 'user_permissions',)
The user model to be authenticated is defined in settings.py
as being "users.User".
settings.py
AUTH_USER_MODEL = 'users.User'
You can migrate by hitting the following command.
$ docker-compose -f docker-compose.dev.yml run python ./manage.py migrate
This is also a long and cumbersome command, so create a `` `Makefile```.
Makefile (excerpt)
migrate:
docker-compose -f docker-compose.dev.yml run python ./manage.py migrate
You can now migrate with `` `$ make migrate```. It's easy.
If the migration is successful, you will be able to log in to the management screen with your email address.
You can also register users as customized.
By the way, my company address is as above, so please feel free to contact me if you want to work as an engineer at ** Our company **.
Migration admin.0001_initial is applied before its dependency account.0001_initial on database
** "What's this !!!!" **, a bug that Yusaku Matsuda in me shouted.
When I googled at the speed of light, many of the ancestors faced the same bug. Apparently, if you create a custom user, the migration will always fail. Is there a problem on Django's side?
Let's deal with this as taught by our predecessors.
settings.py
INSTALLED_APPS = [
# 'django.contrib.admin', #Comment out
...
]
urls.py
urlpatterns = [
# path('admin/', admin.site.urls), #Comment out
...
]
After commenting out these two places and migrating, I put it back and migrated again and it worked.
If you write it like this, it looks as if you crushed the bug in a blink of an eye, but in reality, it was a lot of trial and error along with the process of creating a User. Because, if you play around with it, you'll get various errors. ..
So, as usual, I created a Makefile
to simplify the command.
Makefile (excerpt)
all_clear:
docker-compose -f docker-compose.dev.yml down
docker volume rm app.db.volume
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
find . -path "*/migrations/*.pyc" -delete
Even in the development environment, it's a spell of destruction that you don't want to use if you can. If you cast this spell, all the contents of the DB running in the container will be blown away, and you will be returned to the initial migration and superuser creation.
Let's pray quietly so that we don't have to chant such a megante.
--Replace custom User model (https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#substituting-a-custom-user-model) -Django textbook << practice >> that can be used in the field -Django User Customization Method -#django | Demo of how to create a custom user with AbstractUser and migrate -Customize User Model with Django -Django always uses the CustomUser model! Story -Try logging in with your email address and password in Django -Django, multilingual --What to do if custom user migration fails in Django