pax_global_header00006660000000000000000000000064122621164210014507gustar00rootroot0000000000000052 comment=421512620c91050a9c49cc2c6b2203c85b674ed1 django-braces-1.3.1/000077500000000000000000000000001226211642100142105ustar00rootroot00000000000000django-braces-1.3.1/.coveragerc000066400000000000000000000000241226211642100163250ustar00rootroot00000000000000[run] branch = true django-braces-1.3.1/.gitignore000066400000000000000000000001511226211642100161750ustar00rootroot00000000000000.DS_Store ._* *.pyc *.egg-info /docs/_build/ /.coverage /.coverage.xml /htmlcov /.tox dist/ .idea build/ django-braces-1.3.1/.travis.yml000066400000000000000000000006171226211642100163250ustar00rootroot00000000000000language: python services: sqlite env: - DJANGO_VERSION=1.4.10 - DJANGO_VERSION=1.5.6 - DJANGO_VERSION=1.6 python: - "3.3" - "2.7" - "2.6" install: - "pip install -q django==$DJANGO_VERSION" - "python setup.py -q install" - "pip install -r requirements.txt" script: "py.test tests/" matrix: exclude: - python: 3.3 env: DJANGO_VERSION=1.4.10 django-braces-1.3.1/CONTRIBUTORS.txt000066400000000000000000000007661226211642100167170ustar00rootroot00000000000000==== Team ==== Project Leads ============= * Kenneth Love * Chris Jones Direct Contributors =================== * Daniel Greenfeld * Drew Tempelmeyer * Baptiste Mispelon * Derek Payton * Rafal Stozek * Ethan Soergel * Piotr Kilczuk * Rodney Folz * Markus Zapke-Gründemann * Kamil Gałuszka * Danilo Bargen Other Contributors ================== * The entire Python and Django communities for providing us the tools and desire we to build these things. django-braces-1.3.1/LICENSE000066400000000000000000000027761226211642100152310ustar00rootroot00000000000000Copyright (c) Brack3t and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Brack3t nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-braces-1.3.1/MANIFEST.in000066400000000000000000000001311226211642100157410ustar00rootroot00000000000000include README.md include LICENSE include CONTRIBUTORS.txt recursive-include braces *.py django-braces-1.3.1/README.md000066400000000000000000000104301226211642100154650ustar00rootroot00000000000000# django-braces Mixins for Django's class-based views. [![Latest drone.io status](https://drone.io/github.com/brack3t/django-braces/status.png)](https://drone.io/github.com/brack3t/django-braces) [![Latest PyPI version](https://pypip.in/v/django-braces/badge.png)](https://crate.io/packages/django-braces/) [![Number of PyPI downloads](https://pypip.in/d/django-braces/badge.png)](https://crate.io/packages/django-braces/) [![Stories in Ready](https://badge.waffle.io/brack3t/django-braces.png)](http://waffle.io/brack3t/django-braces) ## Documentation [Read The Docs](http://django-braces.readthedocs.org/en/latest/index.html) ## Installation Install from PyPI with `pip`: `pip install django-braces` ## Contributing Fork, make a change, update the docs, add/update tests, make a pull request. Add yourself to `CONTRIBUTORS.txt` if you want. All development dependencies are available in `requirements.txt` file. To run the test suite, execute the following in your shell (Django install is required): `py.test tests/ --cov=braces --cov-report=html` ## Change Log ### 1.3.1 * Removed breakpoint that was added accidentally. * Added the build directory to `.gitignore` ### 1.3.0 * Removed `CreateAndRedirectToEditView` mixin. It was marked for deprecation and removal since 1.0. * Added `JsonRequestAndResponseMixin` mixin which attempts to parse requests as JSON. * Added `CanonicalSlugDetailMixin` mixin which allows for the specification of a canonical slug on a `DetailView` to help with SEO by redirecting on non-canonical requests. * Added `UserPassesTestMixin` mixin to replicate the behavior of Django's `@user_passes_test` decorator. * Some fixes for `CanonicalSlugDetailMixin`. * `AccessMixin` now has a runtime-overridable `login_url` attribute. * Fixed problem with `GroupRequiredMixin` that made it not actually work. * 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). * Tests and documentation changes for all of the above. ### 1.2.1 * Fix to allow `reverse_lazy` on all `AccessMixin`-derived mixins. ### 1.2.0 * `FormValidMessageMixin` which provides a `messages` message when the processed form is valid. * `FormInvalidMessageMixin` which provides a `messages` message when the processed form is invalid. * `FormMessagesMixin` which provides the functionality of both of the above mixins. * `GroupRequiredMixin` which is a new access-level mixin which requires that a user be part of a specified group to access a view. ### 1.1.0 * `JSONResponseMixin.render_json_response` method updated to accept a status code. * `JSONResponseMixin` added `json_dumps_kwargs` attribute & get method to pass args to the json encoder. * New `OrderableListMixin` allows ordering of list views by GET params. * Tests updated to test against latest stable Django release (1.5.1) * Small fixes and additions to documentation. ### 1.0.0 * New 'abstract' `AccessMixin` which provides overridable `get_login_url` and `get_redirect_field_name methods` for all access-based mixins. * Rewritten `LoginRequiredMixin` which provides same customization as other access mixins with `login_url`, `raise_exception` & `redirect_field_name`. * New `PrefetchRelatedMixin`. Works the same as `SelectRelatedMixin` but uses Django's `prefetch_related` method. * `CreateAndRedirectToEditView` is marked for deprecation. * `PermissionRequiredMixin` no longer requires dot syntax for permission names. * Marked package as supporting 2.6 thru 3.3 (from rafales). * Fixes to documentation. * Tests to cover new additions and changes. ### 0.2.3 * Tests for all mixins (from rafales). * New `CsrfExemptMixin` for marking views as being CSRF exempt (from jarcoal). * Some documentation updates and a spelling error correction (from shabda). * `SuccessURLRedirectListMixin` raises `ImproperlyConfigured` if no `success_list_url` attribute is supplied (from kennethlove). ### 0.2.2 * Try importing the built-in json module first, drop back to Django if necessary. Django 1.5 compatibility. ### 0.2.1 * Fixed signature of `UserFormKwargsMixin.get_form_kwargs` * Updated `JSONResponseMixin` to work with non-ASCII characters and other datatypes (such as datetimes) * Fixed all mixins that have `raise_exception` as an argument to properly raise a `PermissionDenied` exception to allow for custom 403s. django-braces-1.3.1/braces/000077500000000000000000000000001226211642100154475ustar00rootroot00000000000000django-braces-1.3.1/braces/__init__.py000066400000000000000000000000001226211642100175460ustar00rootroot00000000000000django-braces-1.3.1/braces/forms.py000066400000000000000000000011061226211642100171450ustar00rootroot00000000000000class UserKwargModelFormMixin(object): """ Generic model form mixin for popping user out of the kwargs and attaching it to the instance. This mixin must precede forms.ModelForm/forms.Form. The form is not expecting these kwargs to be passed in, so they must be popped off before anything else is done. """ def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) # Pop the user off the # passed in kwargs. super(UserKwargModelFormMixin, self).__init__(*args, **kwargs) django-braces-1.3.1/braces/views.py000066400000000000000000000725611226211642100171710ustar00rootroot00000000000000import six from django.conf import settings from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login from django.core import serializers from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.serializers.json import DjangoJSONEncoder from django.core.urlresolvers import resolve, reverse from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.utils.encoding import force_text from django.views.decorators.csrf import csrf_exempt ## Django 1.5+ compat try: import json except ImportError: # pragma: no cover from django.utils import simplejson as json class AccessMixin(object): """ 'Abstract' mixin that gives access mixins the same customizable functionality. """ login_url = None raise_exception = False # Default whether to raise an exception to none redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth def get_login_url(self): """ Override this method to customize the login_url. """ login_url = self.login_url or settings.LOGIN_URL if not login_url: raise ImproperlyConfigured( "Define %(cls)s.login_url or settings.LOGIN_URL or override " "%(cls)s.get_login_url()." % {"cls": self.__class__.__name__}) return force_text(login_url) def get_redirect_field_name(self): """ Override this method to customize the redirect_field_name. """ if self.redirect_field_name is None: raise ImproperlyConfigured( "%(cls)s is missing the " "redirect_field_name. Define %(cls)s.redirect_field_name or " "override %(cls)s.get_redirect_field_name()." % { "cls": self.__class__.__name__}) return self.redirect_field_name class LoginRequiredMixin(AccessMixin): """ View mixin which verifies that the user is authenticated. NOTE: This should be the left-most mixin of a view, except when combined with CsrfExemptMixin - which in that case should be the left-most mixin. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated(): if self.raise_exception: raise PermissionDenied # return a forbidden response else: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(LoginRequiredMixin, self).dispatch( request, *args, **kwargs) class CsrfExemptMixin(object): """ Exempts the view from CSRF requirements. NOTE: This should be the left-most mixin of a view. """ @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) class PermissionRequiredMixin(AccessMixin): """ View mixin which verifies that the logged in user has the specified permission. Class Settings `permission_required` - the permission to check for. `login_url` - the login url of site `redirect_field_name` - defaults to "next" `raise_exception` - defaults to False - raise 403 if set to True Example Usage class SomeView(PermissionRequiredMixin, ListView): ... # required permission_required = "app.permission" # optional login_url = "/signup/" redirect_field_name = "hollaback" raise_exception = True ... """ permission_required = None # Default required perms to none def dispatch(self, request, *args, **kwargs): # Make sure that the permission_required attribute is set on the # view, or raise a configuration error. if self.permission_required is None: raise ImproperlyConfigured( "'PermissionRequiredMixin' requires " "'permission_required' attribute to be set.") # Check to see if the request's user has the required permission. has_permission = request.user.has_perm(self.permission_required) if not has_permission: # If the user lacks the permission if self.raise_exception: # *and* if an exception was desired raise PermissionDenied # return a forbidden response. else: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(PermissionRequiredMixin, self).dispatch( request, *args, **kwargs) class MultiplePermissionsRequiredMixin(AccessMixin): """ View mixin which allows you to specify two types of permission requirements. The `permissions` attribute must be a dict which specifies two keys, `all` and `any`. You can use either one on its own or combine them. The value of each key is required to be a list or tuple of permissions. The standard Django permissions style is not strictly enforced. If you have created your own permissions in a different format, they should still work. By specifying the `all` key, the user must have all of the permissions in the passed in list. By specifying The `any` key , the user must have ONE of the set permissions in the list. Class Settings `permissions` - This is required to be a dict with one or both keys of `all` and/or `any` containing a list or tuple of permissions. `login_url` - the login url of site `redirect_field_name` - defaults to "next" `raise_exception` - defaults to False - raise 403 if set to True Example Usage class SomeView(MultiplePermissionsRequiredMixin, ListView): ... #required permissions = { "all": ("blog.add_post", "blog.change_post"), "any": ("blog.delete_post", "user.change_user") } #optional login_url = "/signup/" redirect_field_name = "hollaback" raise_exception = True """ permissions = None # Default required perms to none def dispatch(self, request, *args, **kwargs): self._check_permissions_attr() perms_all = self.permissions.get('all') or None perms_any = self.permissions.get('any') or None self._check_permissions_keys_set(perms_all, perms_any) self._check_perms_keys("all", perms_all) self._check_perms_keys("any", perms_any) # If perms_all, check that user has all permissions in the list/tuple if perms_all: if not request.user.has_perms(perms_all): if self.raise_exception: raise PermissionDenied return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) # If perms_any, check that user has at least one in the list/tuple if perms_any: has_one_perm = False for perm in perms_any: if request.user.has_perm(perm): has_one_perm = True break if not has_one_perm: if self.raise_exception: raise PermissionDenied return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(MultiplePermissionsRequiredMixin, self).dispatch( request, *args, **kwargs) def _check_permissions_attr(self): """ Check permissions attribute is set and that it is a dict. """ if self.permissions is None or not isinstance(self.permissions, dict): raise ImproperlyConfigured( "'PermissionsRequiredMixin' requires " "'permissions' attribute to be set to a dict.") def _check_permissions_keys_set(self, perms_all=None, perms_any=None): """ Check to make sure the keys `any` or `all` are not both blank. If both are blank either an empty dict came in or the wrong keys came in. Both are invalid and should raise an exception. """ if perms_all is None and perms_any is None: raise ImproperlyConfigured( "'PermissionsRequiredMixin' requires" "'permissions' attribute to be set to a dict and the 'any' " "or 'all' key to be set.") def _check_perms_keys(self, key=None, perms=None): """ If the permissions list/tuple passed in is set, check to make sure that it is of the type list or tuple. """ if perms and not isinstance(perms, (list, tuple)): raise ImproperlyConfigured( "'MultiplePermissionsRequiredMixin' " "requires permissions dict '%s' value to be a list " "or tuple." % key) class GroupRequiredMixin(AccessMixin): group_required = None def get_group_required(self): if self.group_required is None or ( not isinstance(self.group_required, (list, tuple) + six.string_types) ): raise ImproperlyConfigured( "'GroupRequiredMixin' requires " "'group_required' attribute to be set and be one of the " "following types: string, unicode, list, or tuple.") return self.group_required def check_membership(self, group): """ Check required group(s) """ return group in self.request.user.groups.values_list("name", flat=True) def dispatch(self, request, *args, **kwargs): self.request = request in_group = False if self.request.user.is_authenticated(): in_group = self.check_membership(self.get_group_required()) if not in_group: if self.raise_exception: raise PermissionDenied else: return redirect_to_login( request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(GroupRequiredMixin, self).dispatch( request, *args, **kwargs) class UserPassesTestMixin(AccessMixin): """ CBV Mixin allows you to define test that every user should pass to get access into view. Class Settings `test_func` - This is required to be a method that takes user instance and return True or False after checking conditions. `login_url` - the login url of site `redirect_field_name` - defaults to "next" `raise_exception` - defaults to False - raise 403 if set to True """ def test_func(self, user): raise NotImplementedError( "%(cls)s is missing implementation of the " "test_func method. You should write one." % { "cls": self.__class__.__name__}) def get_test_func(self): return getattr(self, "test_func") def dispatch(self, request, *args, **kwargs): user_test_result = self.get_test_func()(request.user) if not user_test_result: # If user don't pass the test if self.raise_exception: # *and* if an exception was desired raise PermissionDenied else: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(UserPassesTestMixin, self).dispatch( request, *args, **kwargs) class UserFormKwargsMixin(object): """ CBV mixin which puts the user from the request into the form kwargs. Note: Using this mixin requires you to pop the `user` kwarg out of the dict in the super of your form's `__init__`. """ def get_form_kwargs(self): kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() # Update the existing form kwargs dict with the request's user. kwargs.update({"user": self.request.user}) return kwargs class SuccessURLRedirectListMixin(object): """ Simple CBV mixin which sets the success url to the list view of a given app. Set success_list_url as a class attribute of your CBV and don't worry about overloading the get_success_url. This is only to be used for redirecting to a list page. If you need to reverse the url with kwargs, this is not the mixin to use. """ success_list_url = None # Default the success url to none def get_success_url(self): # Return the reversed success url. if self.success_list_url is None: raise ImproperlyConfigured( "%(cls)s is missing a succes_list_url " "name to reverse and redirect to. Define " "%(cls)s.success_list_url or override " "%(cls)s.get_success_url()" "." % {"cls": self.__class__.__name__}) return reverse(self.success_list_url) class SuperuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_superuser` set to True. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_superuser: # If the user is a standard user, if self.raise_exception: # *and* if an exception was desired raise PermissionDenied # return a forbidden response. else: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(SuperuserRequiredMixin, self).dispatch( request, *args, **kwargs) class SetHeadlineMixin(object): """ Mixin allows you to set a static headline through a static property on the class or programmatically by overloading the get_headline method. """ headline = None # Default the headline to none def get_context_data(self, **kwargs): kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) # Update the existing context dict with the provided headline. kwargs.update({"headline": self.get_headline()}) return kwargs def get_headline(self): if self.headline is None: # If no headline was provided as a view # attribute and this method wasn't # overridden raise a configuration error. raise ImproperlyConfigured( "%(cls)s is missing a headline. " "Define %(cls)s.headline, or override " "%(cls)s.get_headline()." % {"cls": self.__class__.__name__} ) return self.headline class SelectRelatedMixin(object): """ Mixin allows you to provide a tuple or list of related models to perform a select_related on. """ select_related = None # Default related fields to none def get_queryset(self): if self.select_related is None: # If no fields were provided, # raise a configuration error raise ImproperlyConfigured( "%(cls)s is missing the " "select_related property. This must be a tuple or list." % { "cls": self.__class__.__name__}) if not isinstance(self.select_related, (tuple, list)): # If the select_related argument is *not* a tuple or list, # raise a configuration error. raise ImproperlyConfigured( "%(cls)s's select_related property " "must be a tuple or list." % {"cls": self.__class__.__name__}) # Get the current queryset of the view queryset = super(SelectRelatedMixin, self).get_queryset() return queryset.select_related(*self.select_related) class PrefetchRelatedMixin(object): """ Mixin allows you to provide a tuple or list of related models to perform a prefetch_related on. """ prefetch_related = None # Default prefetch fields to none def get_queryset(self): if self.prefetch_related is None: # If no fields were provided, # raise a configuration error raise ImproperlyConfigured( "%(cls)s is missing the " "prefetch_related property. This must be a tuple or list." % { "cls": self.__class__.__name__}) if not isinstance(self.prefetch_related, (tuple, list)): # If the select_related argument is *not* a tuple or list, # raise a configuration error. raise ImproperlyConfigured( "%(cls)s's prefetch_related property " "must be a tuple or list." % {"cls": self.__class__.__name__}) # Get the current queryset of the view queryset = super(PrefetchRelatedMixin, self).get_queryset() return queryset.prefetch_related(*self.prefetch_related) class StaffuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_staff` set to True. """ def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: # If the request's user is not staff, if self.raise_exception: # *and* if an exception was desired raise PermissionDenied # return a forbidden response else: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) return super(StaffuserRequiredMixin, self).dispatch( request, *args, **kwargs) class JSONResponseMixin(object): """ A mixin that allows you to easily serialize simple data such as a dict or Django models. """ content_type = u"application/json" json_dumps_kwargs = None def get_content_type(self): if self.content_type is None: raise ImproperlyConfigured( u"%(cls)s is missing a content type. " u"Define %(cls)s.content_type, or override " u"%(cls)s.get_content_type()." % { u"cls": self.__class__.__name__} ) return self.content_type def get_json_dumps_kwargs(self): if self.json_dumps_kwargs is None: self.json_dumps_kwargs = {} self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) return self.json_dumps_kwargs def render_json_response(self, context_dict, status=200): """ Limited serialization for shipping plain data. Do not use for models or other complex or custom objects. """ json_context = json.dumps(context_dict, cls=DjangoJSONEncoder, **self.get_json_dumps_kwargs()).encode( u'utf-8') return HttpResponse(json_context, content_type=self.get_content_type(), status=status) def render_json_object_response(self, objects, **kwargs): """ Serializes objects using Django's builtin JSON serializer. Additional kwargs can be used the same way for django.core.serializers.serialize. """ json_data = serializers.serialize(u"json", objects, **kwargs) return HttpResponse(json_data, content_type=self.get_content_type()) class AjaxResponseMixin(object): """ Mixin allows you to define alternative methods for ajax requests. Similar to the normal get, post, and put methods, you can use get_ajax, post_ajax, and put_ajax. """ def dispatch(self, request, *args, **kwargs): request_method = request.method.lower() if request.is_ajax() and request_method in self.http_method_names: handler = getattr(self, u"{0}_ajax".format(request_method), self.http_method_not_allowed) self.request = request self.args = args self.kwargs = kwargs return handler(request, *args, **kwargs) return super(AjaxResponseMixin, self).dispatch( request, *args, **kwargs) def get_ajax(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def post_ajax(self, request, *args, **kwargs): return self.post(request, *args, **kwargs) def put_ajax(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def delete_ajax(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) class JsonRequestResponseMixin(JSONResponseMixin): """ Extends JSONResponseMixin. Attempts to parse request as JSON. If request is properly formatted, the json is saved to self.request_json as a Python object. request_json will be None for imparsible requests. Set the attribute require_json to True to return a 400 "Bad Request" error for requests that don't contain JSON. Note: To allow public access to your view, you'll need to use the csrf_exempt decorator or CsrfExemptMixin. Example Usage: class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): def post(self, request, *args, **kwargs): do_stuff_with_contents_of_request_json() return self.render_json_response( {'message': 'Thanks!'}) """ require_json = False error_response_dict = {u"errors": [u"Improperly formatted request"]} def render_bad_request_response(self, error_dict=None): if error_dict is None: error_dict = self.error_response_dict json_context = json.dumps( error_dict, cls=DjangoJSONEncoder, **self.get_json_dumps_kwargs() ).encode(u'utf-8') return HttpResponseBadRequest( json_context, content_type=self.get_content_type()) def get_request_json(self): try: return json.loads(self.request.body.decode(u'utf-8')) except ValueError: return None def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.request_json = self.get_request_json() if self.require_json and self.request_json is None: return self.render_bad_request_response() return super(JsonRequestResponseMixin, self).dispatch( request, *args, **kwargs) class OrderableListMixin(object): """ Mixin allows your users to order records using GET parameters """ orderable_columns = None orderable_columns_default = None order_by = None ordering = None def get_context_data(self, **kwargs): """ Augments context with: * ``order_by`` - name of the field * ``ordering`` - order of ordering, either ``asc`` or ``desc`` """ context = super(OrderableListMixin, self).get_context_data(**kwargs) context["order_by"] = self.order_by context["ordering"] = self.ordering return context def get_orderable_columns(self): if not self.orderable_columns: raise ImproperlyConfigured( "Please define allowed ordering columns") return self.orderable_columns def get_orderable_columns_default(self): if not self.orderable_columns_default: raise ImproperlyConfigured("Please define default ordering column") return self.orderable_columns_default def get_ordered_queryset(self, queryset=None): """ Augments ``QuerySet`` with order_by statement if possible :param QuerySet queryset: ``QuerySet`` to ``order_by`` :return: QuerySet """ get_order_by = self.request.GET.get("order_by") if get_order_by in self.get_orderable_columns(): order_by = get_order_by else: order_by = self.get_orderable_columns_default() self.order_by = order_by self.ordering = "asc" if order_by and self.request.GET.get("ordering", "asc") == "desc": order_by = "-" + order_by self.ordering = "desc" return queryset.order_by(order_by) def get_queryset(self): """ Returns ordered ``QuerySet`` """ unordered_queryset = super(OrderableListMixin, self).get_queryset() return self.get_ordered_queryset(unordered_queryset) class CanonicalSlugDetailMixin(object): """ A mixin that enforces a canonical slug in the url. 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. """ def dispatch(self, request, *args, **kwargs): # Set up since we need to super() later instead of earlier. self.request = request self.args = args self.kwargs = kwargs # Get the current object, url slug, and # urlpattern name (namespace aware). obj = self.get_object() slug = self.kwargs.get(self.slug_url_kwarg, None) match = resolve(request.path_info) url_parts = match.namespaces url_parts.append(match.url_name) current_urlpattern = ":".join(url_parts) # Figure out what the slug is supposed to be. if hasattr(obj, "get_canonical_slug"): canonical_slug = obj.get_canonical_slug() else: canonical_slug = self.get_canonical_slug() # If there's a discrepancy between the slug in the url and the # canonical slug, redirect to the canonical slug. if canonical_slug != slug: return redirect(current_urlpattern, pk=obj.pk, slug=canonical_slug, permanent=True) return super(CanonicalSlugDetailMixin, self).dispatch( request, *args, **kwargs) def get_canonical_slug(self): """ Override this method to customize what slug should be considered canonical. Alternatively, define the get_canonical_slug method on this view's object class. In that case, this method will never be called. """ return self.get_object().slug class FormValidMessageMixin(object): """ Mixin allows you to set static message which is displayed by Django's messages framework through a static property on the class or programmatically by overloading the get_form_valid_message method. """ form_valid_message = None # Default to None def get_form_valid_message(self): """ Validate that form_valid_message is set and is either a unicode or str object. """ if self.form_valid_message is None: raise ImproperlyConfigured( '{0}.form_valid_message is not set. Define ' '{0}.form_valid_message, or override ' '{0}.get_form_valid_message().'.format(self.__class__.__name__) ) if not isinstance(self.form_valid_message, six.string_types): raise ImproperlyConfigured( '{0}.form_valid_message must be a str or unicode ' 'object.'.format(self.__class__.__name__) ) return self.form_valid_message def form_valid(self, form): """ Call the super first, so that when overriding get_form_valid_message, we have access to the newly saved object. """ response = super(FormValidMessageMixin, self).form_valid(form) messages.success(self.request, self.get_form_valid_message(), fail_silently=True) return response class FormInvalidMessageMixin(object): """ Mixin allows you to set static message which is displayed by Django's messages framework through a static property on the class or programmatically by overloading the get_form_invalid_message method. """ form_invalid_message = None def get_form_invalid_message(self): """ Validate that form_invalid_message is set and is either a unicode or str object. """ if self.form_invalid_message is None: raise ImproperlyConfigured( '{0}.form_invalid_message is not set. Define ' '{0}.form_invalid_message, or override ' '{0}.get_form_invalid_message().'.format( self.__class__.__name__) ) if not isinstance(self.form_invalid_message, (six.string_types, six.text_type)): raise ImproperlyConfigured( '{0}.form_invalid_message must be a str or unicode ' 'object.'.format(self.__class__.__name__) ) return self.form_invalid_message def form_invalid(self, form): response = super(FormInvalidMessageMixin, self).form_invalid(form) messages.error(self.request, self.get_form_invalid_message(), fail_silently=True) return response class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): """ Mixin is a shortcut to use both FormValidMessageMixin and FormInvalidMessageMixin. """ pass django-braces-1.3.1/conftest.py000066400000000000000000000002451226211642100164100ustar00rootroot00000000000000import os from django.conf import settings def pytest_configure(): if not settings.configured: os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' django-braces-1.3.1/docs/000077500000000000000000000000001226211642100151405ustar00rootroot00000000000000django-braces-1.3.1/docs/Makefile000066400000000000000000000127301226211642100166030ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-braces.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-braces.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-braces" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-braces" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-braces-1.3.1/docs/access.rst000066400000000000000000000163371226211642100171450ustar00rootroot00000000000000Access Mixins ============= These mixins all control a user's access to a given view. Since they all extend the ``AccessMixin``, the implement a common API that includes the following class attributes: :: login_url = settings.LOGIN_URL redirect_field_name = REDIRECT_FIELD_NAME raise_exception = False 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. .. contents:: .. _LoginRequiredMixin: LoginRequiredMixin ------------------ This mixin is rather simple and is generally the first inherited class in any of our views. If we 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. All we are doing here is requiring a user to be authenticated to be able to get to this view. While this doesn't look like much, it frees us up from having to manually overload the dispatch method on every single view that requires a user to be authenticated. If that's all that is needed on this view, we just saved 3 lines of code. Example usage below. .. note:: 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`` and ``raise_exception``. .. note:: 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. :: from django.views.generic import TemplateView from braces.views import LoginRequiredMixin class SomeSecretView(LoginRequiredMixin, TemplateView): template_name = "path/to/template.html" #optional login_url = "/signup/" redirect_field_name = "hollaback" raise_exception = True def get(self, request): return self.render_to_response({}) .. _PermissionRequiredMixin: PermissionRequiredMixin ----------------------- This mixin was originally written, I believe, by `Daniel Sokolowski`_ (`code here`_), but we have updated it to eliminate an unneeded render if the permissions check fails. Rather than overloading the dispatch method manually on every view that needs to check for the existence of a permission, we inherit this class and set the ``permission_required`` class attribute on our view. If you don't specify ``permission_required`` on your view, an ``ImproperlyConfigured`` exception is raised reminding you that you haven't set it. The one limitation of this mixin is that it can **only** accept a single permission. If you need multiple permissions use ``MultiplePermissionsRequiredMixin``. In our normal use case for this mixin, ``LoginRequiredMixin`` comes first, then the ``PermissionRequiredMixin``. If we don't have an authenticated user, there is no sense in checking for any permissions. .. note:: If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. :: from braces.views import LoginRequiredMixin, PermissionRequiredMixin class SomeProtectedView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView): permission_required = "auth.change_user" template_name = "path/to/template.html" .. _MultiplePermissionsRequiredMixin: MultiplePermissionsRequiredMixin -------------------------------- The multiple permissions required view mixin is a more powerful version of the ``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/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 ``PermissionRequiredMixin`` is all you need. .. note:: If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. :: from braces.views import LoginRequiredMixin, MultiplePermissionsRequiredMixin class SomeProtectedView(LoginRequiredMixin, MultiplePermissionsRequiredMixin, TemplateView): #required permissions = { "all": ("blog.add_post", "blog.change_post"), "any": ("blog.delete_post", "user.change_user") } .. _GroupRequiredMixin: GroupRequiredMixin ------------------ .. versionadded:: 1.2 The group required view mixin 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. .. note:: The mixin assumes you're using Django's default Group model and that your user model provides ``groups`` as a ManyToMany relationship. If this **is not** the case, you'll need to override ``check_membership`` in the mixin to handle your custom set up. Standard Django Usage ^^^^^^^^^^^^^^^^^^^^^ :: from braces.views import GroupRequiredMixin class SomeProtectedView(GroupRequiredMixin, TemplateView): #required group_required = u'editors' Custom Group Usage ^^^^^^^^^^^^^^^^^^ :: from braces.views import GroupRequiredMixin class SomeProtectedView(GroupRequiredMixin, TemplateView): #required group_required = u'editors' def check_membership(self, group): ... # Check some other system for group membership if user_in_group: return True else: return False .. _UserPassesTestMixin: UserPassesTestMixin ------------------ .. versionadded:: dev 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 specific a domain). :: from braces.views import UserPassesTestMixin class SomeUserPassView(UserPassesTestMixin, TemplateView): def test_func(self, user): return (user.is_staff and not user.is_superuser and user.email.endswith("mydomain.com")) .. _SuperuserRequiredMixin: SuperuserRequiredMixin ---------------------- 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. :: from braces.views import LoginRequiredMixin, SuperuserRequiredMixin class SomeSuperuserView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): template_name = "path/to/template.html" .. _StaffuserRequiredMixin: StaffuserRequiredMixin ---------------------- Similar to ``SuperuserRequiredMixin``, this mixin allows you to require a user with ``is_staff`` set to True. :: from braces.views import LoginRequiredMixin, StaffuserRequiredMixin class SomeStaffuserView(LoginRequiredMixin, StaffuserRequiredMixin, TemplateView): template_name = "path/to/template.html" .. _Daniel Sokolowski: https://github.com/danols .. _code here: https://github.com/lukaszb/django-guardian/issues/48 django-braces-1.3.1/docs/conf.py000066400000000000000000000172521226211642100164460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-braces documentation build configuration file, created by # sphinx-quickstart on Mon Apr 30 10:31:44 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'django-braces' copyright = u'2013, Kenneth Love and Chris Jones' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.3.1' # The full version, including alpha/beta/rc tags. release = '1.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'django-bracesdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-braces.tex', u'django-braces Documentation', u'Kenneth Love and Chris Jones', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'django-braces', u'django-braces Documentation', [u'Kenneth Love and Chris Jones'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'django-braces', u'django-braces Documentation', u'Kenneth Love and Chris Jones', 'django-braces', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' django-braces-1.3.1/docs/form.rst000066400000000000000000000177121226211642100166450ustar00rootroot00000000000000Form Mixins =========== 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. .. contents:: .. _CsrfExemptMixin: CsrfExemptMixin --------------- 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. .. note:: This should always be the left-most mixin of a view. :: from django.views.generic import UpdateView from braces.views import LoginRequiredMixin, CsrfExemptMixin from profiles.models import Profile class UpdateProfileView(LoginRequiredMixin, CsrfExemptMixin, UpdateView): model = Profile .. _UserFormKwargsMixin: UserFormKwargsMixin ------------------- In one of our client's CMS, we have a lot of form-based views that require a user to be passed in for permission-based form tools. For example, only superusers can delete or disable certain objects. To custom tailor the form for users, we have to pass that user instance into the form and based on their permission level, change certain fields or add specific options within the forms ``__init__`` method. 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. We can then pop the user off in the form and do with it what we need. **Always** remember to pop the user from the kwargs before calling ``super`` on your form, otherwise the form gets an unexpected keyword argument and everything blows up. Usage ^^^^^ :: from django.views.generic import CreateView from braces.views import LoginRequiredMixin, UserFormKwargsMixin from next.example import UserForm class SomeSecretView(LoginRequiredMixin, UserFormKwargsMixin, TemplateView): form_class = UserForm model = User template_name = "path/to/template.html" This obviously pairs very nicely with the following ``Form`` mixin. .. _UserKwargModelFormMixin: UserKwargModelFormMixin ----------------------- The ``UserKwargModelFormMixin`` is a form mixin to go along with our :ref:`UserFormKwargsMixin`. This becomes the first inherited class of our forms that receive the user keyword argument. With this mixin, we have automated the popping off of the keyword argument in our form and no longer have to do it manually on every form that works this way. While this may be overkill for a weekend project, for us, it speeds up adding new features. Usage ^^^^^ :: from braces.forms import UserKwargModelFormMixin class UserForm(UserKwargModelFormMixin, forms.ModelForm): class Meta: model = User def __init__(self, *args, **kwargs): super(UserForm, self).__init__(*args, **kwargs) if not self.user.is_superuser: del self.fields["group"] .. _SuccessURLRedirectListMixin: SuccessURLRedirectListMixin --------------------------- The ``SuccessURLRedirectListMixin`` is a bit more tailored to how we have handled CRUD_ within the CMSes we've built. One CMS's workflow, by design, redirects 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, we simply use this mixin and pass it a reversible route name. Example: :: # urls.py url(r"^users/$", UserListView.as_view(), name="cms_users_list"), # views.py from braces.views import (LoginRequiredMixin, PermissionRequiredMixin, SuccessURLRedirectListMixin) class UserCreateView(LoginRequiredMixin, PermissionRequiredMixin, SuccessURLRedirectListMixin, CreateView): form_class = UserForm model = User permission_required = "auth.add_user" success_list_url = "cms_users_list" ... .. _FormValidMessageMixin: FormValidMessageMixin --------------------- .. versionadded:: 1.2 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. .. warning:: This mixin requires the Django `messages`_ app to be enabled. .. note:: This mixin is designed for use with Django's generic form class-based views, e.g. ``FormView``, ``CreateView``, ``UpdateView`` Static Example ^^^^^^^^^^^^^^ :: from django.views.generic import CreateView from braces.views import FormValidMessageMixin class BlogPostCreateView(FormValidMessageMixin, CreateView): form_class = PostForm model = Post form_valid_message = 'Blog post created!' Dynamic Example ^^^^^^^^^^^^^^^ :: from django.views.generic import CreateView from braces.views import FormValidMessageMixin class BlogPostCreateView(FormValidMessageMixin, CreateView): form_class = PostForm model = Post def get_form_valid_message(self): return '{0} created!'.format(self.object.title) .. _FormInvalidMessageMixin: FormInvalidMessageMixin ----------------------- .. versionadded:: 1.2 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. .. warning:: This mixin requires the Django `messages`_ app to be enabled. .. note:: This mixin is designed for use with Django's generic form class-based views, e.g. ``FormView``, ``CreateView``, ``UpdateView`` Static Example ^^^^^^^^^^^^^^ :: from django.views.generic import CreateView from braces.views import FormInvalidMessageMixin class BlogPostCreateView(FormInvalidMessageMixin, CreateView): form_class = PostForm model = Post form_invalid_message = 'Oh snap, something went wrong!' Dynamic Example ^^^^^^^^^^^^^^^ :: from django.views.generic import CreateView from braces.views import FormInvalidMessageMixin class BlogPostCreateView(FormInvalidMessageMixin, CreateView): form_class = PostForm model = Post def get_form_invalid_message(self): return 'Some custom message' .. _FormMessagesMixin: FormMessagesMixin ----------------- .. versionadded:: 1.2 ``FormMessagesMixin`` is a convenience mixin which combines :ref:`FormValidMessageMixin` and :ref:`FormInvalidMessageMixin` since we commonly provide messages for both states (``form_valid``, ``form_invalid``). .. warning:: This mixin requires the Django `messages`_ app to be enabled. Static & Dynamic Example ^^^^^^^^^^^^^^^^^^^^^^^^ :: from django.views.generic import CreateView from braces.views import FormMessagesMixin class BlogPostCreateView(FormMessagesMixin, CreateView): form_class = PostForm form_invalid_message = 'Something went wrong, post was not saved' model = Post def get_form_valid_message(self): return '{0} created!'.format(self.object.title) .. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete .. _CSRF protection: https://docs.djangoproject.com/en/1.5/ref/contrib/csrf/ .. _csrf_exempt: https://docs.djangoproject.com/en/1.5/ref/contrib/csrf/#django.views.decorators.csrf.csrf_exempt .. _messages: https://docs.djangoproject.com/en/1.5/ref/contrib/messages/ django-braces-1.3.1/docs/index.rst000066400000000000000000000012531226211642100170020ustar00rootroot00000000000000.. django-braces documentation master file, created by sphinx-quickstart on Mon Apr 30 10:31:44 2012. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to django-braces's documentation! ========================================= You can view the code of our project or fork it and add your own mixins (please, send them back to us), on `Github`_. .. toctree:: :maxdepth: 2 Access Mixins Form Mixins
Other Mixins Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Github: https://github.com/brack3t/django-braces django-braces-1.3.1/docs/other.rst000066400000000000000000000242311226211642100170150ustar00rootroot00000000000000Other Mixins ============ These mixins handle other random bits of Django's views, like controlling output, controlling content types, or setting values in the context. .. contents:: .. _SetHeadlineMixin: SetHeadlineMixin ---------------- The ``SetHeadlineMixin`` is a newer edition to our client's CMS. It allows us to *statically* or *programmatically* set the headline of any of our views. We like to write as few templates as possible, so a mixin like this helps us 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. Static Example ^^^^^^^^^^^^^^ :: from braces.views import SetHeadlineMixin class HeadlineView(SetHeadlineMixin, TemplateView): headline = "This is our headline" template_name = "path/to/template.html" Dynamic Example ^^^^^^^^^^^^^^^ :: from datetime import date from braces.views import SetHeadlineMixin class HeadlineView(SetHeadlineMixin, TemplateView): template_name = "path/to/template.html" def get_headline(self): return u"This is our headline for %s" % date.today().isoformat() In both usages, in the template, just print out ``{{ headline }}`` to show the generated headline. .. _SelectRelatedMixin: SelectRelatedMixin ------------------ 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`_. :: # views.py from django.views.generic import DetailView from braces.views import SelectRelatedMixin from profiles.models import Profile class UserProfileView(SelectRelatedMixin, DetailView): model = Profile select_related = ["user"] template_name = "profiles/detail.html" .. _PrefetchRelatedMixin: PrefetchRelatedMixin -------------------- 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`_. :: # views.py from django.contrib.auth.models import User from django.views.generic import DetailView from braces.views import PrefetchRelatedMixin class UserView(PrefetchRelatedMixin, DetailView): model = User prefetch_related = ["post_set"] # where the Post model has an FK to the User model as an author. template_name = "users/detail.html" .. _JSONResponseMixin: JSONResponseMixin ----------------- .. versionchanged:: 1.1 ``render_json_response`` now accepts a ``status_code`` keyword argument. ``json_dumps_kwargs`` class-attribute and ``get_json_dumps_kwargs`` method to provide arguments to the ``json.dumps()`` method. A simple mixin to handle very simple serialization as a response to the browser. :: # views.py from django.views.generic import DetailView from braces.views import JSONResponseMixin class UserProfileAJAXView(JSONResponseMixin, DetailView): model = Profile json_dumps_kwargs = {'indent': 2} def get(self, request, *args, **kwargs): self.object = self.get_object() context_dict = { 'name': self.object.user.name, 'location': self.object.location } return self.render_json_response(context_dict) You can additionally use the `AjaxResponseMixin` :: # views.py from braces.views import AjaxResponseMixin class UserProfileView(JSONResponseMixin, AjaxResponseMixin, DetailView): model = Profile def get_ajax(self, request, *args, **kwargs): return self.render_json_object_response(self.get_object()) The `JSONResponseMixin` provides a class-level variable to control the response type as well. By default it is `application/json`, but you can override that by providing the `content_type` variable a different value or, programmatically, by overriding the `get_content_type()` method. :: from braces.views import JSONResponseMixin class UserProfileAJAXView(JSONResponseMixin, DetailView): content_type = 'application/javascript' model = Profile def get(self, request, *args, **kwargs): self.object = self.get_object() context_dict = { 'name': self.object.user.name, 'location': self.object.location } return self.render_json_response(context_dict) def get_content_type(self): # Shown just for illustrative purposes return 'application/javascript' .. _JsonRequestResponseMixin: JsonRequestResponseMixin ------------------------ .. versionadded:: 1.3 A mixin that attempts to parse request as JSON. If request is properly formatted, the json is saved to self.request_json as a Python object. request_json will be None for imparsible requests. To catch requests that aren't JSON-formatted, set the class attribute ``require_json`` to True. Override the class attribute ``error_response_dict`` to customize the default error message. It extends :ref:`JSONResponseMixin`, so those utilities are available as well. Note: To allow public access to your view, you'll need to use the ``csrf_exempt`` decorator or :ref:`CsrfExemptMixin`. :: from django.views.generic import View from braces.views import CsrfExemptMixin, JsonRequestResponseMixin class SomeView(CsrfExemptMixin, JsonRequestResponseMixin, View): require_json = True def post(self, request, *args, **kwargs): try: burrito = self.request_json['burrito'] toppings = self.request_json['toppings'] except: error_dict = {'message': 'your order must include a burrito AND toppings'} return self.render_bad_request_response(error_dict) place_order(burrito, toppings) return self.render_json_response( {'message': 'Your order has been placed!'}) .. _AjaxResponseMixin: AjaxResponseMixin ----------------- A mixin to allow you to provide alternative methods for handling AJAX requests. 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. :: # views.py from django.views.generic import View from braces.views import AjaxResponseMixin, JSONResponseMixin class SomeView(JSONResponseMixin, AjaxResponseMixin, View): def get_ajax(self, request, *args, **kwargs): json_dict = { 'name': "Benny's Burritos", 'location': "New York, NY" } return self.render_json_response(json_dict) .. _OrderableListMixin: OrderableListMixin ------------------ .. versionadded:: 1.1 A mixin to allow easy ordering of your queryset basing on the GET parameters. Works with `ListView`. To use it, define columns that the data can be order by as well as the default column to order by in your view. This can be done either by simply setting the class attributes... :: # views.py class OrderableListView(OrderableListMixin, ListView): model = Article orderable_columns = ('id', 'title',) orderable_columns_default = 'id' ...or by using similarly name methods to set the ordering constraints more dynamically: :: # views.py class OrderableListView(OrderableListMixin, ListView): model = Article def get_orderable_columns(self): # return an iterable return ('id', 'title', ) def get_orderable_columns_default(self): # return a string return 'id' The ``orderable_columns`` restriction is here in order to stop your users from launching inefficient queries, like ordering by binary columns. ``OrderableListMixin`` will order your queryset basing on following GET params: * ``order_by``: column name, e.g. ``'title'`` * ``ordering``: `'asc'` (default) or ``'desc'`` Example url: `http://127.0.0.1:8000/articles/?order_by=title&ordering=asc` .. _CanonicalSlugDetailMixin: CanonicalSlugDetailMixin ------------------------ .. versionadded:: 1.3 A mixin that enforces a canonical slug in the url. Works with ``DetailView``. 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. To use it, the urlpattern must accept both a ``pk`` and ``slug`` argument in its regex: :: # urls.py urlpatterns = patterns('', url(r'^article/(?P\d+)-(?P[-\w]+)$') ArticleView.as_view(), 'view_article' ) Then create a standard DetailView that inherits this mixin: :: class ArticleView(CanonicalSlugDetailMixin, DetailView): model = Article 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. Control the canonical slug by either implementing the method ``get_canonical_slug()`` on the model class: :: class Article(models.Model): blog = models.ForeignKey('Blog') slug = models.SlugField() def get_canonical_slug(self): return "{}-{}".format(self.blog.get_canonical_slug(), self.slug) Or by overriding the ``get_canonical_slug()`` method on the view: :: class ArticleView(CanonicalSlugDetailMixin, DetailView): model = Article def get_canonical_slug(): import codecs return codecs.encode(self.get_object().slug, 'rot_13') 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. .. _select_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#select-related .. _prefetch_related: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#prefetch-related django-braces-1.3.1/requirements.txt000066400000000000000000000000561226211642100174750ustar00rootroot00000000000000factory_boy mock pytest-django pytest-cov six django-braces-1.3.1/setup.py000066400000000000000000000020321226211642100157170ustar00rootroot00000000000000from setuptools import setup setup( name="django-braces", version="1.3.1", description="Reusable, generic mixins for Django", long_description="Mixins to add easy functionality to Django class-based views, forms, and models.", keywords="django, views, forms, mixins", author="Kenneth Love , Chris Jones ", author_email="devs@brack3t.com", url="https://github.com/brack3t/django-braces/", license="BSD", packages=["braces"], zip_safe=False, install_requires=["six"], include_package_data=True, classifiers=[ "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Django", "Environment :: Web Environment", "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3" ], ) django-braces-1.3.1/tests/000077500000000000000000000000001226211642100153525ustar00rootroot00000000000000django-braces-1.3.1/tests/__init__.py000066400000000000000000000000001226211642100174510ustar00rootroot00000000000000django-braces-1.3.1/tests/compat.py000066400000000000000000000005711226211642100172120ustar00rootroot00000000000000try: from django.utils.encoding import force_text except ImportError: from django.utils.encoding import force_unicode as force_text try: import json except ImportError: from django.utils import simplejson as json try: from django.conf.urls import patterns, url, include except ImportError: from django.conf.urls.defaults import patterns, url, include django-braces-1.3.1/tests/factories.py000066400000000000000000000032551226211642100177100ustar00rootroot00000000000000import factory from django.contrib.auth.models import Group, Permission, User from .models import Article def _get_perm(perm_name): """ Returns permission instance with given name. Permission name is a string like 'auth.add_user'. """ app_label, codename = perm_name.split('.') return Permission.objects.get( content_type__app_label=app_label, codename=codename) class ArticleFactory(factory.django.DjangoModelFactory): FACTORY_FOR = Article title = factory.Sequence(lambda n: 'Article number {0}'.format(n)) body = factory.Sequence(lambda n: 'Body of article {0}'.format(n)) class GroupFactory(factory.django.DjangoModelFactory): FACTORY_FOR = Group name = factory.Sequence(lambda n: 'group{0}'.format(n)) class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = User username = factory.Sequence(lambda n: 'user{0}'.format(n)) first_name = factory.Sequence(lambda n: 'John {0}'.format(n)) last_name = factory.Sequence(lambda n: 'Doe {0}'.format(n)) email = factory.Sequence(lambda n: 'user{0}@example.com'.format(n)) password = 'asdf1234' @classmethod def _prepare(cls, create, **kwargs): password = kwargs.pop('password', None) user = super(UserFactory, cls)._prepare(create, **kwargs) if password: user.set_password(password) if create: user.save() return user @factory.post_generation def permissions(self, create, extracted, **kwargs): if create and extracted: # We have a saved object and a list of permission names self.user_permissions.add(*[_get_perm(pn) for pn in extracted]) django-braces-1.3.1/tests/forms.py000066400000000000000000000004241226211642100170520ustar00rootroot00000000000000from django import forms from braces.forms import UserKwargModelFormMixin from .models import Article class FormWithUserKwarg(UserKwargModelFormMixin, forms.Form): field1 = forms.CharField() class ArticleForm(forms.ModelForm): class Meta: model = Article django-braces-1.3.1/tests/helpers.py000066400000000000000000000027111226211642100173670ustar00rootroot00000000000000from django import test from django.contrib.auth.models import AnonymousUser class TestViewHelper(object): """ Helper class for unit-testing class based views. """ view_class = None request_factory_class = test.RequestFactory def setUp(self): super(TestViewHelper, self).setUp() self.factory = self.request_factory_class() def build_request(self, method='GET', path='/test/', user=None, **kwargs): """ Creates a request using request factory. """ fn = getattr(self.factory, method.lower()) if user is None: user = AnonymousUser() req = fn(path, **kwargs) req.user = user return req def build_view(self, request, args=None, kwargs=None, view_class=None, **viewkwargs): """ Creates a `view_class` view instance. """ if not args: args = () if not kwargs: kwargs = {} if view_class is None: view_class = self.view_class return view_class( request=request, args=args, kwargs=kwargs, **viewkwargs) def dispatch_view(self, request, args=None, kwargs=None, view_class=None, **viewkwargs): """ Creates and dispatches `view_class` view. """ view = self.build_view(request, args, kwargs, view_class, **viewkwargs) return view.dispatch(request, *view.args, **view.kwargs) django-braces-1.3.1/tests/models.py000066400000000000000000000011731226211642100172110ustar00rootroot00000000000000from django.db import models class Article(models.Model): author = models.ForeignKey('auth.User', null=True, blank=True) title = models.CharField(max_length=30) body = models.TextField() slug = models.SlugField(blank=True) class CanonicalArticle(models.Model): author = models.ForeignKey('auth.User', null=True, blank=True) title = models.CharField(max_length=30) body = models.TextField() slug = models.SlugField(blank=True) def get_canonical_slug(self): if self.author: return "{0.author.username}-{0.slug}".format(self) return "unauthored-{0.slug}".format(self) django-braces-1.3.1/tests/settings.py000066400000000000000000000032321226211642100175640ustar00rootroot00000000000000DEBUG = False TEMPLATE_DEBUG = DEBUG TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en-US' SITE_ID = 1 USE_L10N = True USE_TZ = True SECRET_KEY = 'local' ROOT_URLCONF = 'tests.urls' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } MIDDLEWARE_CLASSES = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] TEMPLATE_CONTEXT_PROCESSORS = [ 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.static', 'django.core.context_processors.tz', 'django.core.context_processors.request', 'django.contrib.messages.context_processors.messages' ] STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'tests', ) PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.MD5PasswordHasher', ) import django if django.VERSION < (1, 4): TEMPLATE_CONTEXT_PROCESSORS.remove('django.core.context_processors.tz') MIDDLEWARE_CLASSES.remove( 'django.middleware.clickjacking.XFrameOptionsMiddleware' ) django-braces-1.3.1/tests/templates/000077500000000000000000000000001226211642100173505ustar00rootroot00000000000000django-braces-1.3.1/tests/templates/blank.html000066400000000000000000000000001226211642100213130ustar00rootroot00000000000000django-braces-1.3.1/tests/templates/form.html000066400000000000000000000002541226211642100212020ustar00rootroot00000000000000 {% if messages %} {% for message in messages %} {{ message }} {% endfor %} {% endif %} {{ form.as_p }} django-braces-1.3.1/tests/test_access_mixins.py000066400000000000000000000332251226211642100216200ustar00rootroot00000000000000# -*- coding: utf-8 -*- from django import test from django.test.utils import override_settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.urlresolvers import reverse_lazy from braces.views import UserPassesTestMixin from .compat import force_text from .factories import GroupFactory, UserFactory from .helpers import TestViewHelper from .views import (PermissionRequiredView, MultiplePermissionsRequiredView, SuperuserRequiredView, StaffuserRequiredView, LoginRequiredView, GroupRequiredView, UserPassesTestView, UserPassesTestNotImplementedView) class _TestAccessBasicsMixin(TestViewHelper): """ A set of basic tests for access mixins. """ view_url = None def build_authorized_user(self): """ Returns user authorized to access view. """ raise NotImplementedError def build_unauthorized_user(self): """ Returns user not authorized to access view. """ raise NotImplementedError def test_success(self): """ If user is authorized then view should return normal response. """ user = self.build_authorized_user() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual('OK', force_text(resp.content)) def test_redirects_to_login(self): """ Browser should be redirected to login page if user is not authorized to view this page. """ user = self.build_unauthorized_user() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertRedirects(resp, u'/accounts/login/?next={0}'.format( self.view_url)) def test_raise_permission_denied(self): """ PermissionDenied should be raised if user is not authorized and raise_exception attribute is set to True. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) with self.assertRaises(PermissionDenied): self.dispatch_view(req, raise_exception=True) def test_custom_login_url(self): """ Login url should be customizable. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) resp = self.dispatch_view(req, login_url='/login/') self.assertEqual( u'/login/?next={0}'.format(self.view_url), resp['Location']) # Test with reverse_lazy resp = self.dispatch_view(req, login_url=reverse_lazy('headline')) self.assertEqual(u'/headline/?next={0}'.format( self.view_url), resp['Location']) def test_custom_redirect_field_name(self): """ Redirect field name should be customizable. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) resp = self.dispatch_view(req, redirect_field_name='foo') expected_url = u'/accounts/login/?foo={0}'.format(self.view_url) self.assertEqual(expected_url, resp['Location']) @override_settings(LOGIN_URL=None) def test_get_login_url_raises_exception(self): """ Test that get_login_url from AccessMixin raises ImproperlyConfigured. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view( self.build_request(path=self.view_url), login_url=None) def test_get_redirect_field_name_raises_exception(self): """ Test that get_redirect_field_name from AccessMixin raises ImproperlyConfigured. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view( self.build_request(path=self.view_url), redirect_field_name=None) @override_settings(LOGIN_URL="/auth/login/") def test_overridden_login_url(self): """ Test that login_url is not set in stone on module load but can be overridden dynamically. """ user = self.build_unauthorized_user() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertRedirects(resp, u'/auth/login/?next={0}'.format( self.view_url)) class TestLoginRequiredMixin(TestViewHelper, test.TestCase): """ Tests for LoginRequiredMixin. """ view_class = LoginRequiredView view_url = '/login_required/' def test_anonymous(self): resp = self.client.get(self.view_url) self.assertRedirects(resp, '/accounts/login/?next=/login_required/') def test_anonymous_raises_exception(self): with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True) def test_authenticated(self): user = UserFactory() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) assert resp.status_code == 200 assert force_text(resp.content) == 'OK' class TestPermissionRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """ Tests for PermissionRequiredMixin. """ view_class = PermissionRequiredView view_url = '/permission_required/' def build_authorized_user(self): return UserFactory(permissions=['auth.add_user']) def build_unauthorized_user(self): return UserFactory() def test_invalid_permission(self): """ ImproperlyConfigured exception should be raised in two situations: if permission is None or if permission has invalid name. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), permission_required=None) class TestMultiplePermissionsRequiredMixin( _TestAccessBasicsMixin, test.TestCase): view_class = MultiplePermissionsRequiredView view_url = '/multiple_permissions_required/' def build_authorized_user(self): return UserFactory(permissions=[ 'tests.add_article', 'tests.change_article', 'auth.change_user']) def build_unauthorized_user(self): return UserFactory(permissions=['tests.add_article']) def test_redirects_to_login(self): """ User should be redirected to login page if he or she does not have sufficient permissions. """ url = '/multiple_permissions_required/' test_cases = ( # missing one permission from 'any' ['tests.add_article', 'tests.change_article'], # missing one permission from 'all' ['tests.add_article', 'auth.add_user'], # no permissions at all [], ) for permissions in test_cases: user = UserFactory(permissions=permissions) self.client.login(username=user.username, password='asdf1234') resp = self.client.get(url) self.assertRedirects(resp, u'/accounts/login/?next={0}'.format( url)) def test_invalid_permissions(self): """ ImproperlyConfigured exception should be raised if permissions attribute is set incorrectly. """ permissions = ( None, # permissions must be set (), # and they must be a dict {}, # at least one of 'all', 'any' keys must be present {'all': None}, # both all and any must be list or a tuple {'all': {'a': 1}}, {'any': None}, {'any': {'a': 1}}, ) for attr in permissions: with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), permissions=attr) def test_raise_permission_denied(self): """ PermissionDenied should be raised if user does not have sufficient permissions and raise_exception is set to True. """ test_cases = ( # missing one permission from 'any' ['tests.add_article', 'tests.change_article'], # missing one permission from 'all' ['tests.add_article', 'auth.add_user'], # no permissions at all [], ) for permissions in test_cases: user = UserFactory(permissions=permissions) req = self.build_request(user=user) with self.assertRaises(PermissionDenied): self.dispatch_view(req, raise_exception=True) def test_all_permissions_key(self): """ Tests if everything works if only 'all' permissions has been set. """ permissions = {'all': ['auth.add_user', 'tests.add_article']} user = UserFactory(permissions=permissions['all']) req = self.build_request(user=user) resp = self.dispatch_view(req, permissions=permissions) self.assertEqual('OK', force_text(resp.content)) user = UserFactory(permissions=['auth.add_user']) with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(user=user), raise_exception=True, permissions=permissions) def test_any_permissions_key(self): """ Tests if everything works if only 'any' permissions has been set. """ permissions = {'any': ['auth.add_user', 'tests.add_article']} user = UserFactory(permissions=['tests.add_article']) req = self.build_request(user=user) resp = self.dispatch_view(req, permissions=permissions) self.assertEqual('OK', force_text(resp.content)) user = UserFactory(permissions=[]) with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(user=user), raise_exception=True, permissions=permissions) class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = SuperuserRequiredView view_url = '/superuser_required/' def build_authorized_user(self): return UserFactory(is_superuser=True, is_staff=True) def build_unauthorized_user(self): return UserFactory() class TestStaffuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = StaffuserRequiredView view_url = '/staffuser_required/' def build_authorized_user(self): return UserFactory(is_staff=True) def build_unauthorized_user(self): return UserFactory() class TestGroupRequiredMixin(_TestAccessBasicsMixin, test.TestCase): view_class = GroupRequiredView view_url = '/group_required/' def build_authorized_user(self): user = UserFactory() group = GroupFactory(name='test_group') user.groups.add(group) return user def build_unauthorized_user(self): return UserFactory() def test_with_group_list(self): view = self.view_class() view.group_required = ['test_group', 'editors'] user = self.build_authorized_user() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual('OK', force_text(resp.content)) def test_improperly_configured(self): view = self.view_class() view.group_required = None with self.assertRaises(ImproperlyConfigured): view.get_group_required() view.group_required = {'foo': 'bar'} with self.assertRaises(ImproperlyConfigured): view.get_group_required() def test_with_unicode(self): view = self.view_class() view.group_required = u'niño' user = self.build_authorized_user() user.groups.all()[0].name = u'niño' user.groups.all()[0].save() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual('OK', force_text(resp.content)) class TestUserPassesTestMixin(_TestAccessBasicsMixin, test.TestCase): view_class = UserPassesTestView view_url = '/user_passes_test/' view_not_implemented_class = UserPassesTestNotImplementedView view_not_implemented_url = '/user_passes_test_not_implemented/' # for testing with passing and not passsing func_test def build_authorized_user(self, is_superuser=False): return UserFactory(is_superuser=is_superuser, is_staff=True, email="user@mydomain.com") def build_unauthorized_user(self): return UserFactory() def test_with_user_pass(self): user = self.build_authorized_user() self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual('OK', force_text(resp.content)) def test_with_user_not_pass(self): user = self.build_authorized_user(is_superuser=True) self.client.login(username=user.username, password='asdf1234') resp = self.client.get(self.view_url) self.assertRedirects(resp, '/accounts/login/?next=/user_passes_test/') def test_with_user_raise_exception(self): with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True) def test_not_implemented(self): view = self.view_not_implemented_class() with self.assertRaises(NotImplementedError): view.dispatch( self.build_request(path=self.view_not_implemented_url), raise_exception=True) django-braces-1.3.1/tests/test_ajax_mixins.py000066400000000000000000000154321226211642100213020ustar00rootroot00000000000000import mock from django import test from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from braces.views import AjaxResponseMixin from .compat import force_text from .factories import ArticleFactory, UserFactory from .helpers import TestViewHelper from .views import SimpleJsonView, JsonRequestResponseView from .compat import json class TestAjaxResponseMixin(TestViewHelper, test.TestCase): """ Tests for AjaxResponseMixin. """ methods = [u'get', u'post', u'put', u'delete'] def test_xhr(self): """ Checks if ajax_* method has been called for every http method. """ # AjaxResponseView returns 'AJAX_OK' when requested with XmlHttpRequest for m in self.methods: fn = getattr(self.client, m) resp = fn(u'/ajax_response/', HTTP_X_REQUESTED_WITH=u'XMLHttpRequest') assert force_text(resp.content) == u'AJAX_OK' def test_not_xhr(self): """ Normal methods (get, post, etc) should be used when handling non-ajax requests. """ for m in self.methods: fn = getattr(self.client, m) resp = fn(u'/ajax_response/') assert force_text(resp.content) == u'OK' def test_fallback_to_normal_methods(self): """ Ajax methods should fallback to normal methods by default. """ test_cases = [ (u'get', u'get'), (u'post', u'post'), (u'put', u'get'), (u'delete', u'get'), ] for ajax_method, fallback in test_cases: m, mixin = mock.Mock(), AjaxResponseMixin() m.return_value = HttpResponse() req = self.build_request() setattr(mixin, fallback, m) fn = getattr(mixin, u"{0}_ajax".format(ajax_method)) ret = fn(req, 1, 2, meth=ajax_method) # check if appropriate method has been called m.assert_called_once_with(req, 1, 2, meth=ajax_method) # check if appropriate value has been returned self.assertIs(m.return_value, ret) class TestJSONResponseMixin(TestViewHelper, test.TestCase): """ Tests for JSONResponseMixin. """ view_class = SimpleJsonView def assert_json_response(self, resp, status_code=200): self.assertEqual(status_code, resp.status_code) self.assertEqual(u'application/json', resp[u'content-type'].split(u';')[0]) def get_content(self, url): """ GET url and return content """ resp = self.client.get(url) self.assert_json_response(resp) content = force_text(resp.content) return content def test_simple_json(self): """ Tests render_json_response() method. """ user = UserFactory() self.client.login(username=user.username, password=u'asdf1234') data = json.loads(self.get_content(u'/simple_json/')) self.assertEqual({u'username': user.username}, data) def test_serialization(self): """ Tests render_json_object_response() method which serializes objects using django's serializer framework. """ a1, a2 = [ArticleFactory() for __ in range(2)] data = json.loads(self.get_content(u'/article_list_json/')) self.assertIsInstance(data, list) self.assertEqual(2, len(data)) titles = [] for row in data: # only title has been serialized self.assertEqual(1, len(row[u'fields'])) titles.append(row[u'fields'][u'title']) self.assertIn(a1.title, titles) self.assertIn(a2.title, titles) def test_missing_content_type(self): """ ImproperlyConfigured exception should be raised if content_type attribute is not set correctly. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), content_type=None) def test_pretty_json(self): """ Success if JSON responses are the same, and the well-indented response is longer than the normal one. """ user = UserFactory() self.client.login(username=user.username, password=u'asfa') normal_content = self.get_content(u'/simple_json/') self.view_class.json_dumps_kwargs = {u'indent': 2} pretty_content = self.get_content(u'/simple_json/') normal_json = json.loads(u'{0}'.format(normal_content)) pretty_json = json.loads(u'{0}'.format(pretty_content)) self.assertEqual(normal_json, pretty_json) self.assertTrue(len(pretty_content) > len(normal_content)) class TestJsonRequestResponseMixin(TestViewHelper, test.TestCase): view_class = JsonRequestResponseView request_dict = {u'status': u'operational'} def test_get_request_json_properly_formatted(self): """ Properly formatted JSON requests should result in a JSON object """ data = json.dumps(self.request_dict).encode(u'utf-8') response = self.client.post( u'/json_request/', content_type=u'application/json', data=data ) response_json = json.loads(response.content.decode(u'utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(response_json, self.request_dict) def test_get_request_json_improperly_formatted(self): """ Improperly formatted JSON requests should make request_json == None """ response = self.client.post( u'/json_request/', data=self.request_dict ) response_json = json.loads(response.content.decode(u'utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(response_json, None) def test_bad_request_response(self): """ If a view calls render_bad_request_response when request_json is empty or None, the client should get a 400 error """ response = self.client.post( u'/json_bad_request/', data=self.request_dict ) response_json = json.loads(response.content.decode(u'utf-8')) self.assertEqual(response.status_code, 400) self.assertEqual(response_json, self.view_class.error_response_dict) def test_bad_request_response_with_custom_error_message(self): """ If a view calls render_bad_request_response when request_json is empty or None, the client should get a 400 error """ response = self.client.post( u'/json_custom_bad_request/', data=self.request_dict ) response_json = json.loads(response.content.decode(u'utf-8')) self.assertEqual(response.status_code, 400) self.assertEqual(response_json, {u'error': u'you messed up'}) django-braces-1.3.1/tests/test_forms.py000066400000000000000000000013341226211642100201120ustar00rootroot00000000000000from django import test from django.contrib.auth.models import User from . import forms class TestUserKwargModelFormMixin(test.TestCase): """ Tests for UserKwargModelFormMixin. """ def test_without_user_kwarg(self): """ It should be possible to create form without 'user' kwarg. In that case 'user' attribute should be set to None. """ form = forms.FormWithUserKwarg() assert form.user is None def test_with_user_kwarg(self): """ Form's 'user' attribute should be set to value passed as 'user' argument. """ user = User(username='test') form = forms.FormWithUserKwarg(user=user) assert form.user is user django-braces-1.3.1/tests/test_other_mixins.py000066400000000000000000000354211226211642100215000ustar00rootroot00000000000000# -*- coding: utf-8 -*- import mock from django import test from django.core.exceptions import ImproperlyConfigured from braces.views import (SetHeadlineMixin, FormValidMessageMixin, FormInvalidMessageMixin) from .models import Article, CanonicalArticle from .helpers import TestViewHelper from .views import (CreateArticleView, ArticleListView, AuthorDetailView, OrderableListView, FormMessagesView) from .factories import UserFactory from .compat import force_text class TestSuccessURLRedirectListMixin(test.TestCase): """ Tests for SuccessURLRedirectListMixin. """ def test_redirect(self): """ Test if browser is redirected to list view. """ data = {'title': "Test body", 'body': "Test body"} resp = self.client.post('/article_list/create/', data) self.assertRedirects(resp, '/article_list/') def test_no_url_name(self): """ Test that ImproperlyConfigured is raised. """ data = {'title': "Test body", 'body': "Test body"} with self.assertRaises(ImproperlyConfigured): self.client.post('/article_list_bad/create/', data) class TestUserFormKwargsMixin(test.TestCase): """ Tests for UserFormKwargsMixin. """ def test_post_method(self): user = UserFactory() self.client.login(username=user.username, password='asdf1234') resp = self.client.post('/form_with_user_kwarg/', {'field1': 'foo'}) assert force_text(resp.content) == "username: %s" % user.username def test_get_method(self): user = UserFactory() self.client.login(username=user.username, password='asdf1234') resp = self.client.get('/form_with_user_kwarg/') assert resp.context['form'].user == user class TestSetHeadlineMixin(test.TestCase): """ Tests for SetHeadlineMixin. """ def test_dynamic_headline(self): """ Tests if get_headline() is called properly. """ resp = self.client.get('/headline/test-headline/') self.assertEqual('test-headline', resp.context['headline']) def test_context_data(self): """ Tests if mixin adds proper headline to template context. """ resp = self.client.get('/headline/foo-bar/') self.assertEqual("foo-bar", resp.context['headline']) def test_get_headline(self): """ Tests if get_headline() method works correctly. """ mixin = SetHeadlineMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_headline() mixin.headline = "Test headline" self.assertEqual("Test headline", mixin.get_headline()) class TestCsrfExemptMixin(test.TestCase): """ Tests for TestCsrfExemptMixin. """ def setUp(self): super(TestCsrfExemptMixin, self).setUp() self.client = self.client_class(enforce_csrf_checks=True) def test_csrf_token_is_not_required(self): """ Tests if csrf token is not required. """ resp = self.client.post('/csrf_exempt/', {'field1': 'test'}) self.assertEqual(200, resp.status_code) self.assertEqual("OK", force_text(resp.content)) class TestSelectRelatedMixin(TestViewHelper, test.TestCase): view_class = ArticleListView def test_missing_select_related(self): """ ImproperlyConfigured exception should be raised if select_related attribute is missing. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), select_related=None) def test_invalid_select_related(self): """ ImproperlyConfigured exception should be raised if select_related is not a tuple or a list. :return: """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), select_related={'a': 1}) @mock.patch('django.db.models.query.QuerySet.select_related') def test_select_related_called(self, m): """ Checks if QuerySet's select_related() was called with correct arguments. """ qs = Article.objects.all() m.return_value = qs.select_related('author') qs.select_related = m m.reset_mock() resp = self.dispatch_view(self.build_request()) self.assertEqual(200, resp.status_code) m.assert_called_once_with('author') class TestPrefetchRelatedMixin(TestViewHelper, test.TestCase): view_class = AuthorDetailView def test_missing_prefetch_related(self): """ ImproperlyConfigured exception should be raised if prefetch_related attribute is missing. """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), prefetch_related=None) def test_invalid_prefetch_related(self): """ ImproperlyConfigured exception should be raised if prefetch_related is not a tuple or a list. :return: """ with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), prefetch_related={'a': 1}) @mock.patch('django.db.models.query.QuerySet.prefetch_related') def test_prefetch_related_called(self, m): """ Checks if QuerySet's prefetch_related() was called with correct arguments. """ qs = Article.objects.all() m.return_value = qs.prefetch_related('article_set') qs.prefetch_related = m m.reset_mock() resp = self.dispatch_view(self.build_request()) self.assertEqual(200, resp.status_code) m.assert_called_once_with('article_set') class TestOrderableListMixin(TestViewHelper, test.TestCase): view_class = OrderableListView def __make_test_articles(self): a1 = Article.objects.create(title='Alpha', body='Zet') a2 = Article.objects.create(title='Zet', body='Alpha') return a1, a2 def test_correct_order(self): """ Objects must be properly ordered if requested with valid column names """ a1, a2 = self.__make_test_articles() resp = self.dispatch_view( self.build_request(path='?order_by=title&ordering=asc'), orderable_columns=None, get_orderable_columns=lambda: ('id', 'title', )) self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) resp = self.dispatch_view( self.build_request(path='?order_by=id&ordering=desc'), orderable_columns=None, get_orderable_columns=lambda: ('id', 'title', )) self.assertEqual(list(resp.context_data['object_list']), [a2, a1]) def test_default_column(self): """ When no ordering specified in GET, use View.get_orderable_columns_default() """ a1, a2 = self.__make_test_articles() resp = self.dispatch_view(self.build_request()) self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) def test_get_orderable_columns_returns_correct_values(self): """ OrderableListMixin.get_orderable_columns() should return View.orderable_columns attribute by default or raise ImproperlyConfigured exception in the attribute is None """ view = self.view_class() self.assertEqual(view.get_orderable_columns(), view.orderable_columns) view.orderable_columns = None self.assertRaises(ImproperlyConfigured, lambda: view.get_orderable_columns()) def test_get_orderable_columns_default_returns_correct_values(self): """ OrderableListMixin.get_orderable_columns_default() should return View.orderable_columns_default attribute by default or raise ImproperlyConfigured exception in the attribute is None """ view = self.view_class() self.assertEqual(view.get_orderable_columns_default(), view.orderable_columns_default) view.orderable_columns_default = None self.assertRaises(ImproperlyConfigured, lambda: view.get_orderable_columns_default()) def test_only_allowed_columns(self): """ If column is not in Model.Orderable.columns iterable, the objects should be ordered by default column. """ a1, a2 = self.__make_test_articles() resp = self.dispatch_view( self.build_request(path='?order_by=body&ordering=asc'), orderable_columns_default=None, get_orderable_columns_default=lambda: 'title') self.assertEqual(list(resp.context_data['object_list']), [a1, a2]) class TestCanonicalSlugDetailView(test.TestCase): def setUp(self): a1 = Article.objects.create(title='Alpha', body='Zet', slug='alpha') a2 = Article.objects.create(title='Zet', body='Alpha', slug='zet') def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical. """ resp = self.client.get('/article-canonical/1-alpha/') self.assertEqual(resp.status_code, 200) resp = self.client.get('/article-canonical/2-zet/') self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ resp = self.client.get('/article-canonical/1-bad-slug/') self.assertEqual(resp.status_code, 301) resp = self.client.get('/article-canonical/2-bad-slug/') self.assertEqual(resp.status_code, 301) class TestNamespaceAwareCanonicalSlugDetailView(test.TestCase): def setUp(self): a1 = Article.objects.create(title='Alpha', body='Zet', slug='alpha') a2 = Article.objects.create(title='Zet', body='Alpha', slug='zet') def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical. """ resp = self.client.get('/article-canonical-namespaced/article/1-alpha/') self.assertEqual(resp.status_code, 200) resp = self.client.get('/article-canonical-namespaced/article/2-zet/') self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical and that the redirect is namespace aware. """ resp = self.client.get('/article-canonical-namespaced/article/1-bad-slug/') self.assertEqual(resp.status_code, 301) resp = self.client.get('/article-canonical-namespaced/article/2-bad-slug/') self.assertEqual(resp.status_code, 301) class TestOverriddenCanonicalSlugDetailView(test.TestCase): def setUp(self): a1 = Article.objects.create(title='Alpha', body='Zet', slug='alpha') a2 = Article.objects.create(title='Zet', body='Alpha', slug='zet') def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical according to the overridden canonical slug. """ resp = self.client.get('/article-canonical-override/1-nycun/') self.assertEqual(resp.status_code, 200) resp = self.client.get('/article-canonical-override/2-mrg/') self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ resp = self.client.get('/article-canonical-override/1-bad-slug/') self.assertEqual(resp.status_code, 301) resp = self.client.get('/article-canonical-override/2-bad-slug/') self.assertEqual(resp.status_code, 301) class TestModelCanonicalSlugDetailView(test.TestCase): def setUp(self): a1 = CanonicalArticle.objects.create(title='Alpha', body='Zet', slug='alpha') a2 = CanonicalArticle.objects.create(title='Zet', body='Alpha', slug='zet') def test_canonical_slug(self): """ Test that no redirect occurs when slug is canonical according to the model's canonical slug. """ resp = self.client.get('/article-canonical-model/1-unauthored-alpha/') self.assertEqual(resp.status_code, 200) resp = self.client.get('/article-canonical-model/2-unauthored-zet/') self.assertEqual(resp.status_code, 200) def test_non_canonical_slug(self): """ Test that a redirect occurs when the slug is non-canonical. """ resp = self.client.get('/article-canonical-model/1-bad-slug/') self.assertEqual(resp.status_code, 301) resp = self.client.get('/article-canonical-model/2-bad-slug/') self.assertEqual(resp.status_code, 301) class TestFormMessageMixins(test.TestCase): def setUp(self): self.good_data = { 'title': 'Good', 'body': 'Body' } self.bad_data = { 'body': 'Missing title' } def test_valid_message(self): url = '/form_messages/' response = self.client.get(url) self.assertEqual(response.status_code, 200) response = self.client.post(url, self.good_data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, FormMessagesView().form_valid_message) def test_invalid_message(self): url = '/form_messages/' response = self.client.get(url) self.assertEqual(response.status_code, 200) response = self.client.post(url, self.bad_data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, FormMessagesView().form_invalid_message) def test_form_valid_message_not_set(self): mixin = FormValidMessageMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_form_valid_message() def test_form_valid_message_not_str(self): mixin = FormValidMessageMixin() mixin.form_valid_message = ['bad'] with self.assertRaises(ImproperlyConfigured): mixin.get_form_valid_message() def test_form_valid_returns_message(self): mixin = FormValidMessageMixin() mixin.form_valid_message = u'Good øø' self.assertEqual(u'Good øø', mixin.get_form_valid_message()) def test_form_invalid_message_not_set(self): mixin = FormInvalidMessageMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_form_invalid_message() def test_form_invalid_message_not_str(self): mixin = FormInvalidMessageMixin() mixin.form_invalid_message = ['bad'] with self.assertRaises(ImproperlyConfigured): mixin.get_form_invalid_message() def test_form_invalid_returns_message(self): mixin = FormInvalidMessageMixin() mixin.form_invalid_message = u'Bad øø' self.assertEqual(u'Bad øø', mixin.get_form_invalid_message()) django-braces-1.3.1/tests/urls.py000066400000000000000000000064061226211642100167170ustar00rootroot00000000000000from . import views from .compat import patterns, include, url urlpatterns = patterns( '', # LoginRequiredMixin tests url(r'^login_required/$', views.LoginRequiredView.as_view()), # AjaxResponseMixin tests url(r'^ajax_response/$', views.AjaxResponseView.as_view()), # CreateAndRedirectToEditView tests url(r'^article/create/$', views.CreateArticleView.as_view()), url(r'^article/(?P\d+)/edit/$', views.EditArticleView.as_view(), name="edit_article"), url(r'^article_list/create/$', views.CreateArticleAndRedirectToListView.as_view()), url(r'^article_list_bad/create/$', views.CreateArticleAndRedirectToListViewBad.as_view()), url(r'^article_list/$', views.ArticleListView.as_view(), name='article_list'), # CanonicalSlugDetailMixin tests url(r'^article-canonical/(?P\d+)-(?P[-\w]+)/$', views.CanonicalSlugDetailView.as_view()), url(r'^article-canonical-namespaced/', include('tests.urls_namespaced', namespace='some_namespace')), url(r'^article-canonical-override/(?P\d+)-(?P[-\w]+)/$', views.OverriddenCanonicalSlugDetailView.as_view()), url(r'^article-canonical-model/(?P\d+)-(?P[-\w]+)/$', views.ModelCanonicalSlugDetailView.as_view()), # UserFormKwargsMixin tests url(r'^form_with_user_kwarg/$', views.FormWithUserKwargView.as_view()), # SetHeadlineMixin tests url(r'^headline/$', views.HeadlineView.as_view(), name='headline'), url(r'^headline/(?P[\w-]+)/$', views.DynamicHeadlineView.as_view()), # PermissionRequiredMixin tests url(r'^permission_required/$', views.PermissionRequiredView.as_view()), # MultiplePermissionsRequiredMixin tests url(r'^multiple_permissions_required/$', views.MultiplePermissionsRequiredView.as_view()), # SuperuserRequiredMixin tests url(r'^superuser_required/$', views.SuperuserRequiredView.as_view()), # StaffuserRequiredMixin tests url(r'^staffuser_required/$', views.StaffuserRequiredView.as_view()), # GroupRequiredMixin tests url(r'^group_required/$', views.GroupRequiredView.as_view()), # UserPassesTestMixin tests url(r'^user_passes_test/$', views.UserPassesTestView.as_view()), # UserPassesTestMixin tests url(r'^user_passes_test_not_implemented/$', views.UserPassesTestNotImplementedView.as_view()), # CsrfExemptMixin tests url(r'^csrf_exempt/$', views.CsrfExemptView.as_view()), # JSONResponseMixin tests url(r'^simple_json/$', views.SimpleJsonView.as_view()), url(r'^simple_json_400/$', views.SimpleJsonBadRequestView.as_view()), url(r'^article_list_json/$', views.ArticleListJsonView.as_view()), # JsonRequestResponseMixin tests url(r'^json_request/$', views.JsonRequestResponseView.as_view()), url(r'^json_bad_request/$', views.JsonBadRequestView.as_view()), url(r'^json_custom_bad_request/$', views.JsonCustomBadRequestView.as_view()), # FormMessagesMixin tests url(r'form_messages/$', views.FormMessagesView.as_view()), ) urlpatterns += patterns( 'django.contrib.auth.views', # login page, required by some tests url(r'^accounts/login/$', 'login', {'template_name': 'blank.html'}), url(r'^auth/login/$', 'login', {'template_name': 'blank.html'}), ) django-braces-1.3.1/tests/urls_namespaced.py000066400000000000000000000004221226211642100210670ustar00rootroot00000000000000from . import views from .compat import patterns, url urlpatterns = patterns( '', # CanonicalSlugDetailMixin namespace tests url(r'^article/(?P\d+)-(?P[\w-]+)/$', views.CanonicalSlugDetailView.as_view(), name="namespaced_article"), ) django-braces-1.3.1/tests/views.py000066400000000000000000000157611226211642100170730ustar00rootroot00000000000000import codecs from django.contrib.auth.models import User from django.http import HttpResponse from django.views.generic import (View, UpdateView, FormView, TemplateView, ListView, DetailView, CreateView) from braces import views from .models import Article, CanonicalArticle from .forms import ArticleForm, FormWithUserKwarg class OkView(View): """ A view which simply returns "OK" for every request. """ def get(self, request): return HttpResponse("OK") def post(self, request): return self.get(request) def put(self, request): return self.get(request) def delete(self, request): return self.get(request) class LoginRequiredView(views.LoginRequiredMixin, OkView): """ A view for testing LoginRequiredMixin. """ class AjaxResponseView(views.AjaxResponseMixin, OkView): """ A view for testing AjaxResponseMixin. """ def get_ajax(self, request): return HttpResponse("AJAX_OK") def post_ajax(self, request): return self.get_ajax(request) def put_ajax(self, request): return self.get_ajax(request) def delete_ajax(self, request): return self.get_ajax(request) class SimpleJsonView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's render_json_response() method. """ def get(self, request): object = {'username': request.user.username} return self.render_json_response(object) class SimpleJsonBadRequestView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's render_json_response() method with 400 HTTP status code. """ def get(self, request): object = {'username': request.user.username} return self.render_json_response(object, status=400) class ArticleListJsonView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's render_json_object_response() method. """ def get(self, request): queryset = Article.objects.all() return self.render_json_object_response( queryset, fields=('title',)) class JsonRequestResponseView(views.JsonRequestResponseMixin, View): """ A view for testing JsonRequestResponseMixin's json conversion """ def post(self, request): return self.render_json_response(self.request_json) class JsonBadRequestView(views.JsonRequestResponseMixin, View): """ A view for testing JsonRequestResponseMixin's require_json and render_bad_request_response methods """ require_json = True def post(self, request, *args, **kwargs): return self.render_json_response(self.request_json) class JsonCustomBadRequestView(views.JsonRequestResponseMixin, View): """ A view for testing JsonRequestResponseMixin's render_bad_request_response method with a custom error message """ def post(self, request, *args, **kwargs): if not self.request_json: return self.render_bad_request_response( {'error': 'you messed up'}) return self.render_json_response(self.request_json) class CreateArticleView(CreateView): """ View for testing CreateAndRedirectEditToView. """ model = Article template_name = 'form.html' class EditArticleView(UpdateView): """ View for testing CreateAndRedirectEditToView. """ model = Article template_name = 'form.html' class CreateArticleAndRedirectToListView(views.SuccessURLRedirectListMixin, CreateArticleView): """ View for testing SuccessURLRedirectListMixin """ success_list_url = 'article_list' class CreateArticleAndRedirectToListViewBad(views.SuccessURLRedirectListMixin, CreateArticleView): """ View for testing SuccessURLRedirectListMixin """ success_list_url = None class ArticleListView(views.SelectRelatedMixin, ListView): """ A list view for articles, required for testing SuccessURLRedirectListMixin. Also used to test SelectRelatedMixin. """ model = Article template_name = 'blank.html' select_related = ('author',) class FormWithUserKwargView(views.UserFormKwargsMixin, FormView): """ View for testing UserFormKwargsMixin. """ form_class = FormWithUserKwarg template_name = 'form.html' def form_valid(self, form): return HttpResponse("username: %s" % form.user.username) class HeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin. """ template_name = 'blank.html' headline = "Test headline" class DynamicHeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin's get_headline() method. """ template_name = 'blank.html' def get_headline(self): return self.kwargs['s'] class PermissionRequiredView(views.PermissionRequiredMixin, OkView): """ View for testing PermissionRequiredMixin. """ permission_required = 'auth.add_user' class MultiplePermissionsRequiredView(views.MultiplePermissionsRequiredMixin, OkView): permissions = { 'all': ['tests.add_article', 'tests.change_article'], 'any': ['auth.add_user', 'auth.change_user'], } class SuperuserRequiredView(views.SuperuserRequiredMixin, OkView): pass class StaffuserRequiredView(views.StaffuserRequiredMixin, OkView): pass class CsrfExemptView(views.CsrfExemptMixin, OkView): pass class AuthorDetailView(views.PrefetchRelatedMixin, ListView): model = User prefetch_related = ['article_set'] template_name = 'blank.html' class OrderableListView(views.OrderableListMixin, ListView): model = Article orderable_columns = ('id', 'title', ) orderable_columns_default = 'id' class CanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): model = Article template_name = 'blank.html' class OverriddenCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): model = Article template_name = 'blank.html' def get_canonical_slug(self): return codecs.encode(self.get_object().slug, 'rot_13') class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): model = CanonicalArticle template_name = 'blank.html' class FormMessagesView(views.FormMessagesMixin, CreateView): form_class = ArticleForm form_invalid_message = 'Invalid' form_valid_message = 'Valid' model = Article success_url = '/form_messages/' template_name = 'form.html' class GroupRequiredView(views.GroupRequiredMixin, OkView): group_required = 'test_group' class UserPassesTestView(views.UserPassesTestMixin, OkView): def test_func(self, user): return user.is_staff and not user.is_superuser \ and user.email.endswith('@mydomain.com') class UserPassesTestNotImplementedView(views.UserPassesTestMixin, OkView): pass django-braces-1.3.1/tox.ini000066400000000000000000000015401226211642100155230ustar00rootroot00000000000000[tox] envlist = py2.6-d1.4, py2.7-d1.4, py2.7-d1.5, py3.3-d1.5, py3.3-d1.6, py2.7-d1.6 install_command = pip install {opts} {packages} [testenv] commands = py.test tests/ [base] deps = mock pytest-django factory_boy [testenv:py2.6-d1.4] basepython = python2.6 deps = {[base]deps} django>=1.4,<1.5 [testenv:py2.7-d1.4] basepython = python2.7 deps = {[base]deps} django>=1.4,<1.5 [testenv:py2.7-d1.5] basepython = python2.7 deps = {[base]deps} django>=1.5,<1.6 [testenv:py3.2-d1.5] basepython = python3.2 deps = {[base]deps} django>=1.5,<1.6 [testenv:py3.3-d1.5] basepython = python3.3 deps = {[base]deps} django>=1.5,<1.6 [testenv:py3.3-d1.6] basepython = python3.3 deps = {[base]deps} django>=1.6,<1.7 [testenv:py2.7-d1.6] basepython = python2.7 deps = {[base]deps} django>=1.6,<1.7