728x90

장고(django)가 기본 제공하는 유저 모델(User Model)이 아닌 커스텀 유저 모델(Custom User Model)을 활용 방법이다.

 

제대로 된 방법 찾으려고 엄청 삽질을 한 끝에 이해하고 테스트한 결과를 적어둔다.

구글링을 해도 제대로 이해가 잘 안되는 것은 설명이 부족해서 인지 이해가 부족해서인지 본인 스스로 판단해야 한다.

 

https://link2me.tistory.com/2109 에 게시한 내용은 장고 기본 User Model 과는 별도의 User Model를 생성해서 별개로 로그인하는 방법이다.

 

이번 주제는 장고 기본 User Model 대신에 원하는 칼럼을 마음껏 추가할 수 있는 Custom User Model 이다.

구글링을 하면 장고 기본 User Model을 약간 수정하는 정도를 다루는 방법이 소개되어 있기도 하다.

 

Custom User Model을 구현하기 위해서는 BaseUserManagerAbstractBaseUser 클래스를 상속받아 새롭게 구현해야 한다. 여기서 BaseUserManager는 유저를 생성하는 역할을 하는 헬퍼 클래스이고, AbstractBaseUser는 실제 모델이 상속받아 생성하는 클래스이다.

 

기본 설치사항

터미날에서 아래 사항을 실행한다.

python manage.py startapp account
python manage.py startapp blog
mkdir static
mkdir media
pip install django-bootstrap4
pip install pillow

 

settings.py 수정사항

import os
from pathlib import Path
 
ALLOWED_HOSTS = ['*']
 
if DEBUG:
    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # development only
 
AUTH_USER_MODEL = 'account.Account'
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.AllowAllUsersModelBackend',
    'account.backends.CaseInsensitiveModelBackend',
    )
 
# Application definition
 
INSTALLED_APPS = [
    # built in django app
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 3rd party app
    'bootstrap4',  # add, pip install django-bootstrap4
    # local app
    'account.apps.AccountConfig',  # add
    'blog.apps.BlogConfig'# add
 
]
 
DATABASES = {
    'default': {
        'ENGINE''django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        # 'ENGINE': 'django.db.backends.mysql', # mysqlclient librarly 설치
        # 'NAME': 'pythondb',
        # 'USER': 'root',
        # 'PASSWORD': 'autoset', # mariaDB 설치 시 입력한 root 비밀번호 입력
        # 'HOST': 'localhost',
        # 'PORT': ''
    }
}
 
 
LANGUAGE_CODE = 'en-us'
# 수정
TIME_ZONE = 'Asia/Seoul'
 
USE_I18N = True
# 수정
USE_TZ = False
 
 
STATIC_URL = 'static/'
MEDIA_URL = 'media/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
    os.path.join(BASE_DIR, 'media'),
]
 
BASE_URL = "http://127.0.0.1:8000"
 

 

models.py

테이블 구성요소를 작성한다.

 

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
 
class MyAccountManager(BaseUserManager):
    # 일반 user 생성, username 이 userID를 의미함
    def create_user(self, email, username, name, password=None):
        if not email:
            raise ValueError("Users must have an email address.")
        if not username:
            raise ValueError("Users must have an userID.")
        if not name:
            raise ValueError("Users must have an name.")
        user = self.model(
            email = self.normalize_email(email),
            username = username,
            name = name
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
 
    # 관리자 User 생성
    def create_superuser(self, email, username, name, password):
        user = self.create_user(
            email = self.normalize_email(email),
            username = username,
            name = name,
            password=password
        )
        user.is_admin = True
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user
 
class Account(AbstractBaseUser):
    email       = models.EmailField(verbose_name='email', max_length=60, unique=True)
    username    = models.CharField(max_length=30, unique=True)
    name        = models.CharField(max_length=40null=False, blank=False)
    create_at = models.DateTimeField(verbose_name='date joined', auto_now_add=True)
    last_login  = models.DateTimeField(verbose_name='last login', auto_now=True)
    is_admin    = models.BooleanField(default=False)
    is_active   = models.BooleanField(default=True)
    is_staff    = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
 
    object = MyAccountManager()  # 헬퍼 클래스 사용
 
    USERNAME_FIELD = 'email'  # 로그인 ID로 사용할 필드
    REQUIRED_FIELDS = ['username''name'# 필수 작성 필드
 
    def __str__(self):
        return self.username
 
    def has_perm(self, perm, obj=None):
        return self.is_admin
 
    def has_module_perms(self, app_lable):
        return True
 

 

여기까지 하고 나서 아래 두줄을 터미널 창에서 실행한다.

python manage.py makemigrations
python manage.py migrate 

 

admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from account.models import Account
 
class AccountAdmin(UserAdmin):
    # 관리자 화면에 보여질 칼럼 지정
    list_display = ('email','username','name','create_at','last_login','is_admin','is_staff')
    search_fields = ('email''username''name')
    readonly_fields = ('id''create_at''last_login')
 
    filter_horizontal = ()
    list_filter = ()
    fieldsets = ()
 
admin.site.register(Account, AccountAdmin)
 

 

Templates

Layout 구성을 위해서는 Bootstrap Template를 받아서 활용하는 것이 좋다.

https://github.com/mdbootstrap/Bootstrap-4-templates 에서 템플릿을 받아서 static 폴더에 저장한다.

https://startbootstrap.com/previews/sb-admin-2 에서 무료 템플릿을 받아서 static 폴더에 저장한다.

둘 중 선택하거나 그 외 다른 템플릿을 선택해도 된다.

 

 

base.html

{% load static %}
 
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}{% endblock %}</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.11.2/css/all.css">
    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <!-- Material Design Bootstrap -->
    <link rel="stylesheet" href="{% static 'css/mdb.min.css' %}">
    <!-- Your custom styles (optional) -->
    <link rel="stylesheet" href="{% static 'css/style.min.css' %}">
 
    <!-- SCRIPTS -->
    <script src="{% static 'js/jquery.min.js' %}"></script>
    <script src="{% static 'js/popper.min.js' %}"></script>
    <script src="{% static 'js/jquery.js' %}"></script>
    <script type="text/javascript" src="{% static 'js/bootstrap.min.js' %}"></script>
    <!-- MDB core JavaScript -->
    <script type="text/javascript" src="{% static 'js/mdb.min.js' %}"></script>
    <script type="text/javascript">
        // Animations initialization
        new WOW().init();
    </script>
 
    {% block head %}
    {% endblock %}
</head>
<body>
<header>
    {% include 'header.html' %}
</header>
<!--Main layout-->
<main class="mt-5 pt-5">
    <div class="container">
        {% block content %}
        {% endblock %}
    </div>
</main>
 
<!-- SCRIPTS -->
</body>
</html>

 

header.html

수정해서 사용할 영역이며, 완벽한 예시는 아니다.

 
{% load static %}
 
<!-- Navbar -->
<nav class="navbar fixed-top navbar-expand-lg navbar-light white scrolling-navbar">
    <div class="container">
 
        <!-- Brand -->
        <a class="navbar-brand waves-effect" href="#">
            <strong class="blue-text">Home</strong>
        </a>
 
        <!-- Collapse -->
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
 
        <!-- Links -->
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
 
            <!-- Left -->
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link waves-effect" href="#" target="_blank">About MDB</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link waves-effect" href="#" target="_blank">Free
                        tutorials</a>
                </li>
            </ul>
 
            <form class="search-bar justify-content-start" onsubmit="return executeQuery();">
                <input type="text" class="form-control" name="q" id="id_q_large" placeholder="Search...">
            </form>
 
            <!-- Right -->
            <ul class="navbar-nav nav-flex-icons">
                {% if request.user.is_authenticated %}
                    <li class="nav-item">
                        <a href="{% url 'logout' %}" class="nav-link waves-effect" title="로그아웃">
                            <i class="fas fa-sign-out-alt"></i>
                        </a>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a href="{% url 'login' %}" class="nav-link waves-effect" title="로그인">
                            <i class="fas fa-sign-in-alt"></i>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a href="{% url 'register' %}" class="nav-link waves-effect" title="회원가입">
                            <i class="far fa-registered"></i>
                        </a>
                    </li>
                {% endif %}
            </ul>
 
        </div>
 
    </div>
</nav>
<!-- Navbar -->
<script type="text/javascript">
    function executeQuery() {
        var query = ""
        query = document.getElementById('id_q_small').value;
        if (query == ""){
            query = document.getElementById('id_q_large').value;
        }
        {#window.location.replace("{% url 'search' %}?q=" + query)#}
        return false
    }
</script>
 

 

 

home.html

{% extends "base.html" %}
{% load static %}
{% block title %}Home{% endblock %}
 
{% block content %}
 
    <div>Welcome to python django world!</div>
    {% if request.user.is_authenticated %}
    <div>{{ user.name }}</div>
    <div>{{ user }}</div>
    {% endif %}
 
{% endblock %}

 

register.html

 
{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% block title %}회원 가입{% endblock %}
 
{% block content %}
    <div class="row justify-content-center">
        <form method="post" class="text-center border border-light p-5">
            {% csrf_token %}
            <class="h4 mb-4">회원 가입</p>
            <div class="form-group">
                <input type="email" name="email" id="inputEmail" class="form-control"
                       placeholder="Email address" required autofocus>
            </div>
            <div class="form-group">
                <input type="text" name="username" id="inputUsername" class="form-control"
                       placeholder="UserID(아이디)" required>
            </div>
            <div class="form-group">
                <input type="text" name="name" id="inputName" class="form-control"
                       placeholder="UserNM(성명)" required>
            </div>
            <div class="form-group">
                <input type="password" name="password1" id="inputPassword1" class="form-control"
                       placeholder="Password" required>
            </div>
            <div class="form-group">
                <input type="password" name="password2" id="inputPassword2" class="form-control"
                       placeholder="Confirm password" required>
            </div>
            {% for field in registration_form %}
                <p>
                    {% for error in field.errors %}
                        <p style="color: red">{{ error }}</p>
                    {% endfor %}
                </p>
            {% endfor %}
            {% if registration_form.non_field_errors %}
                <div style="color: red">
                    <p>{{ registration_form.non_field_errors }}</p>
                </div>
 
            {% endif %}
 
            <button class="btn btn-primary btn-block" type="submit">Register</button>
 
        </form>
 
    </div>
 
{% endblock content %}

 

login.html

{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% block title %}로그인{% endblock %}
 
{% block content %}
    <div class="row justify-content-center">
        <form method="post" class="text-center border border-light p-5">
            {% csrf_token %}
            <class="h4 mb-4 text-center">로그인</p>
            <div class="form-group">
                <input type="email" name="email" id="inputEmail" class="form-control mb-4" placeholder="Email address"
                       required="required">
            </div>
            <div class="form-group">
                <input type="password" name="password" id="inputPassword" class="form-control mb-4"
                       placeholder="비밀번호 입력하세요" required="required">
            </div>
 
            {% for field in login_form %}
                <p>
                    {% for error in field.errors %}
                        <p style="color: red">{{ error }}</p>
                    {% endfor %}
                </p>
            {% endfor %}
            {% if login_form.non_field_errors %}
                <div style="color: red">
                    <p>{{ login_form.non_field_errors }}</p>
                </div>
 
            {% endif %}
            {% buttons %}
                <button class="btn btn-info btn-block my-4" type="submit">로그인</button>
            {% endbuttons %}
        </form>
 
        <div class="d-flex flex-column my-4">
            {#    <a class="m-auto" href="{% url 'password_reset' %}">Reset password</a>#}
        </div>
    </div>
 
{% endblock content %}
 

 

 

views.py

 
# account/views.py
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.contrib.auth import login, authenticate, logout
from account.forms import RegistrationForm, AccountAuthForm
 
 
def register_view(request, *args, **kwargs):
    user = request.user
    if user.is_authenticated:
        return HttpResponse("You are already authenticated as " + str(user.email))
 
    context = {}
    if request.POST:
        form = RegistrationForm(request.POST)
        if form.is_valid():
            form.save()
            email = form.cleaned_data.get('email').lower()
            raw_password = form.cleaned_data.get('password1')
            account = authenticate(email=email, password=raw_password)
            login(request, account)
            # destination = kwargs.get("next")
            destination = get_redirect_if_exists(request)
            if destination: # if destination != None
                return redirect(destination)
            return redirect('home')
        else:
            context['registration_form'= form
    else:
        form = RegistrationForm()
        context['registration_form'= form
 
    return render(request, 'account/register.html', context)
 
 
def logout_view(request):
    logout(request)
    return redirect("home")
 
 
def login_view(request, *args, **kwargs):
    context = {}
 
    user = request.user
    if user.is_authenticated:
        return redirect("home")
 
    destination = get_redirect_if_exists(request)
    if request.POST:
        form = AccountAuthForm(request.POST)
        if form.is_valid():
            email = request.POST.get('email')
            password = request.POST.get('password')
            user = authenticate(email=email, password=password)
            if user:
                login(request, user)
                if destination:
                    return redirect(destination)
                return redirect("home")
    else:
        form = AccountAuthForm()
 
    context['login_form'= form
 
    return render(request, "account/login.html", context)
 
 
def get_redirect_if_exists(request):
    redirect = None
    if request.GET:
        if request.GET.get("next"):
            redirect = str(request.GET.get("next"))
    return redirect

 

forms.py

# account/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import authenticate
from account.models import Account
 
# 회원 가입 폼
class RegistrationForm(UserCreationForm):
    email = forms.EmailField(max_length=254, help_text='Required. Add a valid email address.')
 
    class Meta:
        model = Account
        fields = ('email''username''name''password1''password2', )
 
    def clean_email(self):
        email = self.cleaned_data['email'].lower()
        try:
            account = Account.objects.get(email=email)
        except Exception as e:
            return email
        raise forms.ValidationError(f"Email {email} is already in use.")
 
    def clean_username(self):
        username = self.cleaned_data['username']
        try:
            account = Account.objects.get(username=username)
        except Exception as e:
            return username
        raise forms.ValidationError(f"UserID {username} is already in use.")
 
 
 
# 로그인 인증 폼
class AccountAuthForm(forms.ModelForm):
    password = forms.CharField(label='Password', widget=forms.PasswordInput)
 
    class Meta:
        model = Account
        fields = ('email''password')
 
    def clean(self):
        if self.is_valid():
            email = self.cleaned_data['email']
            password = self.cleaned_data['password']
            if not authenticate(email=email, password=password):
                raise forms.ValidationError("Invalid login")
 

 

backends.py

# account/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
 
class CaseInsensitiveModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None**kwargs):
        UserModel = get_user_model()
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
            user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
 

 

urls.py

# project urls.py
from django.conf import settings
from django.contrib import admin
from django.conf.urls.static import static
from django.urls import path, include
from .views import home_screen_view
from account.views import register_view, login_view, logout_view
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', home_screen_view, name='home'),
    path('register/', register_view, name='register' ),
    path('login/', login_view, name='login'),
    path('logout/', logout_view, name='logout'),
]
 
if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 

 

현재까지 테스트한 전체 소스 코드는 https://github.com/jsk005/PythonDjango 에 올려두었다.

 

참고자료

https://codingwithmitch.com/courses/real-time-chat-messenger/

 

CodingWithMitch.com

If you restart this course, all your progress will be reset

codingwithmitch.com

 

728x90
블로그 이미지

Link2Me

,