Constructor. Called in the URLconf; can contain helpful extra
+keyword arguments, and other things.
+
37
+38
+39
+40
+41
+42
+43
+44
+45
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.
+ forkey,valueinkwargs.items():
+ setattr(self,key,value)
+
@classonlymethod
+defas_view(cls,**initkwargs):
+ """Main entry point for a request-response process."""
+ forkeyininitkwargs:
+ ifkeyincls.http_method_names:
+ raiseTypeError(
+ 'The method name %s is not accepted as a keyword argument '
+ 'to %s().'%(key,cls.__name__)
+ )
+ ifnothasattr(cls,key):
+ raiseTypeError("%s() received an invalid keyword %r. as_view "
+ "only accepts arguments that are already "
+ "attributes of the class."%(cls.__name__,key))
+ defview(request,*args,**kwargs):
+ self=cls(**initkwargs)
+ self.setup(request,*args,**kwargs)
+ ifnothasattr(self,'request'):
+ raiseAttributeError(
+ "%s instance has no 'request' attribute. Did you override "
+ "setup() and forget to call super()?"%cls.__name__
+ )
+ returnself.dispatch(request,*args,**kwargs)
+ view.view_class=cls
+ view.view_initkwargs=initkwargs
+ # __name__ and __qualname__ are intentionally left unchanged as
+ # view_class should be used to robustly determine the name of the view
+ # instead.
+ view.__doc__=cls.__doc__
+ view.__module__=cls.__module__
+ view.__annotations__=cls.dispatch.__annotations__
+ # Copy possible attributes set by decorators, e.g. @csrf_exempt, from
+ # the dispatch method.
+ view.__dict__.update(cls.dispatch.__dict__)
+ returnview
+
defdispatch(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.
+ ifrequest.method.lower()inself.http_method_names:
+ handler=getattr(self,request.method.lower(),self.http_method_not_allowed)
+ else:
+ handler=self.http_method_not_allowed
+ returnhandler(request,*args,**kwargs)
+
defform_invalid(self,form):
+ """If the form is invalid, render the invalid form."""
+ returnself.render_to_response(self.get_context_data(form=form))
+
Handle GET requests: instantiate a blank version of the form.
+
133
+134
+135
defget(self,request,*args,**kwargs):
+ """Handle GET requests: instantiate a blank version of the form."""
+ returnself.render_to_response(self.get_context_data())
+
defget_context_data(self,**kwargs):
+ """Insert the form into the context dict."""
+ if'form'notinkwargs:
+ kwargs['form']=self.get_form()
+ returnsuper().get_context_data(**kwargs)
+
+
+
+
+
+
+
+
+
+
+
SingleObjectMixin
+
+
+
+
Insert the single object into the context dict.
+
91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
defget_context_data(self,**kwargs):
+ """Insert the single object into the context dict."""
+ context={}
+ ifself.object:
+ context['object']=self.object
+ context_object_name=self.get_context_object_name(self.object)
+ ifcontext_object_name:
+ context[context_object_name]=self.object
+ context.update(kwargs)
+ returnsuper().get_context_data(**context)
+
defget_context_object_name(self,obj):
+ """Get the name to use for the object."""
+ ifself.context_object_name:
+ returnself.context_object_name
+ elifisinstance(obj,models.Model):
+ returnobj._meta.model_name
+ else:
+ returnNone
+
Return an instance of the form to be used in this view.
+
31
+32
+33
+34
+35
defget_form(self,form_class=None):
+ """Return an instance of the form to be used in this view."""
+ ifform_classisNone:
+ form_class=self.get_form_class()
+ returnform_class(**self.get_form_kwargs())
+
defget_form_class(self):
+ """Return the form class to use in this view."""
+ ifself.fieldsisnotNoneandself.form_class:
+ raiseImproperlyConfigured(
+ "Specifying both 'fields' and 'form_class' is not permitted."
+ )
+ ifself.form_class:
+ returnself.form_class
+ else:
+ ifself.modelisnotNone:
+ # If a model has been explicitly provided, use it
+ model=self.model
+ elifgetattr(self,'object',None)isnotNone:
+ # If this view is operating on a single object, use
+ # the class of that object
+ model=self.object.__class__
+ else:
+ # Try to get a queryset and extract the model class
+ # from that
+ model=self.get_queryset().model
+ ifself.fieldsisNone:
+ raiseImproperlyConfigured(
+ "Using ModelFormMixin (base class of %s) without "
+ "the 'fields' attribute is prohibited."%self.__class__.__name__
+ )
+ returnmodel_forms.modelform_factory(model,fields=self.fields)
+
+
+
+
+
+
+
+
+
+
+
FormMixin
+
+
+
+
Return the form class to use.
+
27
+28
+29
defget_form_class(self):
+ """Return the form class to use."""
+ returnself.form_class
+
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.
defget_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
+ ifquerysetisNone:
+ 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)
+ ifpkisnotNone:
+ queryset=queryset.filter(pk=pk)
+ # Next, try looking up by slug.
+ ifslugisnotNoneand(pkisNoneorself.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.
+ ifpkisNoneandslugisNone:
+ raiseAttributeError(
+ "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()
+ exceptqueryset.model.DoesNotExist:
+ raiseHttp404(_("No %(verbose_name)s found matching the query")%
+ {'verbose_name':queryset.model._meta.verbose_name})
+ returnobj
+
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.
defget_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.
+ """
+ ifself.querysetisNone:
+ ifself.model:
+ returnself.model._default_manager.all()
+ else:
+ raiseImproperlyConfigured(
+ "%(cls)s is missing a QuerySet. Define "
+ "%(cls)s.model, %(cls)s.queryset, or override "
+ "%(cls)s.get_queryset()."%{
+ 'cls':self.__class__.__name__
+ }
+ )
+ returnself.queryset.all()
+
defget_success_url(self):
+ """Return the URL to redirect to after processing a valid form."""
+ ifself.success_url:
+ url=self.success_url.format(**self.object.__dict__)
+ else:
+ try:
+ url=self.object.get_absolute_url()
+ exceptAttributeError:
+ raiseImproperlyConfigured(
+ "No URL to redirect to. Either provide a url or define"
+ " a get_absolute_url method on the Model.")
+ returnurl
+
+
+
+
+
+
+
+
+
+
+
FormMixin
+
+
+
+
Return the URL to redirect to after processing a valid form.
+
51
+52
+53
+54
+55
defget_success_url(self):
+ """Return the URL to redirect to after processing a valid form."""
+ ifnotself.success_url:
+ raiseImproperlyConfigured("No URL to redirect to. Provide a success_url.")
+ returnstr(self.success_url)# success_url may be lazy
+
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``
defget_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()
+ exceptImproperlyConfigured:
+ # 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.
+ ifself.objectandself.template_name_field:
+ name=getattr(self.object,self.template_name_field,None)
+ ifname:
+ 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.
+ ifisinstance(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
+ ))
+ elifgetattr(self,'model',None)isnotNoneandissubclass(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.
+ ifnotnames:
+ raise
+ returnnames
+
+
+
+
+
+
+
+
+
+
+
TemplateResponseMixin
+
+
+
+
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.
defget_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.
+ """
+ ifself.template_nameisNone:
+ raiseImproperlyConfigured(
+ "TemplateResponseMixin requires either a definition of "
+ "'template_name' or an implementation of 'get_template_names()'")
+ else:
+ return[self.template_name]
+
Handle POST requests: instantiate a form instance with the passed
+POST variables and then check if it's valid.
+
137
+138
+139
+140
+141
+142
+143
+144
+145
+146
defpost(self,request,*args,**kwargs):
+ """
+ Handle POST requests: instantiate a form instance with the passed
+ POST variables and then check if it's valid.
+ """
+ form=self.get_form()
+ ifform.is_valid():
+ returnself.form_valid(form)
+ else:
+ returnself.form_invalid(form)
+
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.
defrender_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)
+ returnself.response_class(
+ request=self.request,
+ template=self.get_template_names(),
+ context=context,
+ using=self.template_engine,
+ **response_kwargs
+ )
+
defsetup(self,request,*args,**kwargs):
+ """Initialize attributes shared by all view methods."""
+ ifhasattr(self,'get')andnothasattr(self,'head'):
+ self.head=self.get
+ self.request=request
+ self.args=args
+ self.kwargs=kwargs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_models.py b/tests/test_models.py
index 6e9280f3..0287c9f1 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,4 +1,8 @@
import pytest
+from django.core.management import call_command
+from django.views.generic import UpdateView
+
+from cbv.models import Klass
from .factories import InheritanceFactory, KlassFactory
@@ -53,3 +57,26 @@ def test_diamond(self) -> None:
mro = d.get_all_ancestors()
assert mro == [b, c, a]
+
+ def test_real(self) -> None:
+ """
+ Test the MRO of a real class hierarchy, taken from the Django's UpdateView.
+ """
+ # The version of Django that we are using for this test is 3.1
+ # because that is the version we have in our dependencies when we run tests.
+ # If this fails in future, it is probably because the version of Django
+ # has been updated and the MRO of the UpdateView has changed.
+ # In that case, feel free to update the version we're loading here.
+ call_command("loaddata", "3.1.json")
+
+ mro = Klass.objects.get(name="UpdateView").get_all_ancestors()
+
+ # For the sake of comparison, we convert the Klass objects to a tuple of strings,
+ # where the first element is the module name and the second is the class name.
+ mro_strings = [(k.module.name, k.name) for k in mro]
+
+ # The first element is the class itself, so we skip it.
+ # The last element is object, so we skip it.
+ real_ancestors = UpdateView.__mro__[1:][:-1]
+
+ assert mro_strings == [(c.__module__, c.__name__) for c in real_ancestors]
diff --git a/tests/test_page_snapshots.py b/tests/test_page_snapshots.py
index c0d49f18..4278a33a 100644
--- a/tests/test_page_snapshots.py
+++ b/tests/test_page_snapshots.py
@@ -43,6 +43,18 @@
},
),
),
+ (
+ "updateview.html",
+ 33,
+ reverse(
+ "klass-detail",
+ kwargs={
+ "version": "4.0",
+ "module": "django.views.generic.edit",
+ "klass": "UpdateView",
+ },
+ ),
+ ),
(
"klass-detail-old.html",
30,