Package, 패키지: 여러 파이썬 파일의 모음이자 파이썬 파일들을 담고 있는 디렉토리
App, 앱
Package vs. 앱
django.contrib.auth
django-allauth
django.contrib.auth vs. django-allauth
django-allauth는 django.contrib.auth가 제공하지 않는 아래의 기능 제공
django-allauth는 유저기능이 모두 완성되어 있어 짧은 코드 몇줄만 수정; django.contrib.auth는 필요한 부분을 직접 구현해야 함
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',
]
django.contrib.auth의 models.py는 아래를 제공함:
유저 정의하기
#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"
Migrate하기
#terminal
>>> python manage.py makemigrations
>>> python manage.py migrate
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)**
superuser 만들기
>>> python manage.py createsuperuser
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'
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")),
]
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"
<!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"
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**
창을 닫아도 로그인 유지
#settings.py
ACCOUNT_SESSION_REMEMBER = True
로그인 세션 시간 설정
#settings.py
SESSION_COOKIE_AGE = 3600
사용자가 직접 로그아웃하면 세션이 삭제되지만 세션 시간 만료로 인한 로그아웃은 세션이 쌓임
>>> 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하기 전에 아래 두가지 중 한가지 방법 실행
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 필드 추가
회원가입 페이지에 닉네임 필드 추가
coplate app에 forms.py 생성
SignupForm 클래스 생성
#forms.py
from django import forms
from .models import User
class SignupForm(form.ModelForm):
class Meta:
model = User
fields = ["nickname"]
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()**
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자 이상의 영문 대/소문자, 숫자, 특수문자 조합을 입력해 주세요."
settings.py에 validator 설정
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "coplate.validators.CustomPasswordValidator"
},
]
form에 대한 오류가 나도 입력했던 비밀번호를 폼에 그대로 채워넣어줌
#settings.py
...
ACCOUNT_PASSWORD_INPUT_RENDER_VALUE = True
: 가입한 이메일이 본인이 실제로 사용하는 이메일인지 확인하는 절차
이메일 인증 여부 결정하기
#settings.py
...
ACCOUNT_EMAIL_VERIFICATION = "optional"
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
비밀번호 변경 추가하기
#index.html
...
{% if user.is_authenticated %}
<p>{{user}}님의 닉네임은 {{user.nickname}}</p>
**<p><a href="{% url 'account_change_password' %}">비밀번호 변경</a></p>**
{% else %}
<p>로그아웃된 상태입니다.</p>
{% endif %}
비밀번호 변경 성공시 페이지 변경: 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")
urls.py에 설정
#urls.py
...
urls = [
...
path('password/change/', CustomPasswordChangeView.as_view(),
name="account_password_change"),
]
pip install django-widget-tweaks
#settings.py
INSTALLED_APPS = [
...
'widget_tweaks',
...
]
#signup.html
...
{% load widget_tweaks %}
...
(ex) {{ form.email|attr: "class:cp-input"|attr:"placeholder:이메일"}}
(ex) {{ form.email|add_class:"cp-input"|attr:"placeholder:이메일"}}
{{ 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 %}**
<input type="email" name="email" placeholder="이메일" autocomplete="email" class="cp-input" required id="id_email">
type: 필드에 들어가는 데이터 유형
name: 폼 데이터가 서버로 전송될때 사용되는 이름
placeholder: HTML 폼 필드 안에 디스플레이되는 텍스트
maxlength: 입력받는 값의 최대 길이 제한
autocomplete: HTML 폼 필드에 값을 입력할 때, 과거에 비슷한 필드에 입력했던 값을 제안해주는 기능
class: 디자인을 위한 속성
required: HTML 폼 필드를 비워놓을 수 없게 하는 속성
id
: 폼 전체에 대한 오류 설정
{% for error in form.non_field_errors %}
<div class="form-error error-message">{{error}}</div>
{% endfor %}
allauth는 email 내용이 오버라이딩 되더라도 제목에 자동으로 도메인을 추가하기 때문에 이를 없애기 위해 settings.py에 설정
ACCOUNT_EMAIL_SUBJECT_PREFIX = ""
allauth의 템플릿 파일을 오버라이드하려면 allauth의 템플릿 파일과 똑같은 이름을 가진 파일을 app_name/teampltes/account 폴더에 넣어야 함.
settings.py 파일의 INSTALLED_APPS 목록에서 app_name은 allauth보다 위에 와야함
페이지 | 템플릿 이름 | 필드 |
---|
| 회원가입
’account_signup’ | signup.html | - 유저네임: {{ form.username }}
allauth URL: 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 폼
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()
ACCOUNT_SIGNUP_FORM_CLASS
설정#settings.py
ACCOUNT_SIGNUP_FORM_CLASS = 'app.forms.SignupForm'
디자인: 템플릿 오버라이딩
뷰 오버라이딩
app/views.py에서 뷰 정의
class CustomPasswordChangeView(PasswordChangeView):
def get_success_url(self):
return reverse('index')
오버라이딩한 뷰를 사용하기 위해서 URL 패턴 변경
#project/urls.py
urlpatterns = [
...
path('password/change/', CustomPasswordChangeView.as_view(), name='account_password_change'),
path('', include('allauth.urls')),
]
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
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: 웹 어플리케이션에서 바뀌지 않는 파일
STATIC_URL = 'static/'
설정미디어 파일, Media File: 사용자가 직접 업로드하는 파일 (삭제 될수도)
#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)
업로드한 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
blank
null과 blank 사용 방법
# 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가 여러개 있을 수 있는데, 빈 값을 모두 “”로 저장하면 “”가 중복됨 → 데이터베이스에서 중복 오류
# 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: 주어진 객체들과 정보를 가지고 필요한 장고 모델을 설계하는 것
#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='지니')
#shell
python manage.py loaddata reviews.json
urlpatterns = [
path("", views.**IndexView.as_view()**, name="index")
]
...
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"] #내림차순
...
<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일" }}
{% empty %}
상세페이지 구현
#urls.py
...
urlpatterns = [
...
path("reviews/<int:revew_id>/",
views.ReviewDetailView.as_view(),
name="review-detail"),
]
#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"**
...
<a href="{% url 'review-detail' review.id %}">
...
...
<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' %}">< 목록으로</a>
<div class="buttons">
<a class="cp-button warn" href="#">삭제</a>
<a class="cp-button secondary" href="#">수정</a>
</div>
</div>
...
리뷰 작성 페이지 구현
...
urlpatterns = [
...
path("reviews/new/", views.ReviewCreateView.as_view(), name='review-create')
]
...
from .models import User, Review
...
class ReviewForm(forms.ModelForm):
class Meta:
model = Review
fields = [
"title",
"restaurant_name",
"restaurant_link",
"rating",
"image1",
"image2",
"image3",
"content",
]
...
from .forms import ReviewForm
...
class ReviewCreateView(CreateView):
model = Review
form_class = ReviewForm
template_name = "coplate/review_form.html"
{% 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 %}
#models.py
...
class Review(models.Model):
...
RATING_CHOICES = [
(1, '★'), #첫번째 값: 모델 필드에 실제로 들어갈 값 (모델에 저장), 두번째 값: 디스플레이에 사용되는 값 (화면에 보임)
(2, '★★'),
(3, '★★★'),
(4, '★★★★'),
(5, '★★★★★'),
]
rating = models.IntegerField(choices=RATING_CHOICES, **default=None**)
...
#forms.py
class ReviewForm(forms.ModelForm):
class Meta:
...
**widgets={
"rating": forms.RadioSelect, #초이스가 라디오버튼으로 렌더됨
}**
CreateView의 form_valid 메소드 오버라이딩
#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 수정 페이지 구현
...
urlpatterns = [
...
path("reviews/<int:review_id>/edit/",
views.ReviewUpdateView.as_view(),
name='review-update')
]
#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>
리뷰 삭제 페이지 구현
...
urlpatterns = [
...
path("reviews/<int:review_id/delete/", views.ReviewDeleteView.as_view(), name = 'review-delete'),
]
#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")
웹사이트 제어: 로그인을 해야 리뷰 작성 가능, 내가 작성한 리뷰만 수정/삭제 가능
→ 유저가 특정 웹페이지에 들어가는 걸 제안
리퀘스트 처리 과정
django-braces: Access Mixin(접근 제어에 관련된 Mixin들) 제공
#views.py
...
from braces.views import LoginRequiredMixin
...
class ReviewCreateView(LoginRequiredMixin, CreateView):
...
#settings.py
LOGIN_REDIRECT_URL = "index"
LOGIN_URL = "account_login"
제어 흐름
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()
redirect_unauthenticated_users: 뷰 접근이 차단된 유저 중에, 로그인이 되어있는 유저와 로그인이 되어있지 않은 유저를 다르게 처리할 것인지
#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()
본인이 작성한 리뷰일 때만 삭제/수정 버튼 보이게 하기
#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 %}
...
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
Mixin: 파이썬의 일반적인 개념으로 기존의 클래스에 어떤 기능을 더해줄 때 사용 (뷰 클래스에 접근 제어 기능 더해주기 등)
LoginRequiredMixin: 로그인이 돼있는 유저만 뷰에 접근할 수 있게
LOGIN_URL
에 해당하는 URL)로 리디렉트UserPassesTestMixin: (로그인 여부가 아닌) 어떤 특정 조건을 충족하는지 확인하고 싶을 때
UserPassesTestMixin
은 우리가 정의하는 커스텀 테스트 (test_func
)를 통과하는 유저만 뷰에 접근할 수 있게 해줌LoginRequiredMixin
test_func: 뷰에 접근할 수 있으면 True, 없으면 False 리턴
뷰에 접근하지 못한 유저들 처리하기
Decorator: 함수형 뷰에 사용하는 접근 제어
login_required (decorator 종류): LoginRequiredMixin과 비슷한 역할
#views.py
from django.contrib.auth.decorators import login_required
**@login_required**
def my_view(request):
...
#로그인이 안 돼있는데 my_view에 접근하려고 하면 로그인 페이지로 리디렉트
#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과 비슷함
프로필: 유저를 나타내는 다양한 정보 (유저닉네임 등)
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")}),
)
...
#urls.py
...
urlpatterns = [
...
path("users/<int:user_id>/", views.ProfileView.as_view(), name="profile"),
]
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
#urls.py
...
urlpatterns = [
...
path("users/<int:user_id>/reviews/",
views.UserReviewListView.as_view(),
name = "user-review-list"),
]
#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_queryse
메소드를 오버라이드하면 IndexView
처럼 ordering
속성을 사용할 수 없음...
urlpatterns = [
#reviews
...
#profile urls
...
path("set-profile/", views.ProfileSetView.as_view(), name="profile-set"),
]
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')
class ProfileForm(forms.ModelForm):
class Meta:
model = User
fields = [
"nickname",
"profile_pic",
"intro",
]
widgets = {
"intro":forms.Textarea, #textinput보다 넓은 input인 textarea 활용 위함
}
<form method="post" enctype="multipart/form-data" autocomplete="off">
Middleware: 리퀘스트와 리스폰스 cycle을 제어해줌
로그인이 돼있음 + 프로필 작성 X + 프로필 설정 페이지가 아닌 다른 페이지로 request 보냄 → 프로필 설정 페이지로 리디렉트
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
...
MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
"coplate.middleware.ProfileSetupMiddleware",
]
#urls.py
from django.urls import path
from . import views
urlpatterns = [
#reviews
...
#profile urls
...
path("edit-profile/", views.ProfileUpdateView.as_view(), name="profile-update"),
]
#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})
{% if profile_user == user%}
get_success_url 메소드: POST 리퀘스트가 성공적으로 처리됐을 때 리디렉트되는 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)에 저장해주는 역할
# 폼 데이터가 오브젝트에 저장되기 전에, 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
get_queryset 메소드:
class MyView(ListView):
model = Foo
...
def get_queryset(self):
return Foo.objects.filter(<조건>)
get_object 메소드: DetailView, UpdateView, DeleteView에서 다루는 오브젝트 설정
이메일 전송 로직
#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"
Production: 웹 어플리케이션이 배포된 후 실제로 운영되고 있는 상태
배포 과정