././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262641.1077347 django_braces-1.17.0/0000775000175000017500000000000014766150561013460 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/CONTRIBUTORS.txt0000664000175000017500000000131714766146657016173 0ustar00kloveklove==== 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 * Jon Bolt * Kit Sunde * Ben Cardy * Rag Sagar.V * Lacey Williams Henschel * Gregory Shikhman * Mike Bryant * Fabio C. Barrionuevo da Luz * Sam Spencer * Ben Wilber * Mfon Eti-mfon * Irtaza Akram * Matthew Ethan Tam Other Contributors ================== * The entire Python and Django communities for providing us the tools and desire to build these things. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/LICENSE0000664000175000017500000000277614766144450014501 0ustar00klovekloveCopyright (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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/MANIFEST.in0000664000175000017500000000042314766144450015215 0ustar00klovekloveinclude README.md *.toml include LICENSE include CONTRIBUTORS.txt recursive-include braces *.py recursive-include docs Makefile conf.py *.rst recursive-include tests *.py recursive-include tests/templates *.html include conftest.py tox.ini requirements.txt prune docs/_build ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262641.1077347 django_braces-1.17.0/PKG-INFO0000644000175000017500000000324214766150561014554 0ustar00klovekloveMetadata-Version: 2.2 Name: django-braces Version: 1.17.0 Summary: Reusable, generic mixins for Django Home-page: https://github.com/brack3t/django-braces/ Author: Kenneth Love , Chris Jones Author-email: devs@brack3t.com License: BSD Keywords: django,views,forms,mixins Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.2 Classifier: Framework :: Django :: 4.0 Classifier: Framework :: Django :: 4.1 Classifier: Framework :: Django :: 4.2 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: Django>=2.2 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: requires-dist Dynamic: summary Mixins to add easy functionality to Django class-based views, forms, and models. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/README.md0000664000175000017500000000277314766144450014750 0ustar00kloveklove# django-braces `django-braces` provides useful Mixins for Django's class-based views. Most of these mixins replicate the behavior of Django's function-based view decorators. Others solve common headaches with working with class-based views. You can read more in [the documentation](https://django-braces.readthedocs.io/en/latest/index.html). ![Build](https://github.com/brack3t/django-braces/actions/workflows/ci.yml/badge.svg?branch=main) [![PyPI version](https://badge.fury.io/py/django-braces.svg)](http://badge.fury.io/py/django-braces) [![codecov](https://codecov.io/gh/brack3t/django-braces/branch/main/graph/badge.svg?token=aBhzbsyyTi)](https://codecov.io/gh/brack3t/django-braces) ## Notes `django-braces` is stable and time-tested. It does not receive a lot of updates and is not in active development. `django-braces` also only officially supports Python version that are still receiving fixes and Django LTS versions. `django-braces` will work with most modern version of Python and Django, however. ## Installation Install from PyPI with `pip`: `pip install django-braces` ## Contributing See our [contribution guide](https://django-braces.readthedocs.io/en/latest/contributing.html) Add yourself to `CONTRIBUTORS.txt` if you want. All development dependencies are available in `requirements.txt` file. To run the test suite, please install `pytest` and run `pytest` at the root of the repository. ## Change Log [Changelog on Read The Docs](https://django-braces.readthedocs.io/en/latest/changelog.html) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262638.4677434 django_braces-1.17.0/braces/0000775000175000017500000000000014766150556014723 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261802.0 django_braces-1.17.0/braces/__init__.py0000664000175000017500000000063514766147052017035 0ustar00kloveklove""" django-braces mixins library ---------------------------- Mixins to make Django's generic class-based views simpler. :copyright: (c) 2013 by Kenneth Love and Chris Jones :license: BSD 3-clause. See LICENSE for more details """ __title__ = "braces" __version__ = "1.17.0" __author__ = "Kenneth Love and Chris Jones" __license__ = "BSD 3-clause" __copyright__ = "Copyright 2013 Kenneth Love and Chris Jones" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/forms.py0000664000175000017500000000102014766144450016410 0ustar00klovekloveclass UserKwargModelFormMixin: """ 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): """Remove the user from **kwargs and assign it on the object""" self.user = kwargs.pop("user", None) super().__init__(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262639.1517413 django_braces-1.17.0/braces/views/0000775000175000017500000000000014766150557016061 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/views/__init__.py0000664000175000017500000000335614766144450020174 0ustar00kloveklovefrom ._access import ( AnonymousRequiredMixin, GroupRequiredMixin, LoginRequiredMixin, MultiplePermissionsRequiredMixin, PermissionRequiredMixin, StaffuserRequiredMixin, SuperuserRequiredMixin, UserPassesTestMixin, SSLRequiredMixin, RecentLoginRequiredMixin, ) from ._ajax import ( AjaxResponseMixin, JSONRequestResponseMixin, JSONResponseMixin, JsonRequestResponseMixin, ) from ._forms import ( CsrfExemptMixin, FormInvalidMessageMixin, FormMessagesMixin, FormValidMessageMixin, MessageMixin, SuccessURLRedirectListMixin, UserFormKwargsMixin, ) from ._other import ( AllVerbsMixin, CanonicalSlugDetailMixin, SetHeadlineMixin, StaticContextMixin, HeaderMixin, CacheControlMixin, NeverCacheMixin ) from ._queries import ( OrderableListMixin, PrefetchRelatedMixin, SelectRelatedMixin, ) __all__ = [ "AjaxResponseMixin", "AllVerbsMixin", "AnonymousRequiredMixin", "CacheControlMixin", "CanonicalSlugDetailMixin", "CsrfExemptMixin", "FormInvalidMessageMixin", "FormMessagesMixin", "FormValidMessageMixin", "GroupRequiredMixin", "HeaderMixin", "JSONRequestResponseMixin", "JsonRequestResponseMixin", "JSONResponseMixin", "LoginRequiredMixin", "MessageMixin", "MultiplePermissionsRequiredMixin", "NeverCacheMixin", "OrderableListMixin", "PermissionRequiredMixin", "PrefetchRelatedMixin", "SelectRelatedMixin", "SetHeadlineMixin", "StaffuserRequiredMixin", "StaticContextMixin", "SuccessURLRedirectListMixin", "SuperuserRequiredMixin", "UserFormKwargsMixin", "UserPassesTestMixin", "SSLRequiredMixin", "RecentLoginRequiredMixin", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/braces/views/_access.py0000664000175000017500000004104714766146657020047 0ustar00klovekloveimport inspect import datetime import urllib.parse from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login, logout_then_login from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.http import ( HttpResponseRedirect, HttpResponsePermanentRedirect, Http404, HttpResponse, StreamingHttpResponse, ) from django.shortcuts import resolve_url from django.utils.encoding import force_str from django.utils.timezone import now class AccessMixin: """ Base access mixin. All other access mixins should extend this one. """ login_url = None raise_exception = False redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth redirect_unauthenticated_users = False def __init__(self, *args, **kwargs): self._class_name = self.__class__.__name__ super().__init__(*args, **kwargs) 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( f"Define {self._class_name}.login_url or settings.LOGIN_URL or " f"override {self._class_name}.get_login_url()." ) return force_str(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( f"{self._class_name} is missing the redirect_field_name. " f"Define {self._class_name}.redirect_field_name or " f"override {self._class_name}.get_redirect_field_name()." ) return self.redirect_field_name def handle_no_permission(self, request): """What should happen if the user doesn't have permission?""" if self.raise_exception: if ( self.redirect_unauthenticated_users and not request.user.is_authenticated ): return self.no_permissions_fail(request) else: if inspect.isclass(self.raise_exception) and issubclass( self.raise_exception, Exception ): raise self.raise_exception if callable(self.raise_exception): ret = self.raise_exception(request) if isinstance(ret, (HttpResponse, StreamingHttpResponse)): return ret raise PermissionDenied return self.no_permissions_fail(request) def no_permissions_fail(self, request=None): """ Called when the user has no permissions and no exception was raised. This should only return a valid HTTP response. By default we redirect to login. """ return redirect_to_login( request.get_full_path(), self.get_login_url(), self.get_redirect_field_name(), ) class LoginRequiredMixin(AccessMixin): """ Requires the user to be 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): """Call the appropriate method after checking authentication""" if not request.user.is_authenticated: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class AnonymousRequiredMixin(AccessMixin): """ Requires the user to be unauthenticated. NOTE: This should be the left-most mixin of a view. ## Example Usage class SomeView(AnonymousRequiredMixin, ListView): ... # required authenticated_redirect_url = "/accounts/profile/" ... """ authenticated_redirect_url = settings.LOGIN_REDIRECT_URL def dispatch(self, request, *args, **kwargs): """Call the appropriate handler after guaranteeing anonymity""" if request.user.is_authenticated: return HttpResponseRedirect(self.get_authenticated_redirect_url()) return super().dispatch(request, *args, **kwargs) def get_authenticated_redirect_url(self): """Return the reversed authenticated redirect url.""" if not self.authenticated_redirect_url: raise ImproperlyConfigured( f"{self._class_name} is missing an authenticated_redirect_url " f"url to redirect to. Define {self._class_name}.authenticated_redirect_url " f"or override {self._class_name}.get_authenticated_redirect_url()." ) return resolve_url(self.authenticated_redirect_url) class PermissionRequiredMixin(AccessMixin): """ The request users must have certain permission(s) ## Attributes `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 # No permissions are required by default object_level_permissions = False def get_permission_required(self, request=None): """ Get the required permissions and return them. Override this to allow for custom permission_required values. """ # 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( f'{self._class_name} requires the "permission_required" ' "attribute to be set." ) return self.permission_required def check_permissions(self, request): """ Returns whether or not the user has permissions """ perms = self.get_permission_required(request) has_permission = False if self.object_level_permissions: if hasattr(self, "object") and self.object is not None: has_permission = request.user.has_perm( perms, self.object ) elif hasattr(self, "get_object") and callable(self.get_object): has_permission = request.user.has_perm( perms, self.get_object() ) else: has_permission = request.user.has_perm(perms) return has_permission def dispatch(self, request, *args, **kwargs): """ Check to see if the user in the request has the required permission. """ has_permission = self.check_permissions(request) if not has_permission: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class MultiplePermissionsRequiredMixin(PermissionRequiredMixin): """ 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 list. By specifying the `any` key , the user must have at least one of the permissions in the list. Class Settings `permissions` - This is required to be a dict with one or both keys of `all` and `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 get_permission_required(self, request=None): """Get which permission is required""" self._check_permissions_attr() return self.permissions def check_permissions(self, request): """Get the permissions, both all and any.""" permissions = self.get_permission_required(request) perms_all = permissions.get("all") perms_any = permissions.get("any") instance_object = 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 self.object_level_permissions: if hasattr(self, "object") and self.object is not None: instance_object = self.object elif hasattr(self, "get_object") and callable(self.get_object): instance_object = self.get_object() # Check that user has all permissions in the list/tuple if perms_all: # Why not `return request.user.has_perms(perms_all)`? # There may be optional permissions below. if not request.user.has_perms(perms_all, instance_object): return False # If perms_any, check that user has at least one in the list/tuple if perms_any: any_perms = [ request.user.has_perm(perm, instance_object) for perm in perms_any ] if not any_perms or not any(any_perms): return False return True 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( f'{self._class_name} requires the `permissions` attribute' "to be set as 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( f'{self._class_name} requires the `permissions` attribute to ' f"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( f"{self._class_name} requires the permissions dict {key} value " "to be a list or tuple." ) class GroupRequiredMixin(AccessMixin): group_required = None def get_group_required(self): """Get which group's membership is required""" if any([ self.group_required is None, not isinstance(self.group_required, (list, tuple, str)) ]): raise ImproperlyConfigured( f'{self._class_name} requires the `group_required` attribute ' "to be set and be a string, list, or tuple." ) if not isinstance(self.group_required, (list, tuple)): self.group_required = (self.group_required,) return self.group_required def check_membership(self, groups): """Check for user's membership in required groups. Superusers are automatically members""" if self.request.user.is_superuser: return True user_groups = self.request.user.groups.values_list("name", flat=True) return set(groups).intersection(set(user_groups)) def dispatch(self, request, *args, **kwargs): """Call the appropriate handler if the user is a group member""" self.request = request in_group = False if request.user.is_authenticated: in_group = self.check_membership(self.get_group_required()) if not in_group: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class UserPassesTestMixin(AccessMixin): """ User must pass a test before being allowed access to the 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): """The function to test the user with""" raise NotImplementedError( f"{self._class_name} is missing implementation of the " "`test_func` method. A function to test the user is required." ) def get_test_func(self): """Get the test function""" return getattr(self, "test_func") def dispatch(self, request, *args, **kwargs): """Call the appropriate handler if the users passes the test""" user_test_result = self.get_test_func()(request.user) if not user_test_result: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class SuperuserRequiredMixin(AccessMixin): """ Require users to be superusers to access the view. """ def dispatch(self, request, *args, **kwargs): """Call the appropriate handler if the user is a superuser""" if not request.user.is_superuser: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class StaffuserRequiredMixin(AccessMixin): """ Require users to be marked as staff to access the view. """ def dispatch(self, request, *args, **kwargs): """Call the appropriate handler if the user is a staff member""" if not request.user.is_staff: return self.handle_no_permission(request) return super().dispatch(request, *args, **kwargs) class SSLRequiredMixin: """ Require requests to be made over a secure connection. """ raise_exception = False def dispatch(self, request, *args, **kwargs): """Call the appropriate handler if the connection is secure""" if getattr(settings, "DEBUG", False): # Don't enforce the check during development return super().dispatch(request, *args, **kwargs) if not request.is_secure(): if self.raise_exception: raise Http404 return HttpResponsePermanentRedirect( self._build_https_url(request) ) return super(SSLRequiredMixin, self).dispatch(request, *args, **kwargs) def _build_https_url(self, request): """Get the full url, replace http with https""" url = request.build_absolute_uri(request.get_full_path()) return urllib.parse.urlunsplit( ("https",)+urllib.parse.urlsplit(url)[1:] ) class RecentLoginRequiredMixin(LoginRequiredMixin): """ Require the user to have logged in within a number of seconds. """ max_last_login_delta = 1800 # Defaults to 30 minutes def dispatch(self, request, *args, **kwargs): """Call the appropriate method if the user's login is recent""" resp = super().dispatch(request, *args, **kwargs) if resp.status_code == 200: delta = datetime.timedelta(seconds=self.max_last_login_delta) if now() > (request.user.last_login + delta): return logout_then_login(request, self.get_login_url()) return resp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/views/_ajax.py0000664000175000017500000001407614766144450017520 0ustar00klovekloveimport json from django.core import serializers from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse class JSONResponseMixin: """ Basic serialized responses. For anything more complicated than basic Python types or Django models, please use something like django-rest-framework. """ content_type = None json_dumps_kwargs = None json_encoder_class = None def get_content_type(self): """Get the appropriate content type for the response""" if self.content_type is not None and not isinstance( self.content_type, str ): class_name = self.__class__.__name__ raise ImproperlyConfigured( f"{class_name} is missing a content type. Define {class_name}" ".content_type or override {class_name}.get_content_type()." ) return self.content_type or "application/json" def get_json_dumps_kwargs(self): """Get kwargs for custom JSON compilation""" dumps_kwargs = getattr(self, "json_dumps_kwargs", None) or {} dumps_kwargs.setdefault("ensure_ascii", False) return dumps_kwargs def get_json_encoder_class(self): """Get the encoder class to use""" if self.json_encoder_class is None: self.json_encoder_class = DjangoJSONEncoder return self.json_encoder_class def render_json_response(self, context_dict, status=200): """ Limited serialization for shipping plain data. Do not use for models or other complex objects. """ response = JsonResponse( data=context_dict, safe=False, encoder=self.get_json_encoder_class(), json_dumps_params=self.get_json_dumps_kwargs(), content_type=self.get_content_type(), status=status ) return response 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. """ try: response = self.render_json_response(objects, **kwargs) except TypeError: json_data = serializers.serialize("json", objects, **kwargs) response = HttpResponse(json_data, content_type=self.get_content_type()) return response class AjaxResponseMixin: """ 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): """Call the appropriate handler method""" if all([ request.headers.get("x-requested-with") == "XMLHttpRequest", request.method.lower() in self.http_method_names ]): handler = getattr( self, f"{request.method.lower()}_ajax", self.http_method_not_allowed, ) self.request = request self.args = args self.kwargs = kwargs return handler(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs) def get_ajax(self, request, *args, **kwargs): """Handle a GET request made with AJAX""" return self.get(request, *args, **kwargs) def post_ajax(self, request, *args, **kwargs): """Handle a POST request made with AJAX""" return self.post(request, *args, **kwargs) def put_ajax(self, request, *args, **kwargs): """Handle a PUT request made with AJAX""" return self.get(request, *args, **kwargs) def delete_ajax(self, request, *args, **kwargs): """Handle a DELETE request made with AJAX""" return self.get(request, *args, **kwargs) class JsonRequestResponseMixin(JSONResponseMixin): """ Attempt to parse the request body as JSON. If successful, self.request_json will contain the deserialized object. Otherwise, self.request_json will be None. 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 = {"errors": ["Improperly formatted request"]} def render_bad_request_response(self, error_dict=None): """Generate errors for bad content""" if error_dict is None: error_dict = self.error_response_dict json_context = json.dumps( error_dict, cls=self.get_json_encoder_class(), **self.get_json_dumps_kwargs() ).encode("utf-8") return HttpResponseBadRequest( json_context, content_type=self.get_content_type() ) def get_request_json(self): """Get the JSON included in the body""" try: return json.loads(self.request.body.decode("utf-8")) except (json.JSONDecodeError, ValueError): return None def dispatch(self, request, *args, **kwargs): """Trigger the appropriate method""" self.request = request self.args = args self.kwargs = kwargs self.request_json = self.get_request_json() if all( [ request.method != "OPTIONS", self.require_json, self.request_json is None, ] ): return self.render_bad_request_response() return super().dispatch(request, *args, **kwargs) class JSONRequestResponseMixin(JsonRequestResponseMixin): """Convenience alias""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/views/_forms.py0000664000175000017500000001351514766144450017720 0ustar00kloveklovefrom functools import partial from django.contrib import messages from django.core.exceptions import ImproperlyConfigured from django.utils.decorators import method_decorator from django.utils.encoding import force_str from django.utils.functional import Promise from django.views.decorators.csrf import csrf_exempt from django.urls import reverse class CsrfExemptMixin: """ 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().dispatch(*args, **kwargs) class UserFormKwargsMixin: """ Automatically include `request.user` in form kwargs. ## Note You will need to handle the `user` kwarg in your form. Usually this means `user = kwargs.pop("user")` in your form's `__init__`. """ def get_form_kwargs(self): kwargs = super().get_form_kwargs() # Update the existing form kwargs dict with the request's user. kwargs.update({"user": self.request.user}) return kwargs class SuccessURLRedirectListMixin: """ Automatically reverses `success_list_url` and returns that as the `success_url` for a form view. This is meant to redirect to a view without arguments. If you need to include arguments to `reverse`, you can omit this mixin. """ 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: class_name = self.__class__.__name__ raise ImproperlyConfigured( f"{class_name} is missing a success_list_url attribute. " f"Define {class_name}.success_list_url or override " f"{class_name}.get_success_url()." ) return reverse(self.success_list_url) class _MessageAPIWrapper: """ Wrapper for the django.contrib.messages.api module. Automatically pass a request object as the first parameter of message function calls. """ API = set( [ "add_message", "get_messages", "get_level", "set_level", "debug", "info", "success", "warning", "error", ] ) def __init__(self, request): for name in self.API: api_fn = getattr(messages.api, name) setattr(self, name, partial(api_fn, request)) class _MessageDescriptor: """ A descriptor that binds the _MessageAPIWrapper to the view's request. """ def __get__(self, instance, *args, **kwargs): return _MessageAPIWrapper(instance.request) class MessageMixin: """ Add a `messages` attribute on the view instance that wraps `django.contrib.messages`, automatically passing the current request object. """ messages = _MessageDescriptor() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._class_name = self.__class__.__name__ class FormValidMessageMixin(MessageMixin): """ Set a string to be sent via Django's messages framework when a form passes validation. """ form_valid_message = None # Default to None def get_form_valid_message(self): """ Validate that form_valid_message is set correctly """ if self.form_valid_message is None: raise ImproperlyConfigured( f"{self._class_name}.form_valid_message is not set. Define " f"{self._class_name}.form_valid_message, or override " f"{self._class_name}.get_form_valid_message()." ) if not isinstance(self.form_valid_message, (str, Promise)): raise ImproperlyConfigured( f"{self._class_name}.form_valid_message must be a str or Promise." ) return force_str(self.form_valid_message) def form_valid(self, form): """ Set the "form valid" message for standard form validation """ response = super().form_valid(form) self.messages.success( self.get_form_valid_message(), fail_silently=True ) return response def delete(self, *args, **kwargs): """ Set the "form valid" message for delete form validation """ response = super().delete(*args, **kwargs) self.messages.success( self.get_form_valid_message(), fail_silently=True ) return response class FormInvalidMessageMixin(MessageMixin): """ Set a string to be sent via Django's messages framework when a form fails validation. """ form_invalid_message = None def get_form_invalid_message(self): """ Validate that form_invalid_message is set correctly. """ if self.form_invalid_message is None: raise ImproperlyConfigured( f"{self._class_name}.form_invalid_message is not set. Define " f"{self._class_name}.form_invalid_message, or override " f"{self._class_name}.get_form_invalid_message()." ) if not isinstance(self.form_invalid_message, (str, Promise)): raise ImproperlyConfigured( f"{self._class_name}.form_invalid_message must be a str or Promise." ) return force_str(self.form_invalid_message) def form_invalid(self, form): """ Set the "form invalid" message for standard form validation """ response = super().form_invalid(form) self.messages.error( self.get_form_invalid_message(), fail_silently=True ) return response class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): """ Set messages to be sent whether a form is valid or invalid. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/views/_other.py0000664000175000017500000001563114766144450017714 0ustar00kloveklovefrom django.core.exceptions import ImproperlyConfigured from django.shortcuts import redirect from django.views.decorators.cache import cache_control, never_cache from django.urls import resolve from django.utils.encoding import force_str from django.urls import resolve class SetHeadlineMixin: """ Define a `headline` context item as a view attribute """ headline = None # Default the headline to none def get_context_data(self, **kwargs): """Add the headline to the context""" kwargs = super().get_context_data(**kwargs) kwargs.update({"headline": self.get_headline()}) return kwargs def get_headline(self): """Fetch the headline from the instance""" if self.headline is None: class_name = self.__class__.__name__ raise ImproperlyConfigured( f"{class_name} is missing the headline attribute. " f"Define {class_name}.headline, or override {class_name}.get_headline()." ) return force_str(self.headline) class StaticContextMixin: """ Set static context items via an attribute on the view. """ static_context = None def get_context_data(self, **kwargs): """Update the context to include the static content""" kwargs = super().get_context_data(**kwargs) try: kwargs.update(self.get_static_context()) except (TypeError, ValueError): raise ImproperlyConfigured( f"{self.__class__.__name__}.static_context must be a " "dictionary or a series of two-tuples." ) else: return kwargs def get_static_context(self): """Fetch the static content from the view""" if self.static_context is None: class_name = self.__class__.__name__ raise ImproperlyConfigured( f"{class_name} is missing the static_context attribute. Define " f"{class_name}.static_context, or override {class_name}.get_static_context()" ) return self.static_context class CanonicalSlugDetailMixin: """ Enforce a canonical slug in the URL. If a URL 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): """ Redirect to the appropriate URL if necessary. Otherwise, trigger HTTP-method-appropriate handler. """ self.request = request self.args = args self.kwargs = kwargs # Get the current object, url slug, and url name. 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) # Find the canonical slug for the object if hasattr(obj, "get_canonical_slug"): canonical_slug = obj.get_canonical_slug() else: canonical_slug = self.get_canonical_slug() # Redirect if current slug is not the canonical one if canonical_slug != slug: params = { self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: canonical_slug, "permanent": True, } return redirect(current_urlpattern, **params) return super().dispatch(request, *args, **kwargs) def get_canonical_slug(self): """ Provide a method to return the correct slug for this object. """ return self.get_object().slug class AllVerbsMixin: """ Call a single method for all HTTP verbs. The name of the method should be specified using the class attribute `all_handler`. The default value of this attribute is 'all'. """ all_handler = "all" def dispatch(self, request, *args, **kwargs): """Call the all handler""" if not self.all_handler: raise ImproperlyConfigured( f"{self.__class__.__name__} requires the all_handler attribute to be set." ) handler = getattr(self, self.all_handler, self.http_method_not_allowed) return handler(request, *args, **kwargs) class HeaderMixin: """ Add extra HTTP headers to a response by specifying them in the ``headers`` attribute or by overriding the ``get_headers()`` method. """ headers = {} def get_headers(self, request): return self.headers def dispatch(self, request, *args, **kwargs): """ Override this method to customize the way additional headers are retrieved. It is mandatory that the returned value supports the ``.items()`` method. """ response = super().dispatch(request, *args, **kwargs) for key, value in self.get_headers(request).items(): if key not in response: response[key] = value return response class CacheControlMixin: """ Mixin that allows setting Cache-Control options. Specify Cache-Control options as class attributes on the view class. Cache-Control directive explanations: http://condor.depaul.edu/dmumaugh/readings/handouts/SE435/HTTP/node24.html Django's ``django.views.decorators.cache.cache_control`` options: https://docs.djangoproject.com/en/dev/topics/cache/#controlling-cache-using-other-headers """ # These are all ``None``, which indicates unset. cachecontrol_public = None cachecontrol_private = None cachecontrol_no_cache = None cachecontrol_no_transform = None cachecontrol_must_revalidate = None cachecontrol_proxy_revalidate = None cachecontrol_max_age = None cachecontrol_s_maxage = None @classmethod def get_cachecontrol_options(cls): """Compile a dictionary of selected cache options""" opts = ( 'public', 'private', 'no_cache', 'no_transform', 'must_revalidate', 'proxy_revalidate', 'max_age', 's_maxage' ) options = {} for opt in opts: value = getattr(cls, f'cachecontrol_{opt}', None) if value is not None: options[opt] = value return options @classmethod def as_view(cls, *args, **kwargs): """Wrap the view with appropriate cache controls""" view_func = super().as_view(*args, **kwargs) options = cls.get_cachecontrol_options() return cache_control(**options)(view_func) class NeverCacheMixin: """ Mixin that applies Django's `never_cache` view decorator to prevent upstream HTTP-based caching. """ @classmethod def as_view(cls, *args, **kwargs): """ Wrap the view with the `never_cache` decorator. """ view_func = super().as_view(*args, **kwargs) return never_cache(view_func) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/braces/views/_queries.py0000664000175000017500000001176314766144450020252 0ustar00klovekloveimport warnings from django.core.exceptions import ImproperlyConfigured class SelectRelatedMixin: """ Automatically apply `select_related` for a list of relations. """ select_related = None # Default related fields to none def get_queryset(self): """Apply select_related, with appropriate fields, to the queryset""" if self.select_related is None: # If no fields were provided, raise a configuration error raise ImproperlyConfigured( f"{self.__class__.__name__} is missing the select_related attribute." ) 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( f"{self.__class__.__name__}'s select_related property must be " "a tuple or list." ) # Get the current queryset of the view queryset = super().get_queryset() if not self.select_related: warnings.warn("The select_related attribute is empty") return queryset return queryset.select_related(*self.select_related) class PrefetchRelatedMixin: """ Automatically apply `prefetch_related` for a list of relations. """ prefetch_related = None # Default prefetch fields to none def get_queryset(self): """Apply prefetch_related, with appropriate fields, to the queryset""" if self.prefetch_related is None: # If no fields were provided, raise a configuration error raise ImproperlyConfigured( f"{self.__class__.__name__} is missing the prefetch_related attribute." ) if not isinstance(self.prefetch_related, (tuple, list)): # If the prefetch_related argument is *not* a tuple or list, # raise a configuration error. raise ImproperlyConfigured( f"{self.__class__.__name__}'s prefetch_related property must be a tuple or list." ) # Get the current queryset of the view queryset = super().get_queryset() if not self.prefetch_related: warnings.warn("The prefetch_related attribute is empty") return queryset return queryset.prefetch_related(*self.prefetch_related) class OrderableListMixin: """ Order the queryset based on GET parameters. """ orderable_columns = None orderable_columns_default = None ordering_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().get_context_data(**kwargs) context["order_by"] = self.order_by context["ordering"] = self.ordering return context def get_orderable_columns(self): """Check that the orderable columns are set and return them""" if not self.orderable_columns: raise ImproperlyConfigured( f"{self.__class__.__name__} needs the ordering columns defined." ) return self.orderable_columns def get_orderable_columns_default(self): """Which column(s) should be sorted by, by default?""" if not self.orderable_columns_default: raise ImproperlyConfigured( f"{self.__class__.__name__} needs the default ordering column defined." ) return self.orderable_columns_default def get_ordering_default(self): """Which direction should things be sorted?""" if not self.ordering_default: return "asc" else: if self.ordering_default not in ["asc", "desc"]: raise ImproperlyConfigured( f"{self.__class__.__name__} only allows asc or desc as ordering option" ) return self.ordering_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 = self.get_ordering_default() if all([order_by, self.request.GET.get("ordering", self.ordering) == "desc" ]): order_by = f"-{order_by}" self.ordering = self.request.GET.get("ordering", self.ordering) return queryset.order_by(order_by) def get_queryset(self): """ Returns ordered ``QuerySet`` """ unordered_queryset = super().get_queryset() return self.get_ordered_queryset(unordered_queryset) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/conftest.py0000664000175000017500000000041614766144450015660 0ustar00klovekloveimport os from django.conf import settings from tests import settings as test_settings def pytest_configure(): """Setup Django settings""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") settings.configure(default_settings=test_settings) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262641.1077347 django_braces-1.17.0/django_braces.egg-info/0000775000175000017500000000000014766150561017733 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262636.0 django_braces-1.17.0/django_braces.egg-info/PKG-INFO0000644000175000017500000000324214766150554021031 0ustar00klovekloveMetadata-Version: 2.2 Name: django-braces Version: 1.17.0 Summary: Reusable, generic mixins for Django Home-page: https://github.com/brack3t/django-braces/ Author: Kenneth Love , Chris Jones Author-email: devs@brack3t.com License: BSD Keywords: django,views,forms,mixins Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 6 - Mature Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 2.2 Classifier: Framework :: Django :: 3.2 Classifier: Framework :: Django :: 4.0 Classifier: Framework :: Django :: 4.1 Classifier: Framework :: Django :: 4.2 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: Django>=2.2 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: requires-dist Dynamic: summary Mixins to add easy functionality to Django class-based views, forms, and models. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262637.0 django_braces-1.17.0/django_braces.egg-info/SOURCES.txt0000664000175000017500000000176214766150555021630 0ustar00klovekloveCONTRIBUTORS.txt LICENSE MANIFEST.in README.md conftest.py pyproject.toml requirements.txt setup.cfg setup.py braces/__init__.py braces/forms.py braces/views/__init__.py braces/views/_access.py braces/views/_ajax.py braces/views/_forms.py braces/views/_other.py braces/views/_queries.py django_braces.egg-info/PKG-INFO django_braces.egg-info/SOURCES.txt django_braces.egg-info/dependency_links.txt django_braces.egg-info/not-zip-safe django_braces.egg-info/requires.txt django_braces.egg-info/top_level.txt docs/Makefile docs/access.rst docs/changelog.rst docs/conf.py docs/contributing.rst docs/form.rst docs/index.rst docs/other.rst tests/__init__.py tests/backends.py tests/factories.py tests/forms.py tests/helpers.py tests/models.py tests/settings.py tests/test_access_mixins.py tests/test_ajax_mixins.py tests/test_forms.py tests/test_other_mixins.py tests/test_queries.py tests/urls.py tests/urls_namespaced.py tests/views.py tests/templates/404.html tests/templates/blank.html tests/templates/form.html././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262637.0 django_braces-1.17.0/django_braces.egg-info/dependency_links.txt0000664000175000017500000000000114766150555024004 0ustar00kloveklove ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260776.0 django_braces-1.17.0/django_braces.egg-info/not-zip-safe0000664000175000017500000000000114766145050022156 0ustar00kloveklove ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262637.0 django_braces-1.17.0/django_braces.egg-info/requires.txt0000664000175000017500000000001414766150555022331 0ustar00klovekloveDjango>=2.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262637.0 django_braces-1.17.0/django_braces.egg-info/top_level.txt0000664000175000017500000000000714766150555022465 0ustar00kloveklovebraces ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262639.8917387 django_braces-1.17.0/docs/0000775000175000017500000000000014766150560014407 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/Makefile0000664000175000017500000001273014766144450016053 0ustar00kloveklove# 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." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/docs/access.rst0000664000175000017500000003056014766146657016422 0ustar00klovekloveAccess Mixins ============= These mixins all control a user's access to a given view. Since many of them extend the ``AccessMixin``, the following are common attributes: :: login_url = settings.LOGIN_URL raise_exception = False redirect_field_name = REDIRECT_FIELD_NAME redirect_unauthenticated_users = False The ``raise_exception`` attribute allows for these scenarios, in case a permission is denied: * ``False`` (default): redirects to the provided login view. * ``True``: raises a ``PermissionDenied`` exception. * A subclass of ``Exception``: raises this exception. * A callable: gets called with the ``request`` argument. The function has to return a ``HttpResponse`` or ``StreamingHttpResponse``, otherwise a ``PermissionDenied`` exception gets raised. This gets done in ``handle_no_permission``, which can be overridden itself. .. contents:: .. _LoginRequiredMixin: LoginRequiredMixin ------------------ This mixin is rather simple and is generally the first inherited class in any view. If you don't have an authenticated user, there's no need to go any further. If you've used Django before you are probably familiar with the ``login_required`` decorator. This mixin replicates the decorator's functionality. .. 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({}) An optional class attribute of ``redirect_unauthenticated_users`` can be set to ``True`` if you are using another ``access`` mixin with ``raise_exception`` set to ``True``. This will redirect to the login page if the user is not authenticated, but raises an exception if they are but do not have the required access defined by the other mixins. This defaults to ``False``. .. _PermissionRequiredMixin: PermissionRequiredMixin ----------------------- This mixin was originally written by `Daniel Sokolowski`_ (`code here`_), but this version eliminates 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, use this mixin and set the ``permission_required`` class attribute on your view. If you don't specify ``permission_required`` on your view, an ``ImproperlyConfigured`` exception is raised reminding you that you haven't set it. If you need to enforce permissions against a given object, set the ``object_level_permissions`` attribute to ``True``. The one limitation of this mixin is that it can **only** accept a single permission. If you need multiple permissions use :ref:`MultiplePermissionsRequiredMixin`. In normal use of this mixin, :ref:`LoginRequiredMixin` comes first, then the ``PermissionRequiredMixin``. If the user isn't an authenticated user, there is no point in checking for any permissions. .. note:: If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. :: from django.views.generic import TemplateView from braces import views class SomeProtectedView(views.LoginRequiredMixin, views.PermissionRequiredMixin, TemplateView): permission_required = "auth.change_user" template_name = "path/to/template.html" The ``PermissionRequiredMixin`` also offers a ``check_permissions`` method that should be overridden if you need custom permissions checking. .. _MultiplePermissionsRequiredMixin: MultiplePermissionsRequiredMixin -------------------------------- The ``MultiplePermissionsRequiredMixin`` is a more powerful version of the :ref:`PermissionRequiredMixin`. This view mixin can handle multiple permissions by setting the mandatory ``permissions`` attribute as a dict with the keys ``any`` and/or ``all`` to a list or tuple of permissions. The ``all`` key requires the ``request.user`` to have **all** of the specified permissions. The ``any`` key requires the ``request.user`` to have **at least one** of the specified permissions. If you only need to check a single permission, the :ref:`PermissionRequiredMixin` is a better choice. .. note:: If you are using Django's built in auth system, ``superusers`` automatically have all permissions in your system. :: from django.views.generic import TemplateView from braces import views class SomeProtectedView(views.LoginRequiredMixin, views.MultiplePermissionsRequiredMixin, TemplateView): #required permissions = { "all": ("blog.add_post", "blog.change_post"), "any": ("blog.delete_post", "user.change_user") } The ``MultiplePermissionsRequiredMixin`` also offers a ``check_permissions`` method that should be overridden if you need custom permissions checking. Additionally similar to ``PermissionRequiredMixin``, ``MultiplePermissionsRequiredMixin`` offers object level permission checking. .. _GroupRequiredMixin: GroupRequiredMixin ------------------ .. versionadded:: 1.2 The ``GroupRequiredMixin`` ensures that the requesting user is in the group or groups specified. This view mixin can handle multiple groups by setting the mandatory ``group_required`` attribute as a list or tuple. .. 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 django.views.generic import TemplateView from braces.views import GroupRequiredMixin class SomeProtectedView(GroupRequiredMixin, TemplateView): #required group_required = u"editors" Multiple Groups Possible Usage ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import GroupRequiredMixin class SomeProtectedView(GroupRequiredMixin, TemplateView): #required group_required = [u"editors", u"admins"] Custom Group Usage ^^^^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView 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 Dynamically Build Groups ^^^^^^^^^^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import GroupRequiredMixin class SomeProtectedView(GroupRequiredMixin, TemplateView): def get_group_required(self): # Get group or groups however you wish group = 'secret_group' return group .. _UserPassesTestMixin: UserPassesTestMixin ------------------- .. versionadded:: 1.3.0 Mixin that reimplements the `user_passes_test`_ decorator. This is helpful for much more complicated cases than checking if user ``is_superuser`` (for example if their email is from a specific domain). :: from django.views.generic import TemplateView 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(u"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 django.views.generic import TemplateView from braces import views class SomeSuperuserView(views.LoginRequiredMixin, views.SuperuserRequiredMixin, TemplateView): template_name = u"path/to/template.html" .. _AnonymousRequiredMixin: AnonymousRequiredMixin ---------------------- .. versionadded:: 1.4.0 Mixin that will redirect authenticated users to a different view. The default redirect is to Django's `settings.LOGIN_REDIRECT_URL`_. Static Examples ^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import AnonymousRequiredMixin class SomeView(AnonymousRequiredMixin, TemplateView): authenticated_redirect_url = u"/send/away/" :: from django.core.urlresolvers import reverse_lazy from django.views.generic import TemplateView from braces.views import AnonymousRequiredMixin class SomeLazyView(AnonymousRequiredMixin, TemplateView): authenticated_redirect_url = reverse_lazy(u"view_url") Dynamic Example ^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import AnonymousRequiredMixin class SomeView(AnonymousRequiredMixin, TemplateView): """ Redirect based on user level """ def get_authenticated_redirect_url(self): if self.request.user.is_superuser: return u"/admin/" return u"/somewhere/else/" .. _StaffuserRequiredMixin: StaffuserRequiredMixin ---------------------- Similar to :ref:`SuperuserRequiredMixin`, this mixin allows you to require a user with ``is_staff`` set to ``True``. :: from django.views.generic import TemplateView from braces import views class SomeStaffuserView(views.LoginRequiredMixin, views.StaffuserRequiredMixin, TemplateView): template_name = u"path/to/template.html" .. _SSLRequiredMixin: SSLRequiredMixin ---------------- .. versionadded:: 1.8.0 Simple view mixin that requires the incoming request to be secure by checking Django's `request.is_secure()` method. By default the mixin will return a permanent (301) redirect to the https version of the current url. Optionally you can set `raise_exception=True` and a 404 will be raised. Standard Django Usage ^^^^^^^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import SSLRequiredMixin class SomeSecureView(SSLRequiredMixin, TemplateView): """ Redirects from http -> https """ template_name = "path/to/template.html" Standard Django Usage ^^^^^^^^^^^^^^^^^^^^^ :: from django.views.generic import TemplateView from braces.views import SSLRequiredMixin class SomeSecureView(SSLRequiredMixin, TemplateView): """ http request would raise 404. https renders view """ raise_exception = True template_name = "path/to/template.html" .. _RecentLoginRequiredMixin: RecentLoginRequiredMixin ------------------------ .. versionadded:: 1.8.0 This mixin requires a user to have logged in within a certain number of seconds. This is to prevent stale sessions or to create a session time-out, as is often used for financial applications and the like. This mixin includes the functionality of `LoginRequiredMixin`_, so you don't need to use both on the same view. :: from django.views.generic import TemplateView from braces.views import RecentLoginRequiredMixin class SomeSecretView(RecentLoginRequiredMixin, TemplateView): max_last_login_delta = 600 # Require a login within the last 10 minutes template_name = "path/to/template.html" .. _Daniel Sokolowski: https://github.com/danols .. _code here: https://github.com/lukaszb/django-guardian/issues/48 .. _user_passes_test: https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test .. _settings.LOGIN_REDIRECT_URL: https://docs.djangoproject.com/en/1.6/ref/settings/#login-redirect-url ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742262232.0 django_braces-1.17.0/docs/changelog.rst0000664000175000017500000002545714766147730017112 0ustar00kloveklove:orphan: ========= Changelog ========= * :release:`1.17.0 <2025-03-17>` * :bug:`316 major` `MultiplePermissionsRequiredMixin` didn't do object-level permission checks. * :bug:`309 minor` `RecentLoginRequiredMixin` no longer throws error about `GET` requests in Django >= 5.0 * :release:`1.16.0 <2024-10-09>` * :support:`290` Fixed a few typos in the docs. * :support:`287` No longer building a universal wheel * :support:`300` Removed unnecessarily explicit `super()` call * :support:`301` Removed unnecessarily explicit `super()` call * :support:`307` Use `unittest.mock` instead of `mock` package * :feature:`305` Add support for Django 4.2 * :support:`277` Updated the codebase to be more modern Python/Django * :feature:`245` Cache-related headers * :release:`1.15.0 <2021-11-05>` * :support:`-` Formatted project with black * :support:`-` Updated README * :feature:`265` Drop old Python and Django versions, bring in newer ones * :release:`1.14.0 <2019-12-30>` * :support:`260` Fixes the goshdang Travis tests. * :support:`250` Include documentation Makefile and conf.py in source distribution. * :support:`246` README more accurately explains the supported Django versions. * :release:`1.13.0 <2018-04-06>` * :support:`243` Adds support for Python 3.7 and Django 2.1. * :support:`244` Documentation link fix. * :support:`236` Refines the Django and Python versions tested against. * :support:`241` Fixes a documentation typo, "altenate" should be "alternate". * :release:`1.12.0 <2018-04-06>` * :support:`237` Updates for Django 2.0. * :support:`232` Updates for Django 1.11. * :support:`227` Use SVG in README instead of PNG. * :support:`221` Renamed a duplicative method name. * :support:`220` Adds a warning for cases where ``prefetch_related`` or ``select_related`` are empty in their respective mixins. * :release:`1.11.0 <2017-02-01>` * :bug:`215 major` Imports for 1.11 and 2.x ``reverse`` and ``reverse_lazy`` functions. * :support:`248` Include some files necessary for testing in the source distribution. * :feature:`228` Adds an ``object_level_permissions`` attribute to the ``PermissionRequiredMixin`` to allow for object-level permission checks instead of just view-level checks. * :bug:`224 major` Allows ``OPTIONS`` requests to be body-less. * :bug:`218 major` ``AccessMixin.handle_no_permission` now accepts a ``request`` parameter. * :feature:`198` New :ref:`OrderableListMixin` allows to switch the default ordering setting from `asc` to `desc`. * :support:`215` Imports updated for Django 2.0. * :feature:`204` New :ref:`HeaderMixin` that allows custom headers to be set on a view. * :release:`1.10.0 <2016-10-24>` * :bug:`212 major` Small changes for Django 1.10 compatibility. * :bug:`211 major` ReadTheDocs links updated. * :bug:`209 major` Django documentation link updated. * :release:`1.9.0 <2016-05-31>` * :bug:`208 major` Fixed errors from combining certain access mixins. * :bug:`196 major` Refactor how users without permissions are handled. * :bug:`181 major` Fixed redirect loops based on user permissions. * :bug:`161 major` Fixed redirect loop for users without proper groups for ``MultipleGroupRequiredMixin`` and ``GroupRequiredMixin``. * :support:`209` Fixed link to Django documentation for ``user_passes_test`` decorator. * :feature:`203` Use Django's supplied version of ``six`` to remove an external dependency. * :support:`202` Fixed typo in ``PermissionsRequiredMixin`` and ``MultiplePermissionsRequiredMixin``. * :support:`201` Fixed typo in ``SuccessURLRedirectListMixin``. * :support:`192` Added example for ``OrderableListView``. * :release:`1.8.1 <2015-07-12>` * :bug:`176` Only check time delta for authenticated users in :ref:`RecentLoginRequiredMixin`. * :bug:`-` Changed :ref:`JsonRequestResponseMixin` docs to not use `ugettext_lazy`. * :bug:`-` Updated tests to include Python 3.2. * :bug:`185` Removed `u` prefixes to allow Python 3.2 support. * :support:`-` Added note to docs about Python and Django versions used in tests. * :bug:`-` Fix small issue in docs for :ref:JsonResponseMixin. The accepted kwarg for the render_to_response method is status not status_code. * :release:`1.8.0 <2015-04-16>` * :support:`145` Allow custom exceptions to be raised by all AccessMixins. * :feature:`171` New ``SSLRequiredMixin``. Redirect http -> https. * :feature:`138` New :ref:`RecentLoginRequiredMixin` to require user sessions to have a given freshness. * :bug:`164 major` Use `resolve_url` to handle `LOGIN_REDIRECT_URL`s in `settings.py` that are just URL names. * :bug:`130 major` New attribute on :ref:`JSONResponseMixin` to allow setting a custom JSON encoder class. * :bug:`131 major` New attribute on :ref:`LoginRequiredMixin` so it's possible to redirect unauthenticated users while using ``AccessMixin``-derived mixins instead of throwing an exception. * :release:`1.4.0 <2014-03-04>` * :support:`129` Split ``views.py`` out into multiple files since it was approaching 1000 LoC. * :feature:`119` :ref:`SetHeadlineMixin` now accepts ``headline`` with ``ugettext_lazy()``-wrapped strings. * :bug:`94 major` Fixed a bug where :ref:`JSONResponseMixin` would override the ``content_type`` of Django's ``TemplateView`` in Django 1.6. * :bug:`- major` Fixed bug in :ref:`PermissionRequiredMixin` where if ``PermissionRequiredMixin.no_permissions_fail`` returned a false-y value, the user lacking the permission would pass instead of being denied access. * :support:`73` Added doc for how to contribute. * :feature:`120` Added :ref:`MessageMixin` to allow easier access to Django's ``contrib.messages`` messages. :ref:`FormValidMessageMixin` and :ref:`FormInvalidMessageMixin` were updated to use it. * :bug:`98 major` Fixed bug in :ref:`CanonicalSlugDetailMixin` to allow it to use custom URL kwargs. * :bug:`105 major` Fixed bug in :ref:`GroupRequiredMixin` where superusers were blocked by lack of group memberships. * :bug:`106 major` Fixed bug in :ref:`GroupRequiredMixin` which now correctly checks for group membership against a list. * :feature:`102` Added new :ref:`StaticContextMixin` mixin which lets you pass in ``static_context`` as a property of the view. * :feature:`89` Added new :ref:`AnonymousRequiredMixin` which redirects authenticated users to another view. * :feature:`104` Added new :ref:`AllVerbsMixin` which allows a single method to response to all HTTP verbs. * :bug:`- major` Provided ``JSONRequestResponseMixin`` as a mirror of :ref:`JsonRequestResponseMixin` because we're not PHP. * :feature:`107` :ref:`FormValidMessageMixin`, :ref:`FormInvalidMessageMixin`, and :ref:`FormMessagesMixin` all allow ``ugettext_lazy``-wrapped strings. * :feature:`67` Extended :ref:`PermissionRequiredMixin` and :ref:`MultiplePermissionsRequiredMixin` to accept django-guardian-style custom/object permissions. * :release:`1.3.1 <2014-01-04>` * :bug:`95` Removed accidentally-added breakpoint. * :support:`96 backported` Added ``build/`` to ``.gitignore`` * :release:`1.3.0 <2014-01-03>` * :support:`59` Removed ``CreateAndRedirectToEditView`` mixin which was marked for deprecation and removal since 1.0.0. * :feature:`51` Added :ref:`JsonRequestResponseMixin` which attempts to parse requests as JSON. * :feature:`61` Added :ref:`CanonicalSlugDetailMixin` mixin which allows for the specification of a canonical slug on a ``DetailView`` to help with SEO by redirecting on non-canonical requests. * :feature:`76` Added :ref:`UserPassesTestMixin` mixin to replicate the behavior of Django's ``@user_passes_test`` decorator. * :bug:`- major` Some fixes for :ref:`CanonicalSlugDetailMixin`. * :feature:`92` ``AccessMixin`` now has a runtime-overridable ``login_url`` attribute. * :bug:`- major` Fixed problem with :ref:`GroupRequiredMixin` that made it not actually work. * :support:`-` All tests pass for Django versions 1.4 through 1.6 and Python versions 2.6, 2.7, and 3.3 (Django 1.4 and 1.5 not tested with Python 3.3). * :release:`1.2.2 <2013-08-07>` * :support:`-` Uses ``six.string_types`` instead of explicitly relying on ``str`` and ``unicode`` types. * :release:`1.2.1 <2013-07-28>` * :bug:`-` Fix to allow ``reverse_lazy`` to work for all ``AccessMixin``-derived mixins. * :release:`1.2.0 <2013-07-27>` * :feature:`57` :ref:`FormValidMessageMixin` which provides a ``messages`` message when the processed form is valid. * :feature:`-` :ref:`FormInvalidMessageMixin` which provides a ``messages`` message when the processed form is invalid. * :feature:`-` :ref:`FormMessagesMixin` which provides the functionality of both of the above mixins. * :feature:`-` :ref:`GroupRequiredMixin` which is a new access-level mixin which requires that a user be part of a specified group to access a view. * :release:`1.1.0 <2013-07-18>` * :bug:`52 major` :ref:`JSONResponseMixin` ``.render_json_response`` method updated to accept a status code. * :bug:`43 major` :ref:`JSONResponseMixin` added ``json_dumps_kwargs`` attribute & get method to pass args to the JSON encoder. * :feature:`45` New :ref:`OrderableListMixin` allows ordering of list views by GET params. * :support:`-` Tests updated to test against latest stable Django release (1.5.1) * :support:`-` Small fixes and additions to documentation. * :release:`1.0.0 <2013-02-28>` * :feature:`-` New 'abstract' ``AccessMixin`` which provides overridable ``get_login_url`` and ``get_redirect_field_name`` methods for all access-based mixins. * :feature:`32` Rewritten :ref:`LoginRequiredMixin` which provides same customization as other access mixins with ``login_url``, ``raise_exception`` & ``redirect_field_name``. * :feature:`33` New :ref:`PrefetchRelatedMixin`. Works the same as :ref:`SelectRelatedMixin` but uses Django's ``prefetch_related`` method. * :support:`-` ``CreateAndRedirectToEditView`` is marked for deprecation. * :bug:`- major` :ref:`PermissionRequiredMixin` no longer requires dot syntax for permission names. * :support:`-` Marked package as supporting 2.6 thru 3.3 (from rafales). * :support:`-` Fixes to documentation. * :support:`-` Tests to cover new additions and changes. * :release:`0.2.3 <2013-02-22>` * :support:`30 backported` Tests for all mixins (from rafales). * :feature:`26 backported` New :ref:`CsrfExemptMixin` for marking views as being CSRF exempt (from jarcoal). * :support:`- backported` Some documentation updates and a spelling error correction (from shabda). * :bug:`-` :ref:`SuccessURLRedirectListMixin` raises ``ImproperlyConfigured`` if no ``success_list_url`` attribute is supplied (from kennethlove). * :release:`0.2.2 <2013-01-21>` * :bug:`25` Try importing the built-in ``json`` module first, drop back to Django if necessary. * :support:`- backported` Django 1.5 compatibility. * :release:`0.2.1 <2012-12-10>` * :bug:`21 major` Fixed signature of :ref:`UserFormKwargsMixin` ``.get_form_kwargs`` * :feature:`22` Updated :ref:`JSONResponseMixin` to work with non-ASCII characters and other datatypes (such as datetimes) * :bug:`- major` Fixed all mixins that have ``raise_exception`` as an argument to properly raise a ``PermissionDenied`` exception to allow for custom 403s. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/conf.py0000664000175000017500000002025114766144450015707 0ustar00kloveklove# -*- 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 sys.path.insert(0, os.path.abspath("..")) import braces from braces import __version__ # 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 = ["releases"] releases_issue_uri = "https://github.com/brack3t/django-braces/issues/%s" releases_release_uri = "https://github.com/brack3t/django-braces/tree/%s" releases_unstable_prehistory = True releases_debug = True # 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 = __version__ # The full version, including alpha/beta/rc tags. release = version # 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", "django-braces Documentation", "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", "django-braces Documentation", ["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", "django-braces Documentation", "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' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/contributing.rst0000664000175000017500000000350414766144450017653 0ustar00kloveklove:orphan: ============ Contributing ============ First of all, thank you for wanting to make **django-braces** better! We love getting input and suggestions from the community. Secondly, we just want to put out a few ground rules for contributing so that we can get your pull requests in sooner and cause less headaches all around. .. _Code Style: Code Style ---------- We stick to `PEP8 `_ as much as possible, so please make sure your code passes a lint test. That means two blank lines before class names and all of those other wonderful rules. We like docstrings in the classes, too. This helps us and those that come later what the class is meant to do. Docstrings in methods are great, too, especially if the method makes any assumptions about how it'll be used. .. _Docs: Docs ---- If you're reading this, you should already know that docs are important to this project and, honestly, all of them. We like any new mixins, or changes in existing mixins, to come with documentation changes showing how to use the mixin. Ideally, you show at least one example usage, but if your mixin provides multiple paths, perhaps a static attribute or a dynamic method, it's really great if your documentation shows both avenues, too. .. _Tests: Tests ----- All code changes should come with test changes. We use `py.test `_ instead of Python's ``unittest``. We try to keep the project at high test coverage but know this isn't something we can achieve alone. Tests should be included with your pull requests and should cover 100% of your changes. We test ``django-braces`` against currently supported versions of Python and LTS versions of Django. All pull requests are run through a matrix of these Python and Django versions. Locally, use a recent Python and Django. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/form.rst0000664000175000017500000001757014766144450016117 0ustar00klovekloveForm 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 mixin should always be the left-most plugin. :: from django.views.generic import UpdateView from braces.views import LoginRequiredMixin, CsrfExemptMixin from profiles.models import Profile class UpdateProfileView(CsrfExemptMixin, LoginRequiredMixin, UpdateView): model = Profile .. _UserFormKwargsMixin: UserFormKwargsMixin ------------------- A common pattern in Django is to have forms that are customized to a user. To custom tailor the form for users, you have to pass that user instance into the form and, based on their permission level or other details, change certain fields or add specific options within the forms ``__init__`` method. This mixin automates the process of overloading the ``get_form_kwargs`` (this method is available in any generic view which handles a form) method and stuffs the user instance into the form kwargs. The user can then be ``pop()``\ ped off in the form. **Always** remember to pop the user from the kwargs before calling ``super()`` on your form, otherwise the form will get an unexpected keyword argument. Usage ^^^^^ :: from django.views.generic import CreateView from braces.views import LoginRequiredMixin, UserFormKwargsMixin from next.example import UserForm class SomeSecretView(LoginRequiredMixin, UserFormKwargsMixin, CreateView): form_class = UserForm model = User template_name = "path/to/template.html" This obviously pairs very nicely with the following 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, the ``pop()``\ ping of the ``user`` is automated and no longer has to be done manually on every form that needs this behavior. 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 CRUD_ is often handled within CMSes. Many CMSes, by design, redirect the user to the ``ListView`` for whatever model they are working with, whether they are creating a new instance, editing an existing one, or deleting one. Rather than having to override ``get_success_url`` on every view, use this mixin and pass it a reversible route name. Example: :: # urls.py url(r"^users/$", UserListView.as_view(), name="users_list"), # views.py from django.views import CreateView from braces import views class UserCreateView(views.LoginRequiredMixin, views.PermissionRequiredMixin, views.SuccessURLRedirectListMixin, CreateView): form_class = UserForm model = User permission_required = "auth.add_user" success_list_url = "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.utils.translation import ugettext_lazy as _ from django.views.generic import CreateView from braces.views import FormValidMessageMixin class BlogPostCreateView(FormValidMessageMixin, CreateView): form_class = PostForm model = Post form_valid_message = _(u"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 u"{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.utils.translation import ugettext_lazy from django.views.generic import CreateView from braces.views import FormInvalidMessageMixin class BlogPostCreateView(FormInvalidMessageMixin, CreateView): form_class = PostForm model = Post form_invalid_message = _(u"Oh snap, something went wrong!") Dynamic Example ^^^^^^^^^^^^^^^ :: from django.utils.translation import ugettext_lazy as _ 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 _(u"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.utils.translation import ugettext_lazy as _ from django.views.generic import CreateView from braces.views import FormMessagesMixin class BlogPostCreateView(FormMessagesMixin, CreateView): form_class = PostForm form_invalid_message = _(u"Something went wrong, post was not saved") model = Post def get_form_valid_message(self): return u"{0} created!".format(self.object.title) .. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete .. _CSRF protection: https://docs.djangoproject.com/en/stable/ref/csrf/ .. _csrf_exempt: https://docs.djangoproject.com/en/stable/ref/csrf/#django.views.decorators.csrf.csrf_exempt .. _messages: https://docs.djangoproject.com/en/stable/ref/contrib/messages/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/index.rst0000664000175000017500000000137714766144450016261 0ustar00kloveklove.. 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 `View our Changelog `_ `Want to contribute? `_ Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Github: https://github.com/brack3t/django-braces ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/docs/other.rst0000664000175000017500000005132414766144450016270 0ustar00klovekloveOther 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`` allows you to *statically* or *programmatically* set the headline of any of your views. Ideally, you'll write as few templates as possible, so a mixin like this helps you reuse generic templates. Its usage is amazingly straightforward and works much like Django's built-in ``get_queryset`` method. This mixin has two ways of being used: Static Example ^^^^^^^^^^^^^^ :: from django.utils.translation import ugettext_lazy as _ from django.views import TemplateView from braces.views import SetHeadlineMixin class HeadlineView(SetHeadlineMixin, TemplateView): headline = _(u"This is our headline") template_name = u"path/to/template.html" Dynamic Example ^^^^^^^^^^^^^^^ :: from datetime import date from django.views import TemplateView from braces.views import SetHeadlineMixin class HeadlineView(SetHeadlineMixin, TemplateView): template_name = u"path/to/template.html" def get_headline(self): return u"This is our headline for {0}".format(date.today().isoformat()) For both usages, the context now contains a ``headline`` key with your headline. .. _StaticContextMixin: StaticContextMixin ------------------ .. versionadded:: 1.4 The ``StaticContextMixin`` allows you to easily set static context data by using the ``static_context`` property. .. note:: While it's possible to override the ``StaticContextMixin.get_static_context method``, it's not very practical. If you have a need to override a method for dynamic context data it's best to override the standard ``get_context_data`` method of Django's generic class-based views. View Example ^^^^^^^^^^^^ :: # views.py from django.views import TemplateView from braces.views import StaticContextMixin class ContextTemplateView(StaticContextMixin, TemplateView): static_context = {u"nav_home": True} URL Example ^^^^^^^^^^^ :: # urls.py urlpatterns = patterns( '', url(ur"^$", ContextTemplateView.as_view( template_name=u"index.html", static_context={u"nav_home": True} ), name=u"index") ) .. _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 = [u"user"] template_name = u"profiles/detail.html" .. _select_related: https://docs.djangoproject.com/en/dev/ref/models/querysets/#select-related .. _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 = [u"post_set"] # where the Post model has an FK to the User model as an author. template_name = u"users/detail.html" .. _prefetch_related: https://docs.djangoproject.com/en/dev/ref/models/querysets/#prefetch-related .. _JSONResponseMixin: JSONResponseMixin ----------------- .. versionchanged:: 1.1 ``render_json_response`` now accepts a ``status`` 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 = {u"indent": 2} def get(self, request, *args, **kwargs): self.object = self.get_object() context_dict = { u"name": self.object.user.name, u"location": self.object.location } return self.render_json_response(context_dict) You can additionally use the `AjaxResponseMixin` :: # views.py from django.views import DetailView from braces import views class UserProfileView(views.JSONResponseMixin, views.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 django.views import DetailView from braces.views import JSONResponseMixin class UserProfileAJAXView(JSONResponseMixin, DetailView): content_type = u"application/javascript" model = Profile def get(self, request, *args, **kwargs): self.object = self.get_object() context_dict = { u"name": self.object.user.name, u"location": self.object.location } return self.render_json_response(context_dict) def get_content_type(self): # Shown just for illustrative purposes return u"application/javascript" The `JSONResponseMixin` provides another class-level variable `json_encoder_class` to use a custom json encoder with `json.dumps`. By default it is `django.core.serializers.json.DjangoJsonEncoder` :: from django.core.serializers.json import DjangoJSONEncoder from braces.views import JSONResponseMixin class SetJSONEncoder(DjangoJSONEncoder): """ A custom JSONEncoder extending `DjangoJSONEncoder` to handle serialization of `set`. """ def default(self, obj): if isinstance(obj, set): return list(obj) return super(DjangoJSONEncoder, self).default(obj) class GetSetDataView(JSONResponseMixin, View): json_encoder_class = SetJSONEncoder def get(self, request, *args, **kwargs): numbers_set = set(range(10)) data = {'numbers': numbers_set} return self.render_json_response(data) .. _JsonRequestResponseMixin: JsonRequestResponseMixin ------------------------ .. versionadded:: 1.3 A mixin that attempts to parse the request as JSON. If the request is properly formatted, the JSON is saved to ``self.request_json`` as a Python object. ``request_json`` will be ``None`` for imparsible requests. 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 import views class SomeView(views.CsrfExemptMixin, views.JsonRequestResponseMixin, View): require_json = True def post(self, request, *args, **kwargs): try: burrito = self.request_json[u"burrito"] toppings = self.request_json[u"toppings"] except KeyError: error_dict = {u"message": u"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( {u"message": u"Your order has been placed!"}) .. _AjaxResponseMixin: AjaxResponseMixin ----------------- This mixin provides hooks for alternate processing of AJAX requests based on HTTP verb. 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 import views class SomeView(views.JSONResponseMixin, views.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) .. note:: This mixin is only useful if you need to have behavior in your view fork based on ``request.is_ajax()``. .. _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 ordered by, as well as the default column to order by in your view. This can be done either by simply setting the class attributes: :: # views.py from django.views import ListView from braces.views import OrderableListMixin class OrderableListView(OrderableListMixin, ListView): model = Article orderable_columns = (u"id", u"title",) orderable_columns_default = u"id" Or by using similarly-named methods to set the ordering constraints more dynamically: :: # views.py from django.views import ListView from braces.views import OrderableListMixin class OrderableListView(OrderableListMixin, ListView): model = Article def get_orderable_columns(self): # return an iterable return (u"id", u"title",) def get_orderable_columns_default(self): # return a string return u"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` You can also override the default ordering from ``"asc"`` to ``"desc"`` by setting the ``"ordering_default"`` in your view class. :: # views.py from django.views import ListView from braces.views import OrderableListMixin class OrderableListView(OrderableListMixin, ListView): model = Article orderable_columns = (u"id", u"title",) orderable_columns_default = u"id" ordering_default = u"desc" This will reverse the order of list objects if no query param is given. **Front-end Example Usage** If you're using bootstrap you could create a template like the following: .. code:: html
{% for object in object_list %} {% endfor %}
ID Title
{{ object.id }} {{ object.title }}
.. _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 "{0}-{1}".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. .. _MessageMixin: MessageMixin ------------ .. versionadded:: 1.4 A mixin that adds a ``messages`` attribute on the view which acts as a wrapper to ``django.contrib.messages`` and passes the ``request`` object automatically. .. warning:: If you're using Django 1.4, then the ``message`` attribute is only available after the base view's ``dispatch`` method has been called (so our second example would not work for instance). :: from django.views.generic import TemplateView from braces.views import MessageMixin class MyView(MessageMixin, TemplateView): """ This view will add a debug message which can then be displayed in the template. """ template_name = "my_template.html" def get(self, request, *args, **kwargs): self.messages.debug("This is a debug message.") return super(MyView, self).get(request, *args, **kwargs) :: from django.contrib import messages from django.views.generic import TemplateView from braces.views import MessageMixin class OnlyWarningView(MessageMixin, TemplateView): """ This view will only show messages that have a level above `warning`. """ template_name = "my_template.html" def dispatch(self, request, *args, **kwargs): self.messages.set_level(messages.WARNING) return super(OnlyWarningView, self).dispatch(request, *args, **kwargs) .. _AllVerbsMixin: AllVerbsMixin ------------- .. versionadded:: 1.4 This mixin allows you to specify a single method that will respond to all HTTP verbs, making a class-based view behave much like a function-based view. :: from django.views import TemplateView from braces.views import AllVerbsMixin class JustShowItView(AllVerbsMixin, TemplateView): template_name = "just/show_it.html" def all(self, request, *args, **kwargs): return super(JustShowItView, self).get(request, *args, **kwargs) If you need to change the name of the method called, provide a new value to the ``all_handler`` attribute (default is ``'all'``) .. _HeaderMixin: HeaderMixin ------------- .. versionadded:: 1.11 This mixin allows you to add arbitrary HTTP header to a response. Static headers can be defined in the ``headers`` attribute of the view. :: from django.views import TemplateView from braces.views import HeaderMixin class StaticHeadersView(HeaderMixin, TemplateView): template_name = "some/headers.html" headers = { 'X-Header-Sample': 'some value', 'X-Some-Number': 42 } If you need to set the headers dynamically, e.g depending on some request information, override the ``get_headers`` method instead. :: from django.views import TemplateView from braces.views import HeaderMixin class EchoHeadersView(HeaderMixin, TemplateView): template_name = "some/headers.html" def get_headers(self, request): """ Echo back request headers with ``X-Request-`` prefix. """ for key, value in request.META.items(): yield "X-Request-{}".format(key), value .. _CacheControlMixin: CacheControlMixin ----------------------- Mixin that allows setting ``Cache-Control`` header options. ``Cache-Control`` directive explanations: http://condor.depaul.edu/dmumaugh/readings/handouts/SE435/HTTP/node24.html Django's ``django.views.decorators.cache.cache_control`` options: https://docs.djangoproject.com/en/dev/topics/cache/#controlling-cache-using-other-headers :: from django.views import TemplateView from braces.views import CacheControlMixin class MyView(CacheControlMixin, TemplateView): template_name = "project/template.html" # Specify Cache-Control options as class-level attributes. # The arguments are passed to Django's `cache_control()` view decorator # directly (with the `cachecontrol_` prefix removed.) cachecontrol_public = True cachecontrol_max_age = 60 # Alternatively, you can implement the ``get_cachecontrol_options()`` # classmethod that should return a dictionary of the kwargs passed to # Django's `cache_control()` view decorator. @classmethod def get_cachecontrol_options(cls): return {'public': True, 'max_age': 60} This will cause the following header to be emitted for every request to this view: :: Cache-Control: public, max-age=60 Available ``Cache-Control`` options: :: # These are all `None` by default, which indicates unset. cachecontrol_public = None cachecontrol_private = None cachecontrol_no_cache = None cachecontrol_no_transform = None cachecontrol_must_revalidate = None cachecontrol_proxy_revalidate = None cachecontrol_max_age = None cachecontrol_s_maxage = None .. _NeverCacheMixin: NeverCacheMixin ----------------------- Mixin that applies Django's ``django.views.decorators.cache.never_cache`` view decorator to prevent upstream HTTP-based caching. :: from django.views import TemplateView from braces.views import NeverCacheMixin class MyView(NeverCacheMixin, TemplateView): template_name = "project/template.html" This will cause the following header to be emitted for every request to this view: :: Cache-Control: max-age=0, no-cache, no-store, must-revalidate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/pyproject.toml0000664000175000017500000000124614766144450016377 0ustar00kloveklove[build-system] requires = ["setuptools>=40.6.6", "wheel"] build-backend = "setuptools.build_meta" [tool.black] line-length = 79 [tool.pytest.ini_options] addopts = "--cov --nomigrations" [tool.interrogate] ignore-init-method = true ignore-init-module = false ignore-magic = false ignore-semiprivate = false ignore-private = false ignore-property-decorators = false ignore-module = true ignore-nested-functions = false ignore-nested-classes = true fail-under = 75 exclude = ["setup.py", "conftest.py", "docs", "build"] ignore-regex = ["^get$", "^mock_.*"] # possible values: 0 (minimal output), 1 (-v), 2 (-vv) verbose = 1 quiet = false color = true omit-covered-files = true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/requirements.txt0000664000175000017500000000006514766144450016745 0ustar00kloveklovefactory-boy pytest-django pytest-cov coverage django ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262641.1117346 django_braces-1.17.0/setup.cfg0000664000175000017500000000026114766150561015300 0ustar00kloveklove[bumpversion] current_version = 1.16.0 commit = True tag = True [bumpversion:file:braces/__init__.py] [metadata] license_file = LICENSE [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261676.0 django_braces-1.17.0/setup.py0000664000175000017500000000466114766146654015211 0ustar00klovekloveimport os import re from setuptools import setup NAME = "braces" def _add_default(m): """Add on a default""" attr_name, attr_value = m.groups() return ((attr_name, attr_value.strip("\"'")),) def parse_dist_meta(): """Extract metadata information from ``$dist/__init__.py``.""" re_meta = re.compile(r"__(\w+?)__\s*=\s*(.*)") re_doc = re.compile(r'^"""(.+?)"""') here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, NAME, "__init__.py")) as meta_fh: distmeta = {} for line in meta_fh: if line.strip() == "# -eof meta-": break match = re_meta.match(line.strip()) if match: distmeta.update(_add_default(match)) return distmeta meta = parse_dist_meta() setup( name="django-braces", version=meta["version"], 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", "braces.views"], zip_safe=False, include_package_data=True, classifiers=[ "Development Status :: 5 - Production/Stable", "Development Status :: 6 - Mature", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 2.2", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", ], install_requires=["Django>=2.2"], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1742262640.8477356 django_braces-1.17.0/tests/0000775000175000017500000000000014766150561014622 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/__init__.py0000664000175000017500000000000014766144450016721 0ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/backends.py0000664000175000017500000000157214766146657016766 0ustar00kloveklove"""Testing backend for object level permissions""" from tests.helpers import PermissionChecker class PermissionsCheckerBackend: """ Custom Permission Backend for testing Object Level Permissions. """ supports_object_permissions = True supports_anonymous_user = True supports_inactive_user = True @staticmethod def authenticate(): """Required for a backend""" return None @staticmethod def has_perm(user_obj, perm, obj=None): """Used for checking permissions using the `PermissionChecker`""" check = PermissionChecker(user_obj) return check.has_perm(perm, obj) @staticmethod def has_perms(user_obj, perms: list[str], obj=None): """Used for checking multiple permissions using the `PermissionChecker`""" check = PermissionChecker(user_obj) return check.has_perms(perms, obj) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/factories.py0000664000175000017500000000535314766146657017174 0ustar00klovekloveimport factory from django.contrib.auth.models import Group, Permission, User from django.contrib.contenttypes.models import ContentType from .models import Article, UserObjectPermissions 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): """Generates Articles""" title = factory.Sequence(lambda n: f"Article number {n}") body = factory.Sequence(lambda n: "Body of article {n}") class Meta: model = Article abstract = False class GroupFactory(factory.django.DjangoModelFactory): """Artificial divides as a service""" name = factory.Sequence(lambda n: f"group{n}") class Meta: model = Group abstract = False class UserFactory(factory.django.DjangoModelFactory): """The people who make it all possible""" username = factory.Sequence(lambda n: f"user{n}") first_name = factory.Sequence(lambda n: f"John {n}") last_name = factory.Sequence(lambda n: f"Doe {n}") email = factory.Sequence(lambda n: f"user{n}@example.com") password = factory.PostGenerationMethodCall("set_password", "asdf1234") class Meta: model = User abstract = False @factory.post_generation def permissions(self, create, extracted, **kwargs): """Give the user some permissions""" 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]) class ContentTypeFactory(factory.django.DjangoModelFactory): """Factory for creating `ContentType` model objects""" app_label = factory.Sequence(lambda n: f"app_label_{n}") model = factory.Sequence(lambda n: f"model_{n}") class Meta: model = ContentType abstract = False class PermissionFactory(factory.django.DjangoModelFactory): """Factory for creating `Permission` model objects""" name = factory.Sequence(lambda n: f"name_{n}") codename = factory.Sequence(lambda n: f"codename_{n}") content_type = factory.SubFactory(ContentTypeFactory) class Meta: model = Permission abstract = False class UserObjectPermissionsFactory(factory.django.DjangoModelFactory): """Factory for creating `UserObjectPermissions` model objects""" user = factory.SubFactory(UserFactory) permission = factory.SubFactory(PermissionFactory) article_object = factory.SubFactory(ArticleFactory) class Meta: model = UserObjectPermissions abstract = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/forms.py0000664000175000017500000000064014766144450016322 0ustar00kloveklovefrom django import forms from braces.forms import UserKwargModelFormMixin from .models import Article class FormWithUserKwarg(UserKwargModelFormMixin, forms.Form): """This form will get a `user` kwarg""" field1 = forms.CharField() class ArticleForm(forms.ModelForm): """This form represents an Article""" class Meta: model = Article fields = ["author", "title", "body", "slug"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/helpers.py0000664000175000017500000000614214766146657016654 0ustar00kloveklovefrom django import test from django.contrib.auth.models import AnonymousUser, User, Permission from django.core.serializers.json import DjangoJSONEncoder from tests.models import UserObjectPermissions class TestViewHelper: """ Helper class for unit-testing class based views. """ view_class = None request_factory_class = test.RequestFactory def setUp(self): super().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) class SetJSONEncoder(DjangoJSONEncoder): """ A custom JSONEncoder extending `DjangoJSONEncoder` to handle serialization of `set`. """ def default(self, obj): """Control default methods of encoding data""" if isinstance(obj, set): return list(obj) return super(DjangoJSONEncoder, self).default(obj) class PermissionChecker: """ Custom Permission checker for testing of Object Level Permissions """ def __init__(self, user: User): self.user = user def has_perm(self, perm: str, obj=None) -> bool: """This function is used to check for object level permissions""" if self.user and not self.user.is_active: return False elif self.user and self.user.is_superuser: return True if "." in perm: perm = perm.split(".", maxsplit=1)[1] permission_obj = Permission.objects.get(codename=perm) if obj is None: return perm in self.user.get_all_permissions(perm) return UserObjectPermissions.objects.filter( permission=permission_obj, user=self.user, article_object=obj ).exists() def has_perms(self, perms: list[str], obj=None) -> bool: """This function is used to check for object level permissions""" if self.user and not self.user.is_active: return False elif self.user and self.user.is_superuser: return True if not perms: return False return all(self.has_perm(perm) for perm in perms) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/models.py0000664000175000017500000000242514766146657016475 0ustar00kloveklovefrom django.contrib.auth.models import Permission, User from django.db import models class Article(models.Model): """ A small but useful model for testing most features """ author = models.ForeignKey( "auth.User", null=True, blank=True, on_delete=models.CASCADE ) title = models.CharField(max_length=30) body = models.TextField() slug = models.SlugField(blank=True) class CanonicalArticle(models.Model): """ Model specifically for testing the canonical slug mixins """ author = models.ForeignKey( "auth.User", null=True, blank=True, on_delete=models.CASCADE ) title = models.CharField(max_length=30) body = models.TextField() slug = models.SlugField(blank=True) def get_canonical_slug(self): """Required by mixin to use the model as the source of truth""" if self.author: return f"{self.author.username}-{self.slug}" return f"unauthored-{self.slug}" class UserObjectPermissions(models.Model): """Django model used to test and assign object level permissions""" user = models.ForeignKey(User, on_delete=models.CASCADE) permission = models.ForeignKey(Permission, on_delete=models.CASCADE) article_object = models.ForeignKey(Article, on_delete=models.CASCADE) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/settings.py0000664000175000017500000000374014766146657017053 0ustar00kloveklovefrom django.conf.global_settings import * DEBUG = False TEMPLATE_DEBUG = DEBUG TIME_ZONE = "UTC" LANGUAGE_CODE = "en-US" SITE_ID = 1 USE_TZ = True SECRET_KEY = "local" ROOT_URLCONF = "tests.urls" DATABASES = { "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} } AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'tests.backends.PermissionsCheckerBackend', ) 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", ] MIDDLEWARE = MIDDLEWARE_CLASSES 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",) TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ], }, }, ] AUTH_PASSWORD_VALIDATORS = [] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1742262641.007735 django_braces-1.17.0/tests/templates/0000775000175000017500000000000014766150561016620 5ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/templates/404.html0000664000175000017500000000002114766144450020006 0ustar00kloveklove

404!!!!

././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/templates/blank.html0000664000175000017500000000000014766144450020563 0ustar00kloveklove././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/templates/form.html0000664000175000017500000000025414766144450020452 0ustar00kloveklove {% if messages %} {% for message in messages %} {{ message }} {% endfor %} {% endif %} {{ form.as_p }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261785.0 django_braces-1.17.0/tests/test_access_mixins.py0000664000175000017500000010147014766147031021064 0ustar00klovekloveimport datetime import pytest from django import test from django.contrib.auth.models import Permission from django.test.utils import override_settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.http import Http404, HttpResponse from django.utils.encoding import force_str from django.utils.timezone import make_aware, get_current_timezone from django.urls import reverse_lazy from .factories import ( GroupFactory, UserFactory, UserObjectPermissionsFactory, ArticleFactory, ) from .helpers import TestViewHelper from .views import ( PermissionRequiredView, MultiplePermissionsRequiredView, SuperuserRequiredView, StaffuserRequiredView, LoginRequiredView, GroupRequiredView, UserPassesTestView, UserPassesTestNotImplementedView, AnonymousRequiredView, SSLRequiredView, RecentLoginRequiredView, UserPassesTestLoginRequiredView, ) @pytest.mark.django_db 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_str(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, "/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_raise_custom_exception(self): """ Http404 should be raised if user is not authorized and raise_exception attribute is set to Http404. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) with self.assertRaises(Http404): self.dispatch_view(req, raise_exception=Http404) def test_raise_func_pass(self): """ An exception should be raised if user is not authorized and raise_exception attribute is set to a function that returns nothing. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) def func(request): pass with self.assertRaises(PermissionDenied): self.dispatch_view(req, raise_exception=func) def test_raise_func_response(self): """ A custom response should be returned if user is not authorized and raise_exception attribute is set to a function that returns a response. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) def func(request): return HttpResponse("CUSTOM") resp = self.dispatch_view(req, raise_exception=func) assert resp.status_code == 200 assert force_str(resp.content) == "CUSTOM" def test_raise_func_false(self): """ PermissionDenied should be raised, if a custom raise_exception function does not return HttpResponse or StreamingHttpResponse. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) def func(request): return False with self.assertRaises(PermissionDenied): self.dispatch_view(req, raise_exception=func) def test_raise_func_raises(self): """ A custom exception should be raised if user is not authorized and raise_exception attribute is set to a callable that raises an exception. """ user = self.build_unauthorized_user() req = self.build_request(user=user, path=self.view_url) def func(request): raise Http404 with self.assertRaises(Http404): self.dispatch_view(req, raise_exception=func) 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("/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("/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 = "/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, "/auth/login/?next={0}".format(self.view_url)) def test_redirect_unauthenticated(self): resp = self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True, redirect_unauthenticated_users=True, ) assert resp.status_code == 302 assert resp["Location"] == "/accounts/login/?next={0}".format(self.view_url) def test_redirect_unauthenticated_false(self): with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True, redirect_unauthenticated_users=False, ) @pytest.mark.django_db class TestLoginRequiredMixin(TestViewHelper, test.TestCase): """Scenarios around requiring an authenticated session""" view_class = LoginRequiredView view_url = "/login_required/" def test_anonymous(self): """Anonymous users should be redirected""" resp = self.client.get(self.view_url) self.assertRedirects(resp, "/accounts/login/?next=/login_required/") def test_anonymous_raises_exception(self): """Anonymous users should raise an exception""" with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True ) def test_authenticated(self): """Authenticated users should get 'OK'""" user = UserFactory() self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) assert resp.status_code == 200 assert force_str(resp.content) == "OK" def test_anonymous_redirects(self): """Anonymous users are redirected with a 302""" resp = self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True, redirect_unauthenticated_users=True, ) assert resp.status_code == 302 assert resp["Location"] == "/accounts/login/?next=/login_required/" @pytest.mark.django_db class TestChainedLoginRequiredMixin(TestViewHelper, test.TestCase): """ Tests for LoginRequiredMixin combined with another AccessMixin. """ view_class = UserPassesTestLoginRequiredView view_url = "/chained_view/" def assert_redirect_to_login(self, response): """ Check that the response is a redirect to the login view. """ assert response.status_code == 302 assert response["Location"] == "/accounts/login/?next=/chained_view/" def test_anonymous(self): """ Check that anonymous users redirect to login by default. """ resp = self.dispatch_view(self.build_request(path=self.view_url)) self.assert_redirect_to_login(resp) def test_anonymous_raises_exception(self): """ Check that when anonymous users hit a view that has only raise_exception set, they get a PermissionDenied. """ with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True ) def test_authenticated_raises_exception(self): """ Check that when authenticated users hit a view that has raise_exception set, they get a PermissionDenied. """ user = UserFactory() with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url, user=user), raise_exception=True, ) with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url, user=user), raise_exception=True, redirect_unauthenticated_users=True, ) def test_anonymous_redirects(self): """ Check that anonymous users are redirected to login when raise_exception is overridden by redirect_unauthenticated_users. """ resp = self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True, redirect_unauthenticated_users=True, ) self.assert_redirect_to_login(resp) @pytest.mark.django_db class TestAnonymousRequiredMixin(TestViewHelper, test.TestCase): """ Tests for AnonymousRequiredMixin. """ view_class = AnonymousRequiredView view_url = "/unauthenticated_view/" def test_anonymous(self): """ As a non-authenticated user, it should be possible to access the URL. """ resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual("OK", force_str(resp.content)) # Test with reverse_lazy resp = self.dispatch_view( self.build_request(), login_url=reverse_lazy(self.view_url) ) self.assertEqual(200, resp.status_code) self.assertEqual("OK", force_str(resp.content)) def test_authenticated(self): """ Check that the authenticated user has been successfully directed to the appropriate view. """ user = UserFactory() self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url) self.assertEqual(302, resp.status_code) resp = self.client.get(self.view_url, follow=True) self.assertRedirects(resp, "/authenticated_view/") def test_no_url(self): """View should raise an exception if no URL is provided""" self.view_class.authenticated_redirect_url = None user = UserFactory() self.client.login(username=user.username, password="asdf1234") with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) def test_bad_url(self): """Redirection can be misconfigured""" self.view_class.authenticated_redirect_url = "/epicfailurl/" user = UserFactory() self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.view_url, follow=True) self.assertEqual(404, resp.status_code) @pytest.mark.django_db class TestPermissionRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios around requiring a permission""" view_class = PermissionRequiredView view_url = "/permission_required/" def build_authorized_user(self): """Create a user with permissions""" return UserFactory(permissions=["auth.add_user"]) def build_unauthorized_user(self): """Create a user without permissions""" 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) def test_object_level_permissions(self): """ Tests that object level permissions perform as expected, where object level permissions and global level permissions """ # Arrange article = ArticleFactory() self.view_class = PermissionRequiredView self.view_url = f"/object_level_permission_required/?pk={article.pk}" tests_add_article = Permission.objects.get(codename="add_article") permissions = "tests.add_article" valid_user = UserFactory(permissions=[permissions]) invalid_user_1 = UserFactory(permissions=["auth.add_user"]) invalid_user_2 = UserFactory(permissions=[permissions]) UserObjectPermissionsFactory( user=valid_user, permission=tests_add_article, article_object=article ) # Act valid_req = self.build_request(path=self.view_url, user=valid_user) valid_resp = self.dispatch_view( valid_req, permission_required=permissions, object_level_permissions=True, raise_exception=True, ) invalid_req_1 = self.build_request(path=self.view_url, user=invalid_user_1) invalid_req_2 = self.build_request(path=self.view_url, user=invalid_user_2) # Assert self.assertEqual(valid_resp.status_code, 200) with self.assertRaises(PermissionDenied): self.dispatch_view( invalid_req_1, permission_required=permissions, object_level_permissions=True, raise_exception=True, ) with self.assertRaises(PermissionDenied): self.dispatch_view( invalid_req_2, permission_required=permissions, object_level_permissions=True, raise_exception=True, ) @pytest.mark.django_db class TestMultiplePermissionsRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios around requiring multiple permissions""" view_class = MultiplePermissionsRequiredView view_url = "/multiple_permissions_required/" def build_authorized_user(self): """Get a user with permissions""" return UserFactory( permissions=[ "tests.add_article", "tests.change_article", "auth.change_user", ] ) def build_unauthorized_user(self): """Get a user without the important permissions""" 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, "/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_str(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_str(resp.content)) user = UserFactory(permissions=[]) with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(user=user), raise_exception=True, permissions=permissions, ) def test_all_object_level_permissions_key(self): """ Tests that when a user has all the correct object level permissions, response is OK, else forbidden. """ # Arrange article = ArticleFactory() self.view_class = MultiplePermissionsRequiredView self.view_url = f"/multiple_object_level_permissions_required/?pk={article.pk}" auth_add_user = Permission.objects.get(codename="add_user") tests_add_article = Permission.objects.get(codename="add_article") permissions = {"all": ["auth.add_user", "tests.add_article"]} valid_user = UserFactory(permissions=permissions["all"]) invalid_user = UserFactory(permissions=["auth.add_user"]) UserObjectPermissionsFactory( user=valid_user, permission=auth_add_user, article_object=article ) UserObjectPermissionsFactory( user=valid_user, permission=tests_add_article, article_object=article ) # Act valid_req = self.build_request(path=self.view_url, user=valid_user) valid_resp = self.dispatch_view( valid_req, permissions=permissions, object_level_permissions=True ) invalid_req = self.build_request(path=self.view_url, user=invalid_user) # Arrange self.assertEqual(valid_resp.status_code, 200) with self.assertRaises(PermissionDenied): self.dispatch_view( invalid_req, permissions=permissions, object_level_permissions=True, raise_exception=True, ) def test_any_object_level_permissions_key(self): """ Tests that when a user has any the correct object level permissions, response is OK, else forbidden. """ # Arrange article = ArticleFactory() self.view_url = f"/multiple_object_level_permissions_required/?pk={article.pk}" self.view_class = MultiplePermissionsRequiredView auth_add_user = Permission.objects.get(codename="add_user") tests_add_article = Permission.objects.get(codename="add_article") permissions = {"any": ["auth.add_user", "tests.add_article"]} user = UserFactory(permissions=[permissions["any"][0]]) user_1 = UserFactory() user_2 = UserFactory(permissions=permissions["any"]) UserObjectPermissionsFactory( user=user, permission=auth_add_user, article_object=article ) UserObjectPermissionsFactory( user=user, permission=tests_add_article, article_object=article ) # Act valid_req = self.build_request(path=self.view_url, user=user) valid_resp = self.dispatch_view( valid_req, permissions=permissions, object_level_permissions=True, raise_exception=True, ) invalid_req_1 = self.build_request(path=self.view_url, user=user_1) invalid_req_2 = self.build_request(path=self.view_url, user=user_2) # Assert self.assertEqual(valid_resp.status_code, 200) with self.assertRaises(PermissionDenied): self.dispatch_view( invalid_req_1, permissions=permissions, object_level_permissions=True, raise_exception=True, ) with self.assertRaises(PermissionDenied): self.dispatch_view( invalid_req_2, permissions=permissions, object_level_permissions=True, raise_exception=True, ) @pytest.mark.django_db class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios requiring a superuser""" view_class = SuperuserRequiredView view_url = "/superuser_required/" def build_authorized_user(self): """Make a superuser""" return UserFactory(is_superuser=True, is_staff=True) def build_unauthorized_user(self): """Make a non-superuser""" return UserFactory() @pytest.mark.django_db class TestStaffuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios requiring a staff user""" view_class = StaffuserRequiredView view_url = "/staffuser_required/" def build_authorized_user(self): """Hire a user""" return UserFactory(is_staff=True) def build_unauthorized_user(self): """Get a customer""" return UserFactory() @pytest.mark.django_db class TestGroupRequiredMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios requiring membership in a certain group""" view_class = GroupRequiredView view_url = "/group_required/" def build_authorized_user(self): """Get a user with the right group""" user = UserFactory() group = GroupFactory(name="test_group") user.groups.add(group) return user def build_superuser(self): """Get a superuser""" user = UserFactory() user.is_superuser = True user.save() return user def build_unauthorized_user(self): """Just a normal users, not super and no groups""" return UserFactory() def test_with_string(self): """A group name as a string should restrict access""" self.assertEqual("test_group", self.view_class.group_required) 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_str(resp.content)) def test_with_group_list(self): """A list of group names should restrict access""" group_list = ["test_group", "editors"] # the test client will instantiate a new view on request, so we have to # modify the class variable (and restore it when the test finished) self.view_class.group_required = group_list self.assertEqual(group_list, self.view_class.group_required) 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_str(resp.content)) self.view_class.group_required = "test_group" self.assertEqual("test_group", self.view_class.group_required) def test_superuser_allowed(self): """Superusers should always be allowed, regardless of group rules""" user = self.build_superuser() 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_str(resp.content)) def test_improperly_configured(self): """No group(s) specified should raise ImproperlyConfigured""" 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): """Unicode in group names should restrict access""" self.view_class.group_required = "niño" self.assertEqual("niño", self.view_class.group_required) user = self.build_authorized_user() group = user.groups.all()[0] group.name = "niño" group.save() self.assertEqual("niño", user.groups.all()[0].name) 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_str(resp.content)) self.view_class.group_required = "test_group" self.assertEqual("test_group", self.view_class.group_required) @pytest.mark.django_db class TestUserPassesTestMixin(_TestAccessBasicsMixin, test.TestCase): """Scenarios requiring a user to pass a test""" 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 passing func_test def build_authorized_user(self, is_superuser=False): """Get a test-passing user""" return UserFactory( is_superuser=is_superuser, is_staff=True, email="user@mydomain.com" ) def build_unauthorized_user(self): """Get a blank user""" return UserFactory() def test_with_user_pass(self): """Valid username and password should pass the test""" 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_str(resp.content)) def test_with_user_not_pass(self): """A failing user should be redirected""" 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): """PermissionDenied should be raised""" with self.assertRaises(PermissionDenied): self.dispatch_view( self.build_request(path=self.view_url), raise_exception=True ) def test_not_implemented(self): """NotImplemented should be raised""" view = self.view_not_implemented_class() with self.assertRaises(NotImplementedError): view.dispatch( self.build_request(path=self.view_not_implemented_url), raise_exception=True, ) @pytest.mark.django_db class TestSSLRequiredMixin(test.TestCase): """Scenarios around requiring SSL""" view_class = SSLRequiredView view_url = "/sslrequired/" def test_ssl_redirection(self): """Should redirect if not SSL""" self.view_url = f"https://testserver{self.view_url}" self.view_class.raise_exception = False resp = self.client.get(self.view_url) self.assertRedirects(resp, self.view_url, status_code=301) resp = self.client.get(self.view_url, follow=True) self.assertEqual(200, resp.status_code) self.assertEqual("https", resp.request.get("wsgi.url_scheme")) def test_raises_exception(self): """Should return 404""" self.view_class.raise_exception = True resp = self.client.get(self.view_url) self.assertEqual(404, resp.status_code) @override_settings(DEBUG=True) def test_debug_bypasses_redirect(self): """Debug mode should not require SSL""" self.view_class.raise_exception = False resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) def test_https_does_not_redirect(self): """SSL requests should not redirect""" self.view_class.raise_exception = False resp = self.client.get(self.view_url, secure=True) self.assertEqual(200, resp.status_code) self.assertEqual("https", resp.request.get("wsgi.url_scheme")) @pytest.mark.django_db class TestRecentLoginRequiredMixin(test.TestCase): """Scenarios requiring a recent login""" view_class = RecentLoginRequiredView recent_view_url = "/recent_login/" outdated_view_url = "/outdated_login/" def test_recent_login(self): """A recent login should get a 200""" self.view_class.max_last_login_delta = 1800 last_login = datetime.datetime.now() last_login = make_aware(last_login, get_current_timezone()) user = UserFactory(last_login=last_login) self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.recent_view_url) assert resp.status_code == 200 assert force_str(resp.content) == "OK" def test_outdated_login(self): """An outdated login should get a 302""" self.view_class.max_last_login_delta = 0 last_login = datetime.datetime.now() - datetime.timedelta(hours=2) last_login = make_aware(last_login, get_current_timezone()) user = UserFactory(last_login=last_login) self.client.login(username=user.username, password="asdf1234") resp = self.client.get(self.outdated_view_url) assert resp.status_code in [ 302, 405, ] # 302 is for Django < 5, while 405 is for Django >= 5 def test_not_logged_in(self): """Anonymous requests should be handled appropriately""" last_login = datetime.datetime.now() last_login = make_aware(last_login, get_current_timezone()) resp = self.client.get(self.recent_view_url) assert resp.status_code != 200 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/test_ajax_mixins.py0000664000175000017500000001713414766144450020553 0ustar00klovekloveimport json from unittest import mock from django import test from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from django.utils.encoding import force_str from braces.views import AjaxResponseMixin from .factories import ArticleFactory, UserFactory from .helpers import TestViewHelper from .views import SimpleJsonView, JsonRequestResponseView, JsonBadRequestView class TestAjaxResponseMixin(TestViewHelper, test.TestCase): """ Tests for AjaxResponseMixin. """ methods = ["get", "post", "put", "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( "/ajax_response/", HTTP_X_REQUESTED_WITH="XMLHttpRequest" ) assert force_str(resp.content) == "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("/ajax_response/") assert force_str(resp.content) == "OK" def test_fallback_to_normal_methods(self): """ Ajax methods should fallback to normal methods by default. """ test_cases = [ ("get", "get"), ("post", "post"), ("put", "get"), ("delete", "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, "{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( "application/json", resp["content-type"].split(";")[0] ) def get_content(self, url): """ GET url and return content """ resp = self.client.get(url) self.assert_json_response(resp) content = force_str(resp.content) return content def test_simple_json(self): """ Tests render_json_response() method. """ user = UserFactory() self.client.login(username=user.username, password="asdf1234") data = json.loads(self.get_content("/simple_json/")) self.assertEqual({"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("/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["fields"])) titles.append(row["fields"]["title"]) self.assertIn(a1.title, titles) self.assertIn(a2.title, titles) def test_bad_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=["a"]) 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="asfa") normal_content = self.get_content("/simple_json/") self.view_class.json_dumps_kwargs = {"indent": 2} pretty_content = self.get_content("/simple_json/") normal_json = json.loads("{0}".format(normal_content)) pretty_json = json.loads("{0}".format(pretty_content)) self.assertEqual(normal_json, pretty_json) self.assertTrue(len(pretty_content) > len(normal_content)) def test_json_encoder_class_atrribute(self): """ Tests setting custom `json_encoder_class` attribute. """ data = json.loads(self.get_content("/simple_json_custom_encoder/")) self.assertEqual({"numbers": [1, 2, 3]}, data) class TestJsonRequestResponseMixin(TestViewHelper, test.TestCase): view_class = JsonRequestResponseView request_dict = {"status": "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("utf-8") response = self.client.post( "/json_request/", content_type="application/json", data=data ) response_json = json.loads(response.content.decode("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("/json_request/", data=self.request_dict) response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 200) self.assertEqual(response_json, None) 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( "/json_custom_bad_request/", data=self.request_dict ) response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 400) self.assertEqual(response_json, {"error": "you messed up"}) class TestJsonBadRequestMixin(TestViewHelper, test.TestCase): view_class = JsonBadRequestView request_dict = {"status": "operational"} 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( "/json_bad_request/", data=self.request_dict ) response_json = json.loads(response.content.decode("utf-8")) self.assertEqual(response.status_code, 400) self.assertEqual(response_json, self.view_class.error_response_dict) def test_options_request_with_required_json_should_pass(self): """ If a the client sends an OPTIONS request, even if require_json is set to true the client should get not a 400 error, because: * it's not possible to send HTTP body within an OPTIONS request * it's not up to an OPTIONS request to decide if the payload is valid """ response = self.client.options("/json_bad_request/", data=None) self.assertEqual(response.status_code, 200) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/test_forms.py0000664000175000017500000000133614766144450017364 0ustar00kloveklovefrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/test_other_mixins.py0000664000175000017500000005703014766144450020750 0ustar00kloveklovefrom django.contrib import messages from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.storage.base import Message from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from django import test from django.test.utils import override_settings from django.views.generic import View from django.utils.encoding import force_str from braces.views import ( SetHeadlineMixin, MessageMixin, FormValidMessageMixin, FormInvalidMessageMixin, ) from .factories import UserFactory from .models import Article, CanonicalArticle from .views import ( FormMessagesView, ContextView, ) class TestSuccessURLRedirectListMixin(test.TestCase): """Scenarios around redirecting after a successful form submission""" def test_redirect(self): """Successful POST should redirect""" 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): """Improper setup should raise ImproperlyConfigured""" data = {"title": "Test body", "body": "Test body"} with self.assertRaises(ImproperlyConfigured): # The view at this endpoint has no success_url defined self.client.post("/article_list_bad/create/", data) class TestUserFormKwargsMixin(test.TestCase): """Scenarios around automatically including a user in form submissions""" def test_post_method(self): """A POST request should include the user kwarg""" user = UserFactory() self.client.login(username=user.username, password="asdf1234") resp = self.client.post("/form_with_user_kwarg/", {"field1": "foo"}) assert force_str(resp.content) == f"username: {user.username}" def test_get_method(self): """A GET request should include the user kwarg""" 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): """Scenarios around setting a headline""" def test_dynamic_headline(self): """A method-provided headline should be included in context""" resp = self.client.get("/headline/test-headline/") self.assertEqual("test-headline", resp.context["headline"]) def test_context_data(self): """An attribute-provided headline should be in the context""" resp = self.client.get("/headline/foo-bar/") self.assertEqual("foo-bar", resp.context["headline"]) def test_improper_configuration(self): """Not providing a headline should raise an exception""" mixin = SetHeadlineMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_headline() def test_get_headline_lazy(self): """Lazy evaluation of text should still provide the headline""" resp = self.client.get("/headline/lazy/") self.assertEqual("Test Headline", resp.context["headline"]) class TestStaticContextMixin(test.TestCase): """Scenarios around including static content in the context""" view_class = ContextView view_url = "/context/" def test_dictionary(self): """Static content can be included as a dictionary""" self.view_class.static_context = {"test": True} resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual(True, resp.context["test"]) def test_two_tuple(self): """Static content can be included as a two-tuple pair""" self.view_class.static_context = [("a", 1), ("b", 2)] resp = self.client.get(self.view_url) self.assertEqual(200, resp.status_code) self.assertEqual(1, resp.context["a"]) self.assertEqual(2, resp.context["b"]) def test_not_set(self): """ImproperlyConfigured should be raised if no static content is set""" self.view_class.static_context = None with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) def test_string_value_error(self): """A string should raise ImproperlyConfigured""" self.view_class.static_context = "Fail" with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) def test_list_error(self): """A list should raise ImproperlyConfigured""" self.view_class.static_context = ["fail", "fail"] with self.assertRaises(ImproperlyConfigured): self.client.get(self.view_url) class TestCsrfExemptMixin(test.TestCase): """Scenarios around views which are CSRF exempt""" def setUp(self): """Ensure the client enforces CSRF checks""" super(TestCsrfExemptMixin, self).setUp() self.client = self.client_class(enforce_csrf_checks=True) def test_csrf_token_is_not_required(self): """CSRF tokens should not be required""" resp = self.client.post("/csrf_exempt/", {"field1": "test"}) self.assertEqual(200, resp.status_code) self.assertEqual("OK", force_str(resp.content)) class TestCanonicalSlugDetailView(test.TestCase): """Scenarios involving canonical slugs""" def setUp(self): """Create the two articles""" Article.objects.create(title="Alpha", body="Zet", slug="alpha") 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): """Scenarios around canonical slugs and namespaces""" def setUp(self): """Create the necessary articles""" Article.objects.create(title="Alpha", body="Zet", slug="alpha") 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): """Scenarios involving overridden canonical slugs""" def setUp(self): """Create the necessary articles""" Article.objects.create(title="Alpha", body="Zet", slug="alpha") 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 TestCustomUrlKwargsCanonicalSlugDetailView(test.TestCase): """Scenarios around canonical slugs and custom URL kwargs""" def setUp(self): """Create the articles""" Article.objects.create(title="Alpha", body="Zet", slug="alpha") 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-custom-kwargs/1-alpha/") self.assertEqual(resp.status_code, 200) resp = self.client.get("/article-canonical-custom-kwargs/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-custom-kwargs/1-bad-slug/") self.assertEqual(resp.status_code, 301) resp = self.client.get("/article-canonical-custom-kwargs/2-bad-slug/") self.assertEqual(resp.status_code, 301) class TestModelCanonicalSlugDetailView(test.TestCase): """Scenarios around canonical slugs and model fields""" def setUp(self): """Generate the necessary articles""" CanonicalArticle.objects.create( title="Alpha", body="Zet", slug="alpha" ) 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) # CookieStorage is used because it doesn't require middleware to be installed @override_settings( MESSAGE_STORAGE="django.contrib.messages.storage.cookie.CookieStorage" ) class MessageMixinTests(test.TestCase): """Scenarios around the messaging framework""" def setUp(self): """Create necessary objects""" self.rf = test.RequestFactory() self.middleware = MessageMiddleware("") def get_request(self, *args, **kwargs): """Generate a request that has passed through the middleware""" request = self.rf.get("/") self.middleware.process_request(request) return request def get_response(self, request, view): """Generate a response that has been passed through the middleware""" response = view(request) self.middleware.process_response(request, response) return response def get_request_response(self, view, *args, **kwargs): """Get both a request and a response, middleware-processed""" request = self.get_request(*args, **kwargs) response = self.get_response(request, view) return request, response def test_add_messages(self): """Message should be added through the class attribute""" class TestView(MessageMixin, View): def get(self, request): self.messages.add_message(messages.SUCCESS, "test") return HttpResponse("OK") request, response = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0].message, "test") self.assertEqual(msg[0].level, messages.SUCCESS) def test_get_messages(self): """get_messages should get the stored messages""" class TestView(MessageMixin, View): def get(self, request): self.messages.add_message(messages.SUCCESS, "success") self.messages.add_message(messages.WARNING, "warning") content = ",".join( m.message for m in self.messages.get_messages() ) return HttpResponse(content) _, response = self.get_request_response(TestView.as_view()) self.assertEqual(response.content, b"success,warning") def test_get_level(self): """Should be able to get message levels""" class TestView(MessageMixin, View): def get(self, request): return HttpResponse(self.messages.get_level()) _, response = self.get_request_response(TestView.as_view()) self.assertEqual(int(response.content), messages.INFO) # default def test_set_level(self): """Should be able to set message levels""" class TestView(MessageMixin, View): def get(self, request): self.messages.set_level(messages.WARNING) self.messages.add_message(messages.SUCCESS, "success") self.messages.add_message(messages.WARNING, "warning") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(msg, [Message(messages.WARNING, "warning")]) @override_settings(MESSAGE_LEVEL=messages.DEBUG) def test_debug(self): """Messages should able to be set as DEBUG""" class TestView(MessageMixin, View): def get(self, request): self.messages.debug("test") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0], Message(messages.DEBUG, "test")) def test_info(self): """Messages should able to be set as INFO""" class TestView(MessageMixin, View): def get(self, request): self.messages.info("test") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0], Message(messages.INFO, "test")) def test_success(self): """Messages should able to be set as SUCCESS""" class TestView(MessageMixin, View): def get(self, request): self.messages.success("test") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0], Message(messages.SUCCESS, "test")) def test_warning(self): """Messages should able to be set as WARNING""" class TestView(MessageMixin, View): def get(self, request): self.messages.warning("test") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0], Message(messages.WARNING, "test")) def test_error(self): """Messages should able to be set as ERROR""" class TestView(MessageMixin, View): def get(self, request): self.messages.error("test") return HttpResponse("OK") request, _ = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0], Message(messages.ERROR, "test")) def test_invalid_attribute(self): """Raise an AttributeError if setting an invalid level""" class TestView(MessageMixin, View): def get(self, request): self.messages.invalid() return HttpResponse("OK") with self.assertRaises(AttributeError): self.get_request_response(TestView.as_view()) def test_wrapper_available_in_dispatch(self): """ Make sure that self.messages is available in dispatch() even before calling the parent's implementation. """ class TestView(MessageMixin, View): def dispatch(self, request): self.messages.add_message(messages.SUCCESS, "test") return super(TestView, self).dispatch(request) def get(self, request): return HttpResponse("OK") request, response = self.get_request_response(TestView.as_view()) msg = list(request._messages) self.assertEqual(len(msg), 1) self.assertEqual(msg[0].message, "test") self.assertEqual(msg[0].level, messages.SUCCESS) def test_API(self): """ Make sure that our assumptions about messages.api are still valid. """ # This test is designed to break when django.contrib.messages.api # changes (items being added or removed). excluded_API = set() excluded_API.add("MessageFailure") class TestFormMessageMixins(test.TestCase): """Scenarios around form valid/invalid messages""" def setUp(self): self.good_data = {"title": "Good", "body": "Body"} self.bad_data = {"body": "Missing title"} def test_valid_message(self): """If the form is valid, the valid message should be available""" 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): """If the form is invalid, the invalid message should be available""" 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): """Not setting a form_valid message should raise ImproperlyConfigured""" mixin = FormValidMessageMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_form_valid_message() def test_form_valid_message_not_str(self): """Non-strings for the form_valid message should raise ImproperlyConfigured""" mixin = FormValidMessageMixin() mixin.form_valid_message = ["bad"] with self.assertRaises(ImproperlyConfigured): mixin.get_form_valid_message() def test_form_valid_returns_message(self): """get_form_valid_message should return the form_valid message""" mixin = FormValidMessageMixin() mixin.form_valid_message = "Good øø" self.assertEqual(force_str("Good øø"), mixin.get_form_valid_message()) def test_form_invalid_message_not_set(self): """Not setting a form_invalid message should raise ImproperlyConfigured""" mixin = FormInvalidMessageMixin() with self.assertRaises(ImproperlyConfigured): mixin.get_form_invalid_message() def test_form_invalid_message_not_str(self): """Non-strings for the form_invalid message should raise ImproperlyConfigured""" mixin = FormInvalidMessageMixin() mixin.form_invalid_message = ["bad"] with self.assertRaises(ImproperlyConfigured): mixin.get_form_invalid_message() def test_form_invalid_returns_message(self): """get_form_invalid_message should return the form_invalid message""" mixin = FormInvalidMessageMixin() mixin.form_invalid_message = "Bad øø" self.assertEqual(force_str("Bad øø"), mixin.get_form_invalid_message()) class TestAllVerbsMixin(test.TestCase): """Scenarios around the AllVerbsMixin""" def setUp(self): self.url = "/all_verbs/" self.no_handler_url = "/all_verbs_no_handler/" def test_options(self): """AllVerbs should respond to OPTION""" response = self.client.options(self.url) self.assertEqual(response.status_code, 200) def test_get(self): """AllVerbs should respond to GET""" response = self.client.get(self.url) self.assertEqual(response.status_code, 200) def test_head(self): """AllVerbs should respond to HEAD""" response = self.client.head(self.url) self.assertEqual(response.status_code, 200) def test_post(self): """AllVerbs should respond to POST""" response = self.client.post(self.url) self.assertEqual(response.status_code, 200) def test_put(self): """AllVerbs should respond to PUT""" response = self.client.put(self.url) self.assertEqual(response.status_code, 200) def test_delete(self): """AllVerbs should respond to DELETE""" response = self.client.delete(self.url) self.assertEqual(response.status_code, 200) def test_patch(self): """AllVerbs should respond to PATCH""" response = self.client.patch(self.url) self.assertEqual(response.status_code, 200) def test_no_all_handler(self): """A missing handler should raise ImproperlyConfigured""" with self.assertRaises(ImproperlyConfigured): self.client.get("/all_verbs_no_handler/") class TestHeaderMixin(test.TestCase): """Scenarios around the extra headers mixin""" def test_attribute(self): """Headers can be set via an attribute""" response = self.client.get("/headers/attribute/") self.assertEqual(response["X-DJANGO-BRACES-1"], "1") self.assertEqual(response["X-DJANGO-BRACES-2"], "2") def test_method(self): """Headers can be set via a method""" response = self.client.get("/headers/method/") self.assertEqual(response["X-DJANGO-BRACES-1"], "1") self.assertEqual(response["X-DJANGO-BRACES-2"], "2") def test_existing(self): """Existing headers should still come through""" response = self.client.get('/headers/existing/') self.assertEqual(response['X-DJANGO-BRACES-EXISTING'], 'value') class TestCacheControlMixin(test.TestCase): """Scenarios around controlling cache""" def test_cachecontrol_public(self): """Cache settings should be respected and included""" response = self.client.get('/cachecontrol/public/') options = [i.strip() for i in response['Cache-Control'].split(',')] self.assertEqual(sorted(options), ['max-age=60', 'public']) class TestNeverCacheMixin(test.TestCase): """Scenarios around marking a view as never-cached""" def test_nevercache(self): """Views marked as no-cache should not be cached""" response = self.client.get('/nevercache/') options = [i.strip() for i in response['Cache-Control'].split(',')] expected_cache_control_options = {"max-age=0", "must-revalidate", "no-cache", "no-store", "private"} self.assertTrue(set(options).intersection(expected_cache_control_options)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/test_queries.py0000664000175000017500000002244214766144450017714 0ustar00kloveklovefrom unittest import mock import pytest from django.core.exceptions import ImproperlyConfigured from django import test from .helpers import TestViewHelper from .models import Article from .views import ( ArticleListView, ArticleListViewWithCustomQueryset, AuthorDetailView, OrderableListView ) class TestSelectRelatedMixin(TestViewHelper, test.TestCase): """Scenarios related to adding select_related to queries""" view_class = ArticleListView def test_missing_select_related(self): """If select_related is unset, raise ImproperlyConfigured""" with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), select_related=None) def test_invalid_select_related(self): """If select_related is not a list or tuple, raise ImproperlyConfigured""" 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): """QuerySet.select_related should be called with the 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") @mock.patch("django.db.models.query.QuerySet.select_related") def test_select_related_keeps_select_related_from_queryset(self, m): """ Checks that an empty select_related attribute does not cancel a select_related provided by queryset. """ qs = Article.objects.all() qs.select_related = m m.reset_mock() with pytest.warns(UserWarning): resp = self.dispatch_view( self.build_request(), view_class=ArticleListViewWithCustomQueryset, ) self.assertEqual(200, resp.status_code) self.assertEqual(0, m.call_count) class TestPrefetchRelatedMixin(TestViewHelper, test.TestCase): """Scenarios related to adding prefetch_related to queries""" view_class = AuthorDetailView def test_missing_prefetch_related(self): """If prefetch_related is missing/None, raise ImproperlyConfigured""" with self.assertRaises(ImproperlyConfigured): self.dispatch_view(self.build_request(), prefetch_related=None) def test_invalid_prefetch_related(self): """If prefetch_related is not a list or tuple, raise ImproperlyConfigured""" 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): """QuerySet.prefetch_related() should be 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") @mock.patch("django.db.models.query.QuerySet.prefetch_related") def test_prefetch_related_keeps_select_related_from_queryset(self, m): """ Checks that an empty prefetch_related attribute does not cancel a prefetch_related provided by queryset. """ qs = Article.objects.all() qs.prefetch_related = m m.reset_mock() with pytest.warns(UserWarning): resp = self.dispatch_view( self.build_request(), view_class=ArticleListViewWithCustomQueryset, ) self.assertEqual(200, resp.status_code) self.assertEqual(0, m.call_count) class TestOrderableListMixin(TestViewHelper, test.TestCase): """Scenarios involving ordering records""" view_class = OrderableListView def __make_test_articles(self): """Generate a couple of articles""" a1 = Article.objects.create(title="Alpha", body="Zet") a2 = Article.objects.create(title="Zet", body="Alpha") return a1, a2 def test_correct_order(self): """Valid column and order query arguments should order the objects""" 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_correct_order_with_default_ordering(self): """A valid order_by query argument should sort the default direction""" a1, a2 = self.__make_test_articles() resp = self.dispatch_view( self.build_request(path="?order_by=id"), orderable_columns=None, ordering_default=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"), orderable_columns=None, ordering_default="asc", 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"), orderable_columns=None, ordering_default="desc", get_orderable_columns=lambda: ( "id", "title", ), ) self.assertEqual(list(resp.context_data["object_list"]), [a2, a1]) def test_correct_order_with_param_not_default_ordering(self): """ Objects must be properly ordered if requested with valid column names and ordering option in the query params. In this case, the ordering_default will be overwritten. """ a1, a2 = self.__make_test_articles() resp = self.dispatch_view( self.build_request(path="?order_by=id&ordering=asc"), orderable_columns=None, ordering_default="desc", get_orderable_columns=lambda: ( "id", "title", ), ) self.assertEqual(list(resp.context_data["object_list"]), [a1, a2]) def test_correct_order_with_incorrect_default_ordering(self): """ Objects must be properly ordered if requested with valid column names and with the default ordering """ view = self.view_class() view.ordering_default = "improper_default_value" self.assertRaises( ImproperlyConfigured, lambda: view.get_ordering_default() ) 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 if 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 if 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]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/urls.py0000664000175000017500000001256514766146657016205 0ustar00kloveklovefrom django.contrib.auth.views import LoginView from django.urls import path from django.urls import include, re_path from . import views urlpatterns = [ # LoginRequiredMixin tests path("login_required/", views.LoginRequiredView.as_view()), # AnonymousRequiredView tests path( "unauthenticated_view/", views.AnonymousRequiredView.as_view(), name="unauthenticated_view", ), path( "authenticated_view/", views.AuthenticatedView.as_view(), name="authenticated_view", ), # AjaxResponseMixin tests path("ajax_response/", views.AjaxResponseView.as_view()), # CreateAndRedirectToEditView tests path("article/create/", views.CreateArticleView.as_view()), path( "article//edit/", views.EditArticleView.as_view(), name="edit_article", ), path( "article_list/create/", views.CreateArticleAndRedirectToListView.as_view(), ), path( "article_list_bad/create/", views.CreateArticleAndRedirectToListViewBad.as_view(), ), path( "article_list/", views.ArticleListView.as_view(), name="article_list", ), # CanonicalSlugDetailMixin tests re_path( r"^article-canonical/(?P\d+)-(?P[-\w]+)/$", views.CanonicalSlugDetailView.as_view(), name="canonical_slug", ), re_path( r"^article-canonical-override/(?P\d+)-(?P[-\w]+)/$", views.OverriddenCanonicalSlugDetailView.as_view(), name="canonical_override", ), re_path( r"^article-canonical-custom-kwargs/(?P\d+)-(?P[-\w]+)/$", views.CanonicalSlugDetailCustomUrlKwargsView.as_view(), name="canonical_custom_kwargs", ), re_path( r"^article-canonical-model/(?P\d+)-(?P[-\w]+)/$", views.ModelCanonicalSlugDetailView.as_view(), name="canonical_model", ), # UserFormKwargsMixin tests path("form_with_user_kwarg/", views.FormWithUserKwargView.as_view()), # SetHeadlineMixin tests path("headline/", views.HeadlineView.as_view(), name="headline"), path("headline/lazy/", views.LazyHeadlineView.as_view()), re_path(r"^headline/(?P[\w-]+)/$", views.DynamicHeadlineView.as_view()), # ExtraContextMixin tests path("context/", views.ContextView.as_view(), name="context"), # PermissionRequiredMixin tests path("permission_required/", views.PermissionRequiredView.as_view()), path("object_level_permission_required/", views.PermissionRequiredView.as_view(object_level_permissions=True)), # MultiplePermissionsRequiredMixin tests path( "multiple_permissions_required/", views.MultiplePermissionsRequiredView.as_view(), ), path( "multiple_object_level_permissions_required/", views.MultiplePermissionsRequiredView.as_view(object_level_permissions=True), ), # SuperuserRequiredMixin tests path("superuser_required/", views.SuperuserRequiredView.as_view()), # StaffuserRequiredMixin tests path("staffuser_required/", views.StaffuserRequiredView.as_view()), # GroupRequiredMixin tests path("group_required/", views.GroupRequiredView.as_view()), # UserPassesTestMixin tests path("user_passes_test/", views.UserPassesTestView.as_view()), # UserPassesTestMixin tests path( "user_passes_test_not_implemented/", views.UserPassesTestNotImplementedView.as_view(), ), # CsrfExemptMixin tests path("csrf_exempt/", views.CsrfExemptView.as_view()), # JSONResponseMixin tests path("simple_json/", views.SimpleJsonView.as_view()), path( "simple_json_custom_encoder/", views.CustomJsonEncoderView.as_view() ), path("simple_json_400/", views.SimpleJsonBadRequestView.as_view()), path("article_list_json/", views.ArticleListJsonView.as_view()), # JsonRequestResponseMixin tests path("json_request/", views.JsonRequestResponseView.as_view()), path("json_bad_request/", views.JsonBadRequestView.as_view()), path( "json_custom_bad_request/", views.JsonCustomBadRequestView.as_view() ), # FormMessagesMixin tests path("form_messages/", views.FormMessagesView.as_view()), # AllVerbsMixin tests path("all_verbs/", views.AllVerbsView.as_view()), path( "all_verbs_no_handler/", views.AllVerbsView.as_view(all_handler=None) ), # SSLRequiredMixin tests path("sslrequired/", views.SSLRequiredView.as_view()), # RecentLoginRequiredMixin tests path("recent_login/", views.RecentLoginRequiredView.as_view()), path("outdated_login/", views.RecentLoginRequiredView.as_view()), # HeaderMixin tests path('headers/attribute/', views.AttributeHeaderView.as_view()), path('headers/method/', views.MethodHeaderView.as_view()), path('headers/existing/', views.ExistingHeaderView.as_view()), # CacheControlMixin tests path('cachecontrol/public/', views.CacheControlPublicView.as_view()), # NeverCacheMixin tests path('nevercache/', views.NeverCacheView.as_view()), ] urlpatterns += [ path( "accounts/login/", LoginView.as_view(template_name="blank.html") ), path("auth/login/", LoginView.as_view(template_name="blank.html")), ] urlpatterns += [ path( "article-canonical-namespaced/", include( ("tests.urls_namespaced", "tests"), namespace="some_namespace" ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742260520.0 django_braces-1.17.0/tests/urls_namespaced.py0000664000175000017500000000034514766144450020343 0ustar00kloveklovefrom django.urls import re_path from . import views urlpatterns = [ re_path( r"^article/(?P\d+)-(?P[\w-]+)/$", views.CanonicalSlugDetailView.as_view(), name="namespaced_article", ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1742261679.0 django_braces-1.17.0/tests/views.py0000664000175000017500000003060714766146657016352 0ustar00klovekloveimport codecs from django.contrib.auth.models import User from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ 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 from .helpers import SetJSONEncoder class OkView(View): """ A view which simply returns "OK" for every request. """ def get(self, request): """Everything is going to be OK""" return HttpResponse("OK") def post(self, request): """Get it?""" return self.get(request) def put(self, request): """Get it?""" return self.get(request) def delete(self, request): """Get it?""" return self.get(request) class LoginRequiredView(views.LoginRequiredMixin, OkView): """ A view for testing LoginRequiredMixin. """ class AnonymousRequiredView(views.AnonymousRequiredMixin, OkView): """ A view for testing AnonymousRequiredMixin. Should accept unauthenticated users and redirect authenticated users to the authenticated_redirect_url set on the view. """ authenticated_redirect_url = "/authenticated_view/" class AuthenticatedView(views.LoginRequiredMixin, OkView): """ A view for testing AnonymousRequiredMixin. Should accept authenticated users. """ class AjaxResponseView(views.AjaxResponseMixin, OkView): """ A view for testing AjaxResponseMixin. """ def get_ajax(self, request): """Everything will eventually be OK""" return HttpResponse("AJAX_OK") def post_ajax(self, request): """Get it?""" return self.get_ajax(request) def put_ajax(self, request): """Get it?""" return self.get_ajax(request) def delete_ajax(self, request): """Get it?""" return self.get_ajax(request) class SimpleJsonView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's render_json_response() method. """ def get(self, request): """Send back some JSON""" object = {"username": request.user.username} return self.render_json_response(object) class CustomJsonEncoderView(views.JSONResponseMixin, View): """ A view for testing JSONResponseMixin's `json_encoder_class` attribute with custom JSONEncoder class. """ json_encoder_class = SetJSONEncoder def get(self, request): """Send back some JSON""" object = {"numbers": set([1, 2, 3])} 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): """Send back some JSON""" 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): """Send back some JSON""" 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): """Send back some JSON""" 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): """Send back some JSON""" 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): """Handle the POST request""" 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. """ fields = ["author", "title", "body", "slug"] 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 ArticleListViewWithCustomQueryset(views.SelectRelatedMixin, ListView): """ Another list view for articles, required to test SelectRelatedMixin. """ queryset = Article.objects.select_related("author").prefetch_related( "article_set" ) template_name = "blank.html" select_related = () class FormWithUserKwargView(views.UserFormKwargsMixin, FormView): """ View for testing UserFormKwargsMixin. """ form_class = FormWithUserKwarg template_name = "form.html" def form_valid(self, form): """A simple response to watch for""" return HttpResponse(f"username: {form.user.username}") class HeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin. """ template_name = "blank.html" headline = "Test headline" class LazyHeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin. """ template_name = "blank.html" headline = _("Test Headline") class ContextView(views.StaticContextMixin, TemplateView): """View for testing StaticContextMixin.""" template_name = "blank.html" static_context = {"test": True} class DynamicHeadlineView(views.SetHeadlineMixin, TemplateView): """ View for testing SetHeadlineMixin's get_headline() method. """ template_name = "blank.html" def get_headline(self): """Return the headline passed in via kwargs""" return self.kwargs["s"] class PermissionRequiredView(views.PermissionRequiredMixin, OkView): """ View for testing PermissionRequiredMixin. """ permission_required = "auth.add_user" def get_object(self): """ Get the article object for the query parameter's primary key """ article_obj = None object_pk = self.request.GET.get("pk") if object_pk: article_obj = Article.objects.get(pk=object_pk) return article_obj class MultiplePermissionsRequiredView( views.MultiplePermissionsRequiredMixin, OkView ): permissions = { "all": ["tests.add_article", "tests.change_article"], "any": ["auth.add_user", "auth.change_user"], } def get_object(self): """ Get the article object for the query parameter's primary key """ article_obj = None object_pk = self.request.GET.get("pk") if object_pk: article_obj = Article.objects.get(pk=object_pk) return article_obj class SuperuserRequiredView(views.SuperuserRequiredMixin, OkView): """Require a superuser""" class StaffuserRequiredView(views.StaffuserRequiredMixin, OkView): """Require a user marked as `is_staff`""" class CsrfExemptView(views.CsrfExemptMixin, OkView): """Ignore CSRF""" class AuthorDetailView(views.PrefetchRelatedMixin, ListView): """A basic detail view to test prefetching""" model = User prefetch_related = ["article_set"] template_name = "blank.html" class OrderableListView(views.OrderableListMixin, ListView): """A basic list view to test ordering the output""" model = Article orderable_columns = ( "id", "title", ) orderable_columns_default = "id" class CanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): """A basic detail view to test a canonical slug""" model = Article template_name = "blank.html" class OverriddenCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): """A basic detail view to test an overridden slug""" model = Article template_name = "blank.html" def get_canonical_slug(self): """Give back a different, encoded slug. My slug secrets are safe""" return codecs.encode(self.get_object().slug, "rot_13") class CanonicalSlugDetailCustomUrlKwargsView(views.CanonicalSlugDetailMixin, DetailView): """A basic detail view to test a slug with custom URL stuff""" model = Article template_name = "blank.html" pk_url_kwarg = "my_pk" slug_url_kwarg = "my_slug" class ModelCanonicalSlugDetailView(views.CanonicalSlugDetailMixin, DetailView): """A basic detail view to test a model with a canonical slug""" model = CanonicalArticle template_name = "blank.html" class FormMessagesView(views.FormMessagesMixin, CreateView): """A basic form view to test valid/invalid messages""" 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): """Is everything OK in this group?""" group_required = "test_group" class UserPassesTestView(views.UserPassesTestMixin, OkView): """Did I pass a test?""" def test_func(self, user): return ( user.is_staff and not user.is_superuser and user.email.endswith("@mydomain.com") ) class UserPassesTestLoginRequiredView( views.LoginRequiredMixin, views.UserPassesTestMixin, OkView ): """Am I logged in _and_ passing a test?""" 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): """The test went missing?""" pass class AllVerbsView(views.AllVerbsMixin, View): """I know, like, all the verbs""" def all(self, request, *args, **kwargs): return HttpResponse("All verbs return this!") class SSLRequiredView(views.SSLRequiredMixin, OkView): """Speak friend and enter""" pass class RecentLoginRequiredView(views.RecentLoginRequiredMixin, OkView): """ A view for testing RecentLoginRequiredMixin. """ class AttributeHeaderView(views.HeaderMixin, OkView): """Set headers in an attribute w/o a template render class""" headers = { "X-DJANGO-BRACES-1": 1, "X-DJANGO-BRACES-2": 2, } class MethodHeaderView(views.HeaderMixin, OkView): """Set headers in a method w/o a template render class""" def get_headers(self, request): return { "X-DJANGO-BRACES-1": 1, "X-DJANGO-BRACES-2": 2, } class AuxiliaryHeaderView(View): """A view with a header already set""" def dispatch(self, request, *args, **kwargs): response = HttpResponse("OK with headers") response["X-DJANGO-BRACES-EXISTING"] = "value" return response class ExistingHeaderView(views.HeaderMixin, AuxiliaryHeaderView): """A view trying to override a parent's header""" headers = { 'X-DJANGO-BRACES-EXISTING': 'other value' } class CacheControlPublicView(views.CacheControlMixin, OkView): """A public-cached page with a 60 second timeout""" cachecontrol_public = True cachecontrol_max_age = 60 class NeverCacheView(views.NeverCacheMixin, OkView): """ View that will never be cached upstream. """