├── .editorconfig ├── .github └── workflows │ ├── main.yml │ └── pre-commit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── api │ ├── base-views.md │ └── model-views.md ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap.css │ ├── default.css │ └── prettify.css ├── img │ ├── djangocbv.png │ ├── example.png │ ├── favicon.ico │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ └── grid.png ├── index.md ├── js │ ├── bootstrap-2.1.1-min.js │ ├── jquery-1.8.1-min.js │ └── prettify-1.0.js ├── migration │ ├── base-views.md │ └── model-views.md ├── template.html └── topics │ ├── django-braces-compatibility.md │ ├── django-extra-views-compatibility.md │ ├── frequently-asked-questions.md │ └── release-notes.md ├── example ├── example │ ├── __init__.py │ ├── notes │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── requirements.txt ├── statics │ └── css │ │ └── bootstrap.css └── templates │ └── notes │ ├── note_form.html │ └── note_list.html ├── manage.py ├── mkdocs.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── testsettings.py ├── tox.ini └── vanilla ├── __init__.py ├── model_views.py ├── models.py ├── tests.py └── views.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.{css,html,js,yaml,yml}] 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-20.04 13 | 14 | strategy: 15 | matrix: 16 | python-version: 17 | - 3.6 18 | - 3.7 19 | - 3.8 20 | - 3.9 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - uses: actions/cache@v2 30 | with: 31 | path: ~/.cache/pip 32 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip setuptools wheel 39 | python -m pip install --upgrade tox tox-py 40 | 41 | - name: Run tox targets for ${{ matrix.python-version }} 42 | run: tox --py current 43 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | pre-commit: 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-python@v2 19 | 20 | - uses: pre-commit/action@v2.0.0 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | .coverage 4 | MANIFEST 5 | dist/ 6 | build/ 7 | env/ 8 | html/ 9 | htmlcov/ 10 | *.egg-info/ 11 | .tox/ 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-case-conflict 7 | - id: check-json 8 | - id: check-merge-conflict 9 | - id: check-symlinks 10 | - id: check-toml 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | - repo: https://github.com/psf/black 14 | rev: 22.8.0 15 | hooks: 16 | - id: black 17 | language_version: python3 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.10.1 20 | hooks: 21 | - id: isort 22 | - repo: https://github.com/PyCQA/flake8 23 | rev: 5.0.4 24 | hooks: 25 | - id: flake8 26 | additional_dependencies: 27 | - flake8-bugbear 28 | - flake8-comprehensions 29 | - flake8-tidy-imports 30 | - repo: https://github.com/mgedmin/check-manifest 31 | rev: "0.48" 32 | hooks: 33 | - id: check-manifest 34 | args: [--no-build-isolation] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © Tom Christie. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] 2 | prune docs 3 | prune example 4 | prune tests 5 | prune __pycache__ 6 | include LICENSE 7 | include README.md 8 | exclude .editorconfig 9 | exclude .pre-commit-config.yaml 10 | exclude manage.py 11 | exclude mkdocs.py 12 | include pyproject.toml 13 | exclude testsettings.py 14 | exclude tox.ini 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Vanilla Views 2 | 3 | **Beautifully simple class-based views.** 4 | 5 | Please see the documentation at [django-vanilla-views.org][docs]. 6 | 7 | [docs]: http://django-vanilla-views.org 8 | -------------------------------------------------------------------------------- /docs/api/base-views.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Base Views 4 | 5 | The base views provide a simple set of generic views for working with Django querysets and model instances. 6 | 7 | They replicate the functionality of Django's existing `TemplateView` and `FormView` but present a simpler API and implementation. Django's standard `RedirectView` is also included for completeness. 8 | 9 | View --+-------------------- RedirectView 10 | | 11 | +-- GenericView --+-- TemplateView 12 | | 13 | +-- FormView 14 | 15 | --- 16 | 17 | ## GenericView 18 | 19 | The `GenericView` class is used as the base class for both `TemplateView` and `FormView`, and provides methods allowing for a default set of simple template and form actions. 20 | 21 | ### Attributes 22 | 23 | #### form_class 24 | 25 | The form class that should be used for edit views. If you are using `FormView`, or your own custom view that calls `get_form()`, then you should either set this attribute, or override one of the form generation methods. Defaults to `None`. 26 | 27 | #### template_name 28 | 29 | A string representing the template name that should be used when rendering the response content. You should either set this attribute or override one of the methods controlling how responses are rendered. Defaults to `None`. 30 | 31 | ### Methods 32 | 33 | #### get_form_class(self) 34 | 35 | This method returns the class that should be used for generating forms. 36 | 37 | The default behavior for this method is: 38 | 39 | * If `form_class` is specified on the view then use that. 40 | * Otherwise raise a configuration error. 41 | 42 | You can customize how the form class for the view is determined by overriding this method. For example: 43 | 44 | def get_form_class(self): 45 | is self.request.user.is_staff(): 46 | return AccountForm 47 | return BasicAccountForm 48 | 49 | #### get_form(self, data=None, files=None, **kwargs) 50 | 51 | The method instantiates and returns the form instance that should be used for the view. 52 | 53 | By default this method simply calls `get_form_class()`, and then instantiates the class with the parameters that have been passed to it. 54 | 55 | You can customize this method in order to supply additional arguments to the form class, add initial data, or other customizations. For example: 56 | 57 | def get_form(self, data=None, files=None, **kwargs): 58 | kwargs['user'] = self.request.user 59 | return AccountForm(data, files, **kwargs) 60 | 61 | #### get_context_data(self, **kwargs) 62 | 63 | This method takes a set of keyword arguments supplied by the view and returns a dictionary to use as context when rendering the response template. 64 | 65 | The default behavior of this method is to return a dictionary populated with the following keys: 66 | 67 | * `view` - A reference to the view instance. 68 | * Any additional keyword arguments supplied to the method. In particular, `FormView` includes the `form` context key. 69 | 70 | You can override the method either to add additional context data: 71 | 72 | def get_context_data(self, **kwargs): 73 | context = super(MyView, self).get_context_data(**kwargs) 74 | context['is_admin'] = self.request.user.is_admin 75 | return context 76 | 77 | Or to specify the complete set of context data explicitly: 78 | 79 | def get_context_data(self, **kwargs): 80 | kwargs['view'] = self 81 | kwargs['is_admin'] = self.request.user.is_admin 82 | kwargs['account'] = self.object 83 | return kwargs 84 | 85 | #### get_template_names(self) 86 | 87 | Returns a list of strings that should be used for determining the template name when rendering the response. 88 | 89 | The default behavior for this method is: 90 | 91 | * If `template_name` is specified on the view then use that. 92 | * Otherwise raise a configuration error. 93 | 94 | #### render_to_response(self, context) 95 | 96 | Generates the response that should be returned by the view. Takes a single argument which should be a dictionary of context data to use when rendering the response template. 97 | 98 | The default behaviour of this method is to return an instance of Django's standard `TemplateResponse`. 99 | 100 | You can override this method if you need to customize how the response is generated. For example, to return a response with the `text/plain` content type instead of the standard `text/html`, you could write something like this: 101 | 102 | def render_to_response(self, context): 103 | template = self.get_template_names() 104 | return TemplateResponse(self.request, template, context, content_type='text/plain') 105 | 106 | You can also override this class in order to use a subclass of Django's standard `HttpResponse` or `TemplateResponse`. For example, if you had a written a custom `JSONResponse` class, you might override the method like this: 107 | 108 | def render_to_response(self, context): 109 | return JSONResponse(self.request, context) 110 | 111 | --- 112 | 113 | ## RedirectView 114 | 115 | For completeness, Django's standard `RedirectView` is included in the `django-vanilla-views` package. The class does not have any implementation or API differences from Django's implementation. 116 | 117 | You should [refer to the Django documentation][redirect-view-docs] for further information. 118 | 119 | --- 120 | 121 | ## TemplateView 122 | 123 | A page which simply returns a template response. 124 | 125 | The context passed to the response template will be: 126 | 127 | * `view` - The view instance. 128 | 129 | --- 130 | 131 | ## FormView 132 | 133 | A page which allows the user to preview and submit a form. 134 | 135 | The context passed to the response template will be: 136 | 137 | * `view` - The view instance. 138 | * `form` - The form instance. 139 | 140 | #### success_url 141 | 142 | The URL that should be used when redirecting after a successful form submission. 143 | 144 | #### form_valid(self, form) 145 | 146 | This method will be run when a valid form submission occurs, and should return a response object. The default behavior is to return a redirect response as determined by calling `get_success_url()`. 147 | 148 | #### form_invalid(self, form) 149 | 150 | This method will be run when a valid form submission occurs, and should return a response object. The default behavior is to return a `TemplateResponse` which renders the form errors. 151 | 152 | 153 | #### get_success_url() 154 | 155 | Returns the URL that should be used when redirecting after a successful form submission. Defaults to returning the value of the `success_url` attribute. 156 | 157 | **Note**: If you are customizing the view behavior, we'd typically recommend overriding the `form_valid()` method directly rather than overriding `get_success_url()`, as it will result in simpler, more obvious flow control. 158 | 159 | [redirect-view-docs]: https://docs.djangoproject.com/en/dev/ref/class-based-views/base/#redirectview 160 | -------------------------------------------------------------------------------- /docs/api/model-views.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Model Views 4 | 5 | The model views provide a simple set of generic views for working with Django querysets and model instances. 6 | 7 | They replicate the functionality of Django's existing `ListView`, `DetailView`, `CreateView`, `UpdateView` and `DeleteView`, but present a simpler API and implementation. 8 | 9 | View -- GenericModelView --+-- ListView 10 | | 11 | +-- DetailView 12 | | 13 | +-- CreateView 14 | | 15 | +-- UpdateView 16 | | 17 | +-- DeleteView 18 | 19 | --- 20 | 21 | ## GenericModelView 22 | 23 | The `GenericModelView` class is used as the base class for all of the model views, and provides methods allowing for a default set of viewing, listing and editing actions. 24 | 25 | ### Attributes 26 | 27 | #### model 28 | 29 | The model class that the view operates on. This is used as a shortcut to provide default behavior for the view. The default behaviour may be overridden by setting more specific attributes, or by overriding methods on the view. 30 | 31 | #### queryset 32 | 33 | The base queryset that should be used for list views, or used when performing object lookups for detail views. If set to `None` then a default queryset will be used based on the `model` attribute. Defaults to `None`. 34 | 35 | #### lookup_field 36 | 37 | The name of the model field that should be used for object lookups. Defaults to `'pk'`. 38 | 39 | #### lookup_url_kwarg 40 | 41 | The name of the URLconf keyword argument that should be used for object lookups. If unset this defaults to the same value as `lookup_field`. 42 | 43 | #### form_class 44 | 45 | The form class that should be used for create or update views. If set to `None` then a default form class will be used based on the `model` and `fields` attributes. Defaults to `None`. 46 | 47 | #### fields 48 | 49 | A list of strings, representing the fields that should be displayed by the form. This may be used along with the `model` attribute, as a shortcut to setting the `form_class` attribute. Defaults to `None`. 50 | 51 | #### paginate_by 52 | 53 | The number of items to return in each page. Set to a positive integer value to enable pagination. If set to `None` then pagination is disabled. Defaults to `None`. 54 | 55 | #### page_kwarg 56 | 57 | The name of the URL query parameter that is used to select the active page in a paginated list. For example: `http://example.com/widget_list?page=6`. Defaults to `'page'`. 58 | 59 | #### template_name 60 | 61 | A string representing the template name that should be used when rendering the response content. If set to `None`, then the template name will be automatically generated based on the `model` attribute. Defaults to `None`. 62 | 63 | #### template_name_suffix 64 | 65 | A suffix that should be appended when automatically generating template names based on the `model` attribute. Defaults to `None`, but is set to an appropriate value of either `'_detail'`, `'_list'` or `'_form'` by each of the model view subclasses. 66 | 67 | #### context_object_name 68 | 69 | A key to use when passing the queryset or instance as context to the response. If set to `None` then the context object name will be automatically generated based on the `model` attribute. Defaults to `None`. 70 | 71 | --- 72 | 73 | ### Methods 74 | 75 | #### get_queryset(self) 76 | 77 | This method should return a queryset representing the set of instances that the view should operate on. 78 | 79 | The default behavior of this method is: 80 | 81 | * If the `queryset` attribute is set, then return that. 82 | * Otherwise fallback to returning the default queryset for the model class as determined by the `model` attribute. 83 | * If neither the `queryset` or `model` attributes are set then a configuration error will be raised. 84 | 85 | You can customize how the querysets for the view are determined by overriding this method. For example: 86 | 87 | def get_queryset(self): 88 | """ 89 | Custom queryset that only returns book instances owned by the logged-in user. 90 | """ 91 | return Book.objects.filter(owner=self.request.user) 92 | 93 | #### get_object(self) 94 | 95 | This method should return a single model instance that the view should operate on, and is used by `DetailView`, `UpdateView` and `DeleteView`. 96 | 97 | The default behavior for this method is: 98 | 99 | * Call `get_queryset()` to determine the base queryset to use for the lookup. 100 | * Perform the object lookup based on the `lookup_field` and `lookup_url_kwarg` attributes. 101 | * Raise an `HTTP 404 Not Found` response if the instance does not exist. 102 | 103 | You can perform custom object lookups by overriding this method. For example: 104 | 105 | def get_object(self): 106 | """ 107 | Custom object lookup that returns an instance based on both the 108 | 'account' and 'slug' as provided in the URL keyword arguments. 109 | """ 110 | queryset = self.get_queryset() 111 | account = self.kwargs['account'] 112 | slug = self.kwargs['slug'] 113 | return get_object_or_404(queryset, account=account, slug=slug) 114 | 115 | #### get_form_class(self) 116 | 117 | This method returns the class that should be used for generating forms. 118 | 119 | The default behavior of this method is: 120 | 121 | * If the `form_class` attribute is set, then return that. 122 | * Otherwise fallback to returning an automatically generated form class based on the `model` attribute. 123 | * If neither the `form_class` or `model` attributes are set then a configuration error will be raised. 124 | 125 | You can customize how the form class for the view is determined by overriding this method. For example: 126 | 127 | def get_form_class(self): 128 | is self.request.user.is_staff(): 129 | return AccountForm 130 | return BasicAccountForm 131 | 132 | #### get_form(self, data=None, files=None, **kwargs) 133 | 134 | The method instantiates and returns the form instance that should be used for the view. 135 | 136 | By default this method simply calls `get_form_class`, and then instantiates the class with the parameters that have been passed to it. 137 | 138 | You can customize this method in order to supply additional arguments to the form class, add initial data, or other customizations. For example: 139 | 140 | def get_form(self, data=None, files=None, **kwargs): 141 | kwargs['user'] = self.request.user 142 | return AccountForm(data, files, **kwargs) 143 | 144 | #### get_paginate_by(self) 145 | 146 | Returns an integer representing the number of items to display on each page of a paginated list. Returns `None` if pagination is not enabled. 147 | 148 | By default this method simply returns value of the `paginate_by` attribute. 149 | 150 | You can override this method to provide more complex behavior. For example, to allow the user to override the default pagination size using a query parameter in the URL, you might write something like this: 151 | 152 | def get_paginate_by(self): 153 | try: 154 | return int(self.request.GET('page_size', self.paginate_by)) 155 | except ValueError: 156 | return None 157 | 158 | #### get_paginator(self, queryset, page_size) 159 | 160 | Given a queryset and a page size, returns a paginator instance to use for a paginated list view. 161 | 162 | By default this method simply instantiates Django's standard `Paginator` class with the arguments passed. 163 | 164 | If you need to customize how the paginator is instantiated you can override this method. For example to ensure that the final page must always contain more than a single item, you could write something like this: 165 | 166 | def get_paginator(self, queryset, page_size): 167 | return Paginator(queryset, page_size, orphans=2) 168 | 169 | #### paginate_queryset(self, queryset, page_size) 170 | 171 | Given a queryset and a page size, this method should return a `page` instance representing the current page that should be displayed in a paginated list view. You can override this method if you need to customize how the page object is determined, but the default behavior should typically be sufficient. 172 | 173 | #### get_context_object_name(self, is_list=False) 174 | 175 | This method returns a descriptive name that should be used when passing the object or object list as context to the template. The name is used *in addition* to the default `'object'` or `'object_list'` context name. 176 | 177 | The method takes a single parameter `is_list`, which is a boolean indicating if the context object should be named as representing a list of data, or if it should be named as representing a single object. 178 | 179 | The default behavior of this method is: 180 | 181 | * If the `context_object_name` attribute is set, then use that. 182 | * Otherwise fallback to automatically using `` or `_list` based on the `model` attribute. 183 | * If neither the `context_object_name` or `model` attributes are set, then only the standard `'object'` or `'object_list'` key will be used. 184 | 185 | #### get_context_data(self, **kwargs) 186 | 187 | This method takes a set of keyword arguments supplied by the view and returns a dictionary to use as context when rendering the response template. 188 | 189 | The default behavior of this method is to return a dictionary populated with the following keys: 190 | 191 | * `view` - A reference to the view instance. 192 | * `object` or `object_list` - The instance or queryset being operated on by the view. 193 | * `` - A more descriptive name for the instance or queryset as returned by `get_context_object_name`. 194 | * Any additional keyword arguments supplied to the method. In particular, the model editing views include the `form` context key. 195 | 196 | You can override the method either to add additional context data: 197 | 198 | def get_context_data(self, **kwargs): 199 | context = super(MyView, self).get_context_data(**kwargs) 200 | context['is_admin'] = self.request.user.is_admin 201 | return context 202 | 203 | Or to specify the complete set of context data explicitly: 204 | 205 | def get_context_data(self, **kwargs): 206 | kwargs['view'] = self 207 | kwargs['is_admin'] = self.request.user.is_admin 208 | kwargs['account'] = self.object 209 | return kwargs 210 | 211 | #### get_template_names(self) 212 | 213 | Returns a list of strings that should be used for determining the template name when rendering the response. 214 | 215 | The default behavior for this method is: 216 | 217 | * If `template_name` is specified on the view then use that. 218 | * Otherwise fallback to automatically generating a template name as `{app_label}/{model_name}{suffix}.html`, using the `model` attribute as set on the view. 219 | * If neither of `template_name` or `model` attributes are set then raise a configuration error. 220 | 221 | #### render_to_response(self, context) 222 | 223 | Generates the response that should be returned by the view. Takes a single argument which should be a dictionary of context data to use when rendering the response template. 224 | 225 | The default behaviour of this method is to return an instance of Django's standard `TemplateResponse`. 226 | 227 | You can override this method if you need to customize how the response is generated. For example, to return a response with the `text/plain` content type instead of the standard `text/html`, you could write something like this: 228 | 229 | def render_to_response(context): 230 | template = self.get_template_names() 231 | return TemplateResponse(self.request, template, context, content_type='text/plain') 232 | 233 | You can also override this class in order to use a subclass of Django's standard `HttpResponse` or `TemplateResponse`. For example, if you had a written a custom `JSONResponse` class, you might override the method like this: 234 | 235 | def render_to_response(context): 236 | return JSONResponse(self.request, context) 237 | 238 | --- 239 | 240 | ## ListView 241 | 242 | A page representing a list of objects. Optionally this may represent a paginated view onto the list. 243 | 244 | The `object_list` attribute will be set on this view, and will typically be a queryset instance. 245 | 246 | #### allow_empty 247 | 248 | A boolean indicating if empty lists may be returned using the standard page template, or if they should cause an `HTTP 404 Not Found` response to be returned. Defaults to `True`, indicating that empty pages should be allowed. 249 | 250 | --- 251 | 252 | ## DetailView 253 | 254 | A page representing a single object. 255 | 256 | The `object` attribute will be set on this view, and will typically be a model instance. 257 | 258 | --- 259 | 260 | ## CreateView 261 | 262 | A page which allows the user to create objects. 263 | 264 | If successfully created, then the `object` attribute will be set on this view. 265 | 266 | #### success_url 267 | 268 | The URL that should be used when redirecting after a successful form submission. 269 | 270 | #### form_valid(self, form) 271 | 272 | This method will be run when a valid form submission occurs, and should return a response object. The default behavior is to return a redirect response as determined by calling `get_success_url()`. 273 | 274 | #### form_invalid(self, form) 275 | 276 | This method will be run when an invalid form submission occurs, and should return a response object. The default behavior is to return a `TemplateResponse` which renders the form errors. 277 | 278 | #### get_success_url() 279 | 280 | Returns the URL that should be used when redirecting after a successful form submission. Defaults to returning the value of the `success_url` attribute if it is set, or will be the return value of calling `get_absolute_url()` on the object instance. 281 | 282 | **Note**: If you are customizing the view behavior, we'd typically recommend overriding the `form_valid()` method directly rather than overriding `get_success_url()`, as it will result in simpler, more obvious flow control. 283 | 284 | --- 285 | 286 | ## UpdateView 287 | 288 | A page which allows the user to update an existing object. 289 | 290 | The `object` attribute will be set on this view. 291 | 292 | #### success_url 293 | 294 | The URL that should be used when redirecting after a successful form submission. 295 | 296 | #### form_valid(self, form) 297 | 298 | This method will be run when a valid form submission occurs, and should return a response object. The default behavior is to save the updated object instance and then return a redirect response as determined by calling `get_success_url()`. 299 | 300 | #### form_invalid(self, form) 301 | 302 | This method will be run when an invalid form submission occurs, and should return a response object. The default behavior is to return a `TemplateResponse` which renders the form errors. 303 | 304 | #### get_success_url() 305 | 306 | Returns the URL that should be used when redirecting after a successful form submission. Defaults to returning the value of the `success_url` attribute if it is set, or will be the return value of calling `get_absolute_url()` on the object instance. 307 | 308 | **Note**: If you are customizing the view behavior, we'd typically recommend overriding the `form_valid()` method directly rather than overriding `get_success_url()`, as it will result in simpler, more obvious flow control. 309 | 310 | --- 311 | 312 | ## DeleteView 313 | 314 | The `object` attribute will be set on this view. 315 | 316 | #### success_url 317 | 318 | The URL that should be used when redirecting after a successful form submission. 319 | 320 | #### get_success_url() 321 | 322 | Returns the URL that should be used when redirecting after a successful form submission. Defaults to returning the value of the `success_url` attribute. 323 | 324 | **Note**: If you are customizing the view behavior, we'd typically recommend overriding the `post()` method directly rather than overriding `get_success_url()`, as it will result in simpler, more obvious flow control. 325 | -------------------------------------------------------------------------------- /docs/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | .visible-desktop { 61 | display: inherit !important; 62 | } 63 | 64 | @media (min-width: 768px) and (max-width: 979px) { 65 | .hidden-desktop { 66 | display: inherit !important; 67 | } 68 | .visible-desktop { 69 | display: none !important ; 70 | } 71 | .visible-tablet { 72 | display: inherit !important; 73 | } 74 | .hidden-tablet { 75 | display: none !important; 76 | } 77 | } 78 | 79 | @media (max-width: 767px) { 80 | .hidden-desktop { 81 | display: inherit !important; 82 | } 83 | .visible-desktop { 84 | display: none !important; 85 | } 86 | .visible-phone { 87 | display: inherit !important; 88 | } 89 | .hidden-phone { 90 | display: none !important; 91 | } 92 | } 93 | 94 | @media (min-width: 1200px) { 95 | .row { 96 | margin-left: -30px; 97 | *zoom: 1; 98 | } 99 | .row:before, 100 | .row:after { 101 | display: table; 102 | line-height: 0; 103 | content: ""; 104 | } 105 | .row:after { 106 | clear: both; 107 | } 108 | [class*="span"] { 109 | float: left; 110 | min-height: 1px; 111 | margin-left: 30px; 112 | } 113 | .container, 114 | .navbar-static-top .container, 115 | .navbar-fixed-top .container, 116 | .navbar-fixed-bottom .container { 117 | width: 1170px; 118 | } 119 | .span12 { 120 | width: 1170px; 121 | } 122 | .span11 { 123 | width: 1070px; 124 | } 125 | .span10 { 126 | width: 970px; 127 | } 128 | .span9 { 129 | width: 870px; 130 | } 131 | .span8 { 132 | width: 770px; 133 | } 134 | .span7 { 135 | width: 670px; 136 | } 137 | .span6 { 138 | width: 570px; 139 | } 140 | .span5 { 141 | width: 470px; 142 | } 143 | .span4 { 144 | width: 370px; 145 | } 146 | .span3 { 147 | width: 270px; 148 | } 149 | .span2 { 150 | width: 170px; 151 | } 152 | .span1 { 153 | width: 70px; 154 | } 155 | .offset12 { 156 | margin-left: 1230px; 157 | } 158 | .offset11 { 159 | margin-left: 1130px; 160 | } 161 | .offset10 { 162 | margin-left: 1030px; 163 | } 164 | .offset9 { 165 | margin-left: 930px; 166 | } 167 | .offset8 { 168 | margin-left: 830px; 169 | } 170 | .offset7 { 171 | margin-left: 730px; 172 | } 173 | .offset6 { 174 | margin-left: 630px; 175 | } 176 | .offset5 { 177 | margin-left: 530px; 178 | } 179 | .offset4 { 180 | margin-left: 430px; 181 | } 182 | .offset3 { 183 | margin-left: 330px; 184 | } 185 | .offset2 { 186 | margin-left: 230px; 187 | } 188 | .offset1 { 189 | margin-left: 130px; 190 | } 191 | .row-fluid { 192 | width: 100%; 193 | *zoom: 1; 194 | } 195 | .row-fluid:before, 196 | .row-fluid:after { 197 | display: table; 198 | line-height: 0; 199 | content: ""; 200 | } 201 | .row-fluid:after { 202 | clear: both; 203 | } 204 | .row-fluid [class*="span"] { 205 | display: block; 206 | float: left; 207 | width: 100%; 208 | min-height: 30px; 209 | margin-left: 2.564102564102564%; 210 | *margin-left: 2.5109110747408616%; 211 | -webkit-box-sizing: border-box; 212 | -moz-box-sizing: border-box; 213 | box-sizing: border-box; 214 | } 215 | .row-fluid [class*="span"]:first-child { 216 | margin-left: 0; 217 | } 218 | .row-fluid .span12 { 219 | width: 100%; 220 | *width: 99.94680851063829%; 221 | } 222 | .row-fluid .span11 { 223 | width: 91.45299145299145%; 224 | *width: 91.39979996362975%; 225 | } 226 | .row-fluid .span10 { 227 | width: 82.90598290598291%; 228 | *width: 82.8527914166212%; 229 | } 230 | .row-fluid .span9 { 231 | width: 74.35897435897436%; 232 | *width: 74.30578286961266%; 233 | } 234 | .row-fluid .span8 { 235 | width: 65.81196581196582%; 236 | *width: 65.75877432260411%; 237 | } 238 | .row-fluid .span7 { 239 | width: 57.26495726495726%; 240 | *width: 57.21176577559556%; 241 | } 242 | .row-fluid .span6 { 243 | width: 48.717948717948715%; 244 | *width: 48.664757228587014%; 245 | } 246 | .row-fluid .span5 { 247 | width: 40.17094017094017%; 248 | *width: 40.11774868157847%; 249 | } 250 | .row-fluid .span4 { 251 | width: 31.623931623931625%; 252 | *width: 31.570740134569924%; 253 | } 254 | .row-fluid .span3 { 255 | width: 23.076923076923077%; 256 | *width: 23.023731587561375%; 257 | } 258 | .row-fluid .span2 { 259 | width: 14.52991452991453%; 260 | *width: 14.476723040552828%; 261 | } 262 | .row-fluid .span1 { 263 | width: 5.982905982905983%; 264 | *width: 5.929714493544281%; 265 | } 266 | .row-fluid .offset12 { 267 | margin-left: 105.12820512820512%; 268 | *margin-left: 105.02182214948171%; 269 | } 270 | .row-fluid .offset12:first-child { 271 | margin-left: 102.56410256410257%; 272 | *margin-left: 102.45771958537915%; 273 | } 274 | .row-fluid .offset11 { 275 | margin-left: 96.58119658119658%; 276 | *margin-left: 96.47481360247316%; 277 | } 278 | .row-fluid .offset11:first-child { 279 | margin-left: 94.01709401709402%; 280 | *margin-left: 93.91071103837061%; 281 | } 282 | .row-fluid .offset10 { 283 | margin-left: 88.03418803418803%; 284 | *margin-left: 87.92780505546462%; 285 | } 286 | .row-fluid .offset10:first-child { 287 | margin-left: 85.47008547008548%; 288 | *margin-left: 85.36370249136206%; 289 | } 290 | .row-fluid .offset9 { 291 | margin-left: 79.48717948717949%; 292 | *margin-left: 79.38079650845607%; 293 | } 294 | .row-fluid .offset9:first-child { 295 | margin-left: 76.92307692307693%; 296 | *margin-left: 76.81669394435352%; 297 | } 298 | .row-fluid .offset8 { 299 | margin-left: 70.94017094017094%; 300 | *margin-left: 70.83378796144753%; 301 | } 302 | .row-fluid .offset8:first-child { 303 | margin-left: 68.37606837606839%; 304 | *margin-left: 68.26968539734497%; 305 | } 306 | .row-fluid .offset7 { 307 | margin-left: 62.393162393162385%; 308 | *margin-left: 62.28677941443899%; 309 | } 310 | .row-fluid .offset7:first-child { 311 | margin-left: 59.82905982905982%; 312 | *margin-left: 59.72267685033642%; 313 | } 314 | .row-fluid .offset6 { 315 | margin-left: 53.84615384615384%; 316 | *margin-left: 53.739770867430444%; 317 | } 318 | .row-fluid .offset6:first-child { 319 | margin-left: 51.28205128205128%; 320 | *margin-left: 51.175668303327875%; 321 | } 322 | .row-fluid .offset5 { 323 | margin-left: 45.299145299145295%; 324 | *margin-left: 45.1927623204219%; 325 | } 326 | .row-fluid .offset5:first-child { 327 | margin-left: 42.73504273504273%; 328 | *margin-left: 42.62865975631933%; 329 | } 330 | .row-fluid .offset4 { 331 | margin-left: 36.75213675213675%; 332 | *margin-left: 36.645753773413354%; 333 | } 334 | .row-fluid .offset4:first-child { 335 | margin-left: 34.18803418803419%; 336 | *margin-left: 34.081651209310785%; 337 | } 338 | .row-fluid .offset3 { 339 | margin-left: 28.205128205128204%; 340 | *margin-left: 28.0987452264048%; 341 | } 342 | .row-fluid .offset3:first-child { 343 | margin-left: 25.641025641025642%; 344 | *margin-left: 25.53464266230224%; 345 | } 346 | .row-fluid .offset2 { 347 | margin-left: 19.65811965811966%; 348 | *margin-left: 19.551736679396257%; 349 | } 350 | .row-fluid .offset2:first-child { 351 | margin-left: 17.094017094017094%; 352 | *margin-left: 16.98763411529369%; 353 | } 354 | .row-fluid .offset1 { 355 | margin-left: 11.11111111111111%; 356 | *margin-left: 11.004728132387708%; 357 | } 358 | .row-fluid .offset1:first-child { 359 | margin-left: 8.547008547008547%; 360 | *margin-left: 8.440625568285142%; 361 | } 362 | input, 363 | textarea, 364 | .uneditable-input { 365 | margin-left: 0; 366 | } 367 | .controls-row [class*="span"] + [class*="span"] { 368 | margin-left: 30px; 369 | } 370 | input.span12, 371 | textarea.span12, 372 | .uneditable-input.span12 { 373 | width: 1156px; 374 | } 375 | input.span11, 376 | textarea.span11, 377 | .uneditable-input.span11 { 378 | width: 1056px; 379 | } 380 | input.span10, 381 | textarea.span10, 382 | .uneditable-input.span10 { 383 | width: 956px; 384 | } 385 | input.span9, 386 | textarea.span9, 387 | .uneditable-input.span9 { 388 | width: 856px; 389 | } 390 | input.span8, 391 | textarea.span8, 392 | .uneditable-input.span8 { 393 | width: 756px; 394 | } 395 | input.span7, 396 | textarea.span7, 397 | .uneditable-input.span7 { 398 | width: 656px; 399 | } 400 | input.span6, 401 | textarea.span6, 402 | .uneditable-input.span6 { 403 | width: 556px; 404 | } 405 | input.span5, 406 | textarea.span5, 407 | .uneditable-input.span5 { 408 | width: 456px; 409 | } 410 | input.span4, 411 | textarea.span4, 412 | .uneditable-input.span4 { 413 | width: 356px; 414 | } 415 | input.span3, 416 | textarea.span3, 417 | .uneditable-input.span3 { 418 | width: 256px; 419 | } 420 | input.span2, 421 | textarea.span2, 422 | .uneditable-input.span2 { 423 | width: 156px; 424 | } 425 | input.span1, 426 | textarea.span1, 427 | .uneditable-input.span1 { 428 | width: 56px; 429 | } 430 | .thumbnails { 431 | margin-left: -30px; 432 | } 433 | .thumbnails > li { 434 | margin-left: 30px; 435 | } 436 | .row-fluid .thumbnails { 437 | margin-left: 0; 438 | } 439 | } 440 | 441 | @media (min-width: 768px) and (max-width: 979px) { 442 | .row { 443 | margin-left: -20px; 444 | *zoom: 1; 445 | } 446 | .row:before, 447 | .row:after { 448 | display: table; 449 | line-height: 0; 450 | content: ""; 451 | } 452 | .row:after { 453 | clear: both; 454 | } 455 | [class*="span"] { 456 | float: left; 457 | min-height: 1px; 458 | margin-left: 20px; 459 | } 460 | .container, 461 | .navbar-static-top .container, 462 | .navbar-fixed-top .container, 463 | .navbar-fixed-bottom .container { 464 | width: 724px; 465 | } 466 | .span12 { 467 | width: 724px; 468 | } 469 | .span11 { 470 | width: 662px; 471 | } 472 | .span10 { 473 | width: 600px; 474 | } 475 | .span9 { 476 | width: 538px; 477 | } 478 | .span8 { 479 | width: 476px; 480 | } 481 | .span7 { 482 | width: 414px; 483 | } 484 | .span6 { 485 | width: 352px; 486 | } 487 | .span5 { 488 | width: 290px; 489 | } 490 | .span4 { 491 | width: 228px; 492 | } 493 | .span3 { 494 | width: 166px; 495 | } 496 | .span2 { 497 | width: 104px; 498 | } 499 | .span1 { 500 | width: 42px; 501 | } 502 | .offset12 { 503 | margin-left: 764px; 504 | } 505 | .offset11 { 506 | margin-left: 702px; 507 | } 508 | .offset10 { 509 | margin-left: 640px; 510 | } 511 | .offset9 { 512 | margin-left: 578px; 513 | } 514 | .offset8 { 515 | margin-left: 516px; 516 | } 517 | .offset7 { 518 | margin-left: 454px; 519 | } 520 | .offset6 { 521 | margin-left: 392px; 522 | } 523 | .offset5 { 524 | margin-left: 330px; 525 | } 526 | .offset4 { 527 | margin-left: 268px; 528 | } 529 | .offset3 { 530 | margin-left: 206px; 531 | } 532 | .offset2 { 533 | margin-left: 144px; 534 | } 535 | .offset1 { 536 | margin-left: 82px; 537 | } 538 | .row-fluid { 539 | width: 100%; 540 | *zoom: 1; 541 | } 542 | .row-fluid:before, 543 | .row-fluid:after { 544 | display: table; 545 | line-height: 0; 546 | content: ""; 547 | } 548 | .row-fluid:after { 549 | clear: both; 550 | } 551 | .row-fluid [class*="span"] { 552 | display: block; 553 | float: left; 554 | width: 100%; 555 | min-height: 30px; 556 | margin-left: 2.7624309392265194%; 557 | *margin-left: 2.709239449864817%; 558 | -webkit-box-sizing: border-box; 559 | -moz-box-sizing: border-box; 560 | box-sizing: border-box; 561 | } 562 | .row-fluid [class*="span"]:first-child { 563 | margin-left: 0; 564 | } 565 | .row-fluid .span12 { 566 | width: 100%; 567 | *width: 99.94680851063829%; 568 | } 569 | .row-fluid .span11 { 570 | width: 91.43646408839778%; 571 | *width: 91.38327259903608%; 572 | } 573 | .row-fluid .span10 { 574 | width: 82.87292817679558%; 575 | *width: 82.81973668743387%; 576 | } 577 | .row-fluid .span9 { 578 | width: 74.30939226519337%; 579 | *width: 74.25620077583166%; 580 | } 581 | .row-fluid .span8 { 582 | width: 65.74585635359117%; 583 | *width: 65.69266486422946%; 584 | } 585 | .row-fluid .span7 { 586 | width: 57.18232044198895%; 587 | *width: 57.12912895262725%; 588 | } 589 | .row-fluid .span6 { 590 | width: 48.61878453038674%; 591 | *width: 48.56559304102504%; 592 | } 593 | .row-fluid .span5 { 594 | width: 40.05524861878453%; 595 | *width: 40.00205712942283%; 596 | } 597 | .row-fluid .span4 { 598 | width: 31.491712707182323%; 599 | *width: 31.43852121782062%; 600 | } 601 | .row-fluid .span3 { 602 | width: 22.92817679558011%; 603 | *width: 22.87498530621841%; 604 | } 605 | .row-fluid .span2 { 606 | width: 14.3646408839779%; 607 | *width: 14.311449394616199%; 608 | } 609 | .row-fluid .span1 { 610 | width: 5.801104972375691%; 611 | *width: 5.747913483013988%; 612 | } 613 | .row-fluid .offset12 { 614 | margin-left: 105.52486187845304%; 615 | *margin-left: 105.41847889972962%; 616 | } 617 | .row-fluid .offset12:first-child { 618 | margin-left: 102.76243093922652%; 619 | *margin-left: 102.6560479605031%; 620 | } 621 | .row-fluid .offset11 { 622 | margin-left: 96.96132596685082%; 623 | *margin-left: 96.8549429881274%; 624 | } 625 | .row-fluid .offset11:first-child { 626 | margin-left: 94.1988950276243%; 627 | *margin-left: 94.09251204890089%; 628 | } 629 | .row-fluid .offset10 { 630 | margin-left: 88.39779005524862%; 631 | *margin-left: 88.2914070765252%; 632 | } 633 | .row-fluid .offset10:first-child { 634 | margin-left: 85.6353591160221%; 635 | *margin-left: 85.52897613729868%; 636 | } 637 | .row-fluid .offset9 { 638 | margin-left: 79.8342541436464%; 639 | *margin-left: 79.72787116492299%; 640 | } 641 | .row-fluid .offset9:first-child { 642 | margin-left: 77.07182320441989%; 643 | *margin-left: 76.96544022569647%; 644 | } 645 | .row-fluid .offset8 { 646 | margin-left: 71.2707182320442%; 647 | *margin-left: 71.16433525332079%; 648 | } 649 | .row-fluid .offset8:first-child { 650 | margin-left: 68.50828729281768%; 651 | *margin-left: 68.40190431409427%; 652 | } 653 | .row-fluid .offset7 { 654 | margin-left: 62.70718232044199%; 655 | *margin-left: 62.600799341718584%; 656 | } 657 | .row-fluid .offset7:first-child { 658 | margin-left: 59.94475138121547%; 659 | *margin-left: 59.838368402492065%; 660 | } 661 | .row-fluid .offset6 { 662 | margin-left: 54.14364640883978%; 663 | *margin-left: 54.037263430116376%; 664 | } 665 | .row-fluid .offset6:first-child { 666 | margin-left: 51.38121546961326%; 667 | *margin-left: 51.27483249088986%; 668 | } 669 | .row-fluid .offset5 { 670 | margin-left: 45.58011049723757%; 671 | *margin-left: 45.47372751851417%; 672 | } 673 | .row-fluid .offset5:first-child { 674 | margin-left: 42.81767955801105%; 675 | *margin-left: 42.71129657928765%; 676 | } 677 | .row-fluid .offset4 { 678 | margin-left: 37.01657458563536%; 679 | *margin-left: 36.91019160691196%; 680 | } 681 | .row-fluid .offset4:first-child { 682 | margin-left: 34.25414364640884%; 683 | *margin-left: 34.14776066768544%; 684 | } 685 | .row-fluid .offset3 { 686 | margin-left: 28.45303867403315%; 687 | *margin-left: 28.346655695309746%; 688 | } 689 | .row-fluid .offset3:first-child { 690 | margin-left: 25.69060773480663%; 691 | *margin-left: 25.584224756083227%; 692 | } 693 | .row-fluid .offset2 { 694 | margin-left: 19.88950276243094%; 695 | *margin-left: 19.783119783707537%; 696 | } 697 | .row-fluid .offset2:first-child { 698 | margin-left: 17.12707182320442%; 699 | *margin-left: 17.02068884448102%; 700 | } 701 | .row-fluid .offset1 { 702 | margin-left: 11.32596685082873%; 703 | *margin-left: 11.219583872105325%; 704 | } 705 | .row-fluid .offset1:first-child { 706 | margin-left: 8.56353591160221%; 707 | *margin-left: 8.457152932878806%; 708 | } 709 | input, 710 | textarea, 711 | .uneditable-input { 712 | margin-left: 0; 713 | } 714 | .controls-row [class*="span"] + [class*="span"] { 715 | margin-left: 20px; 716 | } 717 | input.span12, 718 | textarea.span12, 719 | .uneditable-input.span12 { 720 | width: 710px; 721 | } 722 | input.span11, 723 | textarea.span11, 724 | .uneditable-input.span11 { 725 | width: 648px; 726 | } 727 | input.span10, 728 | textarea.span10, 729 | .uneditable-input.span10 { 730 | width: 586px; 731 | } 732 | input.span9, 733 | textarea.span9, 734 | .uneditable-input.span9 { 735 | width: 524px; 736 | } 737 | input.span8, 738 | textarea.span8, 739 | .uneditable-input.span8 { 740 | width: 462px; 741 | } 742 | input.span7, 743 | textarea.span7, 744 | .uneditable-input.span7 { 745 | width: 400px; 746 | } 747 | input.span6, 748 | textarea.span6, 749 | .uneditable-input.span6 { 750 | width: 338px; 751 | } 752 | input.span5, 753 | textarea.span5, 754 | .uneditable-input.span5 { 755 | width: 276px; 756 | } 757 | input.span4, 758 | textarea.span4, 759 | .uneditable-input.span4 { 760 | width: 214px; 761 | } 762 | input.span3, 763 | textarea.span3, 764 | .uneditable-input.span3 { 765 | width: 152px; 766 | } 767 | input.span2, 768 | textarea.span2, 769 | .uneditable-input.span2 { 770 | width: 90px; 771 | } 772 | input.span1, 773 | textarea.span1, 774 | .uneditable-input.span1 { 775 | width: 28px; 776 | } 777 | } 778 | 779 | @media (max-width: 767px) { 780 | body { 781 | padding-right: 20px; 782 | padding-left: 20px; 783 | } 784 | .navbar-fixed-top, 785 | .navbar-fixed-bottom, 786 | .navbar-static-top { 787 | margin-right: -20px; 788 | margin-left: -20px; 789 | } 790 | .container-fluid { 791 | padding: 0; 792 | } 793 | .dl-horizontal dt { 794 | float: none; 795 | width: auto; 796 | clear: none; 797 | text-align: left; 798 | } 799 | .dl-horizontal dd { 800 | margin-left: 0; 801 | } 802 | .container { 803 | width: auto; 804 | } 805 | .row-fluid { 806 | width: 100%; 807 | } 808 | .row, 809 | .thumbnails { 810 | margin-left: 0; 811 | } 812 | .thumbnails > li { 813 | float: none; 814 | margin-left: 0; 815 | } 816 | [class*="span"], 817 | .row-fluid [class*="span"] { 818 | display: block; 819 | float: none; 820 | width: 100%; 821 | margin-left: 0; 822 | -webkit-box-sizing: border-box; 823 | -moz-box-sizing: border-box; 824 | box-sizing: border-box; 825 | } 826 | .span12, 827 | .row-fluid .span12 { 828 | width: 100%; 829 | -webkit-box-sizing: border-box; 830 | -moz-box-sizing: border-box; 831 | box-sizing: border-box; 832 | } 833 | .input-large, 834 | .input-xlarge, 835 | .input-xxlarge, 836 | input[class*="span"], 837 | select[class*="span"], 838 | textarea[class*="span"], 839 | .uneditable-input { 840 | display: block; 841 | width: 100%; 842 | min-height: 30px; 843 | -webkit-box-sizing: border-box; 844 | -moz-box-sizing: border-box; 845 | box-sizing: border-box; 846 | } 847 | .input-prepend input, 848 | .input-append input, 849 | .input-prepend input[class*="span"], 850 | .input-append input[class*="span"] { 851 | display: inline-block; 852 | width: auto; 853 | } 854 | .controls-row [class*="span"] + [class*="span"] { 855 | margin-left: 0; 856 | } 857 | .modal { 858 | position: fixed; 859 | top: 20px; 860 | right: 20px; 861 | left: 20px; 862 | width: auto; 863 | margin: 0; 864 | } 865 | .modal.fade.in { 866 | top: auto; 867 | } 868 | } 869 | 870 | @media (max-width: 480px) { 871 | .nav-collapse { 872 | -webkit-transform: translate3d(0, 0, 0); 873 | } 874 | .page-header h1 small { 875 | display: block; 876 | line-height: 20px; 877 | } 878 | input[type="checkbox"], 879 | input[type="radio"] { 880 | border: 1px solid #ccc; 881 | } 882 | .form-horizontal .control-label { 883 | float: none; 884 | width: auto; 885 | padding-top: 0; 886 | text-align: left; 887 | } 888 | .form-horizontal .controls { 889 | margin-left: 0; 890 | } 891 | .form-horizontal .control-list { 892 | padding-top: 0; 893 | } 894 | .form-horizontal .form-actions { 895 | padding-right: 10px; 896 | padding-left: 10px; 897 | } 898 | .modal { 899 | top: 10px; 900 | right: 10px; 901 | left: 10px; 902 | } 903 | .modal-header .close { 904 | padding: 10px; 905 | margin: -10px; 906 | } 907 | .carousel-caption { 908 | position: static; 909 | } 910 | } 911 | 912 | @media (max-width: 979px) { 913 | body { 914 | padding-top: 0; 915 | } 916 | .navbar-fixed-top, 917 | .navbar-fixed-bottom { 918 | position: static; 919 | } 920 | .navbar-fixed-top { 921 | margin-bottom: 20px; 922 | } 923 | .navbar-fixed-bottom { 924 | margin-top: 20px; 925 | } 926 | .navbar-fixed-top .navbar-inner, 927 | .navbar-fixed-bottom .navbar-inner { 928 | padding: 5px; 929 | } 930 | .navbar .container { 931 | width: auto; 932 | padding: 0; 933 | } 934 | .navbar .brand { 935 | padding-right: 10px; 936 | padding-left: 10px; 937 | margin: 0 0 0 -5px; 938 | } 939 | .nav-collapse { 940 | clear: both; 941 | } 942 | .nav-collapse .nav { 943 | float: none; 944 | margin: 0 0 10px; 945 | } 946 | .nav-collapse .nav > li { 947 | float: none; 948 | } 949 | .nav-collapse .nav > li > a { 950 | margin-bottom: 2px; 951 | } 952 | .nav-collapse .nav > .divider-vertical { 953 | display: none; 954 | } 955 | .nav-collapse .nav .nav-header { 956 | color: #777777; 957 | text-shadow: none; 958 | } 959 | .nav-collapse .nav > li > a, 960 | .nav-collapse .dropdown-menu a { 961 | padding: 9px 15px; 962 | font-weight: bold; 963 | color: #777777; 964 | -webkit-border-radius: 3px; 965 | -moz-border-radius: 3px; 966 | border-radius: 3px; 967 | } 968 | .nav-collapse .btn { 969 | padding: 4px 10px 4px; 970 | font-weight: normal; 971 | -webkit-border-radius: 4px; 972 | -moz-border-radius: 4px; 973 | border-radius: 4px; 974 | } 975 | .nav-collapse .dropdown-menu li + li a { 976 | margin-bottom: 2px; 977 | } 978 | .nav-collapse .nav > li > a:hover, 979 | .nav-collapse .dropdown-menu a:hover { 980 | background-color: #f2f2f2; 981 | } 982 | .navbar-inverse .nav-collapse .nav > li > a:hover, 983 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 984 | background-color: #111111; 985 | } 986 | .nav-collapse.in .btn-group { 987 | padding: 0; 988 | margin-top: 5px; 989 | } 990 | .nav-collapse .dropdown-menu { 991 | position: static; 992 | top: auto; 993 | left: auto; 994 | display: block; 995 | float: none; 996 | max-width: none; 997 | padding: 0; 998 | margin: 0 15px; 999 | background-color: transparent; 1000 | border: none; 1001 | -webkit-border-radius: 0; 1002 | -moz-border-radius: 0; 1003 | border-radius: 0; 1004 | -webkit-box-shadow: none; 1005 | -moz-box-shadow: none; 1006 | box-shadow: none; 1007 | } 1008 | .nav-collapse .dropdown-menu:before, 1009 | .nav-collapse .dropdown-menu:after { 1010 | display: none; 1011 | } 1012 | .nav-collapse .dropdown-menu .divider { 1013 | display: none; 1014 | } 1015 | .nav-collapse .nav > li > .dropdown-menu:before, 1016 | .nav-collapse .nav > li > .dropdown-menu:after { 1017 | display: none; 1018 | } 1019 | .nav-collapse .navbar-form, 1020 | .nav-collapse .navbar-search { 1021 | float: none; 1022 | padding: 10px 15px; 1023 | margin: 10px 0; 1024 | border-top: 1px solid #f2f2f2; 1025 | border-bottom: 1px solid #f2f2f2; 1026 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1027 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1028 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1029 | } 1030 | .navbar-inverse .nav-collapse .navbar-form, 1031 | .navbar-inverse .nav-collapse .navbar-search { 1032 | border-top-color: #111111; 1033 | border-bottom-color: #111111; 1034 | } 1035 | .navbar .nav-collapse .nav.pull-right { 1036 | float: none; 1037 | margin-left: 0; 1038 | } 1039 | .nav-collapse, 1040 | .nav-collapse.collapse { 1041 | height: 0; 1042 | overflow: hidden; 1043 | } 1044 | .navbar .btn-navbar { 1045 | display: block; 1046 | } 1047 | .navbar-static .navbar-inner { 1048 | padding-right: 10px; 1049 | padding-left: 10px; 1050 | } 1051 | } 1052 | 1053 | @media (min-width: 980px) { 1054 | .nav-collapse.collapse { 1055 | height: auto !important; 1056 | overflow: visible !important; 1057 | } 1058 | } 1059 | -------------------------------------------------------------------------------- /docs/css/default.css: -------------------------------------------------------------------------------- 1 | /* Set the body padding-top when above 980px to push the content down from 2 | below the navbar, which is fixed at >980px screen widths. */ 3 | pre { 4 | font-size: 12px; 5 | } 6 | 7 | .dropdown .dropdown-menu { 8 | display: none; 9 | } 10 | 11 | .dropdown.open .dropdown-menu { 12 | display: block; 13 | } 14 | 15 | @media (max-width: 480px) { 16 | .repo-link { 17 | display: none; 18 | } 19 | } 20 | 21 | /* Header link to GitHub */ 22 | .repo-link { 23 | float: right; 24 | margin-right: 10px; 25 | margin-top: 9px; 26 | } 27 | 28 | body.index-page #main-content p.badges { 29 | padding-bottom: 1px; 30 | } 31 | 32 | /* GitHub 'Star' badge */ 33 | body.index-page #main-content iframe.github-star-button { 34 | float: right; 35 | margin-top: -12px; 36 | margin-right: -15px; 37 | } 38 | 39 | /* Travis CI badge */ 40 | body.index-page #main-content img.travis-build-image { 41 | float: right; 42 | margin-right: 8px; 43 | margin-top: -11px; 44 | margin-bottom: 0px; 45 | } 46 | 47 | /* Github source file badges */ 48 | a.github { 49 | float: right; 50 | margin-top: -12px; 51 | margin-right: 12px; 52 | } 53 | 54 | a.github:hover { 55 | text-decoration: none; 56 | } 57 | 58 | /* */ 59 | body hr { 60 | border-top: 1px dotted #A30000; 61 | } 62 | 63 | /* Force TOC text to not overrun */ 64 | #table-of-contents { 65 | overflow: hidden; 66 | margin: 0 0 20px 0; 67 | } 68 | 69 | /* Code blocks should scroll horizontally */ 70 | pre { 71 | overflow: auto; 72 | word-wrap: normal; 73 | white-space: pre; 74 | } 75 | 76 | /* Preserve the spacing of the navbar across different screen sizes. */ 77 | .navbar-inner { 78 | /*padding: 5px 0;*/ 79 | } 80 | 81 | @media (max-width: 979px) { 82 | .navbar .brand { 83 | margin-left: 0; 84 | padding-left: 0; 85 | } 86 | .navbar-inner .container-fluid { 87 | padding-left: 15px; 88 | } 89 | } 90 | 91 | .nav-list li.main { 92 | font-weight: bold; 93 | } 94 | 95 | .nav-list a { 96 | overflow: hidden; 97 | } 98 | 99 | .nav-list > li > a { 100 | padding: 2px 15px 3px; 101 | } 102 | 103 | /* Set the table of contents to static so it flows back into the content when 104 | viewed on tablets and smaller. */ 105 | @media (max-width: 767px) { 106 | #table-of-contents { 107 | position: static; 108 | } 109 | } 110 | 111 | /* When the page is in two-column layout, give the main content some room 112 | to breath on the left. */ 113 | @media (min-width: 768px) { 114 | #main-content { 115 | padding-left: 1em; 116 | } 117 | } 118 | 119 | /* Cutesy quote styling */ 120 | blockquote { 121 | font-family: Georgia, serif; 122 | font-size: 18px; 123 | font-style: italic; 124 | margin: 0.25em 0; 125 | padding: 0.25em 40px; 126 | line-height: 1.45; 127 | position: relative; 128 | color: #383838; 129 | border-left: none; 130 | } 131 | 132 | blockquote:before { 133 | display: block; 134 | content: "\201C"; 135 | font-size: 80px; 136 | position: absolute; 137 | left: -10px; 138 | top: -20px; 139 | color: #7a7a7a; 140 | } 141 | 142 | blockquote p:last-child { 143 | color: #999999; 144 | font-size: 14px; 145 | display: block; 146 | margin-top: 5px; 147 | } 148 | 149 | 150 | /*=== dabapps bootstrap styles ====*/ 151 | 152 | html{ 153 | width:100%; 154 | background: none; 155 | } 156 | 157 | body, .navbar .navbar-inner .container-fluid{ 158 | max-width: 1150px; 159 | margin: 0 auto; 160 | } 161 | 162 | body{ 163 | background: url("../img/grid.png") repeat-x; 164 | background-attachment: fixed; 165 | } 166 | 167 | /* custom navigation styles */ 168 | 169 | .navbar .navbar-inner{ 170 | background: #2C2C2C; 171 | color: white; 172 | border: none; 173 | border-top: 5px solid #A30000; 174 | } 175 | 176 | .navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{ 177 | color: white; 178 | } 179 | 180 | .nav-list > .active > a, .navbar .navbar-inner .nav li a:hover { 181 | background:#212121; 182 | color:white; 183 | } 184 | 185 | .navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ 186 | color: #A30000; 187 | } 188 | .navbar .navbar-inner .dropdown-menu li a:hover{ 189 | background: #eeeeee; 190 | color: #c20000; 191 | } 192 | 193 | /* custom general page styles */ 194 | .hero-unit h2, .hero-unit h1{ 195 | color: #A30000; 196 | } 197 | 198 | body a{ 199 | color: #A30000; 200 | } 201 | 202 | body a:hover{ 203 | color: #c20000; 204 | } 205 | 206 | /* subnavigation styles */ 207 | 208 | @media (min-width: 767px) { 209 | .sidebar-nav-fixed { 210 | position:fixed; 211 | width:19%; 212 | max-width: 240px; 213 | } 214 | 215 | .navbar { 216 | position: fixed; 217 | } 218 | .navbar .navbar-inner .container-fluid{ 219 | max-width: 1110px; 220 | } 221 | } 222 | 223 | /* sticky footer and footer */ 224 | html, body { 225 | height: 100%; 226 | } 227 | .wrapper { 228 | min-height: 100%; 229 | height: auto !important; 230 | height: 100%; 231 | margin: 0 auto -60px; 232 | } 233 | 234 | .body-content{ 235 | padding-top: 70px; 236 | padding-bottom: 70px; 237 | } 238 | 239 | @media (max-width: 979px) { 240 | .navbar-fixed-top .navbar-inner { 241 | padding: 0px; 242 | } 243 | } 244 | 245 | @media (max-width: 767px) { 246 | .body-content{ 247 | padding-top: 0px; 248 | } 249 | } 250 | 251 | @media (min-width: 768px) { 252 | footer.span12 { 253 | width: 95%; 254 | } 255 | } 256 | 257 | footer, .push { 258 | height: 60px; /* .push must be the same height as .footer */ 259 | } 260 | 261 | 262 | footer p { 263 | text-align: center; 264 | color: gray; 265 | border-top: 1px solid #DDD; 266 | padding-top: 10px; 267 | } 268 | 269 | footer a { 270 | color: gray; 271 | font-weight: bold; 272 | } 273 | 274 | footer a:hover { 275 | color: gray; 276 | } 277 | 278 | .btn-inverse { 279 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#606060), to(#404040)) !important; 280 | background-image: -webkit-linear-gradient(top, #606060, #404040) !important; 281 | } 282 | 283 | .modal-open .modal,.btn:focus{outline:none;} 284 | 285 | @media (max-width: 650px) { 286 | .repo-link.btn-inverse {display: none;} 287 | } 288 | 289 | td, th { 290 | padding: 0.25em; 291 | background-color: #f7f7f9; 292 | border-color: #e1e1e8; 293 | } 294 | 295 | table { 296 | border-color: white; 297 | margin-bottom: 0.6em; 298 | } 299 | 300 | .side-nav { 301 | overflow-y: scroll; 302 | } 303 | -------------------------------------------------------------------------------- /docs/css/prettify.css: -------------------------------------------------------------------------------- 1 | .com { color: #93a1a1; } 2 | .lit { color: #195f91; } 3 | .pun, .opn, .clo { color: #93a1a1; } 4 | .fun { color: #dc322f; } 5 | .str, .atv { color: #D14; } 6 | .kwd, .prettyprint .tag { color: #1e347b; } 7 | .typ, .atn, .dec, .var { color: teal; } 8 | .pln { color: #48484c; } 9 | 10 | .prettyprint { 11 | padding: 8px; 12 | background-color: #f7f7f9; 13 | border: 1px solid #e1e1e8; 14 | } 15 | .prettyprint.linenums { 16 | -webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 17 | -moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 18 | box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 19 | } 20 | 21 | /* Specify class=linenums on a pre to get line numbering */ 22 | ol.linenums { 23 | margin: 0 0 0 33px; /* IE indents via margin-left */ 24 | } 25 | ol.linenums li { 26 | padding-left: 12px; 27 | color: #bebec5; 28 | line-height: 20px; 29 | text-shadow: 0 1px 0 #fff; 30 | } 31 | -------------------------------------------------------------------------------- /docs/img/djangocbv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/djangocbv.png -------------------------------------------------------------------------------- /docs/img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/example.png -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /docs/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /docs/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/django-vanilla-views/8329c902a50aed0f49c1c27d7bf615f3a3d704e9/docs/img/grid.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | # Django Vanilla Views 7 | 8 | **Beautifully simple class-based views.** 9 | 10 | [![Build Status](https://img.shields.io/github/workflow/status/encode/django-vanilla-views/CI/master?style=for-the-badge)](https://github.com/encode/django-vanilla-views/actions?workflow=CI) [![PyPI version](https://img.shields.io/pypi/v/django-vanilla-views.svg?style=for-the-badge)](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 | ![image](img/example.png) 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('