취미삼아 배우는 프로그래밍

Django Custom User 일지 본문

파이썬(장고)

Django Custom User 일지

Nadure 2019. 12. 26. 14:16

이거.. 잊어먹기 쉬울것 같아서

미리 정리하려 한다.

 

Custom User를 사용하기 위해선

  1. 기존 유저모델을 수정
  2. 수정한 모델을 Auth 모델로 쓸 것임을 설정파일에 저장.

해야한다.

 

말로는 간단해보인다..

실제로 하는것도 간단해보인다.

그냥 코드를 복붙해서 하면 되기 때문,

https://dev-yakuza.github.io/ko/django/custom-user-model/

 

장고(django)의 커스텀 유저 모델(Custom User Model)

장고(django) 프로젝트에서 사용되는 유저 모델(User Model)을 입맛에 맞게(Customization) 수정하여 사용해 봅시다.

dev-yakuza.github.io

 

 

위의 사이트를 참고해서 수정해, 이리저리 테스트해봤다.

 

# app : account
# file : account.models 
from django.db import models
from django.contrib.auth.models import (BaseUserManager, AbstractBaseUser,
                                        PermissionsMixin)
# from django.contrib.auth.models import User

# Choice 테스트용
GRADE_IN_ROLE_CHOICES = [
    ('새싹', '새싹'),
    ('나무', '나무'),
    ('수풀림', '수풀림'),
]

class UserManager(BaseUserManager):
    use_in_migrations = True

    def create_user(self, email, date_of_birth, full_name, password=None):
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
            date_of_birth=date_of_birth,
            full_name=full_name,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, date_of_birth, full_name, password):
        user = self.create_user(
            email,
            password=password,
            date_of_birth=date_of_birth,
            full_name=full_name,
        )
        # user.is_superuser = True
        user.is_admin = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(
        verbose_name='email',
        max_length=255,
        unique=True,
    )
    date_of_birth = models.DateField()
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    # is_superuser = models.BooleanField(default=False)
    # is_staff = models.BooleanField(default=False)

    date_joined = models.DateTimeField(auto_now_add=True)
    full_name = models.CharField(max_length=20, null=False)

    grade_in_role = models.CharField(
        max_length=10,
        choices=GRADE_IN_ROLE_CHOICES,
        default='새싹',
    )

    objects = UserManager()

    # Username을 email로 명시
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['date_of_birth','full_name']

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return True

    @property
    def is_staff(self):
        return self.is_admin


## settings 파일 수정
INSTALLED_APPS = [
    ### ... 
    'account', # 추가
	## ...
]

# settings 파일 내용 추가
AUTH_USER_MODEL = 'account.User' 	# account 앱의 User를 모델로 쓸 것을 명시
LOGIN_REDIRECT_URL = '/'  			# 로그인 시 리디렉트 시켜줄 위치를 명시
LOGOUT_REDIRECT_URL = '/'			# 로그아웃 시 리디렉트 시켜줄 위치를 명시



 

자 이것만 해도

실제 데이터베이스 내에 account.models.User 라는 것을 Auth 모델로 쓰게 되고,

데이터도 저런 형식으로 받게 된다.

적용 방법은

  • python manage.py makemigrations
  • python manage.py migrate
  • # on Error >> python manage.py migrate --run-syncdb 후 다시 makemigrations, migrat

하면 된다.

createsuperuser시 저런식으로 정보도 입력받는다. 착한것 ㅎㅎ

 

 

자, 근데 여기서 문제가 있다.

기껏 테이블을 만들었는데, 쓰질 않아서 내용이 없다.

아니 쓰는 틀 조차도 없다.

카페에 갔는데, 우산 놓아두라고 놔둔 쓰레기통에다

아무 표기도 없고 아무도 우산을 안 꽂아서, 지나가는 사람들이 휴지나 버리고 있는 격이다.

 

그러니, 지금 필요한 것은 사용하라는 틀과, 관리자용 페이지에 해당 내용을 추가하는 것이다.

관리자 페이지에 추가를 안하면,,

만약 따로 view단에서 세이브를 시켜준다 한들,,

관리자는 아무 권한도 없는거다.(볼 수 있어야 고치지..)

 

틀 만들기.(forms.py 작성)

## account.forms
# ++ forms.py

from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField

from .models import User



# 사용자 생성 폼
class UserCreationForm(forms.ModelForm):
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(
        label='Password confirmation', widget=forms.PasswordInput)

    class Meta:
        model = User
        fields = ('email', 'date_of_birth', 'full_name')

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError("Passwords don't match")
        return password2

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


# 사용자의 자기 정보 변경 폼
class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField()

    class Meta:
        model = User
        fields = ('email', 'password', 'date_of_birth', 'full_name',
                  'is_active', 'is_admin')

    def clean_password(self):
        return self.initial["password"]


# 로그인 폼
class LoginForm(forms.ModelForm):
    password = forms.CharField(label='Password', widget=forms.PasswordInput)
    class Meta:
        model = User
        fields = ('email', 'password') # 로그인 시에는 유저이름과 비밀번호만 입력 받는다.

 

여기서 UserCreation은 admin 내에서 사용자를 생성할 때 쓰일 폼이고,

UserChangeForm은 admin 페이지 내에서 사용자 변경을 하거나,

사용자가 자기 정보를 변경할 때 사용하는 용도로 작성해줌

표로 정리하면,

클래스 이름 용도
UserCreation 사용자 생성(Admin, 가입 시)
UserChangeForm 사용자 정보 변경(Admin, 사용자 직접 변경)
Login 사용자 로그인 폼

 

 

그럼, form을 만들었으니,

admin 페이지에 적용시켜 보자.

# account.admin

from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

from .forms import UserChangeForm, UserCreationForm
# 작성한 모델을 사용하는것에 주의
from .models import User


class UserAdmin(BaseUserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm

    list_display = ('email', 'full_name', 'date_of_birth', 'is_admin')
    list_filter = ('is_admin',)

    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('date_of_birth','full_name',)}),
        ('Permissions', {'fields': ('is_admin','grade_in_role',)}),
    )

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'date_of_birth', 'password1', 'password2')}
         ),
    )
    search_fields = ('email','full_name')
    ordering = ('email',)
    filter_horizontal = ()


# User, UserAdmin 을 등록
admin.site.register(User, UserAdmin)
# Group 을 등록해제
admin.site.unregister(Group)

 

그러면

딱 원하는대로 잘 붙어줘 있다. 지금건 UserChangeForm임

 

User추가를 눌러주면
사용자 추가(UserCreationForm)도 잘 붙어줘 있다.

 

 

그러면.. Custom User의 진정한 목표

 

사용자의 프로필을 붙여보자.

나의 경우 여기서 시행착오가 상당히 많았다.

버그인지는 모르겠다.

왜냐하면

뭔 짓을 해도,

모델 부분 에 아무리 뭔가를 추가해도..

기존 작성했던 model 부분에 뭔가를 아무리 열심히 추가로 작성해도

도저히 makemigration, migrate, migrate --run-syncdb

가 먹히지 않는다.

account의 migrations 파일들도 생성되지 않는다.

 

하지만 여기서 굳이 생성을 하고자 한다면,

db.sqlite3를 삭제 해버리는 수가 있긴 했다.

근데 모델이 어멍 자주 바뀌는데 매번 삭제할 수가 없는 노릇,,

 

이때를 위해서 사용하는 방법이 있었다.

https://cjh5414.github.io/extending-user-model-using-one-to-one-link/

 

One-To-One Relationships(일대일 관계)를 이용한 Django User Model 확장하기

Jihun's Development Blog

cjh5414.github.io

 

그 방법은 User 모델을 onetoone 모델로 불러와서, 이에 추가하고 싶은 정보를 저장하는 방법이다.

# profile.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
from .models import User

class Profile(models.Model):
    user                = models.OneToOneField(User, on_delete=models.CASCADE)
    location            = models.CharField(max_length=30, blank=True)
    in_company_name     = models.CharField(max_length=30, 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()

 

그러면, 매 번 User를 호출해서 관련 정보를 저장할 때마다, Profile도 불러오고, id도 불러오고

저장해야 하느냐.. 그건 아니다.

그걸 해주는 것이 바로 데코레이터다.

User가 저장되기 전, 위와 같은 정보를 포함하여 저장하게 될 것이다.

 

내용이 추가됐으니, form도 추가돼야 할 것이다.

써야할 내용이 늘어났으니까!

 

수정하여 사용하거나, 추가해서 사용하는건 마음대로이나, 일단 코드부터 이해해보는게 중요한 듯 하다.

뷰로 한 번 봐보자.

### forms.py
## ....
#  ....

# 위에서 만든 UserCreationForm을 상속받고, 
# 필요한 내용을 추가한 form을 생성한다.

class ProfileForm_withCreation(UserCreationForm):
    location        = forms.CharField(max_length=30)
    in_company_name = forms.CharField(max_length=30)

    class Meta:
        model = User
        fields = ('email', 'date_of_birth', 'full_name', 'location',
         'in_company_name','password1','password2')
         

# views.py
from django.shortcuts import render, redirect, HttpResponse
from .forms import UserCreationForm, LoginForm, ProfileForm, ProfileForm_withCreation
from django.contrib.auth import authenticate, login, get_user_model
from account.models import User
from django.contrib import messages
# ...
# ...

def signup_combined(request):
    if request.method == "POST":
        form = ProfileForm_withCreation(request.POST)
        print(form.is_valid())
        print(form.errors)
        if form.is_valid():
            user = form.save(commit=False)
            user.save()

            return redirect('home')
    
    else:
        form = ProfileForm_withCreation()
        data = {
            'form':form,
        }
        return render(request, 'account/signup.html', data)
<!-- account/templates/account/signup.html -->
<h2>회원가입</h2>
<form method="post" action="">
    {% csrf_token %}
    {{ form.as_p }}
    {{ profile_form.as_p }}
    <input type="submit" value="회원가입" />
</form>

 

만약 이 위에서 만든,

ProfileForm_withCreation 을 admin 에서 쓰게 되면

똑같은 내용을 출력시킬 수 있다.

 

이렇게 구성하면서 생긴 이득,

migration 걱정을 안해도 됨(내 지식선상에서 기존 db를 날려야만 작업가능 했었음..)

추가적으로 필요한 부분 계속하여 추가 삭제 가능.

form을 작성해두었으니 재사용이 용이.

 

했더랬다.

 

정리끝.

 

가입폼 완성 예시

Profile form과

UserCreationForm 을 따로

뷰 단에서 불러들여 쓰게 된다면,

저장이야 잘 되겠지만, Form.is_vaild()가 안될 수도 있다.

(Form.is_valid()가 True 여야만 Form.cleaned_data 를 사용할 수 있다.)

내 경우 안됐었기 때문에, 다른 방법삼아 그냥 가입폼을 따로 만들어버렸다.

 

Comments