Django User

지니🧸·2022년 10월 16일
0

Django

목록 보기
3/9

유저 기능과 django-allauth 패키지

Package vs. App

Package, 패키지: 여러 파이썬 파일의 모음이자 파이썬 파일들을 담고 있는 디렉토리

  • 일반적으로 서로 연관된 파일을 모아서 한 디렉토리에 두기 때문에 패키지는 어느 특정 기능을 구현하는 코드의 모음이기도 함

App, 앱

  • 장고의 개념
  • 장고 프로젝트 (어떤 웹 서비스를 이루는 코드 전체)를 이루는 하나의 컴포넌트
  • 장고 프로젝트의 settings.py 파일의 INSTALLED_APPS 목록의 앱들이 장고 프로젝트를 이루는 앱들임

Package vs. 앱

  • 패키지: 어떤 기능을 구현하기 위한 파이썬 코드의 모음
  • 앱: 장고 프로젝트를 이루는 한 단위

django-allauth와 django.contrib.auth

django.contrib.auth

  • 유저 기능을 구현하는데 쓰이는 장고에 포함된 앱
  • django.contrib.auth:
    • auth: authentication ****(유저 인증)
      • 유저의 아이디와 비밀번호를 확인하는 과정
    • contrib: contributed (기여하다)
      • 장고 프레임워크에 기여된 앱들
        • (예) django.contrib.admin, django.contrib.staticfiles
  • 내용
    • 유저 기능을 구현하기 위한 User model (models.py)
    • /login, /logout과 같은 url 패턴을 정의할 urls.py
    • login logic을 처리할 views.py와 forms.py

django-allauth

  • 유저 기능을 위한 패키지 (기본적으로 포함X 설치 필요)
  • urls.py, views.py, forms.py는 있지만 models.py는 없기 때문에 django.contrib.auth의 models.py를 가져다가 씀

django.contrib.auth vs. django-allauth

django-allauth는 django.contrib.auth가 제공하지 않는 아래의 기능 제공

  • 실제 존재하는 이메일인지 확인하는 이메일 인증
  • 소셜 서비스를 이용해서 로그인하는 소셜 로그인

django-allauth는 유저기능이 모두 완성되어 있어 짧은 코드 몇줄만 수정; django.contrib.auth는 필요한 부분을 직접 구현해야 함

Coplate 프로젝트 생성

shell에서 코드

#shell
>>> cd django
>>> mkdir django-coplate
>>> cd django-coplate
>>> conda activate real-django #virtual env create & activate
>>> django-admin startproject coplate_project
>>> cd coplate_project
>>> python manage.py startapp coplate

settings.py에서 coplate 앱 등록

#settings.py
INSTALLED_APPS = [
		...
		'coplate',
]

User Model 정의하기

django.contrib.auth의 models.py는 아래를 제공함:

  • 기본유저 모델: User
    • 유저 모델을 한 번 정의하면 다른 모델로 바꾸기 어려움
  • 상속받아서 customize할 수 있는 유저 모델: AbstractUser, AbstractBaseUser
    • 원하는 필드 추가해서 사용 가능
    • AbstractUser: 쓸만한 필드들이 많이 정의되어 있음
      • username, password, first_name, last_name, email, date_joined, last_login
    • AbstractBaseUser: 유저를 만들 틀만 제공하고 필드는 직접 정의해줘야함

유저 정의하기

#models.py
from django.db import models
**from django.contrib.auth.models import AbstractUser**

# Create your models here.
**class User(AbstractUser):
		pass**

settings.py에 유저 모델 설정

#settings.py
...
AUTH_USER_MODEL = "coplate.User"
  • “coplate.User”: coplate 앱에 있는 유저 모델

Migrate하기

#terminal
>>> python manage.py makemigrations
>>> python manage.py migrate

User Model에 Admin 페이지 등록하기

admin.py에 admin 등록

#admin.py 
from django.contrib import admin
**from djang.contrib.auth.admin import UserAdmin
from .models import User**

# Register your models here.
**admin.site.register(User, UserAdmin)**
  • UserAdmin 클래스는 User 모델에 대해서 특별한 인터페이스 제공

superuser 만들기

>>> python manage.py createsuperuser
  • 일반 유저는 admin 페이지 접속 불가

allauth 셋업

allauth 설치하기

>>> pip install django-allauth

settings.py 설정하기

#settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',

    'coplate',

    'widget_tweaks', 

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
]

SITE_ID = 1

...

AUTHENTICATION_BACKENDS = [
    # Needed to login by username in Django admin, regardless of 'allauth'
    'django.contrib.auth.backends.ModelBackend',

    # 'allauth' specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
]
...
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
  • ‘django.contrib.sites’
    • 어떤 기능을 여러 웹사이트에서 사용할 수 있게 해줌
    • 장고 프로젝트 하나로 여러 웹사이트 운영 가능
    • SITE_ID 가 이 장고 프로젝트로 만들 각각 웹사이트의 ID
  • EMAIL_BACKEND
    • 이메일을 어떻게 보낼지 설정
    • 'django.core.mail.backends.console.EmailBackend' : 터미널 콘솔로 이메일 보내기 설정

url pattern 설정

from django.contrib import admin
from django.urls import path, **include**

urlpatterns = [
    path('admin/', admin.site.urls),
    **path('', include("allauth.urls"))**
]

홈페이지 만들기

urls.py에 url 생성

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    **path("", include("coplate.urls")),**
    path('', include("allauth.urls")),
]
  • Django는 성공적인 매칭 때까지 위에서 아래로 서칭함

coplate에 urls.py 생성

#coplate의 urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index")
]

views.py에 index 뷰 정의

#views.py
from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, "coplate/index.html")

settings.py에 로그인/signup 성공시 redirect url 설정

#settings.py
...
ACCOUNT_SIGNUP_REDIRECT_URL = "index"
LOGIN_REDIRECT_URL = "index"

현재 유저 접근하기

  • View에서는 request.user로 접근
    • request.user.email → 유저의 이메일
    • request.user.is_authenticated → 로그인 여부를 True/False로 나타냄
  • Template에서는 {{user}} 로 접근
<!DOCTYPE HTML>
<head>
</head>
<body>
    <navbar>
        {% if user.is_authenticated %}
            <a href="{% url 'account_logout' %}">로그아웃</a>
        {% else %}
            <a href="{% url 'account_login' %}">로그인</a>
            <a href="{% url 'account_signup' %}">회원가입</a>
        {% endif %}
    </navbar>
    <h1>홈페이지</h1>
    {% if user.is_authenticated %}
        <p>안녕하세요, {{user}}님!</p>
    {% else %}
        <p>로그아웃된 상태입니다.</p>
    {% endif %}
</body>

logout 시에 confirm 페이지 제거

#settings.py
ACCOUNT_LOGOUT_ON_GET = True

이메일로 로그인하기

settings.py에서 authentication method 설정

#settings.py
ACCOUNT_AUTHENTICATION_METHOD = "email"
  • ACCOUNT_AUTHENTICATION_METHOD
    • default: username으로 로그인
    • = “username_email” : username 또는 email로 로그인

settings.py에서 이메일 입력칸 필수 설정

#settings.py
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False

User Model의 Instance 출력 설정 변경

#models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.
class User(AbstractUser):
    **def __str__(self):
        return self.email**

로그인에 대해

  1. 사용자가 로그인 정보를 입력 후 로그인 버튼 클릭
  2. 로그인 정보가 서버에 전달 (Login Request)
  3. 서버에서 로그인 정보 일치 확인
  4. 아이디 & 비밀번호 일치 시 → 서버에 유저에 대한 세션 생성
    • Session, 세션: 웹사이트 방문에 대한 기록
      • 각 세션에는 ID가 있고, 유저의 ID & 정보 포함
  5. 서버에서 클라이언트로 Login Response 보냄
    • Login Response에 session-id가 포함됨
  6. 클라이언트는 쿠키에 session id 저장
    • 쿠키: 웹사이트에 대한 정보를 저장하는 곳

창을 닫아도 로그인 유지

#settings.py
ACCOUNT_SESSION_REMEMBER = True
  • 기본값은 사용자에게 선택지를 줌

로그인 세션 시간 설정

#settings.py
SESSION_COOKIE_AGE = 3600
  • default는 2주
  • 세션을 평생으로 설정할 수 X

사용자가 직접 로그아웃하면 세션이 삭제되지만 세션 시간 만료로 인한 로그아웃은 세션이 쌓임

  • 웹사이트 운영 시에는 세션이 쌓이면 서버에 무리가 가기 때문에 정리 추천
>>> python manage.py clearsessions

닉네임 필드 추가하기

models.py에 닉네임 필드 정의하기

#models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.
class User(AbstractUser):
    **nickname = models.CharField(max_length=15, unique=True)**

    def __str__(self):
        return self.email

기존 데이터에는 닉네임이 없기 때문에 migrate하기 전에 아래 두가지 중 한가지 방법 실행

  • unique=True 삭제 → default 닉네임 설정 → admin 페이지에서 닉네임 수정
  • null=True 추가 → admin 페이지에서 닉네임 수정
    • nickname값을 회원가입 페이지에서 받으려면 null=True로 설정해야하기 때문에 두번째 방법 선택

    • allauth는 일단 기본정보가 입력된 유저를 생성하고 그 후에 닉네임과 같은 추가 정보를 채워넣음. 그래서 처음 유저를 생성할 때는 닉네임이 채워지지 않음

      #models.py
      ...
      class User(AbstractUser):
          nickname = models.CharField(max_length=15, unique=True, null=True)
      ...
      

      User Model에 대한 추가 필드는 기본 admin 페이지에 나타나지 않음. admins.py에 추가

      #admin.py
      ...
      UserAdmin.fieldsets += (("Custom fields", {"fields": ("nickname",)}),)
    • custom fields라는 섹션 아래에 nickname 필드 추가

회원가입 페이지에 닉네임 필드 추가

  1. coplate app에 forms.py 생성

  2. SignupForm 클래스 생성

    #forms.py
    from django import forms
    from .models import User
    
    class SignupForm(form.ModelForm):
        class Meta:
            model = User
            fields = ["nickname"]
    • ModelForm은 사용할 모델만 모델의 필드에 따라서 폼을 알아서 만들어줌
      • Meta 클래스에 사용할 모델과 추가적인 필드를 명시해주면 됨
  3. SignupForm 클래스 내에 signup function 정의하기

    #forms.py
    from django import forms
    from .models import User
    
    class SignupForm(form.ModelForm):
        class Meta:
            model = User
            fields = ["nickname"]
        
        **def signup(self, request, user):
            user.nickname = self.cleaned_data["nickname"]
            user.save()**
    • 폼에 기입된 데이터는 cleaned_data로 가져올 수 있음
  4. SignupForm을 사용하겠다고 settings.py에 설정

    #settings.py
    ...
    ACCOUNT_SINGUP_FORM_CLASS = "coplate.forms.SignupForm" #coplate 안의 forms의 SignupForm 사용

회원가입 정보 유효성 검사

django.contrib.auth & allauth가 기본적으로 하는 유효성 검사

  • 로그인할 때 아이디 & 비밀번호가 일치하는지
  • 회원가입 시 비밀번호 & 비밀번호 확인이 일치하는지
  • 이메일 주소가 유효한지
  • 이메일 주소가 중복되지 않는지

오류가 한국어로 나오게 하기 → 장고의 언어 상태 변경

#settings.py
...
#LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ko'

오류 메시지 customize하기 & validator 적용하기

from django.db import models
from django.contrib.auth.models import AbstractUser
from .validators import validate_no_special_characters

# Create your models here.
class User(AbstractUser):
    nickname = models.CharField(
        max_length=15, 
        unique=True, 
        null=True, 
        **validators = [validate_no_special_characters],
        error_messages={'unique': '이미 사용중인 닉네임입니다.'},**
    )

    def __str__(self):
        return self.email

validator class 정의하기 (password validator는 클래스형 validator 사용)

#validators.py
class CustomPasswordValidator:
    def validate(self, password, user=None):
        if (
                len(password) < 8 or
                not contains_uppercase_letter(password) or
                not contains_lowercase_letter(password) or
                not contains_number(password) or
                not contains_special_character(password)
        ):
            raise ValidationError("8자 이상의 영문 대/소문자, 숫자, 특수문자 조합이어야 합니다.")

    def get_help_text(self):
        return "8자 이상의 영문 대/소문자, 숫자, 특수문자 조합을 입력해 주세요."
  • validate라는 메소드를 클래스 안에 정의
  • 비밀번호가 유효하지 않으면 raise ValidationError()
  • get_help_text(self)
    • admin 페이지에서 비밀번호를 바꿀 때 필요한 내용
      • admin 페이지에서 비밀번호를 바꿀 때도 PasswordValidator가 적용됨

settings.py에 validator 설정

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "coplate.validators.CustomPasswordValidator"
    },
]

form에 대한 오류가 나도 입력했던 비밀번호를 폼에 그대로 채워넣어줌

#settings.py
...
ACCOUNT_PASSWORD_INPUT_RENDER_VALUE = True

이메일 인증

: 가입한 이메일이 본인이 실제로 사용하는 이메일인지 확인하는 절차

  • 인증 링크 or 인증코드+입력

이메일 인증 여부 결정하기

#settings.py
...
ACCOUNT_EMAIL_VERIFICATION = "optional"
ACCOUNT_CONFIRM_EMAIL_ON_GET = True 
  • ACCOUNT_EMAIL_VERIFICATION = …
    • 3 options: “mandatory”, “optional”, “none”
      • “mandatory”: 이메일 인증하지 않으면 로그인 불가
      • “optional”: 인증 이메일은 발송되지만 인증하지 않아도 로그인 가능
      • “none”: 이메일 인증이 필요없고 발송 X
  • ACCOUNT_CONFIRM_EMAIL_ON_GET = True
    • 링크 클릭하자마자 인증 완료

이메일 인증시 로그인 여부에 따라 로그인하기 or 홈페이지

#project의 urls.py
from django.contrib import admin
from django.urls import path, include
**from django.views.generic import TemplateView**

urlpatterns = [
    #admin
    path('admin/', admin.site.urls),
    #coplate
    path("", include("coplate.urls")),
    #allauth
    **path("email-confirmation-done/",
     TemplateView.as_view(template_name="coplate/email_confirmation_done.html"),
     name="account_email_confirmation_done"),**
    path('', include("allauth.urls")),
]
#settings.py
...
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = "account_email_confirmation_done"
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "account_email_confirmation_done"

렌더할 html 페이지 만들기

#email_confirmation_done.html
<DOCTYPE! HTML>
<header>
</header>
<body>
    이메일 인증이 완료되었습니다. 
    {% if user.is_authenticated %}
        <a href="{% url 'index' %}">홈으로 이동</a>
    {% else %}
        <a href="{% url 'account_login' %}">로그인</a>
    {% endif %}
</body>

비밀번호 관리 기능

allauth는 자동으로 비밀번호 찾기 기능 제공

  • 비밀번호 찾기 이메일 유효기간 설정 가능
    #settings.py
    ...
    PASSWORD_RESET_TIMEOUT_DAYS = 3
    • default는 3일

비밀번호 변경 추가하기

#index.html
...
{% if user.is_authenticated %}
        <p>{{user}}님의 닉네임은 {{user.nickname}}</p>
        **<p><a href="{% url 'account_change_password' %}">비밀번호 변경</a></p>**
    {% else %}
        <p>로그아웃된 상태입니다.</p>
{% endif %}
  • {% url 'account_change_password' %}
    • allauth 소스코드에서 찾아오기

비밀번호 변경 성공시 페이지 변경: PasswordChange 뷰를 상속받아서 로직을 바꿔야함

#views.py
from django.shortcuts import render
from django.urls import reverse
from allauth.account.views import PasswordChangeView
...
class CustomPasswordChangeView(PasswordChangeView): #PasswordChangeView 상속받기
    def get_success_url(self):
        return reverse("index")
  • get_success_url(self)
    • 어떤 폼이 성공적으로 처리되면 어디로 redirection할 건지를 정의해주는 함수

urls.py에 설정

#urls.py
...
urls = [
		...
		path('password/change/', CustomPasswordChangeView.as_view(), 
						name="account_password_change"),
]

자주 사용되는 allauth URL 목록

  • : 이메일 인증/비밀번호 재설정에 사용되는 코드
    • 전송되는 이메일에 자동포함

allauth 유용한 세팅들 목록

폼과 디자인

탬플릿 오버라이딩 (홈페이지 예쁘게 만들기)

  1. templates - account - signup.html
  2. signup.html에 input 대신 django 폼 설정 (django 탬플릿 language 이용)
    • url 태그를 써서 링크 주소 변경
    • form 변수로 form과 view 연결

Django 폼에 속성 더하기: django-widget-tweaks 패키지

  1. django-widget-tweaks 설치
pip install django-widget-tweaks
  1. settings.py에 APP 설정
#settings.py 
INSTALLED_APPS = [
		...
		'widget_tweaks',
		...
]
  1. 이 패키지를 사용하고 싶은 템플릿에 패키지 로드
#signup.html
...
{% load widget_tweaks %}
...
  1. 사용하고 싶은 폼에 속성으로 추가
(ex) {{ form.email|attr: "class:cp-input"|attr:"placeholder:이메일"}}
  • attr: 태그에 있는 속성이면 변경해주고 없는 속성이면 추가해줌
(ex) {{ form.email|add_class:"cp-input"|attr:"placeholder:이메일"}}
  1. 특정 경우에만 나타나는 클래스 추가하기
{{ form.nickname|add_class:"cp-input"|
			attr:"placeholder:닉네임 (Coplate에서 사용되는 이름입니다)"|
			**add_error_class:"error"**
}}
**{% for error in form.nickname.errors %}
     <div class="error-message">{{ error }}</div>
{% endfor %}**
  • add_error_class: 에러가 있을때 클래스 사용

Input 태그의 중요한 설정들

<input type="email" name="email" placeholder="이메일" autocomplete="email" class="cp-input" required id="id_email">

type: 필드에 들어가는 데이터 유형

  • 모델 폼을 사용하면 모델 필드의 종류에 따라 type 설정됨
    • (ex) CharField - type=”text”, URLField - “type=url”, IntegerField - type=”number”, ImageField - type=”file”
  • type에 따라 사용되는 HTML 폼 필드가 결정되고 입력되는 데이터에 대한 유효성 검사 진행
    • 서버 측에서 진행되는 유효성 검사와 다름. 유효하지 않은 데이터는 아예 서버 쪽으로 전달되는 것을 막음
      • 클라이언트 측에서 진행할 수 없는 유효성 검사도 존재 (예) 어떤 값의 중복 여부 확인은 데이터베이스 비교 필요

name: 폼 데이터가 서버로 전송될때 사용되는 이름

  • django 폼을 사용하면 자동으로 설정됨

placeholder: HTML 폼 필드 안에 디스플레이되는 텍스트

  • Django 폼을 사용하면 기본적으로 django 필드의 이름 사용
  • widget-tweaks 패키지로 placeholder 속성 변경 가능

maxlength: 입력받는 값의 최대 길이 제한

  • 클라이언트 측에서 진행되는 유효성 검사의 일부
  • 모델 폼을 사용하면 CharField의 max_length 값에 따라 자동 설정

autocomplete: HTML 폼 필드에 값을 입력할 때, 과거에 비슷한 필드에 입력했던 값을 제안해주는 기능

  • 브라우저는 기본적으로 input 태그에 입력되는 값을 기억함
  • autocomplete 속성은 form 태그 & input 태그에 모두 존재
    • input 태그의 속성이 더 우선순위가 높음
  • autocomplete 속성의 default 값: “on” (자동완성 기능 사용)
  • form 태그의 autocomplete 속성: “on”, “off”
  • input 태그의 autocomplete 속성: “on”, “off”, “email”, “new-password”, 등

class: 디자인을 위한 속성

  • Django가 만들어주는 input 태그에는 class 속성 포함 X

required: HTML 폼 필드를 비워놓을 수 없게 하는 속성

  • 클라이언트 측에서 진행되는 유효성 검사의 일부
  • Django 모델/폼 필드를 필수로 정의해 주면 required 속성 자동 추가

id

  • django 폼을 사용하면 자동으로 설정됨

Non-field error

: 폼 전체에 대한 오류 설정

{% for error in form.non_field_errors %}
    <div class="form-error error-message">{{error}}</div>
{% endfor %}

메세지와 이메일 오버라이딩

  • allauth를 사용하면 action 성공마다 message가 발생. custom 페이지로 message가 보이진 않더라도 쌓임 (admin 페이지에서 확인 가능). 이를 오버라이딩 하려면 templates-accounts-messages에 빈 text 파일 추가
  • allauth가 발송하는 이메일 내용 오버라이딩으로 변경 가능. templates-accounts-email에 원하는 내용 추가
    • allauth는 email 내용이 오버라이딩 되더라도 제목에 자동으로 도메인을 추가하기 때문에 이를 없애기 위해 settings.py에 설정

      ACCOUNT_EMAIL_SUBJECT_PREFIX = ""

allauth 템플릿 오버라이딩 정리

allauth의 템플릿 파일을 오버라이드하려면 allauth의 템플릿 파일과 똑같은 이름을 가진 파일을 app_name/teampltes/account 폴더에 넣어야 함.

settings.py 파일의 INSTALLED_APPS 목록에서 app_name은 allauth보다 위에 와야함

페이지템플릿 이름필드

| 회원가입
’account_signup’ | signup.html | - 유저네임: {{ form.username }}

  • 이메일: {{ form.email }}
  • 비밀번호: {{ form.password1 }}
  • 비밀번호 확인: {{ form.password2}}
  • 추가 필드(extra_field): {{ form.extra_field }} |
    | 로그인
    ’account_login’ | login.html | - 로그인 (유저네임/이메일/둘다 허용): {{ form.login }}
  • 비밀번호: {{ form.password }} |
    | 비밀번호 변경
    ’account_change_password’ | password_change.html | - 현재 비밀번호: {{ form.oldpassword }}
  • 새 비밀번호: {{ form.password1 }}
  • 새 비밀번호 확인: {{ form.password2 }} |
    | 비밀번호 찾기
    ’account_reset_password’ | password_reset.html | - 이메일: {{ form.email }} |
    | 비밀번호 재설정 이메일 발송 완료
    ’account_reset_password_done’ | password_reset_done.html | |
    | 비밀번호 재설정
    ’account_reset_password_from_key’ | password_reset_from_key.html | - 새 비밀번호: {{ form.password1 }}
  • 새 비밀번호 확인: {{ form.password2 }} |
    | 비밀번호 재설정 완료
    ’account_reset_password_from_key_done’ | password_reset_from_key_done.html | |

allauth 종합 정리

  • django-allauth는 유저 기능을 만들기 위한 패키지 (따로 설치 필요)
    • 유저 기능 구현에 필요한 URL 패턴, 뷰, 폼, 템플릿 등 있음
    • 유저 모델은 django.contrib.auth의 유저 모델을 사용

allauth URL: URL 패턴 정의

  • allauth의 URL 정리
    urlpatterns = [
    		...
    		path('accounts/', include('allauth.urls')),
    		...
    ]
    • 이렇게 정의하면 모든 url 앞에 accounts/가 붙음

    • (예) localhost:8000/accounts/login/

      urlpatterns = [
      		...
      		path('', include('allauth.urls')),
      		...
      ]
    • 이렇게 정의해 주면 모든 url 앞에 아무것도 안 붙음

    • (예) localhost:8000/login/

allauth 주요 로직: 세팅 (Configuration)

allauth 폼: custom 폼

  • forms.py 파일에 추가 필드에 대한 폼을 만들고, signup(self, request, user) 메소드를 정의
    class SignupForm(forms.ModelForm):
        class Meta:
            model = User
            fields = ['extra_field1', 'extra_field2', ...]
    
            def signup(self, request, user):
            user.extra_field1 = self.cleaned_data['extra_field1']
            user.extra_field2 = self.cleaned_data['extra_field2']
            ...
            user.save()
    • ModelForm 대신 일반 Form 사용 가능
  • ACCOUNT_SIGNUP_FORM_CLASS 설정
    #settings.py
    ACCOUNT_SIGNUP_FORM_CLASS = 'app.forms.SignupForm'

디자인: 템플릿 오버라이딩

뷰 오버라이딩

  • 뷰: 웹페이지의 주요 로직 담당
  • PasswordChangeView는 비밀번호 변경 후 리디렉트 되는 URL을 configuration으로 설정할 수 없어서 뷰 오버라이딩 필요
  1. app/views.py에서 뷰 정의

    class CustomPasswordChangeView(PasswordChangeView):
        def get_success_url(self):
            return reverse('index')
    • 리디렉트 URL을 ‘index’ (홈페이지)로 설정
  2. 오버라이딩한 뷰를 사용하기 위해서 URL 패턴 변경

    #project/urls.py
    urlpatterns = [
        ...
        path('password/change/', CustomPasswordChangeView.as_view(), name='account_password_change'),
        path('', include('allauth.urls')),
    ]

리뷰 모델과 CRUD 기능 구현

Review Model 만들기

Review Model 만들기

lass Review(models.Model):
    title = models.CharField(max_length=30)
    restaurant_name = models.CharField(max_length=20)
    restaurant_link = models.URLField( #값이 url 형태인지 확인
        validators = [validate_restaurant_link]
    ) 

    RATING_CHOICES = [
        (1, 1), #첫번째 값: 모델 필드에 실제로 들어갈 값 (모델에 저장), 두번째 값: 디스플레이에 사용되는 값 (화면에 보임)
        (2, 2), 
        (3, 3),
        (4, 4), 
        (5, 5),
    ]
    rating = models.IntegerField(choices=RATING_CHOICES)

    image1 = models.ImageField(upload_to="review_pics")
    image2 = models.ImageField(upload_to="review_pics", blank=True)
    image3 = models.ImageField(upload_to="review_pics", blank=True)
    content = models.TextField()
    dt_created = models.DateTimeField(auto_now_add=True)
    dt_updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title
  • blank = True: 이미지 필수 X 설정

url을 위한 validator 설정

#validators.py
def validate_restaurant_link(value):
    if "place.naver.com" not in value and "place.map.kakao.com" not in value:
        raise ValidationError("place.naver.com 또는 place.map.kakao.com이 들어가야 합니다.")

admin에 등록

#admin.py
...
from .models import User, **Review
...**
**admin.site.register(Review)**

정적 파일과 미디어 파일

정적 파일, Static File: 웹 어플리케이션에서 바뀌지 않는 파일

  • django의 template
  • settngs.py에 STATIC_URL = 'static/' 설정
    • 정적 파일의 url 주소가 domain + “/static” + 각각 폴더 안의 경로가 됨
  • url: {% static url %}

미디어 파일, Media File: 사용자가 직접 업로드하는 파일 (삭제 될수도)

  • media url 사용
  • 미디어 파일이 어디에 있는지 django에게 알려줘야 함
    • 샌드위치 구조가 정해져 있는 정적 파일 주소와는 다름
    • media root 셋팅 사용
#settings.py
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/uploads/'

url 설정

#project/urls.py
...
from django.conf import settings
from django.conf.urls.static import static
...
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
  • 미디어 파일에 대한 요청이 들어오면 media root 안의 media 파일을 return

ImageField 다루기

업로드한 image 파일 접근하기

>>> python manage.py shell
>>> from coplate.models import Review
>>> r = Review.objects.all().first()
>>> r.image1 #<ImageFieldFile: review_pics/dribbble_zoombackground__1__4x.png>
>>> r.image1.url #'/uploads/review_pics/dribbble_zoombackground__1__4x.png'

Null vs. Blank

null

  • 데이터베이스와 관련된 옵션
  • null=True: 필드에 해당하는 데이터베이스 컬럼에 NULL(데이터베이스에서 존재하지 않는 값을 의미)를 허용. 필드에 대한 값이 없으면 데이터베이스에 NULL 저장
  • null의 default는 False

blank

  • 폼과 유효성 검사에 관련된 내용
  • blank=True : 폼 (admin 페이지의 폼 포함)에서 필드에 대한 값을 입력해 주지 않아도 됨
    • 즉, optional 필드가 됨

null과 blank 사용 방법

  • 문자열 기반 필드를 optional 필드로 만들고 싶다면 (필드에 빈 값을 허용하고 싶다면) blank=True 사용
    # name 필드는 옵셔널 필드 (폼에서 작성하지 않아도 됨)
    
    class Person(models.Model):
        name = models.CharField(max_length=20, blank=True)
    • 문자열 기반 필드는 필드에 대한 값이 없으면 데이터베이스에 빈 문자열 “” 을 저장 하기 때문에 optional 필드를 만드는 데에 null=True가 필요 없음

      unique=True(중복 금지) 옵션도 함께 사용하고 싶다면 unique=True, null=True, blank=True 모두 사용

      # name 필드는 옵셔널 필드 (폼에서 작성하지 않아도 됨), 하지만 중복 금지
      
      class Person(models.Model):
          name = models.CharField(max_length=20, blank=True, null=True, unique=True)
    • 옵셔널 필드에 unique=True 옵션도 사용하게 되면 필드 값이 없는 object가 여러개 있을 수 있는데, 빈 값을 모두 “”로 저장하면 “”가 중복됨 → 데이터베이스에서 중복 오류

      • “” 대신 NULL을 사용해야 함
        • NULL은 어떤 주어진 값으로 생각하지 않기 때문에 중복돼도 상관 X
  • 문자열 기반이 아닌 필드를 optional 필드로 만들고 싶다면 null=True, blank=True 사용
    # age 필드는 옵셔널 필드 (폼에서 작성하지 않아도 됨)
    
    class Person(models.Model):
        age = models.IntegerField(blank=True, null=True)
    unique=True(중복 금지) 옵션도 함께 사용하고 싶다면 unique=True, null=True, blank=True 모두 사용
    # age 필드는 옵셔널 필드 (폼에서 작성하지 않아도 됨), 하지만 중복 금지
    
    class Person(models.Model):
        age = models.IntegerField(blank=True, null=True, unique=True)

모델과 모델 사이의 관계

  • 유저와 리뷰 사이의 관계: 유저가 리뷰를 작성함 → 이 두 모델을 연결해줘야함

Data Modeling: 주어진 객체들과 정보를 가지고 필요한 장고 모델을 설계하는 것

  • ForeignKey (필드의 종류): 일대다(1:N, one to many) 관계
    • ForeignKey(Model)
    • ‘다(Many)’에 해당하는 모델에 ForeignKey 필드를 추가해야 함
      • (예) 유저가 여러 리뷰를 작성하니 리뷰가 ‘다'에 해당. Review 모델에 ForeignKey 필드 추가, 그 안에 User 모델 작성
    • on_delete (옵션 종류): 참조하는 오브젝트가 삭제됐을 때 지금 이 오브젝트를 어떻게 할지 설정
      • (예) 우재가 리뷰를 작성했는데 우재라는 유저가 데이터베이스에서 삭제됐을 때 우재가 작성한 리뷰를 어떻게 할건지?
      • on_delete=models.CASCADE : 유저가 삭제되면 유저가 쓴 리뷰를 모두 삭제
      • OR 작성한 리뷰가 하나라도 있으면 유저 삭제 금지 OR 유저를 삭제하고 그 유저를 참조하던 리뷰의 author를 null로 설정
#models.py
class Review(models.Model):
    ...
    author = models.ForeignKey(User, on_delete=models.CASCADE)
		...
#shell
>>> from coplate.models import Review, User
>>> Review.objects.all()
>>> r = Review.objects.all().first()
>>> r.author #author's email
>>> r.author.nickname
#특정 author의 리뷰 찾기 - 1. id로 필터링
>>> Review.objects.filter(author__id=1)
#특정 author의 리뷰 찾기 - 1. 닉네임으로 필터링
>>> Review.objects.filter(author__nickname='지니')

테스트 리뷰

  1. 테스트 리뷰 (json 파일) manage.py와 같은 레벨에 위치
  2. 데이터 로드하기
#shell
python manage.py loaddata reviews.json

홈페이지 구현하기

  1. urls.py 재설정
urlpatterns = [
    path("", views.**IndexView.as_view()**, name="index")
]
  1. IndexView 제네릭 뷰 정의
...
from django.views.generic import ListView
from coplate.models import Review
...
class IndexView(ListView):
    model = Review
    template_name = "coplate/index.html"
    context_object_name = "reviews"
    pagninate_by = 4
    orderng = ["-dt_created"] #내림차순
  1. index.html 구현
...
<div class="contents">
        {% for review in reviews %}
        <a href="#">
          <section class="cp-card content">
            <div class="thumb" style="background-image: url('{{review.image1.url}}');"></div>
            <div class="body">
              <span class="cp-chip green">{{review.restaurant_name}}</span>
              <h2 class="title">{{ review.title }}</h2>
              <date class="date">{{ review.dt_created|date:"Y년 n월 j일"}}</date>
              <div class="metadata">
                <div class="review-rating">
                  <span class="cp-stars">
                    {% for i in ""|ljust:review.rating %}*{% endfor %}
                  </span>
                </div>
                <div class="review-author">
                  <span>{{ review.author.nickname }}</span>
                </div>
              </div>
            </div>
          </section>
        </a>

        {% empty %}
        <p class="empty">아직 리뷰가 없어요 :(</p>

        {% endfor %}
      
      </div>
  • {{ review.dt_created|date:"Y년 n월 j일" }}
    • 생선된 날짜를 0000년 0월 0일 포맷에 맞게 디스플레이
    • 소문자 n과 j는 앞에 0이 붙지 않는 월 & 일을 의미
  • {% empty %}
    • 리뷰가 없을 경우에 이 블락 아래의 코드 디스플레이

상세페이지 구현

  1. urls.py에 상세페이지 연결
#urls.py
...
urlpatterns = [
    ...
    path("reviews/<int:revew_id>/", 
					views.ReviewDetailView.as_view(), 
					name="review-detail"),
]
  1. ReviewDetailView 정의
#views.py
from django.views.generic import ListView, **DetailView
...
class ReviewDetailView(DetailView):
    model = Review
    template_name = "coplate/review_detail.html"
    pk_url_kwarg = "review_id"**
  1. index.html에 detail 페이지 연결
...
<a href="{% url 'review-detail' review.id %}">
...
  1. detail 페이지
...
<div class="content">
      <img class="thumb" src="{{ review.image1.url }}">
      {% if review.image2 %}
          <img class="thumb" src="{{ review.image2.url }}">
      {% endif %}
      {% if review.image3 %}
          <img class="thumb" src="{{ review.image3.url }}">
      {% endif %}

      <p class="content">{{ review.content|**linebreaksbr** }}</p>
      <a class="location" target="_blank" href="{{ review.restaurant_link }}">
        <img class="cp-icon" src="{% static 'coplate/icons/ic-pin.svg' %}" alt="Pin Icon">
        <span>위치보기</span>
      </a>
    </div>

    <div class="footer">
      <a class="back-link" href="{% url 'index' %}">&lt; 목록으로</a>
      <div class="buttons">
        <a class="cp-button warn" href="#">삭제</a>
        <a class="cp-button secondary" href="#">수정</a>
      </div>
    </div>
...

리뷰 작성 페이지 구현

  1. urls.py에 연결
...
urlpatterns = [
    ...
    path("reviews/new/", views.ReviewCreateView.as_view(), name='review-create')
]
  1. forms.py에 ReviewForm 정의
...
from .models import User, Review
...
class ReviewForm(forms.ModelForm):
    class Meta:
        model = Review
        fields = [
            "title", 
            "restaurant_name", 
            "restaurant_link", 
            "rating", 
            "image1", 
            "image2", 
            "image3", 
            "content",
        ]
  1. ReviewCreateView 정의
...
from .forms import ReviewForm
...
class ReviewCreateView(CreateView):
    model = Review
    form_class = ReviewForm
    template_name = "coplate/review_form.html"
  1. Create 페이지
{% extends "coplate_base/base_with_navbar.html" %}

{% load widget_tweaks %}

{% block title %}새 포스트 작성 | Coplate{% endblock title %}

{% block content %}
<main class="site-body">
  <form class="review-form max-content-width" 
				method="post" 
				autocomplete="off" 
				**enctype="multipart/form-data"> #파일 업로드가 있으면 필수**
    {% csrf_token %}
    <div class="title">
      {{ form.title|add_class:'cp-input'|attr:"placeholder:제목"}}
      {% for error in form.title.errors %}
        <div class="error-message">{{ error }}</div>
      {% endfor %}
    </div>
    <div class="restaurant-name">
      {{ form.restaurant_name|add_class:'cp-input'|attr:"placeholder:음식점 이름" }}
      {% for error in form.restaurant_name.errors %}
        <div class="error-message">{{ error }}</div>
      {% endfor %}
    </div>
    <div class="restaurant-link">
      {{ form.restaurant_link|add_class:'cp-input'|attr:"placeholder:네이버 또는 카카오 플레이스 주소" }}
      {% for error in form.restaurant_link.errors %}
        <div class="error-message">{{ error }}</div>
      {% endfor %}
    </div>
    <div class="rating">
      <div class="cp-stars">
        **{% for radio in form.rating %}
            {{ radio }}
        {% endfor %} #다음 코드 주목**
      </div>
    </div>
    <div class="content">
      {{ form.content|add_class:'cp-input'|attr:"placeholder:리뷰를 작성해 주세요." }}
      {% for error in form.content.errors %}
        <div class="error-message">{{ error }}</div>
      {% endfor %}
    </div>
    <div class="file">
      <div class="file-content">
        <div>
          {{ form.image1 }}
          {% for error in form.image1.errors %}
              <div class="error-message">error</div>
          {% endfor %}
        </div>
      </div>
    </div>
    <div class="file">
      <div class="file-content">
        <div>
          {{ form.image2 }}
          {% for error in form.image2.errors %}
              <div class="error-message">error</div>
          {% endfor %}
        </div>
      </div>
    </div>
    <div class="file">
      <div class="file-content">
        <div>
          {{ form.image3 }}
          {% for error in form.image3.errors %}
              <div class="error-message">error</div>
          {% endfor %}        </div>
      </div>
    </div>

    <div class="buttons">
      <a class="cp-button secondary cancel" href="{% url 'index' %}">취소</a>
      <button class="cp-button submit" type="submit">완료</button>
    </div>
  </form>
</main>
{% endblock content %}
  1. rating 라디오 버튼 설정
#models.py
...
class Review(models.Model):
    ...
    RATING_CHOICES = [
        (1, '★'), #첫번째 값: 모델 필드에 실제로 들어갈 값 (모델에 저장), 두번째 값: 디스플레이에 사용되는 값 (화면에 보임)
        (2, '★★'), 
        (3, '★★★'),
        (4, '★★★★'), 
        (5, '★★★★★'),
    ]
    rating = models.IntegerField(choices=RATING_CHOICES, **default=None**)
...
  • default=None
    • 맨 위 선택하지 않은 상태 값을 없앰
#forms.py
class ReviewForm(forms.ModelForm):
    class Meta:
        ...
        **widgets={
            "rating": forms.RadioSelect, #초이스가 라디오버튼으로 렌더됨
        }**

CreateView의 form_valid 메소드 오버라이딩

  • form_valid 메소드: 입력받은 데이터가 유효할 때 데이터로 채워진 모델 오브젝트를 만들고 오브젝트를 저장하는 역할
    • 우리가 할 것 → 이 폼에 author 데이터를 추가해서 author 데이터도 Review 모델에 같이 저장되게 할 것
#views.py
class ReviewCreateView(CreateView):
    model = Review
    form_class = ReviewForm
    template_name = "coplate/review_form.html"

    **def form_valid(self, form):
        form.instance.author = self.request.user 
											#뷰에서 현재 유저에 접근할 때는 request.user 사용
				return super().form_valid(form) #상위 클래스의 form_valid 메소드 불러오기
		
		def get_success_url(self):
        return reverse("review-detal", kwargs={"review_id": self.object.id})**

Review 수정 페이지 구현

  1. urls.py에 수정페이지 연결
...
urlpatterns = [
    ...
    path("reviews/<int:review_id>/edit/", 
					views.ReviewUpdateView.as_view(), 
					name='review-update')
]
  1. 수정 기능 추가에 맞게 review_form.html 수정
#review_form.html
...
#글 처음 작성할 때랑 수정할 때 페이지 제목 다르게
{% block title %}
    {% if review %}
      {{review.title}}
    {% else %}
      새 포스트 작성 
    {% endif %} | Coplate
{% endblock title %}
...
#이미지가 이미 존재하는 경우 (수정하면 그럴 수도) 이미지 preview
			<div class="file-content">
        {% if review.image2 %}
          <img src="{{review.image2.url}}">
        {% endif %}
        <div>
          {{ form.image2 }}
          {% for error in form.image2.errors %}
              <div class="error-message">error</div>
          {% endfor %}
        </div>
      </div>
...
<div class="buttons">
      <a 
        class="cp-button secondary cancel" 
        href="{% if review %}{% url 'review-detail' review.id %}{% else %}{% url 'index' %}{% endif %}">
        취소</a>
      <button class="cp-button submit" type="submit">완료</button>
</div>

리뷰 삭제 페이지 구현

  1. urls.py에 삭제 페이지 연결
...
urlpatterns = [
    ...
    path("reviews/<int:review_id/delete/", views.ReviewDeleteView.as_view(), name = 'review-delete'),
]
  1. ReviewDeleteView 정의
#views.py
class ReviewDeleteView(DeleteView):
    model = Review
    template_name = 'coplate/review_confirm_delete.html'
    pk_url_kwarg = 'review_id'

    def get_success_url(self):
        return reverse("index")
  1. review_confirm_delete.html 구현

접근 제어

뷰 접근 제어하기: Decorato와 Mixin

웹사이트 제어: 로그인을 해야 리뷰 작성 가능, 내가 작성한 리뷰만 수정/삭제 가능

→ 유저가 특정 웹페이지에 들어가는 걸 제안

  • 뷰 접근을 제어하기 위해서는:
    • 함수형 뷰는 decorator
    • 클래스형 뷰는 mixin

리퀘스트 처리 과정

  1. 어떤 URL에 대해서 리퀘스트가 들어옴
  2. URL Dispatcher: 장고는 그 URL을 보고 리퀘스틀 올바른 뷰로 보내줌
  3. 리퀘스트가 뷰에 도착하기 전에 Mixin을 거침
    1. Mixin을 통과하면 뷰는 정해진 로직을 통해서 리퀘스트를 처리하고 리스폰스를 돌려줌
    2. Mixin을 통과하지 못하면 redirect

django-braces: Access Mixin(접근 제어에 관련된 Mixin들) 제공

  • LoginRequiredMixin: 뷰에 접근하려면 로그인을 필요로 하는 Mixin
    #views.py
    ...
    from braces.views import LoginRequiredMixin
    ...
    class ReviewCreateView(LoginRequiredMixin, CreateView): 
    ...
    #settings.py
    LOGIN_REDIRECT_URL = "index"
    LOGIN_URL = "account_login"
    • LOGIN_REDIRECT_URL: 로그인을 하면 어떤 페이지로 리디렉트할지
    • LOGIN_URL: 이 웹서비스의 로그인에 대한 URL 설정
      • all-auth가 제공하는 로그인 URL을 사용하면 account_login 설정
      • LoginRequiredMixin은 로그인이 되어있지 않는 상태면 LOGIN_URL에 적혀 있는 페이지로 안내

제어 흐름

Untitled

UserPassesTestMixin

#views.py 
from braces.views import LoginRequiredMixin, **UserPassesTestMixin
from allauth.account.models import EmailAddress**
...
class ReviewCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    ...
    def test_func(self, user):
				return EmailAddress.objects.filter(user=user, verified=True).exists()
  • test_func
    • parameter: self, user
    • 유저가 뷰에 접근할 수 있는지 boolean 값으로 리턴
  • EmailAddress
    • 사용자들의 이메일 주소들이 저장되어 있는 모델
  • EmailAddress.objects.filter(user=user, verified=True).exists()
    • user=user
      • 현재 사용자에 등록이 되어있음
    • verified=True
      • 인증이 완료된 주소

redirect_unauthenticated_users: 뷰 접근이 차단된 유저 중에, 로그인이 되어있는 유저와 로그인이 되어있지 않은 유저를 다르게 처리할 것인지

  • True일 경우: 로그인이 안된 유저는 로그인 페이지로, 로그인된 유저는 raise_exception에 따라 처리됨
  • False일 경우: 로그인이 안된 유저 & 된 유저 모두 raise_exception에 따라 처리됨
#urls.py
...
urlpatterns = [
    ...
    #allauth
    path("email-confirmation-required/",
     TemplateView.as_view(template_name="account/email_confirmation_required.html"),
     name="account_email_confirmation_required"),
    ...
]
...
#functions.py
from django.shortcuts import redirect
from allauth.account.utils import send_email_confirmation

def confirmation_required_redirect(self, request):
    send_email_confirmation(request, request.user) #장고 기능
    return redirect('account_eamil_confirmation_required')
#views.py
...
from coplate.functions import confirmation_required_redirect
...
class ReviewCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    ...
    redirect_unauthenticated_users = True
    raise_exception = confirmation_required_redirect
    ...
    def test_func(self, user):
        return EmailAddress.objects.filter(user=user, verified=True).exists()
  1. LoginRequiredMixin를 통과하면 UserPassesTestMixin을 거쳐 ReviewCreateView의 로직 수행
  2. 로그인이 되어있지 않으면 raise_exception을 따름

본인이 작성한 리뷰에 대해서만 권한 부여

본인이 작성한 리뷰일 때만 삭제/수정 버튼 보이게 하기

#review_detail.html
...
			{% if review.author == user %} 
        <div class="buttons">
          <a class="cp-button warn" href="{% url 'review-delete' review.id %}">삭제</a>
          <a class="cp-button secondary" href="{% url 'review-update' review.id %}">수정</a>
        </div>
      {% endif %}
...
  • template에서는 현재 로그인된 유저를 user로 부를 수 있음

url을 통해 edit/delete 등에 접근하는 경우 차단

...
class ReviewUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
		...
		raise_exception = True
		redirect_unauthenticated_users = False #default가 False라서 필수 아님
		...
		def test_func(self, user):
        review = self.get_object()
        return review.author == user
  • raise_exception = True
    • 4XX 에러 페이지 발생

AccessMixin

Mixin: 파이썬의 일반적인 개념으로 기존의 클래스에 어떤 기능을 더해줄 때 사용 (뷰 클래스에 접근 제어 기능 더해주기 등)

  • django-braces(링크 추카 패키지)는 다양한 MIxin 제공: LoginRequiredMixin, UserPassesTestMixin 등

LoginRequiredMixin: 로그인이 돼있는 유저만 뷰에 접근할 수 있게

  • 로그인 여부를 확인하는 로직이 뷰 로직보다 먼저 실행돼야 하기 때문에 제네릭 뷰 왼쪽에 씀
    • class MyView(LoginRequiredMixin, CreateView):
  • 로그인이 안 돼있으면 로그인 페이지(settings 파일의 LOGIN_URL에 해당하는 URL)로 리디렉트
  • 로그인을 한 후에는 원래 가려고 하던 페이지로 또다시 리디렉트

UserPassesTestMixin: (로그인 여부가 아닌) 어떤 특정 조건을 충족하는지 확인하고 싶을 때

  • UserPassesTestMixin은 우리가 정의하는 커스텀 테스트 (test_func)를 통과하는 유저만 뷰에 접근할 수 있게 해줌
  • 따로 쓸 수도 있지만, 보통 유저가 로그인이 돼있는지를 먼저 확인하기 때문에 LoginRequiredMixin
    과 같이 씀
    - class MyView(LoginRequiredMixin, UserPassesTestMixin, CreateView):

test_func: 뷰에 접근할 수 있으면 True, 없으면 False 리턴

뷰에 접근하지 못한 유저들 처리하기

  1. redirect_unauthenticated_users: 뷰에 접근하지 못하는 유저들 중 로그인 돼있는 유저와 로그인이 안돼있는 유저를 다르게 처리할 것인지 결정
    • True일 경우: 로그인이 안돼있는 유저는 로그인 페이지로 리디렉트, 로그인 돼있는 유저는 raise_exception 속성 값에 따라 처리
    • False일 경우: 로그인 돼있는 유저, 안 돼있는 유저 모두 raise_exception 속성의 값에 따라 처리
  2. raise_exception
    • 가장 흔히 사용되는 값: True, 커스텀 함수 등
    • True일 경우: 유저가 뷰에 접근할 수 없을 경우 403 Forbidden (권한 없음, 금지됨) 오류가 남
      • 커스텀 함수로 설정: 함수 실행
        • 커스텀 함수는 self와 request를 파라미터로 받아야 함

Decorators

Decorator: 함수형 뷰에 사용하는 접근 제어

  • django.contrib.auth가 제공

login_required (decorator 종류): LoginRequiredMixin과 비슷한 역할

  • import 후 decorator를 뷰 위에 달아주면 됨
    #views.py
    from django.contrib.auth.decorators import login_required
    
    **@login_required**
    def my_view(request):
    	 ...
    	#로그인이 안 돼있는데 my_view에 접근하려고 하면 로그인 페이지로 리디렉트
  • URL 패턴 정의에도 사용 가능
    #urls.py
    from django.contrib.auth.decorators import login_required
    
    urlpatterns = [
        path('my/url/', **login_required(my_view)**, name='my_url'),
        path('your/url/', **login_required(YourView.as_view())**, name='your_url'),
    ]
    • YourView는 클래스형 뷰지만 .as_view() 메소드를 적용해 주면 decorator를 사용 가능

user_passes_test (decorator 종류): UserPassesTestMixin과 비슷함

  • 뷰에 접근하지 못하는 유저를 처리하는 로직 customize 불가
  • 뷰에 접근하지 못하는 유저를 어떤 Url로 리디렉트하는 로직만 구현 가능

유저 프로필

프로필 정보 추가하기

프로필: 유저를 나타내는 다양한 정보 (유저닉네임 등)

User Model에 프로필 사진 추가

#models.py
class User(AbstractUser):
    ...
    profile_pic = models.ImageField(
        default="default_profile_pic.jpg", 
        upload_to="profile_pics"
    )

    intro = models.CharField(max_length=60, blank=True)
		...

User Model의 추가 필드는 기본적으로 admin 페이지에 안 나오기 때문에 admin 페이지에 추가해줘야 함

#admin.py
...
UserAdmin.fieldsets += (("Custom fields", 
					{"fields": ("nickname","profile_pic", "intro")}),
)
...

프로필 페이지 구현하기

  1. urls.py에 프로필 페이지 연결
#urls.py
...
urlpatterns = [
		...
		path("users/<int:user_id>/", views.ProfileView.as_view(), name="profile"),
]
  1. views.py에 ProfileView 정의
class ProfileView(DetailView):
    model = User
    template_name = "coplate/profile.html"
    pk_url_kwarg = "user_id"
    context_object_name = "profile_user"
		
		def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.kwargs.get("user_id")
        context["user_reviews"] = Review.objects.filter(author__id=user_id).order_by("-dt_created")[:4]
				return context
  • context_object_name = "profile_user"
    • template에서 user는 현재 유저를 뜻하기 때문에 충돌을 막기 위해 context_object_name 정의
  • def get_context_data(self, **kwargs):
    • 유저의 리뷰들을 템플릿에 전달되는 context에 추가하기
    • 추가 데이터를 넣기 위해 get_context_data를 오버라이딩 한 것
  • user_id
    • user_id는 URL에서부터 가져올 수 있음
    • URL로 전달되는 파라미터는 self.kwargs로 접근 가능
  1. profile.html 구현

유저 리뷰 목록 페이지 구현하기

  1. urls.py에 유저 리뷰 목록 페이지 연결하기
#urls.py
...
urlpatterns = [
    ...    
		path("users/<int:user_id>/reviews/", 
					views.UserReviewListView.as_view(), 
					name = "user-review-list"),
]
  1. UserReviewListView 구현하기
#views.py
from django.shortcuts import render, **get_object_or_404**
...
class UserReviewListView(ListView):
    model = Review
    template_name = "coplate/user_review_list.html"
    context_object_name = "user_reviews"
    paginate_by = 4

    def get_queryset(self):
        user_id = self.kwargs.get("user_id")
        return Review.objects.filter(author__id=user_id).order_by("-dt_created")

		def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["profile_user"] = get_object_or_404(User, id=self.kwargs.get("user_id"))
        return context
  • get_queryset(self)
    • ListView가 전달하는 object를 바꾸고 싶을 때 오버라이딩하면 되는 메소드
    • get_queryse 메소드를 오버라이드하면 IndexView처럼 ordering 속성을 사용할 수 없음
  • get_object_or_404()
    • 찾는 유저가 없으면 404 에러 발생
    • parameter: 찾을 모델, 찾을 조건
  1. user_review_list.html 구현하기
  • 유저의 전체 리뷰 수 확인하기: {{ paginator.count }}

프로필 설정 페이지 구현하기

  1. 프로필 설정 페이지를 urls.py에 연결하기
...
urlpatterns = [
    #reviews
    ...
    #profile urls
    ...    
		path("set-profile/", views.ProfileSetView.as_view(), name="profile-set"),
]
  1. views.py에 ProfileSetView 구현하기
from .forms import ReviewForm, **ProfileForm**
...
class ProfileSetView(UpdateView):
    model = User
    form_class = ProfileForm
    template_name = "coplate/profile_set_form.html"

		def get_object(self, queryset=None):
        return self.request.user
		
		def get_success_url(self):
        return reverse('index')
  • get_object 메소드
    • UpdateView는 어떤 오브젝트를 업데이트할 것인지 알아야 함
      • 보통은 UpdateView로 오브젝트 아이디가 URL에 전달되고, 그걸 pk_url_kwarg을 통해 설정
      • 하지만 이 경우에는 user_id가 전달되지 않고, 오브젝트는 현재 로그인되어 있는 유저에 해당
    • get_object 메소드를 오버라이딩하여 UpdateView에 어떤 오브젝트를 업데이트할 것인지 알림
  1. forms.py에 ProfileForm 구현하기
class ProfileForm(forms.ModelForm):
    class Meta:
        model = User
        fields = [
            "nickname", 
            "profile_pic", 
            "intro",
        ]
        widgets = {
            "intro":forms.Textarea, #textinput보다 넓은 input인 textarea 활용 위함
        }
  1. profile_set_form.html 구현
<form method="post" enctype="multipart/form-data" autocomplete="off">
  • enctype=”multipart/form-data”
    • 파일 업로드가 있어서
  • autocomplete=”off”

Middleware

Untitled

  • 프로필 설정 완료가 되지 않은 유저를 항상 프로필 설정 페이지로 안내하기 위해서는 Middleware를 사용해야 함

Middleware: 리퀘스트와 리스폰스 cycle을 제어해줌

Untitled

  • 뷰 컴포넌트 전체에 적용
    • 뷰 하나에만 적용되는 Mixin과는 다름
  • 모든 리퀘스트는 URL Dispatcher에 도착하기도 전에 항상 Middleware를 먼저 통과해야 함
  • 다양한 로직을 넣을 수 있음
  • Mixin과 비슷하게 리퀘스트를 다음 컴포넌트로 전달하거나 바로 어떤 리스폰스를 돌려줄 수 있음
  • 리퀘스트를 다음 컴포넌트에 그대로 전달하는 것이 아니라 리퀘스트를 조금 프로세싱한 다음에 전달할 수 있음 (예) 리퀘스트에 정보를 추가하거나 리퀘스트 정보를 수정할 수 있음
  • 뷰에서 리턴되는 리스폰스 모두 Middleware를 거침
    • 리퀘스트를 프로세싱하는 것처럼 리스폰스도 프로세싱 가능
  • settings.py의 MIDDLEWARE에서 확인 가능
    • ‘django.contrib.auth.middleware.AuthenticationMiddleware’: 모든 request에 현재 user 정보 추가
      • request가 AuthenticationMiddleware를 지날 때 request에 user 정보 추가
    • Middleware가 여러 개인 경우에:
      • 리퀘스트는 위에서 아래로 통과
      • 리스폰스는 아래에서 위로 통과
      • 중간에 다음 Middleware로 넘어가는 대신 어떤 리스폰스가 리턴될 수도

로그인이 돼있음 + 프로필 작성 X + 프로필 설정 페이지가 아닌 다른 페이지로 request 보냄 → 프로필 설정 페이지로 리디렉트

  1. coplate app 안에 middleware.py 구현
from django.urls import reverse
from django.shortcuts import redirect

class ProfileSetupMiddleware:
    def __init__(self, get_response):
        self.get_response = get_respones
        # One-time configuration and initialization
    
    def __call__(self, request):
        if (
            request.user.is_authenticated and
            not request.user.nickname and
            request.path_info != reverse("profile-set") 
        ):
            return redirect("profile-set")
        
        response = self.get_response(request)

        #Code to be executed for each request/response after the view is called

        return response
  • request.path_info != reverse(’profile-set’)
    • user가 프로필 설정 페이지가 아닌 다른 페이지로 리퀘스트를 보냈는지 확인
  1. settings.py의 Middleware 파트에 연결
...
MIDDLEWARE = [
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
    "coplate.middleware.ProfileSetupMiddleware",
]
  • ProfileSetupMiddleware는 AuthenticationMiddleware 다음에 와야 함
    • 왜냐하면 ProfileSetupMiddleware는 request.user를 사용하는데 그 유저는 AuthenticationMiddleware를 거쳐야지만 사용할 수 있어서

프로필 수정 페이지 구현하기

  1. 프로필 수정 페이지를 urls.py에 연결하기
#urls.py
from django.urls import path
from . import views

urlpatterns = [
    #reviews
    ...
    #profile urls
    ...
		path("edit-profile/", views.ProfileUpdateView.as_view(), name="profile-update"),
]
  1. views.py에 ProfileUpdateView 구현하기
#views.py
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
    model = User
    form_class = ProfileForm
    template_name = "coplate/profile_update_form.html"

    def get_object(self, queryset=None):
        return self.request.user

    def get_success_url(self):
        return reverse("profile", kwargs={"user_id": self.request.user.id})
  1. profile_update_form.html 구현하기
  • 지금 조회하는 유저가 로그인되어 있는 유저인지 확인
{% if profile_user == user%}

Generic View 정리

get_success_url 메소드: POST 리퀘스트가 성공적으로 처리됐을 때 리디렉트되는 URL 설정

  • POST 리퀘스트를 처리하는 CreateView, UpdateView, DeleteView에서 사용
  • URL 경로를 직접 입력하지 않고 URL 네임을 사용하는 것이 좋음
  • 뷰가 다루고 있는 오브젝트는 self.object로 접근할 수 있음
# URL 파라미터가 없는 경우
class MyView(CreateView):
    ...

    def get_success_url(self):
        return reverse('index')

# URL 파라미터가 있는 경우
class MyView(CreateView):
    ...

    def get_success_url(self):
        return reverse('review-detail', kwargs={'review_id': self.object.id})

form_valid 메소드: 폼 데이터가 모두 유효하면 폼 데이터를 새로운 오브젝트 (CreateView), 또는 기존의 오브젝트(UpdateView)에 저장해주는 역할

  • 폼을 사용하는 CreateView, UpdateView에서 사용
  • 폼 데이터가 저장되게 전에 어떤 액션을 취하고 싶다면 form_valid 메소드를 오버라이딩하면 됨
# 폼 데이터가 오브젝트에 저장되기 전에, author 필드를 만들어 준다
class MyView(CreateView):
    ...

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

get_context_data 메소드: 템플릿에 전달되는 컨텍스트를 정해주는 메소드

  • 모든 제네릭 뷰에서 사용
  • 이 메소드를 오버라이딩해서 컨텍스트에 데이터를 추가
class MyView(DetailView):
    ...

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # context 딕셔너리에 데이터 추가
        # 예: context['foo'] = 'bar'
        return context
  • super()를 활용해서 원래 전달되는 컨텍스트를 가져오고, 컨텍스트에 데이터를 추가해준 다음 컨텍스트 리턴

get_queryset 메소드:

  • ListView에서 보여줄 오브젝트 리스트 설정
  • 기본적으로 model에 해당하는 모든 오브젝트 리턴
  • 바꿔주고 싶으면 오버라이딩
class MyView(ListView):
    model = Foo
    ...

    def get_queryset(self):
        return Foo.objects.filter(<조건>)

get_object 메소드: DetailView, UpdateView, DeleteView에서 다루는 오브젝트 설정

  • CreateView는 기존의 오브젝트를 다루는 것이 아니라 새로운 오브젝트를 생성하기 때문에 이 메소드 사용 X
  • 기본적으로 pk_url_kwarg 파라미터로 전달되는 id값을 가진 오브젝트를 리턴해주는데, 이걸 바꾸고 싶으면 이 메소드를 오버라이딩

Untitled

프로젝트 배포

이메일 전송 제대로 하기

이메일 전송 로직

Untitled

  • SMTP 서버: 이메일 서버 중에도 이메일 전송만 다루는 서버
    • Simple Mail Transfer Protocol: 이메일 전송할 때 사용되는 프로토콜 (규약)
  • 이메일을 보내는 클라이언트가 e-mail이 아니라 장고 앱이더라도 로직은 똑같음
    • 다만 SMTP 서버를 따로 설정해 줘야 함
  1. 구글 - myaccount.google.com - 보안 탭 - 2단계 인증 활성화 - 그 밑의 앱 비밀번호 클릭 및 생성
    • 장고 어플리케이션에서는 앱 비밀번호를 사용해서 구글 계정에서 로그인할 수 있음
  2. settings.py의 EMAIL_BACKEND 수정 (console → smtp) 및 세팅 추가
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "becooq81@gmail.com"
EMAIL_HOST_PASSWORD = "tpdqjzgbuwomwytd"
  • EMAIL_HOST = "smtp.gmail.com"
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True
    - 지메일 SMTP 서버를 사용하기 위한 세팅
  • EMAIL_HOST_USER = "becooq81@gmail.com"
    EMAIL_HOST_PASSWORD = "tpdqjzgbuwomwytd"
    - 이메일 인증

Production

Production: 웹 어플리케이션이 배포된 후 실제로 운영되고 있는 상태

배포 과정

  1. pythonanywhere에 프로젝트.zip 업로드
  2. Open Bash console here (오른쪽 상단) 클릭
    1. ls
    2. unzip coplate_project.zip
  3. 뒤로가기 - Directories - coplate_project/ - settings.py 열기
    1. DEBUG = False (line 26)
    2. ALLOWED_HOSTS = [’.pythonanywhere.com’]
  4. 오른쪽 상단 바 클릭 - Consoles - 우클릭 오픈
    1. 콘솔 오픈
    2. virtualenv —python=python3.9 django_coplate_env
    3. ls
    4. source django_coplate_env/bin/activate
    5. 가상환경 적용된 환경이 됨
    6. 필요한 패키지 설치
      1. pip install django==2.2 django-allauth django-widget-tweaks pillow django-braces
  5. 오른쪽 상단 바 클릭 - Web - 우클릭 오픈
    1. add new web app 클릭
      1. 주어진 도메인 쓰기 (업그레이드 필요)
    2. Next - Manual Configuration - Python 3.7
    3. Code 섹션의 Source code 경로 설정
      1. /home/{id}/coplate_project/
    4. Code 섹션의 wsgi 설정
profile
우당탕탕

0개의 댓글