diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd2d8ea3..8b6795be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### Removed - Dropped support for Django <4.2. +- Removed custom `utils.class_view_decorator()` in favor of Django's + `method_decorator()`. ## 1.16.0 ### Fixed diff --git a/docs/class-reference.rst b/docs/class-reference.rst index 6f2332475..b139e0d30 100644 --- a/docs/class-reference.rst +++ b/docs/class-reference.rst @@ -11,9 +11,6 @@ Decorators .. automodule:: django_otp.decorators :members: -.. automodule:: two_factor.views.utils - :members: class_view_decorator - Models ------ .. autoclass:: two_factor.plugins.phonenumber.models.PhoneDevice @@ -51,6 +48,11 @@ Template Tags .. automodule:: two_factor.plugins.phonenumber.templatetags.phonenumber :members: +Utilities +--------- +.. automodule:: two_factor.views.utils + :members: + Views ----- .. autoclass:: two_factor.views.LoginView diff --git a/example/views.py b/example/views.py index 365ed6710..53126e5f3 100644 --- a/example/views.py +++ b/example/views.py @@ -1,11 +1,11 @@ from django.conf import settings from django.contrib.auth.forms import UserCreationForm from django.shortcuts import redirect, resolve_url +from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.generic import FormView, TemplateView from two_factor.views import OTPRequiredMixin -from two_factor.views.utils import class_view_decorator class HomeView(TemplateView): @@ -30,6 +30,6 @@ def get_context_data(self, **kwargs): return context -@class_view_decorator(never_cache) +@method_decorator(never_cache, name='dispatch') class ExampleSecretView(OTPRequiredMixin, TemplateView): template_name = 'secret.html' diff --git a/two_factor/gateways/twilio/views.py b/two_factor/gateways/twilio/views.py index 5aa0ce6ea..5e580a4fa 100644 --- a/two_factor/gateways/twilio/views.py +++ b/two_factor/gateways/twilio/views.py @@ -2,17 +2,16 @@ from django.contrib.sites.shortcuts import get_current_site from django.template.response import TemplateResponse from django.utils import translation +from django.utils.decorators import method_decorator from django.utils.translation import check_for_language, pgettext from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.generic import View -from ...views.utils import class_view_decorator from .gateway import validate_voice_locale -@class_view_decorator(never_cache) -@class_view_decorator(csrf_exempt) +@method_decorator([never_cache, csrf_exempt], name='dispatch') class TwilioCallApp(View): """ View used by Twilio for the interactive token verification by phone. diff --git a/two_factor/plugins/phonenumber/views.py b/two_factor/plugins/phonenumber/views.py index 0f5551ee7..4fb2b9bc0 100644 --- a/two_factor/plugins/phonenumber/views.py +++ b/two_factor/plugins/phonenumber/views.py @@ -1,22 +1,20 @@ from django.conf import settings from django.shortcuts import redirect, resolve_url +from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.generic import DeleteView from django_otp.decorators import otp_required from django_otp.util import random_hex from two_factor.forms import DeviceValidationForm -from two_factor.views.utils import ( - IdempotentSessionWizardView, class_view_decorator, -) +from two_factor.views.utils import IdempotentSessionWizardView from .forms import PhoneNumberMethodForm from .models import PhoneDevice from .utils import get_available_phone_methods -@class_view_decorator(never_cache) -@class_view_decorator(otp_required) +@method_decorator([never_cache, otp_required], name='dispatch') class PhoneSetupView(IdempotentSessionWizardView): """ View for configuring a phone number for receiving tokens. @@ -87,8 +85,7 @@ def get_context_data(self, form, **kwargs): return super().get_context_data(form, **kwargs) -@class_view_decorator(never_cache) -@class_view_decorator(otp_required) +@method_decorator([never_cache, otp_required], name='dispatch') class PhoneDeleteView(DeleteView): """ View for removing a phone number used for verification. diff --git a/two_factor/views/core.py b/two_factor/views/core.py index d37c95329..fedf42196 100644 --- a/two_factor/views/core.py +++ b/two_factor/views/core.py @@ -47,8 +47,8 @@ ) from ..utils import default_device, get_otpauth_url from .utils import ( - IdempotentSessionWizardView, class_view_decorator, - get_remember_device_cookie, validate_remember_device_cookie, + IdempotentSessionWizardView, get_remember_device_cookie, + validate_remember_device_cookie, ) logger = logging.getLogger(__name__) @@ -56,6 +56,7 @@ REMEMBER_COOKIE_PREFIX = getattr(settings, 'TWO_FACTOR_REMEMBER_COOKIE_PREFIX', 'remember-cookie_') +@method_decorator([sensitive_post_parameters(), csrf_protect, never_cache], name='dispatch') class LoginView(RedirectURLMixin, IdempotentSessionWizardView): """ View for handling the login process, including OTP verification. @@ -405,9 +406,6 @@ def delete_cookies_from_response(self, response): # Copied from django.contrib.auth.views.LoginView (Branch: stable/1.11.x) # https://github.com/django/django/blob/58df8aa40fe88f753ba79e091a52f236246260b3/django/contrib/auth/views.py#L49 - @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) - @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if self.redirect_authenticated_user and self.request.user.is_authenticated: redirect_to = self.get_success_url() @@ -420,8 +418,7 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) -@class_view_decorator(never_cache) -@class_view_decorator(login_required) +@method_decorator([never_cache, login_required], name='dispatch') class SetupView(RedirectURLMixin, IdempotentSessionWizardView): """ View for handling OTP setup using a wizard. @@ -628,8 +625,7 @@ def get_form_metadata(self, step): return self.storage.extra_data['forms'].get(step, None) -@class_view_decorator(never_cache) -@class_view_decorator(otp_required) +@method_decorator([never_cache, otp_required], name='dispatch') class BackupTokensView(FormView): """ View for listing and generating backup tokens. @@ -664,8 +660,7 @@ def form_valid(self, form): return redirect(self.success_url) -@class_view_decorator(never_cache) -@class_view_decorator(otp_required) +@method_decorator([never_cache, otp_required], name='dispatch') class SetupCompleteView(TemplateView): """ View congratulation the user when OTP setup has completed. @@ -683,8 +678,7 @@ def get_context_data(self): } -@class_view_decorator(never_cache) -@class_view_decorator(login_required) +@method_decorator([never_cache, login_required], name='dispatch') class QRGeneratorView(View): """ View returns an SVG image with the OTP token information diff --git a/two_factor/views/profile.py b/two_factor/views/profile.py index e9c45b21a..bd6a74b4a 100644 --- a/two_factor/views/profile.py +++ b/two_factor/views/profile.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, resolve_url +from django.utils.decorators import method_decorator from django.utils.functional import lazy from django.views.decorators.cache import never_cache from django.views.generic import FormView, TemplateView @@ -13,11 +14,9 @@ from ..forms import DisableForm from ..utils import default_device -from .utils import class_view_decorator -@class_view_decorator(never_cache) -@class_view_decorator(login_required) +@method_decorator([never_cache, login_required], name='dispatch') class ProfileView(TemplateView): """ View used by users for managing two-factor configuration. @@ -48,7 +47,7 @@ def get_context_data(self, **kwargs): return context -@class_view_decorator(never_cache) +@method_decorator(never_cache, name='dispatch') class DisableView(FormView): """ View for disabling two-factor for a user's account. diff --git a/two_factor/views/utils.py b/two_factor/views/utils.py index 456e6cbd9..94e842f85 100644 --- a/two_factor/views/utils.py +++ b/two_factor/views/utils.py @@ -9,7 +9,6 @@ BadSignature, SignatureExpired, b62_decode, b62_encode, ) from django.utils.crypto import salted_hmac -from django.utils.decorators import method_decorator from django.utils.encoding import force_bytes from django.utils.translation import gettext as _ from formtools.wizard.forms import ManagementForm @@ -218,22 +217,6 @@ def render_done(self, form, **kwargs): return done_response -def class_view_decorator(function_decorator): - """ - Converts a function based decorator into a class based decorator usable - on class based Views. - - Can't subclass the `View` as it breaks inheritance (super in particular), - so we monkey-patch instead. - - From: http://stackoverflow.com/a/8429311/58107 - """ - def simple_decorator(View): - View.dispatch = method_decorator(function_decorator)(View.dispatch) - return View - return simple_decorator - - remember_device_cookie_separator = ':'