= Class-Based Views = The proposal to add a class-based framework for describing views (#6735) has been around for a while. It's been proposed for inclusion in Django 1.0, 1.1, and 1.2. This is a summary of the state of debate, with the intention of landing a patch for the 1.3 release. == The brief == At present, generic views are defined as methods. If you want to customize them, you have to pass in arguments to the view at time of use in urls.py. This is cumbersome: * urls.py rapidly becomes large an unwieldly * complex behaviors that need to be wrapped in callables depend on the underlying views handling callable arguments (which isn't implemented universally) * There's no such thing as a simple extension -- you can't easily say "use that view, but change one argument"; you have to reproduce the full argument list. * .. and much more Moving to a class-based structure means that the complexities of defining and customizing generic views can be handled by subclassing. urls.py returns to being a simple process of pointing at a class. Extension logic can be arbitrarily complex, abstracted behind methods on the generic class. It also means we can provide an entry point for HTTP method-based dispatch -- e.g., a GET() method to handle GET requests, POST() to handle posts, etc. == The problem == However, the devil is in the detail. There are several issues that any class-based view solution needs to address: * '''Deployment''': How do you deploy an instance of the view * '''URLResolver interaction''': Does the approach require any special handling in URLResolver to identify and/or instantiate the view? * '''Subclassing''': How easy is it to subclass and override a specific method on a view class? * '''Thread safety''': Does each request get a clean instance of self to work with? If so, how is this achieved? * '''Ease of testing''': Does testing the view require any special handling (such as a specialized method to instantiate the view) * '''Ease of decoration''': Does the proposed technique pose any impediments to easily decorating views? === View instantiation and thread safety === The [http://groups.google.com/group/django-developers/browse_thread/thread/27cec79b784ef73f?hl=en1 most recent django-developers discussion on the topic], and [http://groups.google.com/group/django-developers/browse_thread/thread/e23d9a5a58fe589 an older thread] discussed many of the available options on instantiating the views and protecting them from threading issues. Here is a summary of the various approaches that have been proposed. Some examples showing the essence of each of them without the other details: http://bitbucket.org/spookylukey/django-class-views-experiments/src/tip/ ==== Store state on request, not view ==== Pass around the request object via method arguments, store arbitrary state on the request or a special "state" object or dict that is passed around or attached to the request. Document that state should be stored on the request, storing it on the view instance is unsafe. Additionally, override `__setattr__` to make setting state on self raise an error or warning. Alternatively, override `__getattr__/__setattr__` to access `self.request.state` under the hood while on the surface it looks as if the state is stored on the view instance. Example usage and view would be the same as shown below in "`__call__` and copy()". Arguments for: * Avoids messy and non-idiomatic hacks. * Avoids copying or creating new view instance on every request. * All the options for actually protecting against thread-unsafety involve some kind of "surprising" behavior. The surprising behavior here (can't store state on self) is explicit and fails immediately, rather than failing confusingly and only under certain circumstances. Arguments against: * It's unusual to have a class where you can't store state on self while in `__init__`. ==== !__call!__() and copy() ==== [http://github.com/bfirsh/django-class-based-views Implementation] Example usage: {{{ url(r'^detail/author/(?P\d+)/$', views.AuthorDetail(), name="author_detail"), }}} Example class: {{{ class AuthorView(View): def GET(self, request, *args, **kwargs) return self.render_to_response('author_list.html', {'authors': Author.objects.all()}) }}} This approach proposes that an class instance be placed in urls.py; the instance has a `__call__()` method, and when that method is invoked, it takes a shallow copy of the instance defined in urls.py, and returns the result of invoking the request method (e.g., GET()). This achieves thread safety by ensuring that every request is given a clean instance on the view class to work on. No special handling is required in !UrlResolver -- the class instance is a callable, so it appears just like an existing view function. Arguments against: * The "copy on `__call__()`" approach is a little messy. Normal Python idiom wouldn't lead users to expect that `__call__()` would cause a copy to be created. * The abstraction of the copy-on-call can leak in surprising ways. Some users will try to set up state using an `__init__` method (common practice). If any of the state they attach to self in `__init__` is mutable (list, dict, object, etc) and they mutate it in the view, this will fail (but not immediately, or in obvious ways). ==== !__new!__()` ==== [http://github.com/fitzgen/django-class-based-views Implementation] Example usage: {{{ url(r'^detail/author/(?P\d+)/$', views.AuthorDetail, name="author_detail"), }}} Example class: {{{ class AuthorView(View): def GET(self, request, *args, **kwargs) return self.render_to_response('author_list.html', {'authors': Author.objects.all()}) }}} This approach is much the same as the `__copy__()` on `__call__()` approach, except that `__new__()` is used to create the new instance. Arguments against: * You can't use arguments to `__init__()` to instantiate a class view (although you can add a `configure` class method to replace this usage) * `x = AuthorView()` returns x as a HTTPResponse, not an `AuthorView` instance. This violates expectations of normal class usage. ==== classmethod ==== [http://github.com/spookylukey/django-class-based-views Implementation] Example usage: {{{ url(r'^detail/author/(?P\d+)/$', views.AuthorDetail.as_view, name="author_detail"), }}} Example class: {{{ class AuthorView(View): def GET(self, request, *args, **kwargs) return self.render_to_response('author_list.html', {'authors': Author.objects.all()}) }}} This is very similar to the `__new__()` approach, except that we have to add a classmethod call to create the callable. Arguments against: * You can't use arguments to `__init__()` to instantiate a class view (although you can add a `configure` class method to replace this usage) * The URLconf is slightly more verbose ==== classmethod2 ==== [http://github.com/spookylukey/django-class-based-views/tree/classmethod2 Implementation]. Similar to classmethod, but `as_view` returns a view functions when called, and arguments to `__init__()` can be passed to the `as_view` call. Example usage: {{{ url(r'^detail/author/(?P\d+)/$', views.AuthorDetail.as_view(), name="author_detail"), url(r'^detail/author/(?P\d+)/foo/$', views.AuthorDetail.as_view(foo=True), name="author_detail"), }}} Arguments against: * The URLconf is slightly more verbose * If you do `AuthorDetail(foo=True).as_view()`, instead of `AuthorDetail.as_view(foo=True)`, you will not get an exception, but `foo=True` will effectively be ignored. ==== HTTPResponse subclassing ==== [http://github.com/roalddevries/class_based_views/ Implementation] This approach exploits the fact that the aim of a view is to produce a HttpResponse instance; so it shortcuts the process, and makes a 'view' the constructor for the !HttpResponse. Arguments against: * Binds the Response object the concept of a view. A view isn't 'is-a' response, in the OO-sense. * Makes it difficult or impossible to subclass !HttpResponse and use that subclass * Makes it difficult to use !HttpResponse-returning methods; you can't just call out to a subview/utility method that returns a !HttpResponse instance, because the view needs to return 'self'. ==== !UrlResolver view instantiation ==== Rather than try to make the view look like a callable, this approach modifies !UrlResolver so that it can identify when a class-based view is in use, and instantiate an instance when one is detected. Arguments against: * Requires special cases in !UrlResolver which almost inevitably involve isinstance(!ViewClass) or some analog. * Decorators become extremely difficult to use; wrapping methods or `__call__` on an uninstantiated class is hard. ==== Recommendation ==== Based on these discussions, classmethod2 appears to be the winner. === Class Hierarchy === There are several ways to lay out the tree of classes that will make up both the base views and the new generic views. The current recommended approach is to use mixins to achieve certain parts of functionality (e.g. "rendering a form", "accepting form arguments", etc.), then combine these into generic views. While mixins are a relatively unused part of Python, and multiple inheritance can cause some odd bugs, the idea is to have the fact mixins are used as more of an implementation detail rather than part of the public API. === Method-based dispatch === This involves having the base view automatically call self.GET for GET requests, self.POST for POST requests, and so forth. This has the advantage of saving boilerplate for several types of view, but the disadvantage of getting in the way if you do want to share a lot of code between GET and POST methods. The recommended solution is that the very base view have only a dispatch() method (or a similar name) that gets called per-request, and then a subclass that also ships with Django that does method-based dispatch, which would probably be used by a lot of the class-based generic views. === Storing request, args, kwargs on self === One more controversial move is, if we have one instance per request (which is looking more likely), whether the request object and any positional/keyword arguments from the URL should be only passed around in the actual function calls, or stored on self. Advantages: - Allows much less fragile code (functions are far less subclassable if it's been decided in advance they will never see the request object) - Cleaner method signatures Disadvantages: - Won't work if there's not one instance per request The current recommendation is that, if one instance per request is chosen, that these are stored on the instance of the view (using the names `request`, `args` and `kwargs`), and that the `dispatch()` and `GET()`, `POST()`, etc. methods still take these as parameters, but all other methods (like `render()`, or `get_form()`) don't. === Methods for everything === Some attempts contain methods for everything - one to get the context instance, one to get the context data, one to get the whole context object, and so on. In real-life usage, this turns out to be both very verbose to override as well as being mostly unused (if I want to override how things are rendered, it's a lot easier to just provide a new `render()` function than to override five different other methods, and the logic flow can be changed as well). For this reason, the current recommendation is to break things down into moderately-sized chunks, but not too small - 5 lines or more. Things like template names/patterns should probably be provided as overrideable attributes on the class, however (just not which context instance one should use). === Justification === There are quite a few sets of class-based views out there already; they include: - Andrew Godwin's baseviews (https://bitbucket.org/andrewgodwin/baseviews/), which was developed alongside a project being built, and has some information about why certain patterns were chosen in the README. - bkondle's django-baseviews (http://github.com/bkonkle/django-baseviews) == How to help == Class-based views are being developed as a [http://github.com/bfirsh/django-class-based-views separate application on Github]. There are a few things yet to be done: * A simple readme for getting started * Testing it in real applications * More test coverage * Support for ModelForms that mimics the current generic views * Full documentation Fork the Github project if you want to help out.