• 본 글은 How to Extend Django User Model을 읽고 어떻게든 해보겠다는 의지를 가지고 테스트를 거치면서 나름대로 요약한 글 입니다. 제 글을 읽기보다는 본문을 먼저 참고하시길 권합니다. 원래 원전만큼 훌륭한 주석서는 없는 법이니까요.

Django(장고)의 사용자 모델(User Model)은 훌륭합니다. 그런 굳건한 믿음이 있습니다. 장고의 사용자 모델을 수정하는게 생각만큼 쉽지 않기 때문에 ‘사용자 모델은 훌륭’하다는 전제를 가지고 기획자를 피해가며 잘 살아가고 있기 때문은 아니고, 장고의 사용자 모델은 필요한 최소한의 기능을 기본적으로 제공해주기 때문입니다.

하지만 살다보면 어쩔 수 없이 사용자 모델을 수정해야 할 일이 있을 수도 있습니다. 그럼 사용자 모델을 어떻게 수정해야 할까요?

어떻게 확장할 것인가?

  • ‘Proxy Model’을 사용한 확장
    • 데이터베이스에 새로운 스키마, 테이블 등을 전혀 만들지 않고 기존 모델의 동작을 변경하는데 사용됩니다.
    • 데이터베이스에 추가 정보를 저장할 필요가 ‘전혀’ 없고, 기존의 모델에서 필요한 몇가지 기능만 추가할 때 사용하면 됩니다. 앞선 문장에서 알 수 있듯이 이걸 사용할 일이 있을까 싶지만 심심치 않게 사용하곤 합니다.
# 프로젝트 하나 만들고, 앱 추가해서 테스트 하시면 됩니다.
from django.contrib.auth.models import User

class Person(User):  
    objects = User.objects.all()

    class Meta:
        proxy = True
        ordering = ('-username',)

    def do_print(self):
        for username in self.objects:
            print("Hello! " + username.__str__())

# 테스트를 간단하게 해보자!
python manage.py shell  
> In [1]: from users.profile import Person
> In [2]: p = Person()
> In [3]: p.do_print()
Hello! admin  
Hello! sigmadream  
  • One-To-One(Profile)
    • 데이터베이스에 추가 정보를 담을 수 있는 테이블을 하나 생성하여 관리할 때 사용하는 방법입니다.
    • 기본 모델을 유지하고, 인증 정보 수정없이 추가 정보를 저장하려고 할 때 사용하면 좋은 방법입니다.
# models.py
from django.db import models  
from django.contrib.auth.models import User  
from django.db.models.signals import post_save  
from django.dispatch import receiver

class Profile(models.Model):  
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):  
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):  
    instance.profile.save()

# admin.py
from django.contrib import admin  
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin  
from django.contrib.auth.models import User  
from .models import Profile


class ProfileInline(admin.StackedInline):  
    model = Profile
    can_delete = False
    verbose_name_plural = 'profile'


class UserAdmin(BaseUserAdmin):  
    inlines = (ProfileInline, )


admin.site.unregister(User)  
admin.site.register(User, UserAdmin)

# 테스트 하는 방법
$ python manager.py makemigrations
$ python manager.py migrate
$ python manager.py runserver
  • AbstractBaseUser를 사용한 확장
    • 완전히 새로운 사용자 모델을 만듭니다. 당연히 세심한 주의를 필요로 합니다. settings.py도 수정할 만큼 프로젝트에 영향을 막대하게 주기 때문에 프로젝트 초기에 완료해야 합니다. 이런 짓(git?!)을 프로젝트 진행 중에 하려면 리뉴얼 할 수 있는 기간을 확보해야 합니다.
    • 이걸 사용해야 하는 이유는 인증 프로세스와 관련하여 특정 요구사항이 있는 경우에만 하시면 됩니다(기획자를 설득하세요).
# managers.py
from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):  
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        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_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', 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)

# models.py
from __future__ import unicode_literals

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 .managers import UserManager


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=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def get_full_name(self):
        '''
        Returns 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):
        '''
        Returns the short name for the user.
        '''
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        '''
        Sends an email to this User.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
  • AbstractUser를 사용한 확장
    • 완전히 새로운 사용자 모델을 만듭니다. 당연히 세심한 주의를 필요로 합니다. 프로젝트의 영향도 AbstracBaseUser를 사용한 확장 방식과 비슷합니다. 이런 짓은 프로젝트 초반에 해야하고, 지금 해야겠다면 리뉴얼 할 수 있는 기간을 확보하세요. 당연히 테스트 코드도 다시 작성해야 합니다(그러니 기획자를 설득합시다).
    • 이걸 사용해야 하는 이유는 인증 프로세스에 특별한 요구사항이 없는데(!), 추가 클래스를 만들 필요없이 사용자 모델에 직접 추가 정보를 추가하려고 할 때 사용하면 됩니다(One-To-One을 사용해서 확장하는 방법을 왜 사용하지 않는지 궁금하지만 여튼 그럴때 사용합니다).
# models
from django.db import models  
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):  
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver