Skip to content

Commit 28acde3

Browse files
authored
feat: Allow actions to add form fields for configuration (#6)
* Add: Configuration options for actions * Update readme, form parameter storage in form Meta class * Simplification
1 parent 74efffb commit 28acde3

File tree

10 files changed

+237
-64
lines changed

10 files changed

+237
-64
lines changed

README.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ First create a ``Form`` plugin to add a form. Each form created with help of the
6969

7070
Add form fields by adding child classes to the form plugin. Child classes can be form fields but also any other CMS Plugin. CMS Plugins may, e.g., be used to add custom formatting or additional help texts to a form.
7171

72+
Form fields
73+
-----------
74+
7275
Currently the following form fields are supported:
7376

7477
* CharField, EmailField, URLField
@@ -80,6 +83,21 @@ Currently the following form fields are supported:
8083

8184
A Form plugin must not be used within another Form plugin.
8285

86+
Actions
87+
-------
88+
89+
Upon submission of a valid form actions can be performed. A project can register as many actions as it likes::
90+
91+
from djangocms_form_builder import actions
92+
93+
@actions.register
94+
class MyAction(actions.FormAction):
95+
verbose_name = _("Everything included action")
96+
97+
def execute(self, form, request):
98+
... # This method is run upon successful submission of the form
99+
100+
83101
Using (existing) Django forms with djangocms-form-builder
84102
=========================================================
85103

@@ -105,8 +123,9 @@ By default the class name is translated to a human readable form (``MyGreatForm`
105123

106124
The verbose name will be shown in a Select field of the Form plugin.
107125

108-
Upon form submission a ``save()`` method of the form (if it has one). After executing the ``save()`` method the user is redirected to the url given in the ``redirect`` attribute.
126+
Upon form submission a ``save()`` method of the form (if it has one). After executing the ``save()`` method the user is redirected to the url given in the ``redirect`` attribute.
109127

128+
Actions are not available for Django forms. Any actions to be performed upon submission should reside in its ``save()`` method.
110129

111130

112131
.. |pypi| image:: https://badge.fury.io/py/djangocms-form-builder.svg

djangocms_form_builder/actions.py

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import hashlib
22

3+
from django import forms
34
from django.core.exceptions import ImproperlyConfigured
45
from django.core.mail import mail_admins, send_mail
6+
from django.core.validators import EmailValidator
7+
from django.template import TemplateDoesNotExist
58
from django.template.loader import render_to_string
69
from django.utils.html import strip_tags
710
from django.utils.translation import gettext_lazy as _
11+
from entangled.forms import EntangledModelFormMixin
812

13+
from . import models
914
from .entry_model import FormEntry
10-
from .helpers import get_option
15+
from .helpers import get_option, insert_fields
16+
from .settings import MAIL_TEMPLATE_SETS
1117

1218
_action_registry = {}
1319

@@ -38,16 +44,63 @@ def register(action_class):
3844
return action_class
3945

4046

47+
def unregister(action_class):
48+
hash = hashlib.sha1(action_class.__name__.encode("utf-8")).hexdigest()
49+
if hash in _action_registry:
50+
del _action_registry[hash]
51+
return action_class
52+
53+
4154
def get_action_class(action):
4255
return _action_registry.get(action, None)
4356

4457

45-
class FormAction:
58+
class ActionMixin:
59+
"""Adds action form elements to Form plugin admin"""
60+
def get_form(self, request, *args, **kwargs):
61+
"""Creates new form class based adding the actions as mixins"""
62+
return type(
63+
"FormActionAdminForm",
64+
(self.form, *_action_registry.values()),
65+
{}
66+
)
67+
68+
def get_fieldsets(self, request, obj=None):
69+
fieldsets = super().get_fieldsets(request, obj)
70+
for action in _action_registry.values():
71+
new_fields = list(action.declared_fields.keys())
72+
if new_fields:
73+
hash = hashlib.sha1(action.__name__.encode("utf-8")).hexdigest()
74+
fieldsets = insert_fields(
75+
fieldsets,
76+
new_fields,
77+
block=None,
78+
position=-1,
79+
blockname=action.verbose_name,
80+
blockattrs=dict(classes=(hash, 'action-hide')),
81+
)
82+
return fieldsets
83+
84+
85+
class FormAction(EntangledModelFormMixin):
86+
class Meta:
87+
entangled_fields = {"action_parameters": []}
88+
model = models.Form
89+
exclude = ()
90+
91+
class Media:
92+
js = ("djangocms_form_builder/js/actions_form.js",)
93+
css = {"all": ("djangocms_form_builder/css/actions_form.css",)}
94+
4695
verbose_name = None
4796

4897
def execute(self, form, request):
4998
raise NotImplementedError()
5099

100+
@staticmethod
101+
def get_parameter(form, param):
102+
return (get_option(form, "form_parameters") or {}).get(param, None)
103+
51104

52105
@register
53106
class SaveToDBAction(FormAction):
@@ -90,36 +143,78 @@ def execute(self, form, request):
90143
SAVE_TO_DB_ACTION = next(iter(_action_registry)) if _action_registry else None
91144

92145

146+
def validate_recipients(value):
147+
recipients = value.split()
148+
for recipient in recipients:
149+
EmailValidator(message=_("Please replace \"%s\" by a valid email address.") % recipient)(recipient)
150+
151+
93152
@register
94153
class SendMailAction(FormAction):
95-
verbose_name = _("Send email to administrators")
96-
recipients = None
154+
class Meta:
155+
entangled_fields = {
156+
"action_parameters": [
157+
"sendemail_recipients",
158+
"sendemail_template",
159+
]
160+
}
161+
162+
verbose_name = _("Send email")
97163
from_mail = None
98164
template = "djangocms_form_builder/actions/mail.html"
99165
subject = _("%(form_name)s form submission")
100166

167+
sendemail_recipients = forms.CharField(
168+
label=_("Mail recipients"),
169+
required=False,
170+
initial="",
171+
validators=[
172+
validate_recipients,
173+
],
174+
help_text=_("Space or newline separated list of email addresses."),
175+
widget=forms.Textarea,
176+
)
177+
178+
sendemail_template = forms.ChoiceField(
179+
label=_("Mail template set"),
180+
required=True,
181+
initial=MAIL_TEMPLATE_SETS[0][0],
182+
choices=MAIL_TEMPLATE_SETS,
183+
widget=forms.Select if len(MAIL_TEMPLATE_SETS) > 1 else forms.HiddenInput,
184+
)
185+
101186
def execute(self, form, request):
187+
recipients = (self.get_parameter(form, "sendemail_recipients") or []).split()
188+
template_set = self.get_parameter(form, "sendemail_template") or "default"
102189
context = dict(
103190
cleaned_data=form.cleaned_data,
191+
form_name=getattr(form.Meta, "verbose_name", ""),
104192
user=request.user,
105193
user_agent=request.headers["User-Agent"],
106194
referer=request.headers["Referer"],
107195
)
108-
html_message = render_to_string(self.template, context)
109-
message = strip_tags(html_message)
110-
form_name = form.cleaned_data["form_name"].replace("-", " ").capitalize()
111-
if self.recipients is None:
196+
197+
html_message = render_to_string(f"djangocms_form_builder/mails/{template_set}/mail_html.html", context)
198+
try:
199+
message = render_to_string(f"djangocms_form_builder/mails/{template_set}/mail.txt", context)
200+
except TemplateDoesNotExist:
201+
message = strip_tags(html_message)
202+
try:
203+
subject = render_to_string(f"djangocms_form_builder/mails/{template_set}/subject.txt", context)
204+
except TemplateDoesNotExist:
205+
subject = self.subject % dict(form_name=context["form_name"])
206+
if not recipients:
112207
mail_admins(
113-
self.subject % dict(form_name=form_name),
208+
subject,
114209
message,
115210
fail_silently=True,
116211
html_message=html_message,
117212
)
118213
else:
119214
send_mail(
120-
self.subject % dict(form_name=form_name),
215+
subject,
121216
message,
122-
self.recipients,
217+
recipients,
123218
self.from_mail,
124219
fail_silently=True,
125220
html_message=html_message,

djangocms_form_builder/cms_plugins/ajax_plugins.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from urllib.parse import urlencode
23

34
from cms.plugin_base import CMSPluginBase
@@ -14,6 +15,7 @@
1415
from djangocms_form_builder import settings
1516

1617
from .. import forms, models, recaptcha
18+
from ..actions import ActionMixin
1719
from ..forms import SimpleFrontendForm
1820
from ..helpers import get_option, insert_fields, mark_safe_lazy
1921

@@ -46,7 +48,11 @@ def json_return(self, errors, result, redirect, content):
4648
)
4749

4850
def form_valid(self, form):
51+
# Execute save method
4952
save = getattr(form, "save", None)
53+
if callable(save):
54+
result = form.save()
55+
# Identify redirect
5056
redirect = get_option(form, "redirect", None)
5157
if isinstance(redirect, str):
5258
try:
@@ -56,8 +62,6 @@ def form_valid(self, form):
5662
elif hasattr(redirect, "get_absolute_url"):
5763
redirect = redirect.get_absolute_url()
5864

59-
if callable(save):
60-
form.save()
6165
get_success_context = "get_success_context"
6266
render_success = "render_success"
6367
if hasattr(form, "slug"):
@@ -242,7 +246,7 @@ def render(self, context, instance, placeholder):
242246

243247

244248
@plugin_pool.register_plugin
245-
class FormPlugin(CMSAjaxForm):
249+
class FormPlugin(ActionMixin, CMSAjaxForm):
246250
name = _("Form")
247251
model = models.Form
248252

@@ -267,13 +271,6 @@ class FormPlugin(CMSAjaxForm):
267271
],
268272
},
269273
),
270-
(
271-
_("Actions"),
272-
{
273-
"classes": ("collapse",),
274-
"fields": ["form_actions"],
275-
},
276-
),
277274
]
278275

279276
cache_parent_classes = False
@@ -289,9 +286,19 @@ def get_parent_classes(cls, slot, page, instance=None):
289286
return super().get_parent_classes(slot, page, instance)
290287

291288
def get_fieldsets(self, request, obj=None):
289+
fieldsets = super().get_fieldsets(request, obj)
290+
if obj is None or not obj.form_selection: # No Actions if a Django form has been selected
291+
fieldsets = insert_fields(
292+
fieldsets,
293+
("form_actions",),
294+
block=None,
295+
position=1,
296+
blockname=_("Actions"),
297+
blockattrs={"classes": ("collapse", "action-auto-hide")},
298+
)
292299
if recaptcha.installed:
293300
return insert_fields(
294-
super().get_fieldsets(request, obj),
301+
fieldsets,
295302
("captcha_widget", "captcha_requirement", "captcha_config"),
296303
block=None,
297304
position=1,
@@ -310,9 +317,10 @@ def get_fieldsets(self, request, obj=None):
310317
)
311318
),
312319
)
313-
return super().get_fieldsets(request, obj)
320+
return fieldsets
314321

315322
def get_form_class(self, slug=None):
323+
"""Retrieve or create form for this plugin"""
316324
if self.instance.child_plugin_instances is None: # not set if in ajax_post
317325
self.instance.child_plugin_instances = [
318326
child.get_plugin_instance()[0] for child in self.instance.get_children()
@@ -359,8 +367,14 @@ def traverse(instance):
359367
] = self.instance.placeholder.page # Default behavior: redirect to same page
360368
meta_options["login_required"] = self.instance.form_login_required
361369
meta_options["unique"] = self.instance.form_unique
362-
meta_options["form_actions"] = self.instance.form_actions
363-
fields["Meta"] = type("Meta", (), dict(options=meta_options)) # Meta class
370+
form_actions = self.instance.form_actions or "[]"
371+
meta_options["form_actions"] = json.loads(form_actions.replace("'", '"'))
372+
meta_options["form_parameters"] = getattr(self.instance, "action_parameters", {})
373+
374+
fields["Meta"] = type("Meta", (), dict(
375+
options=meta_options,
376+
verbose_name=self.instance.form_name.replace("-", " ").replace("_", " ").capitalize(),
377+
)) # Meta class with options and verbose name
364378

365379
return type(
366380
"FrontendAutoForm",

0 commit comments

Comments
 (0)