Source code for flask_admin.form.rules

import typing as t
from collections.abc import Generator

from jinja2.runtime import Context
from markupsafe import Markup
from wtforms import Field as WTField
from wtforms import Form

from flask_admin import helpers
from flask_admin._compat import string_types
from flask_admin._types import T_FORM_OPTS
from flask_admin._types import T_INLINE_BASE_FORM_ADMIN
from flask_admin._types import T_MODEL_VIEW
from flask_admin._types import T_RULES_SEQUENCE
from flask_admin._types import T_TRANSLATABLE


[docs] class BaseRule: """ Base form rule. All form formatting rules should derive from `BaseRule`. """ def __init__(self) -> None: self.parent: BaseRule | None = None self.rule_set: RuleSet | None = None def configure( self, rule_set: "RuleSet", parent: t.Optional["BaseRule"] ) -> "BaseRule": """ Configure rule and assign to rule set. :param rule_set: Rule set :param parent: Parent rule (if any) """ self.parent = parent self.rule_set = rule_set return self @property def visible_fields(self) -> list[str]: """ A list of visible fields for the given rule. """ return [] def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render rule. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to template or the field """ raise NotImplementedError()
[docs] class NestedRule(BaseRule): """ Nested rule. Can contain child rules and render them. """ def __init__( self, rules: t.Optional[T_RULES_SEQUENCE] = None, # ruff: noqa separator: str = "", ) -> None: """ Constructor. :param rules: Child rule list :param separator: Default separator between rules when rendering them. """ if rules is None: rules = [] super().__init__() self.rules: list[str | Header | BaseRule] = list(rules) self.separator = separator def configure(self, rule_set: "RuleSet", parent: BaseRule | None) -> BaseRule: """ Configure rule. :param rule_set: Rule set :param parent: Parent rule (if any) """ self.rules = rule_set.configure_rules(self.rules, self) # type: ignore return super().configure(rule_set, parent) @property def visible_fields(self) -> list[str]: """ Return visible fields for all child rules. """ visible_fields = [] for rule in self.rules: for field in rule.visible_fields: # type:ignore[union-attr] visible_fields.append(field) return visible_fields def __iter__(self) -> t.Iterable[t.Union[BaseRule, str, "Header"]]: """ Return rules. """ return self.rules def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render all children. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to template or the field """ if field_args is None: field_args = {} result: list[str] = [] for r in self.rules: result.append( str( r( # type:ignore[operator] form, form_opts, field_args ) ) ) return Markup(self.separator.join(result))
[docs] class Text(BaseRule): """ Render text (or HTML snippet) from string. """ def __init__(self, text: str, escape: bool = True) -> None: """ Constructor. :param text: Text to render :param escape: Should text be escaped or not. Default is `True`. """ super().__init__() self.text = text self.escape = escape def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: if field_args is None: field_args = {} if self.escape: return self.text return Markup(self.text)
[docs] class HTML(Text): """ Shortcut for `Text` rule with `escape` set to `False`. """ def __init__(self, html: str) -> None: super().__init__(html, escape=False)
[docs] class Macro(BaseRule): """ Render macro by its name from current Jinja2 context. """ def __init__(self, macro_name: str, **kwargs: t.Any) -> None: """ Constructor. :param macro_name: Macro name :param kwargs: Default macro parameters """ super().__init__() self.macro_name = macro_name self.default_args = kwargs def _resolve(self, context: Context, name: str) -> WTField | None: """ Resolve macro in a Jinja2 context :param context: Jinja2 context :param name: Macro name. May be full path (with dots) """ parts = name.split(".") try: field = context.resolve(parts[0]) except AttributeError as err: raise Exception( 'Your template is missing "{% set render_ctx = h.resolve_ctx() %}"' ) from err if not field: return None for p in parts[1:]: field = getattr(field, p, None) if not field: return field return field # type: ignore[return-value] def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render macro rule. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to the macro """ if field_args is None: field_args = {} context = helpers.get_render_ctx() macro = self._resolve(context, self.macro_name) # type:ignore[arg-type] if not macro: raise ValueError(f"Cannot find macro {self.macro_name} in current context.") opts = dict(self.default_args) opts.update(field_args) return macro(**opts)
[docs] class Container(Macro): """ Render container around child rule. """ def __init__(self, macro_name: str, child_rule: BaseRule, **kwargs: t.Any) -> None: """ Constructor. :param macro_name: Macro name that will be used as a container :param child_rule: Child rule to be rendered inside of container :param kwargs: Container macro arguments The container is nothing but a Jinja Macro that is placed within the current jinja context. The rule inside the container is represented by `{{ caller() }}` as it is shown in the follwoing Jinja [snippet](/advanced/index.html#built-in-rules): """ super().__init__(macro_name, **kwargs) self.child_rule = child_rule def configure(self, rule_set: "RuleSet", parent: BaseRule | None) -> BaseRule: """ Configure rule. :param rule_set: Rule set :param parent: Parent rule (if any) """ self.child_rule.configure(rule_set, self) return super().configure(rule_set, parent) @property def visible_fields(self) -> list[str]: return self.child_rule.visible_fields def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render container. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to template or the field """ if field_args is None: field_args = {} context = helpers.get_render_ctx() def caller(**kwargs: t.Any) -> None: return context.call( # type:ignore[union-attr, return-value] self.child_rule, form, form_opts, kwargs ) args = dict(field_args) args["caller"] = caller return super().__call__(form, form_opts, args)
[docs] class Field(Macro): """ Form field rule. """ def __init__(self, field_name: str, render_field: str = "lib.render_field") -> None: """ Constructor. :param field_name: Field name to render :param render_field: Macro that will be used to render the field. """ super().__init__(render_field) self.field_name = field_name @property def visible_fields(self) -> list[str]: return [self.field_name] def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render field. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to template or the field """ if field_args is None: field_args = {} field = getattr(form, self.field_name, None) if field is None: raise ValueError(f"Form {form} does not have field {self.field_name}") opts = {} if form_opts: opts.update(form_opts.widget_args.get(self.field_name, {})) opts.update(field_args) params = {"form": form, "field": field, "kwargs": opts} return super().__call__(form, form_opts, params)
[docs] class FieldSet(NestedRule): """ Field set with header. """ def __init__( self, rules: T_RULES_SEQUENCE, header: T_TRANSLATABLE | None = None, separator: str = "", ) -> None: """ Constructor. :param rules: Child rules :param header: Header text :param separator: Child rule separator """ if header: rule_set = [Header(header)] + list(rules) else: rule_set = list(rules) super().__init__(rule_set, separator=separator)
class Row(NestedRule): """ Takes a list of rules and render them in a single row. """ def __init__(self, *columns: str | BaseRule, **kw: t.Any) -> None: super().__init__() self.rules: list[str | BaseRule] = columns # type:ignore[assignment] def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: if field_args is None: field_args = {} cols = [] for col in self.rules: if col.visible_fields: # type:ignore[union-attr] w_args = form_opts.widget_args.setdefault( # type:ignore[union-attr] col.visible_fields[0], # type:ignore[union-attr] {}, ) w_args.setdefault("column_class", "col") cols.append( col( # type: ignore[operator] form, form_opts, field_args ) ) return Markup('<div class="form-row">{}</div>'.format("".join(cols))) class Group(Macro): def __init__( self, field_name: str, prepend: str | tuple[str] | list[str] | None = None, append: str | tuple[str] | list[str] | None = None, **kwargs: t.Any, ) -> None: """ [Bootstrap Input group](https://getbootstrap.com/docs/4.6/components/input-group/). It renders the target field whatever its type is (text, radio, checkbox..etc), allowing prepended and appended boxes to be filled with any HTML content. """ render_field = kwargs.get("render_field", "lib.render_field") super().__init__(render_field) self.field_name = field_name self._addons = [] if prepend: if not isinstance(prepend, tuple | list): prepend = [prepend] for cnf in prepend: if isinstance(cnf, str): self._addons.append({"pos": "prepend", "type": "text", "text": cnf}) continue if cnf["type"] in ("field", "html", "text"): cnf["pos"] = "prepend" self._addons.append(cnf) if append: if not isinstance(append, tuple | list): append = [append] for cnf in append: if isinstance(cnf, str): self._addons.append({"pos": "append", "type": "text", "text": cnf}) continue if cnf["type"] in ("field", "html", "text"): cnf["pos"] = "append" self._addons.append(cnf) print(self._addons) @property def visible_fields(self) -> list[str]: fields = [self.field_name] for cnf in self._addons: if cnf["type"] == "field": fields.append(cnf["name"]) return fields def __call__( self, form: Form, form_opts: t.Union[T_FORM_OPTS, None] = None, field_args: t.Any = None, ) -> str: """ Render field. :param form: Form object :param form_opts: Form options :param field_args: Optional arguments that should be passed to template or the field """ if field_args is None: field_args = {} field = getattr(form, self.field_name, None) if field is None: raise ValueError(f"Form {form} does not have field {self.field_name}") if form_opts: widget_args = form_opts.widget_args else: widget_args = {} opts: dict[str, Markup] = {} prepend = [] append = [] for cnf in self._addons: ctn: str | None = None typ = cnf["type"] if typ == "field": name = cnf["name"] fld = form._fields.get(name, None) if fld: w_args = widget_args.setdefault(name, {}) if fld.type in ("BooleanField", "RadioField"): w_args.setdefault("class", "form-check-input") else: w_args.setdefault("class", "form-control") ctn = fld(**w_args) elif typ == "text": ctn = '<span class="input-group-text">{}</span>'.format(cnf["text"]) elif typ == "html": ctn = cnf["html"] if ctn: if cnf["pos"] == "prepend": prepend.append(ctn) else: append.append(ctn) if prepend: opts["prepend"] = Markup("".join(prepend)) if append: opts["append"] = Markup("".join(append)) opts.update(widget_args.get(self.field_name, {})) opts.update(field_args) params = {"form": form, "field": field, "kwargs": opts} return super().__call__(form, form_opts, params) class RuleSet: """ Rule set. """ def __init__( self, view: t.Union[T_MODEL_VIEW, T_INLINE_BASE_FORM_ADMIN], rules: t.Sequence[str | tuple[BaseRule] | list[BaseRule] | BaseRule | FieldSet], ) -> None: """ Constructor. :param view: Administrative view :param rules: Rule list """ self.view = view self.rules = self.configure_rules(rules) @property def visible_fields(self) -> list[str]: visible_fields: list[str] = [] for rule in self.rules: for field in rule.visible_fields: visible_fields.append(field) return visible_fields def convert_string(self, value: str) -> BaseRule: """ Convert string to rule. Override this method to change default behavior. """ return Field(value) def configure_rules( self, rules: t.Sequence[str | tuple[BaseRule] | list[BaseRule] | BaseRule | FieldSet], parent: BaseRule | None = None, ) -> list[BaseRule]: """ Configure all rules recursively - bind them to current RuleSet and convert string references to `Field` rules. :param rules: Rule list :param parent: Parent rule (if any) """ result: list[BaseRule] = [] for r in rules: if isinstance(r, string_types): result.append(self.convert_string(r).configure(self, parent)) elif isinstance(r, tuple | list): row = Row(*r) result.append(row.configure(self, parent)) else: try: result.append(r.configure(self, parent)) except AttributeError as err: raise TypeError(f'Could not convert "{repr(r)}" to rule') from err return result def __iter__(self) -> Generator[BaseRule, None, None]: """ Iterate through registered rules. """ yield from self.rules