Skip to content

Commit d74d6d6

Browse files
authored
fix: prevent duplicate submit buttons in forms (#27)
* fix: prevent duplicate submit buttons in forms * perf: optimize submit button check to avoid extra DB query * test: add test for indirect children * fix: handle submit button rendering * fix: remove unnecessary check * fix: add test app for testing nested plugins * fix: remove unnecessary files
1 parent a4889f5 commit d74d6d6

File tree

8 files changed

+223
-20
lines changed

8 files changed

+223
-20
lines changed

djangocms_form_builder/cms_plugins/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
EmailFieldPlugin,
1111
IntegerFieldPlugin,
1212
SelectPlugin,
13+
SubmitPlugin,
1314
TextareaPlugin,
1415
TimeFieldPlugin,
1516
URLFieldPlugin,
@@ -26,6 +27,7 @@
2627
"EmailFieldPlugin",
2728
"IntegerFieldPlugin",
2829
"SelectPlugin",
30+
"SubmitPlugin",
2931
"TextareaPlugin",
3032
"TimeFieldPlugin",
3133
"URLFieldPlugin",

djangocms_form_builder/cms_plugins/ajax_plugins.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,24 @@ def render(self, context, instance, placeholder):
240240
form = self.get_ajax_form()
241241
context.update(self.set_context(context, instance, placeholder))
242242
context["form_counter"] = context.get("form_counter", 0) + 1
243-
context.update({
244-
"instance": instance,
245-
"form": form,
246-
"uid": f"{instance.id}{getattr(form, 'slug', '')}-{context['form_counter']}",
247-
})
243+
244+
def has_submit_button(plugins):
245+
for child in plugins:
246+
if child.plugin_type == "SubmitPlugin":
247+
return True
248+
child_plugins = getattr(child, "child_plugin_instances", None) or []
249+
if has_submit_button(child_plugins):
250+
return True
251+
return False
252+
253+
context.update(
254+
{
255+
"instance": instance,
256+
"form": form,
257+
"uid": f"{instance.id}{getattr(form, 'slug', '')}-{context['form_counter']}",
258+
"has_submit_button": has_submit_button(instance.child_plugin_instances),
259+
}
260+
)
248261
return context
249262

250263

@@ -290,7 +303,9 @@ def get_parent_classes(cls, slot, page, instance=None):
290303

291304
def get_fieldsets(self, request, obj=None):
292305
fieldsets = super().get_fieldsets(request, obj)
293-
if obj is None or not obj.form_selection: # No Actions if a Django form has been selected
306+
if (
307+
obj is None or not obj.form_selection
308+
): # No Actions if a Django form has been selected
294309
fieldsets = insert_fields(
295310
fieldsets,
296311
("form_actions",),
@@ -362,22 +377,28 @@ def traverse(instance):
362377
meta_options = dict(form_name=self.instance.form_name)
363378
if self.instance.form_floating_labels:
364379
meta_options["floating_labels"] = True
365-
meta_options[
366-
"field_sep"
367-
] = f'{self.instance.form_spacing}'
368-
meta_options[
369-
"redirect"
370-
] = SAME_PAGE_REDIRECT # Default behavior: redirect to same page
380+
meta_options["field_sep"] = f"{self.instance.form_spacing}"
381+
meta_options["redirect"] = (
382+
SAME_PAGE_REDIRECT # Default behavior: redirect to same page
383+
)
371384
meta_options["login_required"] = self.instance.form_login_required
372385
meta_options["unique"] = self.instance.form_unique
373386
form_actions = self.instance.form_actions or "[]"
374387
meta_options["form_actions"] = json.loads(form_actions.replace("'", '"'))
375-
meta_options["form_parameters"] = getattr(self.instance, "action_parameters", {})
388+
meta_options["form_parameters"] = getattr(
389+
self.instance, "action_parameters", {}
390+
)
376391

377-
fields["Meta"] = type("Meta", (), dict(
378-
options=meta_options,
379-
verbose_name=self.instance.form_name.replace("-", " ").replace("_", " ").capitalize(),
380-
)) # Meta class with options and verbose name
392+
fields["Meta"] = type(
393+
"Meta",
394+
(),
395+
dict(
396+
options=meta_options,
397+
verbose_name=self.instance.form_name.replace("-", " ")
398+
.replace("_", " ")
399+
.capitalize(),
400+
),
401+
) # Meta class with options and verbose name
381402

382403
return type(
383404
"FrontendAutoForm",

djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/form.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
method="post">
88
{% csrf_token %}
99
{% include 'djangocms_form_builder/ajax_form.html' with form=form instance=instance tracking=instance.tracking_code RECAPTCHA_PUBLIC_KEY=RECAPTCHA_PUBLIC_KEY %}
10-
<input type="submit" value="{{ instance.form_submit_message|default:_("Submit") }}"
11-
class="btn btn-{{ instance.form_submit_context|default:"primary" }}">
10+
{% if not has_submit_button %}
11+
<input type="submit" value="{{ instance.form_submit_message|default:_("Submit") }}"
12+
class="btn btn-{{ instance.form_submit_context|default:"primary" }}">
13+
{% endif %}
1214
</form><div class="clearfix"></div>
1315
{% endif %}
1416
{% endspaceless %}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<input type="submit" value="{{ instance.form_submit_message|default:_("Submit") }}" class="btn btn-{{ instance.form_submit_context|default:"primary" }}
1+
<input type="submit" value="{{ instance.config.submit_cta|default:_("Submit") }}" class="btn btn-{{ instance.form_submit_context|default:"primary" }}">

tests/test_app/cms_plugins.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from cms.plugin_base import CMSPluginBase
2+
from cms.plugin_pool import plugin_pool
3+
from django.utils.translation import gettext_lazy as _
4+
5+
6+
@plugin_pool.register_plugin
7+
class ContainerPlugin(CMSPluginBase):
8+
name = _("Container")
9+
render_template = "test_app/container.html"
10+
allow_children = True
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{% load cms_tags %}
2+
<div class="test-container">
3+
{% for plugin in instance.child_plugin_instances %}
4+
{% render_plugin plugin %}
5+
{% endfor %}
6+
</div>

tests/test_formeditor.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from djangocms_form_builder import cms_plugins
77
from djangocms_form_builder.cms_plugins.form_plugins import FormElementPlugin
8+
from tests.test_app.cms_plugins import ContainerPlugin
89

910
from .fixtures import TestFixture
1011

@@ -24,6 +25,7 @@ def test_form_editor(self):
2425
inspect.isclass(cls)
2526
and issubclass(cls, FormElementPlugin)
2627
and not issubclass(cls, cms_plugins.ChoicePlugin)
28+
and cls is not cms_plugins.SubmitPlugin
2729
):
2830
field = add_plugin(
2931
placeholder=self.placeholder,
@@ -48,5 +50,153 @@ def test_form_editor(self):
4850
inspect.isclass(cls)
4951
and issubclass(cls, FormElementPlugin)
5052
and not issubclass(cls, cms_plugins.ChoicePlugin)
53+
and cls is not cms_plugins.SubmitPlugin
5154
):
5255
self.assertContains(response, f'name="field_{item}"')
56+
57+
def test_auto_submit_button_appears_when_no_button(self):
58+
form = add_plugin(
59+
placeholder=self.placeholder,
60+
plugin_type=cms_plugins.FormPlugin.__name__,
61+
language=self.language,
62+
form_selection="",
63+
form_name="test-form",
64+
)
65+
add_plugin(
66+
placeholder=self.placeholder,
67+
plugin_type=cms_plugins.CharFieldPlugin.__name__,
68+
target=form,
69+
language=self.language,
70+
config=dict(
71+
field_name="text_field",
72+
),
73+
)
74+
self.publish(self.page, self.language)
75+
with self.login_user_context(self.superuser):
76+
response = self.client.get(self.request_url)
77+
78+
self.assertEqual(response.status_code, 200)
79+
self.assertEqual(response.content.decode().count('type="submit"'), 1)
80+
self.assertIn('value="Submit"', response.content.decode())
81+
82+
def test_auto_submit_button_does_not_appear_when_button_exists(self):
83+
form = add_plugin(
84+
placeholder=self.placeholder,
85+
plugin_type=cms_plugins.FormPlugin.__name__,
86+
language=self.language,
87+
form_selection="",
88+
form_name="test-form",
89+
)
90+
add_plugin(
91+
placeholder=self.placeholder,
92+
plugin_type=cms_plugins.CharFieldPlugin.__name__,
93+
target=form,
94+
language=self.language,
95+
config=dict(
96+
field_name="text_field",
97+
),
98+
)
99+
add_plugin(
100+
placeholder=self.placeholder,
101+
plugin_type=cms_plugins.SubmitPlugin.__name__,
102+
target=form,
103+
language=self.language,
104+
config=dict(
105+
submit_cta="Submit Form",
106+
),
107+
)
108+
self.publish(self.page, self.language)
109+
with self.login_user_context(self.superuser):
110+
response = self.client.get(self.request_url)
111+
112+
self.assertEqual(response.status_code, 200)
113+
content = response.content.decode()
114+
self.assertEqual(content.count('type="submit"'), 1)
115+
self.assertIn('value="Submit Form"', content)
116+
self.assertNotIn('value="Submit"', content)
117+
118+
def test_auto_submit_button_does_not_appear_with_nested_button(self):
119+
form = add_plugin(
120+
placeholder=self.placeholder,
121+
plugin_type=cms_plugins.FormPlugin.__name__,
122+
language=self.language,
123+
form_selection="",
124+
form_name="test-form",
125+
)
126+
add_plugin(
127+
placeholder=self.placeholder,
128+
plugin_type=cms_plugins.CharFieldPlugin.__name__,
129+
target=form,
130+
language=self.language,
131+
config=dict(
132+
field_name="text_field",
133+
),
134+
)
135+
container = add_plugin(
136+
placeholder=self.placeholder,
137+
plugin_type=ContainerPlugin.__name__,
138+
target=form,
139+
language=self.language,
140+
)
141+
nested_container = add_plugin(
142+
placeholder=self.placeholder,
143+
plugin_type=ContainerPlugin.__name__,
144+
target=container,
145+
language=self.language,
146+
)
147+
add_plugin(
148+
placeholder=self.placeholder,
149+
plugin_type=cms_plugins.SubmitPlugin.__name__,
150+
target=nested_container,
151+
language=self.language,
152+
config=dict(
153+
submit_cta="Submit Form",
154+
),
155+
)
156+
self.publish(self.page, self.language)
157+
with self.login_user_context(self.superuser):
158+
response = self.client.get(self.request_url)
159+
160+
self.assertEqual(response.status_code, 200)
161+
content = response.content.decode()
162+
self.assertEqual(content.count('type="submit"'), 1)
163+
self.assertIn('value="Submit Form"', content)
164+
self.assertNotIn('value="Submit"', content)
165+
166+
def test_auto_submit_button_appears_with_empty_nested_containers(self):
167+
form = add_plugin(
168+
placeholder=self.placeholder,
169+
plugin_type=cms_plugins.FormPlugin.__name__,
170+
language=self.language,
171+
form_selection="",
172+
form_name="test-form",
173+
)
174+
add_plugin(
175+
placeholder=self.placeholder,
176+
plugin_type=cms_plugins.CharFieldPlugin.__name__,
177+
target=form,
178+
language=self.language,
179+
config=dict(
180+
field_name="text_field",
181+
),
182+
)
183+
container = add_plugin(
184+
placeholder=self.placeholder,
185+
plugin_type=ContainerPlugin.__name__,
186+
target=form,
187+
language=self.language,
188+
)
189+
add_plugin(
190+
placeholder=self.placeholder,
191+
plugin_type=ContainerPlugin.__name__,
192+
target=container,
193+
language=self.language,
194+
)
195+
self.publish(self.page, self.language)
196+
with self.login_user_context(self.superuser):
197+
response = self.client.get(self.request_url)
198+
199+
self.assertEqual(response.status_code, 200)
200+
content = response.content.decode()
201+
self.assertEqual(content.count('type="submit"'), 1)
202+
self.assertIn('value="Submit"', content)

tests/test_settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
from cms.utils.compat import DJANGO_3_1
44

5+
6+
class DisableMigrations(dict):
7+
def __contains__(self, item):
8+
return True
9+
10+
def __getitem__(self, item):
11+
return None
12+
13+
14+
MIGRATION_MODULES = DisableMigrations()
15+
516
INSTALLED_APPS = [
617
"django.contrib.contenttypes",
718
"django.contrib.auth",
@@ -17,6 +28,7 @@
1728
"djangocms_text_ckeditor",
1829
"djangocms_form_builder",
1930
"sekizai",
31+
"tests.test_app",
2032
]
2133

2234
if DJANGO_3_1:

0 commit comments

Comments
 (0)