Fixed #24914 -- Added authentication mixins for CBVs
Added the mixins LoginRequiredMixin, PermissionRequiredMixin and UserPassesTestMixin to contrib.auth as counterparts to the respective view decorators. The authentication mixins UserPassesTestMixin, LoginRequiredMixin and PermissionRequiredMixin have been inspired by django-braces <https://github.com/brack3t/django-braces/> Thanks Raphael Michel for the initial patch, tests and docs on the PR and Ana Balica, Kenneth Love, Marc Tamlyn, and Tim Graham for the review.
This commit is contained in:
parent
2f615b10e6
commit
e5cb4e1411
1
AUTHORS
1
AUTHORS
|
@ -583,6 +583,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Ram Rachum <ram@rachum.com>
|
||||
Randy Barlow <randy@electronsweatshop.com>
|
||||
Raphaël Barrois <raphael.barrois@m4x.org>
|
||||
Raphael Michel <mail@raphaelmichel.de>
|
||||
Raúl Cumplido <raulcumplido@gmail.com>
|
||||
Remco Wendt <remco.wendt@gmail.com>
|
||||
Renaud Parent <renaud.parent@gmail.com>
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
|
||||
class AccessMixin(object):
|
||||
"""
|
||||
Abstract CBV mixin that gives access mixins the same customizable
|
||||
functionality.
|
||||
"""
|
||||
login_url = None
|
||||
permission_denied_message = ''
|
||||
raise_exception = False
|
||||
redirect_field_name = REDIRECT_FIELD_NAME
|
||||
|
||||
def get_login_url(self):
|
||||
"""
|
||||
Override this method to override the login_url attribute.
|
||||
"""
|
||||
login_url = self.login_url or settings.LOGIN_URL
|
||||
if not login_url:
|
||||
raise ImproperlyConfigured(
|
||||
'{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
|
||||
'{0}.get_login_url().'.format(self.__class__.__name__)
|
||||
)
|
||||
return force_text(login_url)
|
||||
|
||||
def get_permission_denied_message(self):
|
||||
"""
|
||||
Override this method to override the permission_denied_message attribute.
|
||||
"""
|
||||
return self.permission_denied_message
|
||||
|
||||
def get_redirect_field_name(self):
|
||||
"""
|
||||
Override this method to override the redirect_field_name attribute.
|
||||
"""
|
||||
return self.redirect_field_name
|
||||
|
||||
def handle_no_permission(self):
|
||||
if self.raise_exception:
|
||||
raise PermissionDenied(self.get_permission_denied_message())
|
||||
return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())
|
||||
|
||||
|
||||
class LoginRequiredMixin(AccessMixin):
|
||||
"""
|
||||
CBV mixin which verifies that the current user is authenticated.
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated():
|
||||
return self.handle_no_permission()
|
||||
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PermissionRequiredMixin(AccessMixin):
|
||||
"""
|
||||
CBV mixin which verifies that the current user has all specified
|
||||
permissions.
|
||||
"""
|
||||
permission_required = None
|
||||
|
||||
def get_permission_required(self):
|
||||
"""
|
||||
Override this method to override the permission_required attribute.
|
||||
Must return an iterable.
|
||||
"""
|
||||
if self.permission_required is None:
|
||||
raise ImproperlyConfigured(
|
||||
'{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
|
||||
'{0}.get_permission_required().'.format(self.__class__.__name__)
|
||||
)
|
||||
if isinstance(self.permission_required, six.string_types):
|
||||
perms = (self.permission_required, )
|
||||
else:
|
||||
perms = self.permission_required
|
||||
return perms
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
perms = self.get_permission_required()
|
||||
if not request.user.has_perms(perms):
|
||||
return self.handle_no_permission()
|
||||
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserPassesTestMixin(AccessMixin):
|
||||
"""
|
||||
CBV Mixin that allows you to define a test function which must return True
|
||||
if the current user can access the view.
|
||||
"""
|
||||
|
||||
def test_func(self):
|
||||
raise NotImplementedError(
|
||||
'{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__)
|
||||
)
|
||||
|
||||
def get_test_func(self):
|
||||
"""
|
||||
Override this method to use a different test_func method.
|
||||
"""
|
||||
return self.test_func
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
user_test_result = self.get_test_func()()
|
||||
if not user_test_result:
|
||||
return self.handle_no_permission()
|
||||
return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)
|
|
@ -65,6 +65,43 @@ the included auth forms for your project, you could set, for example::
|
|||
|
||||
See :ref:`password-validation` for more details.
|
||||
|
||||
Permission mixins for class-based views
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django now ships with the mixins
|
||||
:class:`~django.contrib.auth.mixins.AccessMixin`,
|
||||
:class:`~django.contrib.auth.mixins.LoginRequiredMixin`,
|
||||
:class:`~django.contrib.auth.mixins.PermissionRequiredMixin`, and
|
||||
:class:`~django.contrib.auth.mixins.UserPassesTestMixin` to provide the
|
||||
functionality of the ``django.contrib.auth.decorators`` for class-based views.
|
||||
These mixins have been taken from, or are at least inspired by, the
|
||||
`django-braces`_ project.
|
||||
|
||||
There are a few differences between Django's and django-braces' implementation,
|
||||
though:
|
||||
|
||||
* The :attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` attribute
|
||||
can only be ``True`` or ``False``. Custom exceptions or callables are not
|
||||
supported.
|
||||
|
||||
* The :meth:`~django.contrib.auth.mixins.AccessMixin.handle_no_permission`
|
||||
method does not take a ``request`` argument. The current request is available
|
||||
in ``self.request``.
|
||||
|
||||
* The custom ``test_func()`` of :class:`~django.contrib.auth.mixins.UserPassesTestMixin`
|
||||
does not take a ``user`` argument. The current user is available in
|
||||
``self.request.user``.
|
||||
|
||||
* The :attr:`permission_required <django.contrib.auth.mixins.PermissionRequiredMixin>`
|
||||
attribute supports a string (defining one permission) or a list/tuple of
|
||||
strings (defining multiple permissions) that need to be fulfilled to grant
|
||||
access.
|
||||
|
||||
* The new :attr:`~django.contrib.auth.mixins.AccessMixin.permission_denied_message`
|
||||
attribute allows passing a message to the ``PermissionDenied`` exception.
|
||||
|
||||
.. _django-braces: http://django-braces.readthedocs.org/en/latest/index.html
|
||||
|
||||
Minor features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -425,8 +425,8 @@ login page::
|
|||
|
||||
.. currentmodule:: django.contrib.auth.decorators
|
||||
|
||||
The login_required decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The ``login_required`` decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. function:: login_required([redirect_field_name=REDIRECT_FIELD_NAME, login_url=None])
|
||||
|
||||
|
@ -500,6 +500,43 @@ The login_required decorator
|
|||
:func:`django.contrib.admin.views.decorators.staff_member_required`
|
||||
decorator a useful alternative to ``login_required()``.
|
||||
|
||||
.. currentmodule:: django.contrib.auth.mixins
|
||||
|
||||
The ``LoginRequired`` mixin
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using :doc:`class-based views </topics/class-based-views/index>`, you can
|
||||
achieve the same behavior as with ``login_required`` by using the
|
||||
``LoginRequiredMixin``. This mixin should be at the leftmost position in the
|
||||
inheritance list.
|
||||
|
||||
.. class:: LoginRequiredMixin
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
If a view is using this mixin, all requests by non-authenticated users will
|
||||
be redirected to the login page or shown an HTTP 403 Forbidden error,
|
||||
depending on the
|
||||
:attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` parameter.
|
||||
|
||||
You can set any of the parameters of
|
||||
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
|
||||
of unauthorized users::
|
||||
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
class MyView(LoginRequiredMixin, View):
|
||||
login_url = '/login/'
|
||||
redirect_field_name = 'redirect_to'
|
||||
|
||||
.. note::
|
||||
|
||||
Just as the ``login_required`` decorator, this mixin does NOT check the
|
||||
``is_active`` flag on a user.
|
||||
|
||||
.. currentmodule:: django.contrib.auth.decorators
|
||||
|
||||
Limiting access to logged-in users that pass a test
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -560,8 +597,50 @@ redirects to the login page::
|
|||
def my_view(request):
|
||||
...
|
||||
|
||||
The permission_required decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. currentmodule:: django.contrib.auth.mixins
|
||||
|
||||
.. class:: UserPassesTestMixin
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
When using :doc:`class-based views </topics/class-based-views/index>`, you
|
||||
can use the ``UserPassesTestMixin`` to do this.
|
||||
|
||||
You have to override the ``test_func()`` method of the class to provide
|
||||
the test that is performed. Furthermore, you can set any of the parameters
|
||||
of :class:`~django.contrib.auth.mixins.AccessMixin` to customize the
|
||||
handling of unauthorized users::
|
||||
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
|
||||
class MyView(UserPassesTestMixin, View):
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.email.endswith('@example.com')
|
||||
|
||||
.. admonition: Stacking UserPassesTestMixin
|
||||
|
||||
Due to the way ``UserPassesTestMixin`` is implemented, you cannot stack
|
||||
them in your inheritance list. The following does NOT work::
|
||||
|
||||
class TestMixin1(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
return self.request.user.email.endswith('@example.com')
|
||||
|
||||
class TestMixin2(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
return self.request.user.username.startswith('django')
|
||||
|
||||
class MyView(TestMixin1, TestMixin2, View):
|
||||
...
|
||||
|
||||
If ``TestMixin1`` would call ``super()`` and take that result into
|
||||
account, ``TestMixin1`` wouldn't work standalone anymore.
|
||||
|
||||
.. currentmodule:: django.contrib.auth.decorators
|
||||
|
||||
The ``permission_required`` decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. function:: permission_required(perm, [login_url=None, raise_exception=False])
|
||||
|
||||
|
@ -583,7 +662,7 @@ The permission_required decorator
|
|||
The decorator may also take an iterable of permissions.
|
||||
|
||||
Note that :func:`~django.contrib.auth.decorators.permission_required()`
|
||||
also takes an optional ``login_url`` parameter. Example::
|
||||
also takes an optional ``login_url`` parameter::
|
||||
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
|
||||
|
@ -604,16 +683,74 @@ The permission_required decorator
|
|||
In older versions, the ``permission`` parameter only worked with
|
||||
strings, lists, and tuples instead of strings and any iterable.
|
||||
|
||||
.. _applying-permissions-to-generic-views:
|
||||
.. currentmodule:: django.contrib.auth.mixins
|
||||
|
||||
Applying permissions to generic views
|
||||
The ``PermissionRequiredMixin`` mixin
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To apply a permission to a :doc:`class-based generic view
|
||||
</ref/class-based-views/index>`, decorate the :meth:`View.dispatch
|
||||
<django.views.generic.base.View.dispatch>` method on the class. See
|
||||
:ref:`decorating-class-based-views` for details. Another approach is to
|
||||
:ref:`write a mixin that wraps as_view() <mixins_that_wrap_as_view>`.
|
||||
To apply permission checks to :doc:`class-based views
|
||||
</ref/class-based-views/index>`, you can use the ``PermissionRequiredMixin``:
|
||||
|
||||
.. class:: PermissionRequiredMixin
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
This mixin, just like the ``permisison_required``
|
||||
decorator, checks whether the user accessing a view has all given
|
||||
permissions. You should specify the permission (or an iterable of
|
||||
permissions) using the ``permission_required`` parameter::
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
class MyView(PermissionRequiredMixin, View):
|
||||
permission_required = 'polls.can_vote'
|
||||
# Or multiple of permissions:
|
||||
permission_required = ('polls.can_open', 'polls.can_edit')
|
||||
|
||||
You can set any of the parameters of
|
||||
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
|
||||
of unauthorized users.
|
||||
|
||||
Redirecting unauthorized requests in class-based views
|
||||
------------------------------------------------------
|
||||
|
||||
To ease the handling of access restrictions in :doc:`class-based views
|
||||
</ref/class-based-views/index>`, the ``AccessMixin`` can be used to redirect a
|
||||
user to the login page or issue an HTTP 403 Forbidden response.
|
||||
|
||||
.. class:: AccessMixin
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
.. attribute:: login_url
|
||||
|
||||
The URL that users who don't pass the test will be redirected to.
|
||||
Defaults to :setting:`settings.LOGIN_URL <LOGIN_URL>`.
|
||||
|
||||
.. attribute:: permission_denied_message
|
||||
|
||||
When ``raise_exception`` is ``True``, this attribute can be used to
|
||||
control the error message passed to the error handler for display to
|
||||
the user. Defaults to an empty string.
|
||||
|
||||
.. attribute:: redirect_field_name
|
||||
|
||||
The name of the query parameter that will contain the URL the user
|
||||
should be redirected to after a successful login. If you set this to
|
||||
``None``, a query parameter won't be added. Defaults to ``"next"``.
|
||||
|
||||
.. attribute:: raise_exception
|
||||
|
||||
If this attribute is set to ``True``, a
|
||||
:class:`~django.core.exceptions.PermissionDenied` exception will be
|
||||
raised instead of the redirect. Defaults to ``False``.
|
||||
|
||||
.. method:: handle_no_permission()
|
||||
|
||||
Depending on the value of ``raise_exception``, the method either raises
|
||||
a :exc:`~django.core.exceptions.PermissionDenied` exception or
|
||||
redirects the user to the ``login_url``, optionally including the
|
||||
``redirect_field_name`` if it is set.
|
||||
|
||||
.. _session-invalidation-on-password-change:
|
||||
|
||||
|
|
|
@ -173,29 +173,6 @@ that inherits from ``View`` - for example, trying to use a form at the top of a
|
|||
list and combining :class:`~django.views.generic.edit.ProcessFormView` and
|
||||
:class:`~django.views.generic.list.ListView` - won't work as expected.
|
||||
|
||||
.. _mixins_that_wrap_as_view:
|
||||
|
||||
Mixins that wrap ``as_view()``
|
||||
------------------------------
|
||||
|
||||
One way to apply common behavior to many classes is to write a mixin that wraps
|
||||
the :meth:`~django.views.generic.base.View.as_view()` method.
|
||||
|
||||
For example, if you have many generic views that should be decorated with
|
||||
:func:`~django.contrib.auth.decorators.login_required` you could implement a
|
||||
mixin like this::
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
class LoginRequiredMixin(object):
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
|
||||
return login_required(view)
|
||||
|
||||
class MyView(LoginRequiredMixin, ...):
|
||||
# this is a generic view
|
||||
...
|
||||
|
||||
Handling forms with class-based views
|
||||
=====================================
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
from django.contrib.auth import models
|
||||
from django.contrib.auth.mixins import (
|
||||
LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin,
|
||||
)
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.views.generic import View
|
||||
|
||||
|
||||
class AlwaysTrueMixin(UserPassesTestMixin):
|
||||
|
||||
def test_func(self):
|
||||
return True
|
||||
|
||||
|
||||
class AlwaysFalseMixin(UserPassesTestMixin):
|
||||
|
||||
def test_func(self):
|
||||
return False
|
||||
|
||||
|
||||
class EmptyResponseView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
class AlwaysTrueView(AlwaysTrueMixin, EmptyResponseView):
|
||||
pass
|
||||
|
||||
|
||||
class AlwaysFalseView(AlwaysFalseMixin, EmptyResponseView):
|
||||
pass
|
||||
|
||||
|
||||
class StackedMixinsView1(LoginRequiredMixin, PermissionRequiredMixin, EmptyResponseView):
|
||||
permission_required = ['auth.add_customuser', 'auth.change_customuser']
|
||||
raise_exception = True
|
||||
|
||||
|
||||
class StackedMixinsView2(PermissionRequiredMixin, LoginRequiredMixin, EmptyResponseView):
|
||||
permission_required = ['auth.add_customuser', 'auth.change_customuser']
|
||||
raise_exception = True
|
||||
|
||||
|
||||
class AccessMixinTests(TestCase):
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
def test_stacked_mixins_success(self):
|
||||
user = models.User.objects.create(username='joe', password='qwerty')
|
||||
perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser'))
|
||||
user.user_permissions.add(*perms)
|
||||
request = self.factory.get('/rand')
|
||||
request.user = user
|
||||
|
||||
view = StackedMixinsView1.as_view()
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
view = StackedMixinsView2.as_view()
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_stacked_mixins_missing_permission(self):
|
||||
user = models.User.objects.create(username='joe', password='qwerty')
|
||||
perms = models.Permission.objects.filter(codename__in=('add_customuser',))
|
||||
user.user_permissions.add(*perms)
|
||||
request = self.factory.get('/rand')
|
||||
request.user = user
|
||||
|
||||
view = StackedMixinsView1.as_view()
|
||||
with self.assertRaises(PermissionDenied):
|
||||
view(request)
|
||||
|
||||
view = StackedMixinsView2.as_view()
|
||||
with self.assertRaises(PermissionDenied):
|
||||
view(request)
|
||||
|
||||
def test_stacked_mixins_not_logged_in(self):
|
||||
user = models.User.objects.create(username='joe', password='qwerty')
|
||||
user.is_authenticated = lambda: False
|
||||
perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser'))
|
||||
user.user_permissions.add(*perms)
|
||||
request = self.factory.get('/rand')
|
||||
request.user = user
|
||||
|
||||
view = StackedMixinsView1.as_view()
|
||||
with self.assertRaises(PermissionDenied):
|
||||
view(request)
|
||||
|
||||
view = StackedMixinsView2.as_view()
|
||||
with self.assertRaises(PermissionDenied):
|
||||
view(request)
|
||||
|
||||
|
||||
class UserPassesTestTests(TestCase):
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
def _test_redirect(self, view=None, url='/accounts/login/?next=/rand'):
|
||||
if not view:
|
||||
view = AlwaysFalseView.as_view()
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, url)
|
||||
|
||||
def test_default(self):
|
||||
self._test_redirect()
|
||||
|
||||
def test_custom_redirect_url(self):
|
||||
class AView(AlwaysFalseView):
|
||||
login_url = '/login/'
|
||||
|
||||
self._test_redirect(AView.as_view(), '/login/?next=/rand')
|
||||
|
||||
def test_custom_redirect_parameter(self):
|
||||
class AView(AlwaysFalseView):
|
||||
redirect_field_name = 'goto'
|
||||
|
||||
self._test_redirect(AView.as_view(), '/accounts/login/?goto=/rand')
|
||||
|
||||
def test_no_redirect_parameter(self):
|
||||
class AView(AlwaysFalseView):
|
||||
redirect_field_name = None
|
||||
|
||||
self._test_redirect(AView.as_view(), '/accounts/login/')
|
||||
|
||||
def test_raise_exception(self):
|
||||
class AView(AlwaysFalseView):
|
||||
raise_exception = True
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
self.assertRaises(PermissionDenied, AView.as_view(), request)
|
||||
|
||||
def test_raise_exception_custom_message(self):
|
||||
msg = "You don't have access here"
|
||||
|
||||
class AView(AlwaysFalseView):
|
||||
raise_exception = True
|
||||
permission_denied_message = msg
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
view = AView.as_view()
|
||||
with self.assertRaises(PermissionDenied) as cm:
|
||||
view(request)
|
||||
self.assertEqual(cm.exception.args[0], msg)
|
||||
|
||||
def test_raise_exception_custom_message_function(self):
|
||||
msg = "You don't have access here"
|
||||
|
||||
class AView(AlwaysFalseView):
|
||||
raise_exception = True
|
||||
|
||||
def get_permission_denied_message(self):
|
||||
return msg
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
view = AView.as_view()
|
||||
with self.assertRaises(PermissionDenied) as cm:
|
||||
view(request)
|
||||
self.assertEqual(cm.exception.args[0], msg)
|
||||
|
||||
def test_user_passes(self):
|
||||
view = AlwaysTrueView.as_view()
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class LoginRequiredMixinTests(TestCase):
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = models.User.objects.create(username='joe', password='qwerty')
|
||||
|
||||
def test_login_required(self):
|
||||
"""
|
||||
Check that login_required works on a simple view wrapped in a
|
||||
login_required decorator.
|
||||
"""
|
||||
class AView(LoginRequiredMixin, EmptyResponseView):
|
||||
pass
|
||||
|
||||
view = AView.as_view()
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = AnonymousUser()
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual('/accounts/login/?next=/rand', response.url)
|
||||
request = self.factory.get('/rand')
|
||||
request.user = self.user
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class PermissionsRequiredMixinTests(TestCase):
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = models.User.objects.create(username='joe', password='qwerty')
|
||||
perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser'))
|
||||
cls.user.user_permissions.add(*perms)
|
||||
|
||||
def test_many_permissions_pass(self):
|
||||
class AView(PermissionRequiredMixin, EmptyResponseView):
|
||||
permission_required = ['auth.add_customuser', 'auth.change_customuser']
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = self.user
|
||||
resp = AView.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_single_permission_pass(self):
|
||||
class AView(PermissionRequiredMixin, EmptyResponseView):
|
||||
permission_required = 'auth.add_customuser'
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = self.user
|
||||
resp = AView.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_permissioned_denied_redirect(self):
|
||||
class AView(PermissionRequiredMixin, EmptyResponseView):
|
||||
permission_required = ['auth.add_customuser', 'auth.change_customuser', 'non-existent-permission']
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = self.user
|
||||
resp = AView.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_permissioned_denied_exception_raised(self):
|
||||
class AView(PermissionRequiredMixin, EmptyResponseView):
|
||||
permission_required = ['auth.add_customuser', 'auth.change_customuser', 'non-existent-permission']
|
||||
raise_exception = True
|
||||
|
||||
request = self.factory.get('/rand')
|
||||
request.user = self.user
|
||||
self.assertRaises(PermissionDenied, AView.as_view(), request)
|
Loading…
Reference in New Issue