wagtail-flexible-forms-hg 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2018, NoriPyt
4
+ Copyright (c) 2025, CodeRed LLC
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ * Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ * Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: wagtail-flexible-forms-hg
3
+ Version: 2.2.0
4
+ Summary: Wagtail StreamField-based form builder.
5
+ Author: Bertrand Bordage
6
+ Author-email: CodeRed LLC <info@coderedcorp.com>
7
+ License: BSD 3-Clause License
8
+
9
+ Copyright (c) 2018, NoriPyt
10
+ Copyright (c) 2025, CodeRed LLC
11
+ All rights reserved.
12
+
13
+ Redistribution and use in source and binary forms, with or without
14
+ modification, are permitted provided that the following conditions are met:
15
+
16
+ * Redistributions of source code must retain the above copyright notice, this
17
+ list of conditions and the following disclaimer.
18
+
19
+ * Redistributions in binary form must reproduce the above copyright notice,
20
+ this list of conditions and the following disclaimer in the documentation
21
+ and/or other materials provided with the distribution.
22
+
23
+ * Neither the name of the copyright holder nor the names of its
24
+ contributors may be used to endorse or promote products derived from
25
+ this software without specific prior written permission.
26
+
27
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
31
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
32
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
33
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
34
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
35
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37
+
38
+ Project-URL: Source, https://github.com/happygrizzly/wagtail-flexible-forms
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: License :: OSI Approved :: BSD License
41
+ Classifier: Natural Language :: English
42
+ Classifier: Operating System :: OS Independent
43
+ Classifier: Programming Language :: Python :: 3
44
+ Classifier: Programming Language :: Python :: 3 :: Only
45
+ Classifier: Framework :: Django
46
+ Classifier: Framework :: Wagtail
47
+ Classifier: Framework :: Wagtail :: 6
48
+ Classifier: Framework :: Wagtail :: 7
49
+ Classifier: Framework :: Wagtail :: 8
50
+ Requires-Python: >=3.9
51
+ Description-Content-Type: text/markdown
52
+ License-File: LICENSE.txt
53
+ Requires-Dist: wagtail<8.2,>=6
54
+ Dynamic: license-file
55
+
56
+ Wagtail Flexible Forms
57
+ ======================
58
+
59
+ Wagtail flexible forms provides a more robust StreamField-based form builder (sometimes called a "StreamForm") with similar functionality to `wagtail.contrib.forms`.
60
+
61
+
62
+ Installation & Usage
63
+ --------------------
64
+
65
+ Read the [documentation](https://docs.coderedcorp.com/wagtail-flexible-forms/), which includes a video demo and example project.
66
+
67
+
68
+ History & Future of This Project
69
+ --------------------------------
70
+
71
+ This project was [originally developed by Bertrand Bordage of NoriPyt](https://www.kickstarter.com/projects/noripyt/wagtails-first-hatch) with the intention of being merged into Wagtail as `wagtail.contrib.stream_forms`.
72
+
73
+ In 2019, CodeRed used the code as part of Wagtail CRX (`coderedcms`) to and began providing ongoing maintenance as part of that package.
74
+
75
+ In 2025, with renewed interest from the Wagtail community and the Wagtail core team, CodeRed took ownership of the repository and backported the vendored code from `coderedcms` to produce a stable version of `wagtail-flexible-forms` as its own pip package.
76
+
77
+ Stay tuned as we work to add more documentation, examples, and unit tests with the goal of eventually merging the package into Wagtail as the official "stream forms" implementation. It is a fantastic package and we are excited to be investing in it to share with the Wagtail community!
@@ -0,0 +1,22 @@
1
+ Wagtail Flexible Forms
2
+ ======================
3
+
4
+ Wagtail flexible forms provides a more robust StreamField-based form builder (sometimes called a "StreamForm") with similar functionality to `wagtail.contrib.forms`.
5
+
6
+
7
+ Installation & Usage
8
+ --------------------
9
+
10
+ Read the [documentation](https://docs.coderedcorp.com/wagtail-flexible-forms/), which includes a video demo and example project.
11
+
12
+
13
+ History & Future of This Project
14
+ --------------------------------
15
+
16
+ This project was [originally developed by Bertrand Bordage of NoriPyt](https://www.kickstarter.com/projects/noripyt/wagtails-first-hatch) with the intention of being merged into Wagtail as `wagtail.contrib.stream_forms`.
17
+
18
+ In 2019, CodeRed used the code as part of Wagtail CRX (`coderedcms`) to and began providing ongoing maintenance as part of that package.
19
+
20
+ In 2025, with renewed interest from the Wagtail community and the Wagtail core team, CodeRed took ownership of the repository and backported the vendored code from `coderedcms` to produce a stable version of `wagtail-flexible-forms` as its own pip package.
21
+
22
+ Stay tuned as we work to add more documentation, examples, and unit tests with the goal of eventually merging the package into Wagtail as the official "stream forms" implementation. It is a fantastic package and we are excited to be investing in it to share with the Wagtail community!
@@ -0,0 +1,79 @@
1
+ # -- PACKAGE INFO ---------------------
2
+
3
+ [build-system]
4
+ requires = ["setuptools >= 65.0"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ authors = [
9
+ {name = "CodeRed LLC", email = "info@coderedcorp.com"},
10
+ {name = "Bertrand Bordage"},
11
+ ]
12
+ classifiers = [
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: BSD License",
15
+ "Natural Language :: English",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Framework :: Django",
20
+ "Framework :: Wagtail",
21
+ "Framework :: Wagtail :: 6",
22
+ "Framework :: Wagtail :: 7",
23
+ "Framework :: Wagtail :: 8",
24
+ ]
25
+ dependencies = [
26
+ "wagtail>=6,<8.2",
27
+ ]
28
+ description = "Wagtail StreamField-based form builder."
29
+ dynamic = ["version"]
30
+ license = {file = "LICENSE.txt"}
31
+ name = "wagtail-flexible-forms-hg"
32
+ readme = "README.md"
33
+ requires-python = ">=3.9"
34
+
35
+ [project.urls]
36
+ Source = "https://github.com/happygrizzly/wagtail-flexible-forms"
37
+
38
+ [tool.setuptools]
39
+ packages = ["wagtail_flexible_forms"]
40
+
41
+ [tool.setuptools.dynamic]
42
+ version = {attr = "wagtail_flexible_forms.__version__"}
43
+
44
+
45
+ # -- TOOLS ----------------------------
46
+
47
+ [tool.codespell]
48
+ ignore-words-list = ["doubleclick"]
49
+
50
+ [tool.django-stubs]
51
+ django_settings_module = "testproject.settings"
52
+
53
+ [tool.mypy]
54
+ ignore_missing_imports = true
55
+ check_untyped_defs = true
56
+ exclude = [
57
+ '^\..*',
58
+ 'migrations',
59
+ 'node_modules',
60
+ 'venv',
61
+ ]
62
+
63
+ [tool.pytest.ini_options]
64
+ DJANGO_SETTINGS_MODULE = "testproject.settings"
65
+ junit_family = "xunit2"
66
+ addopts = "--cov wagtail-flexible-forms --cov-report html --cov-report xml --junitxml junit/test-results.xml"
67
+ python_files = "tests.py test_*.py"
68
+
69
+ [tool.ruff]
70
+ extend-exclude = ["build", "migrations"]
71
+ line-length = 80
72
+
73
+ [tool.ruff.lint]
74
+ extend-select = ["I"]
75
+
76
+ [tool.ruff.lint.isort]
77
+ case-sensitive = false
78
+ force-single-line = true
79
+ lines-after-imports = 2
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0"
@@ -0,0 +1,288 @@
1
+ from anyascii import anyascii
2
+ from django import forms
3
+ from django.db.models import BLANK_CHOICE_DASH
4
+ from django.utils.dateparse import parse_datetime
5
+ from django.utils.text import slugify
6
+ from django.utils.translation import gettext_lazy as _
7
+ from wagtail.blocks import BooleanBlock
8
+ from wagtail.blocks import CharBlock
9
+ from wagtail.blocks import ChoiceBlock
10
+ from wagtail.blocks import DateBlock
11
+ from wagtail.blocks import DateTimeBlock
12
+ from wagtail.blocks import ListBlock
13
+ from wagtail.blocks import RichTextBlock
14
+ from wagtail.blocks import StreamBlock
15
+ from wagtail.blocks import StructBlock
16
+ from wagtail.blocks import TextBlock
17
+ from wagtail.blocks import TimeBlock
18
+
19
+
20
+ class FormFieldBlock(StructBlock):
21
+ field_label = CharBlock(label=_("Label"))
22
+ help_text = TextBlock(required=False, label=_("Help text"))
23
+
24
+ field_class = forms.CharField
25
+ widget = None
26
+
27
+ def get_slug(self, struct_value):
28
+ return slugify(anyascii(struct_value["field_label"]))
29
+
30
+ def get_field_class(self, struct_value):
31
+ return self.field_class
32
+
33
+ def get_widget(self, struct_value):
34
+ return self.widget
35
+
36
+ def get_field_kwargs(self, struct_value):
37
+ kwargs = {
38
+ "label": struct_value["field_label"],
39
+ "help_text": struct_value["help_text"],
40
+ "required": struct_value.get("required", False),
41
+ }
42
+ if "default_value" in struct_value:
43
+ kwargs["initial"] = struct_value["default_value"]
44
+ form_widget = self.get_widget(struct_value)
45
+ if form_widget is not None:
46
+ kwargs["widget"] = form_widget
47
+ return kwargs
48
+
49
+ def get_field(self, struct_value):
50
+ return self.get_field_class(struct_value)(
51
+ **self.get_field_kwargs(struct_value)
52
+ )
53
+
54
+
55
+ class OptionalFormFieldBlock(FormFieldBlock):
56
+ required = BooleanBlock(label=_("Required"), required=False)
57
+
58
+
59
+ CHARFIELD_FORMATS = [
60
+ ("email", _("Email")),
61
+ ("url", _("URL")),
62
+ ]
63
+ try:
64
+ from phonenumber_field.formfields import PhoneNumberField
65
+ except ImportError:
66
+ pass
67
+ else:
68
+ CHARFIELD_FORMATS.append(("phone", _("Phone")))
69
+
70
+
71
+ class CharFieldBlock(OptionalFormFieldBlock):
72
+ format = ChoiceBlock(
73
+ choices=CHARFIELD_FORMATS, required=False, label=_("Format")
74
+ )
75
+ default_value = CharBlock(required=False, label=_("Default value"))
76
+
77
+ class Meta:
78
+ label = _("Text field (single line)")
79
+ icon = "pilcrow"
80
+
81
+ def get_field_class(self, struct_value):
82
+ text_format = struct_value["format"]
83
+ if text_format == "url":
84
+ return forms.URLField
85
+ if text_format == "email":
86
+ return forms.EmailField
87
+ if text_format == "phone":
88
+ return PhoneNumberField
89
+ return super().get_field_class(struct_value)
90
+
91
+
92
+ class TextFieldBlock(OptionalFormFieldBlock):
93
+ default_value = TextBlock(required=False, label=_("Default value"))
94
+
95
+ widget = forms.Textarea(attrs={"rows": 5})
96
+
97
+ class Meta:
98
+ label = _("Text field (multi line)")
99
+ icon = "pilcrow"
100
+
101
+
102
+ class NumberFieldBlock(OptionalFormFieldBlock):
103
+ default_value = CharBlock(required=False, label=_("Default value"))
104
+
105
+ widget = forms.NumberInput
106
+
107
+ class Meta:
108
+ label = _("Number field")
109
+ icon = "decimal"
110
+
111
+
112
+ class CheckboxFieldBlock(FormFieldBlock):
113
+ default_value = BooleanBlock(required=False)
114
+
115
+ field_class = forms.BooleanField
116
+
117
+ class Meta:
118
+ label = _("Checkbox field")
119
+ icon = "tick-inverse"
120
+
121
+
122
+ class RadioButtonsFieldBlock(OptionalFormFieldBlock):
123
+ choices = ListBlock(CharBlock(label=_("Choice")))
124
+
125
+ field_class = forms.ChoiceField
126
+ widget = forms.RadioSelect
127
+
128
+ class Meta:
129
+ label = _("Radio buttons")
130
+ icon = "radio-empty"
131
+
132
+ def get_field_kwargs(self, struct_value):
133
+ kwargs = super().get_field_kwargs(struct_value)
134
+ kwargs["choices"] = [
135
+ (choice, choice) for choice in struct_value["choices"]
136
+ ]
137
+ return kwargs
138
+
139
+
140
+ class DropdownFieldBlock(RadioButtonsFieldBlock):
141
+ widget = forms.Select
142
+
143
+ class Meta:
144
+ label = _("Dropdown field")
145
+ icon = "list-ul"
146
+
147
+ def get_field_kwargs(self, struct_value):
148
+ kwargs = super(DropdownFieldBlock, self).get_field_kwargs(struct_value)
149
+ kwargs["choices"].insert(0, BLANK_CHOICE_DASH[0])
150
+ return kwargs
151
+
152
+
153
+ class CheckboxesFieldBlock(OptionalFormFieldBlock):
154
+ checkboxes = ListBlock(CharBlock(label=_("Checkbox")))
155
+
156
+ field_class = forms.MultipleChoiceField
157
+ widget = forms.CheckboxSelectMultiple
158
+
159
+ class Meta:
160
+ label = _("Multiple checkboxes field")
161
+ icon = "tasks"
162
+
163
+ def get_field_kwargs(self, struct_value):
164
+ kwargs = super(CheckboxesFieldBlock, self).get_field_kwargs(
165
+ struct_value
166
+ )
167
+ kwargs["choices"] = [
168
+ (choice, choice) for choice in struct_value["checkboxes"]
169
+ ]
170
+ return kwargs
171
+
172
+
173
+ class DatePickerInput(forms.DateInput):
174
+ def __init__(self, *args, **kwargs):
175
+ attrs = kwargs.get("attrs")
176
+ if attrs is None:
177
+ attrs = {}
178
+ attrs.update(
179
+ {
180
+ "data-provide": "datepicker",
181
+ "data-date-format": "yyyy-mm-dd",
182
+ }
183
+ )
184
+ kwargs["attrs"] = attrs
185
+ super().__init__(*args, **kwargs)
186
+
187
+
188
+ class DateFieldBlock(OptionalFormFieldBlock):
189
+ default_value = DateBlock(required=False)
190
+
191
+ field_class = forms.DateField
192
+ widget = DatePickerInput
193
+
194
+ class Meta:
195
+ label = _("Date field")
196
+ icon = "date"
197
+
198
+
199
+ class HTML5TimeInput(forms.TimeInput):
200
+ input_type = "time"
201
+
202
+
203
+ class TimeFieldBlock(OptionalFormFieldBlock):
204
+ default_value = TimeBlock(required=False)
205
+
206
+ field_class = forms.TimeField
207
+ widget = HTML5TimeInput
208
+
209
+ class Meta:
210
+ label = _("Time field")
211
+ icon = "time"
212
+
213
+
214
+ class DateTimePickerInput(forms.SplitDateTimeWidget):
215
+ def __init__(self, attrs=None, date_format=None, time_format=None):
216
+ super().__init__(
217
+ attrs=attrs, date_format=date_format, time_format=time_format
218
+ )
219
+ self.widgets = (
220
+ DatePickerInput(attrs=attrs, format=date_format),
221
+ HTML5TimeInput(attrs=attrs, format=time_format),
222
+ )
223
+
224
+ def decompress(self, value):
225
+ if isinstance(value, str):
226
+ value = parse_datetime(value)
227
+ return super().decompress(value)
228
+
229
+
230
+ class DateTimeFieldBlock(OptionalFormFieldBlock):
231
+ default_value = DateTimeBlock(required=False)
232
+
233
+ field_class = forms.SplitDateTimeField
234
+ widget = DateTimePickerInput
235
+
236
+ class Meta:
237
+ label = _("Date+time field")
238
+ icon = "date"
239
+
240
+
241
+ class ImageFieldBlock(OptionalFormFieldBlock):
242
+ field_class = forms.ImageField
243
+
244
+ class Meta:
245
+ label = _("Image field")
246
+ icon = "image"
247
+
248
+
249
+ class FileFieldBlock(OptionalFormFieldBlock):
250
+ field_class = forms.FileField
251
+
252
+ class Meta:
253
+ label = _("File field")
254
+ icon = "upload"
255
+
256
+
257
+ class FormFieldsBlock(StreamBlock):
258
+ char = CharFieldBlock(group=_("Fields"))
259
+ text = TextFieldBlock(group=_("Fields"))
260
+ number = NumberFieldBlock(group=_("Fields"))
261
+ checkbox = CheckboxFieldBlock(group=_("Fields"))
262
+ radios = RadioButtonsFieldBlock(group=_("Fields"))
263
+ dropdown = DropdownFieldBlock(group=_("Fields"))
264
+ checkboxes = CheckboxesFieldBlock(group=_("Fields"))
265
+ date = DateFieldBlock(group=_("Fields"))
266
+ time = TimeFieldBlock(group=_("Fields"))
267
+ datetime = DateTimeFieldBlock(group=_("Fields"))
268
+ image = ImageFieldBlock(group=_("Fields"))
269
+ file = FileFieldBlock(group=_("Fields"))
270
+ text_markup = RichTextBlock(group=_("Other"))
271
+
272
+ class Meta:
273
+ label = _("Form fields")
274
+
275
+
276
+ class FormStepBlock(StructBlock):
277
+ name = CharBlock(label=_("Name"), required=False)
278
+ form_fields = FormFieldsBlock()
279
+
280
+ class Meta:
281
+ label = _("Form step")
282
+
283
+
284
+ class FormStepsBlock(StreamBlock):
285
+ step = FormStepBlock()
286
+
287
+ class Meta:
288
+ label = _("Form steps")
@@ -0,0 +1,35 @@
1
+ from django.template.loader import render_to_string
2
+ from django.utils.safestring import mark_safe
3
+ from django.utils.translation import gettext as _
4
+ from wagtail.admin.panels import EditHandler
5
+
6
+
7
+ class FormSubmissionsPanel(EditHandler):
8
+ template = "wagtailforms/edit_handlers/form_responses_panel.html"
9
+
10
+ def bind_to(self, model=None, instance=None, request=None, form=None):
11
+ new = super().bind_to(model=model)
12
+ if self.heading is None:
13
+ new.heading = _("{} submissions").format(model.get_verbose_name())
14
+ return new
15
+
16
+ def render(self):
17
+ Submission = self.model.get_submission_class()
18
+ submissions = Submission.objects.filter(page=self.instance)
19
+ submission_count = submissions.count()
20
+
21
+ if not submission_count:
22
+ return ""
23
+
24
+ return mark_safe(
25
+ render_to_string(
26
+ self.template,
27
+ {
28
+ "self": self,
29
+ "submission_count": submission_count,
30
+ "last_submit_time": (
31
+ submissions.order_by("submit_time").last().submit_time
32
+ ),
33
+ },
34
+ )
35
+ )