Applion 004 | django - 비밀번호 재설정 view + html 커스터마이징

Yunny.Log ·2022년 2월 3일
0

ToyProject

목록 보기
8/13
post-thumbnail
post-custom-banner

참고 블로그1
참고 블로그2
참고 블로그3

1) 장고에서 기본적으로 제공해주는 view들을 urls.py에 붙여넣기 (default)

코드를 입력하세요

해당 뷰를 로컬서버에서 실행시켜본다면

=> 이런식으로 기본값으로는 admin 페이지로 return 하도록 설정돼있음,

=> 우리는 이 화면을 user가 html 에서 보게하고 싶기 때문에 form을 상속받고 view를 커스터마이징해서 수정해줄 예정

2) html 커스터마이징

파일 위치

-> 이 파일위치들이 명확하게 나온 데가 없어서 찾느라 시간을 많이 썼다
-> 이 html 파일들을 각자 커스터마이즈 해주면 된다.

-> 나는 유저가 비밀번호 찾기 위한 이메일을 쳤을 때, 해당하는 이메일로 가입된 유저가 없다면 그 html 페이지에서 에러메시지를 (빨갛게) 띄워주는 것이 목표다
내가 이전에 작성한 회원가입 중복 이메일 에러 핸들링 글과 유사하다.

내가 설계한 방향

  • password_reset : 비밀번호 수정

    views.py
class PasswordResetView(PasswordContextMixin, FormView):
    email_template_name = 'registration/password_reset_email.html'
    extra_email_context = None
    form_class = PasswordResetForm
    from_email = None
    html_email_template_name = None
    subject_template_name = 'registration/password_reset_subject.txt'
    success_url = reverse_lazy('password_reset_done')
    template_name = 'registration/password_reset_form.html'
    title = _('Password reset')
    token_generator = default_token_generator
    @method_decorator(csrf_protect)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)
	def form_valid(self, form):
        if UserModel.objects.filter(email=self.request.POST.get("email")).exists():
            opts = {
                'use_https': self.request.is_secure(),
                'token_generator': self.token_generator,
                'from_email': self.from_email,
                'email_template_name': self.email_template_name,
                'subject_template_name': self.subject_template_name,
                'request': self.request,
                'html_email_template_name': self.html_email_template_name,
                'extra_email_context': self.extra_email_context,
            }
            form.save(**opts)
            return render(self.request, 'registration/password_reset_done.html', {'form' : form})
            #return super().form_valid(form)
        else : 
            return render(self.request, 'registration/password_reset_done_fail.html', {'form' : form})

password_reset_form.html

{% block title %}Forgot Your Password?{% endblock %}

  <form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="비밀번호 재설정 요청">
    <input type="submit" value="확인메일 재전송">
  </form>

  • password_reset_done : 비밀번호 수정 메일 발송 alert창 띄우기
{% block title %}Forgot Your Password?{% endblock %}

  <form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="비밀번호 재설정 요청">
    <input type="submit" value="확인메일 재전송">
  </form>

  <script>
      window.alert("입력해주신 이메일로 비밀번호 재전송 이메일을 보냈습니다");
  </script>

windows.alert() 참고!
https://developer.mozilla.org/ko/docs/Web/API/Window/alert


  • password_reset_done_fail : 입력한 메일로 가입된 유저 없음 경고글 띄워주기
<form method="POST">
    {% csrf_token %}
    {% for field in form %}
        <div>
                {{ field.label }} : {{ field }}
                <p style="color:red;">해당 이메일로 가입된 유저가 없습니다.</p>
        </div>
    {% endfor %}

    <button type="submit">비밀번호 재설정 요청</button><br>
    <button type="submit">확인 메일 재전송</button>
    </form>

=> 이메일 오면


  • password_reset_confirm : 이메일로 재설정 페이지

    password_reset_confirm.html
{% if validlink %}

<h1>Set a new password!</h1>
<form method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="비밀번호 재설정">
</form>

{% else %}

<p>유효하지 않은 링크입니다</p>

{% endif %}
  • password_reset_complete : 비밀번호 재설정 complete

    password_reset_complete.html
{% block title %}Password reset complete{% endblock %}

<h1>재설정 완료</h1>
<p><a href="{% url 'email_login' %}">로그인 하러 가기</a></p>

3) => 근데 위에서 진행한 파일들의 위치는 모두 가상환경임 (myvenv)

=> 따라서 나는 VIEWS.PY 의 관련 뷰들과 관련 HTML 파일들을 모두 내 프로젝트>USER앱 안에다가 데려옴

urls.py

    path('password_reset/', PasswordResetView.as_view(), name="password_reset"),
    path('password_reset_done/', PasswordResetDoneView.as_view(), name="password_reset_done"),
    path('password_reset_confirm/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name="password_reset_confirm"),
    path('password_reset_complete/', PasswordResetCompleteView.as_view(), name="password_reset_complete"),

views.py

from django.conf import settings

from django.contrib.auth.forms import (
    PasswordResetForm, SetPasswordForm,
)
from django.contrib.auth.tokens import default_token_generator

from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import render, resolve_url
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import (
    urlsafe_base64_decode,
)
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.contrib.auth.forms import SetPasswordForm
from django.contrib.auth import authenticate, login, logout
from django.core.exceptions import ValidationError
from django.shortcuts import render, redirect, resolve_url
from django.conf import settings
from django.urls import reverse_lazy
from .models import CustomUser
from .forms import *
import requests

# Class-based password reset views
# - PasswordResetView sends the mail
# - PasswordResetDoneView shows a success message for the above
# - PasswordResetConfirmView checks the link the user clicked and
#   prompts for a new password
# - PasswordResetCompleteView shows a success message for the above

class PasswordContextMixin:
    extra_context = None

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'title': self.title,
            **(self.extra_context or {})
        })
        return context


class PasswordResetView(PasswordContextMixin, FormView):
    email_template_name = 'registration/password_reset_email.html'
    extra_email_context = None
    form_class = PasswordResetForm
    from_email = None
    html_email_template_name = None
    subject_template_name = 'registration/password_reset_subject.txt'
    success_url = reverse_lazy('password_reset_done')
    template_name = 'registration/password_reset_form.html'
    title = _('Password reset')
    token_generator = default_token_generator

    @method_decorator(csrf_protect)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

    def form_valid(self, form):
        if CustomUser.objects.filter(email=self.request.POST.get("email")).exists():
            opts = {
                'use_https': self.request.is_secure(),
                'token_generator': self.token_generator,
                'from_email': self.from_email,
                'email_template_name': self.email_template_name,
                'subject_template_name': self.subject_template_name,
                'request': self.request,
                'html_email_template_name': self.html_email_template_name,
                'extra_email_context': self.extra_email_context,
            }
            form.save(**opts)
            return render(self.request, 'registration/password_reset_done.html', {'form' : form})
            #return super().form_valid(form)
        else : 
            return render(self.request, 'registration/password_reset_done_fail.html', {'form' : form})

INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'


class PasswordResetDoneView(PasswordContextMixin, TemplateView):
    template_name = 'registration/password_reset_done.html'
    title = _('Password reset sent')


class PasswordResetConfirmView(PasswordContextMixin, FormView):
    form_class = SetPasswordForm
    post_reset_login = False
    post_reset_login_backend = None
    reset_url_token = 'set-password'
    success_url = reverse_lazy('password_reset_complete')
    template_name = 'registration/password_reset_confirm.html'
    title = _('Enter new password')
    token_generator = default_token_generator

    @method_decorator(sensitive_post_parameters())
    @method_decorator(never_cache)
    def dispatch(self, *args, **kwargs):
        if 'uidb64' not in kwargs or 'token' not in kwargs:
            raise ImproperlyConfigured(
                "The URL path must contain 'uidb64' and 'token' parameters."
            )

        self.validlink = False
        self.user = self.get_user(kwargs['uidb64'])

        if self.user is not None:
            token = kwargs['token']
            if token == self.reset_url_token:
                session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
                if self.token_generator.check_token(self.user, session_token):
                    # If the token is valid, display the password reset form.
                    self.validlink = True
                    return super().dispatch(*args, **kwargs)
            else:
                if self.token_generator.check_token(self.user, token):
                    # Store the token in the session and redirect to the
                    # password reset form at a URL without the token. That
                    # avoids the possibility of leaking the token in the
                    # HTTP Referer header.
                    self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
                    redirect_url = self.request.path.replace(token, self.reset_url_token)
                    return HttpResponseRedirect(redirect_url)

        # Display the "Password reset unsuccessful" page.
        return self.render_to_response(self.get_context_data())

    def get_user(self, uidb64):
        try:
            # urlsafe_base64_decode() decodes to bytestring
            uid = urlsafe_base64_decode(uidb64).decode()
            user = CustomUser._default_manager.get(pk=uid)
        except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist, ValidationError):
            user = None
        return user

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['user'] = self.user
        return kwargs

    def form_valid(self, form):
        user = form.save()
        del self.request.session[INTERNAL_RESET_SESSION_TOKEN]
        if self.post_reset_login:
            auth_login(self.request, user, self.post_reset_login_backend)
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.validlink:
            context['validlink'] = True
        else:
            context.update({
                'form': None,
                'title': _('Password reset unsuccessful'),
                'validlink': False,
            })
        return context


class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
    template_name = 'registration/password_reset_complete.html'
    title = _('Password reset complete')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['login_url'] = resolve_url(settings.LOGIN_URL)
        return context
  • html 파일도 마찬가지로 데려옴

(+) django 의 의미
[
스택오버플로우 글](https://stackoverflow.com/questions/41075989/pythondjango-what-does-mean)

It is the opposite of your second example. When in the definition of a function, the ** operator gathers all the named arguments and makes a dictionary. When calling a function, it takes a dictionary and breaks it into named arguments

values = {'x': 1, 'y': 2}
f(**values)

f(x=1, y=2)

(+) url reverse
설명 출처 : https://my-repo.tistory.com/29

  • 하나는 def형 뷰에서는 reverse()를 그대로 사용하고, class형 뷰에서는 reverse_lazy()라는 함수를 사용한다.
    왜 그러냐면, Class-level 속성을 가지는 객체들은 import 될 때 배치되는데 URL-seolving 규칙은 import 시간에 세팅되어있지 않아 class 내에서 reverse()를 사용하면 URLconf의 규칙을 인식하지 못한다.
    따라서 class내에서 사용되는 reverse()함수는 URL 규칙을 적용하지 못하여 Included URLconf “does not appear to have any patterns in it” 에러을 뿜어내며 서버가 죽어버린다.
    그러나 def 속성 객체는 import 이후에 배치되므로 URLconf의 규칙을 적용할 수 있다.
  • 그러므로 class 형 뷰를 사용할때는 from django.urls import reverse_lazy 로 reverse_lazy() 함수를 import 해서 사용해주자.
post-custom-banner

0개의 댓글