You have access control on views, but you also want to apply the same checks in templates in a maintainable way.
This kicked off my solution: https://stackoverflow.com/questions/43904784/reuse-the-decorator-of-a-view-from-a-template
You have access control on views, but you also want to apply the same checks in templates in a maintainable way.
This kicked off my solution: https://stackoverflow.com/questions/43904784/reuse-the-decorator-of-a-view-from-a-template
# Copy of `django.contrib.auth.decorators.user_passes_test`, relevant changes noted. | |
from django.conf import settings | |
from django.contrib.auth import REDIRECT_FIELD_NAME | |
from django.contrib.auth.views import redirect_to_login | |
from django.shortcuts import resolve_url | |
from functools import wraps | |
from urllib.parse import urlparse | |
# CHANGED: `*test_args` for the inclusion of `test_func` arguments | |
def user_passes_test(test_func, *test_args, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): | |
def decorator(view_func): | |
@wraps(view_func) | |
def _wrapped_view(request, *args, **kwargs): | |
if test_func(request.user, *test_args): | |
return view_func(request, *args, **kwargs) | |
path = request.build_absolute_uri() | |
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) | |
# If the login url is the same scheme and net location then just | |
# use the path as the "next" url. | |
login_scheme, login_netloc = urlparse(resolved_login_url)[:2] | |
current_scheme, current_netloc = urlparse(path)[:2] | |
if ((not login_scheme or login_scheme == current_scheme) and | |
(not login_netloc or login_netloc == current_netloc)): | |
path = request.get_full_path() | |
return redirect_to_login( | |
path, resolved_login_url, redirect_field_name) | |
_wrapped_view.can_view_validator = (test_func, *test_args) | |
return _wrapped_view | |
return decorator |
from django import template | |
from django.core.exceptions import PermissionDenied | |
from django.urls import resolve, reverse | |
register = template.Library() | |
# Creates a template tag that looks like this: | |
# {{ can_view 'my_app:secured' as can_view_secured }} | |
@register.simple_tag(takes_context=True) | |
def can_view(context, named): | |
# `reverse` the named url to get the url, `resolve` it to get the view function | |
# `user_passes_test` returns True/False or redirects, but `test_func` could also | |
# raise a `PermissionDenied` so handle that, too. | |
match = resolve(reverse(named)) | |
try: | |
if hasattr(match.func, 'can_view_validator'): | |
(test_func, *args) = match.func.can_view_validator | |
return test_func(context.request.user, *args) | |
else: | |
return True | |
except PermissionDenied: | |
return False |
from django.urls import path | |
from . import views | |
app_name = 'my_app' | |
urlpatterns = [ | |
# The url you want secured | |
path("secured/", views.secured, name="secured"), | |
] |
from django.utils.decorators import method_decorator | |
from django.views.generic import ListView | |
from .decorators import user_passes_test | |
from .utils import perms_check | |
# This view has a user perms check | |
# NOTE: This is our custom `user_passes_test` | |
@user_passes_test(perms_check, 'a', ...) | |
def secured(request): | |
pass | |
# Class Based Views | |
@method_decorator(user_passes_test(perms_check, 'a', ...), name='dispatch') | |
class SecuredListView(ListView): | |
pass |