From 136c1f10fc36f674d97a3f15c504f975691d9cf9 Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 4 Mar 2014 12:58:26 -0800 Subject: [PATCH 1/9] from __future__ import absolute_import on all the test files --- tests/factories.py | 2 ++ tests/forms.py | 3 +++ tests/test_access_mixins.py | 2 ++ tests/test_ajax_mixins.py | 5 +++++ tests/test_forms.py | 3 +++ tests/urls.py | 2 ++ tests/urls_namespaced.py | 2 ++ tests/views.py | 2 ++ 8 files changed, 21 insertions(+) diff --git a/tests/factories.py b/tests/factories.py index 379ffe98..b31db999 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import factory from django.contrib.auth.models import Group, Permission, User diff --git a/tests/forms.py b/tests/forms.py index c15deed8..b126a963 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -1,6 +1,9 @@ +from __future__ import absolute_import + from django import forms from braces.forms import UserKwargModelFormMixin + from .models import Article diff --git a/tests/test_access_mixins.py b/tests/test_access_mixins.py index 140f89ed..02a2f266 100644 --- a/tests/test_access_mixins.py +++ b/tests/test_access_mixins.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import + from django import test from django.test.utils import override_settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied diff --git a/tests/test_ajax_mixins.py b/tests/test_ajax_mixins.py index 0514c059..e0b3aa82 100644 --- a/tests/test_ajax_mixins.py +++ b/tests/test_ajax_mixins.py @@ -1,8 +1,13 @@ +from __future__ import absolute_import + import mock + from django import test from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse + from braces.views import AjaxResponseMixin + from .compat import force_text from .factories import ArticleFactory, UserFactory from .helpers import TestViewHelper diff --git a/tests/test_forms.py b/tests/test_forms.py index 5dd8d493..7dc30158 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,5 +1,8 @@ +from __future__ import absolute_import + from django import test from django.contrib.auth.models import User + from . import forms diff --git a/tests/urls.py b/tests/urls.py index 9efa7e20..911342f9 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from . import views from .compat import patterns, include, url diff --git a/tests/urls_namespaced.py b/tests/urls_namespaced.py index cdfa95b7..20920be5 100644 --- a/tests/urls_namespaced.py +++ b/tests/urls_namespaced.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from . import views from .compat import patterns, url diff --git a/tests/views.py b/tests/views.py index 873c221d..105a76f9 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import codecs from django.contrib.auth.models import User From 1e77a6872f6b1d99425a25485a8c69aa4ccb2041 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 4 Mar 2014 13:56:55 -0800 Subject: [PATCH 2/9] blank files to move mixins into. --- braces/views/__init__.py | 0 braces/views/_access.py | 949 +++++++++++++++++++++++++++++++++++++++ braces/views/_ajax.py | 949 +++++++++++++++++++++++++++++++++++++++ braces/views/_forms.py | 949 +++++++++++++++++++++++++++++++++++++++ braces/views/_other.py | 949 +++++++++++++++++++++++++++++++++++++++ braces/views/_queries.py | 949 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 4745 insertions(+) create mode 100644 braces/views/__init__.py create mode 100644 braces/views/_access.py create mode 100644 braces/views/_ajax.py create mode 100644 braces/views/_forms.py create mode 100644 braces/views/_other.py create mode 100644 braces/views/_queries.py diff --git a/braces/views/__init__.py b/braces/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/braces/views/_access.py b/braces/views/_access.py new file mode 100644 index 00000000..e79aedfa --- /dev/null +++ b/braces/views/_access.py @@ -0,0 +1,949 @@ +import six + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.core import serializers +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import resolve, reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.utils.encoding import force_text +from django.utils.functional import curry, Promise +from django.views.decorators.csrf import csrf_exempt + +## Django 1.5+ compat +try: + import json +except ImportError: # pragma: no cover + from django.utils import simplejson as json + + +class AccessMixin(object): + """ + 'Abstract' mixin that gives access mixins the same customizable + functionality. + """ + login_url = None + raise_exception = False # Default whether to raise an exception to none + redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth + + def get_login_url(self): + """ + Override this method to customize the login_url. + """ + login_url = self.login_url or settings.LOGIN_URL + if not login_url: + raise ImproperlyConfigured( + 'Define {0}.login_url or settings.LOGIN_URL or override ' + '{0}.get_login_url().'.format(self.__class__.__name__)) + + return force_text(login_url) + + def get_redirect_field_name(self): + """ + Override this method to customize the redirect_field_name. + """ + if self.redirect_field_name is None: + raise ImproperlyConfigured( + '{0} is missing the ' + 'redirect_field_name. Define {0}.redirect_field_name or ' + 'override {0}.get_redirect_field_name().'.format( + self.__class__.__name__)) + return self.redirect_field_name + + +class LoginRequiredMixin(AccessMixin): + """ + View mixin which verifies that the user is authenticated. + + NOTE: + This should be the left-most mixin of a view, except when + combined with CsrfExemptMixin - which in that case should + be the left-most mixin. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(LoginRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class AnonymousRequiredMixin(object): + """ + View mixin which redirects to a specified URL if authenticated. + Can be useful if you wanted to prevent authenticated users from + accessing signup pages etc. + + 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): + if request.user.is_authenticated(): + return HttpResponseRedirect(self.get_authenticated_redirect_url()) + return super(AnonymousRequiredMixin, self).dispatch( + request, *args, **kwargs) + + def get_authenticated_redirect_url(self): + """ Return the reversed authenticated redirect url. """ + if not self.authenticated_redirect_url: + raise ImproperlyConfigured( + '{0} is missing an authenticated_redirect_url ' + 'url to redirect to. Define ' + '{0}.authenticated_redirect_url or override ' + '{0}.get_authenticated_redirect_url().'.format( + self.__class__.__name__)) + return self.authenticated_redirect_url + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + + NOTE: + This should be the left-most mixin of a view. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class PermissionRequiredMixin(AccessMixin): + """ + View mixin which verifies that the logged in user has the specified + permission. + + Class Settings + `permission_required` - the permission to check for. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + + class SomeView(PermissionRequiredMixin, ListView): + ... + # required + permission_required = "app.permission" + + # optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + ... + """ + permission_required = None # Default required perms to none + + def 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( + '{0} requires the "permission_required" attribute to be ' + 'set.'.format(self.__class__.__name__)) + + return self.permission_required + + def check_permissions(self, request): + """ + Returns whether or not the user has permissions + """ + perms = self.get_permission_required(request) + return request.user.has_perm(perms) + + def no_permissions_fail(self, request=None): + """ + Called when the user has no permissions. 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()) + + 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: # If the user lacks the permission + if self.raise_exception: + raise PermissionDenied # Return a 403 + return self.no_permissions_fail(request) + + return super(PermissionRequiredMixin, self).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 passed in list. + + By specifying The `any` key , the user must have ONE of the set + permissions in the list. + + Class Settings + `permissions` - This is required to be a dict with one or both + keys of `all` and/or `any` containing a list or tuple of + permissions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + class SomeView(MultiplePermissionsRequiredMixin, ListView): + ... + #required + permissions = { + "all": ("blog.add_post", "blog.change_post"), + "any": ("blog.delete_post", "user.change_user") + } + + #optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + """ + permissions = None # Default required perms to none + + def get_permission_required(self, request=None): + self._check_permissions_attr() + return self.permissions + + def check_permissions(self, request): + permissions = self.get_permission_required() + perms_all = permissions.get('all') or None + perms_any = permissions.get('any') or None + + self._check_permissions_keys_set(perms_all, perms_any) + self._check_perms_keys("all", perms_all) + self._check_perms_keys("any", perms_any) + + # If perms_all, check that user has all permissions in the list/tuple + if perms_all: + if not request.user.has_perms(perms_all): + return False + + # If perms_any, check that user has at least one in the list/tuple + if perms_any: + has_one_perm = False + for perm in perms_any: + if request.user.has_perm(perm): + has_one_perm = True + break + + if not has_one_perm: + 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( + '{0} requires the "permissions" attribute to be set as a ' + 'dict.'.format(self.__class__.__name__)) + + 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( + '{0} requires the "permissions" attribute to be set to a ' + 'dict and the "any" or "all" key to be set.'.format( + self.__class__.__name__)) + + 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( + '{0} requires the permisions dict {1} value to be a ' + 'list or tuple.'.format(self.__class__.__name__, key)) + + +class GroupRequiredMixin(AccessMixin): + group_required = None + + def get_group_required(self): + if self.group_required is None or ( + not isinstance(self.group_required, + (list, tuple) + six.string_types) + ): + + raise ImproperlyConfigured( + '{0} requires the "group_required" attribute to be set and be ' + 'one of the following types: string, unicode, list or ' + 'tuple'.format(self.__class__.__name__)) + if not isinstance(self.group_required, (list, tuple)): + self.group_required = (self.group_required,) + return self.group_required + + def check_membership(self, groups): + """ Check required group(s) """ + 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): + self.request = request + in_group = False + if self.request.user.is_authenticated(): + in_group = self.check_membership(self.get_group_required()) + + if not in_group: + if self.raise_exception: + raise PermissionDenied + else: + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(GroupRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class UserPassesTestMixin(AccessMixin): + """ + CBV Mixin allows you to define test that every user should pass + to get access into view. + + Class Settings + `test_func` - This is required to be a method that takes user + instance and return True or False after checking conditions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + """ + + def test_func(self, user): + raise NotImplementedError( + '{0} is missing implementation of the ' + 'test_func method. You should write one.'.format( + self.__class__.__name__)) + + def get_test_func(self): + return getattr(self, "test_func") + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.get_test_func()(request.user) + + if not user_test_result: # If user don't pass the test + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(UserPassesTestMixin, self).dispatch( + request, *args, **kwargs) + + +class UserFormKwargsMixin(object): + """ + CBV mixin which puts the user from the request into the form kwargs. + Note: Using this mixin requires you to pop the `user` kwarg + out of the dict in the super of your form's `__init__`. + """ + def get_form_kwargs(self): + kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() + # Update the existing form kwargs dict with the request's user. + kwargs.update({"user": self.request.user}) + return kwargs + + +class SuccessURLRedirectListMixin(object): + """ + Simple CBV mixin which sets the success url to the list view of + a given app. Set success_list_url as a class attribute of your + CBV and don't worry about overloading the get_success_url. + + This is only to be used for redirecting to a list page. If you need + to reverse the url with kwargs, this is not the mixin to use. + """ + success_list_url = None # Default the success url to none + + def get_success_url(self): + # Return the reversed success url. + if self.success_list_url is None: + raise ImproperlyConfigured( + '{0} is missing a succes_list_url ' + 'name to reverse and redirect to. Define ' + '{0}.success_list_url or override ' + '{0}.get_success_url().'.format(self.__class__.__name__)) + return reverse(self.success_list_url) + + +class SuperuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_superuser` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: # If the user is a standard user, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response. + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(SuperuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class SetHeadlineMixin(object): + """ + Mixin allows you to set a static headline through a static property on the + class or programmatically by overloading the get_headline method. + """ + headline = None # Default the headline to none + + def get_context_data(self, **kwargs): + kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) + # Update the existing context dict with the provided headline. + kwargs.update({"headline": self.get_headline()}) + return kwargs + + def get_headline(self): + if self.headline is None: # If no headline was provided as a view + # attribute and this method wasn't + # overridden raise a configuration error. + raise ImproperlyConfigured( + '{0} is missing a headline. ' + 'Define {0}.headline, or override ' + '{0}.get_headline().'.format(self.__class__.__name__)) + return force_text(self.headline) + + +class StaticContextMixin(object): + """ + Mixin allows you to set static context through a static property on + the class. + """ + static_context = None + + def get_context_data(self, **kwargs): + kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) + + try: + kwargs.update(self.get_static_context()) + except (TypeError, ValueError): + raise ImproperlyConfigured( + '{0}.static_context must be a dictionary or container ' + 'of two-tuples.'.format(self.__class__.__name__)) + else: + return kwargs + + def get_static_context(self): + if self.static_context is None: + raise ImproperlyConfigured( + '{0} is missing the static_context property. Define ' + '{0}.static_context, or override ' + '{0}.get_static_context()'.format(self.__class__.__name__) + ) + return self.static_context + + +class SelectRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a select_related on. + """ + select_related = None # Default related fields to none + + def get_queryset(self): + if self.select_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the select_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + if not isinstance(self.select_related, (tuple, list)): + # If the select_related argument is *not* a tuple or list, + # raise a configuration error. + raise ImproperlyConfigured( + "{0}'s select_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(SelectRelatedMixin, self).get_queryset() + + return queryset.select_related(*self.select_related) + + +class PrefetchRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a prefetch_related on. + """ + prefetch_related = None # Default prefetch fields to none + + def get_queryset(self): + if self.prefetch_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the prefetch_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + 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( + "{0}'s prefetch_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(PrefetchRelatedMixin, self).get_queryset() + + return queryset.prefetch_related(*self.prefetch_related) + + +class StaffuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_staff` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: # If the request's user is not staff, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(StaffuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONResponseMixin(object): + """ + A mixin that allows you to easily serialize simple data such as a dict or + Django models. + """ + content_type = None + json_dumps_kwargs = None + + def get_content_type(self): + if (self.content_type is not None and + not isinstance(self.content_type, + (six.string_types, six.text_type))): + raise ImproperlyConfigured( + '{0} is missing a content type. Define {0}.content_type, ' + 'or override {0}.get_content_type().'.format( + self.__class__.__name__)) + return self.content_type or u"application/json" + + def get_json_dumps_kwargs(self): + if self.json_dumps_kwargs is None: + self.json_dumps_kwargs = {} + self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) + return self.json_dumps_kwargs + + def render_json_response(self, context_dict, status=200): + """ + Limited serialization for shipping plain data. Do not use for models + or other complex or custom objects. + """ + json_context = json.dumps( + context_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs()).encode(u'utf-8') + return HttpResponse(json_context, + content_type=self.get_content_type(), + status=status) + + def render_json_object_response(self, objects, **kwargs): + """ + Serializes objects using Django's builtin JSON serializer. Additional + kwargs can be used the same way for django.core.serializers.serialize. + """ + json_data = serializers.serialize(u"json", objects, **kwargs) + return HttpResponse(json_data, content_type=self.get_content_type()) + + +class AjaxResponseMixin(object): + """ + Mixin allows you to define alternative methods for ajax requests. Similar + to the normal get, post, and put methods, you can use get_ajax, post_ajax, + and put_ajax. + """ + def dispatch(self, request, *args, **kwargs): + request_method = request.method.lower() + + if request.is_ajax() and request_method in self.http_method_names: + handler = getattr(self, u"{0}_ajax".format(request_method), + self.http_method_not_allowed) + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + return super(AjaxResponseMixin, self).dispatch( + request, *args, **kwargs) + + def get_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def post_ajax(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def put_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def delete_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + +class JsonRequestResponseMixin(JSONResponseMixin): + """ + Extends JSONResponseMixin. Attempts to parse request as JSON. If request + is properly formatted, the json is saved to self.request_json as a Python + object. request_json will be None for imparsible requests. + Set the attribute require_json to True to return a 400 "Bad Request" error + for requests that don't contain JSON. + + Note: To allow public access to your view, you'll need to use the + csrf_exempt decorator or CsrfExemptMixin. + + Example Usage: + + class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): + def post(self, request, *args, **kwargs): + do_stuff_with_contents_of_request_json() + return self.render_json_response( + {'message': 'Thanks!'}) + """ + require_json = False + error_response_dict = {u"errors": [u"Improperly formatted request"]} + + def render_bad_request_response(self, error_dict=None): + if error_dict is None: + error_dict = self.error_response_dict + json_context = json.dumps( + error_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs() + ).encode(u'utf-8') + return HttpResponseBadRequest( + json_context, content_type=self.get_content_type()) + + def get_request_json(self): + try: + return json.loads(self.request.body.decode(u'utf-8')) + except ValueError: + return None + + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + self.request_json = self.get_request_json() + if self.require_json and self.request_json is None: + return self.render_bad_request_response() + return super(JsonRequestResponseMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONRequestResponseMixin(JsonRequestResponseMixin): + pass + + +class OrderableListMixin(object): + """ + Mixin allows your users to order records using GET parameters + """ + + orderable_columns = None + orderable_columns_default = None + order_by = None + ordering = None + + def get_context_data(self, **kwargs): + """ + Augments context with: + + * ``order_by`` - name of the field + * ``ordering`` - order of ordering, either ``asc`` or ``desc`` + """ + context = super(OrderableListMixin, self).get_context_data(**kwargs) + context["order_by"] = self.order_by + context["ordering"] = self.ordering + return context + + def get_orderable_columns(self): + if not self.orderable_columns: + raise ImproperlyConfigured( + '{0} needs the ordering columns defined.'.format( + self.__class__.__name__)) + return self.orderable_columns + + def get_orderable_columns_default(self): + if not self.orderable_columns_default: + raise ImproperlyConfigured( + '{0} needs the default ordering column defined.'.format( + self.__class__.__name__)) + return self.orderable_columns_default + + def get_ordered_queryset(self, queryset=None): + """ + Augments ``QuerySet`` with order_by statement if possible + + :param QuerySet queryset: ``QuerySet`` to ``order_by`` + :return: QuerySet + """ + get_order_by = self.request.GET.get("order_by") + + if get_order_by in self.get_orderable_columns(): + order_by = get_order_by + else: + order_by = self.get_orderable_columns_default() + + self.order_by = order_by + self.ordering = "asc" + + if order_by and self.request.GET.get("ordering", "asc") == "desc": + order_by = "-" + order_by + self.ordering = "desc" + + return queryset.order_by(order_by) + + def get_queryset(self): + """ + Returns ordered ``QuerySet`` + """ + unordered_queryset = super(OrderableListMixin, self).get_queryset() + return self.get_ordered_queryset(unordered_queryset) + + +class CanonicalSlugDetailMixin(object): + """ + A mixin that enforces a canonical slug in the url. + + If a urlpattern takes a object's pk and slug as arguments and the slug url + argument does not equal the object's canonical slug, this mixin will + redirect to the url containing the canonical slug. + """ + def dispatch(self, request, *args, **kwargs): + # Set up since we need to super() later instead of earlier. + self.request = request + self.args = args + self.kwargs = kwargs + + # Get the current object, url slug, and + # urlpattern name (namespace aware). + obj = self.get_object() + slug = self.kwargs.get(self.slug_url_kwarg, None) + match = resolve(request.path_info) + url_parts = match.namespaces + url_parts.append(match.url_name) + current_urlpattern = ":".join(url_parts) + + # Figure out what the slug is supposed to be. + if hasattr(obj, "get_canonical_slug"): + canonical_slug = obj.get_canonical_slug() + else: + canonical_slug = self.get_canonical_slug() + + # If there's a discrepancy between the slug in the url and the + # canonical slug, redirect to the canonical slug. + if canonical_slug != slug: + params = {self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + 'permanent': True} + return redirect(current_urlpattern, **params) + + return super(CanonicalSlugDetailMixin, self).dispatch( + request, *args, **kwargs) + + def get_canonical_slug(self): + """ + Override this method to customize what slug should be considered + canonical. + + Alternatively, define the get_canonical_slug method on this view's + object class. In that case, this method will never be called. + """ + return self.get_object().slug + + +class _MessageAPIWrapper(object): + """ + Wrap the django.contrib.messages.api module to automatically pass a given + request object as the first parameter of 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, curry(api_fn, request)) + + +class _MessageDescriptor(object): + """ + A descriptor that binds the _MessageAPIWrapper to the view's + request. + """ + def __get__(self, instance, owner): + return _MessageAPIWrapper(instance.request) + + +class MessageMixin(object): + """ + Add a `messages` attribute on the view instance that wraps + `django.contrib .messages`, automatically passing the current + request object. + """ + messages = _MessageDescriptor() + + +class FormValidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_valid_message method. + """ + form_valid_message = None # Default to None + + def get_form_valid_message(self): + """ + Validate that form_valid_message is set and is either a + unicode or str object. + """ + if self.form_valid_message is None: + raise ImproperlyConfigured( + '{0}.form_valid_message is not set. Define ' + '{0}.form_valid_message, or override ' + '{0}.get_form_valid_message().'.format(self.__class__.__name__) + ) + + if not isinstance(self.form_valid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_valid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__) + ) + + return force_text(self.form_valid_message) + + def form_valid(self, form): + """ + Call the super first, so that when overriding + get_form_valid_message, we have access to the newly saved object. + """ + response = super(FormValidMessageMixin, self).form_valid(form) + self.messages.success(self.get_form_valid_message(), + fail_silently=True) + return response + + +class FormInvalidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_invalid_message method. + """ + form_invalid_message = None + + def get_form_invalid_message(self): + """ + Validate that form_invalid_message is set and is either a + unicode or str object. + """ + if self.form_invalid_message is None: + raise ImproperlyConfigured( + '{0}.form_invalid_message is not set. Define ' + '{0}.form_invalid_message, or override ' + '{0}.get_form_invalid_message().'.format( + self.__class__.__name__)) + + if not isinstance(self.form_invalid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_invalid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__)) + + return force_text(self.form_invalid_message) + + def form_invalid(self, form): + response = super(FormInvalidMessageMixin, self).form_invalid(form) + self.messages.error(self.get_form_invalid_message(), + fail_silently=True) + return response + + +class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): + """ + Mixin is a shortcut to use both FormValidMessageMixin and + FormInvalidMessageMixin. + """ + pass + + +class AllVerbsMixin(object): + """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): + if not self.all_handler: + raise ImproperlyConfigured( + '{0} requires the all_handler attribute to be set.'.format( + self.__class__.__name__)) + + handler = getattr(self, self.all_handler, self.http_method_not_allowed) + return handler(request, *args, **kwargs) diff --git a/braces/views/_ajax.py b/braces/views/_ajax.py new file mode 100644 index 00000000..e79aedfa --- /dev/null +++ b/braces/views/_ajax.py @@ -0,0 +1,949 @@ +import six + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.core import serializers +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import resolve, reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.utils.encoding import force_text +from django.utils.functional import curry, Promise +from django.views.decorators.csrf import csrf_exempt + +## Django 1.5+ compat +try: + import json +except ImportError: # pragma: no cover + from django.utils import simplejson as json + + +class AccessMixin(object): + """ + 'Abstract' mixin that gives access mixins the same customizable + functionality. + """ + login_url = None + raise_exception = False # Default whether to raise an exception to none + redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth + + def get_login_url(self): + """ + Override this method to customize the login_url. + """ + login_url = self.login_url or settings.LOGIN_URL + if not login_url: + raise ImproperlyConfigured( + 'Define {0}.login_url or settings.LOGIN_URL or override ' + '{0}.get_login_url().'.format(self.__class__.__name__)) + + return force_text(login_url) + + def get_redirect_field_name(self): + """ + Override this method to customize the redirect_field_name. + """ + if self.redirect_field_name is None: + raise ImproperlyConfigured( + '{0} is missing the ' + 'redirect_field_name. Define {0}.redirect_field_name or ' + 'override {0}.get_redirect_field_name().'.format( + self.__class__.__name__)) + return self.redirect_field_name + + +class LoginRequiredMixin(AccessMixin): + """ + View mixin which verifies that the user is authenticated. + + NOTE: + This should be the left-most mixin of a view, except when + combined with CsrfExemptMixin - which in that case should + be the left-most mixin. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(LoginRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class AnonymousRequiredMixin(object): + """ + View mixin which redirects to a specified URL if authenticated. + Can be useful if you wanted to prevent authenticated users from + accessing signup pages etc. + + 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): + if request.user.is_authenticated(): + return HttpResponseRedirect(self.get_authenticated_redirect_url()) + return super(AnonymousRequiredMixin, self).dispatch( + request, *args, **kwargs) + + def get_authenticated_redirect_url(self): + """ Return the reversed authenticated redirect url. """ + if not self.authenticated_redirect_url: + raise ImproperlyConfigured( + '{0} is missing an authenticated_redirect_url ' + 'url to redirect to. Define ' + '{0}.authenticated_redirect_url or override ' + '{0}.get_authenticated_redirect_url().'.format( + self.__class__.__name__)) + return self.authenticated_redirect_url + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + + NOTE: + This should be the left-most mixin of a view. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class PermissionRequiredMixin(AccessMixin): + """ + View mixin which verifies that the logged in user has the specified + permission. + + Class Settings + `permission_required` - the permission to check for. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + + class SomeView(PermissionRequiredMixin, ListView): + ... + # required + permission_required = "app.permission" + + # optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + ... + """ + permission_required = None # Default required perms to none + + def 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( + '{0} requires the "permission_required" attribute to be ' + 'set.'.format(self.__class__.__name__)) + + return self.permission_required + + def check_permissions(self, request): + """ + Returns whether or not the user has permissions + """ + perms = self.get_permission_required(request) + return request.user.has_perm(perms) + + def no_permissions_fail(self, request=None): + """ + Called when the user has no permissions. 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()) + + 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: # If the user lacks the permission + if self.raise_exception: + raise PermissionDenied # Return a 403 + return self.no_permissions_fail(request) + + return super(PermissionRequiredMixin, self).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 passed in list. + + By specifying The `any` key , the user must have ONE of the set + permissions in the list. + + Class Settings + `permissions` - This is required to be a dict with one or both + keys of `all` and/or `any` containing a list or tuple of + permissions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + class SomeView(MultiplePermissionsRequiredMixin, ListView): + ... + #required + permissions = { + "all": ("blog.add_post", "blog.change_post"), + "any": ("blog.delete_post", "user.change_user") + } + + #optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + """ + permissions = None # Default required perms to none + + def get_permission_required(self, request=None): + self._check_permissions_attr() + return self.permissions + + def check_permissions(self, request): + permissions = self.get_permission_required() + perms_all = permissions.get('all') or None + perms_any = permissions.get('any') or None + + self._check_permissions_keys_set(perms_all, perms_any) + self._check_perms_keys("all", perms_all) + self._check_perms_keys("any", perms_any) + + # If perms_all, check that user has all permissions in the list/tuple + if perms_all: + if not request.user.has_perms(perms_all): + return False + + # If perms_any, check that user has at least one in the list/tuple + if perms_any: + has_one_perm = False + for perm in perms_any: + if request.user.has_perm(perm): + has_one_perm = True + break + + if not has_one_perm: + 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( + '{0} requires the "permissions" attribute to be set as a ' + 'dict.'.format(self.__class__.__name__)) + + 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( + '{0} requires the "permissions" attribute to be set to a ' + 'dict and the "any" or "all" key to be set.'.format( + self.__class__.__name__)) + + 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( + '{0} requires the permisions dict {1} value to be a ' + 'list or tuple.'.format(self.__class__.__name__, key)) + + +class GroupRequiredMixin(AccessMixin): + group_required = None + + def get_group_required(self): + if self.group_required is None or ( + not isinstance(self.group_required, + (list, tuple) + six.string_types) + ): + + raise ImproperlyConfigured( + '{0} requires the "group_required" attribute to be set and be ' + 'one of the following types: string, unicode, list or ' + 'tuple'.format(self.__class__.__name__)) + if not isinstance(self.group_required, (list, tuple)): + self.group_required = (self.group_required,) + return self.group_required + + def check_membership(self, groups): + """ Check required group(s) """ + 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): + self.request = request + in_group = False + if self.request.user.is_authenticated(): + in_group = self.check_membership(self.get_group_required()) + + if not in_group: + if self.raise_exception: + raise PermissionDenied + else: + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(GroupRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class UserPassesTestMixin(AccessMixin): + """ + CBV Mixin allows you to define test that every user should pass + to get access into view. + + Class Settings + `test_func` - This is required to be a method that takes user + instance and return True or False after checking conditions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + """ + + def test_func(self, user): + raise NotImplementedError( + '{0} is missing implementation of the ' + 'test_func method. You should write one.'.format( + self.__class__.__name__)) + + def get_test_func(self): + return getattr(self, "test_func") + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.get_test_func()(request.user) + + if not user_test_result: # If user don't pass the test + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(UserPassesTestMixin, self).dispatch( + request, *args, **kwargs) + + +class UserFormKwargsMixin(object): + """ + CBV mixin which puts the user from the request into the form kwargs. + Note: Using this mixin requires you to pop the `user` kwarg + out of the dict in the super of your form's `__init__`. + """ + def get_form_kwargs(self): + kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() + # Update the existing form kwargs dict with the request's user. + kwargs.update({"user": self.request.user}) + return kwargs + + +class SuccessURLRedirectListMixin(object): + """ + Simple CBV mixin which sets the success url to the list view of + a given app. Set success_list_url as a class attribute of your + CBV and don't worry about overloading the get_success_url. + + This is only to be used for redirecting to a list page. If you need + to reverse the url with kwargs, this is not the mixin to use. + """ + success_list_url = None # Default the success url to none + + def get_success_url(self): + # Return the reversed success url. + if self.success_list_url is None: + raise ImproperlyConfigured( + '{0} is missing a succes_list_url ' + 'name to reverse and redirect to. Define ' + '{0}.success_list_url or override ' + '{0}.get_success_url().'.format(self.__class__.__name__)) + return reverse(self.success_list_url) + + +class SuperuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_superuser` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: # If the user is a standard user, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response. + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(SuperuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class SetHeadlineMixin(object): + """ + Mixin allows you to set a static headline through a static property on the + class or programmatically by overloading the get_headline method. + """ + headline = None # Default the headline to none + + def get_context_data(self, **kwargs): + kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) + # Update the existing context dict with the provided headline. + kwargs.update({"headline": self.get_headline()}) + return kwargs + + def get_headline(self): + if self.headline is None: # If no headline was provided as a view + # attribute and this method wasn't + # overridden raise a configuration error. + raise ImproperlyConfigured( + '{0} is missing a headline. ' + 'Define {0}.headline, or override ' + '{0}.get_headline().'.format(self.__class__.__name__)) + return force_text(self.headline) + + +class StaticContextMixin(object): + """ + Mixin allows you to set static context through a static property on + the class. + """ + static_context = None + + def get_context_data(self, **kwargs): + kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) + + try: + kwargs.update(self.get_static_context()) + except (TypeError, ValueError): + raise ImproperlyConfigured( + '{0}.static_context must be a dictionary or container ' + 'of two-tuples.'.format(self.__class__.__name__)) + else: + return kwargs + + def get_static_context(self): + if self.static_context is None: + raise ImproperlyConfigured( + '{0} is missing the static_context property. Define ' + '{0}.static_context, or override ' + '{0}.get_static_context()'.format(self.__class__.__name__) + ) + return self.static_context + + +class SelectRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a select_related on. + """ + select_related = None # Default related fields to none + + def get_queryset(self): + if self.select_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the select_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + if not isinstance(self.select_related, (tuple, list)): + # If the select_related argument is *not* a tuple or list, + # raise a configuration error. + raise ImproperlyConfigured( + "{0}'s select_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(SelectRelatedMixin, self).get_queryset() + + return queryset.select_related(*self.select_related) + + +class PrefetchRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a prefetch_related on. + """ + prefetch_related = None # Default prefetch fields to none + + def get_queryset(self): + if self.prefetch_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the prefetch_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + 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( + "{0}'s prefetch_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(PrefetchRelatedMixin, self).get_queryset() + + return queryset.prefetch_related(*self.prefetch_related) + + +class StaffuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_staff` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: # If the request's user is not staff, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(StaffuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONResponseMixin(object): + """ + A mixin that allows you to easily serialize simple data such as a dict or + Django models. + """ + content_type = None + json_dumps_kwargs = None + + def get_content_type(self): + if (self.content_type is not None and + not isinstance(self.content_type, + (six.string_types, six.text_type))): + raise ImproperlyConfigured( + '{0} is missing a content type. Define {0}.content_type, ' + 'or override {0}.get_content_type().'.format( + self.__class__.__name__)) + return self.content_type or u"application/json" + + def get_json_dumps_kwargs(self): + if self.json_dumps_kwargs is None: + self.json_dumps_kwargs = {} + self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) + return self.json_dumps_kwargs + + def render_json_response(self, context_dict, status=200): + """ + Limited serialization for shipping plain data. Do not use for models + or other complex or custom objects. + """ + json_context = json.dumps( + context_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs()).encode(u'utf-8') + return HttpResponse(json_context, + content_type=self.get_content_type(), + status=status) + + def render_json_object_response(self, objects, **kwargs): + """ + Serializes objects using Django's builtin JSON serializer. Additional + kwargs can be used the same way for django.core.serializers.serialize. + """ + json_data = serializers.serialize(u"json", objects, **kwargs) + return HttpResponse(json_data, content_type=self.get_content_type()) + + +class AjaxResponseMixin(object): + """ + Mixin allows you to define alternative methods for ajax requests. Similar + to the normal get, post, and put methods, you can use get_ajax, post_ajax, + and put_ajax. + """ + def dispatch(self, request, *args, **kwargs): + request_method = request.method.lower() + + if request.is_ajax() and request_method in self.http_method_names: + handler = getattr(self, u"{0}_ajax".format(request_method), + self.http_method_not_allowed) + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + return super(AjaxResponseMixin, self).dispatch( + request, *args, **kwargs) + + def get_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def post_ajax(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def put_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def delete_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + +class JsonRequestResponseMixin(JSONResponseMixin): + """ + Extends JSONResponseMixin. Attempts to parse request as JSON. If request + is properly formatted, the json is saved to self.request_json as a Python + object. request_json will be None for imparsible requests. + Set the attribute require_json to True to return a 400 "Bad Request" error + for requests that don't contain JSON. + + Note: To allow public access to your view, you'll need to use the + csrf_exempt decorator or CsrfExemptMixin. + + Example Usage: + + class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): + def post(self, request, *args, **kwargs): + do_stuff_with_contents_of_request_json() + return self.render_json_response( + {'message': 'Thanks!'}) + """ + require_json = False + error_response_dict = {u"errors": [u"Improperly formatted request"]} + + def render_bad_request_response(self, error_dict=None): + if error_dict is None: + error_dict = self.error_response_dict + json_context = json.dumps( + error_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs() + ).encode(u'utf-8') + return HttpResponseBadRequest( + json_context, content_type=self.get_content_type()) + + def get_request_json(self): + try: + return json.loads(self.request.body.decode(u'utf-8')) + except ValueError: + return None + + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + self.request_json = self.get_request_json() + if self.require_json and self.request_json is None: + return self.render_bad_request_response() + return super(JsonRequestResponseMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONRequestResponseMixin(JsonRequestResponseMixin): + pass + + +class OrderableListMixin(object): + """ + Mixin allows your users to order records using GET parameters + """ + + orderable_columns = None + orderable_columns_default = None + order_by = None + ordering = None + + def get_context_data(self, **kwargs): + """ + Augments context with: + + * ``order_by`` - name of the field + * ``ordering`` - order of ordering, either ``asc`` or ``desc`` + """ + context = super(OrderableListMixin, self).get_context_data(**kwargs) + context["order_by"] = self.order_by + context["ordering"] = self.ordering + return context + + def get_orderable_columns(self): + if not self.orderable_columns: + raise ImproperlyConfigured( + '{0} needs the ordering columns defined.'.format( + self.__class__.__name__)) + return self.orderable_columns + + def get_orderable_columns_default(self): + if not self.orderable_columns_default: + raise ImproperlyConfigured( + '{0} needs the default ordering column defined.'.format( + self.__class__.__name__)) + return self.orderable_columns_default + + def get_ordered_queryset(self, queryset=None): + """ + Augments ``QuerySet`` with order_by statement if possible + + :param QuerySet queryset: ``QuerySet`` to ``order_by`` + :return: QuerySet + """ + get_order_by = self.request.GET.get("order_by") + + if get_order_by in self.get_orderable_columns(): + order_by = get_order_by + else: + order_by = self.get_orderable_columns_default() + + self.order_by = order_by + self.ordering = "asc" + + if order_by and self.request.GET.get("ordering", "asc") == "desc": + order_by = "-" + order_by + self.ordering = "desc" + + return queryset.order_by(order_by) + + def get_queryset(self): + """ + Returns ordered ``QuerySet`` + """ + unordered_queryset = super(OrderableListMixin, self).get_queryset() + return self.get_ordered_queryset(unordered_queryset) + + +class CanonicalSlugDetailMixin(object): + """ + A mixin that enforces a canonical slug in the url. + + If a urlpattern takes a object's pk and slug as arguments and the slug url + argument does not equal the object's canonical slug, this mixin will + redirect to the url containing the canonical slug. + """ + def dispatch(self, request, *args, **kwargs): + # Set up since we need to super() later instead of earlier. + self.request = request + self.args = args + self.kwargs = kwargs + + # Get the current object, url slug, and + # urlpattern name (namespace aware). + obj = self.get_object() + slug = self.kwargs.get(self.slug_url_kwarg, None) + match = resolve(request.path_info) + url_parts = match.namespaces + url_parts.append(match.url_name) + current_urlpattern = ":".join(url_parts) + + # Figure out what the slug is supposed to be. + if hasattr(obj, "get_canonical_slug"): + canonical_slug = obj.get_canonical_slug() + else: + canonical_slug = self.get_canonical_slug() + + # If there's a discrepancy between the slug in the url and the + # canonical slug, redirect to the canonical slug. + if canonical_slug != slug: + params = {self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + 'permanent': True} + return redirect(current_urlpattern, **params) + + return super(CanonicalSlugDetailMixin, self).dispatch( + request, *args, **kwargs) + + def get_canonical_slug(self): + """ + Override this method to customize what slug should be considered + canonical. + + Alternatively, define the get_canonical_slug method on this view's + object class. In that case, this method will never be called. + """ + return self.get_object().slug + + +class _MessageAPIWrapper(object): + """ + Wrap the django.contrib.messages.api module to automatically pass a given + request object as the first parameter of 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, curry(api_fn, request)) + + +class _MessageDescriptor(object): + """ + A descriptor that binds the _MessageAPIWrapper to the view's + request. + """ + def __get__(self, instance, owner): + return _MessageAPIWrapper(instance.request) + + +class MessageMixin(object): + """ + Add a `messages` attribute on the view instance that wraps + `django.contrib .messages`, automatically passing the current + request object. + """ + messages = _MessageDescriptor() + + +class FormValidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_valid_message method. + """ + form_valid_message = None # Default to None + + def get_form_valid_message(self): + """ + Validate that form_valid_message is set and is either a + unicode or str object. + """ + if self.form_valid_message is None: + raise ImproperlyConfigured( + '{0}.form_valid_message is not set. Define ' + '{0}.form_valid_message, or override ' + '{0}.get_form_valid_message().'.format(self.__class__.__name__) + ) + + if not isinstance(self.form_valid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_valid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__) + ) + + return force_text(self.form_valid_message) + + def form_valid(self, form): + """ + Call the super first, so that when overriding + get_form_valid_message, we have access to the newly saved object. + """ + response = super(FormValidMessageMixin, self).form_valid(form) + self.messages.success(self.get_form_valid_message(), + fail_silently=True) + return response + + +class FormInvalidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_invalid_message method. + """ + form_invalid_message = None + + def get_form_invalid_message(self): + """ + Validate that form_invalid_message is set and is either a + unicode or str object. + """ + if self.form_invalid_message is None: + raise ImproperlyConfigured( + '{0}.form_invalid_message is not set. Define ' + '{0}.form_invalid_message, or override ' + '{0}.get_form_invalid_message().'.format( + self.__class__.__name__)) + + if not isinstance(self.form_invalid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_invalid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__)) + + return force_text(self.form_invalid_message) + + def form_invalid(self, form): + response = super(FormInvalidMessageMixin, self).form_invalid(form) + self.messages.error(self.get_form_invalid_message(), + fail_silently=True) + return response + + +class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): + """ + Mixin is a shortcut to use both FormValidMessageMixin and + FormInvalidMessageMixin. + """ + pass + + +class AllVerbsMixin(object): + """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): + if not self.all_handler: + raise ImproperlyConfigured( + '{0} requires the all_handler attribute to be set.'.format( + self.__class__.__name__)) + + handler = getattr(self, self.all_handler, self.http_method_not_allowed) + return handler(request, *args, **kwargs) diff --git a/braces/views/_forms.py b/braces/views/_forms.py new file mode 100644 index 00000000..e79aedfa --- /dev/null +++ b/braces/views/_forms.py @@ -0,0 +1,949 @@ +import six + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.core import serializers +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import resolve, reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.utils.encoding import force_text +from django.utils.functional import curry, Promise +from django.views.decorators.csrf import csrf_exempt + +## Django 1.5+ compat +try: + import json +except ImportError: # pragma: no cover + from django.utils import simplejson as json + + +class AccessMixin(object): + """ + 'Abstract' mixin that gives access mixins the same customizable + functionality. + """ + login_url = None + raise_exception = False # Default whether to raise an exception to none + redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth + + def get_login_url(self): + """ + Override this method to customize the login_url. + """ + login_url = self.login_url or settings.LOGIN_URL + if not login_url: + raise ImproperlyConfigured( + 'Define {0}.login_url or settings.LOGIN_URL or override ' + '{0}.get_login_url().'.format(self.__class__.__name__)) + + return force_text(login_url) + + def get_redirect_field_name(self): + """ + Override this method to customize the redirect_field_name. + """ + if self.redirect_field_name is None: + raise ImproperlyConfigured( + '{0} is missing the ' + 'redirect_field_name. Define {0}.redirect_field_name or ' + 'override {0}.get_redirect_field_name().'.format( + self.__class__.__name__)) + return self.redirect_field_name + + +class LoginRequiredMixin(AccessMixin): + """ + View mixin which verifies that the user is authenticated. + + NOTE: + This should be the left-most mixin of a view, except when + combined with CsrfExemptMixin - which in that case should + be the left-most mixin. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(LoginRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class AnonymousRequiredMixin(object): + """ + View mixin which redirects to a specified URL if authenticated. + Can be useful if you wanted to prevent authenticated users from + accessing signup pages etc. + + 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): + if request.user.is_authenticated(): + return HttpResponseRedirect(self.get_authenticated_redirect_url()) + return super(AnonymousRequiredMixin, self).dispatch( + request, *args, **kwargs) + + def get_authenticated_redirect_url(self): + """ Return the reversed authenticated redirect url. """ + if not self.authenticated_redirect_url: + raise ImproperlyConfigured( + '{0} is missing an authenticated_redirect_url ' + 'url to redirect to. Define ' + '{0}.authenticated_redirect_url or override ' + '{0}.get_authenticated_redirect_url().'.format( + self.__class__.__name__)) + return self.authenticated_redirect_url + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + + NOTE: + This should be the left-most mixin of a view. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class PermissionRequiredMixin(AccessMixin): + """ + View mixin which verifies that the logged in user has the specified + permission. + + Class Settings + `permission_required` - the permission to check for. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + + class SomeView(PermissionRequiredMixin, ListView): + ... + # required + permission_required = "app.permission" + + # optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + ... + """ + permission_required = None # Default required perms to none + + def 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( + '{0} requires the "permission_required" attribute to be ' + 'set.'.format(self.__class__.__name__)) + + return self.permission_required + + def check_permissions(self, request): + """ + Returns whether or not the user has permissions + """ + perms = self.get_permission_required(request) + return request.user.has_perm(perms) + + def no_permissions_fail(self, request=None): + """ + Called when the user has no permissions. 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()) + + 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: # If the user lacks the permission + if self.raise_exception: + raise PermissionDenied # Return a 403 + return self.no_permissions_fail(request) + + return super(PermissionRequiredMixin, self).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 passed in list. + + By specifying The `any` key , the user must have ONE of the set + permissions in the list. + + Class Settings + `permissions` - This is required to be a dict with one or both + keys of `all` and/or `any` containing a list or tuple of + permissions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + class SomeView(MultiplePermissionsRequiredMixin, ListView): + ... + #required + permissions = { + "all": ("blog.add_post", "blog.change_post"), + "any": ("blog.delete_post", "user.change_user") + } + + #optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + """ + permissions = None # Default required perms to none + + def get_permission_required(self, request=None): + self._check_permissions_attr() + return self.permissions + + def check_permissions(self, request): + permissions = self.get_permission_required() + perms_all = permissions.get('all') or None + perms_any = permissions.get('any') or None + + self._check_permissions_keys_set(perms_all, perms_any) + self._check_perms_keys("all", perms_all) + self._check_perms_keys("any", perms_any) + + # If perms_all, check that user has all permissions in the list/tuple + if perms_all: + if not request.user.has_perms(perms_all): + return False + + # If perms_any, check that user has at least one in the list/tuple + if perms_any: + has_one_perm = False + for perm in perms_any: + if request.user.has_perm(perm): + has_one_perm = True + break + + if not has_one_perm: + 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( + '{0} requires the "permissions" attribute to be set as a ' + 'dict.'.format(self.__class__.__name__)) + + 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( + '{0} requires the "permissions" attribute to be set to a ' + 'dict and the "any" or "all" key to be set.'.format( + self.__class__.__name__)) + + 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( + '{0} requires the permisions dict {1} value to be a ' + 'list or tuple.'.format(self.__class__.__name__, key)) + + +class GroupRequiredMixin(AccessMixin): + group_required = None + + def get_group_required(self): + if self.group_required is None or ( + not isinstance(self.group_required, + (list, tuple) + six.string_types) + ): + + raise ImproperlyConfigured( + '{0} requires the "group_required" attribute to be set and be ' + 'one of the following types: string, unicode, list or ' + 'tuple'.format(self.__class__.__name__)) + if not isinstance(self.group_required, (list, tuple)): + self.group_required = (self.group_required,) + return self.group_required + + def check_membership(self, groups): + """ Check required group(s) """ + 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): + self.request = request + in_group = False + if self.request.user.is_authenticated(): + in_group = self.check_membership(self.get_group_required()) + + if not in_group: + if self.raise_exception: + raise PermissionDenied + else: + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(GroupRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class UserPassesTestMixin(AccessMixin): + """ + CBV Mixin allows you to define test that every user should pass + to get access into view. + + Class Settings + `test_func` - This is required to be a method that takes user + instance and return True or False after checking conditions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + """ + + def test_func(self, user): + raise NotImplementedError( + '{0} is missing implementation of the ' + 'test_func method. You should write one.'.format( + self.__class__.__name__)) + + def get_test_func(self): + return getattr(self, "test_func") + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.get_test_func()(request.user) + + if not user_test_result: # If user don't pass the test + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(UserPassesTestMixin, self).dispatch( + request, *args, **kwargs) + + +class UserFormKwargsMixin(object): + """ + CBV mixin which puts the user from the request into the form kwargs. + Note: Using this mixin requires you to pop the `user` kwarg + out of the dict in the super of your form's `__init__`. + """ + def get_form_kwargs(self): + kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() + # Update the existing form kwargs dict with the request's user. + kwargs.update({"user": self.request.user}) + return kwargs + + +class SuccessURLRedirectListMixin(object): + """ + Simple CBV mixin which sets the success url to the list view of + a given app. Set success_list_url as a class attribute of your + CBV and don't worry about overloading the get_success_url. + + This is only to be used for redirecting to a list page. If you need + to reverse the url with kwargs, this is not the mixin to use. + """ + success_list_url = None # Default the success url to none + + def get_success_url(self): + # Return the reversed success url. + if self.success_list_url is None: + raise ImproperlyConfigured( + '{0} is missing a succes_list_url ' + 'name to reverse and redirect to. Define ' + '{0}.success_list_url or override ' + '{0}.get_success_url().'.format(self.__class__.__name__)) + return reverse(self.success_list_url) + + +class SuperuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_superuser` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: # If the user is a standard user, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response. + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(SuperuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class SetHeadlineMixin(object): + """ + Mixin allows you to set a static headline through a static property on the + class or programmatically by overloading the get_headline method. + """ + headline = None # Default the headline to none + + def get_context_data(self, **kwargs): + kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) + # Update the existing context dict with the provided headline. + kwargs.update({"headline": self.get_headline()}) + return kwargs + + def get_headline(self): + if self.headline is None: # If no headline was provided as a view + # attribute and this method wasn't + # overridden raise a configuration error. + raise ImproperlyConfigured( + '{0} is missing a headline. ' + 'Define {0}.headline, or override ' + '{0}.get_headline().'.format(self.__class__.__name__)) + return force_text(self.headline) + + +class StaticContextMixin(object): + """ + Mixin allows you to set static context through a static property on + the class. + """ + static_context = None + + def get_context_data(self, **kwargs): + kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) + + try: + kwargs.update(self.get_static_context()) + except (TypeError, ValueError): + raise ImproperlyConfigured( + '{0}.static_context must be a dictionary or container ' + 'of two-tuples.'.format(self.__class__.__name__)) + else: + return kwargs + + def get_static_context(self): + if self.static_context is None: + raise ImproperlyConfigured( + '{0} is missing the static_context property. Define ' + '{0}.static_context, or override ' + '{0}.get_static_context()'.format(self.__class__.__name__) + ) + return self.static_context + + +class SelectRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a select_related on. + """ + select_related = None # Default related fields to none + + def get_queryset(self): + if self.select_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the select_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + if not isinstance(self.select_related, (tuple, list)): + # If the select_related argument is *not* a tuple or list, + # raise a configuration error. + raise ImproperlyConfigured( + "{0}'s select_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(SelectRelatedMixin, self).get_queryset() + + return queryset.select_related(*self.select_related) + + +class PrefetchRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a prefetch_related on. + """ + prefetch_related = None # Default prefetch fields to none + + def get_queryset(self): + if self.prefetch_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the prefetch_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + 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( + "{0}'s prefetch_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(PrefetchRelatedMixin, self).get_queryset() + + return queryset.prefetch_related(*self.prefetch_related) + + +class StaffuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_staff` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: # If the request's user is not staff, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(StaffuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONResponseMixin(object): + """ + A mixin that allows you to easily serialize simple data such as a dict or + Django models. + """ + content_type = None + json_dumps_kwargs = None + + def get_content_type(self): + if (self.content_type is not None and + not isinstance(self.content_type, + (six.string_types, six.text_type))): + raise ImproperlyConfigured( + '{0} is missing a content type. Define {0}.content_type, ' + 'or override {0}.get_content_type().'.format( + self.__class__.__name__)) + return self.content_type or u"application/json" + + def get_json_dumps_kwargs(self): + if self.json_dumps_kwargs is None: + self.json_dumps_kwargs = {} + self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) + return self.json_dumps_kwargs + + def render_json_response(self, context_dict, status=200): + """ + Limited serialization for shipping plain data. Do not use for models + or other complex or custom objects. + """ + json_context = json.dumps( + context_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs()).encode(u'utf-8') + return HttpResponse(json_context, + content_type=self.get_content_type(), + status=status) + + def render_json_object_response(self, objects, **kwargs): + """ + Serializes objects using Django's builtin JSON serializer. Additional + kwargs can be used the same way for django.core.serializers.serialize. + """ + json_data = serializers.serialize(u"json", objects, **kwargs) + return HttpResponse(json_data, content_type=self.get_content_type()) + + +class AjaxResponseMixin(object): + """ + Mixin allows you to define alternative methods for ajax requests. Similar + to the normal get, post, and put methods, you can use get_ajax, post_ajax, + and put_ajax. + """ + def dispatch(self, request, *args, **kwargs): + request_method = request.method.lower() + + if request.is_ajax() and request_method in self.http_method_names: + handler = getattr(self, u"{0}_ajax".format(request_method), + self.http_method_not_allowed) + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + return super(AjaxResponseMixin, self).dispatch( + request, *args, **kwargs) + + def get_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def post_ajax(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def put_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def delete_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + +class JsonRequestResponseMixin(JSONResponseMixin): + """ + Extends JSONResponseMixin. Attempts to parse request as JSON. If request + is properly formatted, the json is saved to self.request_json as a Python + object. request_json will be None for imparsible requests. + Set the attribute require_json to True to return a 400 "Bad Request" error + for requests that don't contain JSON. + + Note: To allow public access to your view, you'll need to use the + csrf_exempt decorator or CsrfExemptMixin. + + Example Usage: + + class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): + def post(self, request, *args, **kwargs): + do_stuff_with_contents_of_request_json() + return self.render_json_response( + {'message': 'Thanks!'}) + """ + require_json = False + error_response_dict = {u"errors": [u"Improperly formatted request"]} + + def render_bad_request_response(self, error_dict=None): + if error_dict is None: + error_dict = self.error_response_dict + json_context = json.dumps( + error_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs() + ).encode(u'utf-8') + return HttpResponseBadRequest( + json_context, content_type=self.get_content_type()) + + def get_request_json(self): + try: + return json.loads(self.request.body.decode(u'utf-8')) + except ValueError: + return None + + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + self.request_json = self.get_request_json() + if self.require_json and self.request_json is None: + return self.render_bad_request_response() + return super(JsonRequestResponseMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONRequestResponseMixin(JsonRequestResponseMixin): + pass + + +class OrderableListMixin(object): + """ + Mixin allows your users to order records using GET parameters + """ + + orderable_columns = None + orderable_columns_default = None + order_by = None + ordering = None + + def get_context_data(self, **kwargs): + """ + Augments context with: + + * ``order_by`` - name of the field + * ``ordering`` - order of ordering, either ``asc`` or ``desc`` + """ + context = super(OrderableListMixin, self).get_context_data(**kwargs) + context["order_by"] = self.order_by + context["ordering"] = self.ordering + return context + + def get_orderable_columns(self): + if not self.orderable_columns: + raise ImproperlyConfigured( + '{0} needs the ordering columns defined.'.format( + self.__class__.__name__)) + return self.orderable_columns + + def get_orderable_columns_default(self): + if not self.orderable_columns_default: + raise ImproperlyConfigured( + '{0} needs the default ordering column defined.'.format( + self.__class__.__name__)) + return self.orderable_columns_default + + def get_ordered_queryset(self, queryset=None): + """ + Augments ``QuerySet`` with order_by statement if possible + + :param QuerySet queryset: ``QuerySet`` to ``order_by`` + :return: QuerySet + """ + get_order_by = self.request.GET.get("order_by") + + if get_order_by in self.get_orderable_columns(): + order_by = get_order_by + else: + order_by = self.get_orderable_columns_default() + + self.order_by = order_by + self.ordering = "asc" + + if order_by and self.request.GET.get("ordering", "asc") == "desc": + order_by = "-" + order_by + self.ordering = "desc" + + return queryset.order_by(order_by) + + def get_queryset(self): + """ + Returns ordered ``QuerySet`` + """ + unordered_queryset = super(OrderableListMixin, self).get_queryset() + return self.get_ordered_queryset(unordered_queryset) + + +class CanonicalSlugDetailMixin(object): + """ + A mixin that enforces a canonical slug in the url. + + If a urlpattern takes a object's pk and slug as arguments and the slug url + argument does not equal the object's canonical slug, this mixin will + redirect to the url containing the canonical slug. + """ + def dispatch(self, request, *args, **kwargs): + # Set up since we need to super() later instead of earlier. + self.request = request + self.args = args + self.kwargs = kwargs + + # Get the current object, url slug, and + # urlpattern name (namespace aware). + obj = self.get_object() + slug = self.kwargs.get(self.slug_url_kwarg, None) + match = resolve(request.path_info) + url_parts = match.namespaces + url_parts.append(match.url_name) + current_urlpattern = ":".join(url_parts) + + # Figure out what the slug is supposed to be. + if hasattr(obj, "get_canonical_slug"): + canonical_slug = obj.get_canonical_slug() + else: + canonical_slug = self.get_canonical_slug() + + # If there's a discrepancy between the slug in the url and the + # canonical slug, redirect to the canonical slug. + if canonical_slug != slug: + params = {self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + 'permanent': True} + return redirect(current_urlpattern, **params) + + return super(CanonicalSlugDetailMixin, self).dispatch( + request, *args, **kwargs) + + def get_canonical_slug(self): + """ + Override this method to customize what slug should be considered + canonical. + + Alternatively, define the get_canonical_slug method on this view's + object class. In that case, this method will never be called. + """ + return self.get_object().slug + + +class _MessageAPIWrapper(object): + """ + Wrap the django.contrib.messages.api module to automatically pass a given + request object as the first parameter of 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, curry(api_fn, request)) + + +class _MessageDescriptor(object): + """ + A descriptor that binds the _MessageAPIWrapper to the view's + request. + """ + def __get__(self, instance, owner): + return _MessageAPIWrapper(instance.request) + + +class MessageMixin(object): + """ + Add a `messages` attribute on the view instance that wraps + `django.contrib .messages`, automatically passing the current + request object. + """ + messages = _MessageDescriptor() + + +class FormValidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_valid_message method. + """ + form_valid_message = None # Default to None + + def get_form_valid_message(self): + """ + Validate that form_valid_message is set and is either a + unicode or str object. + """ + if self.form_valid_message is None: + raise ImproperlyConfigured( + '{0}.form_valid_message is not set. Define ' + '{0}.form_valid_message, or override ' + '{0}.get_form_valid_message().'.format(self.__class__.__name__) + ) + + if not isinstance(self.form_valid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_valid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__) + ) + + return force_text(self.form_valid_message) + + def form_valid(self, form): + """ + Call the super first, so that when overriding + get_form_valid_message, we have access to the newly saved object. + """ + response = super(FormValidMessageMixin, self).form_valid(form) + self.messages.success(self.get_form_valid_message(), + fail_silently=True) + return response + + +class FormInvalidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_invalid_message method. + """ + form_invalid_message = None + + def get_form_invalid_message(self): + """ + Validate that form_invalid_message is set and is either a + unicode or str object. + """ + if self.form_invalid_message is None: + raise ImproperlyConfigured( + '{0}.form_invalid_message is not set. Define ' + '{0}.form_invalid_message, or override ' + '{0}.get_form_invalid_message().'.format( + self.__class__.__name__)) + + if not isinstance(self.form_invalid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_invalid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__)) + + return force_text(self.form_invalid_message) + + def form_invalid(self, form): + response = super(FormInvalidMessageMixin, self).form_invalid(form) + self.messages.error(self.get_form_invalid_message(), + fail_silently=True) + return response + + +class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): + """ + Mixin is a shortcut to use both FormValidMessageMixin and + FormInvalidMessageMixin. + """ + pass + + +class AllVerbsMixin(object): + """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): + if not self.all_handler: + raise ImproperlyConfigured( + '{0} requires the all_handler attribute to be set.'.format( + self.__class__.__name__)) + + handler = getattr(self, self.all_handler, self.http_method_not_allowed) + return handler(request, *args, **kwargs) diff --git a/braces/views/_other.py b/braces/views/_other.py new file mode 100644 index 00000000..e79aedfa --- /dev/null +++ b/braces/views/_other.py @@ -0,0 +1,949 @@ +import six + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.core import serializers +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import resolve, reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.utils.encoding import force_text +from django.utils.functional import curry, Promise +from django.views.decorators.csrf import csrf_exempt + +## Django 1.5+ compat +try: + import json +except ImportError: # pragma: no cover + from django.utils import simplejson as json + + +class AccessMixin(object): + """ + 'Abstract' mixin that gives access mixins the same customizable + functionality. + """ + login_url = None + raise_exception = False # Default whether to raise an exception to none + redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth + + def get_login_url(self): + """ + Override this method to customize the login_url. + """ + login_url = self.login_url or settings.LOGIN_URL + if not login_url: + raise ImproperlyConfigured( + 'Define {0}.login_url or settings.LOGIN_URL or override ' + '{0}.get_login_url().'.format(self.__class__.__name__)) + + return force_text(login_url) + + def get_redirect_field_name(self): + """ + Override this method to customize the redirect_field_name. + """ + if self.redirect_field_name is None: + raise ImproperlyConfigured( + '{0} is missing the ' + 'redirect_field_name. Define {0}.redirect_field_name or ' + 'override {0}.get_redirect_field_name().'.format( + self.__class__.__name__)) + return self.redirect_field_name + + +class LoginRequiredMixin(AccessMixin): + """ + View mixin which verifies that the user is authenticated. + + NOTE: + This should be the left-most mixin of a view, except when + combined with CsrfExemptMixin - which in that case should + be the left-most mixin. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(LoginRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class AnonymousRequiredMixin(object): + """ + View mixin which redirects to a specified URL if authenticated. + Can be useful if you wanted to prevent authenticated users from + accessing signup pages etc. + + 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): + if request.user.is_authenticated(): + return HttpResponseRedirect(self.get_authenticated_redirect_url()) + return super(AnonymousRequiredMixin, self).dispatch( + request, *args, **kwargs) + + def get_authenticated_redirect_url(self): + """ Return the reversed authenticated redirect url. """ + if not self.authenticated_redirect_url: + raise ImproperlyConfigured( + '{0} is missing an authenticated_redirect_url ' + 'url to redirect to. Define ' + '{0}.authenticated_redirect_url or override ' + '{0}.get_authenticated_redirect_url().'.format( + self.__class__.__name__)) + return self.authenticated_redirect_url + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + + NOTE: + This should be the left-most mixin of a view. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class PermissionRequiredMixin(AccessMixin): + """ + View mixin which verifies that the logged in user has the specified + permission. + + Class Settings + `permission_required` - the permission to check for. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + + class SomeView(PermissionRequiredMixin, ListView): + ... + # required + permission_required = "app.permission" + + # optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + ... + """ + permission_required = None # Default required perms to none + + def 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( + '{0} requires the "permission_required" attribute to be ' + 'set.'.format(self.__class__.__name__)) + + return self.permission_required + + def check_permissions(self, request): + """ + Returns whether or not the user has permissions + """ + perms = self.get_permission_required(request) + return request.user.has_perm(perms) + + def no_permissions_fail(self, request=None): + """ + Called when the user has no permissions. 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()) + + 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: # If the user lacks the permission + if self.raise_exception: + raise PermissionDenied # Return a 403 + return self.no_permissions_fail(request) + + return super(PermissionRequiredMixin, self).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 passed in list. + + By specifying The `any` key , the user must have ONE of the set + permissions in the list. + + Class Settings + `permissions` - This is required to be a dict with one or both + keys of `all` and/or `any` containing a list or tuple of + permissions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + class SomeView(MultiplePermissionsRequiredMixin, ListView): + ... + #required + permissions = { + "all": ("blog.add_post", "blog.change_post"), + "any": ("blog.delete_post", "user.change_user") + } + + #optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + """ + permissions = None # Default required perms to none + + def get_permission_required(self, request=None): + self._check_permissions_attr() + return self.permissions + + def check_permissions(self, request): + permissions = self.get_permission_required() + perms_all = permissions.get('all') or None + perms_any = permissions.get('any') or None + + self._check_permissions_keys_set(perms_all, perms_any) + self._check_perms_keys("all", perms_all) + self._check_perms_keys("any", perms_any) + + # If perms_all, check that user has all permissions in the list/tuple + if perms_all: + if not request.user.has_perms(perms_all): + return False + + # If perms_any, check that user has at least one in the list/tuple + if perms_any: + has_one_perm = False + for perm in perms_any: + if request.user.has_perm(perm): + has_one_perm = True + break + + if not has_one_perm: + 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( + '{0} requires the "permissions" attribute to be set as a ' + 'dict.'.format(self.__class__.__name__)) + + 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( + '{0} requires the "permissions" attribute to be set to a ' + 'dict and the "any" or "all" key to be set.'.format( + self.__class__.__name__)) + + 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( + '{0} requires the permisions dict {1} value to be a ' + 'list or tuple.'.format(self.__class__.__name__, key)) + + +class GroupRequiredMixin(AccessMixin): + group_required = None + + def get_group_required(self): + if self.group_required is None or ( + not isinstance(self.group_required, + (list, tuple) + six.string_types) + ): + + raise ImproperlyConfigured( + '{0} requires the "group_required" attribute to be set and be ' + 'one of the following types: string, unicode, list or ' + 'tuple'.format(self.__class__.__name__)) + if not isinstance(self.group_required, (list, tuple)): + self.group_required = (self.group_required,) + return self.group_required + + def check_membership(self, groups): + """ Check required group(s) """ + 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): + self.request = request + in_group = False + if self.request.user.is_authenticated(): + in_group = self.check_membership(self.get_group_required()) + + if not in_group: + if self.raise_exception: + raise PermissionDenied + else: + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(GroupRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class UserPassesTestMixin(AccessMixin): + """ + CBV Mixin allows you to define test that every user should pass + to get access into view. + + Class Settings + `test_func` - This is required to be a method that takes user + instance and return True or False after checking conditions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + """ + + def test_func(self, user): + raise NotImplementedError( + '{0} is missing implementation of the ' + 'test_func method. You should write one.'.format( + self.__class__.__name__)) + + def get_test_func(self): + return getattr(self, "test_func") + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.get_test_func()(request.user) + + if not user_test_result: # If user don't pass the test + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(UserPassesTestMixin, self).dispatch( + request, *args, **kwargs) + + +class UserFormKwargsMixin(object): + """ + CBV mixin which puts the user from the request into the form kwargs. + Note: Using this mixin requires you to pop the `user` kwarg + out of the dict in the super of your form's `__init__`. + """ + def get_form_kwargs(self): + kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() + # Update the existing form kwargs dict with the request's user. + kwargs.update({"user": self.request.user}) + return kwargs + + +class SuccessURLRedirectListMixin(object): + """ + Simple CBV mixin which sets the success url to the list view of + a given app. Set success_list_url as a class attribute of your + CBV and don't worry about overloading the get_success_url. + + This is only to be used for redirecting to a list page. If you need + to reverse the url with kwargs, this is not the mixin to use. + """ + success_list_url = None # Default the success url to none + + def get_success_url(self): + # Return the reversed success url. + if self.success_list_url is None: + raise ImproperlyConfigured( + '{0} is missing a succes_list_url ' + 'name to reverse and redirect to. Define ' + '{0}.success_list_url or override ' + '{0}.get_success_url().'.format(self.__class__.__name__)) + return reverse(self.success_list_url) + + +class SuperuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_superuser` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: # If the user is a standard user, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response. + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(SuperuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class SetHeadlineMixin(object): + """ + Mixin allows you to set a static headline through a static property on the + class or programmatically by overloading the get_headline method. + """ + headline = None # Default the headline to none + + def get_context_data(self, **kwargs): + kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) + # Update the existing context dict with the provided headline. + kwargs.update({"headline": self.get_headline()}) + return kwargs + + def get_headline(self): + if self.headline is None: # If no headline was provided as a view + # attribute and this method wasn't + # overridden raise a configuration error. + raise ImproperlyConfigured( + '{0} is missing a headline. ' + 'Define {0}.headline, or override ' + '{0}.get_headline().'.format(self.__class__.__name__)) + return force_text(self.headline) + + +class StaticContextMixin(object): + """ + Mixin allows you to set static context through a static property on + the class. + """ + static_context = None + + def get_context_data(self, **kwargs): + kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) + + try: + kwargs.update(self.get_static_context()) + except (TypeError, ValueError): + raise ImproperlyConfigured( + '{0}.static_context must be a dictionary or container ' + 'of two-tuples.'.format(self.__class__.__name__)) + else: + return kwargs + + def get_static_context(self): + if self.static_context is None: + raise ImproperlyConfigured( + '{0} is missing the static_context property. Define ' + '{0}.static_context, or override ' + '{0}.get_static_context()'.format(self.__class__.__name__) + ) + return self.static_context + + +class SelectRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a select_related on. + """ + select_related = None # Default related fields to none + + def get_queryset(self): + if self.select_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the select_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + if not isinstance(self.select_related, (tuple, list)): + # If the select_related argument is *not* a tuple or list, + # raise a configuration error. + raise ImproperlyConfigured( + "{0}'s select_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(SelectRelatedMixin, self).get_queryset() + + return queryset.select_related(*self.select_related) + + +class PrefetchRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a prefetch_related on. + """ + prefetch_related = None # Default prefetch fields to none + + def get_queryset(self): + if self.prefetch_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the prefetch_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + 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( + "{0}'s prefetch_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(PrefetchRelatedMixin, self).get_queryset() + + return queryset.prefetch_related(*self.prefetch_related) + + +class StaffuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_staff` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: # If the request's user is not staff, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(StaffuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONResponseMixin(object): + """ + A mixin that allows you to easily serialize simple data such as a dict or + Django models. + """ + content_type = None + json_dumps_kwargs = None + + def get_content_type(self): + if (self.content_type is not None and + not isinstance(self.content_type, + (six.string_types, six.text_type))): + raise ImproperlyConfigured( + '{0} is missing a content type. Define {0}.content_type, ' + 'or override {0}.get_content_type().'.format( + self.__class__.__name__)) + return self.content_type or u"application/json" + + def get_json_dumps_kwargs(self): + if self.json_dumps_kwargs is None: + self.json_dumps_kwargs = {} + self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) + return self.json_dumps_kwargs + + def render_json_response(self, context_dict, status=200): + """ + Limited serialization for shipping plain data. Do not use for models + or other complex or custom objects. + """ + json_context = json.dumps( + context_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs()).encode(u'utf-8') + return HttpResponse(json_context, + content_type=self.get_content_type(), + status=status) + + def render_json_object_response(self, objects, **kwargs): + """ + Serializes objects using Django's builtin JSON serializer. Additional + kwargs can be used the same way for django.core.serializers.serialize. + """ + json_data = serializers.serialize(u"json", objects, **kwargs) + return HttpResponse(json_data, content_type=self.get_content_type()) + + +class AjaxResponseMixin(object): + """ + Mixin allows you to define alternative methods for ajax requests. Similar + to the normal get, post, and put methods, you can use get_ajax, post_ajax, + and put_ajax. + """ + def dispatch(self, request, *args, **kwargs): + request_method = request.method.lower() + + if request.is_ajax() and request_method in self.http_method_names: + handler = getattr(self, u"{0}_ajax".format(request_method), + self.http_method_not_allowed) + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + return super(AjaxResponseMixin, self).dispatch( + request, *args, **kwargs) + + def get_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def post_ajax(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def put_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def delete_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + +class JsonRequestResponseMixin(JSONResponseMixin): + """ + Extends JSONResponseMixin. Attempts to parse request as JSON. If request + is properly formatted, the json is saved to self.request_json as a Python + object. request_json will be None for imparsible requests. + Set the attribute require_json to True to return a 400 "Bad Request" error + for requests that don't contain JSON. + + Note: To allow public access to your view, you'll need to use the + csrf_exempt decorator or CsrfExemptMixin. + + Example Usage: + + class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): + def post(self, request, *args, **kwargs): + do_stuff_with_contents_of_request_json() + return self.render_json_response( + {'message': 'Thanks!'}) + """ + require_json = False + error_response_dict = {u"errors": [u"Improperly formatted request"]} + + def render_bad_request_response(self, error_dict=None): + if error_dict is None: + error_dict = self.error_response_dict + json_context = json.dumps( + error_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs() + ).encode(u'utf-8') + return HttpResponseBadRequest( + json_context, content_type=self.get_content_type()) + + def get_request_json(self): + try: + return json.loads(self.request.body.decode(u'utf-8')) + except ValueError: + return None + + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + self.request_json = self.get_request_json() + if self.require_json and self.request_json is None: + return self.render_bad_request_response() + return super(JsonRequestResponseMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONRequestResponseMixin(JsonRequestResponseMixin): + pass + + +class OrderableListMixin(object): + """ + Mixin allows your users to order records using GET parameters + """ + + orderable_columns = None + orderable_columns_default = None + order_by = None + ordering = None + + def get_context_data(self, **kwargs): + """ + Augments context with: + + * ``order_by`` - name of the field + * ``ordering`` - order of ordering, either ``asc`` or ``desc`` + """ + context = super(OrderableListMixin, self).get_context_data(**kwargs) + context["order_by"] = self.order_by + context["ordering"] = self.ordering + return context + + def get_orderable_columns(self): + if not self.orderable_columns: + raise ImproperlyConfigured( + '{0} needs the ordering columns defined.'.format( + self.__class__.__name__)) + return self.orderable_columns + + def get_orderable_columns_default(self): + if not self.orderable_columns_default: + raise ImproperlyConfigured( + '{0} needs the default ordering column defined.'.format( + self.__class__.__name__)) + return self.orderable_columns_default + + def get_ordered_queryset(self, queryset=None): + """ + Augments ``QuerySet`` with order_by statement if possible + + :param QuerySet queryset: ``QuerySet`` to ``order_by`` + :return: QuerySet + """ + get_order_by = self.request.GET.get("order_by") + + if get_order_by in self.get_orderable_columns(): + order_by = get_order_by + else: + order_by = self.get_orderable_columns_default() + + self.order_by = order_by + self.ordering = "asc" + + if order_by and self.request.GET.get("ordering", "asc") == "desc": + order_by = "-" + order_by + self.ordering = "desc" + + return queryset.order_by(order_by) + + def get_queryset(self): + """ + Returns ordered ``QuerySet`` + """ + unordered_queryset = super(OrderableListMixin, self).get_queryset() + return self.get_ordered_queryset(unordered_queryset) + + +class CanonicalSlugDetailMixin(object): + """ + A mixin that enforces a canonical slug in the url. + + If a urlpattern takes a object's pk and slug as arguments and the slug url + argument does not equal the object's canonical slug, this mixin will + redirect to the url containing the canonical slug. + """ + def dispatch(self, request, *args, **kwargs): + # Set up since we need to super() later instead of earlier. + self.request = request + self.args = args + self.kwargs = kwargs + + # Get the current object, url slug, and + # urlpattern name (namespace aware). + obj = self.get_object() + slug = self.kwargs.get(self.slug_url_kwarg, None) + match = resolve(request.path_info) + url_parts = match.namespaces + url_parts.append(match.url_name) + current_urlpattern = ":".join(url_parts) + + # Figure out what the slug is supposed to be. + if hasattr(obj, "get_canonical_slug"): + canonical_slug = obj.get_canonical_slug() + else: + canonical_slug = self.get_canonical_slug() + + # If there's a discrepancy between the slug in the url and the + # canonical slug, redirect to the canonical slug. + if canonical_slug != slug: + params = {self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + 'permanent': True} + return redirect(current_urlpattern, **params) + + return super(CanonicalSlugDetailMixin, self).dispatch( + request, *args, **kwargs) + + def get_canonical_slug(self): + """ + Override this method to customize what slug should be considered + canonical. + + Alternatively, define the get_canonical_slug method on this view's + object class. In that case, this method will never be called. + """ + return self.get_object().slug + + +class _MessageAPIWrapper(object): + """ + Wrap the django.contrib.messages.api module to automatically pass a given + request object as the first parameter of 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, curry(api_fn, request)) + + +class _MessageDescriptor(object): + """ + A descriptor that binds the _MessageAPIWrapper to the view's + request. + """ + def __get__(self, instance, owner): + return _MessageAPIWrapper(instance.request) + + +class MessageMixin(object): + """ + Add a `messages` attribute on the view instance that wraps + `django.contrib .messages`, automatically passing the current + request object. + """ + messages = _MessageDescriptor() + + +class FormValidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_valid_message method. + """ + form_valid_message = None # Default to None + + def get_form_valid_message(self): + """ + Validate that form_valid_message is set and is either a + unicode or str object. + """ + if self.form_valid_message is None: + raise ImproperlyConfigured( + '{0}.form_valid_message is not set. Define ' + '{0}.form_valid_message, or override ' + '{0}.get_form_valid_message().'.format(self.__class__.__name__) + ) + + if not isinstance(self.form_valid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_valid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__) + ) + + return force_text(self.form_valid_message) + + def form_valid(self, form): + """ + Call the super first, so that when overriding + get_form_valid_message, we have access to the newly saved object. + """ + response = super(FormValidMessageMixin, self).form_valid(form) + self.messages.success(self.get_form_valid_message(), + fail_silently=True) + return response + + +class FormInvalidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_invalid_message method. + """ + form_invalid_message = None + + def get_form_invalid_message(self): + """ + Validate that form_invalid_message is set and is either a + unicode or str object. + """ + if self.form_invalid_message is None: + raise ImproperlyConfigured( + '{0}.form_invalid_message is not set. Define ' + '{0}.form_invalid_message, or override ' + '{0}.get_form_invalid_message().'.format( + self.__class__.__name__)) + + if not isinstance(self.form_invalid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_invalid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__)) + + return force_text(self.form_invalid_message) + + def form_invalid(self, form): + response = super(FormInvalidMessageMixin, self).form_invalid(form) + self.messages.error(self.get_form_invalid_message(), + fail_silently=True) + return response + + +class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): + """ + Mixin is a shortcut to use both FormValidMessageMixin and + FormInvalidMessageMixin. + """ + pass + + +class AllVerbsMixin(object): + """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): + if not self.all_handler: + raise ImproperlyConfigured( + '{0} requires the all_handler attribute to be set.'.format( + self.__class__.__name__)) + + handler = getattr(self, self.all_handler, self.http_method_not_allowed) + return handler(request, *args, **kwargs) diff --git a/braces/views/_queries.py b/braces/views/_queries.py new file mode 100644 index 00000000..e79aedfa --- /dev/null +++ b/braces/views/_queries.py @@ -0,0 +1,949 @@ +import six + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.core import serializers +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import resolve, reverse +from django.http import (HttpResponse, HttpResponseBadRequest, + HttpResponseRedirect) +from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.utils.encoding import force_text +from django.utils.functional import curry, Promise +from django.views.decorators.csrf import csrf_exempt + +## Django 1.5+ compat +try: + import json +except ImportError: # pragma: no cover + from django.utils import simplejson as json + + +class AccessMixin(object): + """ + 'Abstract' mixin that gives access mixins the same customizable + functionality. + """ + login_url = None + raise_exception = False # Default whether to raise an exception to none + redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth + + def get_login_url(self): + """ + Override this method to customize the login_url. + """ + login_url = self.login_url or settings.LOGIN_URL + if not login_url: + raise ImproperlyConfigured( + 'Define {0}.login_url or settings.LOGIN_URL or override ' + '{0}.get_login_url().'.format(self.__class__.__name__)) + + return force_text(login_url) + + def get_redirect_field_name(self): + """ + Override this method to customize the redirect_field_name. + """ + if self.redirect_field_name is None: + raise ImproperlyConfigured( + '{0} is missing the ' + 'redirect_field_name. Define {0}.redirect_field_name or ' + 'override {0}.get_redirect_field_name().'.format( + self.__class__.__name__)) + return self.redirect_field_name + + +class LoginRequiredMixin(AccessMixin): + """ + View mixin which verifies that the user is authenticated. + + NOTE: + This should be the left-most mixin of a view, except when + combined with CsrfExemptMixin - which in that case should + be the left-most mixin. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + if self.raise_exception: + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(LoginRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class AnonymousRequiredMixin(object): + """ + View mixin which redirects to a specified URL if authenticated. + Can be useful if you wanted to prevent authenticated users from + accessing signup pages etc. + + 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): + if request.user.is_authenticated(): + return HttpResponseRedirect(self.get_authenticated_redirect_url()) + return super(AnonymousRequiredMixin, self).dispatch( + request, *args, **kwargs) + + def get_authenticated_redirect_url(self): + """ Return the reversed authenticated redirect url. """ + if not self.authenticated_redirect_url: + raise ImproperlyConfigured( + '{0} is missing an authenticated_redirect_url ' + 'url to redirect to. Define ' + '{0}.authenticated_redirect_url or override ' + '{0}.get_authenticated_redirect_url().'.format( + self.__class__.__name__)) + return self.authenticated_redirect_url + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + + NOTE: + This should be the left-most mixin of a view. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class PermissionRequiredMixin(AccessMixin): + """ + View mixin which verifies that the logged in user has the specified + permission. + + Class Settings + `permission_required` - the permission to check for. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + + class SomeView(PermissionRequiredMixin, ListView): + ... + # required + permission_required = "app.permission" + + # optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + ... + """ + permission_required = None # Default required perms to none + + def 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( + '{0} requires the "permission_required" attribute to be ' + 'set.'.format(self.__class__.__name__)) + + return self.permission_required + + def check_permissions(self, request): + """ + Returns whether or not the user has permissions + """ + perms = self.get_permission_required(request) + return request.user.has_perm(perms) + + def no_permissions_fail(self, request=None): + """ + Called when the user has no permissions. 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()) + + 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: # If the user lacks the permission + if self.raise_exception: + raise PermissionDenied # Return a 403 + return self.no_permissions_fail(request) + + return super(PermissionRequiredMixin, self).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 passed in list. + + By specifying The `any` key , the user must have ONE of the set + permissions in the list. + + Class Settings + `permissions` - This is required to be a dict with one or both + keys of `all` and/or `any` containing a list or tuple of + permissions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + + Example Usage + class SomeView(MultiplePermissionsRequiredMixin, ListView): + ... + #required + permissions = { + "all": ("blog.add_post", "blog.change_post"), + "any": ("blog.delete_post", "user.change_user") + } + + #optional + login_url = "/signup/" + redirect_field_name = "hollaback" + raise_exception = True + """ + permissions = None # Default required perms to none + + def get_permission_required(self, request=None): + self._check_permissions_attr() + return self.permissions + + def check_permissions(self, request): + permissions = self.get_permission_required() + perms_all = permissions.get('all') or None + perms_any = permissions.get('any') or None + + self._check_permissions_keys_set(perms_all, perms_any) + self._check_perms_keys("all", perms_all) + self._check_perms_keys("any", perms_any) + + # If perms_all, check that user has all permissions in the list/tuple + if perms_all: + if not request.user.has_perms(perms_all): + return False + + # If perms_any, check that user has at least one in the list/tuple + if perms_any: + has_one_perm = False + for perm in perms_any: + if request.user.has_perm(perm): + has_one_perm = True + break + + if not has_one_perm: + 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( + '{0} requires the "permissions" attribute to be set as a ' + 'dict.'.format(self.__class__.__name__)) + + 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( + '{0} requires the "permissions" attribute to be set to a ' + 'dict and the "any" or "all" key to be set.'.format( + self.__class__.__name__)) + + 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( + '{0} requires the permisions dict {1} value to be a ' + 'list or tuple.'.format(self.__class__.__name__, key)) + + +class GroupRequiredMixin(AccessMixin): + group_required = None + + def get_group_required(self): + if self.group_required is None or ( + not isinstance(self.group_required, + (list, tuple) + six.string_types) + ): + + raise ImproperlyConfigured( + '{0} requires the "group_required" attribute to be set and be ' + 'one of the following types: string, unicode, list or ' + 'tuple'.format(self.__class__.__name__)) + if not isinstance(self.group_required, (list, tuple)): + self.group_required = (self.group_required,) + return self.group_required + + def check_membership(self, groups): + """ Check required group(s) """ + 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): + self.request = request + in_group = False + if self.request.user.is_authenticated(): + in_group = self.check_membership(self.get_group_required()) + + if not in_group: + if self.raise_exception: + raise PermissionDenied + else: + return redirect_to_login( + request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(GroupRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class UserPassesTestMixin(AccessMixin): + """ + CBV Mixin allows you to define test that every user should pass + to get access into view. + + Class Settings + `test_func` - This is required to be a method that takes user + instance and return True or False after checking conditions. + `login_url` - the login url of site + `redirect_field_name` - defaults to "next" + `raise_exception` - defaults to False - raise 403 if set to True + """ + + def test_func(self, user): + raise NotImplementedError( + '{0} is missing implementation of the ' + 'test_func method. You should write one.'.format( + self.__class__.__name__)) + + def get_test_func(self): + return getattr(self, "test_func") + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.get_test_func()(request.user) + + if not user_test_result: # If user don't pass the test + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + return super(UserPassesTestMixin, self).dispatch( + request, *args, **kwargs) + + +class UserFormKwargsMixin(object): + """ + CBV mixin which puts the user from the request into the form kwargs. + Note: Using this mixin requires you to pop the `user` kwarg + out of the dict in the super of your form's `__init__`. + """ + def get_form_kwargs(self): + kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() + # Update the existing form kwargs dict with the request's user. + kwargs.update({"user": self.request.user}) + return kwargs + + +class SuccessURLRedirectListMixin(object): + """ + Simple CBV mixin which sets the success url to the list view of + a given app. Set success_list_url as a class attribute of your + CBV and don't worry about overloading the get_success_url. + + This is only to be used for redirecting to a list page. If you need + to reverse the url with kwargs, this is not the mixin to use. + """ + success_list_url = None # Default the success url to none + + def get_success_url(self): + # Return the reversed success url. + if self.success_list_url is None: + raise ImproperlyConfigured( + '{0} is missing a succes_list_url ' + 'name to reverse and redirect to. Define ' + '{0}.success_list_url or override ' + '{0}.get_success_url().'.format(self.__class__.__name__)) + return reverse(self.success_list_url) + + +class SuperuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_superuser` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: # If the user is a standard user, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response. + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(SuperuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class SetHeadlineMixin(object): + """ + Mixin allows you to set a static headline through a static property on the + class or programmatically by overloading the get_headline method. + """ + headline = None # Default the headline to none + + def get_context_data(self, **kwargs): + kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) + # Update the existing context dict with the provided headline. + kwargs.update({"headline": self.get_headline()}) + return kwargs + + def get_headline(self): + if self.headline is None: # If no headline was provided as a view + # attribute and this method wasn't + # overridden raise a configuration error. + raise ImproperlyConfigured( + '{0} is missing a headline. ' + 'Define {0}.headline, or override ' + '{0}.get_headline().'.format(self.__class__.__name__)) + return force_text(self.headline) + + +class StaticContextMixin(object): + """ + Mixin allows you to set static context through a static property on + the class. + """ + static_context = None + + def get_context_data(self, **kwargs): + kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) + + try: + kwargs.update(self.get_static_context()) + except (TypeError, ValueError): + raise ImproperlyConfigured( + '{0}.static_context must be a dictionary or container ' + 'of two-tuples.'.format(self.__class__.__name__)) + else: + return kwargs + + def get_static_context(self): + if self.static_context is None: + raise ImproperlyConfigured( + '{0} is missing the static_context property. Define ' + '{0}.static_context, or override ' + '{0}.get_static_context()'.format(self.__class__.__name__) + ) + return self.static_context + + +class SelectRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a select_related on. + """ + select_related = None # Default related fields to none + + def get_queryset(self): + if self.select_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the select_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + if not isinstance(self.select_related, (tuple, list)): + # If the select_related argument is *not* a tuple or list, + # raise a configuration error. + raise ImproperlyConfigured( + "{0}'s select_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(SelectRelatedMixin, self).get_queryset() + + return queryset.select_related(*self.select_related) + + +class PrefetchRelatedMixin(object): + """ + Mixin allows you to provide a tuple or list of related models to + perform a prefetch_related on. + """ + prefetch_related = None # Default prefetch fields to none + + def get_queryset(self): + if self.prefetch_related is None: # If no fields were provided, + # raise a configuration error + raise ImproperlyConfigured( + '{0} is missing the prefetch_related property. This must be ' + 'a tuple or list.'.format(self.__class__.__name__)) + + 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( + "{0}'s prefetch_related property must be a tuple or " + "list.".format(self.__class__.__name__)) + + # Get the current queryset of the view + queryset = super(PrefetchRelatedMixin, self).get_queryset() + + return queryset.prefetch_related(*self.prefetch_related) + + +class StaffuserRequiredMixin(AccessMixin): + """ + Mixin allows you to require a user with `is_staff` set to True. + """ + def dispatch(self, request, *args, **kwargs): + if not request.user.is_staff: # If the request's user is not staff, + if self.raise_exception: # *and* if an exception was desired + raise PermissionDenied # return a forbidden response + else: + return redirect_to_login(request.get_full_path(), + self.get_login_url(), + self.get_redirect_field_name()) + + return super(StaffuserRequiredMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONResponseMixin(object): + """ + A mixin that allows you to easily serialize simple data such as a dict or + Django models. + """ + content_type = None + json_dumps_kwargs = None + + def get_content_type(self): + if (self.content_type is not None and + not isinstance(self.content_type, + (six.string_types, six.text_type))): + raise ImproperlyConfigured( + '{0} is missing a content type. Define {0}.content_type, ' + 'or override {0}.get_content_type().'.format( + self.__class__.__name__)) + return self.content_type or u"application/json" + + def get_json_dumps_kwargs(self): + if self.json_dumps_kwargs is None: + self.json_dumps_kwargs = {} + self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) + return self.json_dumps_kwargs + + def render_json_response(self, context_dict, status=200): + """ + Limited serialization for shipping plain data. Do not use for models + or other complex or custom objects. + """ + json_context = json.dumps( + context_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs()).encode(u'utf-8') + return HttpResponse(json_context, + content_type=self.get_content_type(), + status=status) + + def render_json_object_response(self, objects, **kwargs): + """ + Serializes objects using Django's builtin JSON serializer. Additional + kwargs can be used the same way for django.core.serializers.serialize. + """ + json_data = serializers.serialize(u"json", objects, **kwargs) + return HttpResponse(json_data, content_type=self.get_content_type()) + + +class AjaxResponseMixin(object): + """ + Mixin allows you to define alternative methods for ajax requests. Similar + to the normal get, post, and put methods, you can use get_ajax, post_ajax, + and put_ajax. + """ + def dispatch(self, request, *args, **kwargs): + request_method = request.method.lower() + + if request.is_ajax() and request_method in self.http_method_names: + handler = getattr(self, u"{0}_ajax".format(request_method), + self.http_method_not_allowed) + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + return super(AjaxResponseMixin, self).dispatch( + request, *args, **kwargs) + + def get_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def post_ajax(self, request, *args, **kwargs): + return self.post(request, *args, **kwargs) + + def put_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + def delete_ajax(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + + +class JsonRequestResponseMixin(JSONResponseMixin): + """ + Extends JSONResponseMixin. Attempts to parse request as JSON. If request + is properly formatted, the json is saved to self.request_json as a Python + object. request_json will be None for imparsible requests. + Set the attribute require_json to True to return a 400 "Bad Request" error + for requests that don't contain JSON. + + Note: To allow public access to your view, you'll need to use the + csrf_exempt decorator or CsrfExemptMixin. + + Example Usage: + + class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): + def post(self, request, *args, **kwargs): + do_stuff_with_contents_of_request_json() + return self.render_json_response( + {'message': 'Thanks!'}) + """ + require_json = False + error_response_dict = {u"errors": [u"Improperly formatted request"]} + + def render_bad_request_response(self, error_dict=None): + if error_dict is None: + error_dict = self.error_response_dict + json_context = json.dumps( + error_dict, + cls=DjangoJSONEncoder, + **self.get_json_dumps_kwargs() + ).encode(u'utf-8') + return HttpResponseBadRequest( + json_context, content_type=self.get_content_type()) + + def get_request_json(self): + try: + return json.loads(self.request.body.decode(u'utf-8')) + except ValueError: + return None + + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + self.request_json = self.get_request_json() + if self.require_json and self.request_json is None: + return self.render_bad_request_response() + return super(JsonRequestResponseMixin, self).dispatch( + request, *args, **kwargs) + + +class JSONRequestResponseMixin(JsonRequestResponseMixin): + pass + + +class OrderableListMixin(object): + """ + Mixin allows your users to order records using GET parameters + """ + + orderable_columns = None + orderable_columns_default = None + order_by = None + ordering = None + + def get_context_data(self, **kwargs): + """ + Augments context with: + + * ``order_by`` - name of the field + * ``ordering`` - order of ordering, either ``asc`` or ``desc`` + """ + context = super(OrderableListMixin, self).get_context_data(**kwargs) + context["order_by"] = self.order_by + context["ordering"] = self.ordering + return context + + def get_orderable_columns(self): + if not self.orderable_columns: + raise ImproperlyConfigured( + '{0} needs the ordering columns defined.'.format( + self.__class__.__name__)) + return self.orderable_columns + + def get_orderable_columns_default(self): + if not self.orderable_columns_default: + raise ImproperlyConfigured( + '{0} needs the default ordering column defined.'.format( + self.__class__.__name__)) + return self.orderable_columns_default + + def get_ordered_queryset(self, queryset=None): + """ + Augments ``QuerySet`` with order_by statement if possible + + :param QuerySet queryset: ``QuerySet`` to ``order_by`` + :return: QuerySet + """ + get_order_by = self.request.GET.get("order_by") + + if get_order_by in self.get_orderable_columns(): + order_by = get_order_by + else: + order_by = self.get_orderable_columns_default() + + self.order_by = order_by + self.ordering = "asc" + + if order_by and self.request.GET.get("ordering", "asc") == "desc": + order_by = "-" + order_by + self.ordering = "desc" + + return queryset.order_by(order_by) + + def get_queryset(self): + """ + Returns ordered ``QuerySet`` + """ + unordered_queryset = super(OrderableListMixin, self).get_queryset() + return self.get_ordered_queryset(unordered_queryset) + + +class CanonicalSlugDetailMixin(object): + """ + A mixin that enforces a canonical slug in the url. + + If a urlpattern takes a object's pk and slug as arguments and the slug url + argument does not equal the object's canonical slug, this mixin will + redirect to the url containing the canonical slug. + """ + def dispatch(self, request, *args, **kwargs): + # Set up since we need to super() later instead of earlier. + self.request = request + self.args = args + self.kwargs = kwargs + + # Get the current object, url slug, and + # urlpattern name (namespace aware). + obj = self.get_object() + slug = self.kwargs.get(self.slug_url_kwarg, None) + match = resolve(request.path_info) + url_parts = match.namespaces + url_parts.append(match.url_name) + current_urlpattern = ":".join(url_parts) + + # Figure out what the slug is supposed to be. + if hasattr(obj, "get_canonical_slug"): + canonical_slug = obj.get_canonical_slug() + else: + canonical_slug = self.get_canonical_slug() + + # If there's a discrepancy between the slug in the url and the + # canonical slug, redirect to the canonical slug. + if canonical_slug != slug: + params = {self.pk_url_kwarg: obj.pk, + self.slug_url_kwarg: canonical_slug, + 'permanent': True} + return redirect(current_urlpattern, **params) + + return super(CanonicalSlugDetailMixin, self).dispatch( + request, *args, **kwargs) + + def get_canonical_slug(self): + """ + Override this method to customize what slug should be considered + canonical. + + Alternatively, define the get_canonical_slug method on this view's + object class. In that case, this method will never be called. + """ + return self.get_object().slug + + +class _MessageAPIWrapper(object): + """ + Wrap the django.contrib.messages.api module to automatically pass a given + request object as the first parameter of 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, curry(api_fn, request)) + + +class _MessageDescriptor(object): + """ + A descriptor that binds the _MessageAPIWrapper to the view's + request. + """ + def __get__(self, instance, owner): + return _MessageAPIWrapper(instance.request) + + +class MessageMixin(object): + """ + Add a `messages` attribute on the view instance that wraps + `django.contrib .messages`, automatically passing the current + request object. + """ + messages = _MessageDescriptor() + + +class FormValidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_valid_message method. + """ + form_valid_message = None # Default to None + + def get_form_valid_message(self): + """ + Validate that form_valid_message is set and is either a + unicode or str object. + """ + if self.form_valid_message is None: + raise ImproperlyConfigured( + '{0}.form_valid_message is not set. Define ' + '{0}.form_valid_message, or override ' + '{0}.get_form_valid_message().'.format(self.__class__.__name__) + ) + + if not isinstance(self.form_valid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_valid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__) + ) + + return force_text(self.form_valid_message) + + def form_valid(self, form): + """ + Call the super first, so that when overriding + get_form_valid_message, we have access to the newly saved object. + """ + response = super(FormValidMessageMixin, self).form_valid(form) + self.messages.success(self.get_form_valid_message(), + fail_silently=True) + return response + + +class FormInvalidMessageMixin(MessageMixin): + """ + Mixin allows you to set static message which is displayed by + Django's messages framework through a static property on the class + or programmatically by overloading the get_form_invalid_message method. + """ + form_invalid_message = None + + def get_form_invalid_message(self): + """ + Validate that form_invalid_message is set and is either a + unicode or str object. + """ + if self.form_invalid_message is None: + raise ImproperlyConfigured( + '{0}.form_invalid_message is not set. Define ' + '{0}.form_invalid_message, or override ' + '{0}.get_form_invalid_message().'.format( + self.__class__.__name__)) + + if not isinstance(self.form_invalid_message, + (six.string_types, six.text_type, Promise)): + raise ImproperlyConfigured( + '{0}.form_invalid_message must be a str or unicode ' + 'object.'.format(self.__class__.__name__)) + + return force_text(self.form_invalid_message) + + def form_invalid(self, form): + response = super(FormInvalidMessageMixin, self).form_invalid(form) + self.messages.error(self.get_form_invalid_message(), + fail_silently=True) + return response + + +class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): + """ + Mixin is a shortcut to use both FormValidMessageMixin and + FormInvalidMessageMixin. + """ + pass + + +class AllVerbsMixin(object): + """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): + if not self.all_handler: + raise ImproperlyConfigured( + '{0} requires the all_handler attribute to be set.'.format( + self.__class__.__name__)) + + handler = getattr(self, self.all_handler, self.http_method_not_allowed) + return handler(request, *args, **kwargs) From d3b6fee4fcff42ffb57b14048050ee1aec6ed0bc Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 4 Mar 2014 14:05:02 -0800 Subject: [PATCH 3/9] queries --- braces/views/__init__.py | 9 + braces/views/_queries.py | 829 +-------------------------------------- 2 files changed, 10 insertions(+), 828 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index e69de29b..7895e3ce 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import + +from ._queries import ( + OrderableListMixin, + SelectRelatedMixin, + PrefetchRelatedMixin +) + +__all__ = ['OrderableListMixin', 'SelectRelatedMixin', 'PrefetchRelatedMixin'] diff --git a/braces/views/_queries.py b/braces/views/_queries.py index e79aedfa..811a383f 100644 --- a/braces/views/_queries.py +++ b/braces/views/_queries.py @@ -1,493 +1,4 @@ -import six - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login -from django.core import serializers -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) -from django.shortcuts import redirect -from django.utils.decorators import method_decorator -from django.utils.encoding import force_text -from django.utils.functional import curry, Promise -from django.views.decorators.csrf import csrf_exempt - -## Django 1.5+ compat -try: - import json -except ImportError: # pragma: no cover - from django.utils import simplejson as json - - -class AccessMixin(object): - """ - 'Abstract' mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - raise_exception = False # Default whether to raise an exception to none - redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth - - def get_login_url(self): - """ - Override this method to customize the login_url. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) - - return force_text(login_url) - - def get_redirect_field_name(self): - """ - Override this method to customize the redirect_field_name. - """ - if self.redirect_field_name is None: - raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) - return self.redirect_field_name - - -class LoginRequiredMixin(AccessMixin): - """ - View mixin which verifies that the user is authenticated. - - NOTE: - This should be the left-most mixin of a view, except when - combined with CsrfExemptMixin - which in that case should - be the left-most mixin. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - if self.raise_exception: - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class AnonymousRequiredMixin(object): - """ - View mixin which redirects to a specified URL if authenticated. - Can be useful if you wanted to prevent authenticated users from - accessing signup pages etc. - - 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): - if request.user.is_authenticated(): - return HttpResponseRedirect(self.get_authenticated_redirect_url()) - return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) - - def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ - if not self.authenticated_redirect_url: - raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) - return self.authenticated_redirect_url - - -class CsrfExemptMixin(object): - """ - Exempts the view from CSRF requirements. - - NOTE: - This should be the left-most mixin of a view. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) - - -class PermissionRequiredMixin(AccessMixin): - """ - View mixin which verifies that the logged in user has the specified - permission. - - Class Settings - `permission_required` - the permission to check for. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - - class SomeView(PermissionRequiredMixin, ListView): - ... - # required - permission_required = "app.permission" - - # optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - ... - """ - permission_required = None # Default required perms to none - - def 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( - '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) - - return self.permission_required - - def check_permissions(self, request): - """ - Returns whether or not the user has permissions - """ - perms = self.get_permission_required(request) - return request.user.has_perm(perms) - - def no_permissions_fail(self, request=None): - """ - Called when the user has no permissions. 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()) - - 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: # If the user lacks the permission - if self.raise_exception: - raise PermissionDenied # Return a 403 - return self.no_permissions_fail(request) - - return super(PermissionRequiredMixin, self).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 passed in list. - - By specifying The `any` key , the user must have ONE of the set - permissions in the list. - - Class Settings - `permissions` - This is required to be a dict with one or both - keys of `all` and/or `any` containing a list or tuple of - permissions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - class SomeView(MultiplePermissionsRequiredMixin, ListView): - ... - #required - permissions = { - "all": ("blog.add_post", "blog.change_post"), - "any": ("blog.delete_post", "user.change_user") - } - - #optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - """ - permissions = None # Default required perms to none - - def get_permission_required(self, request=None): - self._check_permissions_attr() - return self.permissions - - def check_permissions(self, request): - permissions = self.get_permission_required() - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None - - self._check_permissions_keys_set(perms_all, perms_any) - self._check_perms_keys("all", perms_all) - self._check_perms_keys("any", perms_any) - - # If perms_all, check that user has all permissions in the list/tuple - if perms_all: - if not request.user.has_perms(perms_all): - return False - - # If perms_any, check that user has at least one in the list/tuple - if perms_any: - has_one_perm = False - for perm in perms_any: - if request.user.has_perm(perm): - has_one_perm = True - break - - if not has_one_perm: - 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( - '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) - - 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( - '{0} requires the "permissions" attribute to be set to a ' - 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) - - 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( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) - - -class GroupRequiredMixin(AccessMixin): - group_required = None - - def get_group_required(self): - if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple) + six.string_types) - ): - - raise ImproperlyConfigured( - '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) - if not isinstance(self.group_required, (list, tuple)): - self.group_required = (self.group_required,) - return self.group_required - - def check_membership(self, groups): - """ Check required group(s) """ - 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): - self.request = request - in_group = False - if self.request.user.is_authenticated(): - in_group = self.check_membership(self.get_group_required()) - - if not in_group: - if self.raise_exception: - raise PermissionDenied - else: - return redirect_to_login( - request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin allows you to define test that every user should pass - to get access into view. - - Class Settings - `test_func` - This is required to be a method that takes user - instance and return True or False after checking conditions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - """ - - def test_func(self, user): - raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) - - def get_test_func(self): - return getattr(self, "test_func") - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()(request.user) - - if not user_test_result: # If user don't pass the test - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) - - -class UserFormKwargsMixin(object): - """ - CBV mixin which puts the user from the request into the form kwargs. - Note: Using this mixin requires you to pop the `user` kwarg - out of the dict in the super of your form's `__init__`. - """ - def get_form_kwargs(self): - kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() - # Update the existing form kwargs dict with the request's user. - kwargs.update({"user": self.request.user}) - return kwargs - - -class SuccessURLRedirectListMixin(object): - """ - Simple CBV mixin which sets the success url to the list view of - a given app. Set success_list_url as a class attribute of your - CBV and don't worry about overloading the get_success_url. - - This is only to be used for redirecting to a list page. If you need - to reverse the url with kwargs, this is not the mixin to use. - """ - success_list_url = None # Default the success url to none - - def get_success_url(self): - # Return the reversed success url. - if self.success_list_url is None: - raise ImproperlyConfigured( - '{0} is missing a succes_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) - return reverse(self.success_list_url) - - -class SuperuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_superuser` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_superuser: # If the user is a standard user, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response. - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class SetHeadlineMixin(object): - """ - Mixin allows you to set a static headline through a static property on the - class or programmatically by overloading the get_headline method. - """ - headline = None # Default the headline to none - - def get_context_data(self, **kwargs): - kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) - # Update the existing context dict with the provided headline. - kwargs.update({"headline": self.get_headline()}) - return kwargs - - def get_headline(self): - if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. - raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) - return force_text(self.headline) - - -class StaticContextMixin(object): - """ - Mixin allows you to set static context through a static property on - the class. - """ - static_context = None - - def get_context_data(self, **kwargs): - kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) - - try: - kwargs.update(self.get_static_context()) - except (TypeError, ValueError): - raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) - else: - return kwargs - - def get_static_context(self): - if self.static_context is None: - raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) - ) - return self.static_context +from django.core.exceptions import ImproperlyConfigured class SelectRelatedMixin(object): @@ -544,157 +55,6 @@ def get_queryset(self): return queryset.prefetch_related(*self.prefetch_related) -class StaffuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_staff` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: # If the request's user is not staff, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONResponseMixin(object): - """ - A mixin that allows you to easily serialize simple data such as a dict or - Django models. - """ - content_type = None - json_dumps_kwargs = None - - def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - (six.string_types, six.text_type))): - raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) - return self.content_type or u"application/json" - - def get_json_dumps_kwargs(self): - if self.json_dumps_kwargs is None: - self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) - return self.json_dumps_kwargs - - def render_json_response(self, context_dict, status=200): - """ - Limited serialization for shipping plain data. Do not use for models - or other complex or custom objects. - """ - json_context = json.dumps( - context_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs()).encode(u'utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) - - def render_json_object_response(self, objects, **kwargs): - """ - Serializes objects using Django's builtin JSON serializer. Additional - kwargs can be used the same way for django.core.serializers.serialize. - """ - json_data = serializers.serialize(u"json", objects, **kwargs) - return HttpResponse(json_data, content_type=self.get_content_type()) - - -class AjaxResponseMixin(object): - """ - Mixin allows you to define alternative methods for ajax requests. Similar - to the normal get, post, and put methods, you can use get_ajax, post_ajax, - and put_ajax. - """ - def dispatch(self, request, *args, **kwargs): - request_method = request.method.lower() - - if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, u"{0}_ajax".format(request_method), - self.http_method_not_allowed) - self.request = request - self.args = args - self.kwargs = kwargs - return handler(request, *args, **kwargs) - - return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) - - def get_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post_ajax(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - def put_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - -class JsonRequestResponseMixin(JSONResponseMixin): - """ - Extends JSONResponseMixin. Attempts to parse request as JSON. If request - is properly formatted, the json is saved to self.request_json as a Python - object. request_json will be None for imparsible requests. - Set the attribute require_json to True to return a 400 "Bad Request" error - for requests that don't contain JSON. - - Note: To allow public access to your view, you'll need to use the - csrf_exempt decorator or CsrfExemptMixin. - - Example Usage: - - class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): - def post(self, request, *args, **kwargs): - do_stuff_with_contents_of_request_json() - return self.render_json_response( - {'message': 'Thanks!'}) - """ - require_json = False - error_response_dict = {u"errors": [u"Improperly formatted request"]} - - def render_bad_request_response(self, error_dict=None): - if error_dict is None: - error_dict = self.error_response_dict - json_context = json.dumps( - error_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs() - ).encode(u'utf-8') - return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) - - def get_request_json(self): - try: - return json.loads(self.request.body.decode(u'utf-8')) - except ValueError: - return None - - def dispatch(self, request, *args, **kwargs): - self.request = request - self.args = args - self.kwargs = kwargs - - self.request_json = self.get_request_json() - if self.require_json and self.request_json is None: - return self.render_bad_request_response() - return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONRequestResponseMixin(JsonRequestResponseMixin): - pass - - class OrderableListMixin(object): """ Mixin allows your users to order records using GET parameters @@ -760,190 +120,3 @@ def get_queryset(self): """ unordered_queryset = super(OrderableListMixin, self).get_queryset() return self.get_ordered_queryset(unordered_queryset) - - -class CanonicalSlugDetailMixin(object): - """ - A mixin that enforces a canonical slug in the url. - - If a urlpattern takes a object's pk and slug as arguments and the slug url - argument does not equal the object's canonical slug, this mixin will - redirect to the url containing the canonical slug. - """ - def dispatch(self, request, *args, **kwargs): - # Set up since we need to super() later instead of earlier. - self.request = request - self.args = args - self.kwargs = kwargs - - # Get the current object, url slug, and - # urlpattern name (namespace aware). - obj = self.get_object() - slug = self.kwargs.get(self.slug_url_kwarg, None) - match = resolve(request.path_info) - url_parts = match.namespaces - url_parts.append(match.url_name) - current_urlpattern = ":".join(url_parts) - - # Figure out what the slug is supposed to be. - if hasattr(obj, "get_canonical_slug"): - canonical_slug = obj.get_canonical_slug() - else: - canonical_slug = self.get_canonical_slug() - - # If there's a discrepancy between the slug in the url and the - # canonical slug, redirect to the canonical slug. - if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} - return redirect(current_urlpattern, **params) - - return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) - - def get_canonical_slug(self): - """ - Override this method to customize what slug should be considered - canonical. - - Alternatively, define the get_canonical_slug method on this view's - object class. In that case, this method will never be called. - """ - return self.get_object().slug - - -class _MessageAPIWrapper(object): - """ - Wrap the django.contrib.messages.api module to automatically pass a given - request object as the first parameter of 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, curry(api_fn, request)) - - -class _MessageDescriptor(object): - """ - A descriptor that binds the _MessageAPIWrapper to the view's - request. - """ - def __get__(self, instance, owner): - return _MessageAPIWrapper(instance.request) - - -class MessageMixin(object): - """ - Add a `messages` attribute on the view instance that wraps - `django.contrib .messages`, automatically passing the current - request object. - """ - messages = _MessageDescriptor() - - -class FormValidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_valid_message method. - """ - form_valid_message = None # Default to None - - def get_form_valid_message(self): - """ - Validate that form_valid_message is set and is either a - unicode or str object. - """ - if self.form_valid_message is None: - raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) - ) - - if not isinstance(self.form_valid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) - ) - - return force_text(self.form_valid_message) - - def form_valid(self, form): - """ - Call the super first, so that when overriding - get_form_valid_message, we have access to the newly saved object. - """ - response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) - return response - - -class FormInvalidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_invalid_message method. - """ - form_invalid_message = None - - def get_form_invalid_message(self): - """ - Validate that form_invalid_message is set and is either a - unicode or str object. - """ - if self.form_invalid_message is None: - raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) - - if not isinstance(self.form_invalid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) - - return force_text(self.form_invalid_message) - - def form_invalid(self, form): - response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) - return response - - -class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): - """ - Mixin is a shortcut to use both FormValidMessageMixin and - FormInvalidMessageMixin. - """ - pass - - -class AllVerbsMixin(object): - """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): - if not self.all_handler: - raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) - - handler = getattr(self, self.all_handler, self.http_method_not_allowed) - return handler(request, *args, **kwargs) From 5adf1b9fc43d78414de7e27a2d5aed66af793941 Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 4 Mar 2014 14:08:13 -0800 Subject: [PATCH 4/9] ajax --- braces/views/__init__.py | 16 +- braces/views/_ajax.py | 807 +-------------------------------------- 2 files changed, 17 insertions(+), 806 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index 7895e3ce..aee09c29 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -1,9 +1,23 @@ from __future__ import absolute_import +from ._ajax import ( + JSONResponseMixin, + AjaxResponseMixin, + JsonRequestResponseMixin, + JSONRequestResponseMixin +) from ._queries import ( OrderableListMixin, SelectRelatedMixin, PrefetchRelatedMixin ) -__all__ = ['OrderableListMixin', 'SelectRelatedMixin', 'PrefetchRelatedMixin'] +__all__ = [ + 'OrderableListMixin', + 'SelectRelatedMixin', + 'PrefetchRelatedMixin', + 'JSONResponseMixin', + 'AjaxResponseMixin', + 'JsonRequestResponseMixin', + 'JSONRequestResponseMixin', +] diff --git a/braces/views/_ajax.py b/braces/views/_ajax.py index e79aedfa..7044a5cc 100644 --- a/braces/views/_ajax.py +++ b/braces/views/_ajax.py @@ -1,20 +1,9 @@ import six -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login from django.core import serializers -from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) -from django.shortcuts import redirect -from django.utils.decorators import method_decorator -from django.utils.encoding import force_text -from django.utils.functional import curry, Promise -from django.views.decorators.csrf import csrf_exempt +from django.http import HttpResponse, HttpResponseBadRequest ## Django 1.5+ compat try: @@ -23,544 +12,6 @@ from django.utils import simplejson as json -class AccessMixin(object): - """ - 'Abstract' mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - raise_exception = False # Default whether to raise an exception to none - redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth - - def get_login_url(self): - """ - Override this method to customize the login_url. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) - - return force_text(login_url) - - def get_redirect_field_name(self): - """ - Override this method to customize the redirect_field_name. - """ - if self.redirect_field_name is None: - raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) - return self.redirect_field_name - - -class LoginRequiredMixin(AccessMixin): - """ - View mixin which verifies that the user is authenticated. - - NOTE: - This should be the left-most mixin of a view, except when - combined with CsrfExemptMixin - which in that case should - be the left-most mixin. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - if self.raise_exception: - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class AnonymousRequiredMixin(object): - """ - View mixin which redirects to a specified URL if authenticated. - Can be useful if you wanted to prevent authenticated users from - accessing signup pages etc. - - 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): - if request.user.is_authenticated(): - return HttpResponseRedirect(self.get_authenticated_redirect_url()) - return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) - - def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ - if not self.authenticated_redirect_url: - raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) - return self.authenticated_redirect_url - - -class CsrfExemptMixin(object): - """ - Exempts the view from CSRF requirements. - - NOTE: - This should be the left-most mixin of a view. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) - - -class PermissionRequiredMixin(AccessMixin): - """ - View mixin which verifies that the logged in user has the specified - permission. - - Class Settings - `permission_required` - the permission to check for. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - - class SomeView(PermissionRequiredMixin, ListView): - ... - # required - permission_required = "app.permission" - - # optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - ... - """ - permission_required = None # Default required perms to none - - def 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( - '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) - - return self.permission_required - - def check_permissions(self, request): - """ - Returns whether or not the user has permissions - """ - perms = self.get_permission_required(request) - return request.user.has_perm(perms) - - def no_permissions_fail(self, request=None): - """ - Called when the user has no permissions. 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()) - - 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: # If the user lacks the permission - if self.raise_exception: - raise PermissionDenied # Return a 403 - return self.no_permissions_fail(request) - - return super(PermissionRequiredMixin, self).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 passed in list. - - By specifying The `any` key , the user must have ONE of the set - permissions in the list. - - Class Settings - `permissions` - This is required to be a dict with one or both - keys of `all` and/or `any` containing a list or tuple of - permissions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - class SomeView(MultiplePermissionsRequiredMixin, ListView): - ... - #required - permissions = { - "all": ("blog.add_post", "blog.change_post"), - "any": ("blog.delete_post", "user.change_user") - } - - #optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - """ - permissions = None # Default required perms to none - - def get_permission_required(self, request=None): - self._check_permissions_attr() - return self.permissions - - def check_permissions(self, request): - permissions = self.get_permission_required() - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None - - self._check_permissions_keys_set(perms_all, perms_any) - self._check_perms_keys("all", perms_all) - self._check_perms_keys("any", perms_any) - - # If perms_all, check that user has all permissions in the list/tuple - if perms_all: - if not request.user.has_perms(perms_all): - return False - - # If perms_any, check that user has at least one in the list/tuple - if perms_any: - has_one_perm = False - for perm in perms_any: - if request.user.has_perm(perm): - has_one_perm = True - break - - if not has_one_perm: - 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( - '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) - - 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( - '{0} requires the "permissions" attribute to be set to a ' - 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) - - 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( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) - - -class GroupRequiredMixin(AccessMixin): - group_required = None - - def get_group_required(self): - if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple) + six.string_types) - ): - - raise ImproperlyConfigured( - '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) - if not isinstance(self.group_required, (list, tuple)): - self.group_required = (self.group_required,) - return self.group_required - - def check_membership(self, groups): - """ Check required group(s) """ - 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): - self.request = request - in_group = False - if self.request.user.is_authenticated(): - in_group = self.check_membership(self.get_group_required()) - - if not in_group: - if self.raise_exception: - raise PermissionDenied - else: - return redirect_to_login( - request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin allows you to define test that every user should pass - to get access into view. - - Class Settings - `test_func` - This is required to be a method that takes user - instance and return True or False after checking conditions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - """ - - def test_func(self, user): - raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) - - def get_test_func(self): - return getattr(self, "test_func") - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()(request.user) - - if not user_test_result: # If user don't pass the test - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) - - -class UserFormKwargsMixin(object): - """ - CBV mixin which puts the user from the request into the form kwargs. - Note: Using this mixin requires you to pop the `user` kwarg - out of the dict in the super of your form's `__init__`. - """ - def get_form_kwargs(self): - kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() - # Update the existing form kwargs dict with the request's user. - kwargs.update({"user": self.request.user}) - return kwargs - - -class SuccessURLRedirectListMixin(object): - """ - Simple CBV mixin which sets the success url to the list view of - a given app. Set success_list_url as a class attribute of your - CBV and don't worry about overloading the get_success_url. - - This is only to be used for redirecting to a list page. If you need - to reverse the url with kwargs, this is not the mixin to use. - """ - success_list_url = None # Default the success url to none - - def get_success_url(self): - # Return the reversed success url. - if self.success_list_url is None: - raise ImproperlyConfigured( - '{0} is missing a succes_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) - return reverse(self.success_list_url) - - -class SuperuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_superuser` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_superuser: # If the user is a standard user, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response. - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class SetHeadlineMixin(object): - """ - Mixin allows you to set a static headline through a static property on the - class or programmatically by overloading the get_headline method. - """ - headline = None # Default the headline to none - - def get_context_data(self, **kwargs): - kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) - # Update the existing context dict with the provided headline. - kwargs.update({"headline": self.get_headline()}) - return kwargs - - def get_headline(self): - if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. - raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) - return force_text(self.headline) - - -class StaticContextMixin(object): - """ - Mixin allows you to set static context through a static property on - the class. - """ - static_context = None - - def get_context_data(self, **kwargs): - kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) - - try: - kwargs.update(self.get_static_context()) - except (TypeError, ValueError): - raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) - else: - return kwargs - - def get_static_context(self): - if self.static_context is None: - raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) - ) - return self.static_context - - -class SelectRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a select_related on. - """ - select_related = None # Default related fields to none - - def get_queryset(self): - if self.select_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - if not isinstance(self.select_related, (tuple, list)): - # If the select_related argument is *not* a tuple or list, - # raise a configuration error. - raise ImproperlyConfigured( - "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(SelectRelatedMixin, self).get_queryset() - - return queryset.select_related(*self.select_related) - - -class PrefetchRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a prefetch_related on. - """ - prefetch_related = None # Default prefetch fields to none - - def get_queryset(self): - if self.prefetch_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - 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( - "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(PrefetchRelatedMixin, self).get_queryset() - - return queryset.prefetch_related(*self.prefetch_related) - - -class StaffuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_staff` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: # If the request's user is not staff, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - class JSONResponseMixin(object): """ A mixin that allows you to easily serialize simple data such as a dict or @@ -693,257 +144,3 @@ def dispatch(self, request, *args, **kwargs): class JSONRequestResponseMixin(JsonRequestResponseMixin): pass - - -class OrderableListMixin(object): - """ - Mixin allows your users to order records using GET parameters - """ - - orderable_columns = None - orderable_columns_default = None - order_by = None - ordering = None - - def get_context_data(self, **kwargs): - """ - Augments context with: - - * ``order_by`` - name of the field - * ``ordering`` - order of ordering, either ``asc`` or ``desc`` - """ - context = super(OrderableListMixin, self).get_context_data(**kwargs) - context["order_by"] = self.order_by - context["ordering"] = self.ordering - return context - - def get_orderable_columns(self): - if not self.orderable_columns: - raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) - return self.orderable_columns - - def get_orderable_columns_default(self): - if not self.orderable_columns_default: - raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) - return self.orderable_columns_default - - def get_ordered_queryset(self, queryset=None): - """ - Augments ``QuerySet`` with order_by statement if possible - - :param QuerySet queryset: ``QuerySet`` to ``order_by`` - :return: QuerySet - """ - get_order_by = self.request.GET.get("order_by") - - if get_order_by in self.get_orderable_columns(): - order_by = get_order_by - else: - order_by = self.get_orderable_columns_default() - - self.order_by = order_by - self.ordering = "asc" - - if order_by and self.request.GET.get("ordering", "asc") == "desc": - order_by = "-" + order_by - self.ordering = "desc" - - return queryset.order_by(order_by) - - def get_queryset(self): - """ - Returns ordered ``QuerySet`` - """ - unordered_queryset = super(OrderableListMixin, self).get_queryset() - return self.get_ordered_queryset(unordered_queryset) - - -class CanonicalSlugDetailMixin(object): - """ - A mixin that enforces a canonical slug in the url. - - If a urlpattern takes a object's pk and slug as arguments and the slug url - argument does not equal the object's canonical slug, this mixin will - redirect to the url containing the canonical slug. - """ - def dispatch(self, request, *args, **kwargs): - # Set up since we need to super() later instead of earlier. - self.request = request - self.args = args - self.kwargs = kwargs - - # Get the current object, url slug, and - # urlpattern name (namespace aware). - obj = self.get_object() - slug = self.kwargs.get(self.slug_url_kwarg, None) - match = resolve(request.path_info) - url_parts = match.namespaces - url_parts.append(match.url_name) - current_urlpattern = ":".join(url_parts) - - # Figure out what the slug is supposed to be. - if hasattr(obj, "get_canonical_slug"): - canonical_slug = obj.get_canonical_slug() - else: - canonical_slug = self.get_canonical_slug() - - # If there's a discrepancy between the slug in the url and the - # canonical slug, redirect to the canonical slug. - if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} - return redirect(current_urlpattern, **params) - - return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) - - def get_canonical_slug(self): - """ - Override this method to customize what slug should be considered - canonical. - - Alternatively, define the get_canonical_slug method on this view's - object class. In that case, this method will never be called. - """ - return self.get_object().slug - - -class _MessageAPIWrapper(object): - """ - Wrap the django.contrib.messages.api module to automatically pass a given - request object as the first parameter of 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, curry(api_fn, request)) - - -class _MessageDescriptor(object): - """ - A descriptor that binds the _MessageAPIWrapper to the view's - request. - """ - def __get__(self, instance, owner): - return _MessageAPIWrapper(instance.request) - - -class MessageMixin(object): - """ - Add a `messages` attribute on the view instance that wraps - `django.contrib .messages`, automatically passing the current - request object. - """ - messages = _MessageDescriptor() - - -class FormValidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_valid_message method. - """ - form_valid_message = None # Default to None - - def get_form_valid_message(self): - """ - Validate that form_valid_message is set and is either a - unicode or str object. - """ - if self.form_valid_message is None: - raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) - ) - - if not isinstance(self.form_valid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) - ) - - return force_text(self.form_valid_message) - - def form_valid(self, form): - """ - Call the super first, so that when overriding - get_form_valid_message, we have access to the newly saved object. - """ - response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) - return response - - -class FormInvalidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_invalid_message method. - """ - form_invalid_message = None - - def get_form_invalid_message(self): - """ - Validate that form_invalid_message is set and is either a - unicode or str object. - """ - if self.form_invalid_message is None: - raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) - - if not isinstance(self.form_invalid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) - - return force_text(self.form_invalid_message) - - def form_invalid(self, form): - response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) - return response - - -class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): - """ - Mixin is a shortcut to use both FormValidMessageMixin and - FormInvalidMessageMixin. - """ - pass - - -class AllVerbsMixin(object): - """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): - if not self.all_handler: - raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) - - handler = getattr(self, self.all_handler, self.http_method_not_allowed) - return handler(request, *args, **kwargs) From d7f5822525aeb1008efb7727b142238455afabd9 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 4 Mar 2014 14:11:00 -0800 Subject: [PATCH 5/9] access mixins. --- braces/views/__init__.py | 19 ++ braces/views/_access.py | 560 +-------------------------------------- 2 files changed, 20 insertions(+), 559 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index aee09c29..eecc2ef9 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -1,5 +1,16 @@ from __future__ import absolute_import +from ._access import ( + LoginRequiredMixin, + AnonymousRequiredMixin, + PermissionRequiredMixin, + MultiplePermissionsRequiredMixin, + GroupRequiredMixin, + UserPassesTestMixin, + SuperuserRequiredMixin, + StaffuserRequiredMixin +) + from ._ajax import ( JSONResponseMixin, AjaxResponseMixin, @@ -13,6 +24,14 @@ ) __all__ = [ + 'LoginRequiredMixin', + 'AnonymousRequiredMixin', + 'PermissionRequiredMixin', + 'MultiplePermissionsRequiredMixin', + 'GroupRequiredMixin', + 'UserPassesTestMixin', + 'SuperuserRequiredMixin', + 'StaffuserRequiredMixin', 'OrderableListMixin', 'SelectRelatedMixin', 'PrefetchRelatedMixin', diff --git a/braces/views/_access.py b/braces/views/_access.py index e79aedfa..3f705336 100644 --- a/braces/views/_access.py +++ b/braces/views/_access.py @@ -1,26 +1,11 @@ import six from django.conf import settings -from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login -from django.core import serializers from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) -from django.shortcuts import redirect -from django.utils.decorators import method_decorator +from django.http import HttpResponseRedirect from django.utils.encoding import force_text -from django.utils.functional import curry, Promise -from django.views.decorators.csrf import csrf_exempt - -## Django 1.5+ compat -try: - import json -except ImportError: # pragma: no cover - from django.utils import simplejson as json class AccessMixin(object): @@ -116,19 +101,6 @@ def get_authenticated_redirect_url(self): return self.authenticated_redirect_url -class CsrfExemptMixin(object): - """ - Exempts the view from CSRF requirements. - - NOTE: - This should be the left-most mixin of a view. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) - - class PermissionRequiredMixin(AccessMixin): """ View mixin which verifies that the logged in user has the specified @@ -385,41 +357,6 @@ def dispatch(self, request, *args, **kwargs): request, *args, **kwargs) -class UserFormKwargsMixin(object): - """ - CBV mixin which puts the user from the request into the form kwargs. - Note: Using this mixin requires you to pop the `user` kwarg - out of the dict in the super of your form's `__init__`. - """ - def get_form_kwargs(self): - kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() - # Update the existing form kwargs dict with the request's user. - kwargs.update({"user": self.request.user}) - return kwargs - - -class SuccessURLRedirectListMixin(object): - """ - Simple CBV mixin which sets the success url to the list view of - a given app. Set success_list_url as a class attribute of your - CBV and don't worry about overloading the get_success_url. - - This is only to be used for redirecting to a list page. If you need - to reverse the url with kwargs, this is not the mixin to use. - """ - success_list_url = None # Default the success url to none - - def get_success_url(self): - # Return the reversed success url. - if self.success_list_url is None: - raise ImproperlyConfigured( - '{0} is missing a succes_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) - return reverse(self.success_list_url) - - class SuperuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_superuser` set to True. @@ -437,113 +374,6 @@ def dispatch(self, request, *args, **kwargs): request, *args, **kwargs) -class SetHeadlineMixin(object): - """ - Mixin allows you to set a static headline through a static property on the - class or programmatically by overloading the get_headline method. - """ - headline = None # Default the headline to none - - def get_context_data(self, **kwargs): - kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) - # Update the existing context dict with the provided headline. - kwargs.update({"headline": self.get_headline()}) - return kwargs - - def get_headline(self): - if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. - raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) - return force_text(self.headline) - - -class StaticContextMixin(object): - """ - Mixin allows you to set static context through a static property on - the class. - """ - static_context = None - - def get_context_data(self, **kwargs): - kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) - - try: - kwargs.update(self.get_static_context()) - except (TypeError, ValueError): - raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) - else: - return kwargs - - def get_static_context(self): - if self.static_context is None: - raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) - ) - return self.static_context - - -class SelectRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a select_related on. - """ - select_related = None # Default related fields to none - - def get_queryset(self): - if self.select_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - if not isinstance(self.select_related, (tuple, list)): - # If the select_related argument is *not* a tuple or list, - # raise a configuration error. - raise ImproperlyConfigured( - "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(SelectRelatedMixin, self).get_queryset() - - return queryset.select_related(*self.select_related) - - -class PrefetchRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a prefetch_related on. - """ - prefetch_related = None # Default prefetch fields to none - - def get_queryset(self): - if self.prefetch_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - 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( - "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(PrefetchRelatedMixin, self).get_queryset() - - return queryset.prefetch_related(*self.prefetch_related) - - class StaffuserRequiredMixin(AccessMixin): """ Mixin allows you to require a user with `is_staff` set to True. @@ -559,391 +389,3 @@ def dispatch(self, request, *args, **kwargs): return super(StaffuserRequiredMixin, self).dispatch( request, *args, **kwargs) - - -class JSONResponseMixin(object): - """ - A mixin that allows you to easily serialize simple data such as a dict or - Django models. - """ - content_type = None - json_dumps_kwargs = None - - def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - (six.string_types, six.text_type))): - raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) - return self.content_type or u"application/json" - - def get_json_dumps_kwargs(self): - if self.json_dumps_kwargs is None: - self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) - return self.json_dumps_kwargs - - def render_json_response(self, context_dict, status=200): - """ - Limited serialization for shipping plain data. Do not use for models - or other complex or custom objects. - """ - json_context = json.dumps( - context_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs()).encode(u'utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) - - def render_json_object_response(self, objects, **kwargs): - """ - Serializes objects using Django's builtin JSON serializer. Additional - kwargs can be used the same way for django.core.serializers.serialize. - """ - json_data = serializers.serialize(u"json", objects, **kwargs) - return HttpResponse(json_data, content_type=self.get_content_type()) - - -class AjaxResponseMixin(object): - """ - Mixin allows you to define alternative methods for ajax requests. Similar - to the normal get, post, and put methods, you can use get_ajax, post_ajax, - and put_ajax. - """ - def dispatch(self, request, *args, **kwargs): - request_method = request.method.lower() - - if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, u"{0}_ajax".format(request_method), - self.http_method_not_allowed) - self.request = request - self.args = args - self.kwargs = kwargs - return handler(request, *args, **kwargs) - - return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) - - def get_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post_ajax(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - def put_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - -class JsonRequestResponseMixin(JSONResponseMixin): - """ - Extends JSONResponseMixin. Attempts to parse request as JSON. If request - is properly formatted, the json is saved to self.request_json as a Python - object. request_json will be None for imparsible requests. - Set the attribute require_json to True to return a 400 "Bad Request" error - for requests that don't contain JSON. - - Note: To allow public access to your view, you'll need to use the - csrf_exempt decorator or CsrfExemptMixin. - - Example Usage: - - class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): - def post(self, request, *args, **kwargs): - do_stuff_with_contents_of_request_json() - return self.render_json_response( - {'message': 'Thanks!'}) - """ - require_json = False - error_response_dict = {u"errors": [u"Improperly formatted request"]} - - def render_bad_request_response(self, error_dict=None): - if error_dict is None: - error_dict = self.error_response_dict - json_context = json.dumps( - error_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs() - ).encode(u'utf-8') - return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) - - def get_request_json(self): - try: - return json.loads(self.request.body.decode(u'utf-8')) - except ValueError: - return None - - def dispatch(self, request, *args, **kwargs): - self.request = request - self.args = args - self.kwargs = kwargs - - self.request_json = self.get_request_json() - if self.require_json and self.request_json is None: - return self.render_bad_request_response() - return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONRequestResponseMixin(JsonRequestResponseMixin): - pass - - -class OrderableListMixin(object): - """ - Mixin allows your users to order records using GET parameters - """ - - orderable_columns = None - orderable_columns_default = None - order_by = None - ordering = None - - def get_context_data(self, **kwargs): - """ - Augments context with: - - * ``order_by`` - name of the field - * ``ordering`` - order of ordering, either ``asc`` or ``desc`` - """ - context = super(OrderableListMixin, self).get_context_data(**kwargs) - context["order_by"] = self.order_by - context["ordering"] = self.ordering - return context - - def get_orderable_columns(self): - if not self.orderable_columns: - raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) - return self.orderable_columns - - def get_orderable_columns_default(self): - if not self.orderable_columns_default: - raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) - return self.orderable_columns_default - - def get_ordered_queryset(self, queryset=None): - """ - Augments ``QuerySet`` with order_by statement if possible - - :param QuerySet queryset: ``QuerySet`` to ``order_by`` - :return: QuerySet - """ - get_order_by = self.request.GET.get("order_by") - - if get_order_by in self.get_orderable_columns(): - order_by = get_order_by - else: - order_by = self.get_orderable_columns_default() - - self.order_by = order_by - self.ordering = "asc" - - if order_by and self.request.GET.get("ordering", "asc") == "desc": - order_by = "-" + order_by - self.ordering = "desc" - - return queryset.order_by(order_by) - - def get_queryset(self): - """ - Returns ordered ``QuerySet`` - """ - unordered_queryset = super(OrderableListMixin, self).get_queryset() - return self.get_ordered_queryset(unordered_queryset) - - -class CanonicalSlugDetailMixin(object): - """ - A mixin that enforces a canonical slug in the url. - - If a urlpattern takes a object's pk and slug as arguments and the slug url - argument does not equal the object's canonical slug, this mixin will - redirect to the url containing the canonical slug. - """ - def dispatch(self, request, *args, **kwargs): - # Set up since we need to super() later instead of earlier. - self.request = request - self.args = args - self.kwargs = kwargs - - # Get the current object, url slug, and - # urlpattern name (namespace aware). - obj = self.get_object() - slug = self.kwargs.get(self.slug_url_kwarg, None) - match = resolve(request.path_info) - url_parts = match.namespaces - url_parts.append(match.url_name) - current_urlpattern = ":".join(url_parts) - - # Figure out what the slug is supposed to be. - if hasattr(obj, "get_canonical_slug"): - canonical_slug = obj.get_canonical_slug() - else: - canonical_slug = self.get_canonical_slug() - - # If there's a discrepancy between the slug in the url and the - # canonical slug, redirect to the canonical slug. - if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} - return redirect(current_urlpattern, **params) - - return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) - - def get_canonical_slug(self): - """ - Override this method to customize what slug should be considered - canonical. - - Alternatively, define the get_canonical_slug method on this view's - object class. In that case, this method will never be called. - """ - return self.get_object().slug - - -class _MessageAPIWrapper(object): - """ - Wrap the django.contrib.messages.api module to automatically pass a given - request object as the first parameter of 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, curry(api_fn, request)) - - -class _MessageDescriptor(object): - """ - A descriptor that binds the _MessageAPIWrapper to the view's - request. - """ - def __get__(self, instance, owner): - return _MessageAPIWrapper(instance.request) - - -class MessageMixin(object): - """ - Add a `messages` attribute on the view instance that wraps - `django.contrib .messages`, automatically passing the current - request object. - """ - messages = _MessageDescriptor() - - -class FormValidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_valid_message method. - """ - form_valid_message = None # Default to None - - def get_form_valid_message(self): - """ - Validate that form_valid_message is set and is either a - unicode or str object. - """ - if self.form_valid_message is None: - raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) - ) - - if not isinstance(self.form_valid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) - ) - - return force_text(self.form_valid_message) - - def form_valid(self, form): - """ - Call the super first, so that when overriding - get_form_valid_message, we have access to the newly saved object. - """ - response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) - return response - - -class FormInvalidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_invalid_message method. - """ - form_invalid_message = None - - def get_form_invalid_message(self): - """ - Validate that form_invalid_message is set and is either a - unicode or str object. - """ - if self.form_invalid_message is None: - raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) - - if not isinstance(self.form_invalid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) - - return force_text(self.form_invalid_message) - - def form_invalid(self, form): - response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) - return response - - -class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): - """ - Mixin is a shortcut to use both FormValidMessageMixin and - FormInvalidMessageMixin. - """ - pass - - -class AllVerbsMixin(object): - """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): - if not self.all_handler: - raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) - - handler = getattr(self, self.all_handler, self.http_method_not_allowed) - return handler(request, *args, **kwargs) From 1e2e65b29708968865b1f44391e49ae1155dcde7 Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 4 Mar 2014 14:12:25 -0800 Subject: [PATCH 6/9] forms --- braces/views/__init__.py | 16 + braces/views/_forms.py | 778 +-------------------------------------- 2 files changed, 18 insertions(+), 776 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index eecc2ef9..c07f69ac 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -17,6 +17,15 @@ JsonRequestResponseMixin, JSONRequestResponseMixin ) +from ._forms import ( + CsrfExemptMixin, + UserFormKwargsMixin, + SuccessURLRedirectListMixin, + MessageMixin, + FormValidMessageMixin, + FormInvalidMessageMixin, + FormMessagesMixin +) from ._queries import ( OrderableListMixin, SelectRelatedMixin, @@ -39,4 +48,11 @@ 'AjaxResponseMixin', 'JsonRequestResponseMixin', 'JSONRequestResponseMixin', + 'CsrfExemptMixin', + 'UserFormKwargsMixin', + 'SuccessURLRedirectListMixin', + 'MessageMixin', + 'FormValidMessageMixin', + 'FormInvalidMessageMixin', + 'FormMessagesMixin', ] diff --git a/braces/views/_forms.py b/braces/views/_forms.py index e79aedfa..3131b3a6 100644 --- a/braces/views/_forms.py +++ b/braces/views/_forms.py @@ -1,120 +1,13 @@ import six -from django.conf import settings from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login -from django.core import serializers -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) -from django.shortcuts import redirect +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse from django.utils.decorators import method_decorator from django.utils.encoding import force_text from django.utils.functional import curry, Promise from django.views.decorators.csrf import csrf_exempt -## Django 1.5+ compat -try: - import json -except ImportError: # pragma: no cover - from django.utils import simplejson as json - - -class AccessMixin(object): - """ - 'Abstract' mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - raise_exception = False # Default whether to raise an exception to none - redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth - - def get_login_url(self): - """ - Override this method to customize the login_url. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) - - return force_text(login_url) - - def get_redirect_field_name(self): - """ - Override this method to customize the redirect_field_name. - """ - if self.redirect_field_name is None: - raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) - return self.redirect_field_name - - -class LoginRequiredMixin(AccessMixin): - """ - View mixin which verifies that the user is authenticated. - - NOTE: - This should be the left-most mixin of a view, except when - combined with CsrfExemptMixin - which in that case should - be the left-most mixin. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - if self.raise_exception: - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class AnonymousRequiredMixin(object): - """ - View mixin which redirects to a specified URL if authenticated. - Can be useful if you wanted to prevent authenticated users from - accessing signup pages etc. - - 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): - if request.user.is_authenticated(): - return HttpResponseRedirect(self.get_authenticated_redirect_url()) - return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) - - def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ - if not self.authenticated_redirect_url: - raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) - return self.authenticated_redirect_url - class CsrfExemptMixin(object): """ @@ -129,262 +22,6 @@ def dispatch(self, *args, **kwargs): return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) -class PermissionRequiredMixin(AccessMixin): - """ - View mixin which verifies that the logged in user has the specified - permission. - - Class Settings - `permission_required` - the permission to check for. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - - class SomeView(PermissionRequiredMixin, ListView): - ... - # required - permission_required = "app.permission" - - # optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - ... - """ - permission_required = None # Default required perms to none - - def 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( - '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) - - return self.permission_required - - def check_permissions(self, request): - """ - Returns whether or not the user has permissions - """ - perms = self.get_permission_required(request) - return request.user.has_perm(perms) - - def no_permissions_fail(self, request=None): - """ - Called when the user has no permissions. 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()) - - 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: # If the user lacks the permission - if self.raise_exception: - raise PermissionDenied # Return a 403 - return self.no_permissions_fail(request) - - return super(PermissionRequiredMixin, self).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 passed in list. - - By specifying The `any` key , the user must have ONE of the set - permissions in the list. - - Class Settings - `permissions` - This is required to be a dict with one or both - keys of `all` and/or `any` containing a list or tuple of - permissions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - class SomeView(MultiplePermissionsRequiredMixin, ListView): - ... - #required - permissions = { - "all": ("blog.add_post", "blog.change_post"), - "any": ("blog.delete_post", "user.change_user") - } - - #optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - """ - permissions = None # Default required perms to none - - def get_permission_required(self, request=None): - self._check_permissions_attr() - return self.permissions - - def check_permissions(self, request): - permissions = self.get_permission_required() - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None - - self._check_permissions_keys_set(perms_all, perms_any) - self._check_perms_keys("all", perms_all) - self._check_perms_keys("any", perms_any) - - # If perms_all, check that user has all permissions in the list/tuple - if perms_all: - if not request.user.has_perms(perms_all): - return False - - # If perms_any, check that user has at least one in the list/tuple - if perms_any: - has_one_perm = False - for perm in perms_any: - if request.user.has_perm(perm): - has_one_perm = True - break - - if not has_one_perm: - 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( - '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) - - 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( - '{0} requires the "permissions" attribute to be set to a ' - 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) - - 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( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) - - -class GroupRequiredMixin(AccessMixin): - group_required = None - - def get_group_required(self): - if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple) + six.string_types) - ): - - raise ImproperlyConfigured( - '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) - if not isinstance(self.group_required, (list, tuple)): - self.group_required = (self.group_required,) - return self.group_required - - def check_membership(self, groups): - """ Check required group(s) """ - 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): - self.request = request - in_group = False - if self.request.user.is_authenticated(): - in_group = self.check_membership(self.get_group_required()) - - if not in_group: - if self.raise_exception: - raise PermissionDenied - else: - return redirect_to_login( - request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin allows you to define test that every user should pass - to get access into view. - - Class Settings - `test_func` - This is required to be a method that takes user - instance and return True or False after checking conditions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - """ - - def test_func(self, user): - raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) - - def get_test_func(self): - return getattr(self, "test_func") - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()(request.user) - - if not user_test_result: # If user don't pass the test - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) - - class UserFormKwargsMixin(object): """ CBV mixin which puts the user from the request into the form kwargs. @@ -420,399 +57,6 @@ def get_success_url(self): return reverse(self.success_list_url) -class SuperuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_superuser` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_superuser: # If the user is a standard user, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response. - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class SetHeadlineMixin(object): - """ - Mixin allows you to set a static headline through a static property on the - class or programmatically by overloading the get_headline method. - """ - headline = None # Default the headline to none - - def get_context_data(self, **kwargs): - kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) - # Update the existing context dict with the provided headline. - kwargs.update({"headline": self.get_headline()}) - return kwargs - - def get_headline(self): - if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. - raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) - return force_text(self.headline) - - -class StaticContextMixin(object): - """ - Mixin allows you to set static context through a static property on - the class. - """ - static_context = None - - def get_context_data(self, **kwargs): - kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) - - try: - kwargs.update(self.get_static_context()) - except (TypeError, ValueError): - raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) - else: - return kwargs - - def get_static_context(self): - if self.static_context is None: - raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) - ) - return self.static_context - - -class SelectRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a select_related on. - """ - select_related = None # Default related fields to none - - def get_queryset(self): - if self.select_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - if not isinstance(self.select_related, (tuple, list)): - # If the select_related argument is *not* a tuple or list, - # raise a configuration error. - raise ImproperlyConfigured( - "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(SelectRelatedMixin, self).get_queryset() - - return queryset.select_related(*self.select_related) - - -class PrefetchRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a prefetch_related on. - """ - prefetch_related = None # Default prefetch fields to none - - def get_queryset(self): - if self.prefetch_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - 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( - "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(PrefetchRelatedMixin, self).get_queryset() - - return queryset.prefetch_related(*self.prefetch_related) - - -class StaffuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_staff` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: # If the request's user is not staff, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONResponseMixin(object): - """ - A mixin that allows you to easily serialize simple data such as a dict or - Django models. - """ - content_type = None - json_dumps_kwargs = None - - def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - (six.string_types, six.text_type))): - raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) - return self.content_type or u"application/json" - - def get_json_dumps_kwargs(self): - if self.json_dumps_kwargs is None: - self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) - return self.json_dumps_kwargs - - def render_json_response(self, context_dict, status=200): - """ - Limited serialization for shipping plain data. Do not use for models - or other complex or custom objects. - """ - json_context = json.dumps( - context_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs()).encode(u'utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) - - def render_json_object_response(self, objects, **kwargs): - """ - Serializes objects using Django's builtin JSON serializer. Additional - kwargs can be used the same way for django.core.serializers.serialize. - """ - json_data = serializers.serialize(u"json", objects, **kwargs) - return HttpResponse(json_data, content_type=self.get_content_type()) - - -class AjaxResponseMixin(object): - """ - Mixin allows you to define alternative methods for ajax requests. Similar - to the normal get, post, and put methods, you can use get_ajax, post_ajax, - and put_ajax. - """ - def dispatch(self, request, *args, **kwargs): - request_method = request.method.lower() - - if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, u"{0}_ajax".format(request_method), - self.http_method_not_allowed) - self.request = request - self.args = args - self.kwargs = kwargs - return handler(request, *args, **kwargs) - - return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) - - def get_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post_ajax(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - def put_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - -class JsonRequestResponseMixin(JSONResponseMixin): - """ - Extends JSONResponseMixin. Attempts to parse request as JSON. If request - is properly formatted, the json is saved to self.request_json as a Python - object. request_json will be None for imparsible requests. - Set the attribute require_json to True to return a 400 "Bad Request" error - for requests that don't contain JSON. - - Note: To allow public access to your view, you'll need to use the - csrf_exempt decorator or CsrfExemptMixin. - - Example Usage: - - class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): - def post(self, request, *args, **kwargs): - do_stuff_with_contents_of_request_json() - return self.render_json_response( - {'message': 'Thanks!'}) - """ - require_json = False - error_response_dict = {u"errors": [u"Improperly formatted request"]} - - def render_bad_request_response(self, error_dict=None): - if error_dict is None: - error_dict = self.error_response_dict - json_context = json.dumps( - error_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs() - ).encode(u'utf-8') - return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) - - def get_request_json(self): - try: - return json.loads(self.request.body.decode(u'utf-8')) - except ValueError: - return None - - def dispatch(self, request, *args, **kwargs): - self.request = request - self.args = args - self.kwargs = kwargs - - self.request_json = self.get_request_json() - if self.require_json and self.request_json is None: - return self.render_bad_request_response() - return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONRequestResponseMixin(JsonRequestResponseMixin): - pass - - -class OrderableListMixin(object): - """ - Mixin allows your users to order records using GET parameters - """ - - orderable_columns = None - orderable_columns_default = None - order_by = None - ordering = None - - def get_context_data(self, **kwargs): - """ - Augments context with: - - * ``order_by`` - name of the field - * ``ordering`` - order of ordering, either ``asc`` or ``desc`` - """ - context = super(OrderableListMixin, self).get_context_data(**kwargs) - context["order_by"] = self.order_by - context["ordering"] = self.ordering - return context - - def get_orderable_columns(self): - if not self.orderable_columns: - raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) - return self.orderable_columns - - def get_orderable_columns_default(self): - if not self.orderable_columns_default: - raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) - return self.orderable_columns_default - - def get_ordered_queryset(self, queryset=None): - """ - Augments ``QuerySet`` with order_by statement if possible - - :param QuerySet queryset: ``QuerySet`` to ``order_by`` - :return: QuerySet - """ - get_order_by = self.request.GET.get("order_by") - - if get_order_by in self.get_orderable_columns(): - order_by = get_order_by - else: - order_by = self.get_orderable_columns_default() - - self.order_by = order_by - self.ordering = "asc" - - if order_by and self.request.GET.get("ordering", "asc") == "desc": - order_by = "-" + order_by - self.ordering = "desc" - - return queryset.order_by(order_by) - - def get_queryset(self): - """ - Returns ordered ``QuerySet`` - """ - unordered_queryset = super(OrderableListMixin, self).get_queryset() - return self.get_ordered_queryset(unordered_queryset) - - -class CanonicalSlugDetailMixin(object): - """ - A mixin that enforces a canonical slug in the url. - - If a urlpattern takes a object's pk and slug as arguments and the slug url - argument does not equal the object's canonical slug, this mixin will - redirect to the url containing the canonical slug. - """ - def dispatch(self, request, *args, **kwargs): - # Set up since we need to super() later instead of earlier. - self.request = request - self.args = args - self.kwargs = kwargs - - # Get the current object, url slug, and - # urlpattern name (namespace aware). - obj = self.get_object() - slug = self.kwargs.get(self.slug_url_kwarg, None) - match = resolve(request.path_info) - url_parts = match.namespaces - url_parts.append(match.url_name) - current_urlpattern = ":".join(url_parts) - - # Figure out what the slug is supposed to be. - if hasattr(obj, "get_canonical_slug"): - canonical_slug = obj.get_canonical_slug() - else: - canonical_slug = self.get_canonical_slug() - - # If there's a discrepancy between the slug in the url and the - # canonical slug, redirect to the canonical slug. - if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} - return redirect(current_urlpattern, **params) - - return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) - - def get_canonical_slug(self): - """ - Override this method to customize what slug should be considered - canonical. - - Alternatively, define the get_canonical_slug method on this view's - object class. In that case, this method will never be called. - """ - return self.get_object().slug - - class _MessageAPIWrapper(object): """ Wrap the django.contrib.messages.api module to automatically pass a given @@ -929,21 +173,3 @@ class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): FormInvalidMessageMixin. """ pass - - -class AllVerbsMixin(object): - """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): - if not self.all_handler: - raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) - - handler = getattr(self, self.all_handler, self.http_method_not_allowed) - return handler(request, *args, **kwargs) From 7fff29e9bf8925e209d2593ad64c75ed5933880c Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 4 Mar 2014 14:21:13 -0800 Subject: [PATCH 7/9] move other mixins. --- braces/views/__init__.py | 11 +- braces/views/_other.py | 827 +-------------------------------------- 2 files changed, 12 insertions(+), 826 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index c07f69ac..0668b595 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -10,7 +10,6 @@ SuperuserRequiredMixin, StaffuserRequiredMixin ) - from ._ajax import ( JSONResponseMixin, AjaxResponseMixin, @@ -26,6 +25,12 @@ FormInvalidMessageMixin, FormMessagesMixin ) +from ._other import ( + SetHeadlineMixin, + StaticContextMixin, + CanonicalSlugDetailMixin, + AllVerbsMixin +) from ._queries import ( OrderableListMixin, SelectRelatedMixin, @@ -55,4 +60,8 @@ 'FormValidMessageMixin', 'FormInvalidMessageMixin', 'FormMessagesMixin', + 'AllVerbsMixin', + 'CanonicalSlugDetailMixin', + 'SetHeadlineMixin', + 'StaticContextMixin' ] diff --git a/braces/views/_other.py b/braces/views/_other.py index e79aedfa..fc2647ee 100644 --- a/braces/views/_other.py +++ b/braces/views/_other.py @@ -1,440 +1,7 @@ -import six - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login -from django.core import serializers -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import resolve from django.shortcuts import redirect -from django.utils.decorators import method_decorator from django.utils.encoding import force_text -from django.utils.functional import curry, Promise -from django.views.decorators.csrf import csrf_exempt - -## Django 1.5+ compat -try: - import json -except ImportError: # pragma: no cover - from django.utils import simplejson as json - - -class AccessMixin(object): - """ - 'Abstract' mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - raise_exception = False # Default whether to raise an exception to none - redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth - - def get_login_url(self): - """ - Override this method to customize the login_url. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) - - return force_text(login_url) - - def get_redirect_field_name(self): - """ - Override this method to customize the redirect_field_name. - """ - if self.redirect_field_name is None: - raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) - return self.redirect_field_name - - -class LoginRequiredMixin(AccessMixin): - """ - View mixin which verifies that the user is authenticated. - - NOTE: - This should be the left-most mixin of a view, except when - combined with CsrfExemptMixin - which in that case should - be the left-most mixin. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - if self.raise_exception: - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class AnonymousRequiredMixin(object): - """ - View mixin which redirects to a specified URL if authenticated. - Can be useful if you wanted to prevent authenticated users from - accessing signup pages etc. - - 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): - if request.user.is_authenticated(): - return HttpResponseRedirect(self.get_authenticated_redirect_url()) - return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) - - def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ - if not self.authenticated_redirect_url: - raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) - return self.authenticated_redirect_url - - -class CsrfExemptMixin(object): - """ - Exempts the view from CSRF requirements. - - NOTE: - This should be the left-most mixin of a view. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) - - -class PermissionRequiredMixin(AccessMixin): - """ - View mixin which verifies that the logged in user has the specified - permission. - - Class Settings - `permission_required` - the permission to check for. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - - class SomeView(PermissionRequiredMixin, ListView): - ... - # required - permission_required = "app.permission" - - # optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - ... - """ - permission_required = None # Default required perms to none - - def 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( - '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) - - return self.permission_required - - def check_permissions(self, request): - """ - Returns whether or not the user has permissions - """ - perms = self.get_permission_required(request) - return request.user.has_perm(perms) - - def no_permissions_fail(self, request=None): - """ - Called when the user has no permissions. 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()) - - 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: # If the user lacks the permission - if self.raise_exception: - raise PermissionDenied # Return a 403 - return self.no_permissions_fail(request) - - return super(PermissionRequiredMixin, self).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 passed in list. - - By specifying The `any` key , the user must have ONE of the set - permissions in the list. - - Class Settings - `permissions` - This is required to be a dict with one or both - keys of `all` and/or `any` containing a list or tuple of - permissions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - class SomeView(MultiplePermissionsRequiredMixin, ListView): - ... - #required - permissions = { - "all": ("blog.add_post", "blog.change_post"), - "any": ("blog.delete_post", "user.change_user") - } - - #optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - """ - permissions = None # Default required perms to none - - def get_permission_required(self, request=None): - self._check_permissions_attr() - return self.permissions - - def check_permissions(self, request): - permissions = self.get_permission_required() - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None - - self._check_permissions_keys_set(perms_all, perms_any) - self._check_perms_keys("all", perms_all) - self._check_perms_keys("any", perms_any) - - # If perms_all, check that user has all permissions in the list/tuple - if perms_all: - if not request.user.has_perms(perms_all): - return False - - # If perms_any, check that user has at least one in the list/tuple - if perms_any: - has_one_perm = False - for perm in perms_any: - if request.user.has_perm(perm): - has_one_perm = True - break - - if not has_one_perm: - 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( - '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) - - 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( - '{0} requires the "permissions" attribute to be set to a ' - 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) - - 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( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) - - -class GroupRequiredMixin(AccessMixin): - group_required = None - - def get_group_required(self): - if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple) + six.string_types) - ): - - raise ImproperlyConfigured( - '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) - if not isinstance(self.group_required, (list, tuple)): - self.group_required = (self.group_required,) - return self.group_required - - def check_membership(self, groups): - """ Check required group(s) """ - 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): - self.request = request - in_group = False - if self.request.user.is_authenticated(): - in_group = self.check_membership(self.get_group_required()) - - if not in_group: - if self.raise_exception: - raise PermissionDenied - else: - return redirect_to_login( - request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin allows you to define test that every user should pass - to get access into view. - - Class Settings - `test_func` - This is required to be a method that takes user - instance and return True or False after checking conditions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - """ - - def test_func(self, user): - raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) - - def get_test_func(self): - return getattr(self, "test_func") - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()(request.user) - - if not user_test_result: # If user don't pass the test - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) - - -class UserFormKwargsMixin(object): - """ - CBV mixin which puts the user from the request into the form kwargs. - Note: Using this mixin requires you to pop the `user` kwarg - out of the dict in the super of your form's `__init__`. - """ - def get_form_kwargs(self): - kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() - # Update the existing form kwargs dict with the request's user. - kwargs.update({"user": self.request.user}) - return kwargs - - -class SuccessURLRedirectListMixin(object): - """ - Simple CBV mixin which sets the success url to the list view of - a given app. Set success_list_url as a class attribute of your - CBV and don't worry about overloading the get_success_url. - - This is only to be used for redirecting to a list page. If you need - to reverse the url with kwargs, this is not the mixin to use. - """ - success_list_url = None # Default the success url to none - - def get_success_url(self): - # Return the reversed success url. - if self.success_list_url is None: - raise ImproperlyConfigured( - '{0} is missing a succes_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) - return reverse(self.success_list_url) - - -class SuperuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_superuser` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_superuser: # If the user is a standard user, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response. - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) class SetHeadlineMixin(object): @@ -490,278 +57,6 @@ def get_static_context(self): return self.static_context -class SelectRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a select_related on. - """ - select_related = None # Default related fields to none - - def get_queryset(self): - if self.select_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - if not isinstance(self.select_related, (tuple, list)): - # If the select_related argument is *not* a tuple or list, - # raise a configuration error. - raise ImproperlyConfigured( - "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(SelectRelatedMixin, self).get_queryset() - - return queryset.select_related(*self.select_related) - - -class PrefetchRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a prefetch_related on. - """ - prefetch_related = None # Default prefetch fields to none - - def get_queryset(self): - if self.prefetch_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - 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( - "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(PrefetchRelatedMixin, self).get_queryset() - - return queryset.prefetch_related(*self.prefetch_related) - - -class StaffuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_staff` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: # If the request's user is not staff, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONResponseMixin(object): - """ - A mixin that allows you to easily serialize simple data such as a dict or - Django models. - """ - content_type = None - json_dumps_kwargs = None - - def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - (six.string_types, six.text_type))): - raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) - return self.content_type or u"application/json" - - def get_json_dumps_kwargs(self): - if self.json_dumps_kwargs is None: - self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) - return self.json_dumps_kwargs - - def render_json_response(self, context_dict, status=200): - """ - Limited serialization for shipping plain data. Do not use for models - or other complex or custom objects. - """ - json_context = json.dumps( - context_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs()).encode(u'utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) - - def render_json_object_response(self, objects, **kwargs): - """ - Serializes objects using Django's builtin JSON serializer. Additional - kwargs can be used the same way for django.core.serializers.serialize. - """ - json_data = serializers.serialize(u"json", objects, **kwargs) - return HttpResponse(json_data, content_type=self.get_content_type()) - - -class AjaxResponseMixin(object): - """ - Mixin allows you to define alternative methods for ajax requests. Similar - to the normal get, post, and put methods, you can use get_ajax, post_ajax, - and put_ajax. - """ - def dispatch(self, request, *args, **kwargs): - request_method = request.method.lower() - - if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, u"{0}_ajax".format(request_method), - self.http_method_not_allowed) - self.request = request - self.args = args - self.kwargs = kwargs - return handler(request, *args, **kwargs) - - return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) - - def get_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post_ajax(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - def put_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - -class JsonRequestResponseMixin(JSONResponseMixin): - """ - Extends JSONResponseMixin. Attempts to parse request as JSON. If request - is properly formatted, the json is saved to self.request_json as a Python - object. request_json will be None for imparsible requests. - Set the attribute require_json to True to return a 400 "Bad Request" error - for requests that don't contain JSON. - - Note: To allow public access to your view, you'll need to use the - csrf_exempt decorator or CsrfExemptMixin. - - Example Usage: - - class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): - def post(self, request, *args, **kwargs): - do_stuff_with_contents_of_request_json() - return self.render_json_response( - {'message': 'Thanks!'}) - """ - require_json = False - error_response_dict = {u"errors": [u"Improperly formatted request"]} - - def render_bad_request_response(self, error_dict=None): - if error_dict is None: - error_dict = self.error_response_dict - json_context = json.dumps( - error_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs() - ).encode(u'utf-8') - return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) - - def get_request_json(self): - try: - return json.loads(self.request.body.decode(u'utf-8')) - except ValueError: - return None - - def dispatch(self, request, *args, **kwargs): - self.request = request - self.args = args - self.kwargs = kwargs - - self.request_json = self.get_request_json() - if self.require_json and self.request_json is None: - return self.render_bad_request_response() - return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONRequestResponseMixin(JsonRequestResponseMixin): - pass - - -class OrderableListMixin(object): - """ - Mixin allows your users to order records using GET parameters - """ - - orderable_columns = None - orderable_columns_default = None - order_by = None - ordering = None - - def get_context_data(self, **kwargs): - """ - Augments context with: - - * ``order_by`` - name of the field - * ``ordering`` - order of ordering, either ``asc`` or ``desc`` - """ - context = super(OrderableListMixin, self).get_context_data(**kwargs) - context["order_by"] = self.order_by - context["ordering"] = self.ordering - return context - - def get_orderable_columns(self): - if not self.orderable_columns: - raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) - return self.orderable_columns - - def get_orderable_columns_default(self): - if not self.orderable_columns_default: - raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) - return self.orderable_columns_default - - def get_ordered_queryset(self, queryset=None): - """ - Augments ``QuerySet`` with order_by statement if possible - - :param QuerySet queryset: ``QuerySet`` to ``order_by`` - :return: QuerySet - """ - get_order_by = self.request.GET.get("order_by") - - if get_order_by in self.get_orderable_columns(): - order_by = get_order_by - else: - order_by = self.get_orderable_columns_default() - - self.order_by = order_by - self.ordering = "asc" - - if order_by and self.request.GET.get("ordering", "asc") == "desc": - order_by = "-" + order_by - self.ordering = "desc" - - return queryset.order_by(order_by) - - def get_queryset(self): - """ - Returns ordered ``QuerySet`` - """ - unordered_queryset = super(OrderableListMixin, self).get_queryset() - return self.get_ordered_queryset(unordered_queryset) - - class CanonicalSlugDetailMixin(object): """ A mixin that enforces a canonical slug in the url. @@ -813,124 +108,6 @@ def get_canonical_slug(self): return self.get_object().slug -class _MessageAPIWrapper(object): - """ - Wrap the django.contrib.messages.api module to automatically pass a given - request object as the first parameter of 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, curry(api_fn, request)) - - -class _MessageDescriptor(object): - """ - A descriptor that binds the _MessageAPIWrapper to the view's - request. - """ - def __get__(self, instance, owner): - return _MessageAPIWrapper(instance.request) - - -class MessageMixin(object): - """ - Add a `messages` attribute on the view instance that wraps - `django.contrib .messages`, automatically passing the current - request object. - """ - messages = _MessageDescriptor() - - -class FormValidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_valid_message method. - """ - form_valid_message = None # Default to None - - def get_form_valid_message(self): - """ - Validate that form_valid_message is set and is either a - unicode or str object. - """ - if self.form_valid_message is None: - raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) - ) - - if not isinstance(self.form_valid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) - ) - - return force_text(self.form_valid_message) - - def form_valid(self, form): - """ - Call the super first, so that when overriding - get_form_valid_message, we have access to the newly saved object. - """ - response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) - return response - - -class FormInvalidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_invalid_message method. - """ - form_invalid_message = None - - def get_form_invalid_message(self): - """ - Validate that form_invalid_message is set and is either a - unicode or str object. - """ - if self.form_invalid_message is None: - raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) - - if not isinstance(self.form_invalid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) - - return force_text(self.form_invalid_message) - - def form_invalid(self, form): - response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) - return response - - -class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): - """ - Mixin is a shortcut to use both FormValidMessageMixin and - FormInvalidMessageMixin. - """ - pass - - class AllVerbsMixin(object): """Call a single method for all HTTP verbs. From baa937f436e3d2ea8f925f8dbd403a85b055281a Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 4 Mar 2014 14:41:16 -0800 Subject: [PATCH 8/9] remove too damn long views file and add import to fix tests. --- braces/views.py | 949 --------------------------------------- braces/views/__init__.py | 3 +- 2 files changed, 2 insertions(+), 950 deletions(-) delete mode 100644 braces/views.py diff --git a/braces/views.py b/braces/views.py deleted file mode 100644 index e79aedfa..00000000 --- a/braces/views.py +++ /dev/null @@ -1,949 +0,0 @@ -import six - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.views import redirect_to_login -from django.core import serializers -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import resolve, reverse -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect) -from django.shortcuts import redirect -from django.utils.decorators import method_decorator -from django.utils.encoding import force_text -from django.utils.functional import curry, Promise -from django.views.decorators.csrf import csrf_exempt - -## Django 1.5+ compat -try: - import json -except ImportError: # pragma: no cover - from django.utils import simplejson as json - - -class AccessMixin(object): - """ - 'Abstract' mixin that gives access mixins the same customizable - functionality. - """ - login_url = None - raise_exception = False # Default whether to raise an exception to none - redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth - - def get_login_url(self): - """ - Override this method to customize the login_url. - """ - login_url = self.login_url or settings.LOGIN_URL - if not login_url: - raise ImproperlyConfigured( - 'Define {0}.login_url or settings.LOGIN_URL or override ' - '{0}.get_login_url().'.format(self.__class__.__name__)) - - return force_text(login_url) - - def get_redirect_field_name(self): - """ - Override this method to customize the redirect_field_name. - """ - if self.redirect_field_name is None: - raise ImproperlyConfigured( - '{0} is missing the ' - 'redirect_field_name. Define {0}.redirect_field_name or ' - 'override {0}.get_redirect_field_name().'.format( - self.__class__.__name__)) - return self.redirect_field_name - - -class LoginRequiredMixin(AccessMixin): - """ - View mixin which verifies that the user is authenticated. - - NOTE: - This should be the left-most mixin of a view, except when - combined with CsrfExemptMixin - which in that case should - be the left-most mixin. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated(): - if self.raise_exception: - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(LoginRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class AnonymousRequiredMixin(object): - """ - View mixin which redirects to a specified URL if authenticated. - Can be useful if you wanted to prevent authenticated users from - accessing signup pages etc. - - 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): - if request.user.is_authenticated(): - return HttpResponseRedirect(self.get_authenticated_redirect_url()) - return super(AnonymousRequiredMixin, self).dispatch( - request, *args, **kwargs) - - def get_authenticated_redirect_url(self): - """ Return the reversed authenticated redirect url. """ - if not self.authenticated_redirect_url: - raise ImproperlyConfigured( - '{0} is missing an authenticated_redirect_url ' - 'url to redirect to. Define ' - '{0}.authenticated_redirect_url or override ' - '{0}.get_authenticated_redirect_url().'.format( - self.__class__.__name__)) - return self.authenticated_redirect_url - - -class CsrfExemptMixin(object): - """ - Exempts the view from CSRF requirements. - - NOTE: - This should be the left-most mixin of a view. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) - - -class PermissionRequiredMixin(AccessMixin): - """ - View mixin which verifies that the logged in user has the specified - permission. - - Class Settings - `permission_required` - the permission to check for. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - - class SomeView(PermissionRequiredMixin, ListView): - ... - # required - permission_required = "app.permission" - - # optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - ... - """ - permission_required = None # Default required perms to none - - def 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( - '{0} requires the "permission_required" attribute to be ' - 'set.'.format(self.__class__.__name__)) - - return self.permission_required - - def check_permissions(self, request): - """ - Returns whether or not the user has permissions - """ - perms = self.get_permission_required(request) - return request.user.has_perm(perms) - - def no_permissions_fail(self, request=None): - """ - Called when the user has no permissions. 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()) - - 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: # If the user lacks the permission - if self.raise_exception: - raise PermissionDenied # Return a 403 - return self.no_permissions_fail(request) - - return super(PermissionRequiredMixin, self).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 passed in list. - - By specifying The `any` key , the user must have ONE of the set - permissions in the list. - - Class Settings - `permissions` - This is required to be a dict with one or both - keys of `all` and/or `any` containing a list or tuple of - permissions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - - Example Usage - class SomeView(MultiplePermissionsRequiredMixin, ListView): - ... - #required - permissions = { - "all": ("blog.add_post", "blog.change_post"), - "any": ("blog.delete_post", "user.change_user") - } - - #optional - login_url = "/signup/" - redirect_field_name = "hollaback" - raise_exception = True - """ - permissions = None # Default required perms to none - - def get_permission_required(self, request=None): - self._check_permissions_attr() - return self.permissions - - def check_permissions(self, request): - permissions = self.get_permission_required() - perms_all = permissions.get('all') or None - perms_any = permissions.get('any') or None - - self._check_permissions_keys_set(perms_all, perms_any) - self._check_perms_keys("all", perms_all) - self._check_perms_keys("any", perms_any) - - # If perms_all, check that user has all permissions in the list/tuple - if perms_all: - if not request.user.has_perms(perms_all): - return False - - # If perms_any, check that user has at least one in the list/tuple - if perms_any: - has_one_perm = False - for perm in perms_any: - if request.user.has_perm(perm): - has_one_perm = True - break - - if not has_one_perm: - 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( - '{0} requires the "permissions" attribute to be set as a ' - 'dict.'.format(self.__class__.__name__)) - - 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( - '{0} requires the "permissions" attribute to be set to a ' - 'dict and the "any" or "all" key to be set.'.format( - self.__class__.__name__)) - - 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( - '{0} requires the permisions dict {1} value to be a ' - 'list or tuple.'.format(self.__class__.__name__, key)) - - -class GroupRequiredMixin(AccessMixin): - group_required = None - - def get_group_required(self): - if self.group_required is None or ( - not isinstance(self.group_required, - (list, tuple) + six.string_types) - ): - - raise ImproperlyConfigured( - '{0} requires the "group_required" attribute to be set and be ' - 'one of the following types: string, unicode, list or ' - 'tuple'.format(self.__class__.__name__)) - if not isinstance(self.group_required, (list, tuple)): - self.group_required = (self.group_required,) - return self.group_required - - def check_membership(self, groups): - """ Check required group(s) """ - 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): - self.request = request - in_group = False - if self.request.user.is_authenticated(): - in_group = self.check_membership(self.get_group_required()) - - if not in_group: - if self.raise_exception: - raise PermissionDenied - else: - return redirect_to_login( - request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(GroupRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class UserPassesTestMixin(AccessMixin): - """ - CBV Mixin allows you to define test that every user should pass - to get access into view. - - Class Settings - `test_func` - This is required to be a method that takes user - instance and return True or False after checking conditions. - `login_url` - the login url of site - `redirect_field_name` - defaults to "next" - `raise_exception` - defaults to False - raise 403 if set to True - """ - - def test_func(self, user): - raise NotImplementedError( - '{0} is missing implementation of the ' - 'test_func method. You should write one.'.format( - self.__class__.__name__)) - - def get_test_func(self): - return getattr(self, "test_func") - - def dispatch(self, request, *args, **kwargs): - user_test_result = self.get_test_func()(request.user) - - if not user_test_result: # If user don't pass the test - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - return super(UserPassesTestMixin, self).dispatch( - request, *args, **kwargs) - - -class UserFormKwargsMixin(object): - """ - CBV mixin which puts the user from the request into the form kwargs. - Note: Using this mixin requires you to pop the `user` kwarg - out of the dict in the super of your form's `__init__`. - """ - def get_form_kwargs(self): - kwargs = super(UserFormKwargsMixin, self).get_form_kwargs() - # Update the existing form kwargs dict with the request's user. - kwargs.update({"user": self.request.user}) - return kwargs - - -class SuccessURLRedirectListMixin(object): - """ - Simple CBV mixin which sets the success url to the list view of - a given app. Set success_list_url as a class attribute of your - CBV and don't worry about overloading the get_success_url. - - This is only to be used for redirecting to a list page. If you need - to reverse the url with kwargs, this is not the mixin to use. - """ - success_list_url = None # Default the success url to none - - def get_success_url(self): - # Return the reversed success url. - if self.success_list_url is None: - raise ImproperlyConfigured( - '{0} is missing a succes_list_url ' - 'name to reverse and redirect to. Define ' - '{0}.success_list_url or override ' - '{0}.get_success_url().'.format(self.__class__.__name__)) - return reverse(self.success_list_url) - - -class SuperuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_superuser` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_superuser: # If the user is a standard user, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response. - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(SuperuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class SetHeadlineMixin(object): - """ - Mixin allows you to set a static headline through a static property on the - class or programmatically by overloading the get_headline method. - """ - headline = None # Default the headline to none - - def get_context_data(self, **kwargs): - kwargs = super(SetHeadlineMixin, self).get_context_data(**kwargs) - # Update the existing context dict with the provided headline. - kwargs.update({"headline": self.get_headline()}) - return kwargs - - def get_headline(self): - if self.headline is None: # If no headline was provided as a view - # attribute and this method wasn't - # overridden raise a configuration error. - raise ImproperlyConfigured( - '{0} is missing a headline. ' - 'Define {0}.headline, or override ' - '{0}.get_headline().'.format(self.__class__.__name__)) - return force_text(self.headline) - - -class StaticContextMixin(object): - """ - Mixin allows you to set static context through a static property on - the class. - """ - static_context = None - - def get_context_data(self, **kwargs): - kwargs = super(StaticContextMixin, self).get_context_data(**kwargs) - - try: - kwargs.update(self.get_static_context()) - except (TypeError, ValueError): - raise ImproperlyConfigured( - '{0}.static_context must be a dictionary or container ' - 'of two-tuples.'.format(self.__class__.__name__)) - else: - return kwargs - - def get_static_context(self): - if self.static_context is None: - raise ImproperlyConfigured( - '{0} is missing the static_context property. Define ' - '{0}.static_context, or override ' - '{0}.get_static_context()'.format(self.__class__.__name__) - ) - return self.static_context - - -class SelectRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a select_related on. - """ - select_related = None # Default related fields to none - - def get_queryset(self): - if self.select_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the select_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - if not isinstance(self.select_related, (tuple, list)): - # If the select_related argument is *not* a tuple or list, - # raise a configuration error. - raise ImproperlyConfigured( - "{0}'s select_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(SelectRelatedMixin, self).get_queryset() - - return queryset.select_related(*self.select_related) - - -class PrefetchRelatedMixin(object): - """ - Mixin allows you to provide a tuple or list of related models to - perform a prefetch_related on. - """ - prefetch_related = None # Default prefetch fields to none - - def get_queryset(self): - if self.prefetch_related is None: # If no fields were provided, - # raise a configuration error - raise ImproperlyConfigured( - '{0} is missing the prefetch_related property. This must be ' - 'a tuple or list.'.format(self.__class__.__name__)) - - 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( - "{0}'s prefetch_related property must be a tuple or " - "list.".format(self.__class__.__name__)) - - # Get the current queryset of the view - queryset = super(PrefetchRelatedMixin, self).get_queryset() - - return queryset.prefetch_related(*self.prefetch_related) - - -class StaffuserRequiredMixin(AccessMixin): - """ - Mixin allows you to require a user with `is_staff` set to True. - """ - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: # If the request's user is not staff, - if self.raise_exception: # *and* if an exception was desired - raise PermissionDenied # return a forbidden response - else: - return redirect_to_login(request.get_full_path(), - self.get_login_url(), - self.get_redirect_field_name()) - - return super(StaffuserRequiredMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONResponseMixin(object): - """ - A mixin that allows you to easily serialize simple data such as a dict or - Django models. - """ - content_type = None - json_dumps_kwargs = None - - def get_content_type(self): - if (self.content_type is not None and - not isinstance(self.content_type, - (six.string_types, six.text_type))): - raise ImproperlyConfigured( - '{0} is missing a content type. Define {0}.content_type, ' - 'or override {0}.get_content_type().'.format( - self.__class__.__name__)) - return self.content_type or u"application/json" - - def get_json_dumps_kwargs(self): - if self.json_dumps_kwargs is None: - self.json_dumps_kwargs = {} - self.json_dumps_kwargs.setdefault(u'ensure_ascii', False) - return self.json_dumps_kwargs - - def render_json_response(self, context_dict, status=200): - """ - Limited serialization for shipping plain data. Do not use for models - or other complex or custom objects. - """ - json_context = json.dumps( - context_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs()).encode(u'utf-8') - return HttpResponse(json_context, - content_type=self.get_content_type(), - status=status) - - def render_json_object_response(self, objects, **kwargs): - """ - Serializes objects using Django's builtin JSON serializer. Additional - kwargs can be used the same way for django.core.serializers.serialize. - """ - json_data = serializers.serialize(u"json", objects, **kwargs) - return HttpResponse(json_data, content_type=self.get_content_type()) - - -class AjaxResponseMixin(object): - """ - Mixin allows you to define alternative methods for ajax requests. Similar - to the normal get, post, and put methods, you can use get_ajax, post_ajax, - and put_ajax. - """ - def dispatch(self, request, *args, **kwargs): - request_method = request.method.lower() - - if request.is_ajax() and request_method in self.http_method_names: - handler = getattr(self, u"{0}_ajax".format(request_method), - self.http_method_not_allowed) - self.request = request - self.args = args - self.kwargs = kwargs - return handler(request, *args, **kwargs) - - return super(AjaxResponseMixin, self).dispatch( - request, *args, **kwargs) - - def get_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post_ajax(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - def put_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete_ajax(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - -class JsonRequestResponseMixin(JSONResponseMixin): - """ - Extends JSONResponseMixin. Attempts to parse request as JSON. If request - is properly formatted, the json is saved to self.request_json as a Python - object. request_json will be None for imparsible requests. - Set the attribute require_json to True to return a 400 "Bad Request" error - for requests that don't contain JSON. - - Note: To allow public access to your view, you'll need to use the - csrf_exempt decorator or CsrfExemptMixin. - - Example Usage: - - class SomeView(CsrfExemptMixin, JsonRequestResponseMixin): - def post(self, request, *args, **kwargs): - do_stuff_with_contents_of_request_json() - return self.render_json_response( - {'message': 'Thanks!'}) - """ - require_json = False - error_response_dict = {u"errors": [u"Improperly formatted request"]} - - def render_bad_request_response(self, error_dict=None): - if error_dict is None: - error_dict = self.error_response_dict - json_context = json.dumps( - error_dict, - cls=DjangoJSONEncoder, - **self.get_json_dumps_kwargs() - ).encode(u'utf-8') - return HttpResponseBadRequest( - json_context, content_type=self.get_content_type()) - - def get_request_json(self): - try: - return json.loads(self.request.body.decode(u'utf-8')) - except ValueError: - return None - - def dispatch(self, request, *args, **kwargs): - self.request = request - self.args = args - self.kwargs = kwargs - - self.request_json = self.get_request_json() - if self.require_json and self.request_json is None: - return self.render_bad_request_response() - return super(JsonRequestResponseMixin, self).dispatch( - request, *args, **kwargs) - - -class JSONRequestResponseMixin(JsonRequestResponseMixin): - pass - - -class OrderableListMixin(object): - """ - Mixin allows your users to order records using GET parameters - """ - - orderable_columns = None - orderable_columns_default = None - order_by = None - ordering = None - - def get_context_data(self, **kwargs): - """ - Augments context with: - - * ``order_by`` - name of the field - * ``ordering`` - order of ordering, either ``asc`` or ``desc`` - """ - context = super(OrderableListMixin, self).get_context_data(**kwargs) - context["order_by"] = self.order_by - context["ordering"] = self.ordering - return context - - def get_orderable_columns(self): - if not self.orderable_columns: - raise ImproperlyConfigured( - '{0} needs the ordering columns defined.'.format( - self.__class__.__name__)) - return self.orderable_columns - - def get_orderable_columns_default(self): - if not self.orderable_columns_default: - raise ImproperlyConfigured( - '{0} needs the default ordering column defined.'.format( - self.__class__.__name__)) - return self.orderable_columns_default - - def get_ordered_queryset(self, queryset=None): - """ - Augments ``QuerySet`` with order_by statement if possible - - :param QuerySet queryset: ``QuerySet`` to ``order_by`` - :return: QuerySet - """ - get_order_by = self.request.GET.get("order_by") - - if get_order_by in self.get_orderable_columns(): - order_by = get_order_by - else: - order_by = self.get_orderable_columns_default() - - self.order_by = order_by - self.ordering = "asc" - - if order_by and self.request.GET.get("ordering", "asc") == "desc": - order_by = "-" + order_by - self.ordering = "desc" - - return queryset.order_by(order_by) - - def get_queryset(self): - """ - Returns ordered ``QuerySet`` - """ - unordered_queryset = super(OrderableListMixin, self).get_queryset() - return self.get_ordered_queryset(unordered_queryset) - - -class CanonicalSlugDetailMixin(object): - """ - A mixin that enforces a canonical slug in the url. - - If a urlpattern takes a object's pk and slug as arguments and the slug url - argument does not equal the object's canonical slug, this mixin will - redirect to the url containing the canonical slug. - """ - def dispatch(self, request, *args, **kwargs): - # Set up since we need to super() later instead of earlier. - self.request = request - self.args = args - self.kwargs = kwargs - - # Get the current object, url slug, and - # urlpattern name (namespace aware). - obj = self.get_object() - slug = self.kwargs.get(self.slug_url_kwarg, None) - match = resolve(request.path_info) - url_parts = match.namespaces - url_parts.append(match.url_name) - current_urlpattern = ":".join(url_parts) - - # Figure out what the slug is supposed to be. - if hasattr(obj, "get_canonical_slug"): - canonical_slug = obj.get_canonical_slug() - else: - canonical_slug = self.get_canonical_slug() - - # If there's a discrepancy between the slug in the url and the - # canonical slug, redirect to the canonical slug. - if canonical_slug != slug: - params = {self.pk_url_kwarg: obj.pk, - self.slug_url_kwarg: canonical_slug, - 'permanent': True} - return redirect(current_urlpattern, **params) - - return super(CanonicalSlugDetailMixin, self).dispatch( - request, *args, **kwargs) - - def get_canonical_slug(self): - """ - Override this method to customize what slug should be considered - canonical. - - Alternatively, define the get_canonical_slug method on this view's - object class. In that case, this method will never be called. - """ - return self.get_object().slug - - -class _MessageAPIWrapper(object): - """ - Wrap the django.contrib.messages.api module to automatically pass a given - request object as the first parameter of 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, curry(api_fn, request)) - - -class _MessageDescriptor(object): - """ - A descriptor that binds the _MessageAPIWrapper to the view's - request. - """ - def __get__(self, instance, owner): - return _MessageAPIWrapper(instance.request) - - -class MessageMixin(object): - """ - Add a `messages` attribute on the view instance that wraps - `django.contrib .messages`, automatically passing the current - request object. - """ - messages = _MessageDescriptor() - - -class FormValidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_valid_message method. - """ - form_valid_message = None # Default to None - - def get_form_valid_message(self): - """ - Validate that form_valid_message is set and is either a - unicode or str object. - """ - if self.form_valid_message is None: - raise ImproperlyConfigured( - '{0}.form_valid_message is not set. Define ' - '{0}.form_valid_message, or override ' - '{0}.get_form_valid_message().'.format(self.__class__.__name__) - ) - - if not isinstance(self.form_valid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_valid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__) - ) - - return force_text(self.form_valid_message) - - def form_valid(self, form): - """ - Call the super first, so that when overriding - get_form_valid_message, we have access to the newly saved object. - """ - response = super(FormValidMessageMixin, self).form_valid(form) - self.messages.success(self.get_form_valid_message(), - fail_silently=True) - return response - - -class FormInvalidMessageMixin(MessageMixin): - """ - Mixin allows you to set static message which is displayed by - Django's messages framework through a static property on the class - or programmatically by overloading the get_form_invalid_message method. - """ - form_invalid_message = None - - def get_form_invalid_message(self): - """ - Validate that form_invalid_message is set and is either a - unicode or str object. - """ - if self.form_invalid_message is None: - raise ImproperlyConfigured( - '{0}.form_invalid_message is not set. Define ' - '{0}.form_invalid_message, or override ' - '{0}.get_form_invalid_message().'.format( - self.__class__.__name__)) - - if not isinstance(self.form_invalid_message, - (six.string_types, six.text_type, Promise)): - raise ImproperlyConfigured( - '{0}.form_invalid_message must be a str or unicode ' - 'object.'.format(self.__class__.__name__)) - - return force_text(self.form_invalid_message) - - def form_invalid(self, form): - response = super(FormInvalidMessageMixin, self).form_invalid(form) - self.messages.error(self.get_form_invalid_message(), - fail_silently=True) - return response - - -class FormMessagesMixin(FormValidMessageMixin, FormInvalidMessageMixin): - """ - Mixin is a shortcut to use both FormValidMessageMixin and - FormInvalidMessageMixin. - """ - pass - - -class AllVerbsMixin(object): - """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): - if not self.all_handler: - raise ImproperlyConfigured( - '{0} requires the all_handler attribute to be set.'.format( - self.__class__.__name__)) - - handler = getattr(self, self.all_handler, self.http_method_not_allowed) - return handler(request, *args, **kwargs) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index 0668b595..72009110 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -23,7 +23,8 @@ MessageMixin, FormValidMessageMixin, FormInvalidMessageMixin, - FormMessagesMixin + FormMessagesMixin, + _MessageAPIWrapper ) from ._other import ( SetHeadlineMixin, From da77000d4ca0b48f4a2345cd934e704bd960ebb5 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 4 Mar 2014 14:53:24 -0800 Subject: [PATCH 9/9] alphabetical... because OCD --- braces/views/__init__.py | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/braces/views/__init__.py b/braces/views/__init__.py index 72009110..8ae500b6 100644 --- a/braces/views/__init__.py +++ b/braces/views/__init__.py @@ -1,68 +1,68 @@ from __future__ import absolute_import from ._access import ( - LoginRequiredMixin, AnonymousRequiredMixin, - PermissionRequiredMixin, - MultiplePermissionsRequiredMixin, GroupRequiredMixin, - UserPassesTestMixin, + LoginRequiredMixin, + MultiplePermissionsRequiredMixin, + PermissionRequiredMixin, + StaffuserRequiredMixin, SuperuserRequiredMixin, - StaffuserRequiredMixin + UserPassesTestMixin ) from ._ajax import ( - JSONResponseMixin, AjaxResponseMixin, - JsonRequestResponseMixin, - JSONRequestResponseMixin + JSONRequestResponseMixin, + JSONResponseMixin, + JsonRequestResponseMixin ) from ._forms import ( CsrfExemptMixin, - UserFormKwargsMixin, - SuccessURLRedirectListMixin, - MessageMixin, - FormValidMessageMixin, FormInvalidMessageMixin, FormMessagesMixin, + FormValidMessageMixin, + MessageMixin, + SuccessURLRedirectListMixin, + UserFormKwargsMixin, _MessageAPIWrapper ) from ._other import ( - SetHeadlineMixin, - StaticContextMixin, + AllVerbsMixin, CanonicalSlugDetailMixin, - AllVerbsMixin + SetHeadlineMixin, + StaticContextMixin ) from ._queries import ( OrderableListMixin, - SelectRelatedMixin, - PrefetchRelatedMixin + PrefetchRelatedMixin, + SelectRelatedMixin ) __all__ = [ - 'LoginRequiredMixin', - 'AnonymousRequiredMixin', - 'PermissionRequiredMixin', - 'MultiplePermissionsRequiredMixin', - 'GroupRequiredMixin', - 'UserPassesTestMixin', - 'SuperuserRequiredMixin', - 'StaffuserRequiredMixin', - 'OrderableListMixin', - 'SelectRelatedMixin', - 'PrefetchRelatedMixin', - 'JSONResponseMixin', 'AjaxResponseMixin', - 'JsonRequestResponseMixin', - 'JSONRequestResponseMixin', + 'AllVerbsMixin', + 'AnonymousRequiredMixin', + 'CanonicalSlugDetailMixin', 'CsrfExemptMixin', - 'UserFormKwargsMixin', - 'SuccessURLRedirectListMixin', - 'MessageMixin', - 'FormValidMessageMixin', 'FormInvalidMessageMixin', 'FormMessagesMixin', - 'AllVerbsMixin', - 'CanonicalSlugDetailMixin', + 'FormValidMessageMixin', + 'GroupRequiredMixin', + 'JSONRequestResponseMixin', + 'JsonRequestResponseMixin', + 'JSONResponseMixin', + 'LoginRequiredMixin', + 'MessageMixin', + 'MultiplePermissionsRequiredMixin', + 'OrderableListMixin', + 'PermissionRequiredMixin', + 'PrefetchRelatedMixin', + 'SelectRelatedMixin', 'SetHeadlineMixin', - 'StaticContextMixin' + 'StaffuserRequiredMixin', + 'StaticContextMixin', + 'SuccessURLRedirectListMixin', + 'SuperuserRequiredMixin', + 'UserFormKwargsMixin', + 'UserPassesTestMixin' ]