├── tests ├── __init__.py ├── templates │ ├── blank.html │ ├── 404.html │ └── form.html ├── urls_namespaced.py ├── forms.py ├── compat.py ├── models.py ├── test_forms.py ├── factories.py ├── settings.py ├── helpers.py ├── urls.py ├── test_ajax_mixins.py ├── views.py ├── test_access_mixins.py └── test_other_mixins.py ├── setup.cfg ├── requirements-docs.txt ├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── .coveragerc ├── conftest.py ├── braces ├── __init__.py ├── forms.py └── views │ ├── __init__.py │ ├── _queries.py │ ├── _other.py │ ├── _ajax.py │ ├── _forms.py │ └── _access.py ├── CONTRIBUTORS.txt ├── .travis.yml ├── docs ├── index.rst ├── contributing.rst ├── Makefile ├── changelog.rst ├── form.rst ├── conf.py ├── access.rst └── other.rst ├── setup.py ├── LICENSE ├── README.md └── tox.ini /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/templates/blank.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | releases 3 | -------------------------------------------------------------------------------- /tests/templates/404.html: -------------------------------------------------------------------------------- 1 |

404!!!!

2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | factory_boy 2 | mock 3 | pytest-django 4 | pytest-cov 5 | six 6 | coverage 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include CONTRIBUTORS.txt 4 | recursive-include braces *.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .python-version 3 | ._* 4 | *.pyc 5 | *.egg-info 6 | /docs/_build/ 7 | /.coverage 8 | /.coverage.xml 9 | /htmlcov 10 | /.tox 11 | dist/ 12 | .idea 13 | build/ 14 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # django-braces coverage config file 2 | [run] 3 | branch = true 4 | 5 | [report] 6 | omit = 7 | *site-packages* 8 | *tests* 9 | *.tox* 10 | *conftest* 11 | show_missing = True 12 | -------------------------------------------------------------------------------- /tests/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if messages %} 5 | {% for message in messages %} 6 | {{ message }} 7 | {% endfor %} 8 | {% endif %} 9 | 10 | {{ form.as_p }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | from tests import settings as test_settings 4 | 5 | 6 | def pytest_configure(): 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 8 | settings.configure(default_settings=test_settings) 9 | -------------------------------------------------------------------------------- /tests/urls_namespaced.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import views 4 | from .compat import patterns, url 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | # CanonicalSlugDetailMixin namespace tests 10 | url(r'^article/(?P\d+)-(?P[\w-]+)/$', 11 | views.CanonicalSlugDetailView.as_view(), 12 | name="namespaced_article"), 13 | ) 14 | -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django import forms 4 | 5 | from braces.forms import UserKwargModelFormMixin 6 | 7 | from .models import Article 8 | 9 | 10 | class FormWithUserKwarg(UserKwargModelFormMixin, forms.Form): 11 | field1 = forms.CharField() 12 | 13 | 14 | class ArticleForm(forms.ModelForm): 15 | class Meta: 16 | model = Article 17 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.utils.encoding import force_text 3 | except ImportError: 4 | from django.utils.encoding import force_unicode as force_text 5 | 6 | try: 7 | import json 8 | except ImportError: 9 | from django.utils import simplejson as json 10 | 11 | try: 12 | from django.conf.urls import patterns, url, include 13 | except ImportError: 14 | from django.conf.urls.defaults import patterns, url, include 15 | -------------------------------------------------------------------------------- /braces/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-braces mixins library 3 | ---------------------------- 4 | 5 | Several mixins for making Django's generic class-based views more useful. 6 | 7 | :copyright: (c) 2013 by Kenneth Love and Chris Jones 8 | :license: BSD 3-clause. See LICENSE for more details 9 | """ 10 | 11 | __title__ = 'braces' 12 | __version__ = '1.4.0' 13 | __author__ = 'Kenneth Love and Chris Jones' 14 | __license__ = 'BSD 3-clause' 15 | __copyright__ = 'Copyright 2013 Kenneth Love and Chris Jones' 16 | -------------------------------------------------------------------------------- /braces/forms.py: -------------------------------------------------------------------------------- 1 | class UserKwargModelFormMixin(object): 2 | """ 3 | Generic model form mixin for popping user out of the kwargs and 4 | attaching it to the instance. 5 | 6 | This mixin must precede forms.ModelForm/forms.Form. The form is not 7 | expecting these kwargs to be passed in, so they must be popped off before 8 | anything else is done. 9 | """ 10 | def __init__(self, *args, **kwargs): 11 | self.user = kwargs.pop("user", None) # Pop the user off the 12 | # passed in kwargs. 13 | super(UserKwargModelFormMixin, self).__init__(*args, **kwargs) 14 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | ==== 2 | Team 3 | ==== 4 | 5 | Project Leads 6 | ============= 7 | 8 | * Kenneth Love 9 | * Chris Jones 10 | 11 | Direct Contributors 12 | =================== 13 | 14 | * Daniel Greenfeld 15 | * Drew Tempelmeyer 16 | * Baptiste Mispelon 17 | * Derek Payton 18 | * Rafal Stozek 19 | * Ethan Soergel 20 | * Piotr Kilczuk 21 | * Rodney Folz 22 | * Markus Zapke-Gründemann 23 | * Kamil Gałuszka 24 | * Danilo Bargen 25 | * Jon Bolt 26 | * Kit Sunde 27 | * Ben Cardy 28 | * Rag Sagar.V 29 | 30 | Other Contributors 31 | ================== 32 | 33 | * The entire Python and Django communities for providing us the tools and desire we to build these things. 34 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Article(models.Model): 5 | author = models.ForeignKey('auth.User', null=True, blank=True) 6 | title = models.CharField(max_length=30) 7 | body = models.TextField() 8 | slug = models.SlugField(blank=True) 9 | 10 | 11 | class CanonicalArticle(models.Model): 12 | author = models.ForeignKey('auth.User', null=True, blank=True) 13 | title = models.CharField(max_length=30) 14 | body = models.TextField() 15 | slug = models.SlugField(blank=True) 16 | 17 | def get_canonical_slug(self): 18 | if self.author: 19 | return "{0.author.username}-{0.slug}".format(self) 20 | return "unauthored-{0.slug}".format(self) 21 | 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | services: sqlite 3 | env: 4 | - DJANGO='django>=1.4,<1.5' 5 | - DJANGO='django>=1.5,<1.6' 6 | - DJANGO='django>=1.6,<1.7' 7 | - DJANGO='django>=1.7,<1.8' 8 | python: 9 | - 3.4 10 | - 3.3 11 | - 2.7 12 | - 2.6 13 | - pypy 14 | - pypy3 15 | install: 16 | - pip install $DJANGO 17 | - python setup.py install 18 | - pip install -r requirements.txt 19 | script: py.test tests/ 20 | matrix: 21 | exclude: 22 | - python: 3.3 23 | env: DJANGO='django>=1.4,<1.5' 24 | - python: 3.4 25 | env: DJANGO='django>=1.4,<1.5' 26 | - python: pypy3 27 | env: DJANGO='django>=1.4,<1.5' 28 | - python: 2.6 29 | env: DJANGO='django>=1.7,<1.8' 30 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django import test 4 | from django.contrib.auth.models import User 5 | 6 | from . import forms 7 | 8 | 9 | class TestUserKwargModelFormMixin(test.TestCase): 10 | """ 11 | Tests for UserKwargModelFormMixin. 12 | """ 13 | def test_without_user_kwarg(self): 14 | """ 15 | It should be possible to create form without 'user' kwarg. 16 | 17 | In that case 'user' attribute should be set to None. 18 | """ 19 | form = forms.FormWithUserKwarg() 20 | assert form.user is None 21 | 22 | def test_with_user_kwarg(self): 23 | """ 24 | Form's 'user' attribute should be set to value passed as 'user' 25 | argument. 26 | """ 27 | user = User(username='test') 28 | form = forms.FormWithUserKwarg(user=user) 29 | assert form.user is user 30 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-braces documentation master file, created by 2 | sphinx-quickstart on Mon Apr 30 10:31:44 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-braces's documentation! 7 | ========================================= 8 | 9 | You can view the code of our project or fork it and add your own mixins (please, send them back to us), on `Github`_. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Access Mixins 15 | Form Mixins
16 | Other Mixins 17 | 18 | `View our Changelog `_ 19 | 20 | `Want to contribute? `_ 21 | 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | 32 | .. _Github: https://github.com/brack3t/django-braces 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import braces 4 | 5 | setup( 6 | name="django-braces", 7 | version=braces.__version__, 8 | description="Reusable, generic mixins for Django", 9 | long_description="Mixins to add easy functionality to Django class-based views, forms, and models.", 10 | keywords="django, views, forms, mixins", 11 | author="Kenneth Love , Chris Jones ", 12 | author_email="devs@brack3t.com", 13 | url="https://github.com/brack3t/django-braces/", 14 | license="BSD", 15 | packages=["braces"], 16 | zip_safe=False, 17 | install_requires=["six"], 18 | include_package_data=True, 19 | classifiers=[ 20 | "Programming Language :: Python", 21 | "Topic :: Software Development :: Libraries :: Python Modules", 22 | "Framework :: Django", 23 | "Environment :: Web Environment", 24 | "Development Status :: 5 - Production/Stable", 25 | "Programming Language :: Python :: 2.6", 26 | "Programming Language :: Python :: 2.7", 27 | "Programming Language :: Python :: 3.2", 28 | "Programming Language :: Python :: 3.3" 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Brack3t and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Brack3t nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /braces/views/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from ._access import ( 4 | AnonymousRequiredMixin, 5 | GroupRequiredMixin, 6 | LoginRequiredMixin, 7 | MultiplePermissionsRequiredMixin, 8 | PermissionRequiredMixin, 9 | StaffuserRequiredMixin, 10 | SuperuserRequiredMixin, 11 | UserPassesTestMixin 12 | ) 13 | from ._ajax import ( 14 | AjaxResponseMixin, 15 | JSONRequestResponseMixin, 16 | JSONResponseMixin, 17 | JsonRequestResponseMixin 18 | ) 19 | from ._forms import ( 20 | CsrfExemptMixin, 21 | FormInvalidMessageMixin, 22 | FormMessagesMixin, 23 | FormValidMessageMixin, 24 | MessageMixin, 25 | SuccessURLRedirectListMixin, 26 | UserFormKwargsMixin, 27 | _MessageAPIWrapper 28 | ) 29 | from ._other import ( 30 | AllVerbsMixin, 31 | CanonicalSlugDetailMixin, 32 | SetHeadlineMixin, 33 | StaticContextMixin 34 | ) 35 | from ._queries import ( 36 | OrderableListMixin, 37 | PrefetchRelatedMixin, 38 | SelectRelatedMixin 39 | ) 40 | 41 | __all__ = [ 42 | 'AjaxResponseMixin', 43 | 'AllVerbsMixin', 44 | 'AnonymousRequiredMixin', 45 | 'CanonicalSlugDetailMixin', 46 | 'CsrfExemptMixin', 47 | 'FormInvalidMessageMixin', 48 | 'FormMessagesMixin', 49 | 'FormValidMessageMixin', 50 | 'GroupRequiredMixin', 51 | 'JSONRequestResponseMixin', 52 | 'JsonRequestResponseMixin', 53 | 'JSONResponseMixin', 54 | 'LoginRequiredMixin', 55 | 'MessageMixin', 56 | 'MultiplePermissionsRequiredMixin', 57 | 'OrderableListMixin', 58 | 'PermissionRequiredMixin', 59 | 'PrefetchRelatedMixin', 60 | 'SelectRelatedMixin', 61 | 'SetHeadlineMixin', 62 | 'StaffuserRequiredMixin', 63 | 'StaticContextMixin', 64 | 'SuccessURLRedirectListMixin', 65 | 'SuperuserRequiredMixin', 66 | 'UserFormKwargsMixin', 67 | 'UserPassesTestMixin' 68 | ] 69 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | First of all, thank you for wanting to make **django-braces** better! We love 8 | getting input and suggestions from the community. 9 | 10 | Secondly, we just want to put out a few ground rules for contributing so that 11 | we can get your pull requests in sooner and cause less headaches all around. 12 | 13 | .. _Code Style: 14 | 15 | Code Style 16 | ---------- 17 | 18 | We stick to `PEP8 `_ as much as 19 | possible, so please make sure your code passes a lint test. That means two 20 | blank lines before class names and all of those other wonderful rules. 21 | 22 | We like docstrings in the classes, too. This helps us and those that come 23 | later what the class is meant to do. Docstrings in methods are great, too, 24 | especially if the method makes any assumptions about how it'll be used. 25 | 26 | 27 | .. _Docs: 28 | 29 | Docs 30 | ---- 31 | 32 | If you're reading this, you should already know that docs are important to 33 | this project and, honestly, all of them. We like any new mixins, or changes 34 | in existing mixins, to come with documentation changes showing how to use 35 | the mixin. Ideally, you show at least one example usage, but if your mixin 36 | provides multiple paths, perhaps a static attribute or a dynamic method, 37 | it's really great if your documentation shows both avenues, too. 38 | 39 | .. _Tests: 40 | 41 | Tests 42 | ----- 43 | 44 | All code changes should come with test changes. We use 45 | `py.test `_ instead of Python's 46 | ``unittest``. This seems to only be really important when marking tests for 47 | skipping. 48 | 49 | We try to keep the project at 100% test coverage but know this isn't something 50 | we can achieve forever. So long as your tests cover your contribution 80% or 51 | better, we're happy to try and bump up that last bit, or just accept the code. 52 | -------------------------------------------------------------------------------- /tests/factories.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import factory 4 | 5 | from django.contrib.auth.models import Group, Permission, User 6 | 7 | from .models import Article 8 | 9 | 10 | def _get_perm(perm_name): 11 | """ 12 | Returns permission instance with given name. 13 | 14 | Permission name is a string like 'auth.add_user'. 15 | """ 16 | app_label, codename = perm_name.split('.') 17 | return Permission.objects.get( 18 | content_type__app_label=app_label, codename=codename) 19 | 20 | 21 | class ArticleFactory(factory.django.DjangoModelFactory): 22 | FACTORY_FOR = Article 23 | 24 | title = factory.Sequence(lambda n: 'Article number {0}'.format(n)) 25 | body = factory.Sequence(lambda n: 'Body of article {0}'.format(n)) 26 | 27 | 28 | class GroupFactory(factory.django.DjangoModelFactory): 29 | FACTORY_FOR = Group 30 | 31 | name = factory.Sequence(lambda n: 'group{0}'.format(n)) 32 | 33 | 34 | class UserFactory(factory.django.DjangoModelFactory): 35 | FACTORY_FOR = User 36 | 37 | username = factory.Sequence(lambda n: 'user{0}'.format(n)) 38 | first_name = factory.Sequence(lambda n: 'John {0}'.format(n)) 39 | last_name = factory.Sequence(lambda n: 'Doe {0}'.format(n)) 40 | email = factory.Sequence(lambda n: 'user{0}@example.com'.format(n)) 41 | password = 'asdf1234' 42 | 43 | @classmethod 44 | def _prepare(cls, create, **kwargs): 45 | password = kwargs.pop('password', None) 46 | user = super(UserFactory, cls)._prepare(create, **kwargs) 47 | if password: 48 | user.set_password(password) 49 | if create: 50 | user.save() 51 | return user 52 | 53 | @factory.post_generation 54 | def permissions(self, create, extracted, **kwargs): 55 | if create and extracted: 56 | # We have a saved object and a list of permission names 57 | self.user_permissions.add(*[_get_perm(pn) for pn in extracted]) 58 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf.global_settings import * 2 | 3 | DEBUG = False 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | TIME_ZONE = 'UTC' 7 | LANGUAGE_CODE = 'en-US' 8 | SITE_ID = 1 9 | USE_L10N = True 10 | USE_TZ = True 11 | 12 | SECRET_KEY = 'local' 13 | 14 | ROOT_URLCONF = 'tests.urls' 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': ':memory:', 20 | } 21 | } 22 | 23 | MIDDLEWARE_CLASSES = [ 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.contrib.sessions.middleware.SessionMiddleware', 26 | 'django.middleware.csrf.CsrfViewMiddleware', 27 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 28 | 'django.contrib.messages.middleware.MessageMiddleware', 29 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 30 | ] 31 | 32 | TEMPLATE_CONTEXT_PROCESSORS = [ 33 | 'django.contrib.auth.context_processors.auth', 34 | 'django.core.context_processors.i18n', 35 | 'django.core.context_processors.media', 36 | 'django.core.context_processors.static', 37 | 'django.core.context_processors.tz', 38 | 'django.core.context_processors.request', 39 | 'django.contrib.messages.context_processors.messages' 40 | ] 41 | 42 | STATICFILES_FINDERS = ( 43 | 'django.contrib.staticfiles.finders.FileSystemFinder', 44 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 45 | ) 46 | 47 | INSTALLED_APPS = ( 48 | 'django.contrib.auth', 49 | 'django.contrib.contenttypes', 50 | 'django.contrib.sessions', 51 | 'django.contrib.messages', 52 | 'django.contrib.staticfiles', 53 | 'django.contrib.sites', 54 | 55 | 'tests', 56 | ) 57 | 58 | PASSWORD_HASHERS = ( 59 | 'django.contrib.auth.hashers.MD5PasswordHasher', 60 | ) 61 | 62 | import django 63 | if django.VERSION < (1, 4): 64 | TEMPLATE_CONTEXT_PROCESSORS.remove('django.core.context_processors.tz') 65 | MIDDLEWARE_CLASSES.remove( 66 | 'django.middleware.clickjacking.XFrameOptionsMiddleware' 67 | ) 68 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.contrib.auth.models import AnonymousUser 3 | from django.core.serializers.json import DjangoJSONEncoder 4 | 5 | 6 | class TestViewHelper(object): 7 | """ 8 | Helper class for unit-testing class based views. 9 | """ 10 | view_class = None 11 | request_factory_class = test.RequestFactory 12 | 13 | def setUp(self): 14 | super(TestViewHelper, self).setUp() 15 | self.factory = self.request_factory_class() 16 | 17 | def build_request(self, method='GET', path='/test/', user=None, **kwargs): 18 | """ 19 | Creates a request using request factory. 20 | """ 21 | fn = getattr(self.factory, method.lower()) 22 | if user is None: 23 | user = AnonymousUser() 24 | 25 | req = fn(path, **kwargs) 26 | req.user = user 27 | return req 28 | 29 | def build_view(self, request, args=None, kwargs=None, view_class=None, 30 | **viewkwargs): 31 | """ 32 | Creates a `view_class` view instance. 33 | """ 34 | if not args: 35 | args = () 36 | if not kwargs: 37 | kwargs = {} 38 | if view_class is None: 39 | view_class = self.view_class 40 | 41 | return view_class( 42 | request=request, args=args, kwargs=kwargs, **viewkwargs) 43 | 44 | def dispatch_view(self, request, args=None, kwargs=None, view_class=None, 45 | **viewkwargs): 46 | """ 47 | Creates and dispatches `view_class` view. 48 | """ 49 | view = self.build_view(request, args, kwargs, view_class, **viewkwargs) 50 | return view.dispatch(request, *view.args, **view.kwargs) 51 | 52 | 53 | class SetJSONEncoder(DjangoJSONEncoder): 54 | """ 55 | A custom JSONEncoder extending `DjangoJSONEncoder` to handle serialization 56 | of `set`. 57 | """ 58 | def default(self, obj): 59 | if isinstance(obj, set): 60 | return list(obj) 61 | return super(DjangoJSONEncoder, self).default(obj) 62 | 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-braces 2 | Mixins for Django's class-based views. 3 | 4 | [![Latest drone.io status](https://drone.io/github.com/brack3t/django-braces/status.png)](https://drone.io/github.com/brack3t/django-braces) 5 | [![Latest PyPI version](https://pypip.in/v/django-braces/badge.png)](https://crate.io/packages/django-braces/) 6 | [![Number of PyPI downloads](https://pypip.in/d/django-braces/badge.png)](https://crate.io/packages/django-braces/) 7 | [![Stories in Ready](https://badge.waffle.io/brack3t/django-braces.png)](http://waffle.io/brack3t/django-braces) 8 | 9 | ## Documentation 10 | [Read The Docs](http://django-braces.readthedocs.org/en/latest/index.html) 11 | 12 | ## Installation 13 | Install from PyPI with `pip`: 14 | `pip install django-braces` 15 | 16 | ## Building the Docs 17 | 1. Install docs requirements: `pip install -r requirements-docs.txt`. 18 | 2. `cd docs`. 19 | 3. `make html`. 20 | 4. Open `_build/index.html` in your browser. 21 | 22 | ## Contributing 23 | 24 | See our [contribution guide](https://django-braces.readthedocs.org/en/latest/contributing.html) 25 | 26 | Add yourself to `CONTRIBUTORS.txt` if you want. 27 | 28 | All development dependencies are available in `requirements.txt` file. 29 | 30 | To run the test suite, execute the following in your shell (Django install is required): 31 | `py.test tests/ --cov=braces --cov-report=html` 32 | 33 | Or test with `tox` if you have `tox` installed. 34 | 35 | ## Change Log 36 | 37 | [Changelog on Read The Docs](https://django-braces.readthedocs.org/en/latest/changelog.html) 38 | 39 | ## Use Django 1.4.x? 40 | 41 | `django-braces` 1.4.x will be the last version to officially support Django 1.4.x. Since Django 1.4.x is an LTS, we'll update `django-braces` 1.4.x as needed for bug fixes but it won't receive new functionality unless backporting is 100% painless. 42 | 43 | Our policy going forward is that `django-braces` officially supports the current version of Django and one version each direction (e.g. 1.6.x is current, so 1.5.x, 1.6.x, and 1.7.x are all supported). There won't be any restraints on using other versions of Django, though, but it will be a "buyer beware" situation. 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py2.6-d1.4, py2.6-d1.5, py2.6-d1.6, 3 | py2.7-d1.4, py2.7-d1.5, py2.7-d1.6, py2.7-d1.7, 4 | py3.3-d1.5, py3.3-d1.6, py3.3-d1.7, 5 | py3.4-d1.5, py3.4-d1.6, py3.4-d1.7 6 | install_command = pip install {opts} {packages} 7 | 8 | [testenv] 9 | commands = 10 | {envbindir}/coverage erase 11 | {envbindir}/coverage run {envbindir}/py.test tests/ 12 | {envbindir}/coverage report 13 | 14 | [base] 15 | deps = 16 | mock 17 | pytest-django 18 | factory_boy 19 | coverage 20 | argparse 21 | 22 | ;============================================ 23 | ; Python 2.6 24 | ;============================================ 25 | [testenv:py2.6-d1.4] 26 | basepython = python2.6 27 | deps = 28 | {[base]deps} 29 | django>=1.4,<1.5 30 | 31 | [testenv:py2.6-d1.5] 32 | basepython = python2.6 33 | deps = 34 | {[base]deps} 35 | django>=1.5,<1.6 36 | 37 | [testenv:py2.6-d1.6] 38 | basepython = python2.6 39 | deps = 40 | {[base]deps} 41 | django>=1.6,<1.7 42 | 43 | 44 | ;============================================ 45 | ; Python 2.7 46 | ;============================================ 47 | [testenv:py2.7-d1.4] 48 | basepython = python2.7 49 | deps = 50 | {[base]deps} 51 | django>=1.4,<1.5 52 | 53 | [testenv:py2.7-d1.5] 54 | basepython = python2.7 55 | deps = 56 | {[base]deps} 57 | django>=1.5,<1.6 58 | 59 | [testenv:py2.7-d1.6] 60 | basepython = python2.7 61 | deps = 62 | {[base]deps} 63 | django>=1.6,<1.7 64 | 65 | [testenv:py2.7-d1.7] 66 | basepython = python2.7 67 | deps = 68 | {[base]deps} 69 | django>=1.7,<1.8 70 | 71 | 72 | ;============================================ 73 | ; Python 3.3 74 | ;============================================ 75 | [testenv:py3.3-d1.5] 76 | basepython = python3.3 77 | deps = 78 | {[base]deps} 79 | django>=1.5,<1.6 80 | 81 | [testenv:py3.3-d1.6] 82 | basepython = python3.3 83 | deps = 84 | {[base]deps} 85 | django>=1.6,<1.7 86 | 87 | [testenv:py3.3-d1.7] 88 | basepython = python3.3 89 | deps = 90 | {[base]deps} 91 | django>=1.7,<1.8 92 | 93 | 94 | ;============================================ 95 | ; Python 3.4 96 | ;============================================ 97 | [testenv:py3.4-d1.5] 98 | basepython = python3.4 99 | deps = 100 | {[base]deps} 101 | django>=1.5,<1.6 102 | 103 | [testenv:py3.4-d1.6] 104 | basepython = python3.4 105 | deps = 106 | {[base]deps} 107 | django>=1.6,<1.7 108 | 109 | [testenv:py3.4-d1.7] 110 | basepython = python3.4 111 | deps = 112 | {[base]deps} 113 | django>=1.7,<1.8 114 | 115 | 116 | ;============================================ 117 | ; PyPy 118 | ;============================================ 119 | [testenv:pypy-d1.7] 120 | basepython = pypy 121 | deps = 122 | {[base]deps} 123 | django>=1.7,<1.8 124 | 125 | [testenv:pypy3-d1.7] 126 | basepython = pypy 127 | deps = 128 | {[base]deps} 129 | django>=1.7,<1.8 130 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from . import views 4 | from .compat import patterns, include, url 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | # LoginRequiredMixin tests 10 | url(r'^login_required/$', views.LoginRequiredView.as_view()), 11 | 12 | # AnonymousRequiredView tests 13 | url(r'^unauthenticated_view/$', views.AnonymousRequiredView.as_view(), 14 | name='unauthenticated_view'), 15 | url(r'^authenticated_view/$', views.AuthenticatedView.as_view(), 16 | name='authenticated_view'), 17 | 18 | # AjaxResponseMixin tests 19 | url(r'^ajax_response/$', views.AjaxResponseView.as_view()), 20 | 21 | # CreateAndRedirectToEditView tests 22 | url(r'^article/create/$', views.CreateArticleView.as_view()), 23 | url(r'^article/(?P\d+)/edit/$', views.EditArticleView.as_view(), 24 | name="edit_article"), 25 | 26 | url(r'^article_list/create/$', 27 | views.CreateArticleAndRedirectToListView.as_view()), 28 | url(r'^article_list_bad/create/$', 29 | views.CreateArticleAndRedirectToListViewBad.as_view()), 30 | url(r'^article_list/$', views.ArticleListView.as_view(), 31 | name='article_list'), 32 | 33 | # CanonicalSlugDetailMixin tests 34 | url(r'^article-canonical/(?P\d+)-(?P[-\w]+)/$', 35 | views.CanonicalSlugDetailView.as_view()), 36 | url(r'^article-canonical-namespaced/', 37 | include('tests.urls_namespaced', namespace='some_namespace')), 38 | url(r'^article-canonical-override/(?P\d+)-(?P[-\w]+)/$', 39 | views.OverriddenCanonicalSlugDetailView.as_view()), 40 | url(r'^article-canonical-custom-kwargs/(?P\d+)-(?P[-\w]+)/$', 41 | views.CanonicalSlugDetailCustomUrlKwargsView.as_view()), 42 | url(r'^article-canonical-model/(?P\d+)-(?P[-\w]+)/$', 43 | views.ModelCanonicalSlugDetailView.as_view()), 44 | 45 | # UserFormKwargsMixin tests 46 | url(r'^form_with_user_kwarg/$', views.FormWithUserKwargView.as_view()), 47 | 48 | # SetHeadlineMixin tests 49 | url(r'^headline/$', views.HeadlineView.as_view(), name='headline'), 50 | url(r'^headline/lazy/$', views.LazyHeadlineView.as_view()), 51 | url(r'^headline/(?P[\w-]+)/$', views.DynamicHeadlineView.as_view()), 52 | 53 | # ExtraContextMixin tests 54 | url(r'^context/$', views.ContextView.as_view(), name='context'), 55 | 56 | # PermissionRequiredMixin tests 57 | url(r'^permission_required/$', views.PermissionRequiredView.as_view()), 58 | 59 | # MultiplePermissionsRequiredMixin tests 60 | url(r'^multiple_permissions_required/$', 61 | views.MultiplePermissionsRequiredView.as_view()), 62 | 63 | # SuperuserRequiredMixin tests 64 | url(r'^superuser_required/$', views.SuperuserRequiredView.as_view()), 65 | 66 | # StaffuserRequiredMixin tests 67 | url(r'^staffuser_required/$', views.StaffuserRequiredView.as_view()), 68 | 69 | # GroupRequiredMixin tests 70 | url(r'^group_required/$', views.GroupRequiredView.as_view()), 71 | 72 | # UserPassesTestMixin tests 73 | url(r'^user_passes_test/$', views.UserPassesTestView.as_view()), 74 | 75 | # UserPassesTestMixin tests 76 | url(r'^user_passes_test_not_implemented/$', views.UserPassesTestNotImplementedView.as_view()), 77 | 78 | # CsrfExemptMixin tests 79 | url(r'^csrf_exempt/$', views.CsrfExemptView.as_view()), 80 | 81 | # JSONResponseMixin tests 82 | url(r'^simple_json/$', views.SimpleJsonView.as_view()), 83 | url(r'^simple_json_custom_encoder/$', views.CustomJsonEncoderView.as_view()), 84 | url(r'^simple_json_400/$', views.SimpleJsonBadRequestView.as_view()), 85 | url(r'^article_list_json/$', views.ArticleListJsonView.as_view()), 86 | 87 | # JsonRequestResponseMixin tests 88 | url(r'^json_request/$', views.JsonRequestResponseView.as_view()), 89 | url(r'^json_bad_request/$', views.JsonBadRequestView.as_view()), 90 | url(r'^json_custom_bad_request/$', views.JsonCustomBadRequestView.as_view()), 91 | 92 | # FormMessagesMixin tests 93 | url(r'form_messages/$', views.FormMessagesView.as_view()), 94 | 95 | # AllVerbsMixin tests 96 | url(r'all_verbs/$', views.AllVerbsView.as_view()), 97 | url(r'all_verbs_no_handler/$', views.AllVerbsView.as_view(all_handler=None)), 98 | ) 99 | 100 | 101 | urlpatterns += patterns( 102 | 'django.contrib.auth.views', 103 | # login page, required by some tests 104 | url(r'^accounts/login/$', 'login', {'template_name': 'blank.html'}), 105 | url(r'^auth/login/$', 'login', {'template_name': 'blank.html'}), 106 | ) 107 | -------------------------------------------------------------------------------- /braces/views/_queries.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | 3 | 4 | class SelectRelatedMixin(object): 5 | """ 6 | Mixin allows you to provide a tuple or list of related models to 7 | perform a select_related on. 8 | """ 9 | select_related = None # Default related fields to none 10 | 11 | def get_queryset(self): 12 | if self.select_related is None: # If no fields were provided, 13 | # raise a configuration error 14 | raise ImproperlyConfigured( 15 | '{0} is missing the select_related property. This must be ' 16 | 'a tuple or list.'.format(self.__class__.__name__)) 17 | 18 | if not isinstance(self.select_related, (tuple, list)): 19 | # If the select_related argument is *not* a tuple or list, 20 | # raise a configuration error. 21 | raise ImproperlyConfigured( 22 | "{0}'s select_related property must be a tuple or " 23 | "list.".format(self.__class__.__name__)) 24 | 25 | # Get the current queryset of the view 26 | queryset = super(SelectRelatedMixin, self).get_queryset() 27 | 28 | if not self.select_related: 29 | return queryset 30 | return queryset.select_related(*self.select_related) 31 | 32 | 33 | class PrefetchRelatedMixin(object): 34 | """ 35 | Mixin allows you to provide a tuple or list of related models to 36 | perform a prefetch_related on. 37 | """ 38 | prefetch_related = None # Default prefetch fields to none 39 | 40 | def get_queryset(self): 41 | if self.prefetch_related is None: # If no fields were provided, 42 | # raise a configuration error 43 | raise ImproperlyConfigured( 44 | '{0} is missing the prefetch_related property. This must be ' 45 | 'a tuple or list.'.format(self.__class__.__name__)) 46 | 47 | if not isinstance(self.prefetch_related, (tuple, list)): 48 | # If the prefetch_related argument is *not* a tuple or list, 49 | # raise a configuration error. 50 | raise ImproperlyConfigured( 51 | "{0}'s prefetch_related property must be a tuple or " 52 | "list.".format(self.__class__.__name__)) 53 | 54 | # Get the current queryset of the view 55 | queryset = super(PrefetchRelatedMixin, self).get_queryset() 56 | 57 | if not self.prefetch_related: 58 | return queryset 59 | return queryset.prefetch_related(*self.prefetch_related) 60 | 61 | 62 | class OrderableListMixin(object): 63 | """ 64 | Mixin allows your users to order records using GET parameters 65 | """ 66 | 67 | orderable_columns = None 68 | orderable_columns_default = None 69 | order_by = None 70 | ordering = None 71 | 72 | def get_context_data(self, **kwargs): 73 | """ 74 | Augments context with: 75 | 76 | * ``order_by`` - name of the field 77 | * ``ordering`` - order of ordering, either ``asc`` or ``desc`` 78 | """ 79 | context = super(OrderableListMixin, self).get_context_data(**kwargs) 80 | context["order_by"] = self.order_by 81 | context["ordering"] = self.ordering 82 | return context 83 | 84 | def get_orderable_columns(self): 85 | if not self.orderable_columns: 86 | raise ImproperlyConfigured( 87 | '{0} needs the ordering columns defined.'.format( 88 | self.__class__.__name__)) 89 | return self.orderable_columns 90 | 91 | def get_orderable_columns_default(self): 92 | if not self.orderable_columns_default: 93 | raise ImproperlyConfigured( 94 | '{0} needs the default ordering column defined.'.format( 95 | self.__class__.__name__)) 96 | return self.orderable_columns_default 97 | 98 | def get_ordered_queryset(self, queryset=None): 99 | """ 100 | Augments ``QuerySet`` with order_by statement if possible 101 | 102 | :param QuerySet queryset: ``QuerySet`` to ``order_by`` 103 | :return: QuerySet 104 | """ 105 | get_order_by = self.request.GET.get("order_by") 106 | 107 | if get_order_by in self.get_orderable_columns(): 108 | order_by = get_order_by 109 | else: 110 | order_by = self.get_orderable_columns_default() 111 | 112 | self.order_by = order_by 113 | self.ordering = "asc" 114 | 115 | if order_by and self.request.GET.get("ordering", "asc") == "desc": 116 | order_by = "-" + order_by 117 | self.ordering = "desc" 118 | 119 | return queryset.order_by(order_by) 120 | 121 | def get_queryset(self): 122 | """ 123 | Returns ordered ``QuerySet`` 124 | """ 125 | unordered_queryset = super(OrderableListMixin, self).get_queryset() 126 | return self.get_ordered_queryset(unordered_queryset) 127 | -------------------------------------------------------------------------------- /braces/views/_other.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.core.urlresolvers import resolve 3 | from django.shortcuts import redirect 4 | from django.utils.encoding import force_text 5 | 6 | 7 | class SetHeadlineMixin(object): 8 | """ 9 | Mixin allows you to set a static headline through a static property on the 10 | class or programmatically by overloading the get_headline method. 11 | """ 12 | headline = None # Default the headline to none 13 | 14 | def get_context_data(self, **kwargs): 15 | kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) 16 | # Update the existing context dict with the provided headline. 17 | kwargs.update({"headline": self.get_headline()}) 18 | return kwargs 19 | 20 | def get_headline(self): 21 | if self.headline is None: # If no headline was provided as a view 22 | # attribute and this method wasn't 23 | # overridden raise a configuration error. 24 | raise ImproperlyConfigured( 25 | '{0} is missing a headline. ' 26 | 'Define {0}.headline, or override ' 27 | '{0}.get_headline().'.format(self.__class__.__name__)) 28 | return force_text(self.headline) 29 | 30 | 31 | class StaticContextMixin(object): 32 | """ 33 | Mixin allows you to set static context through a static property on 34 | the class. 35 | """ 36 | static_context = None 37 | 38 | def get_context_data(self, **kwargs): 39 | kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) 40 | 41 | try: 42 | kwargs.update(self.get_static_context()) 43 | except (TypeError, ValueError): 44 | raise ImproperlyConfigured( 45 | '{0}.static_context must be a dictionary or container ' 46 | 'of two-tuples.'.format(self.__class__.__name__)) 47 | else: 48 | return kwargs 49 | 50 | def get_static_context(self): 51 | if self.static_context is None: 52 | raise ImproperlyConfigured( 53 | '{0} is missing the static_context property. Define ' 54 | '{0}.static_context, or override ' 55 | '{0}.get_static_context()'.format(self.__class__.__name__) 56 | ) 57 | return self.static_context 58 | 59 | 60 | class CanonicalSlugDetailMixin(object): 61 | """ 62 | A mixin that enforces a canonical slug in the url. 63 | 64 | If a urlpattern takes a object's pk and slug as arguments and the slug url 65 | argument does not equal the object's canonical slug, this mixin will 66 | redirect to the url containing the canonical slug. 67 | """ 68 | def dispatch(self, request, *args, **kwargs): 69 | # Set up since we need to super() later instead of earlier. 70 | self.request = request 71 | self.args = args 72 | self.kwargs = kwargs 73 | 74 | # Get the current object, url slug, and 75 | # urlpattern name (namespace aware). 76 | obj = self.get_object() 77 | slug = self.kwargs.get(self.slug_url_kwarg, None) 78 | match = resolve(request.path_info) 79 | url_parts = match.namespaces 80 | url_parts.append(match.url_name) 81 | current_urlpattern = ":".join(url_parts) 82 | 83 | # Figure out what the slug is supposed to be. 84 | if hasattr(obj, "get_canonical_slug"): 85 | canonical_slug = obj.get_canonical_slug() 86 | else: 87 | canonical_slug = self.get_canonical_slug() 88 | 89 | # If there's a discrepancy between the slug in the url and the 90 | # canonical slug, redirect to the canonical slug. 91 | if canonical_slug != slug: 92 | params = {self.pk_url_kwarg: obj.pk, 93 | self.slug_url_kwarg: canonical_slug, 94 | 'permanent': True} 95 | return redirect(current_urlpattern, **params) 96 | 97 | return super(CanonicalSlugDetailMixin, self).dispatch( 98 | request, *args, **kwargs) 99 | 100 | def get_canonical_slug(self): 101 | """ 102 | Override this method to customize what slug should be considered 103 | canonical. 104 | 105 | Alternatively, define the get_canonical_slug method on this view's 106 | object class. In that case, this method will never be called. 107 | """ 108 | return self.get_object().slug 109 | 110 | 111 | class AllVerbsMixin(object): 112 | """Call a single method for all HTTP verbs. 113 | 114 | The name of the method should be specified using the class attribute 115 | ``all_handler``. The default value of this attribute is 'all'. 116 | """ 117 | all_handler = 'all' 118 | 119 | def dispatch(self, request, *args, **kwargs): 120 | if not self.all_handler: 121 | raise ImproperlyConfigured( 122 | '{0} requires the all_handler attribute to be set.'.format( 123 | self.__class__.__name__)) 124 | 125 | handler = getattr(self, self.all_handler, self.http_method_not_allowed) 126 | return handler(request, *args, **kwargs) 127 | -------------------------------------------------------------------------------- /braces/views/_ajax.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from django.core import serializers 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.core.serializers.json import DjangoJSONEncoder 6 | from django.http import HttpResponse, HttpResponseBadRequest 7 | 8 | ## Django 1.5+ compat 9 | try: 10 | import json 11 | except ImportError: # pragma: no cover 12 | from django.utils import simplejson as json 13 | 14 | 15 | class JSONResponseMixin(object): 16 | """ 17 | A mixin that allows you to easily serialize simple data such as a dict or 18 | Django models. 19 | """ 20 | content_type = None 21 | json_dumps_kwargs = None 22 | json_encoder_class = DjangoJSONEncoder 23 | 24 | def get_content_type(self): 25 | if (self.content_type is not None and 26 | not isinstance(self.content_type, 27 | (six.string_types, six.text_type))): 28 | raise ImproperlyConfigured( 29 | '{0} is missing a content type. Define {0}.content_type, ' 30 | 'or override {0}.get_content_type().'.format( 31 | self.__class__.__name__)) 32 | return self.content_type or u"application/json" 33 | 34 | def get_json_dumps_kwargs(self): 35 | if self.json_dumps_kwargs is None: 36 | self.json_dumps_kwargs = {} 37 | self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) 38 | return self.json_dumps_kwargs 39 | 40 | def render_json_response(self, context_dict, status=200): 41 | """ 42 | Limited serialization for shipping plain data. Do not use for models 43 | or other complex or custom objects. 44 | """ 45 | json_context = json.dumps( 46 | context_dict, 47 | cls=self.json_encoder_class, 48 | **self.get_json_dumps_kwargs()).encode(u'utf-8') 49 | return HttpResponse(json_context, 50 | content_type=self.get_content_type(), 51 | status=status) 52 | 53 | def render_json_object_response(self, objects, **kwargs): 54 | """ 55 | Serializes objects using Django's builtin JSON serializer. Additional 56 | kwargs can be used the same way for django.core.serializers.serialize. 57 | """ 58 | json_data = serializers.serialize(u"json", objects, **kwargs) 59 | return HttpResponse(json_data, content_type=self.get_content_type()) 60 | 61 | 62 | class AjaxResponseMixin(object): 63 | """ 64 | Mixin allows you to define alternative methods for ajax requests. Similar 65 | to the normal get, post, and put methods, you can use get_ajax, post_ajax, 66 | and put_ajax. 67 | """ 68 | def dispatch(self, request, *args, **kwargs): 69 | request_method = request.method.lower() 70 | 71 | if request.is_ajax() and request_method in self.http_method_names: 72 | handler = getattr(self, u"{0}_ajax".format(request_method), 73 | self.http_method_not_allowed) 74 | self.request = request 75 | self.args = args 76 | self.kwargs = kwargs 77 | return handler(request, *args, **kwargs) 78 | 79 | return super(AjaxResponseMixin, self).dispatch( 80 | request, *args, **kwargs) 81 | 82 | def get_ajax(self, request, *args, **kwargs): 83 | return self.get(request, *args, **kwargs) 84 | 85 | def post_ajax(self, request, *args, **kwargs): 86 | return self.post(request, *args, **kwargs) 87 | 88 | def put_ajax(self, request, *args, **kwargs): 89 | return self.get(request, *args, **kwargs) 90 | 91 | def delete_ajax(self, request, *args, **kwargs): 92 | return self.get(request, *args, **kwargs) 93 | 94 | 95 | class JsonRequestResponseMixin(JSONResponseMixin): 96 | """ 97 | Extends JSONResponseMixin. Attempts to parse request as JSON. If request 98 | is properly formatted, the json is saved to self.request_json as a Python 99 | object. request_json will be None for imparsible requests. 100 | Set the attribute require_json to True to return a 400 "Bad Request" error 101 | for requests that don't contain JSON. 102 | 103 | Note: To allow public access to your view, you'll need to use the 104 | csrf_exempt decorator or CsrfExemptMixin. 105 | 106 | Example Usage: 107 | 108 | class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): 109 | def post(self, request, *args, **kwargs): 110 | do_stuff_with_contents_of_request_json() 111 | return self.render_json_response( 112 | {'message': 'Thanks!'}) 113 | """ 114 | require_json = False 115 | error_response_dict = {u"errors": [u"Improperly formatted request"]} 116 | 117 | def render_bad_request_response(self, error_dict=None): 118 | if error_dict is None: 119 | error_dict = self.error_response_dict 120 | json_context = json.dumps( 121 | error_dict, 122 | cls=self.json_encoder_class, 123 | **self.get_json_dumps_kwargs() 124 | ).encode(u'utf-8') 125 | return HttpResponseBadRequest( 126 | json_context, content_type=self.get_content_type()) 127 | 128 | def get_request_json(self): 129 | try: 130 | return json.loads(self.request.body.decode(u'utf-8')) 131 | except ValueError: 132 | return None 133 | 134 | def dispatch(self, request, *args, **kwargs): 135 | self.request = request 136 | self.args = args 137 | self.kwargs = kwargs 138 | 139 | self.request_json = self.get_request_json() 140 | if self.require_json and self.request_json is None: 141 | return self.render_bad_request_response() 142 | return super(JsonRequestResponseMixin, self).dispatch( 143 | request, *args, **kwargs) 144 | 145 | 146 | class JSONRequestResponseMixin(JsonRequestResponseMixin): 147 | pass 148 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-braces.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-braces.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-braces" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-braces" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ========= 4 | Changelog 5 | ========= 6 | 7 | * :bug:`130` New attribute on :ref:`JSONResponseMixin` to allow setting a custom JSON encoder class. 8 | * :bug:`131` New attribute on :ref:`LoginRequiredMixin` so it's possible to redirect unauthenticated users while 9 | using ``AccessMixin``-derived mixins instead of throwing an exception. 10 | * :release:`1.4.0 <2014-03-04>` 11 | * :support:`129` Split ``views.py`` out into multiple files since it was approaching 1000 LoC. 12 | * :feature:`119` :ref:`SetHeadlineMixin` now accepts ``headline`` with ``ugettext_lazy()``-wrapped strings. 13 | * :bug:`94 major` Fixed a bug where :ref:`JSONResponseMixin` would override the ``content_type`` of Django's ``TemplateView`` in Django 1.6. 14 | * :bug:`- major` Fixed bug in :ref:`PermissionRequiredMixin` where if ``PermissionRequiredMixin.no_permissions_fail`` returned a false-y value, the user lacking the permission would pass instead of being denied access. 15 | * :support:`73` Added doc for how to contribute. 16 | * :feature:`120` Added :ref:`MessageMixin` to allow easier access to Django's ``contrib.messages`` messages. :ref:`FormValidMessageMixin` and :ref:`FormInvalidMessageMixin` were updated to use it. 17 | * :bug:`98 major` Fixed bug in :ref:`CanonicalSlugDetailMixin` to allow it to use custom URL kwargs. 18 | * :bug:`105 major` Fixed bug in :ref:`GroupRequiredMixin` where superusers were blocked by lack of group memberships. 19 | * :bug:`106 major` Fixed bug in :ref:`GroupRequiredMixin` which now correctly checks for group membership against a list. 20 | * :feature:`102` Added new :ref:`StaticContextMixin` mixin which lets you pass in ``static_context`` as a property of the view. 21 | * :feature:`89` Added new :ref:`AnonymousRequiredMixin` which redirects authenticated users to another view. 22 | * :feature:`104` Added new :ref:`AllVerbsMixin` which allows a single method to response to all HTTP verbs. 23 | * :bug:`- major` Provided ``JSONRequestResponseMixin`` as a mirror of :ref:`JsonRequestResponseMixin` because we're not PHP. 24 | * :feature:`107` :ref:`FormValidMessageMixin`, :ref:`FormInvalidMessageMixin`, and :ref:`FormMessagesMixin` all allow ``ugettext_lazy``-wrapped strings. 25 | * :feature:`67` Extended :ref:`PermissionRequiredMixin` and :ref:`MultiplePermissionsRequiredMixin` to accept django-guardian-style custom/object permissions. 26 | * :release:`1.3.1 <2014-01-04>` 27 | * :bug:`95` Removed accidentally-added breakpoint. 28 | * :support:`96 backported` Added ``build/`` to ``.gitignore`` 29 | * :release:`1.3.0 <2014-01-03>` 30 | * :support:`59` Removed ``CreateAndRedirectToEditView`` mixin which was marked for deprecation and removal since 1.0.0. 31 | * :feature:`51` Added :ref:`JsonRequestResponseMixin` which attempts to parse requests as JSON. 32 | * :feature:`61` Added :ref:`CanonicalSlugDetailMixin` mixin which allows for the specification of a canonical slug on a ``DetailView`` to help with SEO by redirecting on non-canonical requests. 33 | * :feature:`76` Added :ref:`UserPassesTestMixin` mixin to replicate the behavior of Django's ``@user_passes_test`` decorator. 34 | * :bug:`- major` Some fixes for :ref:`CanonicalSlugDetailMixin`. 35 | * :feature:`92` ``AccessMixin`` now has a runtime-overridable ``login_url`` attribute. 36 | * :bug:`- major` Fixed problem with :ref:`GroupRequiredMixin` that made it not actually work. 37 | * :support:`-` All tests pass for Django versions 1.4 through 1.6 and Python versions 2.6, 2.7, and 3.3 (Django 1.4 and 1.5 not tested with Python 3.3). 38 | * :release:`1.2.2 <2013-08-07>` 39 | * :support:`-` Uses ``six.string_types`` instead of explicitly relying on ``str`` and ``unicode`` types. 40 | * :release:`1.2.1 <2013-07-28>` 41 | * :bug:`-` Fix to allow ``reverse_lazy`` to work for all ``AccessMixin``-derived mixins. 42 | * :release:`1.2.0 <2013-07-27>` 43 | * :feature:`57` :ref:`FormValidMessageMixin` which provides a ``messages`` message when the processed form is valid. 44 | * :feature:`-` :ref:`FormInvalidMessageMixin` which provides a ``messages`` message when the processed form is invalid. 45 | * :feature:`-` :ref:`FormMessagesMixin` which provides the functionality of both of the above mixins. 46 | * :feature:`-` :ref:`GroupRequiredMixin` which is a new access-level mixin which requires that a user be part of a specified group to access a view. 47 | * :release:`1.1.0 <2013-07-18>` 48 | * :bug:`52 major` :ref:`JSONResponseMixin` ``.render_json_response`` method updated to accept a status code. 49 | * :bug:`43 major` :ref:`JSONResponseMixin` added ``json_dumps_kwargs`` attribute & get method to pass args to the JSON encoder. 50 | * :feature:`45` New :ref:`OrderableListMixin` allows ordering of list views by GET params. 51 | * :support:`-` Tests updated to test against latest stable Django release (1.5.1) 52 | * :support:`-` Small fixes and additions to documentation. 53 | * :release:`1.0.0 <2013-02-28>` 54 | * :feature:`-` New 'abstract' ``AccessMixin`` which provides overridable ``get_login_url`` and ``get_redirect_field_name`` methods for all access-based mixins. 55 | * :feature:`32` Rewritten :ref:`LoginRequiredMixin` which provides same customization as other access mixins with ``login_url``, ``raise_exception`` & ``redirect_field_name``. 56 | * :feature:`33` New :ref:`PrefetchRelatedMixin`. Works the same as :ref:`SelectRelatedMixin` but uses Django's ``prefetch_related`` method. 57 | * :support:`-` ``CreateAndRedirectToEditView`` is marked for deprecation. 58 | * :bug:`- major` :ref:`PermissionRequiredMixin` no longer requires dot syntax for permission names. 59 | * :support:`-` Marked package as supporting 2.6 thru 3.3 (from rafales). 60 | * :support:`-` Fixes to documentation. 61 | * :support:`-` Tests to cover new additions and changes. 62 | * :release:`0.2.3 <2013-02-22>` 63 | * :support:`30` Tests for all mixins (from rafales). 64 | * :feature:`26` New :ref:`CsrfExemptMixin` for marking views as being CSRF exempt (from jarcoal). 65 | * :support:`-` Some documentation updates and a spelling error correction (from shabda). 66 | * :bug:`-` :ref:`SuccessURLRedirectListMixin` raises ``ImproperlyConfigured`` if no ``success_list_url`` attribute is supplied (from kennethlove). 67 | * :release:`0.2.2 <2013-01-21>` 68 | * :bug:`25` Try importing the built-in ``json`` module first, drop back to Django if necessary. 69 | * :support:`-` Django 1.5 compatibility. 70 | * :release:`0.2.1 <2012-12-10>` 71 | * :bug:`21 major` Fixed signature of :ref:`UserFormKwargsMixin` ``.get_form_kwargs`` 72 | * :feature:`22` Updated :ref:`JSONResponseMixin` to work with non-ASCII characters and other datatypes (such as datetimes) 73 | * :bug:`- major` Fixed all mixins that have ``raise_exception`` as an argument to properly raise a ``PermissionDenied`` exception to allow for custom 403s. 74 | -------------------------------------------------------------------------------- /braces/views/_forms.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from django.contrib import messages 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.core.urlresolvers import reverse 6 | from django.utils.decorators import method_decorator 7 | from django.utils.encoding import force_text 8 | from django.utils.functional import curry, Promise 9 | from django.views.decorators.csrf import csrf_exempt 10 | 11 | 12 | class CsrfExemptMixin(object): 13 | """ 14 | Exempts the view from CSRF requirements. 15 | 16 | NOTE: 17 | This should be the left-most mixin of a view. 18 | """ 19 | 20 | @method_decorator(csrf_exempt) 21 | def dispatch(self, *args, **kwargs): 22 | return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) 23 | 24 | 25 | class UserFormKwargsMixin(object): 26 | """ 27 | CBV mixin which puts the user from the request into the form kwargs. 28 | Note: Using this mixin requires you to pop the `user` kwarg 29 | out of the dict in the super of your form's `__init__`. 30 | """ 31 | def get_form_kwargs(self): 32 | kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() 33 | # Update the existing form kwargs dict with the request's user. 34 | kwargs.update({"user": self.request.user}) 35 | return kwargs 36 | 37 | 38 | class SuccessURLRedirectListMixin(object): 39 | """ 40 | Simple CBV mixin which sets the success url to the list view of 41 | a given app. Set success_list_url as a class attribute of your 42 | CBV and don't worry about overloading the get_success_url. 43 | 44 | This is only to be used for redirecting to a list page. If you need 45 | to reverse the url with kwargs, this is not the mixin to use. 46 | """ 47 | success_list_url = None # Default the success url to none 48 | 49 | def get_success_url(self): 50 | # Return the reversed success url. 51 | if self.success_list_url is None: 52 | raise ImproperlyConfigured( 53 | '{0} is missing a succes_list_url ' 54 | 'name to reverse and redirect to. Define ' 55 | '{0}.success_list_url or override ' 56 | '{0}.get_success_url().'.format(self.__class__.__name__)) 57 | return reverse(self.success_list_url) 58 | 59 | 60 | class _MessageAPIWrapper(object): 61 | """ 62 | Wrap the django.contrib.messages.api module to automatically pass a given 63 | request object as the first parameter of function calls. 64 | """ 65 | API = set([ 66 | 'add_message', 'get_messages', 67 | 'get_level', 'set_level', 68 | 'debug', 'info', 'success', 'warning', 'error', 69 | ]) 70 | 71 | def __init__(self, request): 72 | for name in self.API: 73 | api_fn = getattr(messages.api, name) 74 | setattr(self, name, curry(api_fn, request)) 75 | 76 | 77 | class _MessageDescriptor(object): 78 | """ 79 | A descriptor that binds the _MessageAPIWrapper to the view's 80 | request. 81 | """ 82 | def __get__(self, instance, owner): 83 | return _MessageAPIWrapper(instance.request) 84 | 85 | 86 | class MessageMixin(object): 87 | """ 88 | Add a `messages` attribute on the view instance that wraps 89 | `django.contrib .messages`, automatically passing the current 90 | request object. 91 | """ 92 | messages = _MessageDescriptor() 93 | 94 | 95 | class FormValidMessageMixin(MessageMixin): 96 | """ 97 | Mixin allows you to set static message which is displayed by 98 | Django's messages framework through a static property on the class 99 | or programmatically by overloading the get_form_valid_message method. 100 | """ 101 | form_valid_message = None # Default to None 102 | 103 | def get_form_valid_message(self): 104 | """ 105 | Validate that form_valid_message is set and is either a 106 | unicode or str object. 107 | """ 108 | if self.form_valid_message is None: 109 | raise ImproperlyConfigured( 110 | '{0}.form_valid_message is not set. Define ' 111 | '{0}.form_valid_message, or override ' 112 | '{0}.get_form_valid_message().'.format(self.__class__.__name__) 113 | ) 114 | 115 | if not isinstance(self.form_valid_message, 116 | (six.string_types, six.text_type, Promise)): 117 | raise ImproperlyConfigured( 118 | '{0}.form_valid_message must be a str or unicode ' 119 | 'object.'.format(self.__class__.__name__) 120 | ) 121 | 122 | return force_text(self.form_valid_message) 123 | 124 | def form_valid(self, form): 125 | """ 126 | Call the super first, so that when overriding 127 | get_form_valid_message, we have access to the newly saved object. 128 | """ 129 | response = super(FormValidMessageMixin, self).form_valid(form) 130 | self.messages.success(self.get_form_valid_message(), 131 | fail_silently=True) 132 | return response 133 | 134 | 135 | class FormInvalidMessageMixin(MessageMixin): 136 | """ 137 | Mixin allows you to set static message which is displayed by 138 | Django's messages framework through a static property on the class 139 | or programmatically by overloading the get_form_invalid_message method. 140 | """ 141 | form_invalid_message = None 142 | 143 | def get_form_invalid_message(self): 144 | """ 145 | Validate that form_invalid_message is set and is either a 146 | unicode or str object. 147 | """ 148 | if self.form_invalid_message is None: 149 | raise ImproperlyConfigured( 150 | '{0}.form_invalid_message is not set. Define ' 151 | '{0}.form_invalid_message, or override ' 152 | '{0}.get_form_invalid_message().'.format( 153 | self.__class__.__name__)) 154 | 155 | if not isinstance(self.form_invalid_message, 156 | (six.string_types, six.text_type, Promise)): 157 | raise ImproperlyConfigured( 158 | '{0}.form_invalid_message must be a str or unicode ' 159 | 'object.'.format(self.__class__.__name__)) 160 | 161 | return force_text(self.form_invalid_message) 162 | 163 | def form_invalid(self, form): 164 | response = super(FormInvalidMessageMixin, self).form_invalid(form) 165 | self.messages.error(self.get_form_invalid_message(), 166 | fail_silently=True) 167 | return response 168 | 169 | 170 | class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): 171 | """ 172 | Mixin is a shortcut to use both FormValidMessageMixin and 173 | FormInvalidMessageMixin. 174 | """ 175 | pass 176 | -------------------------------------------------------------------------------- /tests/test_ajax_mixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import mock 4 | 5 | from django import test 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.http import HttpResponse 8 | 9 | from braces.views import AjaxResponseMixin 10 | 11 | from .compat import force_text 12 | from .factories import ArticleFactory, UserFactory 13 | from .helpers import TestViewHelper 14 | from .views import (SimpleJsonView, JsonRequestResponseView, 15 | CustomJsonEncoderView) 16 | from .compat import json 17 | 18 | 19 | class TestAjaxResponseMixin(TestViewHelper, test.TestCase): 20 | """ 21 | Tests for AjaxResponseMixin. 22 | """ 23 | methods = [u'get', u'post', u'put', u'delete'] 24 | 25 | def test_xhr(self): 26 | """ 27 | Checks if ajax_* method has been called for every http method. 28 | """ 29 | # AjaxResponseView returns 'AJAX_OK' when requested with XmlHttpRequest 30 | for m in self.methods: 31 | fn = getattr(self.client, m) 32 | resp = fn(u'/ajax_response/', 33 | HTTP_X_REQUESTED_WITH=u'XMLHttpRequest') 34 | assert force_text(resp.content) == u'AJAX_OK' 35 | 36 | def test_not_xhr(self): 37 | """ 38 | Normal methods (get, post, etc) should be used when handling non-ajax 39 | requests. 40 | """ 41 | for m in self.methods: 42 | fn = getattr(self.client, m) 43 | resp = fn(u'/ajax_response/') 44 | assert force_text(resp.content) == u'OK' 45 | 46 | def test_fallback_to_normal_methods(self): 47 | """ 48 | Ajax methods should fallback to normal methods by default. 49 | """ 50 | test_cases = [ 51 | (u'get', u'get'), 52 | (u'post', u'post'), 53 | (u'put', u'get'), 54 | (u'delete', u'get'), 55 | ] 56 | 57 | for ajax_method, fallback in test_cases: 58 | m, mixin = mock.Mock(), AjaxResponseMixin() 59 | m.return_value = HttpResponse() 60 | req = self.build_request() 61 | setattr(mixin, fallback, m) 62 | fn = getattr(mixin, u"{0}_ajax".format(ajax_method)) 63 | ret = fn(req, 1, 2, meth=ajax_method) 64 | # check if appropriate method has been called 65 | m.assert_called_once_with(req, 1, 2, meth=ajax_method) 66 | # check if appropriate value has been returned 67 | self.assertIs(m.return_value, ret) 68 | 69 | 70 | class TestJSONResponseMixin(TestViewHelper, test.TestCase): 71 | """ 72 | Tests for JSONResponseMixin. 73 | """ 74 | view_class = SimpleJsonView 75 | 76 | def assert_json_response(self, resp, status_code=200): 77 | self.assertEqual(status_code, resp.status_code) 78 | self.assertEqual(u'application/json', 79 | resp[u'content-type'].split(u';')[0]) 80 | 81 | def get_content(self, url): 82 | """ 83 | GET url and return content 84 | """ 85 | resp = self.client.get(url) 86 | self.assert_json_response(resp) 87 | content = force_text(resp.content) 88 | return content 89 | 90 | def test_simple_json(self): 91 | """ 92 | Tests render_json_response() method. 93 | """ 94 | user = UserFactory() 95 | self.client.login(username=user.username, password=u'asdf1234') 96 | data = json.loads(self.get_content(u'/simple_json/')) 97 | self.assertEqual({u'username': user.username}, data) 98 | 99 | def test_serialization(self): 100 | """ 101 | Tests render_json_object_response() method which serializes objects 102 | using django's serializer framework. 103 | """ 104 | a1, a2 = [ArticleFactory() for __ in range(2)] 105 | data = json.loads(self.get_content(u'/article_list_json/')) 106 | self.assertIsInstance(data, list) 107 | self.assertEqual(2, len(data)) 108 | titles = [] 109 | for row in data: 110 | # only title has been serialized 111 | self.assertEqual(1, len(row[u'fields'])) 112 | titles.append(row[u'fields'][u'title']) 113 | 114 | self.assertIn(a1.title, titles) 115 | self.assertIn(a2.title, titles) 116 | 117 | def test_bad_content_type(self): 118 | """ 119 | ImproperlyConfigured exception should be raised if content_type 120 | attribute is not set correctly. 121 | """ 122 | with self.assertRaises(ImproperlyConfigured): 123 | self.dispatch_view(self.build_request(), content_type=['a']) 124 | 125 | def test_pretty_json(self): 126 | """ 127 | Success if JSON responses are the same, and the well-indented response 128 | is longer than the normal one. 129 | """ 130 | user = UserFactory() 131 | self.client.login(username=user.username, password=u'asfa') 132 | normal_content = self.get_content(u'/simple_json/') 133 | self.view_class.json_dumps_kwargs = {u'indent': 2} 134 | pretty_content = self.get_content(u'/simple_json/') 135 | normal_json = json.loads(u'{0}'.format(normal_content)) 136 | pretty_json = json.loads(u'{0}'.format(pretty_content)) 137 | self.assertEqual(normal_json, pretty_json) 138 | self.assertTrue(len(pretty_content) > len(normal_content)) 139 | 140 | def test_json_encoder_class_atrribute(self): 141 | """ 142 | Tests setting custom `json_encoder_class` attribute. 143 | """ 144 | data = json.loads(self.get_content(u'/simple_json_custom_encoder/')) 145 | self.assertEqual({u'numbers': [1, 2, 3]}, data) 146 | 147 | 148 | class TestJsonRequestResponseMixin(TestViewHelper, test.TestCase): 149 | view_class = JsonRequestResponseView 150 | request_dict = {u'status': u'operational'} 151 | 152 | def test_get_request_json_properly_formatted(self): 153 | """ 154 | Properly formatted JSON requests should result in a JSON object 155 | """ 156 | data = json.dumps(self.request_dict).encode(u'utf-8') 157 | response = self.client.post( 158 | u'/json_request/', 159 | content_type=u'application/json', 160 | data=data 161 | ) 162 | response_json = json.loads(response.content.decode(u'utf-8')) 163 | self.assertEqual(response.status_code, 200) 164 | self.assertEqual(response_json, self.request_dict) 165 | 166 | def test_get_request_json_improperly_formatted(self): 167 | """ 168 | Improperly formatted JSON requests should make request_json == None 169 | """ 170 | response = self.client.post( 171 | u'/json_request/', 172 | data=self.request_dict 173 | ) 174 | response_json = json.loads(response.content.decode(u'utf-8')) 175 | self.assertEqual(response.status_code, 200) 176 | self.assertEqual(response_json, None) 177 | 178 | def test_bad_request_response(self): 179 | """ 180 | If a view calls render_bad_request_response when request_json is empty 181 | or None, the client should get a 400 error 182 | """ 183 | response = self.client.post( 184 | u'/json_bad_request/', 185 | data=self.request_dict 186 | ) 187 | response_json = json.loads(response.content.decode(u'utf-8')) 188 | self.assertEqual(response.status_code, 400) 189 | self.assertEqual(response_json, self.view_class.error_response_dict) 190 | 191 | def test_bad_request_response_with_custom_error_message(self): 192 | """ 193 | If a view calls render_bad_request_response when request_json is empty 194 | or None, the client should get a 400 error 195 | """ 196 | response = self.client.post( 197 | u'/json_custom_bad_request/', 198 | data=self.request_dict 199 | ) 200 | response_json = json.loads(response.content.decode(u'utf-8')) 201 | self.assertEqual(response.status_code, 400) 202 | self.assertEqual(response_json, {u'error': u'you messed up'}) 203 | -------------------------------------------------------------------------------- /docs/form.rst: -------------------------------------------------------------------------------- 1 | Form Mixins 2 | =========== 3 | 4 | All of these mixins, with one exception, modify how forms are handled within views. The ``UserKwargModelFormMixin`` is a mixin for use in forms to auto-pop a ``user`` kwarg. 5 | 6 | .. contents:: 7 | 8 | .. _CsrfExemptMixin: 9 | 10 | CsrfExemptMixin 11 | --------------- 12 | 13 | If you have Django's `CSRF protection`_ middleware enabled you can exempt views using the `csrf_exempt`_ decorator. This mixin exempts POST requests from the CSRF protection middleware without requiring that you decorate the ``dispatch`` method. 14 | 15 | .. note:: 16 | 17 | This mixin should always be the left-most plugin. 18 | 19 | :: 20 | 21 | from django.views.generic import UpdateView 22 | 23 | from braces.views import LoginRequiredMixin, CsrfExemptMixin 24 | 25 | from profiles.models import Profile 26 | 27 | 28 | class UpdateProfileView(CsrfExemptMixin, LoginRequiredMixin, UpdateView): 29 | model = Profile 30 | 31 | 32 | .. _UserFormKwargsMixin: 33 | 34 | UserFormKwargsMixin 35 | ------------------- 36 | 37 | A common pattern in Django is to have forms that are customized to a user. To custom tailor the form for users, you have to pass that user instance into the form and, based on their permission level or other details, change certain fields or add specific options within the forms ``__init__`` method. 38 | 39 | This mixin automates the process of overloading the ``get_form_kwargs`` (this method is available in any generic view which handles a form) method and stuffs the user instance into the form kwargs. The user can then be ``pop()``ed off in the form. **Always** remember to pop the user from the kwargs before calling ``super()`` on your form, otherwise the form will get an unexpected keyword argument. 40 | 41 | Usage 42 | ^^^^^ 43 | 44 | :: 45 | 46 | from django.views.generic import CreateView 47 | 48 | from braces.views import LoginRequiredMixin, UserFormKwargsMixin 49 | from next.example import UserForm 50 | 51 | 52 | class SomeSecretView(LoginRequiredMixin, UserFormKwargsMixin, CreateView): 53 | form_class = UserForm 54 | model = User 55 | template_name = "path/to/template.html" 56 | 57 | This obviously pairs very nicely with the following mixin. 58 | 59 | 60 | .. _UserKwargModelFormMixin: 61 | 62 | UserKwargModelFormMixin 63 | ----------------------- 64 | 65 | The ``UserKwargModelFormMixin`` is a form mixin to go along with our :ref:`UserFormKwargsMixin`. 66 | This becomes the first inherited class of our forms that receive the ``user`` keyword argument. With this mixin, the ``pop()``ing of the ``user`` is automated and no longer has to be done manually on every form that needs this behavior. 67 | 68 | Usage 69 | ^^^^^ 70 | 71 | :: 72 | 73 | from braces.forms import UserKwargModelFormMixin 74 | 75 | 76 | class UserForm(UserKwargModelFormMixin, forms.ModelForm): 77 | class Meta: 78 | model = User 79 | 80 | def __init__(self, *args, **kwargs): 81 | super(UserForm, self).__init__(*args, **kwargs) 82 | 83 | if not self.user.is_superuser: 84 | del self.fields["group"] 85 | 86 | 87 | .. _SuccessURLRedirectListMixin: 88 | 89 | SuccessURLRedirectListMixin 90 | --------------------------- 91 | 92 | The ``SuccessURLRedirectListMixin`` is a bit more tailored to how CRUD_ is often handled within CMSes. Many CMSes, by design, redirect the user to the ``ListView`` for whatever model they are working with, whether they are creating a new instance, editing an existing one, or deleting one. Rather than having to override ``get_success_url`` on every view, use this mixin and pass it a reversible route name. Example: 93 | 94 | :: 95 | 96 | # urls.py 97 | url(r"^users/$", UserListView.as_view(), name="users_list"), 98 | 99 | # views.py 100 | from django.views import CreateView 101 | 102 | from braces import views 103 | 104 | 105 | class UserCreateView(views.LoginRequiredMixin, views.PermissionRequiredMixin, 106 | views.SuccessURLRedirectListMixin, CreateView): 107 | 108 | form_class = UserForm 109 | model = User 110 | permission_required = "auth.add_user" 111 | success_list_url = "users_list" 112 | ... 113 | 114 | 115 | .. _FormValidMessageMixin: 116 | 117 | FormValidMessageMixin 118 | --------------------- 119 | 120 | .. versionadded:: 1.2 121 | 122 | The ``FormValidMessageMixin`` allows you to to *statically* or *programmatically* set a message to be returned using Django's `messages`_ framework when the form is valid. The returned message is controlled by the ``form_valid_message`` property which can either be set on the view or returned by the ``get_form_valid_message`` method. The message is not processed until the end of the ``form_valid`` method. 123 | 124 | .. warning:: 125 | This mixin requires the Django `messages`_ app to be enabled. 126 | 127 | .. note:: 128 | This mixin is designed for use with Django's generic form class-based views, e.g. ``FormView``, ``CreateView``, ``UpdateView`` 129 | 130 | 131 | Static Example 132 | ^^^^^^^^^^^^^^ 133 | 134 | :: 135 | 136 | from django.utils.translation import ugettext_lazy as _ 137 | from django.views.generic import CreateView 138 | 139 | from braces.views import FormValidMessageMixin 140 | 141 | 142 | class BlogPostCreateView(FormValidMessageMixin, CreateView): 143 | form_class = PostForm 144 | model = Post 145 | form_valid_message = _(u"Blog post created!") 146 | 147 | 148 | Dynamic Example 149 | ^^^^^^^^^^^^^^^ 150 | 151 | :: 152 | 153 | from django.views.generic import CreateView 154 | 155 | from braces.views import FormValidMessageMixin 156 | 157 | 158 | class BlogPostCreateView(FormValidMessageMixin, CreateView): 159 | form_class = PostForm 160 | model = Post 161 | 162 | def get_form_valid_message(self): 163 | return u"{0} created!".format(self.object.title) 164 | 165 | 166 | 167 | .. _FormInvalidMessageMixin: 168 | 169 | FormInvalidMessageMixin 170 | ----------------------- 171 | 172 | .. versionadded:: 1.2 173 | 174 | The ``FormInvalidMessageMixin`` allows you to to *statically* or *programmatically* set a message to be returned using Django's `messages`_ framework when the form is invalid. The returned message is controlled by the ``form_invalid_message`` property which can either be set on the view or returned by the ``get_form_invalid_message`` method. The message is not processed until the end of the ``form_invalid`` method. 175 | 176 | .. warning:: 177 | This mixin requires the Django `messages`_ app to be enabled. 178 | 179 | .. note:: 180 | This mixin is designed for use with Django's generic form class-based views, e.g. ``FormView``, ``CreateView``, ``UpdateView`` 181 | 182 | Static Example 183 | ^^^^^^^^^^^^^^ 184 | 185 | :: 186 | 187 | from django.utils.translation import ugettext_lazy 188 | from django.views.generic import CreateView 189 | 190 | from braces.views import FormInvalidMessageMixin 191 | 192 | 193 | class BlogPostCreateView(FormInvalidMessageMixin, CreateView): 194 | form_class = PostForm 195 | model = Post 196 | form_invalid_message = _(u"Oh snap, something went wrong!") 197 | 198 | 199 | Dynamic Example 200 | ^^^^^^^^^^^^^^^ 201 | 202 | :: 203 | 204 | from django.utils.translation import ugettext_lazy as _ 205 | from django.views.generic import CreateView 206 | 207 | from braces.views import FormInvalidMessageMixin 208 | 209 | 210 | class BlogPostCreateView(FormInvalidMessageMixin, CreateView): 211 | form_class = PostForm 212 | model = Post 213 | 214 | def get_form_invalid_message(self): 215 | return _(u"Some custom message") 216 | 217 | 218 | .. _FormMessagesMixin: 219 | 220 | FormMessagesMixin 221 | ----------------- 222 | 223 | .. versionadded:: 1.2 224 | 225 | ``FormMessagesMixin`` is a convenience mixin which combines :ref:`FormValidMessageMixin` and :ref:`FormInvalidMessageMixin` since we commonly provide messages for both states (``form_valid``, ``form_invalid``). 226 | 227 | .. warning:: 228 | This mixin requires the Django `messages`_ app to be enabled. 229 | 230 | Static & Dynamic Example 231 | ^^^^^^^^^^^^^^^^^^^^^^^^ 232 | 233 | :: 234 | 235 | from django.utils.translation import ugettext_lazy as _ 236 | from django.views.generic import CreateView 237 | 238 | from braces.views import FormMessagesMixin 239 | 240 | 241 | class BlogPostCreateView(FormMessagesMixin, CreateView): 242 | form_class = PostForm 243 | form_invalid_message = _(u"Something went wrong, post was not saved") 244 | model = Post 245 | 246 | def get_form_valid_message(self): 247 | return u"{0} created!".format(self.object.title) 248 | 249 | 250 | .. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete 251 | .. _CSRF protection: https://docs.djangoproject.com/en/1.5/ref/contrib/csrf/ 252 | .. _csrf_exempt: https://docs.djangoproject.com/en/1.5/ref/contrib/csrf/#django.views.decorators.csrf.csrf_exempt 253 | .. _messages: https://docs.djangoproject.com/en/1.5/ref/contrib/messages/ 254 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-braces documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 30 10:31:44 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | sys.path.insert(0, os.path.abspath('..')) 17 | import braces 18 | from braces import __version__ 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['releases'] 33 | 34 | releases_issue_uri = "https://github.com/brack3t/django-braces/issues/%s" 35 | releases_release_uri = "https://github.com/brack3t/django-braces/tree/%s" 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = u'django-braces' 51 | copyright = u'2013, Kenneth Love and Chris Jones' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = __version__ 59 | # The full version, including alpha/beta/rc tags. 60 | release = version 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | #html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | #html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | #html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | #html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | #html_domain_indices = True 149 | 150 | # If false, no index is generated. 151 | #html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | #html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | #html_show_sourcelink = True 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | #html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | #html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | #html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | #html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = 'django-bracesdoc' 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | latex_elements = { 180 | # The paper size ('letterpaper' or 'a4paper'). 181 | #'papersize': 'letterpaper', 182 | 183 | # The font size ('10pt', '11pt' or '12pt'). 184 | #'pointsize': '10pt', 185 | 186 | # Additional stuff for the LaTeX preamble. 187 | #'preamble': '', 188 | } 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, author, documentclass [howto/manual]). 192 | latex_documents = [ 193 | ('index', 'django-braces.tex', 'django-braces Documentation', 194 | 'Kenneth Love and Chris Jones', 'manual'), 195 | ] 196 | 197 | # The name of an image file (relative to this directory) to place at the top of 198 | # the title page. 199 | #latex_logo = None 200 | 201 | # For "manual" documents, if this is true, then toplevel headings are parts, 202 | # not chapters. 203 | #latex_use_parts = False 204 | 205 | # If true, show page references after internal links. 206 | #latex_show_pagerefs = False 207 | 208 | # If true, show URL addresses after external links. 209 | #latex_show_urls = False 210 | 211 | # Documents to append as an appendix to all manuals. 212 | #latex_appendices = [] 213 | 214 | # If false, no module index is generated. 215 | #latex_domain_indices = True 216 | 217 | 218 | # -- Options for manual page output -------------------------------------------- 219 | 220 | # One entry per manual page. List of tuples 221 | # (source start file, name, description, authors, manual section). 222 | man_pages = [ 223 | ('index', 'django-braces', 'django-braces Documentation', 224 | ['Kenneth Love and Chris Jones'], 1) 225 | ] 226 | 227 | # If true, show URL addresses after external links. 228 | #man_show_urls = False 229 | 230 | 231 | # -- Options for Texinfo output ------------------------------------------------ 232 | 233 | # Grouping the document tree into Texinfo files. List of tuples 234 | # (source start file, target name, title, author, 235 | # dir menu entry, description, category) 236 | texinfo_documents = [ 237 | ('index', 'django-braces', 'django-braces Documentation', 238 | 'Kenneth Love and Chris Jones', 'django-braces', 'One line description of project.', 239 | 'Miscellaneous'), 240 | ] 241 | 242 | # Documents to append as an appendix to all manuals. 243 | #texinfo_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | #texinfo_domain_indices = True 247 | 248 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 249 | #texinfo_show_urls = 'footnote' 250 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import codecs 4 | 5 | from django.contrib.auth.models import User 6 | from django.http import HttpResponse 7 | from django.utils.translation import ugettext_lazy as _ 8 | from django.views.generic import (View, UpdateView, FormView, TemplateView, 9 | ListView, DetailView, CreateView) 10 | 11 | from braces import views 12 | 13 | from .models import Article, CanonicalArticle 14 | from .forms import ArticleForm, FormWithUserKwarg 15 | from .helpers import SetJSONEncoder 16 | 17 | 18 | class OkView(View): 19 | """ 20 | A view which simply returns "OK" for every request. 21 | """ 22 | def get(self, request): 23 | return HttpResponse("OK") 24 | 25 | def post(self, request): 26 | return self.get(request) 27 | 28 | def put(self, request): 29 | return self.get(request) 30 | 31 | def delete(self, request): 32 | return self.get(request) 33 | 34 | 35 | class LoginRequiredView(views.LoginRequiredMixin, OkView): 36 | """ 37 | A view for testing LoginRequiredMixin. 38 | """ 39 | 40 | 41 | class AnonymousRequiredView(views.AnonymousRequiredMixin, OkView): 42 | """ 43 | A view for testing AnonymousRequiredMixin. Should accept 44 | unauthenticated users and redirect authenticated users to the 45 | authenticated_redirect_url set on the view. 46 | """ 47 | authenticated_redirect_url = '/authenticated_view/' 48 | 49 | 50 | class AuthenticatedView(views.LoginRequiredMixin, OkView): 51 | """ 52 | A view for testing AnonymousRequiredMixin. Should accept 53 | authenticated users. 54 | """ 55 | 56 | 57 | class AjaxResponseView(views.AjaxResponseMixin, OkView): 58 | """ 59 | A view for testing AjaxResponseMixin. 60 | """ 61 | def get_ajax(self, request): 62 | return HttpResponse("AJAX_OK") 63 | 64 | def post_ajax(self, request): 65 | return self.get_ajax(request) 66 | 67 | def put_ajax(self, request): 68 | return self.get_ajax(request) 69 | 70 | def delete_ajax(self, request): 71 | return self.get_ajax(request) 72 | 73 | 74 | class SimpleJsonView(views.JSONResponseMixin, View): 75 | """ 76 | A view for testing JSONResponseMixin's render_json_response() method. 77 | """ 78 | def get(self, request): 79 | object = {'username': request.user.username} 80 | return self.render_json_response(object) 81 | 82 | 83 | class CustomJsonEncoderView(views.JSONResponseMixin, View): 84 | """ 85 | A view for testing JSONResponseMixin's `json_encoder_class` attribute 86 | with custom JSONEncoder class. 87 | """ 88 | json_encoder_class = SetJSONEncoder 89 | 90 | def get(self, request): 91 | object = {'numbers': set([1, 2, 3])} 92 | return self.render_json_response(object) 93 | 94 | 95 | class SimpleJsonBadRequestView(views.JSONResponseMixin, View): 96 | """ 97 | A view for testing JSONResponseMixin's render_json_response() method with 98 | 400 HTTP status code. 99 | """ 100 | def get(self, request): 101 | object = {'username': request.user.username} 102 | return self.render_json_response(object, status=400) 103 | 104 | 105 | class ArticleListJsonView(views.JSONResponseMixin, View): 106 | """ 107 | A view for testing JSONResponseMixin's render_json_object_response() 108 | method. 109 | """ 110 | def get(self, request): 111 | queryset = Article.objects.all() 112 | return self.render_json_object_response( 113 | queryset, fields=('title',)) 114 | 115 | 116 | class JsonRequestResponseView(views.JsonRequestResponseMixin, View): 117 | """ 118 | A view for testing JsonRequestResponseMixin's json conversion 119 | """ 120 | def post(self, request): 121 | return self.render_json_response(self.request_json) 122 | 123 | 124 | class JsonBadRequestView(views.JsonRequestResponseMixin, View): 125 | """ 126 | A view for testing JsonRequestResponseMixin's require_json 127 | and render_bad_request_response methods 128 | """ 129 | require_json = True 130 | 131 | def post(self, request, *args, **kwargs): 132 | return self.render_json_response(self.request_json) 133 | 134 | 135 | class JsonCustomBadRequestView(views.JsonRequestResponseMixin, View): 136 | """ 137 | A view for testing JsonRequestResponseMixin's 138 | render_bad_request_response method with a custom error message 139 | """ 140 | def post(self, request, *args, **kwargs): 141 | if not self.request_json: 142 | return self.render_bad_request_response( 143 | {'error': 'you messed up'}) 144 | return self.render_json_response(self.request_json) 145 | 146 | 147 | class CreateArticleView(CreateView): 148 | """ 149 | View for testing CreateAndRedirectEditToView. 150 | """ 151 | model = Article 152 | template_name = 'form.html' 153 | 154 | 155 | class EditArticleView(UpdateView): 156 | """ 157 | View for testing CreateAndRedirectEditToView. 158 | """ 159 | model = Article 160 | template_name = 'form.html' 161 | 162 | 163 | class CreateArticleAndRedirectToListView(views.SuccessURLRedirectListMixin, 164 | CreateArticleView): 165 | """ 166 | View for testing SuccessURLRedirectListMixin 167 | """ 168 | success_list_url = 'article_list' 169 | 170 | 171 | class CreateArticleAndRedirectToListViewBad(views.SuccessURLRedirectListMixin, 172 | CreateArticleView): 173 | """ 174 | View for testing SuccessURLRedirectListMixin 175 | """ 176 | success_list_url = None 177 | 178 | 179 | class ArticleListView(views.SelectRelatedMixin, ListView): 180 | """ 181 | A list view for articles, required for testing SuccessURLRedirectListMixin. 182 | 183 | Also used to test SelectRelatedMixin. 184 | """ 185 | model = Article 186 | template_name = 'blank.html' 187 | select_related = ('author',) 188 | 189 | 190 | class ArticleListViewWithCustomQueryset(views.SelectRelatedMixin, ListView): 191 | """ 192 | Another list view for articles, required to test SelectRelatedMixin. 193 | """ 194 | queryset = Article.objects.select_related('author').prefetch_related('article_set') 195 | template_name = 'blank.html' 196 | select_related = () 197 | 198 | 199 | class FormWithUserKwargView(views.UserFormKwargsMixin, FormView): 200 | """ 201 | View for testing UserFormKwargsMixin. 202 | """ 203 | form_class = FormWithUserKwarg 204 | template_name = 'form.html' 205 | 206 | def form_valid(self, form): 207 | return HttpResponse("username: %s" % form.user.username) 208 | 209 | 210 | class HeadlineView(views.SetHeadlineMixin, TemplateView): 211 | """ 212 | View for testing SetHeadlineMixin. 213 | """ 214 | template_name = 'blank.html' 215 | headline = "Test headline" 216 | 217 | 218 | class LazyHeadlineView(views.SetHeadlineMixin, TemplateView): 219 | """ 220 | View for testing SetHeadlineMixin. 221 | """ 222 | template_name = 'blank.html' 223 | headline = _("Test Headline") 224 | 225 | 226 | class ContextView(views.StaticContextMixin, TemplateView): 227 | """ View for testing StaticContextMixin. """ 228 | template_name = 'blank.html' 229 | static_context = {'test': True} 230 | 231 | 232 | class DynamicHeadlineView(views.SetHeadlineMixin, TemplateView): 233 | """ 234 | View for testing SetHeadlineMixin's get_headline() method. 235 | """ 236 | template_name = 'blank.html' 237 | 238 | def get_headline(self): 239 | return self.kwargs['s'] 240 | 241 | 242 | class PermissionRequiredView(views.PermissionRequiredMixin, OkView): 243 | """ 244 | View for testing PermissionRequiredMixin. 245 | """ 246 | permission_required = 'auth.add_user' 247 | 248 | 249 | class MultiplePermissionsRequiredView(views.MultiplePermissionsRequiredMixin, 250 | OkView): 251 | permissions = { 252 | 'all': ['tests.add_article', 'tests.change_article'], 253 | 'any': ['auth.add_user', 'auth.change_user'], 254 | } 255 | 256 | 257 | class SuperuserRequiredView(views.SuperuserRequiredMixin, OkView): 258 | pass 259 | 260 | 261 | class StaffuserRequiredView(views.StaffuserRequiredMixin, OkView): 262 | pass 263 | 264 | 265 | class CsrfExemptView(views.CsrfExemptMixin, OkView): 266 | pass 267 | 268 | 269 | class AuthorDetailView(views.PrefetchRelatedMixin, ListView): 270 | model = User 271 | prefetch_related = ['article_set'] 272 | template_name = 'blank.html' 273 | 274 | 275 | class OrderableListView(views.OrderableListMixin, ListView): 276 | model = Article 277 | orderable_columns = ('id', 'title', ) 278 | orderable_columns_default = 'id' 279 | 280 | 281 | class CanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): 282 | model = Article 283 | template_name = 'blank.html' 284 | 285 | 286 | class OverriddenCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, 287 | DetailView): 288 | model = Article 289 | template_name = 'blank.html' 290 | 291 | def get_canonical_slug(self): 292 | return codecs.encode(self.get_object().slug, 'rot_13') 293 | 294 | 295 | class CanonicalSlugDetailCustomUrlKwargsView(views.CanonicalSlugDetailMixin, 296 | DetailView): 297 | model = Article 298 | template_name = 'blank.html' 299 | pk_url_kwarg = 'my_pk' 300 | slug_url_kwarg = 'my_slug' 301 | 302 | 303 | class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, 304 | DetailView): 305 | model = CanonicalArticle 306 | template_name = 'blank.html' 307 | 308 | 309 | class FormMessagesView(views.FormMessagesMixin, CreateView): 310 | form_class = ArticleForm 311 | form_invalid_message = _('Invalid') 312 | form_valid_message = _('Valid') 313 | model = Article 314 | success_url = '/form_messages/' 315 | template_name = 'form.html' 316 | 317 | 318 | class GroupRequiredView(views.GroupRequiredMixin, OkView): 319 | group_required = 'test_group' 320 | 321 | 322 | class UserPassesTestView(views.UserPassesTestMixin, OkView): 323 | def test_func(self, user): 324 | return user.is_staff and not user.is_superuser \ 325 | and user.email.endswith('@mydomain.com') 326 | 327 | 328 | class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView): 329 | pass 330 | 331 | 332 | class AllVerbsView(views.AllVerbsMixin, View): 333 | def all(self, request, *args, **kwargs): 334 | return HttpResponse('All verbs return this!') 335 | -------------------------------------------------------------------------------- /docs/access.rst: -------------------------------------------------------------------------------- 1 | Access Mixins 2 | ============= 3 | 4 | These mixins all control a user's access to a given view. Since many of them extend the ``AccessMixin``, the following are common attributes: 5 | 6 | :: 7 | 8 | login_url = settings.LOGIN_URL 9 | redirect_field_name = REDIRECT_FIELD_NAME 10 | raise_exception = False 11 | 12 | The ``raise_exception`` attribute will cause the view to raise a ``PermissionDenied`` exception if it is set to ``True``, otherwise the view will redirect to the login view provided. 13 | 14 | .. contents:: 15 | 16 | .. _LoginRequiredMixin: 17 | 18 | LoginRequiredMixin 19 | ------------------ 20 | 21 | This mixin is rather simple and is generally the first inherited class in any view. If you don't have an authenticated user, there's no need to go any further. If you've used Django before you are probably familiar with the ``login_required`` decorator. This mixin replicates the decorator's functionality. 22 | 23 | .. note:: 24 | As of version 1.0, the LoginRequiredMixin has been rewritten to behave like the rest of the ``access`` mixins. It now accepts ``login_url``, ``redirect_field_name`` 25 | and ``raise_exception``. 26 | 27 | .. note:: 28 | 29 | This should be the left-most mixin of a view, except when combined with :ref:`CsrfExemptMixin` - which in that case should be the left-most mixin. 30 | 31 | :: 32 | 33 | from django.views.generic import TemplateView 34 | 35 | from braces.views import LoginRequiredMixin 36 | 37 | 38 | class SomeSecretView(LoginRequiredMixin, TemplateView): 39 | template_name = "path/to/template.html" 40 | 41 | #optional 42 | login_url = "/signup/" 43 | redirect_field_name = "hollaback" 44 | raise_exception = True 45 | 46 | def get(self, request): 47 | return self.render_to_response({}) 48 | 49 | An optional class attribute of ``redirect_unauthenticated_users`` can be set to ``True`` if you are using another ``access`` mixin with ``raise_exception`` set to ``True``. This will redirect to the login page if the user is not authenticated, but raises an exception if they are but do not have the required access defined by the other mixins. This defaults to ``False``. 50 | 51 | .. _PermissionRequiredMixin: 52 | 53 | PermissionRequiredMixin 54 | ----------------------- 55 | 56 | This mixin was originally written by `Daniel Sokolowski`_ (`code here`_), but this version eliminates an unneeded render if the permissions check fails. 57 | 58 | Rather than overloading the dispatch method manually on every view that needs to check for the existence of a permission, use this mixin and set the ``permission_required`` class attribute on your view. If you don't specify ``permission_required`` on your view, an ``ImproperlyConfigured`` exception is raised reminding you that you haven't set it. 59 | 60 | The one limitation of this mixin is that it can **only** accept a single permission. If you need multiple permissions use :ref:`MultiplePermissionsRequiredMixin`. 61 | 62 | In normal use of this mixin, :ref:`LoginRequiredMixin` comes first, then the ``PermissionRequiredMixin``. If the user isn't an authenticated user, there is no point in checking for any permissions. 63 | 64 | .. note:: 65 | If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. 66 | 67 | :: 68 | 69 | from django.views import TemplateView 70 | 71 | from braces import views 72 | 73 | 74 | class SomeProtectedView(views.LoginRequiredMixin, 75 | views.PermissionRequiredMixin, 76 | TemplateView): 77 | 78 | permission_required = "auth.change_user" 79 | template_name = "path/to/template.html" 80 | 81 | The ``PermissionRequiredMixin`` also offers a ``check_permssions`` method that should be overridden if you need custom permissions checking. 82 | 83 | 84 | .. _MultiplePermissionsRequiredMixin: 85 | 86 | MultiplePermissionsRequiredMixin 87 | -------------------------------- 88 | 89 | The ``MultiplePermissionsRequiredMixin`` is a more powerful version of the :ref:`PermissionRequiredMixin`. This view mixin can handle multiple permissions by setting the mandatory ``permissions`` attribute as a dict with the keys ``any`` and/or ``all`` to a list or tuple of permissions. The ``all`` key requires the ``request.user`` to have **all** of the specified permissions. The ``any`` key requires the ``request.user`` to have **at least one** of the specified permissions. If you only need to check a single permission, the :ref:`PermissionRequiredMixin` is a better choice. 90 | 91 | .. note:: 92 | If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. 93 | 94 | :: 95 | 96 | from django.views import TemplateView 97 | 98 | from braces import views 99 | 100 | 101 | class SomeProtectedView(views.LoginRequiredMixin, 102 | views.MultiplePermissionsRequiredMixin, 103 | TemplateView): 104 | 105 | #required 106 | permissions = { 107 | "all": ("blog.add_post", "blog.change_post"), 108 | "any": ("blog.delete_post", "user.change_user") 109 | } 110 | 111 | The ``MultiplePermissionsRequiredMixin`` also offers a ``check_permssions`` method that should be overridden if you need custom permissions checking. 112 | 113 | 114 | .. _GroupRequiredMixin: 115 | 116 | GroupRequiredMixin 117 | ------------------ 118 | 119 | .. versionadded:: 1.2 120 | 121 | The ``GroupRequiredMixin`` ensures that the requesting user is in the group or groups specified. This view mixin can handle multiple groups by setting the mandatory ``group_required`` attribute as a list or tuple. 122 | 123 | .. note:: 124 | The mixin assumes you're using Django's default Group model and that your user model provides ``groups`` as a ManyToMany relationship. 125 | If this **is not** the case, you'll need to override ``check_membership`` in the mixin to handle your custom set up. 126 | 127 | Standard Django Usage 128 | ^^^^^^^^^^^^^^^^^^^^^ 129 | 130 | :: 131 | 132 | from django.views import TemplateView 133 | 134 | from braces.views import GroupRequiredMixin 135 | 136 | 137 | class SomeProtectedView(GroupRequiredMixin, TemplateView): 138 | 139 | #required 140 | group_required = u"editors" 141 | 142 | Multiple Groups Possible Usage 143 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 144 | 145 | :: 146 | 147 | from django.views import TemplateView 148 | 149 | from braces.views import GroupRequiredMixin 150 | 151 | 152 | class SomeProtectedView(GroupRequiredMixin, TemplateView): 153 | 154 | #required 155 | group_required = [u"editors", u"admins"] 156 | 157 | 158 | Custom Group Usage 159 | ^^^^^^^^^^^^^^^^^^ 160 | 161 | :: 162 | 163 | from django.views import TemplateView 164 | 165 | from braces.views import GroupRequiredMixin 166 | 167 | 168 | class SomeProtectedView(GroupRequiredMixin, TemplateView): 169 | 170 | #required 171 | group_required = u"editors" 172 | 173 | def check_membership(self, group): 174 | ... 175 | # Check some other system for group membership 176 | if user_in_group: 177 | return True 178 | else: 179 | return False 180 | 181 | 182 | .. _UserPassesTestMixin: 183 | 184 | UserPassesTestMixin 185 | ------------------- 186 | 187 | .. versionadded:: 1.3.0 188 | 189 | Mixin that reimplements the `user_passes_test`_ decorator. This is helpful for much more complicated cases than checking if user ``is_superuser`` (for example if their email is from a specific domain). 190 | 191 | :: 192 | 193 | from django.views import TemplateView 194 | 195 | from braces.views import UserPassesTestMixin 196 | 197 | 198 | class SomeUserPassView(UserPassesTestMixin, TemplateView): 199 | def test_func(self, user): 200 | return (user.is_staff and not user.is_superuser 201 | and user.email.endswith(u"mydomain.com")) 202 | 203 | 204 | .. _SuperuserRequiredMixin: 205 | 206 | SuperuserRequiredMixin 207 | ---------------------- 208 | 209 | Another permission-based mixin. This is specifically for requiring a user to be a superuser. Comes in handy for tools that only privileged users should have access to. 210 | 211 | :: 212 | 213 | from django.views import TemplateView 214 | 215 | from braces import views 216 | 217 | 218 | class SomeSuperuserView(views.LoginRequiredMixin, 219 | views.SuperuserRequiredMixin, 220 | TemplateView): 221 | 222 | template_name = u"path/to/template.html" 223 | 224 | 225 | .. _AnonymousRequiredMixin: 226 | 227 | AnonymousRequiredMixin 228 | ---------------------- 229 | 230 | .. versionadded:: 1.4.0 231 | 232 | Mixin that will redirect authenticated users to a different view. The default redirect is to 233 | Django's `settings.LOGIN_REDIRECT_URL`_. 234 | 235 | 236 | Static Examples 237 | ^^^^^^^^^^^^^^^ 238 | 239 | :: 240 | 241 | from django.views import TemplateView 242 | 243 | from braces.views import AnonymousRequiredMixin 244 | 245 | 246 | class SomeView(AnonymousRequiredMixin, TemplateView): 247 | authenticated_redirect_url = u"/send/away/" 248 | 249 | 250 | :: 251 | 252 | from django.core.urlresolvers import reverse_lazy 253 | from django.views import TemplateView 254 | 255 | from braces.views import AnonymousRequiredMixin 256 | 257 | 258 | class SomeLazyView(AnonymousRequiredMixin, TemplateView): 259 | authenticated_redirect_url = reverse_lazy(u"view_url") 260 | 261 | 262 | Dynamic Example 263 | ^^^^^^^^^^^^^^^ 264 | 265 | :: 266 | 267 | from django.views import TemplateView 268 | 269 | from braces.views import AnonymousRequiredMixin 270 | 271 | 272 | class SomeView(AnonymousRequiredMixin, TemplateView): 273 | """ Redirect based on user level """ 274 | def get_authenticated_redirect_url(self): 275 | if self.request.user.is_superuser: 276 | return u"/admin/" 277 | return u"/somewhere/else/" 278 | 279 | 280 | .. _StaffuserRequiredMixin: 281 | 282 | StaffuserRequiredMixin 283 | ---------------------- 284 | 285 | Similar to :ref:`SuperuserRequiredMixin`, this mixin allows you to require a user with ``is_staff`` set to ``True``. 286 | 287 | :: 288 | 289 | from django.views import TemplateView 290 | 291 | from braces import views 292 | 293 | 294 | class SomeStaffuserView(views.LoginRequiredMixin, 295 | views.StaffuserRequiredMixin, 296 | TemplateView): 297 | 298 | template_name = u"path/to/template.html" 299 | 300 | .. _Daniel Sokolowski: https://github.com/danols 301 | .. _code here: https://github.com/lukaszb/django-guardian/issues/48 302 | .. _user_passes_test: https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.decorators.user_passes_test 303 | .. _settings.LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/1.6/ref/settings/#login-redirect-url 304 | -------------------------------------------------------------------------------- /braces/views/_access.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import REDIRECT_FIELD_NAME 5 | from django.contrib.auth.views import redirect_to_login 6 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied 7 | from django.http import HttpResponseRedirect 8 | from django.utils.encoding import force_text 9 | 10 | 11 | class AccessMixin(object): 12 | """ 13 | 'Abstract' mixin that gives access mixins the same customizable 14 | functionality. 15 | """ 16 | login_url = None 17 | raise_exception = False # Default whether to raise an exception to none 18 | redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth 19 | 20 | def get_login_url(self): 21 | """ 22 | Override this method to customize the login_url. 23 | """ 24 | login_url = self.login_url or settings.LOGIN_URL 25 | if not login_url: 26 | raise ImproperlyConfigured( 27 | 'Define {0}.login_url or settings.LOGIN_URL or override ' 28 | '{0}.get_login_url().'.format(self.__class__.__name__)) 29 | 30 | return force_text(login_url) 31 | 32 | def get_redirect_field_name(self): 33 | """ 34 | Override this method to customize the redirect_field_name. 35 | """ 36 | if self.redirect_field_name is None: 37 | raise ImproperlyConfigured( 38 | '{0} is missing the ' 39 | 'redirect_field_name. Define {0}.redirect_field_name or ' 40 | 'override {0}.get_redirect_field_name().'.format( 41 | self.__class__.__name__)) 42 | return self.redirect_field_name 43 | 44 | 45 | class LoginRequiredMixin(AccessMixin): 46 | """ 47 | View mixin which verifies that the user is authenticated. 48 | 49 | NOTE: 50 | This should be the left-most mixin of a view, except when 51 | combined with CsrfExemptMixin - which in that case should 52 | be the left-most mixin. 53 | """ 54 | redirect_unauthenticated_users = False 55 | 56 | def dispatch(self, request, *args, **kwargs): 57 | if not request.user.is_authenticated(): 58 | if self.raise_exception and not self.redirect_unauthenticated_users: 59 | raise PermissionDenied # return a forbidden response 60 | else: 61 | return redirect_to_login(request.get_full_path(), 62 | self.get_login_url(), 63 | self.get_redirect_field_name()) 64 | 65 | return super(LoginRequiredMixin, self).dispatch( 66 | request, *args, **kwargs) 67 | 68 | 69 | class AnonymousRequiredMixin(object): 70 | """ 71 | View mixin which redirects to a specified URL if authenticated. 72 | Can be useful if you wanted to prevent authenticated users from 73 | accessing signup pages etc. 74 | 75 | NOTE: 76 | This should be the left-most mixin of a view. 77 | 78 | Example Usage 79 | 80 | class SomeView(AnonymousRequiredMixin, ListView): 81 | ... 82 | # required 83 | authenticated_redirect_url = "/accounts/profile/" 84 | ... 85 | """ 86 | authenticated_redirect_url = settings.LOGIN_REDIRECT_URL 87 | 88 | def dispatch(self, request, *args, **kwargs): 89 | if request.user.is_authenticated(): 90 | return HttpResponseRedirect(self.get_authenticated_redirect_url()) 91 | return super(AnonymousRequiredMixin, self).dispatch( 92 | request, *args, **kwargs) 93 | 94 | def get_authenticated_redirect_url(self): 95 | """ Return the reversed authenticated redirect url. """ 96 | if not self.authenticated_redirect_url: 97 | raise ImproperlyConfigured( 98 | '{0} is missing an authenticated_redirect_url ' 99 | 'url to redirect to. Define ' 100 | '{0}.authenticated_redirect_url or override ' 101 | '{0}.get_authenticated_redirect_url().'.format( 102 | self.__class__.__name__)) 103 | return self.authenticated_redirect_url 104 | 105 | 106 | class PermissionRequiredMixin(AccessMixin): 107 | """ 108 | View mixin which verifies that the logged in user has the specified 109 | permission. 110 | 111 | Class Settings 112 | `permission_required` - the permission to check for. 113 | `login_url` - the login url of site 114 | `redirect_field_name` - defaults to "next" 115 | `raise_exception` - defaults to False - raise 403 if set to True 116 | 117 | Example Usage 118 | 119 | class SomeView(PermissionRequiredMixin, ListView): 120 | ... 121 | # required 122 | permission_required = "app.permission" 123 | 124 | # optional 125 | login_url = "/signup/" 126 | redirect_field_name = "hollaback" 127 | raise_exception = True 128 | ... 129 | """ 130 | permission_required = None # Default required perms to none 131 | 132 | def get_permission_required(self, request=None): 133 | """ 134 | Get the required permissions and return them. 135 | 136 | Override this to allow for custom permission_required values. 137 | """ 138 | # Make sure that the permission_required attribute is set on the 139 | # view, or raise a configuration error. 140 | if self.permission_required is None: 141 | raise ImproperlyConfigured( 142 | '{0} requires the "permission_required" attribute to be ' 143 | 'set.'.format(self.__class__.__name__)) 144 | 145 | return self.permission_required 146 | 147 | def check_permissions(self, request): 148 | """ 149 | Returns whether or not the user has permissions 150 | """ 151 | perms = self.get_permission_required(request) 152 | return request.user.has_perm(perms) 153 | 154 | def no_permissions_fail(self, request=None): 155 | """ 156 | Called when the user has no permissions. This should only 157 | return a valid HTTP response. 158 | 159 | By default we redirect to login. 160 | """ 161 | return redirect_to_login(request.get_full_path(), 162 | self.get_login_url(), 163 | self.get_redirect_field_name()) 164 | 165 | def dispatch(self, request, *args, **kwargs): 166 | """ 167 | Check to see if the user in the request has the required 168 | permission. 169 | """ 170 | has_permission = self.check_permissions(request) 171 | 172 | if not has_permission: # If the user lacks the permission 173 | if self.raise_exception: 174 | raise PermissionDenied # Return a 403 175 | return self.no_permissions_fail(request) 176 | 177 | return super(PermissionRequiredMixin, self).dispatch( 178 | request, *args, **kwargs) 179 | 180 | 181 | class MultiplePermissionsRequiredMixin(PermissionRequiredMixin): 182 | """ 183 | View mixin which allows you to specify two types of permission 184 | requirements. The `permissions` attribute must be a dict which 185 | specifies two keys, `all` and `any`. You can use either one on 186 | its own or combine them. The value of each key is required to be a 187 | list or tuple of permissions. The standard Django permissions 188 | style is not strictly enforced. If you have created your own 189 | permissions in a different format, they should still work. 190 | 191 | By specifying the `all` key, the user must have all of 192 | the permissions in the passed in list. 193 | 194 | By specifying The `any` key , the user must have ONE of the set 195 | permissions in the list. 196 | 197 | Class Settings 198 | `permissions` - This is required to be a dict with one or both 199 | keys of `all` and/or `any` containing a list or tuple of 200 | permissions. 201 | `login_url` - the login url of site 202 | `redirect_field_name` - defaults to "next" 203 | `raise_exception` - defaults to False - raise 403 if set to True 204 | 205 | Example Usage 206 | class SomeView(MultiplePermissionsRequiredMixin, ListView): 207 | ... 208 | #required 209 | permissions = { 210 | "all": ("blog.add_post", "blog.change_post"), 211 | "any": ("blog.delete_post", "user.change_user") 212 | } 213 | 214 | #optional 215 | login_url = "/signup/" 216 | redirect_field_name = "hollaback" 217 | raise_exception = True 218 | """ 219 | permissions = None # Default required perms to none 220 | 221 | def get_permission_required(self, request=None): 222 | self._check_permissions_attr() 223 | return self.permissions 224 | 225 | def check_permissions(self, request): 226 | permissions = self.get_permission_required(request) 227 | perms_all = permissions.get('all') or None 228 | perms_any = permissions.get('any') or None 229 | 230 | self._check_permissions_keys_set(perms_all, perms_any) 231 | self._check_perms_keys("all", perms_all) 232 | self._check_perms_keys("any", perms_any) 233 | 234 | # If perms_all, check that user has all permissions in the list/tuple 235 | if perms_all: 236 | if not request.user.has_perms(perms_all): 237 | return False 238 | 239 | # If perms_any, check that user has at least one in the list/tuple 240 | if perms_any: 241 | has_one_perm = False 242 | for perm in perms_any: 243 | if request.user.has_perm(perm): 244 | has_one_perm = True 245 | break 246 | 247 | if not has_one_perm: 248 | return False 249 | 250 | return True 251 | 252 | def _check_permissions_attr(self): 253 | """ 254 | Check permissions attribute is set and that it is a dict. 255 | """ 256 | if self.permissions is None or not isinstance(self.permissions, dict): 257 | raise ImproperlyConfigured( 258 | '{0} requires the "permissions" attribute to be set as a ' 259 | 'dict.'.format(self.__class__.__name__)) 260 | 261 | def _check_permissions_keys_set(self, perms_all=None, perms_any=None): 262 | """ 263 | Check to make sure the keys `any` or `all` are not both blank. 264 | If both are blank either an empty dict came in or the wrong keys 265 | came in. Both are invalid and should raise an exception. 266 | """ 267 | if perms_all is None and perms_any is None: 268 | raise ImproperlyConfigured( 269 | '{0} requires the "permissions" attribute to be set to a ' 270 | 'dict and the "any" or "all" key to be set.'.format( 271 | self.__class__.__name__)) 272 | 273 | def _check_perms_keys(self, key=None, perms=None): 274 | """ 275 | If the permissions list/tuple passed in is set, check to make 276 | sure that it is of the type list or tuple. 277 | """ 278 | if perms and not isinstance(perms, (list, tuple)): 279 | raise ImproperlyConfigured( 280 | '{0} requires the permisions dict {1} value to be a ' 281 | 'list or tuple.'.format(self.__class__.__name__, key)) 282 | 283 | 284 | class GroupRequiredMixin(AccessMixin): 285 | group_required = None 286 | 287 | def get_group_required(self): 288 | if self.group_required is None or ( 289 | not isinstance(self.group_required, 290 | (list, tuple) + six.string_types) 291 | ): 292 | 293 | raise ImproperlyConfigured( 294 | '{0} requires the "group_required" attribute to be set and be ' 295 | 'one of the following types: string, unicode, list or ' 296 | 'tuple'.format(self.__class__.__name__)) 297 | if not isinstance(self.group_required, (list, tuple)): 298 | self.group_required = (self.group_required,) 299 | return self.group_required 300 | 301 | def check_membership(self, groups): 302 | """ Check required group(s) """ 303 | if self.request.user.is_superuser: 304 | return True 305 | user_groups = self.request.user.groups.values_list("name", flat=True) 306 | return set(groups).intersection(set(user_groups)) 307 | 308 | def dispatch(self, request, *args, **kwargs): 309 | self.request = request 310 | in_group = False 311 | if self.request.user.is_authenticated(): 312 | in_group = self.check_membership(self.get_group_required()) 313 | 314 | if not in_group: 315 | if self.raise_exception: 316 | raise PermissionDenied 317 | else: 318 | return redirect_to_login( 319 | request.get_full_path(), 320 | self.get_login_url(), 321 | self.get_redirect_field_name()) 322 | return super(GroupRequiredMixin, self).dispatch( 323 | request, *args, **kwargs) 324 | 325 | 326 | class UserPassesTestMixin(AccessMixin): 327 | """ 328 | CBV Mixin allows you to define test that every user should pass 329 | to get access into view. 330 | 331 | Class Settings 332 | `test_func` - This is required to be a method that takes user 333 | instance and return True or False after checking conditions. 334 | `login_url` - the login url of site 335 | `redirect_field_name` - defaults to "next" 336 | `raise_exception` - defaults to False - raise 403 if set to True 337 | """ 338 | 339 | def test_func(self, user): 340 | raise NotImplementedError( 341 | '{0} is missing implementation of the ' 342 | 'test_func method. You should write one.'.format( 343 | self.__class__.__name__)) 344 | 345 | def get_test_func(self): 346 | return getattr(self, "test_func") 347 | 348 | def dispatch(self, request, *args, **kwargs): 349 | user_test_result = self.get_test_func()(request.user) 350 | 351 | if not user_test_result: # If user don't pass the test 352 | if self.raise_exception: # *and* if an exception was desired 353 | raise PermissionDenied 354 | else: 355 | return redirect_to_login(request.get_full_path(), 356 | self.get_login_url(), 357 | self.get_redirect_field_name()) 358 | return super(UserPassesTestMixin, self).dispatch( 359 | request, *args, **kwargs) 360 | 361 | 362 | class SuperuserRequiredMixin(AccessMixin): 363 | """ 364 | Mixin allows you to require a user with `is_superuser` set to True. 365 | """ 366 | def dispatch(self, request, *args, **kwargs): 367 | if not request.user.is_superuser: # If the user is a standard user, 368 | if self.raise_exception: # *and* if an exception was desired 369 | raise PermissionDenied # return a forbidden response. 370 | else: 371 | return redirect_to_login(request.get_full_path(), 372 | self.get_login_url(), 373 | self.get_redirect_field_name()) 374 | 375 | return super(SuperuserRequiredMixin, self).dispatch( 376 | request, *args, **kwargs) 377 | 378 | 379 | class StaffuserRequiredMixin(AccessMixin): 380 | """ 381 | Mixin allows you to require a user with `is_staff` set to True. 382 | """ 383 | def dispatch(self, request, *args, **kwargs): 384 | if not request.user.is_staff: # If the request's user is not staff, 385 | if self.raise_exception: # *and* if an exception was desired 386 | raise PermissionDenied # return a forbidden response 387 | else: 388 | return redirect_to_login(request.get_full_path(), 389 | self.get_login_url(), 390 | self.get_redirect_field_name()) 391 | 392 | return super(StaffuserRequiredMixin, self).dispatch( 393 | request, *args, **kwargs) 394 | -------------------------------------------------------------------------------- /docs/other.rst: -------------------------------------------------------------------------------- 1 | Other Mixins 2 | ============ 3 | 4 | These mixins handle other random bits of Django's views, like controlling output, controlling content types, or setting values in the context. 5 | 6 | .. contents:: 7 | 8 | .. _SetHeadlineMixin: 9 | 10 | SetHeadlineMixin 11 | ---------------- 12 | 13 | The ``SetHeadlineMixin`` allows you to *statically* or *programmatically* set the headline of any of your views. Ideally, you'll write as few templates as possible, so a mixin like this helps you reuse generic templates. Its usage is amazingly straightforward and works much like Django's built-in ``get_queryset`` method. This mixin has two ways of being used: 14 | 15 | Static Example 16 | ^^^^^^^^^^^^^^ 17 | 18 | :: 19 | 20 | from django.utils.translation import ugettext_lazy as _ 21 | from django.views import TemplateView 22 | 23 | from braces.views import SetHeadlineMixin 24 | 25 | 26 | class HeadlineView(SetHeadlineMixin, TemplateView): 27 | headline = _(u"This is our headline") 28 | template_name = u"path/to/template.html" 29 | 30 | 31 | Dynamic Example 32 | ^^^^^^^^^^^^^^^ 33 | 34 | :: 35 | 36 | from datetime import date 37 | 38 | from django.views import TemplateView 39 | 40 | from braces.views import SetHeadlineMixin 41 | 42 | 43 | class HeadlineView(SetHeadlineMixin, TemplateView): 44 | template_name = u"path/to/template.html" 45 | 46 | def get_headline(self): 47 | return u"This is our headline for {0}".format(date.today().isoformat()) 48 | 49 | For both usages, the context now contains a ``headline`` key with your headline. 50 | 51 | 52 | .. _StaticContextMixin: 53 | 54 | StaticContextMixin 55 | ------------------ 56 | 57 | .. versionadded:: 1.4 58 | 59 | The ``StaticContextMixin`` allows you to easily set static context data by using the ``static_context`` property. 60 | 61 | .. note:: 62 | While it's possible to override the ``StaticContextMixin.get_static_context method``, it's not very practical. If you have a need to override a method for dynamic context data it's best to override the standard ``get_context_data`` method of Django's generic class-based views. 63 | 64 | 65 | View Example 66 | ^^^^^^^^^^^^ 67 | 68 | :: 69 | 70 | # views.py 71 | 72 | from django.views import TemplateView 73 | 74 | from braces.views import StaticContextMixin 75 | 76 | 77 | class ContextTemplateView(StaticContextMixin, TemplateView): 78 | static_context = {u"nav_home": True} 79 | 80 | 81 | URL Example 82 | ^^^^^^^^^^^ 83 | 84 | :: 85 | 86 | # urls.py 87 | 88 | urlpatterns = patterns( 89 | '', 90 | url(ur"^$", 91 | ContextTemplateView.as_view( 92 | template_name=u"index.html", 93 | static_context={u"nav_home": True} 94 | ), 95 | name=u"index") 96 | ) 97 | 98 | 99 | .. _SelectRelatedMixin: 100 | 101 | SelectRelatedMixin 102 | ------------------ 103 | 104 | A simple mixin which allows you to specify a list or tuple of foreign key fields to perform a `select_related`_ on. See Django's docs for more information on `select_related`_. 105 | 106 | :: 107 | 108 | # views.py 109 | from django.views.generic import DetailView 110 | 111 | from braces.views import SelectRelatedMixin 112 | 113 | from profiles.models import Profile 114 | 115 | 116 | class UserProfileView(SelectRelatedMixin, DetailView): 117 | model = Profile 118 | select_related = [u"user"] 119 | template_name = u"profiles/detail.html" 120 | 121 | .. _select_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#select-related 122 | 123 | 124 | .. _PrefetchRelatedMixin: 125 | 126 | PrefetchRelatedMixin 127 | -------------------- 128 | 129 | A simple mixin which allows you to specify a list or tuple of reverse foreign key or ManyToMany fields to perform a `prefetch_related`_ on. See Django's docs for more information on `prefetch_related`_. 130 | 131 | :: 132 | 133 | # views.py 134 | from django.contrib.auth.models import User 135 | from django.views.generic import DetailView 136 | 137 | from braces.views import PrefetchRelatedMixin 138 | 139 | 140 | class UserView(PrefetchRelatedMixin, DetailView): 141 | model = User 142 | prefetch_related = [u"post_set"] # where the Post model has an FK to the User model as an author. 143 | template_name = u"users/detail.html" 144 | 145 | .. _prefetch_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#prefetch-related 146 | 147 | 148 | .. _JSONResponseMixin: 149 | 150 | JSONResponseMixin 151 | ----------------- 152 | 153 | .. versionchanged:: 1.1 154 | ``render_json_response`` now accepts a ``status_code`` keyword argument. 155 | ``json_dumps_kwargs`` class-attribute and ``get_json_dumps_kwargs`` method to provide arguments to the ``json.dumps()`` method. 156 | 157 | A simple mixin to handle very simple serialization as a response to the browser. 158 | 159 | :: 160 | 161 | # views.py 162 | from django.views.generic import DetailView 163 | 164 | from braces.views import JSONResponseMixin 165 | 166 | class UserProfileAJAXView(JSONResponseMixin, DetailView): 167 | model = Profile 168 | json_dumps_kwargs = {u"indent": 2} 169 | 170 | def get(self, request, *args, **kwargs): 171 | self.object = self.get_object() 172 | 173 | context_dict = { 174 | u"name": self.object.user.name, 175 | u"location": self.object.location 176 | } 177 | 178 | return self.render_json_response(context_dict) 179 | 180 | You can additionally use the `AjaxResponseMixin` 181 | 182 | :: 183 | 184 | # views.py 185 | from django.views import DetailView 186 | 187 | from braces import views 188 | 189 | 190 | class UserProfileView(views.JSONResponseMixin, 191 | views.AjaxResponseMixin, 192 | DetailView): 193 | model = Profile 194 | 195 | def get_ajax(self, request, *args, **kwargs): 196 | return self.render_json_object_response(self.get_object()) 197 | 198 | The `JSONResponseMixin` provides a class-level variable to control the response 199 | type as well. By default it is `application/json`, but you can override that by 200 | providing the `content_type` variable a different value or, programmatically, by 201 | overriding the `get_content_type()` method. 202 | 203 | :: 204 | 205 | from django.views import DetailView 206 | 207 | from braces.views import JSONResponseMixin 208 | 209 | 210 | class UserProfileAJAXView(JSONResponseMixin, DetailView): 211 | content_type = u"application/javascript" 212 | model = Profile 213 | 214 | def get(self, request, *args, **kwargs): 215 | self.object = self.get_object() 216 | 217 | context_dict = { 218 | u"name": self.object.user.name, 219 | u"location": self.object.location 220 | } 221 | 222 | return self.render_json_response(context_dict) 223 | 224 | def get_content_type(self): 225 | # Shown just for illustrative purposes 226 | return u"application/javascript" 227 | 228 | The `JSONResponseMixin` provides another class-level variable 229 | `json_encoder_class` to use a custom json encoder with `json.dumps`. 230 | By default it is `django.core.serializers.json.DjangoJsonEncoder` 231 | 232 | :: 233 | 234 | from django.core.serializers.json import DjangoJSONEncoder 235 | 236 | from braces.views import JSONResponseMixin 237 | 238 | 239 | class SetJSONEncoder(DjangoJSONEncoder): 240 | """ 241 | A custom JSONEncoder extending `DjangoJSONEncoder` to handle serialization 242 | of `set`. 243 | """ 244 | def default(self, obj): 245 | if isinstance(obj, set): 246 | return list(obj) 247 | return super(DjangoJSONEncoder, self).default(obj) 248 | 249 | 250 | class GetSetDataView(JSONResponseMixin, View): 251 | json_encoder_class = SetJSONEncoder 252 | 253 | def get(self, request, *args, **kwargs): 254 | numbers_set = set(range(10)) 255 | data = {'numbers': numbers_set} 256 | return self.render_json_response(data) 257 | 258 | .. _JsonRequestResponseMixin: 259 | 260 | JsonRequestResponseMixin 261 | ------------------------ 262 | 263 | .. versionadded:: 1.3 264 | 265 | A mixin that attempts to parse the request as JSON. If the request is properly formatted, the JSON is saved to ``self.request_json`` as a Python object. ``request_json`` will be ``None`` for imparsible requests. 266 | 267 | To catch requests that aren't JSON-formatted, set the class attribute ``require_json`` to ``True``. 268 | 269 | Override the class attribute ``error_response_dict`` to customize the default error message. 270 | 271 | It extends :ref:`JSONResponseMixin`, so those utilities are available as well. 272 | 273 | .. note:: 274 | To allow public access to your view, you'll need to use the ``csrf_exempt`` decorator or :ref:`CsrfExemptMixin`. 275 | 276 | :: 277 | 278 | from django.utils.translation import ugettext_lazy as _ 279 | from django.views.generic import View 280 | 281 | from braces import views 282 | 283 | class SomeView(views.CsrfExemptMixin, views.JsonRequestResponseMixin, View): 284 | require_json = True 285 | 286 | def post(self, request, *args, **kwargs): 287 | try: 288 | burrito = self.request_json[u"burrito"] 289 | toppings = self.request_json[u"toppings"] 290 | except KeyError: 291 | error_dict = {u"message": 292 | _(u"your order must include a burrito AND toppings")} 293 | return self.render_bad_request_response(error_dict) 294 | place_order(burrito, toppings) 295 | return self.render_json_response( 296 | {u"message": _(u"Your order has been placed!")}) 297 | 298 | 299 | .. _AjaxResponseMixin: 300 | 301 | AjaxResponseMixin 302 | ----------------- 303 | 304 | This mixin provides hooks for altenate processing of AJAX requests based on HTTP verb. 305 | 306 | To control AJAX-specific behavior, override ``get_ajax``, ``post_ajax``, ``put_ajax``, or ``delete_ajax``. All four methods take ``request``, ``*args``, and ``**kwargs`` like the standard view methods. 307 | 308 | :: 309 | 310 | # views.py 311 | from django.views.generic import View 312 | 313 | from braces import views 314 | 315 | class SomeView(views.JSONResponseMixin, views.AjaxResponseMixin, View): 316 | def get_ajax(self, request, *args, **kwargs): 317 | json_dict = { 318 | 'name': "Benny's Burritos", 319 | 'location': "New York, NY" 320 | } 321 | return self.render_json_response(json_dict) 322 | 323 | .. note:: 324 | This mixin is only useful if you need to have behavior in your view fork based on ``request.is_ajax()``. 325 | 326 | 327 | .. _OrderableListMixin: 328 | 329 | OrderableListMixin 330 | ------------------ 331 | 332 | .. versionadded:: 1.1 333 | 334 | A mixin to allow easy ordering of your queryset basing on the GET parameters. Works with `ListView`. 335 | 336 | To use it, define columns that the data can be ordered by, as well as the default column to order by in your view. This can be done either by simply setting the class attributes: 337 | 338 | :: 339 | 340 | # views.py 341 | from django.views import ListView 342 | 343 | from braces.views import OrderableListMixin 344 | 345 | 346 | class OrderableListView(OrderableListMixin, ListView): 347 | model = Article 348 | orderable_columns = (u"id", u"title",) 349 | orderable_columns_default = u"id" 350 | 351 | Or by using similarly-named methods to set the ordering constraints more dynamically: 352 | 353 | :: 354 | 355 | # views.py 356 | from django.views import ListView 357 | 358 | from braces.views import OrderableListMixin 359 | 360 | 361 | class OrderableListView(OrderableListMixin, ListView): 362 | model = Article 363 | 364 | def get_orderable_columns(self): 365 | # return an iterable 366 | return (u"id", u"title",) 367 | 368 | def get_orderable_columns_default(self): 369 | # return a string 370 | return u"id" 371 | 372 | The ``orderable_columns`` restriction is here in order to stop your users from launching inefficient queries, like ordering by binary columns. 373 | 374 | ``OrderableListMixin`` will order your queryset basing on following GET params: 375 | 376 | * ``order_by``: column name, e.g. ``"title"`` 377 | * ``ordering``: ``"asc"`` (default) or ``"desc"`` 378 | 379 | Example url: `http://127.0.0.1:8000/articles/?order_by=title&ordering=asc` 380 | 381 | 382 | .. _CanonicalSlugDetailMixin: 383 | 384 | CanonicalSlugDetailMixin 385 | ------------------------ 386 | 387 | .. versionadded:: 1.3 388 | 389 | A mixin that enforces a canonical slug in the URL. Works with ``DetailView``. 390 | 391 | If a ``urlpattern`` takes a object's ``pk`` and ``slug`` as arguments and the ``slug`` URL argument does not equal the object's canonical slug, this mixin will redirect to the URL containing the canonical slug. 392 | 393 | To use it, the ``urlpattern`` must accept both a ``pk`` and ``slug`` argument in its regex: 394 | 395 | :: 396 | 397 | # urls.py 398 | urlpatterns = patterns('', 399 | url(r"^article/(?P\d+)-(?P[-\w]+)$") 400 | ArticleView.as_view(), 401 | "view_article" 402 | ) 403 | 404 | Then create a standard ``DetailView`` that inherits this mixin: 405 | 406 | :: 407 | 408 | class ArticleView(CanonicalSlugDetailMixin, DetailView): 409 | model = Article 410 | 411 | Now, given an ``Article`` object with ``{pk: 1, slug: 'hello-world'}``, the URL `http://127.0.0.1:8000/article/1-goodbye-moon` will redirect to `http://127.0.0.1:8000/article/1-hello-world` with the HTTP status code 301 Moved Permanently. Any other non-canonical slug, not just 'goodbye-moon', will trigger the redirect as well. 412 | 413 | Control the canonical slug by either implementing the method ``get_canonical_slug()`` on the model class: 414 | 415 | :: 416 | 417 | class Article(models.Model): 418 | blog = models.ForeignKey('Blog') 419 | slug = models.SlugField() 420 | 421 | def get_canonical_slug(self): 422 | return "{0}-{1}".format(self.blog.get_canonical_slug(), self.slug) 423 | 424 | Or by overriding the ``get_canonical_slug()`` method on the view: 425 | 426 | :: 427 | 428 | class ArticleView(CanonicalSlugDetailMixin, DetailView): 429 | model = Article 430 | 431 | def get_canonical_slug(): 432 | import codecs 433 | return codecs.encode(self.get_object().slug, "rot_13") 434 | 435 | Given the same Article as before, this will generate urls of `http://127.0.0.1:8000/article/1-my-blog-hello-world` and `http://127.0.0.1:8000/article/1-uryyb-jbeyq`, respectively. 436 | 437 | 438 | .. _MessageMixin: 439 | 440 | MessageMixin 441 | ------------ 442 | 443 | .. versionadded:: 1.4 444 | 445 | A mixin that adds a ``messages`` attribute on the view which acts as a wrapper 446 | to ``django.contrib.messages`` and passes the ``request`` object automatically. 447 | 448 | .. warning:: 449 | If you're using Django 1.4, then the ``message`` attribute is only 450 | available after the base view's ``dispatch`` method has been called 451 | (so our second example would not work for instance). 452 | 453 | :: 454 | 455 | from django.views.generic import TemplateView 456 | 457 | from braces.views import MessageMixin 458 | 459 | 460 | class MyView(MessageMixin, TemplateView): 461 | """ 462 | This view will add a debug message which can then be displayed 463 | in the template. 464 | """ 465 | template_name = "my_template.html" 466 | 467 | def get(self, request, *args, **kwargs): 468 | self.messages.debug("This is a debug message.") 469 | return super(MyView, self).get(request, *args, **kwargs) 470 | 471 | 472 | :: 473 | 474 | from django.contrib import messages 475 | from django.views.generic import TemplateView 476 | 477 | from braces.views import MessageMixin 478 | 479 | 480 | class OnlyWarningView(MessageMixin, TemplateView): 481 | """ 482 | This view will only show messages that have a level 483 | above `warning`. 484 | """ 485 | template_name = "my_template.html" 486 | 487 | def dispatch(self, request, *args, **kwargs): 488 | self.messages.set_level(messages.WARNING) 489 | return super(OnlyWarningView, self).dispatch(request, *args, **kwargs) 490 | 491 | 492 | .. _AllVerbsMixin: 493 | 494 | AllVerbsMixin 495 | ------------- 496 | 497 | .. versionadded:: 1.4 498 | 499 | This mixin allows you to specify a single method that will response to all HTTP verbs, making a class-based view behave much like a function-based view. 500 | 501 | :: 502 | 503 | from django.views import TemplateView 504 | 505 | from braces.views import AllVerbsMixin 506 | 507 | 508 | class JustShowItView(AllVerbsMixin, TemplateView): 509 | template_name = "just/show_it.html" 510 | 511 | def all(self, request, *args, **kwargs): 512 | return super(JustShowItView, self).get(request, *args, **kwargs) 513 | 514 | If you need to change the name of the method called, provide a new value to the ``all_handler`` attribute (default is ``'all'``) 515 | 516 | 517 | .. _select_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#select-related 518 | .. _prefetch_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#prefetch-related 519 | -------------------------------------------------------------------------------- /tests/test_access_mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from django import test 5 | from django.test.utils import override_settings 6 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied 7 | from django.core.urlresolvers import reverse_lazy 8 | 9 | from .compat import force_text 10 | from .factories import GroupFactory, UserFactory 11 | from .helpers import TestViewHelper 12 | from .views import (PermissionRequiredView, MultiplePermissionsRequiredView, 13 | SuperuserRequiredView, StaffuserRequiredView, 14 | LoginRequiredView, GroupRequiredView, UserPassesTestView, 15 | UserPassesTestNotImplementedView, AnonymousRequiredView) 16 | 17 | 18 | class _TestAccessBasicsMixin(TestViewHelper): 19 | """ 20 | A set of basic tests for access mixins. 21 | """ 22 | view_url = None 23 | 24 | def build_authorized_user(self): 25 | """ 26 | Returns user authorized to access view. 27 | """ 28 | raise NotImplementedError 29 | 30 | def build_unauthorized_user(self): 31 | """ 32 | Returns user not authorized to access view. 33 | """ 34 | raise NotImplementedError 35 | 36 | def test_success(self): 37 | """ 38 | If user is authorized then view should return normal response. 39 | """ 40 | user = self.build_authorized_user() 41 | self.client.login(username=user.username, password='asdf1234') 42 | resp = self.client.get(self.view_url) 43 | self.assertEqual(200, resp.status_code) 44 | self.assertEqual('OK', force_text(resp.content)) 45 | 46 | def test_redirects_to_login(self): 47 | """ 48 | Browser should be redirected to login page if user is not authorized 49 | to view this page. 50 | """ 51 | user = self.build_unauthorized_user() 52 | self.client.login(username=user.username, password='asdf1234') 53 | resp = self.client.get(self.view_url) 54 | self.assertRedirects(resp, u'/accounts/login/?next={0}'.format( 55 | self.view_url)) 56 | 57 | def test_raise_permission_denied(self): 58 | """ 59 | PermissionDenied should be raised if user is not authorized and 60 | raise_exception attribute is set to True. 61 | """ 62 | user = self.build_unauthorized_user() 63 | req = self.build_request(user=user, path=self.view_url) 64 | 65 | with self.assertRaises(PermissionDenied): 66 | self.dispatch_view(req, raise_exception=True) 67 | 68 | def test_custom_login_url(self): 69 | """ 70 | Login url should be customizable. 71 | """ 72 | user = self.build_unauthorized_user() 73 | req = self.build_request(user=user, path=self.view_url) 74 | resp = self.dispatch_view(req, login_url='/login/') 75 | self.assertEqual( 76 | u'/login/?next={0}'.format(self.view_url), 77 | resp['Location']) 78 | 79 | # Test with reverse_lazy 80 | resp = self.dispatch_view(req, login_url=reverse_lazy('headline')) 81 | self.assertEqual(u'/headline/?next={0}'.format( 82 | self.view_url), resp['Location']) 83 | 84 | def test_custom_redirect_field_name(self): 85 | """ 86 | Redirect field name should be customizable. 87 | """ 88 | user = self.build_unauthorized_user() 89 | req = self.build_request(user=user, path=self.view_url) 90 | resp = self.dispatch_view(req, redirect_field_name='foo') 91 | expected_url = u'/accounts/login/?foo={0}'.format(self.view_url) 92 | self.assertEqual(expected_url, resp['Location']) 93 | 94 | @override_settings(LOGIN_URL=None) 95 | def test_get_login_url_raises_exception(self): 96 | """ 97 | Test that get_login_url from AccessMixin raises 98 | ImproperlyConfigured. 99 | """ 100 | with self.assertRaises(ImproperlyConfigured): 101 | self.dispatch_view( 102 | self.build_request(path=self.view_url), login_url=None) 103 | 104 | def test_get_redirect_field_name_raises_exception(self): 105 | """ 106 | Test that get_redirect_field_name from AccessMixin raises 107 | ImproperlyConfigured. 108 | """ 109 | with self.assertRaises(ImproperlyConfigured): 110 | self.dispatch_view( 111 | self.build_request(path=self.view_url), 112 | redirect_field_name=None) 113 | 114 | @override_settings(LOGIN_URL="/auth/login/") 115 | def test_overridden_login_url(self): 116 | """ 117 | Test that login_url is not set in stone on module load but can be 118 | overridden dynamically. 119 | """ 120 | user = self.build_unauthorized_user() 121 | self.client.login(username=user.username, password='asdf1234') 122 | resp = self.client.get(self.view_url) 123 | self.assertRedirects(resp, u'/auth/login/?next={0}'.format( 124 | self.view_url)) 125 | 126 | 127 | class TestLoginRequiredMixin(TestViewHelper, test.TestCase): 128 | """ 129 | Tests for LoginRequiredMixin. 130 | """ 131 | view_class = LoginRequiredView 132 | view_url = '/login_required/' 133 | 134 | def test_anonymous(self): 135 | resp = self.client.get(self.view_url) 136 | self.assertRedirects(resp, '/accounts/login/?next=/login_required/') 137 | 138 | def test_anonymous_raises_exception(self): 139 | with self.assertRaises(PermissionDenied): 140 | self.dispatch_view( 141 | self.build_request(path=self.view_url), raise_exception=True) 142 | 143 | def test_authenticated(self): 144 | user = UserFactory() 145 | self.client.login(username=user.username, password='asdf1234') 146 | resp = self.client.get(self.view_url) 147 | assert resp.status_code == 200 148 | assert force_text(resp.content) == 'OK' 149 | 150 | def test_anonymous_redirects(self): 151 | resp = self.dispatch_view( 152 | self.build_request(path=self.view_url), 153 | raise_exception=True, 154 | redirect_unauthenticated_users=True) 155 | assert resp.status_code == 302 156 | assert resp['Location'] == '/accounts/login/?next=/login_required/' 157 | 158 | 159 | class TestAnonymousRequiredMixin(TestViewHelper, test.TestCase): 160 | """ 161 | Tests for AnonymousRequiredMixin. 162 | """ 163 | view_class = AnonymousRequiredView 164 | view_url = '/unauthenticated_view/' 165 | 166 | def test_anonymous(self): 167 | """ 168 | As a non-authenticated user, it should be possible to access 169 | the URL. 170 | """ 171 | resp = self.client.get(self.view_url) 172 | self.assertEqual(200, resp.status_code) 173 | self.assertEqual('OK', force_text(resp.content)) 174 | 175 | # Test with reverse_lazy 176 | resp = self.dispatch_view( 177 | self.build_request(), 178 | login_url=reverse_lazy(self.view_url)) 179 | self.assertEqual(200, resp.status_code) 180 | self.assertEqual('OK', force_text(resp.content)) 181 | 182 | def test_authenticated(self): 183 | """ 184 | Check that the authenticated user has been successfully directed 185 | to the approparite view. 186 | """ 187 | user = UserFactory() 188 | self.client.login(username=user.username, password='asdf1234') 189 | resp = self.client.get(self.view_url) 190 | self.assertEqual(302, resp.status_code) 191 | 192 | resp = self.client.get(self.view_url, follow=True) 193 | self.assertRedirects(resp, '/authenticated_view/') 194 | 195 | def test_no_url(self): 196 | self.view_class.authenticated_redirect_url = None 197 | user = UserFactory() 198 | self.client.login(username=user.username, password='asdf1234') 199 | with self.assertRaises(ImproperlyConfigured): 200 | self.client.get(self.view_url) 201 | 202 | def test_bad_url(self): 203 | self.view_class.authenticated_redirect_url = '/epicfailurl/' 204 | user = UserFactory() 205 | self.client.login(username=user.username, password='asdf1234') 206 | resp = self.client.get(self.view_url, follow=True) 207 | self.assertEqual(404, resp.status_code) 208 | 209 | 210 | class TestPermissionRequiredMixin(_TestAccessBasicsMixin, test.TestCase): 211 | """ 212 | Tests for PermissionRequiredMixin. 213 | """ 214 | view_class = PermissionRequiredView 215 | view_url = '/permission_required/' 216 | 217 | def build_authorized_user(self): 218 | return UserFactory(permissions=['auth.add_user']) 219 | 220 | def build_unauthorized_user(self): 221 | return UserFactory() 222 | 223 | def test_invalid_permission(self): 224 | """ 225 | ImproperlyConfigured exception should be raised in two situations: 226 | if permission is None or if permission has invalid name. 227 | """ 228 | with self.assertRaises(ImproperlyConfigured): 229 | self.dispatch_view(self.build_request(), permission_required=None) 230 | 231 | 232 | class TestMultiplePermissionsRequiredMixin( 233 | _TestAccessBasicsMixin, test.TestCase): 234 | view_class = MultiplePermissionsRequiredView 235 | view_url = '/multiple_permissions_required/' 236 | 237 | def build_authorized_user(self): 238 | return UserFactory(permissions=[ 239 | 'tests.add_article', 'tests.change_article', 'auth.change_user']) 240 | 241 | def build_unauthorized_user(self): 242 | return UserFactory(permissions=['tests.add_article']) 243 | 244 | def test_redirects_to_login(self): 245 | """ 246 | User should be redirected to login page if he or she does not have 247 | sufficient permissions. 248 | """ 249 | url = '/multiple_permissions_required/' 250 | test_cases = ( 251 | # missing one permission from 'any' 252 | ['tests.add_article', 'tests.change_article'], 253 | # missing one permission from 'all' 254 | ['tests.add_article', 'auth.add_user'], 255 | # no permissions at all 256 | [], 257 | ) 258 | 259 | for permissions in test_cases: 260 | user = UserFactory(permissions=permissions) 261 | self.client.login(username=user.username, password='asdf1234') 262 | resp = self.client.get(url) 263 | self.assertRedirects(resp, u'/accounts/login/?next={0}'.format( 264 | url)) 265 | 266 | def test_invalid_permissions(self): 267 | """ 268 | ImproperlyConfigured exception should be raised if permissions 269 | attribute is set incorrectly. 270 | """ 271 | permissions = ( 272 | None, # permissions must be set 273 | (), # and they must be a dict 274 | {}, # at least one of 'all', 'any' keys must be present 275 | {'all': None}, # both all and any must be list or a tuple 276 | {'all': {'a': 1}}, 277 | {'any': None}, 278 | {'any': {'a': 1}}, 279 | ) 280 | 281 | for attr in permissions: 282 | with self.assertRaises(ImproperlyConfigured): 283 | self.dispatch_view(self.build_request(), permissions=attr) 284 | 285 | def test_raise_permission_denied(self): 286 | """ 287 | PermissionDenied should be raised if user does not have sufficient 288 | permissions and raise_exception is set to True. 289 | """ 290 | test_cases = ( 291 | # missing one permission from 'any' 292 | ['tests.add_article', 'tests.change_article'], 293 | # missing one permission from 'all' 294 | ['tests.add_article', 'auth.add_user'], 295 | # no permissions at all 296 | [], 297 | ) 298 | 299 | for permissions in test_cases: 300 | user = UserFactory(permissions=permissions) 301 | req = self.build_request(user=user) 302 | with self.assertRaises(PermissionDenied): 303 | self.dispatch_view(req, raise_exception=True) 304 | 305 | def test_all_permissions_key(self): 306 | """ 307 | Tests if everything works if only 'all' permissions has been set. 308 | """ 309 | permissions = {'all': ['auth.add_user', 'tests.add_article']} 310 | user = UserFactory(permissions=permissions['all']) 311 | req = self.build_request(user=user) 312 | 313 | resp = self.dispatch_view(req, permissions=permissions) 314 | self.assertEqual('OK', force_text(resp.content)) 315 | 316 | user = UserFactory(permissions=['auth.add_user']) 317 | with self.assertRaises(PermissionDenied): 318 | self.dispatch_view( 319 | self.build_request(user=user), raise_exception=True, 320 | permissions=permissions) 321 | 322 | def test_any_permissions_key(self): 323 | """ 324 | Tests if everything works if only 'any' permissions has been set. 325 | """ 326 | permissions = {'any': ['auth.add_user', 'tests.add_article']} 327 | user = UserFactory(permissions=['tests.add_article']) 328 | req = self.build_request(user=user) 329 | 330 | resp = self.dispatch_view(req, permissions=permissions) 331 | self.assertEqual('OK', force_text(resp.content)) 332 | 333 | user = UserFactory(permissions=[]) 334 | with self.assertRaises(PermissionDenied): 335 | self.dispatch_view( 336 | self.build_request(user=user), raise_exception=True, 337 | permissions=permissions) 338 | 339 | 340 | class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): 341 | view_class = SuperuserRequiredView 342 | view_url = '/superuser_required/' 343 | 344 | def build_authorized_user(self): 345 | return UserFactory(is_superuser=True, is_staff=True) 346 | 347 | def build_unauthorized_user(self): 348 | return UserFactory() 349 | 350 | 351 | class TestStaffuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): 352 | view_class = StaffuserRequiredView 353 | view_url = '/staffuser_required/' 354 | 355 | def build_authorized_user(self): 356 | return UserFactory(is_staff=True) 357 | 358 | def build_unauthorized_user(self): 359 | return UserFactory() 360 | 361 | 362 | class TestGroupRequiredMixin(_TestAccessBasicsMixin, test.TestCase): 363 | view_class = GroupRequiredView 364 | view_url = '/group_required/' 365 | 366 | def build_authorized_user(self): 367 | user = UserFactory() 368 | group = GroupFactory(name='test_group') 369 | user.groups.add(group) 370 | return user 371 | 372 | def build_superuser(self): 373 | user = UserFactory() 374 | user.is_superuser = True 375 | user.save() 376 | return user 377 | 378 | def build_unauthorized_user(self): 379 | return UserFactory() 380 | 381 | def test_with_string(self): 382 | self.assertEqual('test_group', self.view_class.group_required) 383 | user = self.build_authorized_user() 384 | self.client.login(username=user.username, password='asdf1234') 385 | resp = self.client.get(self.view_url) 386 | self.assertEqual(200, resp.status_code) 387 | self.assertEqual('OK', force_text(resp.content)) 388 | 389 | def test_with_group_list(self): 390 | group_list = ['test_group', 'editors'] 391 | # the test client will instantiate a new view on request, so we have to 392 | # modify the class variable (and restore it when the test finished) 393 | self.view_class.group_required = group_list 394 | self.assertEqual(group_list, self.view_class.group_required) 395 | user = self.build_authorized_user() 396 | self.client.login(username=user.username, password='asdf1234') 397 | resp = self.client.get(self.view_url) 398 | self.assertEqual(200, resp.status_code) 399 | self.assertEqual('OK', force_text(resp.content)) 400 | self.view_class.group_required = 'test_group' 401 | self.assertEqual('test_group', self.view_class.group_required) 402 | 403 | def test_superuser_allowed(self): 404 | user = self.build_superuser() 405 | self.client.login(username=user.username, password='asdf1234') 406 | resp = self.client.get(self.view_url) 407 | self.assertEqual(200, resp.status_code) 408 | self.assertEqual('OK', force_text(resp.content)) 409 | 410 | def test_improperly_configured(self): 411 | view = self.view_class() 412 | view.group_required = None 413 | with self.assertRaises(ImproperlyConfigured): 414 | view.get_group_required() 415 | 416 | view.group_required = {'foo': 'bar'} 417 | with self.assertRaises(ImproperlyConfigured): 418 | view.get_group_required() 419 | 420 | def test_with_unicode(self): 421 | self.view_class.group_required = u'niño' 422 | self.assertEqual(u'niño', self.view_class.group_required) 423 | 424 | user = self.build_authorized_user() 425 | group = user.groups.all()[0] 426 | group.name = u'niño' 427 | group.save() 428 | self.assertEqual(u'niño', user.groups.all()[0].name) 429 | 430 | self.client.login(username=user.username, password='asdf1234') 431 | resp = self.client.get(self.view_url) 432 | self.assertEqual(200, resp.status_code) 433 | self.assertEqual('OK', force_text(resp.content)) 434 | self.view_class.group_required = 'test_group' 435 | self.assertEqual('test_group', self.view_class.group_required) 436 | 437 | 438 | class TestUserPassesTestMixin(_TestAccessBasicsMixin, test.TestCase): 439 | view_class = UserPassesTestView 440 | view_url = '/user_passes_test/' 441 | view_not_implemented_class = UserPassesTestNotImplementedView 442 | view_not_implemented_url = '/user_passes_test_not_implemented/' 443 | 444 | # for testing with passing and not passsing func_test 445 | def build_authorized_user(self, is_superuser=False): 446 | return UserFactory(is_superuser=is_superuser, is_staff=True, 447 | email="user@mydomain.com") 448 | 449 | def build_unauthorized_user(self): 450 | return UserFactory() 451 | 452 | def test_with_user_pass(self): 453 | user = self.build_authorized_user() 454 | self.client.login(username=user.username, password='asdf1234') 455 | resp = self.client.get(self.view_url) 456 | 457 | self.assertEqual(200, resp.status_code) 458 | self.assertEqual('OK', force_text(resp.content)) 459 | 460 | def test_with_user_not_pass(self): 461 | user = self.build_authorized_user(is_superuser=True) 462 | self.client.login(username=user.username, password='asdf1234') 463 | resp = self.client.get(self.view_url) 464 | 465 | self.assertRedirects(resp, '/accounts/login/?next=/user_passes_test/') 466 | 467 | def test_with_user_raise_exception(self): 468 | with self.assertRaises(PermissionDenied): 469 | self.dispatch_view( 470 | self.build_request(path=self.view_url), raise_exception=True) 471 | 472 | def test_not_implemented(self): 473 | view = self.view_not_implemented_class() 474 | with self.assertRaises(NotImplementedError): 475 | view.dispatch( 476 | self.build_request(path=self.view_not_implemented_url), 477 | raise_exception=True) 478 | -------------------------------------------------------------------------------- /tests/test_other_mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import mock 5 | import pytest 6 | 7 | import django 8 | from django.contrib import messages 9 | from django.contrib.messages.middleware import MessageMiddleware 10 | from django.contrib.messages.storage.base import Message 11 | from django.core.exceptions import ImproperlyConfigured 12 | from django.http import HttpResponse 13 | from django import test 14 | from django.test.utils import override_settings 15 | from django.views.generic import View 16 | 17 | from braces.views import (SetHeadlineMixin, MessageMixin, _MessageAPIWrapper, 18 | FormValidMessageMixin, FormInvalidMessageMixin) 19 | from .compat import force_text 20 | from .factories import UserFactory 21 | from .helpers import TestViewHelper 22 | from .models import Article, CanonicalArticle 23 | from .views import (ArticleListView, ArticleListViewWithCustomQueryset, 24 | AuthorDetailView, OrderableListView, 25 | FormMessagesView, ContextView) 26 | 27 | 28 | class TestSuccessURLRedirectListMixin(test.TestCase): 29 | """ 30 | Tests for SuccessURLRedirectListMixin. 31 | """ 32 | def test_redirect(self): 33 | """ 34 | Test if browser is redirected to list view. 35 | """ 36 | data = {'title': "Test body", 'body': "Test body"} 37 | resp = self.client.post('/article_list/create/', data) 38 | self.assertRedirects(resp, '/article_list/') 39 | 40 | def test_no_url_name(self): 41 | """ 42 | Test that ImproperlyConfigured is raised. 43 | """ 44 | data = {'title': "Test body", 'body': "Test body"} 45 | with self.assertRaises(ImproperlyConfigured): 46 | self.client.post('/article_list_bad/create/', data) 47 | 48 | 49 | class TestUserFormKwargsMixin(test.TestCase): 50 | """ 51 | Tests for UserFormKwargsMixin. 52 | """ 53 | def test_post_method(self): 54 | user = UserFactory() 55 | self.client.login(username=user.username, password='asdf1234') 56 | resp = self.client.post('/form_with_user_kwarg/', {'field1': 'foo'}) 57 | assert force_text(resp.content) == "username: %s" % user.username 58 | 59 | def test_get_method(self): 60 | user = UserFactory() 61 | self.client.login(username=user.username, password='asdf1234') 62 | resp = self.client.get('/form_with_user_kwarg/') 63 | assert resp.context['form'].user == user 64 | 65 | 66 | class TestSetHeadlineMixin(test.TestCase): 67 | """ 68 | Tests for SetHeadlineMixin. 69 | """ 70 | def test_dynamic_headline(self): 71 | """ 72 | Tests if get_headline() is called properly. 73 | """ 74 | resp = self.client.get('/headline/test-headline/') 75 | self.assertEqual('test-headline', resp.context['headline']) 76 | 77 | def test_context_data(self): 78 | """ 79 | Tests if mixin adds proper headline to template context. 80 | """ 81 | resp = self.client.get('/headline/foo-bar/') 82 | self.assertEqual("foo-bar", resp.context['headline']) 83 | 84 | def test_get_headline(self): 85 | """ 86 | Tests if get_headline() method works correctly. 87 | """ 88 | mixin = SetHeadlineMixin() 89 | with self.assertRaises(ImproperlyConfigured): 90 | mixin.get_headline() 91 | 92 | mixin.headline = "Test headline" 93 | self.assertEqual("Test headline", mixin.get_headline()) 94 | 95 | def test_get_headline_lazy(self): 96 | resp = self.client.get('/headline/lazy/') 97 | self.assertEqual('Test Headline', resp.context['headline']) 98 | 99 | 100 | class TestStaticContextMixin(test.TestCase): 101 | """ Tests for StaticContextMixin. """ 102 | view_class = ContextView 103 | view_url = '/context/' 104 | 105 | def test_dict(self): 106 | self.view_class.static_context = {'test': True} 107 | resp = self.client.get(self.view_url) 108 | self.assertEqual(200, resp.status_code) 109 | self.assertEqual(True, resp.context['test']) 110 | 111 | def test_two_tuple(self): 112 | self.view_class.static_context = [('a', 1), ('b', 2)] 113 | resp = self.client.get(self.view_url) 114 | self.assertEqual(200, resp.status_code) 115 | self.assertEqual(1, resp.context['a']) 116 | self.assertEqual(2, resp.context['b']) 117 | 118 | def test_not_set(self): 119 | self.view_class.static_context = None 120 | with self.assertRaises(ImproperlyConfigured): 121 | self.client.get(self.view_url) 122 | 123 | def test_string_value_error(self): 124 | self.view_class.static_context = 'Fail' 125 | with self.assertRaises(ImproperlyConfigured): 126 | self.client.get(self.view_url) 127 | 128 | def test_list_error(self): 129 | self.view_class.static_context = ['fail', 'fail'] 130 | with self.assertRaises(ImproperlyConfigured): 131 | self.client.get(self.view_url) 132 | 133 | 134 | class TestCsrfExemptMixin(test.TestCase): 135 | """ 136 | Tests for TestCsrfExemptMixin. 137 | """ 138 | def setUp(self): 139 | super(TestCsrfExemptMixin, self).setUp() 140 | self.client = self.client_class(enforce_csrf_checks=True) 141 | 142 | def test_csrf_token_is_not_required(self): 143 | """ 144 | Tests if csrf token is not required. 145 | """ 146 | resp = self.client.post('/csrf_exempt/', {'field1': 'test'}) 147 | self.assertEqual(200, resp.status_code) 148 | self.assertEqual("OK", force_text(resp.content)) 149 | 150 | 151 | class TestSelectRelatedMixin(TestViewHelper, test.TestCase): 152 | view_class = ArticleListView 153 | 154 | def test_missing_select_related(self): 155 | """ 156 | ImproperlyConfigured exception should be raised if select_related 157 | attribute is missing. 158 | """ 159 | with self.assertRaises(ImproperlyConfigured): 160 | self.dispatch_view(self.build_request(), select_related=None) 161 | 162 | def test_invalid_select_related(self): 163 | """ 164 | ImproperlyConfigured exception should be raised if select_related is 165 | not a tuple or a list. 166 | :return: 167 | """ 168 | with self.assertRaises(ImproperlyConfigured): 169 | self.dispatch_view(self.build_request(), select_related={'a': 1}) 170 | 171 | @mock.patch('django.db.models.query.QuerySet.select_related') 172 | def test_select_related_called(self, m): 173 | """ 174 | Checks if QuerySet's select_related() was called with correct 175 | arguments. 176 | """ 177 | qs = Article.objects.all() 178 | m.return_value = qs.select_related('author') 179 | qs.select_related = m 180 | m.reset_mock() 181 | 182 | resp = self.dispatch_view(self.build_request()) 183 | self.assertEqual(200, resp.status_code) 184 | m.assert_called_once_with('author') 185 | 186 | @mock.patch('django.db.models.query.QuerySet.select_related') 187 | def test_select_related_keeps_select_related_from_queryset(self, m): 188 | """ 189 | Checks that an empty select_related attribute does not 190 | cancel a select_related provided by queryset. 191 | """ 192 | qs = Article.objects.all() 193 | qs.select_related = m 194 | m.reset_mock() 195 | 196 | resp = self.dispatch_view( 197 | self.build_request(), 198 | view_class=ArticleListViewWithCustomQueryset) 199 | self.assertEqual(200, resp.status_code) 200 | self.assertEqual(0, m.call_count) 201 | 202 | 203 | class TestPrefetchRelatedMixin(TestViewHelper, test.TestCase): 204 | view_class = AuthorDetailView 205 | 206 | def test_missing_prefetch_related(self): 207 | """ 208 | ImproperlyConfigured exception should be raised if 209 | prefetch_related attribute is missing. 210 | """ 211 | with self.assertRaises(ImproperlyConfigured): 212 | self.dispatch_view(self.build_request(), prefetch_related=None) 213 | 214 | def test_invalid_prefetch_related(self): 215 | """ 216 | ImproperlyConfigured exception should be raised if 217 | prefetch_related is not a tuple or a list. 218 | :return: 219 | """ 220 | with self.assertRaises(ImproperlyConfigured): 221 | self.dispatch_view(self.build_request(), prefetch_related={'a': 1}) 222 | 223 | @mock.patch('django.db.models.query.QuerySet.prefetch_related') 224 | def test_prefetch_related_called(self, m): 225 | """ 226 | Checks if QuerySet's prefetch_related() was called with correct 227 | arguments. 228 | """ 229 | qs = Article.objects.all() 230 | m.return_value = qs.prefetch_related('article_set') 231 | qs.prefetch_related = m 232 | m.reset_mock() 233 | 234 | resp = self.dispatch_view(self.build_request()) 235 | self.assertEqual(200, resp.status_code) 236 | m.assert_called_once_with('article_set') 237 | 238 | @mock.patch('django.db.models.query.QuerySet.prefetch_related') 239 | def test_prefetch_related_keeps_select_related_from_queryset(self, m): 240 | """ 241 | Checks that an empty prefetch_related attribute does not 242 | cancel a prefetch_related provided by queryset. 243 | """ 244 | qs = Article.objects.all() 245 | qs.prefetch_related = m 246 | m.reset_mock() 247 | 248 | resp = self.dispatch_view( 249 | self.build_request(), 250 | view_class=ArticleListViewWithCustomQueryset) 251 | self.assertEqual(200, resp.status_code) 252 | self.assertEqual(0, m.call_count) 253 | 254 | 255 | class TestOrderableListMixin(TestViewHelper, test.TestCase): 256 | view_class = OrderableListView 257 | 258 | def __make_test_articles(self): 259 | a1 = Article.objects.create(title='Alpha', body='Zet') 260 | a2 = Article.objects.create(title='Zet', body='Alpha') 261 | return a1, a2 262 | 263 | def test_correct_order(self): 264 | """ 265 | Objects must be properly ordered if requested with valid column names 266 | """ 267 | a1, a2 = self.__make_test_articles() 268 | 269 | resp = self.dispatch_view( 270 | self.build_request(path='?order_by=title&ordering=asc'), 271 | orderable_columns=None, 272 | get_orderable_columns=lambda: ('id', 'title', )) 273 | self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) 274 | 275 | resp = self.dispatch_view( 276 | self.build_request(path='?order_by=id&ordering=desc'), 277 | orderable_columns=None, 278 | get_orderable_columns=lambda: ('id', 'title', )) 279 | self.assertEqual(list(resp.context_data['object_list']), [a2, a1]) 280 | 281 | def test_default_column(self): 282 | """ 283 | When no ordering specified in GET, use 284 | View.get_orderable_columns_default() 285 | """ 286 | a1, a2 = self.__make_test_articles() 287 | 288 | resp = self.dispatch_view(self.build_request()) 289 | self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) 290 | 291 | def test_get_orderable_columns_returns_correct_values(self): 292 | """ 293 | OrderableListMixin.get_orderable_columns() should return 294 | View.orderable_columns attribute by default or raise 295 | ImproperlyConfigured exception in the attribute is None 296 | """ 297 | view = self.view_class() 298 | self.assertEqual(view.get_orderable_columns(), view.orderable_columns) 299 | view.orderable_columns = None 300 | self.assertRaises(ImproperlyConfigured, 301 | lambda: view.get_orderable_columns()) 302 | 303 | def test_get_orderable_columns_default_returns_correct_values(self): 304 | """ 305 | OrderableListMixin.get_orderable_columns_default() should return 306 | View.orderable_columns_default attribute by default or raise 307 | ImproperlyConfigured exception in the attribute is None 308 | """ 309 | view = self.view_class() 310 | self.assertEqual(view.get_orderable_columns_default(), 311 | view.orderable_columns_default) 312 | view.orderable_columns_default = None 313 | self.assertRaises(ImproperlyConfigured, 314 | lambda: view.get_orderable_columns_default()) 315 | 316 | def test_only_allowed_columns(self): 317 | """ 318 | If column is not in Model.Orderable.columns iterable, the objects 319 | should be ordered by default column. 320 | """ 321 | a1, a2 = self.__make_test_articles() 322 | 323 | resp = self.dispatch_view( 324 | self.build_request(path='?order_by=body&ordering=asc'), 325 | orderable_columns_default=None, 326 | get_orderable_columns_default=lambda: 'title') 327 | self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) 328 | 329 | 330 | class TestCanonicalSlugDetailView(test.TestCase): 331 | def setUp(self): 332 | Article.objects.create(title='Alpha', body='Zet', slug='alpha') 333 | Article.objects.create(title='Zet', body='Alpha', slug='zet') 334 | 335 | def test_canonical_slug(self): 336 | """ 337 | Test that no redirect occurs when slug is canonical. 338 | """ 339 | resp = self.client.get('/article-canonical/1-alpha/') 340 | self.assertEqual(resp.status_code, 200) 341 | resp = self.client.get('/article-canonical/2-zet/') 342 | self.assertEqual(resp.status_code, 200) 343 | 344 | def test_non_canonical_slug(self): 345 | """ 346 | Test that a redirect occurs when the slug is non-canonical. 347 | """ 348 | resp = self.client.get('/article-canonical/1-bad-slug/') 349 | self.assertEqual(resp.status_code, 301) 350 | resp = self.client.get('/article-canonical/2-bad-slug/') 351 | self.assertEqual(resp.status_code, 301) 352 | 353 | 354 | class TestNamespaceAwareCanonicalSlugDetailView(test.TestCase): 355 | def setUp(self): 356 | Article.objects.create(title='Alpha', body='Zet', slug='alpha') 357 | Article.objects.create(title='Zet', body='Alpha', slug='zet') 358 | 359 | def test_canonical_slug(self): 360 | """ 361 | Test that no redirect occurs when slug is canonical. 362 | """ 363 | resp = self.client.get( 364 | '/article-canonical-namespaced/article/1-alpha/') 365 | self.assertEqual(resp.status_code, 200) 366 | resp = self.client.get( 367 | '/article-canonical-namespaced/article/2-zet/') 368 | self.assertEqual(resp.status_code, 200) 369 | 370 | def test_non_canonical_slug(self): 371 | """ 372 | Test that a redirect occurs when the slug is non-canonical and that the 373 | redirect is namespace aware. 374 | """ 375 | resp = self.client.get( 376 | '/article-canonical-namespaced/article/1-bad-slug/') 377 | self.assertEqual(resp.status_code, 301) 378 | resp = self.client.get( 379 | '/article-canonical-namespaced/article/2-bad-slug/') 380 | self.assertEqual(resp.status_code, 301) 381 | 382 | 383 | class TestOverriddenCanonicalSlugDetailView(test.TestCase): 384 | def setUp(self): 385 | Article.objects.create(title='Alpha', body='Zet', slug='alpha') 386 | Article.objects.create(title='Zet', body='Alpha', slug='zet') 387 | 388 | def test_canonical_slug(self): 389 | """ 390 | Test that no redirect occurs when slug is canonical according to the 391 | overridden canonical slug. 392 | """ 393 | resp = self.client.get('/article-canonical-override/1-nycun/') 394 | self.assertEqual(resp.status_code, 200) 395 | resp = self.client.get('/article-canonical-override/2-mrg/') 396 | self.assertEqual(resp.status_code, 200) 397 | 398 | def test_non_canonical_slug(self): 399 | """ 400 | Test that a redirect occurs when the slug is non-canonical. 401 | """ 402 | resp = self.client.get('/article-canonical-override/1-bad-slug/') 403 | self.assertEqual(resp.status_code, 301) 404 | resp = self.client.get('/article-canonical-override/2-bad-slug/') 405 | self.assertEqual(resp.status_code, 301) 406 | 407 | 408 | class TestCustomUrlKwargsCanonicalSlugDetailView(test.TestCase): 409 | def setUp(self): 410 | Article.objects.create(title='Alpha', body='Zet', slug='alpha') 411 | Article.objects.create(title='Zet', body='Alpha', slug='zet') 412 | 413 | def test_canonical_slug(self): 414 | """ 415 | Test that no redirect occurs when slug is canonical 416 | """ 417 | resp = self.client.get('/article-canonical-custom-kwargs/1-alpha/') 418 | self.assertEqual(resp.status_code, 200) 419 | resp = self.client.get('/article-canonical-custom-kwargs/2-zet/') 420 | self.assertEqual(resp.status_code, 200) 421 | 422 | def test_non_canonical_slug(self): 423 | """ 424 | Test that a redirect occurs when the slug is non-canonical. 425 | """ 426 | resp = self.client.get('/article-canonical-custom-kwargs/1-bad-slug/') 427 | self.assertEqual(resp.status_code, 301) 428 | resp = self.client.get('/article-canonical-custom-kwargs/2-bad-slug/') 429 | self.assertEqual(resp.status_code, 301) 430 | 431 | 432 | class TestModelCanonicalSlugDetailView(test.TestCase): 433 | def setUp(self): 434 | CanonicalArticle.objects.create( 435 | title='Alpha', body='Zet', slug='alpha') 436 | CanonicalArticle.objects.create( 437 | title='Zet', body='Alpha', slug='zet') 438 | 439 | def test_canonical_slug(self): 440 | """ 441 | Test that no redirect occurs when slug is canonical according to the 442 | model's canonical slug. 443 | """ 444 | resp = self.client.get('/article-canonical-model/1-unauthored-alpha/') 445 | self.assertEqual(resp.status_code, 200) 446 | resp = self.client.get('/article-canonical-model/2-unauthored-zet/') 447 | self.assertEqual(resp.status_code, 200) 448 | 449 | def test_non_canonical_slug(self): 450 | """ 451 | Test that a redirect occurs when the slug is non-canonical. 452 | """ 453 | resp = self.client.get('/article-canonical-model/1-bad-slug/') 454 | self.assertEqual(resp.status_code, 301) 455 | resp = self.client.get('/article-canonical-model/2-bad-slug/') 456 | self.assertEqual(resp.status_code, 301) 457 | 458 | 459 | # CookieStorage is used because it doesn't require middleware to be installed 460 | @override_settings( 461 | MESSAGE_STORAGE='django.contrib.messages.storage.cookie.CookieStorage') 462 | class MessageMixinTests(test.TestCase): 463 | def setUp(self): 464 | self.rf = test.RequestFactory() 465 | self.middleware = MessageMiddleware() 466 | 467 | def get_request(self, *args, **kwargs): 468 | request = self.rf.get('/') 469 | self.middleware.process_request(request) 470 | return request 471 | 472 | def get_response(self, request, view): 473 | response = view(request) 474 | self.middleware.process_response(request, response) 475 | return response 476 | 477 | def get_request_response(self, view, *args, **kwargs): 478 | request = self.get_request(*args, **kwargs) 479 | response = self.get_response(request, view) 480 | return request, response 481 | 482 | def test_add_messages(self): 483 | class TestView(MessageMixin, View): 484 | def get(self, request): 485 | self.messages.add_message(messages.SUCCESS, 'test') 486 | return HttpResponse('OK') 487 | 488 | request, response = self.get_request_response(TestView.as_view()) 489 | msg = list(request._messages) 490 | self.assertEqual(len(msg), 1) 491 | self.assertEqual(msg[0].message, 'test') 492 | self.assertEqual(msg[0].level, messages.SUCCESS) 493 | 494 | def test_get_messages(self): 495 | class TestView(MessageMixin, View): 496 | def get(self, request): 497 | self.messages.add_message(messages.SUCCESS, 'success') 498 | self.messages.add_message(messages.WARNING, 'warning') 499 | content = ','.join( 500 | m.message for m in self.messages.get_messages()) 501 | return HttpResponse(content) 502 | 503 | _, response = self.get_request_response(TestView.as_view()) 504 | self.assertEqual(response.content, b"success,warning") 505 | 506 | def test_get_level(self): 507 | class TestView(MessageMixin, View): 508 | def get(self, request): 509 | return HttpResponse(self.messages.get_level()) 510 | 511 | _, response = self.get_request_response(TestView.as_view()) 512 | self.assertEqual(int(response.content), messages.INFO) # default 513 | 514 | def test_set_level(self): 515 | class TestView(MessageMixin, View): 516 | def get(self, request): 517 | self.messages.set_level(messages.WARNING) 518 | self.messages.add_message(messages.SUCCESS, 'success') 519 | self.messages.add_message(messages.WARNING, 'warning') 520 | return HttpResponse('OK') 521 | 522 | request, _ = self.get_request_response(TestView.as_view()) 523 | msg = list(request._messages) 524 | self.assertEqual(msg, [Message(messages.WARNING, 'warning')]) 525 | 526 | @override_settings(MESSAGE_LEVEL=messages.DEBUG) 527 | def test_debug(self): 528 | class TestView(MessageMixin, View): 529 | def get(self, request): 530 | self.messages.debug("test") 531 | return HttpResponse('OK') 532 | 533 | request, _ = self.get_request_response(TestView.as_view()) 534 | msg = list(request._messages) 535 | self.assertEqual(len(msg), 1) 536 | self.assertEqual(msg[0], Message(messages.DEBUG, 'test')) 537 | 538 | def test_info(self): 539 | class TestView(MessageMixin, View): 540 | def get(self, request): 541 | self.messages.info("test") 542 | return HttpResponse('OK') 543 | 544 | request, _ = self.get_request_response(TestView.as_view()) 545 | msg = list(request._messages) 546 | self.assertEqual(len(msg), 1) 547 | self.assertEqual(msg[0], Message(messages.INFO, 'test')) 548 | 549 | def test_success(self): 550 | class TestView(MessageMixin, View): 551 | def get(self, request): 552 | self.messages.success("test") 553 | return HttpResponse('OK') 554 | 555 | request, _ = self.get_request_response(TestView.as_view()) 556 | msg = list(request._messages) 557 | self.assertEqual(len(msg), 1) 558 | self.assertEqual(msg[0], Message(messages.SUCCESS, 'test')) 559 | 560 | def test_warning(self): 561 | class TestView(MessageMixin, View): 562 | def get(self, request): 563 | self.messages.warning("test") 564 | return HttpResponse('OK') 565 | 566 | request, _ = self.get_request_response(TestView.as_view()) 567 | msg = list(request._messages) 568 | self.assertEqual(len(msg), 1) 569 | self.assertEqual(msg[0], Message(messages.WARNING, 'test')) 570 | 571 | def test_error(self): 572 | class TestView(MessageMixin, View): 573 | def get(self, request): 574 | self.messages.error("test") 575 | return HttpResponse('OK') 576 | 577 | request, _ = self.get_request_response(TestView.as_view()) 578 | msg = list(request._messages) 579 | self.assertEqual(len(msg), 1) 580 | self.assertEqual(msg[0], Message(messages.ERROR, 'test')) 581 | 582 | def test_invalid_attribute(self): 583 | class TestView(MessageMixin, View): 584 | def get(self, request): 585 | self.messages.invalid() 586 | return HttpResponse('OK') 587 | 588 | with self.assertRaises(AttributeError): 589 | self.get_request_response(TestView.as_view()) 590 | 591 | @pytest.mark.skipif( 592 | django.VERSION < (1, 5), 593 | reason='Some features of MessageMixin are only available in ' 594 | 'Django >= 1.5') 595 | def test_wrapper_available_in_dispatch(self): 596 | """ 597 | Make sure that self.messages is available in dispatch() even before 598 | calling the parent's implementation. 599 | """ 600 | class TestView(MessageMixin, View): 601 | def dispatch(self, request): 602 | self.messages.add_message(messages.SUCCESS, 'test') 603 | return super(TestView, self).dispatch(request) 604 | 605 | def get(self, request): 606 | return HttpResponse('OK') 607 | 608 | request, response = self.get_request_response(TestView.as_view()) 609 | msg = list(request._messages) 610 | self.assertEqual(len(msg), 1) 611 | self.assertEqual(msg[0].message, 'test') 612 | self.assertEqual(msg[0].level, messages.SUCCESS) 613 | 614 | def test_API(self): 615 | """ 616 | Make sure that our assumptions about messages.api are still valid. 617 | """ 618 | # This test is designed to break when django.contrib.messages.api 619 | # changes (items being added or removed). 620 | excluded_API = set() 621 | if django.VERSION >= (1, 7): 622 | excluded_API.add('MessageFailure') 623 | self.assertEqual( 624 | _MessageAPIWrapper.API | excluded_API, 625 | set(messages.api.__all__) 626 | ) 627 | 628 | 629 | class TestFormMessageMixins(test.TestCase): 630 | def setUp(self): 631 | self.good_data = { 632 | 'title': 'Good', 633 | 'body': 'Body' 634 | } 635 | self.bad_data = { 636 | 'body': 'Missing title' 637 | } 638 | 639 | def test_valid_message(self): 640 | url = '/form_messages/' 641 | response = self.client.get(url) 642 | self.assertEqual(response.status_code, 200) 643 | 644 | response = self.client.post(url, self.good_data, follow=True) 645 | self.assertEqual(response.status_code, 200) 646 | self.assertContains(response, FormMessagesView().form_valid_message) 647 | 648 | def test_invalid_message(self): 649 | url = '/form_messages/' 650 | response = self.client.get(url) 651 | self.assertEqual(response.status_code, 200) 652 | 653 | response = self.client.post(url, self.bad_data, follow=True) 654 | self.assertEqual(response.status_code, 200) 655 | self.assertContains(response, FormMessagesView().form_invalid_message) 656 | 657 | def test_form_valid_message_not_set(self): 658 | mixin = FormValidMessageMixin() 659 | with self.assertRaises(ImproperlyConfigured): 660 | mixin.get_form_valid_message() 661 | 662 | def test_form_valid_message_not_str(self): 663 | mixin = FormValidMessageMixin() 664 | mixin.form_valid_message = ['bad'] 665 | with self.assertRaises(ImproperlyConfigured): 666 | mixin.get_form_valid_message() 667 | 668 | def test_form_valid_returns_message(self): 669 | mixin = FormValidMessageMixin() 670 | mixin.form_valid_message = u'Good øø' 671 | self.assertEqual(u'Good øø', mixin.get_form_valid_message()) 672 | 673 | def test_form_invalid_message_not_set(self): 674 | mixin = FormInvalidMessageMixin() 675 | with self.assertRaises(ImproperlyConfigured): 676 | mixin.get_form_invalid_message() 677 | 678 | def test_form_invalid_message_not_str(self): 679 | mixin = FormInvalidMessageMixin() 680 | mixin.form_invalid_message = ['bad'] 681 | with self.assertRaises(ImproperlyConfigured): 682 | mixin.get_form_invalid_message() 683 | 684 | def test_form_invalid_returns_message(self): 685 | mixin = FormInvalidMessageMixin() 686 | mixin.form_invalid_message = u'Bad øø' 687 | self.assertEqual(u'Bad øø', mixin.get_form_invalid_message()) 688 | 689 | 690 | class TestAllVerbsMixin(test.TestCase): 691 | def setUp(self): 692 | self.url = "/all_verbs/" 693 | self.no_handler_url = "/all_verbs_no_handler/" 694 | 695 | def test_options(self): 696 | response = self.client.options(self.url) 697 | self.assertEqual(response.status_code, 200) 698 | 699 | def test_get(self): 700 | response = self.client.get(self.url) 701 | self.assertEqual(response.status_code, 200) 702 | 703 | def test_head(self): 704 | response = self.client.head(self.url) 705 | self.assertEqual(response.status_code, 200) 706 | 707 | def test_post(self): 708 | response = self.client.post(self.url) 709 | self.assertEqual(response.status_code, 200) 710 | 711 | def test_put(self): 712 | response = self.client.put(self.url) 713 | self.assertEqual(response.status_code, 200) 714 | 715 | def test_delete(self): 716 | response = self.client.delete(self.url) 717 | self.assertEqual(response.status_code, 200) 718 | 719 | def test_no_all_handler(self): 720 | with self.assertRaises(ImproperlyConfigured): 721 | self.client.get('/all_verbs_no_handler/') 722 | --------------------------------------------------------------------------------