# 장고 유저 모델 확장하기

# Situation

장고로 개인 프로젝트를 개발하는 중이다. 장고 유저 모델을 사용해 회원관리를 할 예정인데, 기본 User는 username을 Id로 사용한다. 나는 email을 ID로 사용하고 싶고, 회원마다 아바타도 추가하고 싶다.

# Solution

# accounts/models.py

# coding: utf-8

from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser,
    PermissionsMixin)


class MyUserManager(BaseUserManager):
    def create_user(self, email, nickname, password=None):
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=MyUserManager.normalize_email(email),
            nickname=nickname,
        )

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

    def create_superuser(self, email, nickname, password):
        u = self.create_user(email=email,
                             nickname=nickname,
                             password=password,
                             )
        u.is_admin = True
        u.save(using=self._db)
        return u


class MyUser(AbstractBaseUser,  PermissionsMixin):
    email = models.EmailField(
        verbose_name='email',
        max_length=255,
        unique=True,
    )
    nickname = models.CharField(
        u'닉네임', 
        max_length=10, 
        blank=False, 
        unique=True, 
        default='')
    avatar = models.ImageField(
        null=True,
        blank=True,
        upload_to='image/avatar/',
    )

    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = MyUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['nickname']

    def get_full_name(self):
        # The user is identified by their email address
        return self.email

    def get_short_name(self):
        # The user is identified by their email address
        return self.email

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

django 공식 사이트 (opens new window) auth부분에서 최하단 full-example을 참고해 모델을 짠다. AbstractBaseUser를 상속받아 새로운 유저 모델을 만들고, USERNAME_FIELD = 'email'로 이메일을 ID로 사용한다 명시해준다.

# settings.py

AUTH_USER_MODEL = 'accounts.MyUser'

settings.py에 유저 모델을 방금 만든걸로 사용한다 명시한다.

# 다른앱/models.py

from django.conf import settings


class Meetup(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL)
    title = models.CharField(max_length=200)

다른 앱에서는 그 세팅값으로 호출하면 된다.

# accounts/forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.core.files.images import get_image_dimensions

from .models import MyUser


class SignupForm(UserCreationForm):
    email = forms.EmailField(required=True, widget=forms.EmailInput(
        attrs={
            'class': 'form-control',
            'placeholder': 'Email',
            'required': 'True',
        }
    ))
    nickname = forms.RegexField(label="Nickname", max_length=30,
                                regex=r'^[\w.@+-]+$',
                                help_text="Required. 30 characters or fewer. Letters, digits and "
                                          "@/./+/-/_ only.",
                                error_messages={
                                    'invalid': "This value may contain only letters, numbers and "
                                               "@/./+/-/_ characters."},
                                widget=forms.TextInput(attrs={
                                    'class': 'form-control',
                                    'placeholder': 'Nickname',
                                    'required': 'true',
                                }))
    password1 = forms.CharField(
        label='Password',
        widget=forms.PasswordInput(
            attrs={
                'class': 'form-control',
                'placeholder': 'Password',
                'required': 'True',
            }
        )
    )
    password2 = forms.CharField(
        label="Password confirmation",
        widget=forms.PasswordInput(
            attrs={
                'class': 'form-control',
                'placeholder': 'Password confirmation',
                'required': 'True',
            }
        ),
        help_text="Enter the same password as above, for verification."
    )

    class Meta: # SignupForm에 대한 기술서
        model = MyUser
        fields = ("email", "nickname", "avatar", "password1", "password2",) # 작성한 필드만큼 화면에 보여짐

    def clean_avatar(self):
        avatar = self.cleaned_data['avatar']

        try:
            w, h = get_image_dimensions(avatar)

            #validate dimensions
            max_width = max_height = 500
            if w > max_width or h > max_height:
                raise forms.ValidationError(
                    u'Please use an image that is '
                    '%s x %s pixels or smaller.' % (max_width, max_height))

            #validate content type
            main, sub = avatar.content_type.split('/')
            if not (main == 'image' and sub in ['jpeg', 'pjpeg', 'gif', 'png']):
                raise forms.ValidationError(u'Please use a JPEG, '
                                            'GIF or PNG image.')

            #validate file size
            if len(avatar) > (20 * 1024):
                raise forms.ValidationError(
                    u'Avatar file size may not exceed 20k.')

        except AttributeError:
            """
            Handles case when we are updating the user profile
            and do not supply a new avatar
            """
            pass

        return avatar


class LoginForm(AuthenticationForm):
    email = forms.CharField(
        max_length=30,
        widget=forms.TextInput(
            attrs={
                'class': 'form-control',
                'placeholder': 'nickname',
                'required': 'True',
            }
        )
    )
    password = forms.CharField(
        widget=forms.PasswordInput(
            attrs={
                'class': 'form-control',
                'placeholder': 'Password',
                'required': 'True',
            }
        )
    )

필요하다면 form들을 명시해준다. 여기서 avatar = forms.ImageField()처럼 avatar는 따로 명시하지 않아도 된다. 이따가 clean_avatar로 추가할것이다. (얘때매 한참 삽질.)

아래 두 개의 링크를 참고해 코딩했다. http://stackoverflow.com/questions/6396442/add-image-avatar-to-users-in-django https://coderwall.com/p/bz0sng/simple-django-image-upload-to-model-imagefield

# signup.html

<form id="signup" class="form-horizontal" method="post" action="{% url 'signup' %}" enctype="multipart/form-data">
    {% csrf_token %}

    <!-- Username input-->
    <div class="row control-group">
        <div class="form-group col-xs-12 floating-label-form-group controls">
            {{ signupform.username.label_tag }}
            {{ signupform.username }}
            <span class="field-error">
            {{ signupform.username.errors|striptags }}
            </span>
        </div>
    </div>

    <!-- Email input-->
    <div class="row control-group">
        <div class="form-group col-xs-12 floating-label-form-group controls">
            {{ signupform.email.label_tag }}
            {{ signupform.email }}
            <span class="field-error">
            {{ signupform.email.errors|striptags }}
            </span>
        </div>
    </div>

    {{ signupform.nickname.label_tag }}
    {{ signupform.nickname }}
    {{ signupform.nickname.errors|striptags }}

    {{ signupform.avatar.label_tag }}
    {{ signupform.avatar }}
    {{ signupform.avatar.errors|striptags }}

    <!-- Password1 input-->
    <div class="row control-group">
        <div class="form-group col-xs-12 floating-label-form-group controls">
            {{ signupform.password1.label_tag }}
            {{ signupform.password1 }}
            <span class="field-error">
            {{ signupform.password1.errors|striptags }}
            </span>
        </div>
    </div>

    <!-- Password2 input-->
    <div class="row control-group">
        <div class="form-group col-xs-12 floating-label-form-group controls">
            {{ signupform.password2.label_tag }}
            {{ signupform.password2 }}
            <span class="field-error">
            {{ signupform.password2.errors|striptags }}
            </span>
        </div>
    </div>

    <br>

    <!-- Button -->
    <div class="row">
        <div class="form-group col-xs-12">
            <button id="signup" name="signup" class="btn btn-block btn-default" type="submit">Sign Up</button>
            <hr>
            <center>
            Or <a href="{% url 'login' %}"><u>log in</u></a>
            if you have an account.
            </center>
        </div>
    </div>
</form>

enctype="multipart/form-data" 적어주고, 아까 작성한 모델대로 폼을 넣어준다.

# accounts/views.py

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse

from .forms import SignupForm


def signup(request):
    signupform = SignupForm()
    if request.method == "POST":
        signupform = SignupForm(request.POST, request.FILES)
        if signupform.is_valid():
            user = signupform.save(commit=False)
            user.email = signupform.cleaned_data['email']
            user.avatar = signupform.clean_avatar()
            user.save()

            return HttpResponseRedirect(
                reverse("signup_ok")
            )

    return render(request, "signup.html", {
        "signupform": signupform,
    })

user save하기 전에 email이랑 avatar는 clean한 데이터로 넣어주고 save한다. 파일 업로드를 위해 SignupForm(request.POST, request.FILES)에 request.FILES도 추가해줬다.

# Conclusion

처음에 이유를 모르게 계속 안 되어서 스트레스 받았던 부분이다. 하고 나니까 되게 속시원하네!!