장고 시작하기 15. 장고 회원가입 기능 구현

장고에서는 회원가입 기능도 굉장히 간단하게 구현할 수 있게 준비되어 있습니다. 장고 회원가입 기능 구현을 간단히 정리했습니다.

추후 다시 정리할 예정 입니다.

장고 회원가입 기능 구현

  • Django는 기본 auth.User를 가지고 있기에 이를 기반으로한 기본적인 회원가입 ModelForm을 제공하고 있습니다.
  • UserCreationForm

username과 password 로 새로운 user를 생성하는 ModelForm

accounts/urls.py

from django.urls import path
from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
    path("logout/", views.logout, name="logout"),
    path("signup/", views.signup, name='signup')
]

accounts/signup.html

  • forms.as_p로 회원가입과 관련된 form을 구현 합니다.
{% extends "base.html" %}

{% block content %}
    <h1>회원가입</h1>
    <form action="{% url "accounts:signup" %}" method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">회원가입</button>
    </form>
{% endblock %}

accounts/views.py

  • signup view 추가
  • from django.contrib.auth.forms import UserCreationForm
  • UserCreationForm을 사용해서 form을 template에 전달함
  • UserCreationForm 을 사용하면 자동으로 input 내용 가져옴
# accounts/views.py

from django.shortcuts import render, redirect
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.forms import AuthenticationForm
from django.views.decorators.http import require_POST, require_GET, require_http_methods
from django.contrib.auth.forms import UserCreationForm


@require_http_methods(['GET', 'POST'])
def login(request):
    if request.method == "POST":
        form = AuthenticationForm(data=request.POST)
        if form.is_valid():
            auth_login(request, form.get_user())
            next_url = request.GET.get("next") or "articles:index"
            return redirect(next_url)
    else:
        form = AuthenticationForm()
    context = {"form": form}
    return render(request, "accounts/login.html", context)


@require_POST
def logout(request):
    auth_logout(request)
    return redirect("articles:index")

@require_http_methods(['GET', 'POST'])
def signup(request):
    if request.method == "POST":
        form = UserCreationForm(request.POST)  # 데이터가 채워진 회원가입 form (binding form)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)  # 회원가입 후, 로그인까지 바로 진행
            return redirect("articles:index")
    else:
        form = UserCreationForm()  # 회원가입 form
    context = {"form": form}
    return render(request, "accounts/signup.html", context)

유저 삭제 기능 구현

accounts/urls.py

from django.urls import path
from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
    path("logout/", views.logout, name="logout"),
    path("signup/", views.signup, name='signup'),
    path("delete/", views.delete, name="delete")
]

accounts/views.py
request.user.delete() : 장고가 알아서 세션 ID를 확인해서 user를 delete 해준다.
auth_logout(request) # 회원 삭제는 되지만 session_id가 DB가 남아있음, 이것을 지워줘야 함

note : request.user.delete() 와 auth_logout(request) 순서 바뀌면 안됨

@require_POST
def delete(request):
    if request.user.is_authenticated:
        request.user.delete()  # 회원 삭제
        auth_logout(request)  # 회원 삭제는 되지만 session_id가 DB가 남아있음, 이것을 지워줘야 함
    return redirect("articles:index")

base.html

<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <div class="navbar">
        {% if request.user.is_authenticated %}
            <h3>안녕하세요, {{ user }}님</h3>
            <form action="{% url 'accounts:logout' %}" method="POST">
                {% csrf_token %}
                <input type="submit" value="로그아웃">
            </form>

            <form action="{% url 'accounts:delete' %}" method="POST">
                {% csrf_token %}
                <input type="submit" value="회원탈퇴">
            </form>
        {% else %}
            <a href="{% url 'accounts:login'%}">로그인</a>
            <a href="{% url 'accounts:signup'%}">회원가입</a>
        {% endif %}
    </div>

    <div class="container">
        {% block content %}
        {% endblock content %}
    </div>

</body>
</html>

회원 정보 수정

UserChangeForm 사용

accounts/urls.py

from django.urls import path
from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
    path("logout/", views.logout, name="logout"),
    path("signup/", views.signup, name='signup'),
    path("delete/", views.delete, name="delete"),
    path("update/", views.update, name='update'),
]

accounts/views.py
UserChangeForm을 쓸 수 있도록 import 해줍니다.
모델에서는 데이터를 instance에 request.user로 받아옵니다.
form을 통해 입력 받은 데이터는 request.POST로 받아옵니다.
form에서 데이터를 받아오는 독특한 형태에 유의해줘야 합니다.

from django.contrib.auth.forms import (
    AuthenticationForm,
    UserChangeForm,
    )

@login_required
@require_http_methods(['GET', 'POST'])
def update(request):
    if request.method == 'POST':
        # 모델에서 form을 받는 방식은 다르다
        form = UserChangeForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return redirect("articles:index")
    elif request.method == 'GET':
        form = UserChangeForm(instance=request.user)
        context = {"form":form}
        return render(request, "accounts/update.html", context)

accounts/template/accounts/update.html
UserChangeForm에 해당하는 form을 받습니다.

{% extends "base.html" %}

{% block content %}
<h1>회원 정보 수정</h1>
<form action="{% url "accounts:update" %}" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">수정</button>
</form>
{% endblock content %}

위의 코드들로 회원수정을 들어가면 아래와 같은 화면이 등장합니다.

회원 정보 수정 2

위의 회원 정보 수정 템플릿은 우리가 원하지 않는 너무 여러가지 회원정보가 들어가있습니다.

우리가 원하는 정보만 수정할 수 있도록 form을 수정하고 싶습니다.

이를 위해서 UserChangeForm를 상속 받아서 원하는 부분만 가져올 수 있도록 오버라이딩 할 수 있습니다.

forms.py를 생성해주고 UserChangeForm를 상속 받은 클래스를 생성해보겠습니다.

UserChangeForm은 아래와 같은 코드를 가지고 있습니다.

class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField(
        label=_("Password"),
        help_text=_(
            "Raw passwords are not stored, so there is no way to see this "
            "user’s password, but you can change the password using "
            '<a href="{}">this form</a>.'
        ),
    )

    class Meta:
        model = User
        fields = "__all__"
        field_classes = {"username": UsernameField}
...
...

accounts/forms.py

UserChangeForm을 상속 받아서 CustomUserChangeForm을 생성합니다.

저희는 Meta 클래스 내용만 오버라이딩 해줍니다.

from django.contrib.auth.models import User로 import해서 써도 되지만 해당 User는 app별로 다를 수 있습니다. 때문에 다른 방식을 권장하는데요.

장고에서는 현재 활성화 되어 있는 모델을 가져오는 get_user_model() 이라는 함수를 사용하기를 권장합니다.

field 내용을 수정하기 원하는 필드만으로 변경해줍니다.

from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth import get_user_model


class CustomUserChangeForm(UserChangeForm):
    class Meta:

        model = get_user_model()
        fields = ["first_name",
                  "last_name",
                  "email",

accounts/views.py

view 함수에서 form 부분을 수정해줍니다.

@login_required
@require_http_methods(['GET', 'POST'])
def update(request):
    if request.method == 'POST':
        # 모델에서 form을 받는 방식은 다르다
        form = CustomUserChangeForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return redirect("articles:index")
    elif request.method == 'GET':
        form = CustomUserChangeForm(instance=request.user)
        context = {"form": form}
        return render(request, "accounts/update.html", context)

브라우저
회원 정보 칸이 수정된 것을 확인 할 수 있습니다.

비밀번호 수정

accounts/urls.py

from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
    path("logout/", views.logout, name="logout"),
    path("signup/", views.signup, name='signup'),
    path("delete/", views.delete, name="delete"),
    path("update/", views.update, name='update'),
    path("password/", views.change_password, name='change_password'),
]

accounts/views.py

from django.contrib.auth.forms import (
    AuthenticationForm,
    PasswordChangeForm,
)

def change_password(request):
    if request.method == "POST":
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            form.save()
            return redirect("articles:index")
    elif request.method == "GET":
        form = PasswordChangeForm(request.user)
        context = {"form": form}
        return render(request, "accounts/change_password.html", context)

accounts/templates/accounts/change_password.html

{% extends "base.html" %}

{% block content %}
<h1>비밀번호 변경</h1>
<form action="{% url "accounts:change_password" %}" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">비밀번호 변경</button>
</form>
{% endblock content %}

브라우저

위의 코드는 문제가 있는데요. 비밀번호 변경까지는 성공적으로 되는데, 비밀번호 변경 후, 로그아웃이 된다는 점 입니다.

이는 세션ID에 해당하는 값이 변경이 되어서 이로 인한 불일치로 로그인이 유지가 되지 않는 겁니다.

이에 대한 해답은 공식문서에 있습니다.
https://docs.djangoproject.com/en/4.2/topics/auth/default/#session-invalidation-on-password-change

이를 고치기 위해서는 view 부분을 아래와 같이 update_session_auth_hash(request, form.user) 메소드를 사용합니다.

@login_required
@require_http_methods(["GET", "POST"])
def change_password(request):
    if request.method == "POST":
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            form.save()
            # 비밀번호 변경 후 세션 위치를 위한 메소드
            update_session_auth_hash(request, form.user)
            return redirect("accounts:index")
    else:
        form = PasswordChangeForm(request.user)
    context = {"form": form}
    return render(request, "accounts/change_password.html", context)

회원 정보 수정 페이지 변경

현재 회원 정보 수정 페이지에서는 비밀번호 수정이 안되고 대신에 비밀번호를 수정할 수 있는 form을 링크로 연결해 줍니다.

하지만 실제 링크는 작동하지 않는데요. 이를 정상 작동 시키기 위해서 정상적인 URL로 변경 해줍니다.

이를 위해서는 상속 받은 CustomUserChangeForm에서 추가적으로 오버라이드를 해주면 됩니다.

reverse는 URL name으로 부터 URL을 찾아서 알아서 넣어주는 매소드 입니다.

accounts.forms.py

from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth import get_user_model
from django.urls import reverse

class CustomUserChangeForm(UserChangeForm):
    # from django.contrib.auth.models import User로 import해서 써도 되지만
    # 해당 User는 app별로 다를 수 있다. 때문에 다른 방식을 권장합니다.
    # 때문에 현재 활성화 되어 있는 모델을 가져오는 get_user_model() 이라는 함수를 사용하기를 권장합니다.

    class Meta:
        model = get_user_model()
        fields = (
            "username",
            "email",
            "first_name",
            "last_name",
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.fields.get("password"):
            password_help_text = (
                "You can change the password " '<a href="{}">here</a>.'
            ).format(f"{reverse('accounts:change_password')}")
            self.fields["password"].help_text = password_help_text

Leave a Comment

목차