はじめに
今回は、ビューの基本的な部分が集約されている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
、テンプレート名
、context
、template_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は、TemplateResponseMixin
、ContextMixin
、View
クラスを継承し、主に静的なページのレンダリングに便利です。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_string
g使われます。
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'),
]