form.parser.core ================ .. py:module:: form.parser.core .. autoapi-nested-parse:: onegov.form includes it's own markdownish form syntax, inspired by https://github.com/maleldil/wmd The goal of this syntax is to enable the creation of forms through the web, without having to use javascript, html or python code. Also, just like Markdown, we want this syntax to be readable by humans. How it works ============ Internally, the form syntax is converted into a YAML file, which is in turn parsed and turned into a WTForms class. We decided to go for the intermediate YAML file because it's easy to define a Syntax which correctly supports indentation. Our pyparsing approach was flimsy at best. Parser Errors ============= There's currently no sophisticated error check. It's possible that the parser misunderstand something you defined without warning. So be careful to check that what you wanted was actually what you got. Syntax ====== Fields ------ Every field is identified by a label, an optional 'required' indicator and a field definition. The Label can be any kind of text, not including ``*`` and ``=``. The ``*`` indicates that a field is required. The ``=`` separates the identifier from the definition. A required field starts like this:: My required field * = An optional field starts like this:: My optional field = Following the identifier is the field definition. For example, this defines a textfield:: My textfield = ___ Comments can be added beneath a field, using the same indentation:: My textfield = ___ << Explanation for my field >> All characters are allowed except ''>''. Complex example:: Delivery * = (x) I want it delivered Alternate Address = (x) No ( ) Yes Street = ___ << street >> Town = ___ << Alt >> ( ) I want to pick it up << delivery >> Kommentar = ... << kommentar >> All possible fields are documented further below. Fieldsets --------- Fields are grouped into fieldsets. The fieldset of a field is the fieldset that was last defined:: # Fieldset 1 I belong to Fieldset 1 = ___ # Fieldset 2 I belong to Fieldset 2 = ___ If no fieldset is defined, the fields don't belong to a fieldset. To stop putting fields in a fieldset, define an empty fieldeset:: # Fieldset 1 I belong to Fieldset 1 = ___ # ... I don't belong to a Fieldset = ___ Available Fields ---------------- Textfield ~~~~~~~~~ A textfield consists of exactly three underscores:: I'm a textfield = ___ If the textfield is limited in length, the length can be given:: I'm a limited textfield = ___[50] The length of such textfields is validated. Additionally, textfields may use regexes to validate their contents:: I'm a numbers-only textfield = ___/^[0-9]+$ You *can* combine the length with a regex, though you probably don't want to:: I'm a length-limited numbers-only textfield = ___[4]/^[0-9]+$ This could be simplified as follows: I'm a length-limited numbers-only textfield = ___/^[0-9]{0,4}$ Note that you don't *need* to specify the beginning (^) and the end ($) of the string, but not doing so might result in unexpected results. For example, while '123abc' is invalid for ``___/^[0-9]+$``, it is perfectly valid for ``___/[0-9]+``. The latter only demands that the text starts with a number, not that it only consists of numbers! Textarea ~~~~~~~~ A textarea has no limit and consists of exactly three dots:: I'm a textarea = ... Optionally, the number of rows can be passed to the field. This changes the way the textarea looks, not the way it acts:: I'm a textarea with 10 rows = ...[10] Password ~~~~~~~~ A password field consists of exactly three stars:: I'm a password = *** E-Mail ~~~~~~ An e-mail field consists of exactly three ``@``:: I'm an e-mail field = @@@ URL ~~~ An url field consists of the http/https prefix:: I'm an url field = http:// I'm the exact same = https:// Whether or not you enter http or https has no bearing on the validation. Video Link ~~~~~~~~~~ An url field pointing to a video ``video-url``:: I am a video link = video-url In case of vimeo or youtube videos the video will be embedded in the page, otherwise the link will be shown. Date ~~~~ A date (without time) is defined by this exact string: ``YYYY.MM.DD``:: I'm a date field = YYYY.MM.DD Note that this doesn't mean that the date format can be influenced. A date field optionally can be limited to a relative or absolute date range. Note that the edges of the interval are inclusive. The list of possible grains for relative dates are ``years``, ``months``, ``weeks`` and ``days`` as well as the special value ``today``. I'm a future date field = YYYY.MM.DD (+1 days..) I'm on today or in the future = YYYY.MM.DD (today..) At least two weeks ago = YYYY.MM.DD (..-2 weeks) Between 2010 and 2020 = YYYY.MM.DD (2010.01.01..2020.12.31) Datetime ~~~~~~~~ A date (with time) is defined by this exact string: ``YYYY.MM.DD HH:MM``:: I'm a datetime field = YYYY.MM.DD HH:MM I'm a futue datetime field = YYYY.MM.DD HH:MM (today..) Again, this doesn't mean that the datetime format can be influenced. The same range validation that can be applied to date fields can also be applied to datetime. Note however that the Validation will be applied to to the date portion. The time portion is ignored completely. Time ~~~~ A Time is defined by this exact string: ``HH:MM``:: I'm a time field = HH:MM One more time, this doesn't mean that the datetime format can be influenced. Numbers ~~~~~~~ There are two types of number fields. An integer and a float field:: I'm an integer field = 0..99 I'm an integer field of a different range = -100..100 I'm a float field = 0.00..99.00 I'm an float field of a different range = -100.00..100.00 Integer fields optionally can have a price attached to them which will be multiplied by the supplied integer:: Number of stamps to include = 0..30 (1.00 CHF) This will result in a price of 1.00 CHF per stamp. Code ~~~~ To write code in a certain syntax, use the following:: Description = Currently, only markdown is supported. Files ~~~~~ A file upload is defined like this:: I'm a file upload field = *.* This particular example would allow any file. To allow only certain files do something like this:: I'm a image filed = *.png|*.jpg|*.gif I'm a document = *.doc I'm any document = *.doc|*.pdf The files are checked against their file extension. Onegov.form also checks that uploaded files have the mimetype they claim to have and it won't accept obviously dangerous uploads like binaries (unless you really want to). These file inputs allow only for one file to be uploaded. If you want to allow multiple files to be uploaded, use the following syntax:: I'm a multiple file upload field = *.* (multiple) This will allow the user to upload multiple files at once. Standard Numbers ~~~~~~~~~~~~~~~~ onegov.form uses `python-stdnum `_ to offer a wide range of standard formats that are guaranteed to be validated. To use, simply use a `#`, followed by the stdnum format to use:: I'm a valid IBAN (or empty) = # iban I'm a valid IBAN (required) * = # iban The format string after the `#` must be importable from stdnum. In other words, this must work, if you are using ``ch.ssn`` (to use an example):: $ python >>> from stdnum.ch import ssn This is a bit of an advanced feature and since it delegates most work to an external library there's no guarantee that a format once used may be reused in the future. Still, the library should be somewhat stable and the benefit is huge. To see the available format, have a look at the docs: ``_ Radio Buttons ~~~~~~~~~~~~~ Radio button fields consist of x radio buttons, out of which one may be preselected. Radio buttons need to be indented on the lines after the definition:: Gender = ( ) Female ( ) Male (x) I don't want to say Radio buttons also have the ability to define optional form parts. Those parts are only shown if a question was answered a certain way. Form parts are properly nested if they lign up with the label above them. For example:: Delivery Method = ( ) Pickup Pickup Time * = ___ (x) Address Street * = ___ Town * = ___ Here, the street and the town only need to be provided, if the delivery method is 'Address'. If the user selects a different option, the fields are not shown and they will not be required. On the other hand, if 'Pickup' is selected, the 'Pickup Time' needs to be filled out and the address options are hidden. This kind of nesting may continue ad infinitum. Meaning you can nest radio buttons as deeply as you like. Note however, that this is discouraged and that your users will not be too happy if you present them with a deeply nested form. More than one level of nesting is a clear indicator that your form is too complex. Checkboxes ~~~~~~~~~~ Checkboxes work exactly like radio buttons, just that you can select multiple fields:: Extras = [x] Phone insurance [ ] Phone case [x] Extra battery Just like radiobuttons, checkboxes may be nested to created dependencies:: Additional toppings = [ ] Salami [ ] Olives [ ] Other Description = ___ Pricing Information ~~~~~~~~~~~~~~~~~~~ Radio buttons and checkboxes may be priced. For example, the following order form can be modeled:: Node Size = ( ) Small (20 USD) (x) Medium (30 USD) ( ) Large (40 USD) Extras = [x] Second IP Address (20 CHF) [x] Backup (20 CHF) Delivery = (x) Pickup (0 CHF) ( ) Delivery (5 CHF!) The additional pricing metadata can be used to provide simple order forms. As in any other form, dependencies are taken into account. The optional `!` at the end of the price indicates that credit card payment will become mandatory if this option is selected. It is possible to achieve this without a price increase too: (0 CHF!) Discounts ~~~~~~~~~ Radio buttons and checkboxes may apply proportial discounts. How those discounts are applied will depend on the consumer. It will not be factored into the price of the form automatically, since there may be other costs associated with the submission, that aren't part of the form. Example discount:: Discount = (x) No discount ( ) Sports club (50%) ( ) School (100%) Attributes ---------- .. autoapisummary:: form.parser.core.ELEMENTS form.parser.core.HalfBoundedRange form.parser.core.DateRange form.parser.core.BasicParsedField form.parser.core.FileParsedField form.parser.core.ParsedField Classes ------- .. autoapisummary:: form.parser.core.LazyElements form.parser.core.CustomLoader form.parser.core.constructor form.parser.core._RangeValidationMixin form.parser.core.Range form.parser.core.RangeEndOptional form.parser.core.RangeStartOptional form.parser.core.Pricing form.parser.core.Choice form.parser.core.BaseField form.parser.core.PasswordField form.parser.core.EmailField form.parser.core.UrlField form.parser.core.VideoURLField form.parser.core.DateField form.parser.core.DatetimeField form.parser.core.TimeField form.parser.core.StringField form.parser.core.TextAreaField form.parser.core.CodeField form.parser.core.StdnumField form.parser.core.ChipNrField form.parser.core.IntegerRangeField form.parser.core.DecimalRangeField form.parser.core.FileinputField form.parser.core.MultipleFileinputField form.parser.core.RadioField form.parser.core.CheckboxField form.parser.core.IndentStack Functions --------- .. autoapisummary:: form.parser.core.create_parser_elements form.parser.core.construct_textfield form.parser.core.construct_textarea form.parser.core.construct_syntax form.parser.core.construct_email form.parser.core.construct_url form.parser.core.construct_video_url form.parser.core.construct_stdnum form.parser.core.construct_chip_nr form.parser.core.construct_date form.parser.core.construct_datetime form.parser.core.construct_time form.parser.core.construct_radio form.parser.core.construct_checkbox form.parser.core.construct_fileinput form.parser.core.construct_multiplefileinput form.parser.core.construct_password form.parser.core.construct_decimal_range form.parser.core.construct_integer_range form.parser.core.flatten_fields form.parser.core.find_field form.parser.core.human_id form.parser.core.write_choices_value form.parser.core.parse_formcode form.parser.core.parse_field_block form.parser.core.format_pricing form.parser.core.format_discount form.parser.core.match form.parser.core.try_parse form.parser.core.validate_indent form.parser.core.translate_to_yaml Module Contents --------------- .. py:function:: create_parser_elements() -> onegov.core.utils.Bunch .. py:class:: LazyElements .. py:attribute:: loaded :value: False .. py:method:: __getattr__(name: str) -> pyparsing.ParserElement .. py:data:: ELEMENTS .. py:class:: CustomLoader(stream) Bases: :py:obj:`yaml.SafeLoader` Extends the default yaml loader with customized constructors. .. py:class:: constructor(tag: str) Adds decorated functions to as constructors to the CustomLoader. .. py:attribute:: tag .. py:method:: __call__(fn: collections.abc.Callable[[CustomLoader, yaml.nodes.ScalarNode], pyparsing.ParseResults]) -> collections.abc.Callable[[CustomLoader, yaml.nodes.ScalarNode], pyparsing.ParseResults] .. py:function:: construct_textfield(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_textarea(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_syntax(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_email(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_url(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_video_url(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_stdnum(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_chip_nr(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_date(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_datetime(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_time(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_radio(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_checkbox(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_fileinput(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_multiplefileinput(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_password(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_decimal_range(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: construct_integer_range(loader: CustomLoader, node: yaml.nodes.ScalarNode) -> pyparsing.ParseResults .. py:function:: flatten_fields(fields: collections.abc.Sequence[ParsedField] | None) -> collections.abc.Iterator[ParsedField] .. py:function:: find_field(fields: collections.abc.Iterable[ParsedField], id: str | None) -> ParsedField | None .. py:class:: _RangeValidationMixin .. py:attribute:: start :type: Any | None .. py:method:: start_before_stop() -> Self .. py:class:: Range[T](/, **data: Any) Bases: :py:obj:`pydantic.BaseModel`, :py:obj:`_RangeValidationMixin` A closed bounded range, between start and stop .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: start :type: T :value: None .. py:attribute:: stop :type: T :value: None .. py:class:: RangeEndOptional[T](/, **data: Any) Bases: :py:obj:`pydantic.BaseModel`, :py:obj:`_RangeValidationMixin` A half-bounded range, between start and optional end .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: start :type: T :value: None .. py:attribute:: stop :type: T | None :value: None .. py:class:: RangeStartOptional[T](/, **data: Any) Bases: :py:obj:`pydantic.BaseModel`, :py:obj:`_RangeValidationMixin` A half-bounded range, between optional start and end .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: start :type: T | None :value: None .. py:attribute:: stop :type: T :value: None .. py:type:: HalfBoundedRange :canonical: Annotated[RangeEndOptional[T] | RangeStartOptional[T], Field(union_mode='left_to_right')] .. py:type:: DateRange :canonical: Annotated[HalfBoundedRange[date_t] | HalfBoundedRange[RelativeDate], Field(union_mode='left_to_right')] .. py:class:: Pricing(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` Pricing information for a priced form input or selection. .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: amount :type: decimal.Decimal :value: None .. py:attribute:: currency :type: pydantic_extra_types.currency_code.Currency :value: None .. py:attribute:: online_payment_required :type: bool :value: None .. py:method:: as_tuple() -> tuple[decimal.Decimal, str, bool] .. py:method:: __str__() -> str .. py:method:: __format__(format_spec: str) -> str .. py:type:: BasicParsedField :canonical: Annotated[PasswordField | EmailField | UrlField | VideoURLField | DateField | DatetimeField | TimeField | StringField | TextAreaField | CodeField | StdnumField | IntegerRangeField | DecimalRangeField | RadioField | CheckboxField | ChipNrField, Field(discriminator='type')] .. py:type:: FileParsedField :canonical: Annotated[FileinputField | MultipleFileinputField, Field(discriminator='type')] .. py:type:: ParsedField :canonical: Annotated[BasicParsedField | FileParsedField, Field(discriminator='type')] .. py:class:: Choice(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` Represents a single choice of a ``radio`` or ``checkbox`` field. Generally the choices are rendered as HTML ```` and ```` respecitvely, based on the ``kind`` of the containing field. Choices may contain subfields, which are only displayed as long as this choice has been selected. .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: label :type: str :value: None .. py:attribute:: selected :type: bool :value: None .. py:attribute:: fields :type: tuple[ParsedField, ...] :value: None .. py:attribute:: pricing :type: Pricing | None :value: None .. py:attribute:: discount :type: decimal.Decimal | None :value: None .. py:method:: pricing_and_discount_are_mutually_exclusive() -> Self .. py:property:: display_label :type: str .. py:method:: __str__() -> str .. py:method:: __format__(format_spec: str) -> str .. py:function:: human_id(label: str, fieldset: str | None, parent_id: str | None = None) -> str .. py:class:: BaseField[KindT: str](/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` Represents a form field. .. py:attribute:: model_config Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. .. py:attribute:: type :type: KindT :value: None .. py:attribute:: label :type: str :value: None .. py:attribute:: required :type: bool :value: None .. py:attribute:: fieldset :type: str | None :value: None .. py:attribute:: field_help :type: str | None :value: None .. py:attribute:: _parent :type: ParsedField | None :value: None .. py:attribute:: _inherits_fieldset :type: bool :value: False .. py:property:: id :type: str .. py:property:: real_fieldset :type: str | None .. py:property:: human_id :type: str .. py:property:: display_label :type: str .. py:method:: parse(value: Any) -> object .. py:method:: from_parse_results[T: ParsedField](field: pyparsing.ParseResults, identifier: pyparsing.ParseResults, fieldset: str | None = None, field_help: str | None = None) -> T :classmethod: .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None :abstractmethod: .. py:method:: write_formcode(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:method:: __eq__(other: object) -> bool .. py:class:: PasswordField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'password'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['password'] :value: 'password' .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:class:: EmailField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'email'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['email'] :value: 'email' .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:class:: UrlField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'url'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['url'] :value: 'url' .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:class:: VideoURLField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'video_url'`\ ]\ ] Represents an HTML ```` pointing to a video. .. py:attribute:: type :type: Literal['video_url'] :value: 'video_url' .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:class:: DateField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'date'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['date'] :value: 'date' .. py:attribute:: valid_date_range :type: DateRange | None :value: None .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:method:: parse(value: Any) -> object .. py:method:: from_parse_results(field: pyparsing.ParseResults, identifier: pyparsing.ParseResults, fieldset: str | None = None, field_help: str | None = None) -> Self :classmethod: .. py:class:: DatetimeField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'datetime'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['datetime'] :value: 'datetime' .. py:attribute:: valid_date_range :type: DateRange | None :value: None .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:method:: from_parse_results(field: pyparsing.ParseResults, identifier: pyparsing.ParseResults, fieldset: str | None = None, field_help: str | None = None) -> Self :classmethod: .. py:class:: TimeField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'time'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['time'] :value: 'time' .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:method:: parse(value: Any) -> object .. py:class:: StringField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'text'`\ ]\ ] Represents an HTML ````. .. py:attribute:: type :type: Literal['text'] :value: 'text' .. py:attribute:: maxlength :type: int | None :value: None .. py:attribute:: regex :type: re.Pattern[str] | None :value: None .. py:method:: write_formcode_value(buffer: _typeshed.SupportsWrite[str], indentation: str = '') -> None .. py:method:: from_parse_results(field: pyparsing.ParseResults, identifier: pyparsing.ParseResults, fieldset: str | None = None, field_help: str | None = None) -> Self :classmethod: .. py:class:: TextAreaField(/, **data: Any) Bases: :py:obj:`BaseField`\ [\ :py:obj:`Literal`\ [\ :py:obj:`'textarea'`\ ]\ ] Represents an HTML ``