2 | 3 | 4 |
5 | 6 | # Django Vanilla Views 7 | 8 | **Beautifully simple class-based views.** 9 | 10 | [](https://github.com/encode/django-vanilla-views/actions?workflow=CI) [](https://pypi.org/project/django-vanilla-views/) 11 | 12 | View --+------------------------- RedirectView 13 | | 14 | +-- GenericView -------+-- TemplateView 15 | | | 16 | | +-- FormView 17 | | 18 | +-- GenericModelView --+-- ListView 19 | | 20 | +-- DetailView 21 | | 22 | +-- CreateView 23 | | 24 | +-- UpdateView 25 | | 26 | +-- DeleteView 27 | 28 | Django's generic class-based view implementation is unnecessarily complicated. 29 | 30 | Django vanilla views gives you **exactly the same functionality**, in a vastly simplified, easier-to-use package, including: 31 | 32 | * No mixin classes. 33 | * No calls to `super()`. 34 | * A sane class hierarchy. 35 | * A stripped down API. 36 | * Simpler method implementations, with less magical behavior. 37 | 38 | Remember, even though the API has been greatly simplified, everything you're able to do with Django's existing implementation is also supported in `django-vanilla-views`. Although note that the package does not yet include the date based generic views. 39 | 40 | If you believe you've found some behavior in Django's generic class-based views that can't also be trivially achieved in `django-vanilla-views`, then please [open a ticket][tickets], and we'll treat it as a bug. To review the full set of API differences between the two implementations, please see the migration guide for the [base views][base-views-migration], and the [model views][model-views-migration]. 41 | 42 | For further background, the original release announcement for `django-vanilla-views` is [available here][release-announcement]. There are also slides to a talk ['Design by minimalism'][design-by-minimalism] which introduces `django-vanilla-views` and was presented at the Django User Group, London. You can also view the Django class hierarchy for the same set of views that `django-vanilla-views` provides, [here][django-cbv-hierarchy]. 43 | 44 | ## Helping you to code smarter 45 | 46 | Django Vanilla Views isn't just easier to use. I'd contest that because it presents fewer points of API to override, you'll also end up writing better, more maintainable code as a result. You'll be working from a smaller set of repeated patterns throughout your projects, and with a much more obvious flow control in your views. 47 | 48 | As an example, a custom view implemented against Django's `CreateView` class might typically look something like this: 49 | 50 | from django.views.generic import CreateView 51 | 52 | class AccountCreateView(CreateView): 53 | model = Account 54 | 55 | def get_success_url(self): 56 | return self.object.account_activated_url() 57 | 58 | def get_form_class(self): 59 | if self.request.user.is_staff: 60 | return AdminAccountForm 61 | return AccountForm 62 | 63 | def get_form_kwargs(self): 64 | kwargs = super(AccountCreateView, self).get_form_kwargs() 65 | kwargs['owner'] = self.request.user 66 | return kwargs 67 | 68 | def form_valid(self, form): 69 | send_activation_email(self.request.user) 70 | return super(AccountCreateView, self).form_valid(form) 71 | 72 | Writing the same code with `django-vanilla-views`, you'd instead arrive at a simpler, more concise, and more direct style: 73 | 74 | from vanilla import CreateView 75 | from django.http import HttpResponseRedirect 76 | 77 | class AccountCreateView(CreateView): 78 | model = Account 79 | 80 | def get_form(self, data=None, files=None, **kwargs): 81 | user = self.request.user 82 | if user.is_staff: 83 | return AdminAccountForm(data, files, owner=user, **kwargs) 84 | return AccountForm(data, files, owner=user, **kwargs) 85 | 86 | def form_valid(self, form): 87 | send_activation_email(self.request.user) 88 | account = form.save() 89 | return HttpResponseRedirect(account.account_activated_url()) 90 | 91 | ## Requirements 92 | 93 | * **Django**: 2.2, 3.0, 3.1, 3.2 94 | * **Python**: 3.6, 3.7, 3.8, 3.9 95 | 96 | ## Installation 97 | 98 | Install using pip. 99 | 100 | pip install django-vanilla-views 101 | 102 | ## Usage 103 | 104 | Import and use the views. 105 | 106 | from vanilla import ListView, DetailView 107 | 108 | For example: 109 | 110 | from django.core.urlresolvers import reverse_lazy 111 | from example.notes.models import Note 112 | from vanilla import CreateView, DeleteView, ListView, UpdateView 113 | 114 | class ListNotes(ListView): 115 | model = Note 116 | 117 | 118 | class CreateNote(CreateView): 119 | model = Note 120 | success_url = reverse_lazy('list_notes') 121 | 122 | 123 | class EditNote(UpdateView): 124 | model = Note 125 | success_url = reverse_lazy('list_notes') 126 | 127 | 128 | class DeleteNote(DeleteView): 129 | model = Note 130 | success_url = reverse_lazy('list_notes') 131 | 132 | 133 | ## Compare and contrast 134 | 135 | To help give you an idea of the relative complexity of `django-vanilla-views` against Django's existing implementations, let's compare the two. 136 | 137 | #### Inheritance hierarchy, Vanilla style. 138 | 139 | The inheritance hierarchy of the views in `django-vanilla-views` is trivial, making it easy to figure out the control flow in the view. 140 | 141 | CreateView --> GenericModelView --> View 142 | 143 | **Total number of source files**: 1 ([model_views.py][model_views.py]) 144 | 145 | #### Inheritance hierarchy, Django style. 146 | 147 | Here's the corresponding inheritance hierarchy in Django's implementation of `CreateView`. 148 | 149 | +--> SingleObjectTemplateResponseMixin --> TemplateResponseMixin 150 | | 151 | CreateView --+ +--> ProcessFormView --> View 152 | | | 153 | +--> BaseCreateView --+ 154 | | +--> FormMixin ----------+ 155 | +--> ModelFormMixin --+ +--> ContextMixin 156 | +--> SingleObjectMixin --+ 157 | 158 | **Total number of source files**: 3 ([edit.py][edit.py], [detail.py][detail.py], [base.py][base.py]) 159 | 160 | --- 161 | 162 | #### Calling hierarchy, Vanilla style. 163 | 164 | Let's take a look at the calling hierarchy when making an HTTP `GET` request to `CreateView`. 165 | 166 | CreateView.get() 167 | | 168 | +--> GenericModelView.get_form() 169 | | | 170 | | +--> GenericModelView.get_form_class() 171 | | 172 | +--> GenericModelView.get_context_data() 173 | | | 174 | | +--> GenericModelView.get_context_object_name() 175 | | 176 | +--> GenericModelView.render_to_response() 177 | | 178 | +--> GenericModelView.get_template_names() 179 | 180 | **Total number of code statements covered**: ~40 181 | 182 | #### Calling hierarchy, Django style. 183 | 184 | Here's the equivalent calling hierarchy in Django's `CreateView` implementation. 185 | 186 | BaseCreateView.get() 187 | | 188 | +--> ProcessFormView.get() 189 | | 190 | +--> ModelFormMixin.get_form_class() 191 | | | 192 | | +--> SingleObjectMixin.get_queryset() 193 | | 194 | +--> FormMixin.get_form() 195 | | | 196 | | +--> ModelFormMixin.get_form_kwargs() 197 | | | | 198 | | | +--> FormMixin.get_form_kwargs() 199 | | | 200 | | +--> FormMixin.get_prefix() 201 | | | 202 | | +--> FormMixin.get_initial() 203 | | 204 | +--> ModelFormMixin.get_context_data() 205 | | | 206 | | +--> SingleObjectMixin.get_context_object_name() 207 | | | 208 | | +--> SingleObjectMixin.get_context_data() 209 | | | 210 | | +--> SingleObjectMixin.get_context_object_name() 211 | | | 212 | | +--> ContextMixin.get_context_data() 213 | | 214 | +--> TemplateResponseMixin.render_to_response() 215 | | 216 | +--> SingleObjectTemplateResponseMixin.get_template_names() 217 | | 218 | +--> TemplateResponseMixin.get_template_names() 219 | 220 | **Total number of code statements covered**: ~70 221 | 222 | ## Example project 223 | 224 | This repository includes an example project in the [example][example] directory. 225 | 226 | You can run the example locally by following these steps: 227 | 228 | git clone git://github.com/tomchristie/django-vanilla-views.git 229 | cd django-vanilla-views/example 230 | 231 | # Create a clean virtualenv environment and install Django 232 | virtualenv env 233 | source env/bin/activate 234 | pip install django 235 | 236 | # Ensure the local copy of the 'vanilla' package is on our path 237 | export PYTHONPATH=..:. 238 | 239 | # Run the project 240 | python ./manage.py migrate 241 | python ./manage.py runserver 242 | 243 | Open a browser and navigate to `http://127.0.0.1:8000`. 244 | 245 | Once you've added a few notes you should see something like the following: 246 | 247 |  248 | 249 | --- 250 | 251 | ## License 252 | 253 | Copyright © Tom Christie. 254 | 255 | All rights reserved. 256 | 257 | Redistribution and use in source and binary forms, with or without 258 | modification, are permitted provided that the following conditions are met: 259 | 260 | Redistributions of source code must retain the above copyright notice, this 261 | list of conditions and the following disclaimer. 262 | Redistributions in binary form must reproduce the above copyright notice, this 263 | list of conditions and the following disclaimer in the documentation and/or 264 | other materials provided with the distribution. 265 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 266 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 267 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 268 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 269 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 270 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 271 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 272 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 273 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 274 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 275 | 276 | [twitter]: http://twitter.com/_tomchristie 277 | [tickets]: https://github.com/tomchristie/django-vanilla-views/issues 278 | [base-views-migration]: migration/base-views.md 279 | [model-views-migration]: migration/model-views.md 280 | [release-announcement]: http://dabapps.com/blog/fixing-djangos-generic-class-based-views/ 281 | [design-by-minimalism]: http://slid.es/tomchristie/design-by-minimalism 282 | [django-cbv-hierarchy]: img/djangocbv.png 283 | [model_views.py]: https://github.com/tomchristie/django-vanilla-views/tree/master/vanilla/model_views.py 284 | [base.py]: https://github.com/django/django/tree/master/django/views/generic/base.py 285 | [detail.py]: https://github.com/django/django/tree/master/django/views/generic/detail.py 286 | [edit.py]: https://github.com/django/django/tree/master/django/views/generic/edit.py 287 | [example]: https://github.com/tomchristie/django-vanilla-views/tree/master/example 288 | -------------------------------------------------------------------------------- /docs/js/bootstrap-2.1.1-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;a("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1).focus(),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,a("body").removeClass("modal-open"),this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(a){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,a.proxy(this.removeBackdrop,this)):this.removeBackdrop()):b&&b()}},a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a(function(){a("body").on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})})}(window.jQuery),!function(a){function d(){e(a(b)).removeClass("open")}function e(b){var c=b.attr("data-target"),d;return c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,"")),d=a(c),d.length||(d=b.parent()),d}var b="[data-toggle=dropdown]",c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),f,g;if(c.is(".disabled, :disabled"))return;return f=e(c),g=f.hasClass("open"),d(),g||(f.toggleClass("open"),c.focus()),!1},keydown:function(b){var c,d,f,g,h,i;if(!/(38|40|27)/.test(b.keyCode))return;c=a(this),b.preventDefault(),b.stopPropagation();if(c.is(".disabled, :disabled"))return;g=e(c),h=g.hasClass("open");if(!h||h&&b.keyCode==27)return c.click();d=a("[role=menu] li:not(.divider) a",g);if(!d.length)return;i=d.index(d.filter(":focus")),b.keyCode==38&&i>0&&i--,b.keyCode==40&&i