[Django] Base views / Generic display views / Generic date views

cdwdeยท2021๋…„ 3์›” 31์ผ
0

Django

๋ชฉ๋ก ๋ณด๊ธฐ
7/13

๐ŸŽˆBase Views

์ฝ”๋“œ ๋ณด๊ธฐ

View

  • ์ตœ์ƒ์œ„ ๋ถ€๋ชจ ์ œ๋„ค๋ฆญ ๋ทฐ ํด๋ž˜์Šค

  • ์ง์ ‘ ์“ธ ์ผ์€ ๊ฑฐ์˜ ์—†์ง€๋งŒ ๋‹ค๋ฅธ ํด๋ž˜์Šค๋“ค์„ ์‚ฌ์šฉํ•จ์œผ๋กœ์„œ ๊ฐ„์ ‘์ ์œผ๋กœ ํ•ญ์ƒ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Œ

  • http method๋ณ„๋กœ ์ง€์ • ์ด๋ฆ„์˜ ๋ฉค๋ฒ„ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ๊ตฌํ˜„

  • __init__: as_view์— ์ธ์ž๋ฅผ ๋„˜๊ฒจ์ฃผ๊ฒŒ ๋˜๋ฉด ์ธ์ž๊ฐ€ **kwargs๋กœ ๋„˜์–ด์™€ dict๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ํ•ด๋‹น ์„ค์ •์„ ํ•ด์ฃผ๊ฒŒ ๋จ

def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)
  • as_view: view๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์„œ return

  • dispatch: as_view ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœ
    request method๋ฅผ ํ™•์ธํ•˜๊ณ  http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] ์•ˆ์— ์žˆ๋Š” ์ •์ƒ์ ์ธ method๋ผ๋ฉด request์—์„œ ์ด๋ฅผ ๊ฐ€์ ธ์˜ด
    ์ •์ƒ์ ์ธ method๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด http_method_not_allowed ํ˜ธ์ถœ

def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

TemplateView

class TemplateView(TemplateResponseMixin, ContextMixin, View):
    """
    Render a template. Pass keyword arguments from the URLconf to the context.
    """
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

  • TemplateResponseMixin
    template์˜ ๊ธฐ๋ณธ์ ์ธ ์„ค์ •๋“ค, template ์ด๋ฆ„, renderingํ•˜๋Š” ๊ณผ์ • ๋“ฑ ๋‹ด๋‹น
class TemplateResponseMixin:
    """A mixin that can be used to render a template."""
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        """
        Return a response, using the `response_class` for this view, with a
        template rendered with the given context.
        Pass response_kwargs to the constructor of the response class.
        """
        response_kwargs.setdefault('content_type', self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

    def get_template_names(self):
        """
        Return a list of template names to be used for the request. Must return
        a list. May not be called if render_to_response() is overridden.
        """
        if self.template_name is None:
            raise ImproperlyConfigured(
                "TemplateResponseMixin requires either a definition of "
                "'template_name' or an implementation of 'get_template_names()'")
        else:
            return [self.template_name]

  • ContextMixin
    ์ •์ ์ธ context์— ๋Œ€ํ•ด์„œ๋Š” extra_context ๋ณ€์ˆ˜ ์‚ฌ์šฉ, ๋™์ ์œผ๋กœ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ get_context_data ์‚ฌ์šฉ
class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    extra_context = None

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

์˜ˆ์‹œ

#๋ฐฉ๋ฒ• 1
urlpatterns = [
	path('', TemplateView.as_view(template_name='root.html'), name='root'),
]

#๋ฐฉ๋ฒ• 2

class RootView(TemplateView):
  template_name = 'root.html'

urlpatterns = [
  path('', RootView.as_view(), name='root'),
]

RedirectView

class RedirectView(View):
    """Provide a redirect on any GET request."""
    permanent = False
    url = None
    pattern_name = None
    query_string = False

    def get_redirect_url(self, *args, **kwargs):
        """
        Return the URL redirect to. Keyword arguments from the URL pattern
        match generating the redirect request are provided as kwargs to this
        method.
        """
        if self.url:
            url = self.url % kwargs
        elif self.pattern_name:
            url = reverse(self.pattern_name, args=args, kwargs=kwargs)
        else:
            return None

        args = self.request.META.get('QUERY_STRING', '')
        if args and self.query_string:
            url = "%s?%s" % (url, args)
        return url

    def get(self, request, *args, **kwargs):
        url = self.get_redirect_url(*args, **kwargs)
        if url:
            if self.permanent:
                return HttpResponsePermanentRedirect(url)
            else:
                return HttpResponseRedirect(url)
        else:
            logger.warning(
                'Gone: %s', request.path,
                extra={'status_code': 410, 'request': request}
            )
            return HttpResponseGone()

    def head(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def options(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)
  • parmanet

    • True: 301 ์‘๋‹ต(์˜๊ตฌ์  ์ด๋™)
    • False: 302 ์‘๋‹ต(์ž„์‹œ ์ด๋™) - default
  • url

  • pattern_name: URL Reverse๋ฅผ ์ˆ˜ํ–‰ํ•  ๋ฌธ์ž์—ด

  • query_string: QueryString์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ธธ ๊ฒƒ์ธ์ง€?

  • get: get_redirect_url์„ ํ˜ธ์ถœํ•˜์—ฌ url์„ ์–ป์Œ
    url์„ ์–ป์ง€ ๋ชปํ•˜๋ฉด error ์ถœ๋ ฅ, ์ œ๋Œ€๋กœ ์–ป์—ˆ๋‹ค๋ฉด parmanent์— ์˜ํ•ด ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ

  • get_redirect_url: ํด๋ž˜์Šค ๋ณ€์ˆ˜ url์„ ํ†ตํ•ด ์ง์ ‘ url์„ ๋ฐ›์•„์˜ค๊ฑฐ๋‚˜ pattern_name์„ ํ†ตํ•ด url์˜ name ๊ฐ’์„ ๋ฐ›์•„์™”์œผ๋ฉด ์ด๋ฅผ return

์˜ˆ์‹œ

  • myproject/urls.py
urlpatterns = [
  path('', RedirectView.as_view(pattern_name='instagram:post_list', name='root'),
]
  • instagram/urls.py
app_name = 'instagram'

urlpatterns = [
  path('', views.post_list, name='post_list'),
]

๐ŸŽˆ Generic display views

DetailView

์ฝ”๋“œ ๋ณด๊ธฐ
1๊ฐœ ๋ชจ๋ธ์˜ 1๊ฐœ Object์— ๋Œ€ํ•œ ํ…œํ”Œ๋ฆฟ ์ฒ˜๋ฆฌ
๊ฐ ์ƒ์„ธํŽ˜์ด์ง€ ๋ณด์—ฌ์ฃผ๋Š” ์—ญํ• 

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
    template_name ๊ฐ’์„ ์„ค์ •ํ•˜๋ฉด ์ด์— ๋งž๋Š” templates ๋ฐ˜ํ™˜
    ๋”ฐ๋กœ ์„ค์ •ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ์•ฑ์ด๋ฆ„/๋ชจ๋ธ์ด๋ฆ„_detail.html

  • BaseDetailView
    get ์š”์ฒญ์— ๋Œ€ํ•ด db๋กœ๋ถ€ํ„ฐ ํ•˜๋‚˜์˜ object๋ฅผ ๊ฐ€์ ธ์™€ ์ด๋ฅผ context์— ๋‹ด์•„ rendering

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

    • pk_url_kwarg, slug_url_kwargs: ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ pk, slug๋กœ ์ง€์ •๋˜์–ด ์žˆ์Œ (urls์—์„œ ๋„˜๊ฒจ์ฃผ๋Š” ์ธ์ž ์ด๋ฆ„์ด ๋‹ค๋ฅด๋‹ค๋ฉด ํ•ด๋‹น ๊ฐ’ ๋ณ€๊ฒฝํ•ด์ฃผ๊ธฐ)

    • get_object: ๋ชจ๋ธ๋กœ๋ถ€ํ„ฐ object ์–ป์–ด์˜ด

    • get_queryset: ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋ธ์˜ ๋ชจ๋“  ๊ฐ์ฒด ๋ฐ˜ํ™˜, ์œ„์— queryset ์ง€์ •๋˜์–ด์žˆ์œผ๋ฉด queryset์— ๋งž๊ฒŒ ๊ฐ์ฒด return

    • get_context_data: ๋ชจ๋ธ์˜ ์ด๋ฆ„์œผ๋กœ context๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” ์—ญํ• 

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)

์˜ˆ์‹œ

  • instagram/views.py
class PostDetailView(DetailView):
  model = Post
  
  def get_queryset(self):
    qs = super().get_queryset()
    if not self.request.user.is_authoricated:
      qs = qs.filter(is_public=True)
    return qs


post_detail = PostDetailView.as_view()

ListView

์ฝ”๋“œ ๋ณด๊ธฐ
1๊ฐœ ๋ชจ๋ธ์— ๋Œ€ํ•œ List ํ…œํ”Œ๋ฆฟ ์ฒ˜๋ฆฌ

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.
    """
  • MultipleObjectTemplateResponseMixin
    ๋‚ด๋ถ€์ ์œผ๋กœ pagination์ด ๊ตฌํ˜„๋˜์–ด์žˆ์Œ
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
    """Mixin for responding with a template and list of objects."""
    template_name_suffix = '_list'

    def get_template_names(self):
        """
        Return a list of template names to be used for the request. Must return
        a list. May not be called if render_to_response is overridden.
        """
        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 the list is a queryset, we'll invent a template name based on the
        # app and model name. This name gets put at the end of the template
        # name list so that user-supplied names override the automatically-
        # generated ones.
        if hasattr(self.object_list, 'model'):
            opts = self.object_list.model._meta
            names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix))
        elif not names:
            raise ImproperlyConfigured(
                "%(cls)s requires either a 'template_name' attribute "
                "or a get_queryset() method that returns a QuerySet." % {
                    'cls': self.__class__.__name__,
                }
            )
        return names

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

์˜ˆ์‹œ
django-bootstrap-v5๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ pagination

  • instagram/views.py
post_list = ListView.as_view(model=Post, paginate_by=2)
  • templates/instagram/post_list.html
  {% if is_paginated%}
    {% bootstrap_pagination page_obj size="small" justify_content="center"%}
  {% endif %}


๐ŸŽˆGeneric date views

ArchiveIndexView

๋‚ ์งœ๋ณ„๋กœ ์ตœ์‹  ๊ฐœ์ฒด๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ตœ์ƒ์œ„ ์ธ๋ฑ์Šค ํŽ˜์ด์ง€

์˜ต์…˜

  • model
  • date_field: ์ •๋ ฌ ๊ธฐ์ค€ ํ•„๋“œ
  • date_list_period (๋””ํดํŠธ: year)

context

  • latest: Queryset
  • date_list: ๋“ฑ๋ก๋œ Record์˜ ๋…„๋„ ๋ชฉ๋ก

๋””ํดํŠธ template_name_suffix: _archive

์˜ˆ์‹œ

  • views.py
from django.views.generic import ArchiveIndexView

post_archive = ArchiveIndexView.as_view(model=Post, date_field='post_detail')
  • urls.py
urlpatterns = [
  path('archive/', views.post_archive, name='post_archive'),
]
  • post_archive.html
{{latest}}

{% for date in date_list %}
  {{date.year}}
{% endfor %}

YearArchiveView

์ฃผ์–ด์ง„ ์—ฐ๋„์˜ ๋ชฉ๋ก

์˜ต์…˜

  • model
  • date_field
  • date_list_period (๋””ํดํŠธ: mohth)
  • make_object_list

context

  • year
  • previos_year
  • next_year
  • date_list
  • object_list

๋””ํดํŠธ template_name_suffix: _archive_year


MonthArchiveView

์˜ต์…˜

  • month_format

context

  • month
  • previous_month
  • next_month
  • date_list
  • object_list

๋””ํดํŠธ template_name_suffix: archive_month

์ฐธ๊ณ 
https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-date-based/#django.views.generic.dates.BaseArchiveIndexView
https://ssungkang.tistory.com/entry/Django-CBV-1-CBV-%EC%99%80-Base-Views?category=320582

0๊ฐœ์˜ ๋Œ“๊ธ€