1. Django Tutorial(Airbnb) - 회원가입(Form & ModelForm)

ID짱재·2021년 8월 12일
1

Django

목록 보기
20/43
post-thumbnail

🌈 회원가입(Form & ModelForm)

🔥 Form 상속받아 회원가입 구현

🔥 ModelForm 상속받아 회원가입 구현

🔥 Email 인증


3. Form 상속받아 회원가입 구현

1) Url, View, Template 초기 세팅

  • 회원가입이 요청되면, SignUpView(CBV)가 호출가 호출될 수 있도록 url을 매핑하였어요.
# users/urls.py
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
    path("login/", views.LoginView.as_view(), name="login"),
    path("logout/", views.log_out, name="logout"),
    path("signup/", views.SignUpView.as_view(), name="signup"), # 👈 추가
]
  • 회원가입 Btn은 로그인된 상태에서는 보일 필요가 없어요. Login버튼 바로 아래에 함께 나타날 수 있도록 위치시켰어요.
<a href="{% url "core:home" %}">Nbnb</a>
    <ul>
    {% if user.is_authenticated %}
        <li><a href="{% url "users:logout" %}">Log out</a></li>
    {% else %}
        <li><a href="{% url "users:login" %}">Log in</a></li>
        <li><a href="{% url "users:signup" %}">Sign up</a></li> # 👈 회원가입 Btn 
    {% endif %}    
    </ul>
  • 회원가입 요청에 따른 View는 FormView를 상속받아 처리해줄께요. FormView의 위치입니다.
    • 🔎 from django.views.generic import FormView
  • FormView의 initial 속성은 form에 기본값을 입력된 상태로 유지해 해줍니다. form에 데이터를 입력하지 않아도 값이 입력되 있어 Test하기 편리합니다. 단, initial 속성은 password 까지 채워지지 않으니, 비밀번호만 매번 입력해줄께요:)
# users/views.py
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms
...
...
class SignUpView(FormView): # 👈 FormView 상속
    template_name = "users/signup.html" # 👈 render할 Template을 지정해줘요:)
    form_class = forms.SignUpForm # 👈 사용될 Form을 지정해줘요:)
    success_url = reverse_lazy("core:home") # 👈 validate가되면 이동합니다.
    initial = {
        "first_name": "Haezin",
        "last_name": "Super",
        "email": "haezin@test.com",
    } # 👈 password를 설정하지 않은 이유는 안되서 제외시켰어요:)
  • View에서 연결시킨 SignupForm은 Form을 상속받은 기본적인 형태의 Form입니다.
  • 회원가입의 경우 비밀번호를 재확인해야하기 때문에 2개의 password 필드를 만들었어요,, label 속성을 통해 템플릿에 표시될 field명을 지정해줄 수 있어요. label 속성을 사용하지 않는다면 "password", "password1"이라는 이름으로 템플릿에 나타나납니다.
  • widget에 PasswordInput은 입력란에 비밀번호가 노출되지 않도록 form의 형태를 변경해줍니다.
    • 🔎 비밀번호 입력 필드 widget 속성 : "widget=forms.PasswordInput"
# users/forms.py
from django import forms
from . import models
...
class SignUpForm(forms.Form):
    first_name = forms.CharField(max_length=80)
    last_name = forms.CharField(max_length=80)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput) # 👈 Password로 템플릿에 필드가 표시됩니다.
    password1 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password") # 👈 label값으로 템플릿에 필드가 표시됩니다.
  • 비밀번호는 중요한 데이터이기 때문에 POST 방식으로 전송하고, 보안을 위해 {% csrf_token %} form 안에 넣습니다.
  • form을 출력하기 위해 {{form.as_p}}를 form 안에 위치시키면 아래처럼 나타납니다.
# users/signup.html
{% extends "base.html" %}
    {% block page_title %}
        Sign Up
    {% endblock page_title %}
    {% block search-bar %}
    {% endblock search-bar %}
    {% block content %}
        <form method="post" action="{% url "users:signup" %}">
            {% csrf_token %}
            {{form.as_p}}
            <button>Sign Up</button>
        </form>
{% endblock content %}

2) create_user()

  • 생성한 form이 화면에 잘 출련된다면,, 이제 사용자가 입력한 값을 받고 DB에 저장하는 로직이 필요합니다. 이 과정에서 사용자가 입력한 값을 어떻게 처리할지에 대한 유효성 검증이 로직의 핵심입니다.
  • "first_name"과 "last_name" field는 입력만된다면 크게 검증이 필요없지만,, "email"은 검증를 진행해줘야해요:) 이미 해당 email이 DB에 존재하면 안되기 때문이죠. 또한 두개의 "password"가 서로 일치한지 유효성 검사를 해줍니다.
  • "def clean_password(self):"이 아닌, "def clean_password1(self):"로 매서드를 만든 이유는 clean 뒤에 들어간 field값까지 유효성 검사를 진행하기 때문입니다. 이에 "clean_password"로 매서드를 만들면 password2값은 clean 매서드 내로 가져올 수 없어요. 위에서 부터 순차적으로 체크하기 때문이죠.
  • "models.User.objects.create"를 쓰지않고, "models.User.objects.create_user"를 이용한 것은 비밀번호를 암호화해서 저장해주기 떄문이에요. "create_user"는 id, email, password를 순서대로 전달해줘야 합니다.
  • 이에 "first_name"과 "last_name"은 생성된 user 객체에 update한 뒤 저장하였어요.
# users/forms.py
from django import forms
from . import models
...
...
class SignUpForm(forms.Form):
    first_name = forms.CharField(max_length=80)
    last_name = forms.CharField(max_length=80)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    password1 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password")
    # email이 이미 등록되었는지에 대한 validation
    def clean_email(self):
        email = self.cleaned_data.get("email") # 👈 필드의 입력값 가져오기
        try:
            models.User.objects.get(email=email) # 👈 필드의 email값이 DB에 존재하는지 확인
            raise forms.ValidationError("User already exists with that email")
        except models.User.DoesNotExist:
            return email  # 👈 존재하지 않는다면, 데이터를 반환시킵니다.
    # 두개의 password가 일치한지에 대한 validation
    def clean_password1(self):
        password = self.cleaned_data.get("password") # 👈 필드의 입력값 가져오기
        password1 = self.cleaned_data.get("password1") # 👈 필드의 입력값 가져오기
        if password != password1:
            raise forms.ValidationError("Password confirmation does not match")
        else:
            return password
    # save 매서드로 DB에 저장
    def save(self):
        first_name = self.cleaned_data.get("first_name")
        last_name = self.cleaned_data.get("last_name")
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        # create_user()에 id(email), email(email), password(password) 값을 순서대로 넣어줘요!
        user = models.User.objects.create_user(email, email, password)
        user.first_name = first_name
        user.last_name = last_name
        user.save()            

3) Create User & Login immediately

  • 유효성 검사에 문제가 없다면 View에서 저장 후, 바로 로그인된 상태로 hom으로 이동할 수 있게 처리해줄께요:)
  • "form_valid" 매서드로 유효성 검사를 진행해서 그 결과가 True라면,, form의 save 매서드를 실행시켜 Object를 생성 후 저장합니다.
  • login된 상태로 home으로 이동하는 것은 인증에서 사용한것과 같이 "authenticate"과 "login"을 사용하면 됩니다!
# from django.views import View
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms
...
...
class SignUpView(FormView):
    template_name = "users/signup.html"
    form_class = forms.SignUpForm
    success_url = reverse_lazy("core:home")
    def form_valid(self, form): # 👈 form을 전달받아옵니다.
        form.save() # 👈 form의 save() 매서드 실행
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user is not None:
            login(self.request, user)
        return super().form_valid(form)

4.ModelForm 상속받아 회원가입 구현

1) class Meta

  • 지금까지 Form을 상속받아 forms.py를 작성했는데요, Django에는 ModelForm을 사용해볼께요. ModelForm은 DB의 field를 form에서 활용할 수 있기 때문에 field의 속성을 지정할 필요 없이 field의 값을 마치 list_display 처럼 등록해주기만 해도 사용 가능합니다.
  • Form Class에서 forms.Form을 상속받았던 것을 ModelForm으로 수정한 뒤, Model을 지정해주고 Model에서 가져다 사용할 Field들을 Class Meta를 통해 선언해주면 되요.
  • password 같은 경우는 암호화해서 DB에 저장해야하기 때문에 fields에서 제외시켰어요.
  • ModelForm을 사용하니, clean_email 매서드가 필요없게 됬어요. ModelForm이 유효성 검사를 진행해줍니다. 또한 문제가 있으면 오류를 발생시켜줍니다.
# users/forms.py
from django import forms
from . import models
...
...
class SignUpForm(forms.ModelForm): # 👈 ModelForm을 상속하면 Model을 활용할 수 있어요!
    class Meta: # 👈 연결할 Model과 사용할 fields를 지정해요!
        model = models.User # 👈 Model 지정
        fields = ( 
            "first_name",
            "last_name",
            "email",
        ) # 👈 field 지정
    password = forms.CharField(widget=forms.PasswordInput)
    password1 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password")
    def clean_password1(self):
        password = self.cleaned_data.get("password")
        password1 = self.cleaned_data.get("password1")
        if password != password1:
            raise forms.ValidationError("Password confirmation does not match")
        else:
            return password

2) commit=False

  • 여러 가지를 자동으로 처리해주지만 몇가지 문제가 존재합니다. Admin Panel에가서 데이터가 저장된 상태를 보면 username이 나타나지 않아요. 이와 함께 Andmin Panel 내부로 들어가보면 비밀번호가 존재하지 않습니다.
  • 이를 해결하기 위해서는 save를 가로채야해요. ModelForm은 clean처럼 save도 자동으로하기 때문이죠. 이에 save를 가로챈 뒤, 문제를 해결하고 다시 save할 수 있도록 처리해볼께요.
  • save를 가로채는 방법은 save(commit=False)를 하면 됩니다. "commit=False"는 Object는 생성하지만 저장하지 않은 상태입니다.
  • 그 다음 username으로 email을 넣어주고, password는 암호화시켜야하기 떄문에 set_password를 사용해서 값을 지정합니다. 마지막으로 저장 합니다.
from django import forms
from . import models
...
...
class SignUpForm(forms.ModelForm):
    class Meta:
        model = models.User
        fields = (
            "first_name",
            "last_name",
            "email",
        )
    password = forms.CharField(widget=forms.PasswordInput)
    password1 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password")
    def clean_password1(self):
        password = self.cleaned_data.get("password")
        password1 = self.cleaned_data.get("password1")
        if password != password1:
            raise forms.ValidationError("Password confirmation does not match")
        else:
            return password
    def save(self, *args, **kwargs): # 👈 save 매서드 가로채기
        user = super().save(commit=False) # 👈 Object는 생성하지만, 저장은 하지 않습니다.
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        user.username = email
        user.set_password(password) # 👈 set_password는 비밀번호를 해쉬값으로 변환해요!
        user.save() # 👈 이제 저장해줄께요:)
  • admin에서 계정을 삭제 후 다시 만들어줬어요! 이번엔 username이 email로 잘 들어간게 보이네요. Admin Panel 내부에 비밀번호가 암호화된 것을 볼 수 있답니다:)

5. Email 인증

1) send_mail

  • Django에서는 email을 보낼 수 있는 "send_mail" 기능이 내장되어 있는데요,, 존재하지 않는 이메일로 가입을 할 경우를 대비해 이메일 인증을 추가해 볼께요. 회원가입 후 메일을 인증해야지만 계정이 생성될 수 있도록 처리해볼께요:)
  • 아래 공식문서를 참고해서 메일을 발송할 수 있는데요,, 대부분의 Email Sevice(google, kakao 등..)는 메일 서버에서 발송된 메일이 아니면 스팸으로 처리해버린 답니다. 이에 mailgun service의 도움을 받아보도록 하겠습니다.
from django.core.mail import send_mail
send_mail(
    'Subject here',
    'Here is the message.',
    'from@example.com',
    ['to@example.com'],
    fail_silently=False,
)

2) mailgun service

  • 메일 서버를 사용하기 위해 "https://www.mailgun.com/" 서비스를 이용할께요. 이 메일 서버를 사용하기 위해서는 메일 서버를 통해 "EMAIL_HOST", "EMAIL_PORT", "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD"를 설정해주어야 해요. 이를 mailgun에서 제공해줍니다.
  • 회원가입과 로그인 후, "Sending" → "Domain settgins" → "SMTP credentials"로 이동하여 "Add new SMTP User"를 생성합니다. 페이지에 나타나는 HOST, PORT, USER, PASSWORD를 settgins.py에 설정하면 메일 서버를 Django에서 이용할 수 있어요!
  • 단, settgins.py의 중요한 정보를 직접 넣으면, git에 올라가 서버의 정보가 노출됩니다. 이에 "django environment"를 통해 중요한 값을 분리시켜 둘께요:)
# settgins.py
# Email Configuration
EMAIL_HOST = "smtp.mailgun.org"
EMAIL_PORT = "587"
EMAIL_HOST_USER = "ID를 복사/붙여넣기"
EMAIL_HOST_PASSWORD = "비밀번호를 복사/붙여넣기"

3) django-dotenv

  • "django-dotenv"는 ".env" 파일에서 코드를 읽어옵니다. 이에 "django-dotenv"를 설치해줍니다.
    • 🔍 pipenv install django-dotenv
  • 설치 후, "manage.py"에 dotenv를 import해주고 dotenv.read_dotenv()를 추가합니다. Django로 서버를 실행할 때, dotenv를 작동시키기 위함이에요!
import os
import sys
import dotenv # 👈 "dotenv" import
def main():
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
if __name__ == "__main__":
    dotenv.read_dotenv()  # 👈 dotenv.read_dotenv() 추가
    main()
  • 이제 ".env" 파일을 프로젝트 내에 생성하고, "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD", "EMAIL_FROM" 값을 변수에 담아줄께요.
  • EMAIL_HOST와 EMAIL_PORT는 특별히 주의할 정보가 아니기 때문에 MAILGUN_USERNAME, MAILGUN_PASSWORD, MAILGUN_FROM만 .env에 추가했습니다.
  • MAILGUN_FROM는 실제 수신자에게 보여질 발송자 메일주소입니다. @앞에 부분은 원하는 내용으로 만들 수 있어요! 다만, 뒷부분은 MAILGUN_USERNAME의 @ 뒷부분과 일치시켜줘야 해요!
# .env
MAILGUN_USERNAME = "ID를 복사/붙여넣기"
MAILGUN_PASSWORD = "비밀번호를 복사/붙여넣기"
MAILGUN_FROM = "airbnb_account@[이 부분은 복사 붙여 넣기]"
  • settgins.py의 변수에서 .env의 변수를 연결시켜, .env파일에 접근하여 값을 load할 수 있도록 처리합니다.
# settings.py
# Email Configuration
EMAIL_HOST = "smtp.mailgun.org"
EMAIL_PORT = "587"
EMAIL_HOST_USER = os.environ.get("MAILGUN_USERNAME")
EMAIL_HOST_PASSWORD = os.environ.get("MAILGUN_PASSWORD")
EMAIL_FROM = os.environ.get("MAILGUN_FROM")

4) render_to_string()

  • user/models.py에 2가지 필드를 추가로 생성하겠습니다. "email_verified"는 해당 사용자가 메일인증을 했는지에 대한 여부를 저장해요. 회원가입을 할 때는 defualt로 False를 갖고 있다가, 메일 인증을하면 True값으로 업데이트합니다.
  • 두번째는 "email_secret" 에요. 이 필드는 인증 메일에 전달할 난수(random_key)를 저장시킬 필드에요. 이 난수는 사용자의 메일에 링크로 담겨 전달되는데 메일을 열람하고 사용자가 링크를 클릭하면 url로 전달된 난수와 DB에 저장된 난수 값을 비교할꺼에요. blank=True로 지정한 이유는 인증이 완료되면 이 난수값은 필요없어 field를 비워주기 위함입니다.
  • 난수는 uuid를 이용하여 손쉽게 생성할 수 있답니다. hex로도 만들어주고 원하는 길이만큼 슬라이싱도 가능해요!
    • 🔎 uuid.uuid4().hex[:20] 👈 hex로 20자리까지만 난수를 생성
  • render_to_string은 HTML파일(emails/verify_email.html)을 문자열로 읽어옵니다. 템플릿 변수도 context처럼 전달할 수 있어요.
import uuid # 👈 "uuid" import
from django.conf import settings  # 👈 "settings.py" import
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.core.mail import send_mail # 👈 "send_mail" import
from django.utils.html import strip_tags # 👈 "strip_tags" import
from django.template.loader import render_to_string # 👈 "render_to_string" import
class User(AbstractUser):
    """Custom User Model"""
...
...
    avatar = models.ImageField(upload_to="avatars", blank=True)
    gender = models.CharField(choices=GENDER_CHOICES, max_length=10, blank=True)
    bio = models.TextField(blank=True)
    birthdate = models.DateField(null=True, blank=True)
    language = models.CharField(
        choices=LANGUAGE_CHOICES, max_length=2, blank=True, default=LANGUAGE_KOREAN
    )
    currency = models.CharField(
        choices=CURRENCY_CHOICES, max_length=3, blank=True, default=CURRENCY_KRW
    )
    superhost = models.BooleanField(default=False)
    email_verified = models.BooleanField(default=False)  # 👈 인증여부(True, False)
    email_secret = models.CharField(max_length=120, default="", blank=True)  # 👈 uuid를 사용하여 난수 임시 저장
    def verify_email(self): # 👈 회원가입 시, email을 인증을 위한 매서드입니다.
        if self.email_verified is False:
            secret = uuid.uuid4().hex[:20] # 👈 random key 생성
            self.email_secret = secret  # 👈 random key를 DB에 저장
            html_message = render_to_string(
                "emails/verify_email.html", {"secret": secret}
            )
            send_mail(
                "Verify Airbnb Account",  # 👈 제목
                strip_tags(html_message),  # 👈 내용
                settings.EMAIL_FROM,  # 👈 발송자
                [self.email],  # 👈 수신자
                fail_silently=False, 
                html_message=html_message,  # 👈 html을 메일로 전송해줍니다.
            )
            self.save() # 👈 저장(save매서드를 통해 필드의 값을 저장합니다.)
        return
  • 전송할 내용은 "emails/verify_email.html"에 별도로 만들어 두었어요. html파일을 별도로 생성하여 render_to_string로 load하면, css도 적용시킬수도 있답니다:)
  • 전달될 메일 내용 안에 a태그로 링크를 넣어, secret키를 전송할께요:)
<h4>Verify Email</h4>
<span>Hello, to verify your email click <a href="http://127.0.0.1:8000/users/verify/{{secret}}">here</a></span>

5) validate secret key

  • 이제 uuid를 담은 메일을 발송할 준비를 마쳤어요,, 이제 사용자가 메일을 열람하여 링크를 클릭하면 Django에 요청될 url에 따라 실행될 로직을 처리해야해요. 경로에 대한 로직부터 매핑해 주었어요.
  • url에 uuid값이 담겨있고 이를 가져와 DB에 저장된 값과 비교해야하기 때문에 "<str:key>"으로 받았습니다.
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
    path("login/", views.LoginView.as_view(), name="login"),
    path("logout/", views.log_out, name="logout"),
    path("signup/", views.SignUpView.as_view(), name="signup"),
    path("verify/<str:key>", views.complete_verification, name="complete-verification"), # 👈 인증 메일 내 링크가 클릭되면 "complete_verification" 함수가 작동합니다:)
]
  • 난수를 생성시켜 메일에 담아 전송하는 User Model의 verify_email() 매서드는 login 함수 뒤에 호출되도록 위치시켰어요.
  • "complete_verification" 함수는 url의 secret(random_key)를 key로 argument로 전달 받아 DB에 이미 저장되어있는 "email_secret" 값과 일치한지 비교합니다. 일치하다면 "email_verified" 필드를 True로 수정하고, "email_secret"는 이제 필요없으니 지워주기만하면 됩니다!
# from django.views import View
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms, models # 👈 "users/models.py" import
...
...
class SignUpView(FormView):
    template_name = "users/signup.html"
    form_class = forms.SignUpForm
    success_url = reverse_lazy("core:home")
    def form_valid(self, form):
        form.save()
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user is not None:
            login(self.request, user)
        user.verify_email() # 👈 users/models.py의 verify_email()는 여기서 실행되요:)
        return super().form_valid(form)
def complete_verification(request, key):
    try:
        user = models.User.objects.get(email_secret=key) # 👈 uuid값을 기준으로 Object를 가져와요!
        user.email_verified = True 
        user.email_secret = ""
        user.save()
        # to do: add succes message
    except models.User.DoesNotExist:
        # to do: add error message
        pass
    return redirect(reverse("core:home"))

profile
Keep Going, Keep Coding!

0개의 댓글