Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from collections import Counter
import bleach
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _
from django.utils.text import mark_safe
from wagtail.core.blocks import StaticBlock, StreamValue, StreamBlock
from tinymce.widgets import TinyMCE
from opentech.apply.stream_forms.blocks import FormFieldBlock, TextFieldBlock
def find_duplicates(items):
counted = Counter(items)
duplicates = [
name for name, count in counted.items() if count > 1
]
return duplicates
def prettify_names(sequence):
return [nice_field_name(item) for item in sequence]
def nice_field_name(name):
return name.title().replace('_', ' ')
class RichTextFieldBlock(TextFieldBlock):
widget = TinyMCE(mce_attrs={
'elementpath': False,
'branding': False,
'toolbar1': 'undo redo | styleselect | bold italic | bullist numlist | link',
'style_formats': [
{'title': 'Headers', 'items': [
{'title': 'Header 1', 'format': 'h1'},
{'title': 'Header 2', 'format': 'h2'},
{'title': 'Header 3', 'format': 'h3'},
]},
{'title': 'Inline', 'items': [
{'title': 'Bold', 'icon': 'bold', 'format': 'bold'},
{'title': 'Italic', 'icon': 'italic', 'format': 'italic'},
{'title': 'Underline', 'icon': 'underline', 'format': 'underline'},
]},
],
})
class Meta:
label = _('Rich text field')
icon = 'form'
def get_searchable_content(self, value, data):
return bleach.clean(data, tags=[], strip=True)
class CustomFormFieldsBlock(StreamBlock):
rich_text = RichTextFieldBlock(group=_('Fields'))
required_blocks = None
required_block_names = None
def __init__(self, *args, **kwargs):
child_blocks = [(block.name, block(group=_('Required'))) for block in self.required_blocks]
self.required_block_names = [block.name for block in self.required_blocks]
super().__init__(child_blocks, *args, **kwargs)
def clean(self, value):
try:
value = super().clean(value)
except ValidationError as e:
error_dict = e.params
else:
error_dict = dict()
block_types = [block.block_type for block in value]
missing = set(self.required_block_names) - set(block_types)
duplicates = [
name for name in find_duplicates(block_types)
if name in self.required_block_names
]
all_errors = list()
if missing:
all_errors.append(
'You are missing the following required fields: {}'.format(', '.join(prettify_names(missing)))
)
if duplicates:
all_errors.append(
'You have duplicates of the following required fields: {}'.format(', '.join(prettify_names(duplicates)))
)
for i, block_name in enumerate(block_types):
if block_name in duplicates:
self.add_error_to_child(error_dict, i, 'info', 'Duplicate field')
if all_errors or error_dict:
error_dict['__all__'] = all_errors
raise ValidationError('Error', params=error_dict)
return value
def add_error_to_child(self, errors, child_number, field, message):
new_error = ErrorList([message])
try:
errors[child_number].data[0].params[field] = new_error
except KeyError:
errors[child_number] = ErrorList(
[ValidationError('Error', params={field: new_error})]
)
def to_python(self, value):
# If the data type is missing, fallback to a CharField
for child_data in value:
if child_data['type'] not in self.child_blocks:
child_data['type'] = 'char'
return StreamValue(self, value, is_lazy=True)
class MustIncludeStatic(StaticBlock):
"""Helper block which displays additional information about the must include block and
helps display the error in a noticeable way.
"""
def __init__(self, *args, description='', **kwargs):
self.description = description
super().__init__(*args, **kwargs)
class Meta:
admin_text = 'Must be included in the form only once.'
def render_form(self, *args, **kwargs):
errors = kwargs.pop('errors')
if errors:
# Pretend the error is a readonly input so that we get nice formatting
# Issue discussed here: https://github.com/wagtail/wagtail/issues/4122
error_message = '<div class="error"><input readonly placeholder="{}"></div>'.format(errors[0])
else:
error_message = ''
form = super().render_form(*args, **kwargs)
form = '<br>'.join([self.description, form]) + error_message
return mark_safe(form)
def deconstruct(self):
return ('wagtail.core.blocks.static_block.StaticBlock', (), {})
class MustIncludeFieldBlock(FormFieldBlock):
"""Any block inheriting from this will need to be included in the application forms
This data will also be available to query on the submission object
"""
def __init__(self, *args, **kwargs):
info_name = f'{self.name.title()} Field'
child_blocks = [('info', MustIncludeStatic(label=info_name, description=self.description))]
super().__init__(child_blocks, *args, **kwargs)
def get_field_kwargs(self, struct_value):
kwargs = super().get_field_kwargs(struct_value)
kwargs['required'] = True
return kwargs
REQUIRED_BLOCK_NAMES = [block.name for block in MustIncludeFieldBlock.__subclasses__()]