Skip to content
Snippets Groups Projects
blocks.py 5.55 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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 opentech.apply.stream_forms.blocks import FormFieldBlock, OptionalFormFieldBlock, TextFieldBlock
    
    from opentech.apply.utils.options import RICH_TEXT_WIDGET
    
    
    
    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):
    
    
        class Meta:
            label = _('Rich text field')
            icon = 'form'
    
        def get_searchable_content(self, value, data):
    
            return bleach.clean(data or '', tags=[], strip=True)
    
        def no_response(self):
            return '<p>No response</p>'
    
    
    
    class CustomFormFieldsBlock(StreamBlock):
        rich_text = RichTextFieldBlock(group=_('Fields'))
    
    Dan Braghis's avatar
    Dan Braghis committed
        required_blocks = []
    
    
        def __init__(self, *args, **kwargs):
            child_blocks = [(block.name, block(group=_('Required'))) for block in self.required_blocks]
    
            child_blocks += [(block.name, block(group=_('Custom'))) for block in self.single_blocks]
    
            self.required_block_names = [block.name for block in self.required_blocks]
    
            self.single_block_names = [block.name for block in self.single_blocks] + self.required_block_names
    
    
            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.single_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 non duplicate 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):
    
    Dan Braghis's avatar
    Dan Braghis committed
            """
            This allows historic data to still be accessible even
            if a custom field type is removed from the code in the future.
            """
    
            # 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 SingleIncludeStatic(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', (), {})
    
    
    
        def __init__(self, *args, **kwargs):
            info_name = f'{self.name.title()} Field'
    
            child_blocks = [('info', SingleIncludeStatic(label=info_name, description=self.description))]
    
            super().__init__(child_blocks, *args, **kwargs)
    
    
    
    class SingleIncludeBlock(SingleIncludeMixin, OptionalFormFieldBlock):
        """A block that is only allowed to be included once in the form, but is optional"""
    
    
    class MustIncludeFieldBlock(SingleIncludeMixin, 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 get_field_kwargs(self, struct_value):
            kwargs = super().get_field_kwargs(struct_value)
            kwargs['required'] = True
            return kwargs