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.
django_recaptchav4 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 fordjango_recaptchav4+Add an optional setting for site default widget:
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.:Modify the WagtailCaptcha models to look for this attribute and pass through to the form builder:
Add possible parameters to
WagtailCaptchaFormBuilder. Set widget via instance > site default > V2 checkbox (django_recaptcha default), widget can be passed as object or string:Modify the
formfieldsproperty to conditionally (only ifdjango_recaptchanamespace in use) build the field attributes. Field attributes include widget and keys (recaptcha keys can also be added per instance withdjango_recaptcha):With no
recaptcha_attrsorsettings.RECAPTCHA_WIDGETdefined, this will just return the default V2 checkbox as per current status.If
django-captchais 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 overridesettings.RECAPTCHA_REQUIRED_SCORE(ref)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:
On a multi-lingual site, ensure the widget display language matches the django activated language:
I can make a PR for this if there's appetite - thought I'd sound it out here for thoughts on improvements/feasibility first.