[PYTHON] Enable E-mail login as a custom user that inherits AbstractBaseUser

Operating environment and history

--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

The theory that you don't need the production environment yet

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.

What I want to do this time

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.

procedure

  1. Create an app named "users"
  2. Create a User class that inherits AbstractBaseUser and a UserManager class that inherits BaseUserManager in users/models.py.
  3. Create an AdminUserAdmin class that inherits UserAdmin in users/admin.py
  4. Define "AUTH_USER_MODEL ='users.User'"
  5. Make sure that you can migrate and register a custom user from the management screen

1. Create a users app

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
]

2. Create User class and UserManager class

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.

  1. 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.

  2. 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.

3. Create AdminUserAdmin class

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',)

4. Define "AUTH_USER_MODEL ='users.User'"

The user model to be authenticated is defined in settings.py as being "users.User".

settings.py


AUTH_USER_MODEL = 'users.User'

5. Make sure that you can migrate and register a custom user from the management screen

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. スクリーンショット 2020-12-29 22.13.50.png

You can also register users as customized. スクリーンショット 2020-12-29 22.10.12.png

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 **.

The bug I suffered this time

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.

reference

--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

Recommended Posts

Enable E-mail login as a custom user that inherits AbstractBaseUser
Use Remotte as a user
Implement a Custom User Model in Django
[tee command] Command for rewriting files that require root privileges as a general user