Two Scoops of Django 3.x를 보고 정리한 글입니다.
규칙 1 장고가 제공하는 기본 뷰 클래스는 항상 오른쪽으로 이동한다.
규칙 2 믹스인은 기본 뷰에서부터 왼쪽으로 이동한다.
규칙 3 믹스인은 다른 클래스에서 상속되어서는 안된다. 상속 체인을 간단하게 유지하라.
앞서 제안한 규칙을 적용한 예시이다. FruityFlavorView클래스의 상속 문법을 보자.
규칙1에 의해 TemplateView가 장고에서 제공하는 기본 클래스이기에 오른쪽으로 이동했다
규칙2에 의해 믹스인은 왼쪽으로 이동했다.
from django.views.generic import TemplateView # 장고가 제공하는 기본 뷰 클래스
class FreshFruitMixin:
"""믹스인 클래스"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["has_fresh_fruit"] = True
return context
class FruityFlavorView(FreshFruitMixin, TemplateView):
# 믹스인은 왼쪽에(규칙 1), 장고의 기본 제공 클래스는 오른쪽으로 이동했다. (규칙 2)
template_name = "fruity_flavor.html"
이름 | 목적 | Two Scoops Example |
---|---|---|
View | 어디에서든 이용 가능한 기본 뷰 | '10.6 django.views.generic.View' 이용하기 참고 |
RedirectView | 사용자를 다른 URL로 리다이렉트 | '/log-in/'을 방문한 사용자를 '/login/'으로 보내기 |
TemplateView | 장고 HTML 템플릿을 보여줌 | 사이트의 '/about/' 페이지 |
ListView | 객체의 목록을 보여줌 | 아이스크림 맛 목록 |
DetailView | 객체를 보여줌 | 아이스크림 맛에 대한 세부사항 |
FormView | 폼 전송(제출) | 사이트 연락처 또는 이메일 폼 |
CreateView | 객체를 만들 때 | 새로운 아이스크림 맛을 만들 때 |
UpdateView | 객체를 업데이트 할 때 | 기존 아이스크림을 맛을 업데이트 |
DeleteView | 객체를 삭제할 때 | 기존 아이스크림 맛을 삭제 |
Generic date views | 일정 시간동안 발생한 객체를 보여줌 | 블로그가 일반적으로 이를 이용한다. Two Scoops의 경우 맛이 추가된 시기에 대한 public history를 만들 수 있다 |
저자는 아래 그룹에 대해 첫번째 그룹을 지지하지만 진짜 실전 해답은 없음을 밝힌다
# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
from .models import Flavor
class FlavorDetailView(LoginRequiredMixin, DetailView):
model = Flavor
GCBV 믹스인 순서를 명심하자
- LoginRequiredMixin은 항상 왼쪽에 위치한다.
- 기본 뷰 클래스는 항상 오른쪽에 위치한다.
django.http.HttpResponseRedirect
이여야 한다.from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import CreateView
from .models import Flavor
class FlavorCreateView(LoginRequiredMixin, CreateView):
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']
def form_valid(self, form):
# Do custom logic here
return super().form_valid(form)
form_invalid()
메서드는 장고의 GCBV 워크플로가 요청을 보내는 곳이다.django.http.HttpResponse
를 반환해야 한다.from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
from .models import Flavor
class FlavorCreateView(LoginRequiredMixin, CreateView):
model = Flavor
def form_invalid(self, form):
# Do custom logic here
return super().form_invalid(form)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.functional import cached_property
from django.views.generic import UpdateView, TemplateView
from .models import Flavor
from .tasks import update_user_who_favorited
class FavoriteMixin:
@cached_property
def likes_and_favorites(self):
"""Returns a dictionary of likes and favorites"""
likes = self.object.likes() favorites = self.object.favorites()
return {
"likes": likes,
"favorites": favorites,
"favorites_count": favorites.count(),
}
class FlavorUpdateView(LoginRequiredMixin, FavoriteMixin, UpdateView):
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']
def form_valid(self, form):
update_user_who_favorited(
instance=self.object,
favorites=self.likes_and_favorites['favorites'] # 다른 뷰의 메서드를 호출하고 있다.
)
return super().form_valid(form)
class FlavorDetailView(LoginRequiredMixin, FavoriteMixin, TemplateView):
model = Flavor
flavors/ 앱 탬플릿에서도 해당 프로퍼티(likes_and_favorites)를 호출할 수 있다.
{# flavors/base.html #}
{% extends "base.html" %}
{% block likes_and_favorites %}
<ul>
<li>Likes: {{ view.likes_and_favorites.likes }}</li>
<li>Favorites: {{ view.likes_and_favorites.favorites_count }}</li>
</ul>
{% endblock likes_and_favorites %}
이번 절에서 아이스크림 종류를 기록하는 예제를 통해 폼과 뷰의 사용법을 알아본다.
아이스크림 종류 모델을 정의하였다.
# flavors/models.py
from django.db import models
from django.urls import reverse
class Flavor(models.Model):
class Scoops(models.IntegerChoices):
SCOOPS_0 = 0
SCOOPS_1 = 1
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
scoops_remaining = models.IntegerField(choices=Scoops.choices,
default=Scoops.SCOOPS_0)
def get_absolute_url(self):
return reverse("flavors:detail", kwargs={"slug": self.slug})
가장 단순하고 일반적인 장고 폼 시나리오. 모델을 생성 후 모델에 대한 새로운 레코드를 추가하거나 기존 레코드를 수정한다.
다음 뷰가 있다.
1. FlavorCreateView : 새로운 종류의 아이스크림을 추가하는 폼에 해당한다.
2. FlavorUpdateView : 기존 아이스크림을 수정하는 폼에 해당한다.
3. FlavorDetailView : 아이스크림 추가와 변경 모두에 대한 확인 페이지에 해당한다.
# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
from .models import Flavor
class FlavorCreateView(LoginRequiredMixin, CreateView):
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']
class FlavorUpdateView(LoginRequiredMixin, UpdateView):
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']
class FlavorDetailView(DetailView):
model = Flavor
# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
from .models import Flavor
class FlavorActionMixin:
fields = ['title', 'slug', 'scoops_remaining']
@property
def success_msg(self):
return NotImplemented
def form_valid(self, form):
messages.info(self.request, self.success_msg)
return super().form_valid(form)
class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView):
model = Flavor
success_msg = "Flavor created!"
class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView):
model = Flavor
success_msg = "Flavor updated!"
class FlavorDetailView(DetailView):
model = Flavor
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li id="message_{{ forloop.counter }}"
{% if message.tags %} class="{{ message.tags }}" {% endif %}>
{{ message }}
</li>
{% endfor %} </ul>
{% endif %}
from django.views.generic import ListView
from .models import Flavor
class FlavorListView(ListView):
model = Flavor
def get_queryset(self):
# 부모 get_queryset으로부터 queryset을 petch
queryset = super().get_queryset()
# GET 파라미터를 받는다.
q = self.request.GET.get("q")
if q:
# 필터된 queryset을 반환
return queryset.filter(title__icontains=q)
# 기본 queryset 반환
return queryset
{# templates/flavors/_flavor_search.html #}
{% comment %}
Usage: {% include "flavors/_flavor_search.html" %}
{% endcomment %}
<form action="{% url "flavor_list" %}" method="GET">
<input type="text" name="q" />
<button type="submit">search</button>
</form>
아래와 같은 장점을 갖는다.
1. if 문으로 처리하는 함수 기반 뷰 대체하기
2. get_context_data()와 form_valid() 메서드 뒤에 숨어있는 클래스 기반 뷰에 직접 접근하기
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect
from django.views.generic import View
from .forms import FlavorForm
from .models import Flavor
class FlavorView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
# Flavor 객체의 디스플레이를 처리
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
return render(request,
"flavors/flavor_detail.html",
{"flavor": flavor}
)
def post(self, request, *args, **kwargs):
# Flavor 객체의 업데이트를 처리
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
form = FlavorForm(request.POST, instance=flavor)
if form.is_valid():
form.save()
return redirect("flavors:detail", flavor.slug)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.generic import View
from .models import Flavor
from .reports import make_flavor_pdf
class FlavorPDFView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
# Get the flavor
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
# create the response
response = HttpResponse(content_type='application/pdf')
# generate the PDF stream and attach to the response
response = make_flavor_pdf(response, flavor)
return response