Django Class Based views (3)
CBV를 좀 더 자세하게 알아보자
제네릭 뷰 중 Display View에 대해 공부 !
Generic Display View
-
객체의 목록 또는 하나의 객체 상세 정보를 보여주는 뷰
-
DetailView와 ListView로 구성된다.
-
DetailView
- 조건에 맞는 하나의 객체를 출력한다
1 2 3 4 5 6
class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView): """ Render a "detail" view of an object. By default this is a model instance looked up from `self.queryset`, but the view will support display of *any* object by overriding `self.get_object()`. """
-
아무런 내용 없이 SingleObjectTemplateResponseMixin과 BaseDetailView 두개의 클래스를 상속받는다
-
BaseDetailView
1 2 3 4 5 6
class BaseDetailView(SingleObjectMixin, View): """A base view for displaying a single object.""" def get(self, request, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) return self.render_to_response(context)
-
get요청에 대해서 db로부터 하나의 object를 가져와 이를 context에 담아 rendering해준다
-
BaseDetailView는 SingleObjectMixin의 상속을 받는다
-
SingleObjectMixin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
class SingleObjectMixin(ContextMixin): """ Provide the ability to retrieve a single object for further manipulation. """ model = None queryset = None slug_field = 'slug' context_object_name = None slug_url_kwarg = 'slug' pk_url_kwarg = 'pk' query_pk_and_slug = False def get_object(self, queryset=None): """ Return the object the view is displaying. Require `self.queryset` and a `pk` or `slug` argument in the URLconf. Subclasses can override this to return any object. """ # Use a custom queryset if provided; this is required for subclasses # like DateDetailView if queryset is None: queryset = self.get_queryset() # Next, try looking up by primary key. pk = self.kwargs.get(self.pk_url_kwarg) slug = self.kwargs.get(self.slug_url_kwarg) if pk is not None: queryset = queryset.filter(pk=pk) # Next, try looking up by slug. if slug is not None and (pk is None or self.query_pk_and_slug): slug_field = self.get_slug_field() queryset = queryset.filter(**{slug_field: slug}) # If none of those are defined, it's an error. if pk is None and slug is None: raise AttributeError( "Generic detail view %s must be called with either an object " "pk or a slug in the URLconf." % self.__class__.__name__ ) try: # Get the single item from the filtered queryset obj = queryset.get() except queryset.model.DoesNotExist: raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}) return obj def get_queryset(self): """ Return the `QuerySet` that will be used to look up the object. This method is called by the default implementation of get_object() and may not be called if get_object() is overridden. """ if self.queryset is None: if self.model: return self.model._default_manager.all() else: raise ImproperlyConfigured( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % { 'cls': self.__class__.__name__ } ) return self.queryset.all() def get_slug_field(self): """Get the name of a slug field to be used to look up by slug.""" return self.slug_field def get_context_object_name(self, obj): """Get the name to use for the object.""" if self.context_object_name: return self.context_object_name elif isinstance(obj, models.Model): return obj._meta.model_name else: return None def get_context_data(self, **kwargs): """Insert the single object into the context dict.""" context = {} if self.object: context['object'] = self.object context_object_name = self.get_context_object_name(self.object) if context_object_name: context[context_object_name] = self.object context.update(kwargs) return super().get_context_data(**context)
slug란
URL의 구성요소로 웹사이트의 특정 페이지를 사람이 읽기 쉬운 형식의 식별자로 바꿔주는 것
-
클래스 변수
-
pk_url_kwarg, slug_url_kwarg
- 두 변수들은 default로 pk, slug로 지정되어있다
- 얘네는 urls.py에서 넘겨주는 인자의 이름을 뜻한다
1 2 3 4 5 6 7 8 9
# urls.py urlpatterns = [ path('<int:id>', views.post_detail, name="post_detail"), ] # views.py post_detail = DetailView.as_view(model=Post, pk_url_kwarg='id')
- 위 처럼 id로 넘겨주려면 해당 값을 id로 수정해준다
-
-
get_object
- 모델로부터 object를 얻어오는 메소드
- 위에서 지정한 클래스 변수들에 대해서 queryset, pk, slug를 설정해서 원하는 queryset을 만들어주는 역할
- 이 메소드를 통해 객체를 가져오고, 실패할 경우 에러를 표시
-
get_queryset
- 클래스 변수 queryset이 지정되어 있으면 그 queryset에 맞게 객체를 return
- 기본적으로는 모델의 모든 객체들을 전부 반환한다
-
get_context_data
- 모델의 이름으로 context를 넘겨주는 역할
- 모델 이름 외에도 object라는 이름으로도 사용이 가능
-
SingleObjectTemplateResponseMixin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
class SingleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_field = None template_name_suffix = '_detail' def get_template_names(self): """ Return a list of template names to be used for the request. May not be called if render_to_response() is overridden. Return the following list: * the value of ``template_name`` on the view (if provided) * the contents of the ``template_name_field`` field on the object instance that the view is operating upon (if available) * ``<app_label>/<model_name><template_name_suffix>.html`` """ try: names = super().get_template_names() except ImproperlyConfigured: # If template_name isn't specified, it's not a problem -- # we just start with an empty list. names = [] # If self.template_name_field is set, grab the value of the field # of that name from the object; this is the most specific template # name, if given. if self.object and self.template_name_field: name = getattr(self.object, self.template_name_field, None) if name: names.insert(0, name) # The least-specific option is the default <app>/<model>_detail.html; # only use this if the object in question is a model. if isinstance(self.object, models.Model): object_meta = self.object._meta names.append("%s/%s%s.html" % ( object_meta.app_label, object_meta.model_name, self.template_name_suffix )) elif getattr(self, 'model', None) is not None and issubclass(self.model, models.Model): names.append("%s/%s%s.html" % ( self.model._meta.app_label, self.model._meta.model_name, self.template_name_suffix )) # If we still haven't managed to find any template names, we should # re-raise the ImproperlyConfigured to alert the user. if not names: raise return names
- template_name값을 따로 설정한다면 이에 맞는 templates를 반환
- 형태는
앱이름/모델이름(소문자)_detail.html
-
사용 예시
1
2
3
4
5
6
7
8
# modesl.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=20)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
1
2
3
4
5
6
7
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('<int:pk>', views.post_detail, name="post_detail"),
]
1
2
3
4
5
# views.py
from django.views.generic import DetailView
from .models import Post
post_detail = DetailView.as_view(model=Post)
1
2
3
4
5
6
<!-- prac/post_detail.html -->
<h1>detail page</h1>
<h2>{{post.title}}</h2>
<h3>{{post.content}}</h3>
-
ListView
- 조건에 맞는 객체 목록 출력
1 2 3 4 5
class ListView(MultipleObjectTemplateResponseMixin, BaseListView): """ Render some list of objects, set by `self.model` or `self.queryset`. `self.queryset` can actually be any iterable of items, not just a queryset. """
-
DetailView처럼 내용 없이 ListView도 MultipleObjectTemplateResponseMixin과 BaseListView를 상속받는다
-
BaseListView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
class BaseListView(MultipleObjectMixin, View): """A base view for displaying a list of objects.""" def get(self, request, *args, **kwargs): self.object_list = self.get_queryset() allow_empty = self.get_allow_empty() if not allow_empty: # When pagination is enabled and object_list is a queryset, # it's better to do a cheap query than to load the unpaginated # queryset in memory. if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'): is_empty = not self.object_list.exists() else: is_empty = not self.object_list if is_empty: raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") % { 'class_name': self.__class__.__name__, }) context = self.get_context_data() return self.render_to_response(context)
- get요청을 받아 queryset으로 object_list를 만들고 rendering해준다
- MultipleObjectMixin을 상속받는다
-
MultipleObjectMixin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
class MultipleObjectMixin(ContextMixin): """A mixin for views manipulating multiple objects.""" allow_empty = True queryset = None model = None paginate_by = None paginate_orphans = 0 context_object_name = None paginator_class = Paginator page_kwarg = 'page' ordering = None def get_queryset(self): """ Return the list of items for this view. The return value must be an iterable and may be an instance of `QuerySet` in which case `QuerySet` specific behavior will be enabled. """ if self.queryset is not None: queryset = self.queryset if isinstance(queryset, QuerySet): queryset = queryset.all() elif self.model is not None: queryset = self.model._default_manager.all() else: raise ImproperlyConfigured( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % { 'cls': self.__class__.__name__ } ) ordering = self.get_ordering() if ordering: if isinstance(ordering, str): ordering = (ordering,) queryset = queryset.order_by(*ordering) return queryset def get_ordering(self): """Return the field or fields to use for ordering the queryset.""" return self.ordering def paginate_queryset(self, queryset, page_size): """Paginate the queryset, if needed.""" paginator = self.get_paginator( queryset, page_size, orphans=self.get_paginate_orphans(), allow_empty_first_page=self.get_allow_empty()) page_kwarg = self.page_kwarg page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 try: page_number = int(page) except ValueError: if page == 'last': page_number = paginator.num_pages else: raise Http404(_("Page is not 'last', nor can it be converted to an int.")) try: page = paginator.page(page_number) return (paginator, page, page.object_list, page.has_other_pages()) except InvalidPage as e: raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { 'page_number': page_number, 'message': str(e) }) def get_paginate_by(self, queryset): """ Get the number of items to paginate by, or ``None`` for no pagination. """ return self.paginate_by def get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs): """Return an instance of the paginator for this view.""" return self.paginator_class( queryset, per_page, orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs) def get_paginate_orphans(self): """ Return the maximum number of orphans extend the last page by when paginating. """ return self.paginate_orphans def get_allow_empty(self): """ Return ``True`` if the view should display empty lists and ``False`` if a 404 should be raised instead. """ return self.allow_empty def get_context_object_name(self, object_list): """Get the name of the item to be used in the context.""" if self.context_object_name: return self.context_object_name elif hasattr(object_list, 'model'): return '%s_list' % object_list.model._meta.model_name else: return None def get_context_data(self, *, object_list=None, **kwargs): """Get the context for this view.""" queryset = object_list if object_list is not None else self.object_list page_size = self.get_paginate_by(queryset) context_object_name = self.get_context_object_name(queryset) if page_size: paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size) context = { 'paginator': paginator, 'page_obj': page, 'is_paginated': is_paginated, 'object_list': queryset } else: context = { 'paginator': None, 'page_obj': None, 'is_paginated': False, 'object_list': queryset } if context_object_name is not None: context[context_object_name] = queryset context.update(kwargs) return super().get_context_data(**context)
- 내부적으로 pagination이 구현이 되어있다
- get_context_data
- context를 얻는 과정, pagination을 할 경우 queryset도 부분적으로 나눠서 가져와야 하기 때문에 바뀌는 부분은 if문을 통해 처리되어있다
-
사용 예시
1
2
3
4
5
6
7
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('home/', views.post_list, name="post_list")
]
1
2
3
4
5
6
7
from django.views.generic import ListView
from .models import Post
post_list = ListView.as_view(
model=Post,
paginate_by=2,
)
- 위와 같이 pagenate_by로 페이지네이션을 쉽게 구현할 수 있다
- 값을 2로 준다면 한 페이지에 두 개의 객체만 표시하게 된다
- 각 페이지는 url 뒤에 querystring으로 ?page=페이지수로 접근할 수 있다
참고사이트