import re
from html import escape
from onegov.form import errors
from onegov.form.core import FieldDependency
from onegov.form.core import Form
from onegov.form.fields import (
MultiCheckboxField, DateTimeLocalField, VideoURLField)
from onegov.form.fields import TimeField, UploadField, UploadMultipleField
from onegov.form.parser.core import parse_formcode
from onegov.form.utils import as_internal_id
from onegov.form.validators import LaxDataRequired
from onegov.form.validators import ExpectedExtensions
from onegov.form.validators import FileSizeLimit
from onegov.form.validators import If
from onegov.form.validators import Stdnum
from onegov.form.validators import StrictOptional
from onegov.form.validators import ValidDateRange
from onegov.form.widgets import DateRangeInput
from onegov.form.widgets import DateTimeLocalRangeInput
from wtforms.fields import DateField
from wtforms.fields import DecimalField
from wtforms.fields import EmailField
from wtforms.fields import IntegerField
from wtforms.fields import PasswordField
from wtforms.fields import RadioField
from wtforms.fields import StringField
from wtforms.fields import TextAreaField
from wtforms.fields import URLField
from wtforms.validators import Email
from wtforms.validators import Length
from wtforms.validators import NumberRange
from wtforms.validators import Regexp
from wtforms.validators import URL
from typing import overload, Any, Generic, TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from onegov.form.parser.core import ParsedField
from onegov.form.types import PricingRules, Validator, Widget
from wtforms import Field as WTField
# increasing the default filesize is *strongly discouarged*, as we are not
# storing those files in the database, so they need to fit in memory
#
# if this value must be higher, we need to store the files outside the
# database
#
[docs]
DEFAULT_UPLOAD_LIMIT = 50 * MEGABYTE
@overload
@overload
def parse_form(
text: str,
enable_edit_checks: bool = False,
*,
base_class: type[_FormT]
) -> type[_FormT]: ...
@overload
def parse_form(
text: str,
enable_edit_checks: bool = False,
base_class: type[Form] = Form
) -> type[Form]: ...
def parse_form(
text: str,
enable_edit_checks: bool = False,
base_class: type[Form] = Form
) -> type[Form]:
""" Takes the given form text, parses it and returns a WTForms form
class (not an instance of it).
:type text: string form text to be parsed
:param enable_edit_checks: bool to activate additional checks after
editing a form.
:param base_class: Form base class
"""
builder = WTFormsClassBuilder(base_class)
for fieldset in parse_formcode(text, enable_edit_checks):
builder.set_current_fieldset(fieldset.label)
for field in fieldset.fields:
handle_field(builder, field)
form_class = builder.form_class
form_class._source = text
return form_class
[docs]
def normalize_label_for_dependency(label: str) -> str:
""" Removes all between '(' and ')' Parentheses (inclusive) """
if '(' in label and ')' in label:
label = re.sub(r'([(]).*?([)])', '', label)
return label[:-1] if label[-1] == ' ' else label
else:
return label
[docs]
def handle_field(
builder: 'WTFormsClassBuilder[Any]',
field: 'ParsedField',
dependency: FieldDependency | None = None
) -> None:
""" Takes the given parsed field and adds it to the form. """
validators: list[Validator[Any, Any]]
widget: Widget[Any] | None
if field.type == 'text':
render_kw = None
if field.maxlength:
validators = [Length(max=field.maxlength)]
render_kw = {'data-max-length': field.maxlength}
else:
validators = []
if field.regex:
validators.append(Regexp(field.regex))
builder.add_field(
field_class=StringField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=validators,
render_kw=render_kw,
description=field.field_help
)
elif field.type == 'textarea':
builder.add_field(
field_class=TextAreaField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
render_kw={'rows': field.rows} if field.rows else None,
description=field.field_help
)
elif field.type == 'password':
builder.add_field(
field_class=PasswordField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
description=field.field_help
)
elif field.type == 'email':
builder.add_field(
field_class=EmailField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[Email()],
description=field.field_help
)
elif field.type == 'url':
builder.add_field(
field_class=URLField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[URL()],
description=field.field_help
)
elif field.type == 'video_url':
builder.add_field(
field_class=VideoURLField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[URL()],
description=field.field_help
)
elif field.type == 'stdnum':
builder.add_field(
field_class=StringField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[Stdnum(field.format)],
description=field.field_help
)
elif field.type == 'date':
widget = None
validators = []
if field.valid_date_range:
start = field.valid_date_range.start
stop = field.valid_date_range.stop
widget = DateRangeInput(start, stop)
validators.append(ValidDateRange(start, stop))
builder.add_field(
field_class=DateField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
description=field.field_help,
validators=validators,
widget=widget
)
elif field.type == 'datetime':
widget = None
validators = []
if field.valid_date_range:
start = field.valid_date_range.start
stop = field.valid_date_range.stop
widget = DateTimeLocalRangeInput(start, stop)
validators.append(ValidDateRange(start, stop))
builder.add_field(
field_class=DateTimeLocalField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
description=field.field_help,
validators=validators,
widget=widget
)
elif field.type == 'time':
builder.add_field(
field_class=TimeField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
description=field.field_help
)
elif field.type == 'fileinput':
expected_extensions = ExpectedExtensions(field.extensions)
# build an accept attribute for the file input
accept = ','.join(expected_extensions.whitelist)
builder.add_field(
field_class=UploadField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[
expected_extensions,
FileSizeLimit(DEFAULT_UPLOAD_LIMIT)
],
render_kw={'accept': accept},
description=field.field_help
)
elif field.type == 'multiplefileinput':
expected_extensions = ExpectedExtensions(field.extensions)
# build an accept attribute for the file input
accept = ','.join(expected_extensions.whitelist)
builder.add_field(
field_class=UploadMultipleField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[
expected_extensions,
FileSizeLimit(DEFAULT_UPLOAD_LIMIT)
],
render_kw={'accept': accept},
description=field.field_help
)
elif field.type == 'radio':
builder.add_field(
field_class=RadioField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
choices=[(c.key, c.label) for c in field.choices],
default=next((c.key for c in field.choices if c.selected), None),
pricing=field.pricing,
# do not coerce None into 'None'
coerce=lambda v: str(v) if v is not None else v,
description=field.field_help
)
elif field.type == 'checkbox':
builder.add_field(
field_class=MultiCheckboxField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
choices=[(c.key, c.label) for c in field.choices],
default=[c.key for c in field.choices if c.selected],
pricing=field.pricing,
# do not coerce None into 'None'
coerce=lambda v: str(v) if v is not None else v,
description=field.field_help
)
elif field.type == 'integer_range':
builder.add_field(
field_class=IntegerField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
pricing=field.pricing,
validators=[
NumberRange(
field.range.start,
field.range.stop
)
],
description=field.field_help
)
elif field.type == 'decimal_range':
builder.add_field(
field_class=DecimalField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
validators=[
NumberRange(
field.range.start,
field.range.stop
)
],
description=field.field_help
)
elif field.type == 'code':
builder.add_field(
field_class=TextAreaField,
field_id=field.id,
label=field.label,
dependency=dependency,
required=field.required,
render_kw={'data-editor': field.syntax},
description=field.field_help
)
else:
raise NotImplementedError
if field.type == 'radio' or field.type == 'checkbox':
for choice in field.choices:
if not choice.fields:
continue
normalized_label = normalize_label_for_dependency(choice.label)
dependency = FieldDependency(field.id, normalized_label)
for choice_field in choice.fields:
handle_field(builder, choice_field, dependency)