[PYTHON] A story about custom users having a sweet and painful look at Django

Introduction

I'd like to improve the user registration / authentication / login functions in Django, and I'm stuck when I'm proceeding, so I'll keep a record.

Conclusion

** When developing an app with Django, define and design the requirements before launching the project **

** Unless it's already woven into the design, it's basically better to protect 1 project and 1 app until you can develop Django. ** **

** Even when developing by studying, start the project again without getting tired and do not reuse it. ** **

What happened

The Django tutorial is over, so [this article](https://qiita.com/okoppe8/items/54eb105c9c94c0960f14#%E6%89%8B%E9%A0%86%EF%BC%96%E3%83% 95% E3% 82% A9% E3% 83% BC% E3% 83% A0% E4% BD% 9C% E6% 88% 90) and [this article](https://narito.ninja/blog/detail Based on / 38 /) I was trying to make something like a template of a TODO application with user registration, authentication, and login functions in Django, but I was diverting the tutorial project because it was troublesome. I'm stuck around the specifications of SuperUser.

Membership registration feature in Django

If you want to use the membership registration feature in Django, basically use the ʻUser` model provided by Django. Rather, as long as you introduce a framework, you will probably use the authentication mechanism provided by the framework, unless there is something wrong with any framework, and it is definitely built-in. Some have models and methods.

So, the problem is from here, but with Django, you can manage multiple applications in one Project folder, and conversely, you can install the same application in multiple project folders. However, in fact, only one ʻUser model can be used per project. Strictly speaking, Django strongly recommends creating a custom user model based on the User model for deployment, and it must be created by the first migration. At that time, in order to handle the User model with the authentication function, the part of ʻAUTH_USER_MODEL set in settings.py is set, but it is faster to restart the project to change this later. It takes time and effort. Reference

In other words, if you want to manage multiple apps with one project, you have to decide which application should have the function around authentication when you launch project.

I don't follow this ... In other words, even though I defined the User class in the tutorial, I wanted to create another application and add an authentication function there. It means that it will be difficult to make.

However, there is nothing I can do about it, so this time I tried to find out if the existing project could be used with the meaning of being for later study.

What i did

--Customize the existing ʻUser model ――Create a new model that extends the ʻUser model Create it in the application folder and relate it using ʻOneToOneField. --Edit ʻadmin.py on the side where the User model is located to reflect the custom User model and the model that extends the User model on the management screen.

Also, at this time, the model that uses the model on the TODO application side as a function and the above ʻUser` extension model (which also overrides the record for the mail authentication flag) are in separate files.

And, of course, messing around with models.py will have to redo make migrations, so let's do it carefully.

Customization of existing User model

2020-05-03_00h05_45.png

Excuse me for the image because drawing a directory diagram is a hassle. Under src is the development environment, and ʻapp, polls, and todounder the project folder define the User model in each application, and the application you want to create this time istodo`.

First of all, we will customize the ʻUser` model body. Reference: Customize User model with Django

app/models.py

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

class CustomUserManager(UserManager):

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

		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(emai, password, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):

	username = models.CharField(max_length=30, unique=True)
	email = models.EmailField(_('email address'), unique=True, validators=[EmailValidator('Invalid email address.')])
	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 this 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 = CustomUserManager()

	EMAIL_FIELD = 'email'
	USERNAME_FIELD = 'email'
	REQUIRED_FIELD = []

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

	def email_user(self, subject, message, from_email=None, **kwargs):
		send_mail(subject, message, from_email, [self.email], **kwargs)


class SampleDB(models.Model):
	class Meta:
		db_table = 'sample_table' # tablename
		verbose_name_plural = 'sample_table' # Admintablename
	sample1 = models.IntegerField('sample1', null=True, blank=True) #Store numbers
	sample2 = models.CharField('sample2', max_length=255, null=True, blank=True)


class CustomUserManager(UserManager):

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

		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(emai, password, **extra_fields)

This part overrides the methods used to create users and superusers. In other words, this is the part related to logging in to the Django management screen, so I copied and pasted the ʻUserManager part of django.contrib.auth.models `and corrected the necessary parts. .. The source is here.


class User(AbstractBaseUser, PermissionsMixin):

	#Basic items of the User model.
	username = models.CharField(max_length=30, unique=True)
	email = models.EmailField(_('email address'), unique=True, validators=[EmailValidator('Invalid email address.')])
	first_name = models.CharField(_('first name'), max_length=30, blank=True)
	last_name = models.CharField(_('last name'), max_length=150, blank=True)

	#admin A method to determine if a user has access to a site
	is_staff = models.BooleanField(
		_('staff status'),
		default=False,
		help_text=_(
			'Designates whether this user can log into this admin site.'),
	)

	#A method to determine if a user is active
	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 = CustomUserManager()

	#To put it plainly, the mail dress field, the field used as the user name, and the field that must be entered when creating a superuser are specified from the top.
	EMAIL_FIELD = 'email'
	USERNAME_FIELD = 'email'
	REQUIRED_FIELD = []

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

	#Methods for sending emails
	def email_user(self, subject, message, from_email=None, **kwargs):
		send_mail(subject, message, from_email, [self.email], **kwargs)

This also inherits ʻAbstractBaseUser from django.contrib.auth.models . Since PermissionsMixinhas inheritance in the original class, it is also inherited here. As a role, it seems to be a collection of methods related to authority. models.BooleanField sets the defaultdefault value forBooleanField in the model field and returns True, False. In the reference source, I want to log in with an email address instead of a user name, so I delete the ʻusername field and let the ʻemail field play that role. The setting is ʻUSERNAME_FIELD ='email'. This time, I left ʻusername` because this TODO app is general purpose and I want to refactor it based on this and add functions.

By the way


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

#Defined in case another application accesses the username attribute, returns the email address when accessed
@property
def username(self):
    return self.email



Items such as are added as needed. This time, the first_name and last_name fields are not used, so they are not implemented, but I think it would be convenient if you create a form that allows you to register your first and last name.

Create a new model that extends the User model Create it in the application folder and use ʻOneToOneField`

todo/models/account.py



from django.conf import settings
from django.db import models
from django.core import validators

class Activate(models.Model):
	user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
	key = models.CharField(max_length=255, blank=True, unique=True)
	expiration_date = models.DateTimeField(blank=True, null=True)

	def __str__(self):
		return self.key


	class Meta:
		verbose_name = 'Email authentication flag'
		verbose_name_plural = 'Email authentication flag'

This time, create a model that extends the ʻUser model on the TODO application side. This time, in addition to the information on the ʻUser model side, define the fields for email authentication. ʻOneToOneFieldis a setting for defining a one-to-one relationship with a model. Since there is only one authentication flag for the user, there is a one-to-one relationship. It seems that many-to-one etc. will be defined byForeign Key`.

After that, delete models.py to package the split model, create amodels folder, create __init__.py in it, and put the split model in the same folder. __init__.py defines to import the model to be packaged as follows:

todo/models/__init__.py



from .todo import Todo
from .account import Activate

This completes the model split.

In order to reflect the custom User model and the model that extends the User model on the management screen, modify ʻadmin.py` on the side where the User model is located.

After that, if you modify ʻadmin.py`, it will be a paragraph.

app/admin.py



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
#todo application account.Import Activate object from py
from todo.models.account import Activate
from.models import SampleDB

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


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

#The related mail authentication flag can be handled on the management screen of User model.
class ActivateInline(admin.StackedInline):
    model = Activate
    max_num = 1
    can_delete = False


class MyUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        (_('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 = ('username','email', 'first_name', 'last_name', 'is_staff')
    list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
    search_fields = ('username','email', 'first_name', 'last_name')
    ordering = ('email',)
    #Specify the ActivateInline class
    inlines = [ActivateInline]


admin.site.register(User, MyUserAdmin)
admin.site.register(SampleDB)


Now borrow from django.contrib.auth.admin.py and override this so you can see your custom user model in the admin screen. The liver is the part of from todo.models.account import Activate.

todo/admin.py



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

# Register your models here.

class TodoAdmin(admin.ModelAdmin):

	fields = ['todo_title', 'memo', 'datetime', 'tags']

	list_display = ('todo_title', 'datetime', 'created_at', 'tag_list')
	list_display_links = ('todo_title', 'datetime','tag_list')

	def get_queryset(self, request):
		return super().get_queryset(request).prefetch_related('tags')

	def tag_list(self, obj):
		return u", ".join(o.name for o in obj.tags.all())

	pass

admin.site.register(Todo,TodoAdmin)
admin.site.register(Activate)


In this way, the ʻActivate model can be managed on the todo application side as well, but it is more convenient to manage it with the ʻUser model because it is related to the relationship, so import the model with from todo.models.account import Activate. hand, It means creating a ʻInline class on the User class side. For the time being, as we proceed with the work, we will deal with each time an error occurs in ʻUSERNAME_FIELD ='email'.

reference

Customize User Model with Django Django, User Model Customization (OneToOne) Allow Django to sign up with custom user model (https://hodalog.com/how-to-create-user-sign-up-view/) Create a custom user with Django AbstractBaseUser (https://noumenon-th.net/programming/2019/12/13/abstractbaseuser/) Customize authentication method from official document From the official documentation django.contrib.auth

Recommended Posts

A story about custom users having a sweet and painful look at Django
A story about Python pop and append
A story about adopting Django instead of Rails at a young seed startup
A story about Go's global variables and scope
A story about implementing a login screen with django
A story about modifying Python and adding functions
A story about kindergartens, nursery schools, and children's gardens
Take a look at profiling and dumping with Dataflow
A quick look at your profile within the django app
About django custom filter arguments
A story about someone who wanted to import django from a python interactive shell and save things to a DB