수학과의 좌충우돌 프로그래밍

[Django] CBV (2) Generic display views 본문

웹프로그래밍/Django

[Django] CBV (2) Generic display views

ssung.k 2019. 8. 6. 08:35

Generic display views 에는 DetailViewListView 가 있습니다.

DetailView 는 각 상세 페이지를 나타내는데 있어서 ListView 는 전체적인 페이지를 나타내는데 있어서 굉장히 효과적입니다. 두 클래스를 알아보도록 합시다.

DetailView

  • 실습

    전체적인 코드를 살펴보기 전에 어떻게 사용되고 있는지 예시를 먼저 보도록 하겠습니다.

    DetailView 의 역할은 각 상세페이지를 보여주는 역할을 합니다. 간단히 Post 모델을 정의하고 이 상세페이지를 보여주도록 하겠습니다.

    # models.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)
    

     

    # urls.py
    from django.urls import path
    from . import views
    
    urlpatterns = [
        path('<int:pk>', views.post_detail, name="post_detail"),
    ]
    

     

    <!-- core/post_detail.html -->
    
    <h1>글의 상세 페이지</h1>
    
    <h2>{{ post.title }}</h2>
    <div>{{post.content}}</div>
    

     

    # views.py
    
    from django.views.generic import DetailView
    from .models import Post
    
    post_detail = DetailView.as_view(model=Post)
    

 

  • DetailView 살펴보기

    전체적은 코드를 살펴보며 위의 구현을 이해해보도록 하겠습니다.

    • DetailView
    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()`.
        """
    

    DetailView 는 아무 내용도 없이 SingleObjectTemplateResponseMixin,BaseDetailView 두 개의 클래스를 상속받고 있습니다.

    • BaseDetailView
    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
    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)
    

    가장 위에 여러 클래스 변수들이 지정이 되어있습니다.

     

    • pk_url_kwarg , slug_url_kwarg

      두 변수에 대해서 이미 기본값으로 pk, slug 로 지정이 되어있습니다. 이는 urls 에서 넘겨주는 인자의 이름을 뜻합니다. 즉 아래와 같이 id 로 넘겨주려면 해당 값을 id 로 바꿔야 합니다.

      # 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')
      
    • get_object

      모델로 부터 object 를 얻어오는 메소드입니다. 위에서 지정한 클래스 변수들에 대해서 queryset, pk, slug 을 설정하여 원하는 queryset 을 만들어줍니다. 이를 통해 모델에서 객체를 가져오고 실패할 경우 에러를 표시합니다.

    • get_queryset

      기본적으로는 모델의 모든 객체들을 전부 반환하지만 위에서 qureyset 이 지정이 되어 있다면 그 queryset 에 맞게 객체를 return 합니다.

    • get_context_data

      위의 예시의 경우에는 Post 모델에 대해서 context 를 따로 넘겨주지 않았지만 templates 에서 post 를 사용하였습니다.이렇게 모델의 이름으로 context 를 넘겨주는 역할을 합니다. 모델의 이름 외에도 object 라는 이름으로도 사용이 가능합니다.

     

    • SingleObjectTemplateResponseMixin

      전체적으로 중요하지 않은 코드가 많아서 따로 코드를 첨부하지는 않겠습니다. template_name 값을 따로 설정한다면 이에 맞는 templates 를 반환해주지만 따로 설정을 하지 않는다면 default 값이 있습니다.

      앱이름/모델이름(소문자)_detail.html 와 같은 꼴을 가집니다. 위 예시에서 앱은 core , Post 모델 이므로 core/post_detail.html 이 됩니다.

       

 

http://127.0.0.1:8000/home/?page=2

ListView

ListView 는 1개의 모델에 대해서 list 템플릿 처리를 해줍니다. SNS의 경우에 main 화면에서 여러 Post 들을 띄우기 위해서 사용한다고 생각하면 됩니다. 또한 ListView 의 장점은 자동으로 페이징 처리를 지원해줌으로서 편리하게 사용할 수 있습니다. 또한 모델이름(소문자)_list 의 queryset 을 templates 에 전달합니다. 여기서는 post_list 가 됩니다.

  • 실습

    # urls.py
    from django.urls import path
    from . import views
    
    urlpatterns = [
    		path('home/', views.post_list, name="post_list"),
    ]
    

     

    <!-- core/post_list.html -->
    
    <h1>list 페이지</h1>
    
    {% for post in post_list %}
        <div>{{post.title}}</div>
        <div>{{post.content}}</div>
    {% endfor %}
    

     

    # views.py
    
    from django.views.generic import ListView
    from .models import Post
    
    post_list = ListView.as_view(
        	model=Post,
        	paginate_by=2,
    		)
    

    다음과 같이 paginate_by 로 페이지네이션을 쉽게 구현 할 수 있습니다. 다음과 같이 값을 2로 준다면 한 페이지의 두 개의 객체만 표시하게 됩니다. 각 페이지는 url 뒤에 querystring으로 ?page=페이지수 로 접근할 수 있습니다.

 

  • ListView 살펴보기

    ListView 는 어떻게 구성되어 있는지 전체적인 코드를 살펴보도록 하겠습니다.

    • ListView
    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 에서 처럼 MultipleObjectTemplateResponseMixinBaseListView 를 상속받습니다.

    • BaseListView
    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 를 만들어줍니다.

    • MultipleObjectMixin
    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 문을 통한 분기로 구현이 되어있습니다.

    아래 링크를 통해 pagination 에 대해 더 알아보실 수 있습니다.

    pagination 더 알아보기

 

[Django] 11. Pagination 을 알아보자

안녕하세요 강민성입니다. 이번 시간에도 마찬가지로 blog project를 이어서 진행해보도록 하겠습니다. 글을 계속 추가할수록 글은 아래로 쌓이게 됩니다. 하지만 대부분의 웹사이트는 일정 수준 이상 쌓이면 다..

ssungkang.tistory.com

 

Comments