563 lines
15 KiB
Python
563 lines
15 KiB
Python
from markupsafe import Markup
|
|
|
|
from flask_admin._compat import string_types
|
|
from flask_admin import helpers
|
|
|
|
|
|
class BaseRule(object):
|
|
"""
|
|
Base form rule. All form formatting rules should derive from `BaseRule`.
|
|
"""
|
|
def __init__(self):
|
|
self.parent = None
|
|
self.rule_set = None
|
|
|
|
def configure(self, rule_set, parent):
|
|
"""
|
|
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):
|
|
"""
|
|
A list of visible fields for the given rule.
|
|
"""
|
|
return []
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
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()
|
|
|
|
|
|
class NestedRule(BaseRule):
|
|
"""
|
|
Nested rule. Can contain child rules and render them.
|
|
"""
|
|
def __init__(self, rules=[], separator=''):
|
|
"""
|
|
Constructor.
|
|
|
|
:param rules:
|
|
Child rule list
|
|
:param separator:
|
|
Default separator between rules when rendering them.
|
|
"""
|
|
super(NestedRule, self).__init__()
|
|
self.rules = list(rules)
|
|
self.separator = separator
|
|
|
|
def configure(self, rule_set, parent):
|
|
"""
|
|
Configure rule.
|
|
|
|
:param rule_set:
|
|
Rule set
|
|
:param parent:
|
|
Parent rule (if any)
|
|
"""
|
|
self.rules = rule_set.configure_rules(self.rules, self)
|
|
return super(NestedRule, self).configure(rule_set, parent)
|
|
|
|
@property
|
|
def visible_fields(self):
|
|
"""
|
|
Return visible fields for all child rules.
|
|
"""
|
|
visible_fields = []
|
|
for rule in self.rules:
|
|
for field in rule.visible_fields:
|
|
visible_fields.append(field)
|
|
return visible_fields
|
|
|
|
def __iter__(self):
|
|
"""
|
|
Return rules.
|
|
"""
|
|
return self.rules
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
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
|
|
"""
|
|
result = []
|
|
|
|
for r in self.rules:
|
|
result.append(r(form, form_opts, field_args))
|
|
|
|
return Markup(self.separator.join(result))
|
|
|
|
|
|
class Text(BaseRule):
|
|
"""
|
|
Render text (or HTML snippet) from string.
|
|
"""
|
|
def __init__(self, text, escape=True):
|
|
"""
|
|
Constructor.
|
|
|
|
:param text:
|
|
Text to render
|
|
:param escape:
|
|
Should text be escaped or not. Default is `True`.
|
|
"""
|
|
super(Text, self).__init__()
|
|
|
|
self.text = text
|
|
self.escape = escape
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
if self.escape:
|
|
return self.text
|
|
|
|
return Markup(self.text)
|
|
|
|
|
|
class HTML(Text):
|
|
"""
|
|
Shortcut for `Text` rule with `escape` set to `False`.
|
|
"""
|
|
def __init__(self, html):
|
|
super(HTML, self).__init__(html, escape=False)
|
|
|
|
|
|
class Macro(BaseRule):
|
|
"""
|
|
Render macro by its name from current Jinja2 context.
|
|
"""
|
|
def __init__(self, macro_name, **kwargs):
|
|
"""
|
|
Constructor.
|
|
|
|
:param macro_name:
|
|
Macro name
|
|
:param kwargs:
|
|
Default macro parameters
|
|
"""
|
|
super(Macro, self).__init__()
|
|
|
|
self.macro_name = macro_name
|
|
self.default_args = kwargs
|
|
|
|
def _resolve(self, context, name):
|
|
"""
|
|
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:
|
|
raise Exception('Your template is missing '
|
|
'"{% set render_ctx = h.resolve_ctx() %}"')
|
|
|
|
if not field:
|
|
return None
|
|
|
|
for p in parts[1:]:
|
|
field = getattr(field, p, None)
|
|
|
|
if not field:
|
|
return field
|
|
|
|
return field
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
Render macro rule.
|
|
|
|
:param form:
|
|
Form object
|
|
:param form_opts:
|
|
Form options
|
|
:param field_args:
|
|
Optional arguments that should be passed to the macro
|
|
"""
|
|
context = helpers.get_render_ctx()
|
|
macro = self._resolve(context, self.macro_name)
|
|
|
|
if not macro:
|
|
raise ValueError('Cannot find macro %s in current context.' % self.macro_name)
|
|
|
|
opts = dict(self.default_args)
|
|
opts.update(field_args)
|
|
return macro(**opts)
|
|
|
|
|
|
class Container(Macro):
|
|
"""
|
|
Render container around child rule.
|
|
"""
|
|
def __init__(self, macro_name, child_rule, **kwargs):
|
|
"""
|
|
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
|
|
"""
|
|
super(Container, self).__init__(macro_name, **kwargs)
|
|
self.child_rule = child_rule
|
|
|
|
def configure(self, rule_set, parent):
|
|
"""
|
|
Configure rule.
|
|
|
|
:param rule_set:
|
|
Rule set
|
|
:param parent:
|
|
Parent rule (if any)
|
|
"""
|
|
self.child_rule.configure(rule_set, self)
|
|
return super(Container, self).configure(rule_set, parent)
|
|
|
|
@property
|
|
def visible_fields(self):
|
|
return self.child_rule.visible_fields
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
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
|
|
"""
|
|
context = helpers.get_render_ctx()
|
|
|
|
def caller(**kwargs):
|
|
return context.call(self.child_rule, form, form_opts, kwargs)
|
|
|
|
args = dict(field_args)
|
|
args['caller'] = caller
|
|
|
|
return super(Container, self).__call__(form, form_opts, args)
|
|
|
|
|
|
class Field(Macro):
|
|
"""
|
|
Form field rule.
|
|
"""
|
|
def __init__(self, field_name, render_field='lib.render_field'):
|
|
"""
|
|
Constructor.
|
|
|
|
:param field_name:
|
|
Field name to render
|
|
:param render_field:
|
|
Macro that will be used to render the field.
|
|
"""
|
|
super(Field, self).__init__(render_field)
|
|
self.field_name = field_name
|
|
|
|
@property
|
|
def visible_fields(self):
|
|
return [self.field_name]
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
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
|
|
"""
|
|
field = getattr(form, self.field_name, None)
|
|
|
|
if field is None:
|
|
raise ValueError('Form %s does not have field %s' % (form, 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(Field, self).__call__(form, form_opts, params)
|
|
|
|
|
|
class Header(Macro):
|
|
"""
|
|
Render header text.
|
|
"""
|
|
def __init__(self, text, header_macro='lib.render_header'):
|
|
"""
|
|
Constructor.
|
|
|
|
:param text:
|
|
Text to render
|
|
:param header_macro:
|
|
Header rendering macro
|
|
"""
|
|
super(Header, self).__init__(header_macro, text=text)
|
|
|
|
|
|
class FieldSet(NestedRule):
|
|
"""
|
|
Field set with header.
|
|
"""
|
|
def __init__(self, rules, header=None, separator=''):
|
|
"""
|
|
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(FieldSet, self).__init__(rule_set, separator=separator)
|
|
|
|
|
|
class Row(NestedRule):
|
|
def __init__(self, *columns, **kw):
|
|
super(Row, self).__init__()
|
|
self.rules = columns
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
cols = []
|
|
for col in self.rules:
|
|
if col.visible_fields:
|
|
w_args = form_opts.widget_args.setdefault(col.visible_fields[0], {})
|
|
w_args.setdefault('column_class', 'col')
|
|
cols.append(col(form, form_opts, field_args))
|
|
|
|
return Markup('<div class="form-row">%s</div>' % ''.join(cols))
|
|
|
|
|
|
class Group(Macro):
|
|
|
|
def __init__(self, field_name, prepend=None, append=None, **kwargs):
|
|
'''
|
|
Bootstrap Input group.
|
|
'''
|
|
render_field = kwargs.get('render_field', 'lib.render_field')
|
|
super(Group, self).__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):
|
|
fields = [self.field_name]
|
|
for cnf in self._addons:
|
|
if cnf['type'] == 'field':
|
|
fields.append(cnf['name'])
|
|
return fields
|
|
|
|
def __call__(self, form, form_opts=None, field_args={}):
|
|
"""
|
|
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
|
|
"""
|
|
field = getattr(form, self.field_name, None)
|
|
|
|
if field is None:
|
|
raise ValueError('Form %s does not have field %s' % (form, self.field_name))
|
|
|
|
if form_opts:
|
|
widget_args = form_opts.widget_args
|
|
else:
|
|
widget_args = {}
|
|
|
|
opts = {}
|
|
prepend = []
|
|
append = []
|
|
for cnf in self._addons:
|
|
ctn = 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">%s</span>' % 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(Group, self).__call__(form, form_opts, params)
|
|
|
|
|
|
class RuleSet(object):
|
|
"""
|
|
Rule set.
|
|
"""
|
|
def __init__(self, view, rules):
|
|
"""
|
|
Constructor.
|
|
|
|
:param view:
|
|
Administrative view
|
|
:param rules:
|
|
Rule list
|
|
"""
|
|
self.view = view
|
|
self.rules = self.configure_rules(rules)
|
|
|
|
@property
|
|
def visible_fields(self):
|
|
visible_fields = []
|
|
for rule in self.rules:
|
|
for field in rule.visible_fields:
|
|
visible_fields.append(field)
|
|
return visible_fields
|
|
|
|
def convert_string(self, value):
|
|
"""
|
|
Convert string to rule.
|
|
|
|
Override this method to change default behavior.
|
|
"""
|
|
return Field(value)
|
|
|
|
def configure_rules(self, rules, parent=None):
|
|
"""
|
|
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 = []
|
|
|
|
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:
|
|
raise TypeError('Could not convert "%s" to rule' % repr(r))
|
|
|
|
return result
|
|
|
|
def __iter__(self):
|
|
"""
|
|
Iterate through registered rules.
|
|
"""
|
|
for r in self.rules:
|
|
yield r
|