Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Adding per instance widget config to WagtailCaptchaFormBuilder #54

Open
enzedonline opened this issue Nov 20, 2023 · 0 comments

Comments

@enzedonline
Copy link

enzedonline commented Nov 20, 2023

django_recaptcha v4 has some improvements in flexibility, including adding a per instance action for the V3 recaptcha.

At the moment, the usual route in WagtailCaptcha to add any config to the widget seems to be to create a custom WagtailCaptchaFormBuilder. This route requires a custom FormBuilder to use anything other than V2 checkbox and a separate FormBuilder for each action.

It got me thinking that a preferable route would be the ability to add a default widget to the site, and an optional dictionary in the model to supply any per instance parameters.

Thought I'd open a discussion to see if people had thoughts on the below:

First thought: if the namespace is captcha, leave everything as is, the additional change is only for django_recaptcha v4+

Add an optional setting for site default widget:

RECAPTCHA_WIDGET = 'ReCaptchaV3'

Add an attribute to the model instance (recaptcha_attrs) which can be used to pass per instance settings through to the form builder. E.g.:

class ContactPage(WagtailCaptchaEmailForm, Page):
    recaptcha_attrs = {
        'required_score': 0.7,
        'action': 'contact',
        'api_params': {'hl': Locale.get_active().language_code},
    }
    ....

Modify the WagtailCaptcha models to look for this attribute and pass through to the form builder:

    def get_form_class(self):
        attrs = getattr(self, 'recaptcha_attrs', {})
        fb = self.form_builder(self.get_form_fields(), **attrs)
        return fb.get_form_class()

Add possible parameters to WagtailCaptchaFormBuilder. Set widget via instance > site default > V2 checkbox (django_recaptcha default), widget can be passed as object or string:

try:
    from django_recaptcha.fields import ReCaptchaField
    from django_recaptcha import widgets
except ImportError:
    from captcha.fields import ReCaptchaField
    from captcha import widgets

class WagtailCaptchaFormBuilder(FormBuilder):
    def __init__(
            self, 
            fields, 
            widget=None, 
            required_score=None, 
            action='form', 
            v2_attrs={}, 
            api_params={}, 
            keys={}, 
            **kwargs
            ):
        super().__init__(fields, **kwargs)
        self.recaptcha_widget = widget or getattr(settings, 'RECAPTCHA_WIDGET', False) or widgets.ReCaptchaV2Checkbox
        self.recaptcha_action = action
        self.required_score = required_score
        self.v2_attrs = v2_attrs
        self.api_params = api_params
        self.keys = keys

Modify the formfields property to conditionally (only if django_recaptcha namespace in use) build the field attributes. Field attributes include widget and keys (recaptcha keys can also be added per instance with django_recaptcha):

    @property
    def formfields(self):
        fields = super(WagtailCaptchaFormBuilder, self).formfields
        if widgets.__package__ == 'django_recaptcha':
            fields[self.CAPTCHA_FIELD_NAME] = ReCaptchaField(**self.field_attrs)
        else:
            fields[self.CAPTCHA_FIELD_NAME] = ReCaptchaField(label='')
        return fields

    @property
    def field_attrs(self):
        kwargs = {'widget': self.get_recaptcha_widget()}
        if self.keys:
            try:
                kwargs['public_key'] = self.keys.get('public_key')
                kwargs['private_key'] = self.keys.get('private_key')
            except:
                raise ImproperlyConfigured(
                    _("`keys` attribute must be a dictionary with `public_key` and `private_key` values."))
        return kwargs

    def get_recaptcha_widget(self):
        import inspect
        if isinstance(self.recaptcha_widget, str):
            self.recaptcha_widget = getattr(
                widgets, self.recaptcha_widget, None)
        if inspect.getmodule(self.recaptcha_widget) != widgets:
            from django.core.exceptions import ImproperlyConfigured
            raise ImproperlyConfigured(
                _("Unsupported widget type. Please select widget from `django_recaptcha.widgets`."))

        recaptcha_kwargs = {}
        if self.recaptcha_widget == widgets.ReCaptchaV3:
            recaptcha_kwargs['action'] = self.recaptcha_action
            if self.required_score:
                recaptcha_kwargs['attrs'] = {
                    'required_score': self.required_score}
        else:
            if self.v2_attrs:
                recaptcha_kwargs['attrs'] = self.v2_attrs
        if self.api_params:
            recaptcha_kwargs['api_params'] = self.api_params

        return self.recaptcha_widget(**recaptcha_kwargs)

With no recaptcha_attrs or settings.RECAPTCHA_WIDGET defined, this will just return the default V2 checkbox as per current status.

If django-captcha is still on v3, no changes happen to the way the form builder sets the ReCaptchaField from the existing method.

With recaptcha_attrs, you get some flexibility with per instance without the need for custom form builders.

Ex, signup form with stronger required score and custom action (settings.RECAPTCHA_WIDGET='ReCaptchaV3'). This score will override settings.RECAPTCHA_REQUIRED_SCORE (ref)

    recaptcha_attrs = {
        'required_score': 0.9,
        'action': 'signup',
    }

Maybe one form on this site needs to use the V2 checkbox. Add the widget as a string, add the keys for the V2 api. Optionally set the V2 widget format:

    recaptcha_attrs = {
        'widget': 'ReCaptchaV2Checkbox',
        'v2_attrs': {
            'data-theme': 'dark',
            'data-size': 'compact',
        },
        'keys': {
            'public_key': settings.RECAPTCHA_V2_CHECK_PUBLIC_KEY,
            'private_key': settings.RECAPTCHA_V2_CHECK_PRIVATE_KEY,
        }
    }

On a multi-lingual site, ensure the widget display language matches the django activated language:

    recaptcha_attrs = {
        'api_params': {'hl': Locale.get_active().language_code},        
    }

I can make a PR for this if there's appetite - thought I'd sound it out here for thoughts on improvements/feasibility first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant