1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DjangoのBaseクラスについて理解する

Last updated at Posted at 2023-10-27

はじめに

今回は、ビューの基本的な部分が集約されているbaseクラスビューについて説明していきます。
ビュークラスの基底クラスであり、なんとなく使っているなら一旦内部の処理を理解するべきかと思います。
そうすれば開発時に柔軟に対応することができます。

全体コードはこちら

ContextMixinクラス

テンプレートにコンテキストデータを提供するためのMixinです。

class ContextMixin:
    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

クラス変数のextra_contextにオブジェクトの辞書型でkeyとvalueを渡すと、テンプレートでその値を使用することができます。(返り値のkwargsに追加しても同様です)

get_context_dataメソッド

引数でキーワード引数を受け取り、viewに格納された各keyにインスタンスをセットしています。
kwargs.setdefault('view', self)では、コンテキストデータにviewというキーが存在しない場合に、そのキーに現在のビューインスタンスをセットしています。
これにより、テンプレート内でviewというキーを使ってビューインスタンスのメソッドや属性にアクセスすることができます。

class MyView(TemplateView):
    template_name = 'my_template.html'

    def is_user_admin(self):
        return self.request.user.is_staff

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_admin'] = self.is_user_admin()
        return context
{% if is_admin %}
  <p>Welcome, admin user!</p>
{% else %}
  <p>Hello, regular user.</p>
{% endif %}

上記の例では、super().get_context_data(**kwargs)でContextMixinクラスのget_context_dataをオーバーライドして、is_adminを追加しています。
テンプレート内でis_adminをif文に使用すると、bool値によって処理を分岐させることができます。

Viewクラス

Webリクエストを処理するための基本的な機能を提供するクラスです。
HTTPリクエストを適切なハンドラメソッド(get, post, put, delete 等)にルーティングするdispatchメソッドを提供します。これにより、ビューはリクエストのHTTPメソッドに基づいて適切な処理を行うことができます。

class View:

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError(
                    'The method name %s is not accepted as a keyword argument '
                    'to %s().' % (key, cls.__name__)
                )
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    def setup(self, request, *args, **kwargs):
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs

    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)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

as_viewメソッド

クラスベースのビューを関数のように扱えるように変換し、URLconf(urls.py)で使用されます。このメソッドにより、クラスベースのビューがURLパターンに組み込まれ、リクエストに応答できるようになります。
例えば、path('my-url/', MyView.as_view(), name='my_view_name') とすると、MyViewクラスが指定されたURLパターンに対応します。

ユーザーが指定したURLと、pathの第一引数(今回だとmy-url/)を順番に比較していき、一致した第二引数を利用するという仕組みになっています。
このURLの遷移を可能にしているのがas_viewメソッドです。

使用例
urlpatterns = [
    path('my-url/', MyView.as_view(), name='my_view_name'),
    path(....),
]

次にコードの説明をいたします。
まず、as_viewメソッドの引数の**initkwargsは、特定のcontextデータを上書きするための値となります。そしてinitkwargsがメソッド名と被っていないかの確認と(被っていればエラー)、引数のkeyがclsに存在しているかを以下で確認します。

@classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError(
                    'The method name %s is not accepted as a keyword argument '
                    'to %s().' % (key, cls.__name__)
                )
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

ちなみに第一引数のclsは、メソッドを呼び出すクラスそのものを指しており、as_viewがクラスメソッドとして定義されています。
例えば、URLパターンにMyView.as_view(template_name="hoge.html")とすると、clsはMyViewになり、initkwargsにはtemplate_nameをキーにhoge.htmlという値が入ります。
具体的には、viewメソッド内のself = cls(**initkwargs))でMyViewクラスの新しいインスタンスを作成するときに、initkwargsを引数にとるので、ここでtemplate_nameがhoge.htmlとなります。
つまり、クラスに新しく使用できる属性が追加されるということです。

使用例
class MyView(View):
    template_name = 'default_template.html'
    page_title = 'Default Page Title' 
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = self.page_title
        return context

    def get(self, request, *args, **kwargs):
        pass

urlpatterns = [
    path('my-view/', MyView.as_view(template_name='hoge.html', page_title='Overridden Page Title'))
]

上記コードの初期値のtemplate_nameはdefault_template.htmlで、page_titleはDefault Page Titleです。as_viewの引数にtemplate_nameとpage_titleを入れることで、as_viewメソッド内のviewメソッドのself = cls(**initkwargs)によってビュークラスのインスタンスが作成される際、initkwargsが __init__メソッドに渡され、クラスの属性が上書きされます。
以下、kwargsにはインスタンス作成時に引数に入れた、template_nameとpage_titleが入っているので、デフォルトで設定している各キーに上書きされます。

属性の上書き
def __init__(self, **kwargs):
    for key, value in kwargs.items():
            setattr(self, key, value)

viewメソッド

def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            if not hasattr(self, 'request'):
                raise AttributeError(
                    "%s instance has no 'request' attribute. Did you override "
                    "setup() and forget to call super()?" % cls.__name__
                )
            return self.dispatch(request, *args, **kwargs)

最終的な返り値として、dispatchメソッドの返り値を返却しています。dispatchメソッドでは、リクエストされたMETHODに対応するviewのメソッドを取得して返却します。
まずviewメソッドの引数について、requestはユーザーのリクエストに関する情報が含まれています。
*args, **kwargsについては、例えばpathの引数がpath('articles/int:year/', ...) の場合、URLが /articles/2022/のような形でアクセスされたとき、kwargs は {'year': 2022} のようになります。
一方、キャプチャグループが名前を持たない正規表現を使用する場合、抽出された値は args に順に格納されます。例としてpathの引数がpath(r'^articles/(\d{4})/(\d{2})/$', ...)の場合、articles/2022/10/のようなURLにマッチしますが、この2022と10がargsに格納されるということです。

self = cls(**initkwargs)は前述しましたが、クラスを初期化して属性の上書きや追加をしています。
self.setupではsetupメソッドを呼び出しており、このメソッドはビューがリクエストを処理するための初期セットアップを行います。setupは初めに呼び出されるメソッドで、これにより、dispatchで使用されるrequestやargs、kwargsをセットしています。
そしてrequestが設定されない場合にはエラーを出力し、そうで無ければdispatchメソッドを実行、リクエストのHTTPメソッド(GET、POSTなど)に基づいて適切なハンドラメソッド(例:get(), post())を呼び出します

最後にクラスベースのビューを関数ベースのビューのように使うため,クラスからview関数にして返却します。

dispatchとhttp_method_not_allowedメソッド

リクエストされたMETHODに対応するviewのメソッドを取得して返却します。
request内にself.http_method_names('get', 'post', 'puts'..)が含まれていれば、getattrでそのHTTPメソッドに対応するメソッドを現在のビュークラスから取得します。
例えば、以下MyViewがGETリクエストを受け取った場合、getattr(self, "get", self.http_method_not_allowed)というコードが実行され、MyViewがgetメソッドを持っているので、handlerにself.getが割り当てられます。
ちなみにgetattr関数は、第一引数に指定されたオブジェクト(self、つまりビュークラスのインスタンス)から、第二引数に指定された名前の属性またはメソッドを取得します。もし指定された名前の属性またはメソッドが存在しない場合は、第三引数の値(ここではself.http_method_not_allowed)が返されます。
PUTを受け取った場合は、MyViewクラスはputメソッドを持っていないため、デフォルトの値としてself.http_method_not_allowedがhandlerに割り当てられます。

class MyView(View):
    def get(self, request, *args, **kwargs):
       ....

http_method_not_allowedでエラーハンドリングを行なっています。

TemplateResponseMixinクラス

クラスベースのビューで、テンプレートをレンダリングするための機能を提供するために使用されます。

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):
        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):
        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]

render_to_responseメソッド

このメソッドは、指定されたテンプレートとコンテキストデータを使用して、TemplateResponseオブジェクトを生成します。
このレスポンスは、最終的なHTMLコンテンツをクライアント(ブラウザ)に送り返すために使用されます。

TemplateResponseクラスは、response_classにインスタンス化しています。
インスタンス化したresponse_classに、requestテンプレート名contexttemplate_engineを指定することで指定されたテンプレートとコンテキストデータをブラウザに送っています。

追加でテンプレートに渡すcontextに情報を追加することもできます。
以下はその例です。

class MyTemplateView(TemplateResponseMixin, View):
    template_name = "my_template.html"

    def get(self, request: HttpRequest, *args, **kwargs):
        context = {
            'message': 'Hello from the class-based view!'
        }
        return self.render_to_response(context)
urlpatterns = [
    path('my-template-view/', MyTemplateView.as_view(), name='my_template_view'),
]

contextにmessageを追加し、継承したTemplateResponseMixinの'''render_to_response'''メソッドの第一引数にcontextを指定することで、テンプレート内でmessageを使用することができます。

get_template_namesメソッド

このメソッドはシンプルにtemplate_name属性から値を取得するメソッドで、未定義の場合エラーを発生させます。

TemplateViewクラス

TemplateViewは、TemplateResponseMixinContextMixinViewクラスを継承し、主に静的なページのレンダリングに便利です。getメソッドがデフォルトで定義されており、コンテキストデータをテンプレートに渡します。template_nameを指定するだけで、GETリクエストに対するHTMLレスポンスを提供できます。

urlpatterns = [
    path('about/', TemplateView.as_view(template_name='about.html'), name='about'),
]

getメソッド

_wrap_url_kwargs_with_deprecation_warning(kwargs)
機能として、URLから渡されるキーワード引数を取得し、それをラップして新しいコンテキストキーワード引数として返します。その際、非推奨の警告を出力するようにしています。
つまり、urlpatternsのpathで指定したURLのキーワード引数を、直接テンプレートのコンテキストとして使用するのは非推奨ということを伝えています。

urlpatterns = [
    path('profile/<username>/', DeprecatedUserProfileView.as_view(), name='deprecated_profile'),
]
class DeprecatedUserProfileView(TemplateView):
    template_name = 'deprecated_user_profile.html'
<h1>Hello, {{ username }}</h1> #非推奨
<h1>Hello, {{ view.kwargs.username }}</h1> #推奨

テンプレート内で直接指定する非推奨の方法と、Viewクラスで返されるviewのkwargsから使用した推奨例です。

RedirectViewクラス

HTTPリダイレクトを処理するために使用されます。

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)

get_redirect_urlメソッド

ここでリダイレクト先のURLを決定しています。
URLが指定されていれば、そのURLにリダイレクトします。pattern_nameが指定されている場合は、DjangoのURLリバース機能を使用してURLを生成します。リダイレクト先のURLが決定できない場合はNoneを返します。

コード説明

まずurlを受け取った場合、文字列フォーマットのプレースホルダが含まれている場合があり、これをkwargsの値で置き換えることで動的なURLを生成します。
urlがなく、pattern_nameがある場合は、その値を使用してリダイレクト先のURLをDjangoのURLリバース機能を使用して動的に生成します。

次に、クエリ文字列が存在し、query_string属性がTrueに設定されている場合、このクエリ文字列をリダイレクト先のURLに追加します。
self.request.METAはDjangoのHttpRequestオブジェクトのメタデータを格納している辞書で、リクエストに関するさまざまな情報が含まれています。そこからgetを使用してQUERY_STRINGキーの値を取得しようとします(URLの ? 以降に含まれるクエリストリングを示します)

以下、pattern_nameを使用してリダイレクト先のURLを指定し、さらにquery_string属性がTrueで、元のリクエストにクエリ文字列が含まれている場合のRedirectViewの使用例

class Article(models.Model):
    title = models.CharField(max_length=200)
urlpatterns = [
    re_path(r'^old-article/(?P<article_id>\d+)/$', RedirectView.as_view(pattern_name='new-article-detail', query_string=True), name='old-article-redirect'),
    path('new-article/<int:article_id>/', views.article_detail, name='new-article-detail'),
]
def article_detail(request, article_id):
    article = get_object_or_404(Article, pk=article_id)
    return render(request, 'article_detail.html', {'article': article})

まず、/old-article/5/?sort=dateというURLにアクセスすると、RedirectViewがマッチします。
pattern_name が new-article-detail に設定されているので、reverse関数を使用してこの名前から対応するURLを取得します。この例では、/new-article/5/というURLが生成されます。
query_string属性がTrueに設定されており、元のリクエストに sort=date というクエリ文字列が含まれているので、このクエリ文字列を新しいURLに追加します。
結果、/new-article/5/?sort=dateというURLにリダイレクトされます。

つまり、元のリクエストのクエリパラメータを保持した状態で新しいURLにアクセスした場合にpattern_nameおよびquery_stringg使われます。

get,head,post,options,delete,put,patchメソッド

getメソッドはget_redirect_urlから取得したURLに基づいてリダイレクト処理を行います。リダイレクトは、permanent属性がTrueであれば永続的なリダイレクト(HttpResponsePermanentRedirect)、Falseであれば一時的なリダイレクト(HttpResponseRedirect)を行います。

永続的とは、リソースが恒久的に新しいURLに移動したことを示し、元のURLが使用されることはないということになります。
一時的とは、リソースが一時的に別の場所に移動していることを示し、元のURLが再び利用される可能性があるということになります。例:ログイン

それぞれのメソッドは、HTTPリクエストに応じて実行されますが、getメソッド以外のメソッドはgetメソッドを呼び出し、同じ動作をするようにオーバーライドされています。
これにより、どのようなHTTPメソッドでリクエストが来ても、同じリダイレクトの動作を保証することができます。

例えば、以下のようにMyRedirectViewを定義して、特定のパスから別のパスへリダイレクトすることができます。
/old-path/へのアクセスは/new-path/にリダイレクトされ、これはGETリクエストだけでなくPOSTリクエストに対しても同様に動作します。

使用例

class MyRedirectView(RedirectView):
    url = '/new-path/'  
urlpatterns = [
    path('old-path/', MyRedirectView.as_view(), name='old-path-redirect'),
]
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?