wcs/wcs/fields/base.py

1102 lines
41 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2023 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import collections
import datetime
import html
import re
import xml.etree.ElementTree as ET
from django.utils.encoding import force_str
from django.utils.formats import date_format as django_date_format
from quixote import get_publisher, get_request
from quixote.html import TemplateIO, htmltext
from wcs.blocks import BlockDef
from wcs.qommon import _, get_cfg, misc
from wcs.qommon.form import (
CheckboxesWidget,
CheckboxWidget,
CompositeWidget,
ConditionWidget,
Form,
RadiobuttonsWidget,
SingleSelectWidget,
StringWidget,
TextWidget,
VarnameWidget,
)
from wcs.qommon.misc import date_format, ellipsize, get_dependencies_from_template, xml_node_text
from wcs.qommon.template import Template, TemplateError
class SetValueError(Exception):
pass
class PrefillSelectionWidget(CompositeWidget):
def __init__(self, name, value=None, field=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
if not value:
value = {}
options = [
('none', _('None')),
('string', _('String / Template')),
]
if not get_publisher().has_site_option('disable-python-expressions'):
options.append(('formula', _('Python Expression (deprecated)')))
options += [
('user', _('User Field')),
('geolocation', _('Geolocation')),
]
if field and field.key == 'items':
# limit choices strings (must be templates giving complex data) or
# python; items field are prefilled with list of strings
options = [x for x in options if x[0] in ('none', 'string', 'formula')]
elif field and field.key == 'map':
# limit choices to geolocation
options = [x for x in options if x[0] in ('none', 'string', 'geolocation')]
self.add(
SingleSelectWidget,
'type',
options=options,
value=value.get('type') or 'none',
attrs={'data-dynamic-display-parent': 'true'},
)
self.parse()
if not self.value or self.value.get('type') == 'none':
self.value = {}
self.prefill_types = prefill_types = collections.OrderedDict(options)
self.add(
StringWidget,
'value_string',
size=80,
value=value.get('value') if value.get('type') == 'string' else None,
attrs={
'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value': prefill_types.get('string'),
},
)
self.add(
StringWidget,
'value_formula',
size=80,
value=value.get('value') if value.get('type') == 'formula' else None,
attrs={
'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value': prefill_types.get('formula'),
},
)
formdef = get_publisher().user_class.get_formdef()
users_cfg = get_cfg('users', {})
if formdef:
user_fields = []
for user_field in formdef.fields:
if user_field.label in [x[1] for x in user_fields]:
# do not allow duplicated field names
continue
user_fields.append((user_field.id, user_field.label))
if not users_cfg.get('field_email'):
user_fields.append(('email', _('Email (builtin)')))
else:
user_fields = [('name', _('Name')), ('email', _('Email'))]
self.add(
SingleSelectWidget,
'value_user',
value=value.get('value') if value.get('type') == 'user' else None,
options=user_fields,
attrs={
'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value': prefill_types.get('user'),
},
)
if field and field.key == 'map':
# different prefilling sources on map fields
geoloc_fields = [('position', _('Position'))]
else:
geoloc_fields = [
('house', _('Number')),
('road', _('Street')),
('number-and-street', _('Number and street')),
('postcode', _('Post Code')),
('city', _('City')),
('country', _('Country')),
]
if field and field.key == 'item':
geoloc_fields.append(('address-id', _('Address Identifier')))
self.add(
SingleSelectWidget,
'value_geolocation',
value=value.get('value') if value.get('type') == 'geolocation' else None,
options=geoloc_fields,
attrs={
'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value': prefill_types.get('geolocation'),
},
)
# exclude geolocation from locked prefill as the data necessarily
# comes from the user device.
self.add(
CheckboxWidget,
'locked',
value=value.get('locked'),
attrs={
'data-dynamic-display-child-of': 'prefill$type',
'data-dynamic-display-value-in': '|'.join(
[str(x[1]) for x in options if x[0] not in ('none', 'geolocation')]
),
'inline_title': _('Locked'),
},
)
self._parsed = False
def _parse(self, request):
values = {}
type_ = self.get('type')
if type_ and type_ != 'none':
values['type'] = type_
values['locked'] = self.get('locked')
value = self.get('value_%s' % type_)
if value:
values['value'] = value
self.value = values or None
if values and values['type'] == 'formula' and values.get('value'):
try:
compile(values.get('value', ''), '<string>', 'eval')
except (SyntaxError, TypeError) as e:
self.set_error(_('invalid expression: %s') % e)
if values and values['type'] == 'string' and Template.is_template_string(values.get('value')):
try:
Template(values.get('value'), raises=True)
except TemplateError as e:
self.set_error(str(e))
def render_content(self):
r = TemplateIO(html=True)
for widget in self.get_widgets():
r += widget.render_content()
return r.getvalue()
class Field:
id = None
varname = None
label = None
extra_css_class = None
convert_value_from_str = None
convert_value_to_str = None
convert_value_from_anything = None
allow_complex = False
allow_statistics = False
display_locations = []
prefill = None
keep_raw_value = True
store_display_value = None
store_structured_value = None
get_opendocument_node_value = None
condition = None
# flag a field for removal by AnonymiseWorkflowStatusItem
# possible values are final, intermediate, no.
# can be overriden in field' settings
anonymise = 'final'
stats = None
# declarations for serialization, they are mostly for legacy files,
# new exports directly include typing attributes.
TEXT_ATTRIBUTES = ['label', 'type', 'hint', 'varname', 'extra_css_class']
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k.replace('-', '_'), v)
@classmethod
def init(cls):
pass
def get_type_label(self):
return self.description
@property
def include_in_listing(self):
return 'listings' in (self.display_locations or [])
@property
def include_in_validation_page(self):
return 'validation' in (self.display_locations or [])
@property
def include_in_summary_page(self):
return 'summary' in (self.display_locations or [])
@property
def include_in_statistics(self):
return self.allow_statistics and self.varname and 'statistics' in (self.display_locations or [])
@property
def unhtmled_label(self):
return force_str(html.unescape(force_str(re.sub('<.*?>', ' ', self.label or ''))).strip())
@property
def ellipsized_label(self):
return ellipsize(self.unhtmled_label)
def get_admin_attributes(self):
return ['label', 'condition']
def export_to_json(self, include_id=False):
field = {}
if include_id:
extra_fields = ['id']
else:
extra_fields = []
for attribute in self.get_admin_attributes() + extra_fields:
if attribute == 'display_locations':
continue
if hasattr(self, attribute) and getattr(self, attribute) is not None:
val = getattr(self, attribute)
field[attribute] = val
field['type'] = self.key
field['in_statistics'] = self.include_in_statistics
return field
def init_with_json(self, elem, include_id=False):
if include_id:
self.id = elem.get('id')
for attribute in self.get_admin_attributes():
if attribute in elem:
setattr(self, attribute, elem.get(attribute))
def export_to_xml(self, charset, include_id=False):
field = ET.Element('field')
extra_fields = ['default_value'] # specific to workflow variables
if include_id:
extra_fields.append('id')
ET.SubElement(field, 'type').text = self.key
for attribute in self.get_admin_attributes() + extra_fields:
if hasattr(self, '%s_export_to_xml' % attribute):
getattr(self, '%s_export_to_xml' % attribute)(field, charset, include_id=include_id)
continue
if hasattr(self, attribute) and getattr(self, attribute) is not None:
val = getattr(self, attribute)
if isinstance(val, dict) and not val:
continue
el = ET.SubElement(field, attribute)
if isinstance(val, dict):
for k, v in sorted(val.items()):
if isinstance(v, str):
text_value = force_str(v, charset, errors='replace')
else:
# field having non str value in dictionnary field must overload
# import_to_xml to handle import
text_value = force_str(v)
ET.SubElement(el, k).text = text_value
elif isinstance(val, list):
if attribute[-1] == 's':
atname = attribute[:-1]
else:
atname = 'item'
# noqa pylint: disable=not-an-iterable
for v in val:
ET.SubElement(el, atname).text = force_str(v, charset, errors='replace')
elif isinstance(val, str):
el.attrib['type'] = 'str'
el.text = force_str(val, charset, errors='replace')
else:
el.text = str(val)
if isinstance(val, bool):
el.attrib['type'] = 'bool'
elif isinstance(val, int):
el.attrib['type'] = 'int'
return field
def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
extra_fields = ['default_value'] # specific to workflow variables
for attribute in self.get_admin_attributes() + extra_fields:
el = elem.find(attribute)
if hasattr(self, '%s_init_with_xml' % attribute):
getattr(self, '%s_init_with_xml' % attribute)(
el, charset, include_id=include_id, snapshot=False
)
continue
if el is None:
continue
if list(el):
if isinstance(getattr(self, attribute), list):
v = [xml_node_text(x) for x in el]
elif isinstance(getattr(self, attribute), dict):
v = {}
for e in el:
v[e.tag] = xml_node_text(e)
else:
print('currently:', self.__dict__)
print(' attribute:', attribute)
# ???
raise AssertionError
setattr(self, attribute, v)
else:
if attribute in self.TEXT_ATTRIBUTES:
elem_type = 'str'
else:
elem_type = el.attrib.get('type')
if el.text is None:
if isinstance(getattr(self, attribute), list):
setattr(self, attribute, [])
else:
setattr(self, attribute, None)
elif elem_type == 'bool' or (not elem_type and el.text in ('False', 'True')):
# boolean
setattr(self, attribute, el.text == 'True')
elif elem_type == 'int' or (not elem_type and isinstance(getattr(self, attribute), int)):
setattr(self, attribute, int(el.text))
else:
setattr(self, attribute, xml_node_text(el))
if include_id:
try:
self.id = xml_node_text(elem.find('id'))
except Exception:
pass
def condition_init_with_xml(self, node, charset, include_id=False, snapshot=False):
self.condition = None
if node is None:
return
if node.findall('type'):
self.condition = {
'type': xml_node_text(node.find('type')),
'value': xml_node_text(node.find('value')),
}
elif node.text:
self.condition = {'type': 'python', 'value': force_str(node.text).strip()}
def data_source_init_with_xml(self, node, charset, include_id=False, snapshot=False):
self.data_source = {}
if node is None:
return
if node.findall('type'):
self.data_source = {
'type': xml_node_text(node.find('type')),
'value': xml_node_text(node.find('value')),
}
if self.data_source.get('type') is None:
self.data_source = {}
elif self.data_source.get('value') is None:
del self.data_source['value']
def prefill_init_with_xml(self, node, charset, include_id=False, snapshot=False):
self.prefill = {}
if node is not None and node.findall('type'):
self.prefill = {
'type': xml_node_text(node.find('type')),
}
if self.prefill['type'] and self.prefill['type'] != 'none':
self.prefill['value'] = xml_node_text(node.find('value'))
if xml_node_text(node.find('locked')) == 'True':
self.prefill['locked'] = True
def get_rst_view_value(self, value, indent=''):
return indent + self.get_view_value(value)
def get_csv_heading(self):
return []
def get_csv_value(self, element, **kwargs):
return []
def get_structured_value(self, data):
if not self.store_structured_value:
return None
return data.get('%s_structured' % self.id)
def get_prefill_configuration(self):
if self.prefill and self.prefill.get('type') == 'none':
# make sure a 'none' prefill is not considered as a value
self.prefill = None
return self.prefill or {}
def get_prefill_value(self, user=None, force_string=True):
# returns a tuple with two items,
# 1. value[str], the value that will be used to prefill
# 2. locked[bool], a flag to know if this is a locked value
# (because it has been explicitely marked so or because it
# comes from verified identity data).
t = self.prefill.get('type')
explicit_lock = bool(self.prefill.get('locked'))
if t == 'string':
value = self.prefill.get('value')
if not Template.is_template_string(value):
return (value, explicit_lock)
from wcs.workflows import WorkflowStatusItem
try:
with get_publisher().complex_data():
v = WorkflowStatusItem.compute(
value,
raises=True,
allow_complex=self.allow_complex and not force_string,
record_errors=False,
)
if v and self.allow_complex:
v = get_publisher().get_cached_complex_data(v)
return (v, explicit_lock)
except TemplateError:
return ('', explicit_lock)
except AttributeError as e:
get_publisher().record_error(
_('Failed to evaluate prefill on field "%s"') % self.label,
formdef=getattr(self, 'formdef', None),
exception=e,
)
return ('', explicit_lock)
elif t == 'user' and user:
x = self.prefill.get('value')
if x == 'phone':
# get mapped field
x = get_cfg('users', {}).get('field_phone') or x
if x == 'email':
return (user.email, explicit_lock or 'email' in (user.verified_fields or []))
elif user.form_data:
userform = user.get_formdef()
for userfield in userform.fields:
if userfield.id == x:
value = user.form_data.get(x)
if (
value
and getattr(userfield, 'validation', None)
and userfield.validation['type'] in ('phone', 'phone-fr')
):
country_code = None
if (
getattr(self, 'validation', None)
and self.validation.get('type') == 'phone-fr'
):
country_code = 'FR'
value = misc.get_formatted_phone(user.form_data.get(x), country_code)
return (
value,
explicit_lock or str(userfield.id) in (user.verified_fields or []),
)
elif t == 'formula':
formula = self.prefill.get('value')
try:
ret = misc.eval_python(
formula,
get_publisher().get_global_eval_dict(),
get_publisher().substitutions.get_context_variables(),
)
if isinstance(ret, datetime.time):
ret = misc.site_encode(django_date_format(ret, format='TIME_FORMAT'))
if isinstance(ret, datetime.date):
ret = ret.strftime(date_format())
if ret:
if force_string:
# prefilling is done with strings for most fields so
# we default to forcing the value as a string.
# (items field are prefilled with list of strings, and
# will get the native python object)
ret = str(ret)
return (ret, explicit_lock)
except Exception:
pass
elif t == 'geolocation':
return (None, False)
return (None, False)
def get_prefill_attributes(self):
if not self.get_prefill_configuration():
return
t = self.prefill.get('type')
if t == 'geolocation':
return {'geolocation': self.prefill.get('value')}
if t == 'user':
formdef = get_publisher().user_class.get_formdef()
for user_field in formdef.fields or []:
if user_field.id != self.prefill.get('value'):
continue
try:
autocomplete_attribute = re.search(
r'\bautocomplete-([a-z0-9-]+)', user_field.extra_css_class
).groups()[0]
except (TypeError, IndexError, AttributeError):
continue
return {'autocomplete': autocomplete_attribute}
return None
def feed_session(self, value, display_value):
pass
def migrate(self):
changed = False
if getattr(self, 'in_listing', None): # 2019-09-28
self.display_locations = self.display_locations[:]
self.display_locations.append('listings')
changed = True
self.in_listing = None
if isinstance(self.anonymise, bool): # 2023-06-13
self.anonymise = 'final' if self.anonymise else 'no'
changed = True
return changed
@staticmethod
def evaluate_condition(dict_vars, formdef, condition, record_errors=True):
from .page import PageCondition
return PageCondition(
condition, {'dict_vars': dict_vars, 'formdef': formdef}, record_errors
).evaluate()
def is_visible(self, dict, formdef):
try:
return self.evaluate_condition(dict, formdef, self.condition)
except RuntimeError:
return True
@classmethod
def get_referenced_varnames(cls, formdef, value):
return re.findall(
r'\b(?:%s)[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|_live_|_structured_|\b)'
% '|'.join(formdef.var_prefixes),
str(value or ''),
)
def get_condition_varnames(self, formdef):
return self.get_referenced_varnames(formdef, self.condition['value'])
def has_live_conditions(self, formdef, hidden_varnames=None):
varnames = self.get_condition_varnames(formdef)
if not varnames:
return False
field_position = formdef.fields.index(self)
# rewind to field page
for field_position in range(field_position, -1, -1):
if formdef.fields[field_position].key == 'page':
break
else:
field_position = -1 # form with no page
# start from there
for field in formdef.fields[field_position + 1 :]:
if field.key == 'page':
# stop at next page
break
if field.varname in varnames and (
hidden_varnames is None or field.varname not in hidden_varnames
):
return True
return False
def from_json_value(self, value):
if value is None:
return value
return str(value)
def set_value(self, data, value, raise_on_error=False):
data['%s' % self.id] = value
if self.store_display_value:
display_value = self.store_display_value(data, self.id)
if raise_on_error and display_value is None:
raise SetValueError('a datasource is unavailable (field id: %s)' % self.id)
data['%s_display' % self.id] = display_value or None
if self.store_structured_value and value:
structured_value = self.store_structured_value(data, self.id, raise_on_error=raise_on_error)
if structured_value:
if isinstance(structured_value, dict) and structured_value.get('id'):
# in case of list field, override id
data['%s' % self.id] = str(structured_value.get('id'))
data['%s_structured' % self.id] = structured_value
else:
data['%s_structured' % self.id] = None
elif self.store_structured_value and '%s_structured' % self.id in data:
data['%s_structured' % self.id] = None
def get_dependencies(self):
if getattr(self, 'data_source', None):
data_source_type = self.data_source.get('type')
if data_source_type and data_source_type.startswith('carddef:'):
from wcs.carddef import CardDef
carddef_slug = data_source_type.split(':')[1]
try:
yield CardDef.get_by_urlname(carddef_slug)
except KeyError:
pass
else:
from wcs.data_sources import NamedDataSource
yield NamedDataSource.get_by_slug(data_source_type, ignore_errors=True)
if getattr(self, 'prefill', None):
prefill = self.prefill
if prefill:
if prefill.get('type') == 'string':
yield from get_dependencies_from_template(prefill.get('value'))
if getattr(self, 'condition', None):
condition = self.condition
if condition:
if condition.get('type') == 'django':
yield from get_dependencies_from_template(condition.get('value'))
def get_parameters_view(self):
r = TemplateIO(html=True)
form = Form()
self.fill_admin_form(form)
parameters = [x for x in self.get_admin_attributes() if getattr(self, x, None) is not None]
r += htmltext('<ul>')
for parameter in parameters:
widget = form.get_widget(parameter)
if not widget:
continue
label = self.get_parameter_view_label(widget, parameter)
if not label:
continue
value = getattr(self, parameter, Ellipsis)
if value is None or value == getattr(self.__class__, parameter, Ellipsis):
continue
parameter_view_value = self.get_parameter_view_value(widget, parameter)
if parameter_view_value:
r += htmltext('<li class="parameter-%s">' % parameter)
r += htmltext('<span class="parameter">%s</span> ') % _('%s:') % label
r += parameter_view_value
r += htmltext('</li>')
r += htmltext('</ul>')
return r.getvalue()
def get_parameter_view_label(self, widget, parameter):
if hasattr(self, 'get_%s_parameter_view_label' % parameter):
return getattr(self, 'get_%s_parameter_view_label' % parameter)()
return widget.get_title()
def get_parameter_view_value(self, widget, parameter):
if hasattr(self, 'get_%s_parameter_view_value' % parameter):
return getattr(self, 'get_%s_parameter_view_value' % parameter)(widget)
value = getattr(self, parameter)
if isinstance(value, bool):
return str(_('Yes') if value else _('No'))
elif hasattr(widget, 'options') and value:
if not isinstance(widget, CheckboxesWidget):
value = [value]
value_labels = []
for option in widget.options:
if isinstance(option, tuple):
if option[0] in value:
value_labels.append(str(option[1]))
else:
if option in value:
value_labels.append(str(option))
return ', '.join(value_labels) if value_labels else '-'
elif isinstance(value, list):
return ', '.join(value)
else:
return str(value)
def get_prefill_parameter_view_value(self, widget):
value = self.get_prefill_configuration()
if not value:
return
r = TemplateIO(html=True)
r += htmltext('<ul>')
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Type'),
_(':'),
widget.prefill_types.get(value.get('type')),
)
if value.get('type') in ('user', 'geolocation'):
select_widget = widget.get_widget('value_%s' % value['type'])
labels = {x[0]: x[1] for x in select_widget.options}
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Value'),
_(':'),
labels.get(value.get('value'), '-'),
)
else:
r += htmltext('<li><span class="parameter">%s%s</span> %s</li>') % (
_('Value'),
_(':'),
value.get('value'),
)
if value.get('locked'):
r += htmltext('<li>%s</li>') % _('Locked')
r += htmltext('</ul>')
return r.getvalue()
def get_data_source_parameter_view_value(self, widget):
value = getattr(self, 'data_source', None)
if not value or value.get('type') == 'none':
return
if value.get('type').startswith('carddef:'):
from wcs.carddef import CardDef
parts = value['type'].split(':')
try:
carddef = CardDef.get_by_urlname(parts[1])
except KeyError:
return str(_('deleted card model'))
custom_view = CardDef.get_data_source_custom_view(value['type'], carddef=carddef)
r = htmltext('<a href="%(url)s">%(label)s</a>') % {
'label': _('card model: %s') % carddef.name,
'url': carddef.get_admin_url(),
}
if custom_view:
r += ', '
r += htmltext('<a href="%(url)s">%(label)s</a>') % {
'label': _('custom view: %s') % custom_view.title,
'url': '%s%s' % (carddef.get_url(), custom_view.get_url_slug()),
}
return r
data_source_types = {
'json': _('JSON URL'),
'jsonp': _('JSONP URL'),
'geojson': _('GeoJSON URL'),
'formula': _('Python Expression (deprecated)'),
'jsonvalue': _('JSON Expression'),
}
if value.get('type') in data_source_types:
return '%s - %s' % (data_source_types[value.get('type')], value.get('value'))
from wcs.data_sources import NamedDataSource
data_source = NamedDataSource.get_by_slug(value['type'], stub_fallback=True)
return htmltext('<a href="%(url)s">%(label)s</a>') % {
'label': data_source.name,
'url': data_source.get_admin_url(),
}
def get_condition_parameter_view_value(self, widget):
if not self.condition or self.condition.get('type') == 'none':
return
return htmltext('<tt class="condition">%s</tt> <span class="condition-type">(%s)</span>') % (
self.condition['value'],
{'django': 'Django', 'python': 'Python'}.get(self.condition['type']),
)
def __repr__(self):
return '<%s %s %r>' % (self.__class__.__name__, self.id, self.label and self.label[:64])
def i18n_scan(self, base_location):
location = '%s%s/' % (base_location, self.id)
yield location, None, self.label
yield location, None, getattr(self, 'hint', None)
class WidgetField(Field):
hint = None
required = True
display_locations = ['validation', 'summary']
extra_attributes = []
prefill = {}
widget_class = None
use_live_server_validation = False
def add_to_form(self, form, value=None):
kwargs = {'required': self.required, 'render_br': False}
if value:
kwargs['value'] = value
for k in self.extra_attributes:
if hasattr(self, k):
kwargs[k] = getattr(self, k)
self.perform_more_widget_changes(form, kwargs)
if self.hint and self.hint.startswith('<'):
hint = htmltext(get_publisher().translate(self.hint))
else:
hint = get_publisher().translate(self.hint or '')
form.add(self.widget_class, 'f%s' % self.id, title=self.label, hint=hint, **kwargs)
widget = form.get_widget('f%s' % self.id)
widget.field = self
widget.use_live_server_validation = self.use_live_server_validation
if self.extra_css_class:
if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
else:
widget.extra_css_class = self.extra_css_class
if self.varname:
widget.div_id = 'var_%s' % self.varname
return widget
def perform_more_widget_changes(self, form, kwargs, edit=True):
pass
def add_to_view_form(self, form, value=None):
kwargs = {'render_br': False}
self.field_key = 'f%s' % self.id
self.perform_more_widget_changes(form, kwargs, False)
for k in self.extra_attributes:
if hasattr(self, k):
kwargs[k] = getattr(self, k)
if self.widget_class is StringWidget and 'size' not in kwargs and value:
# set a size if there is not one already defined, this will be for
# example the case with ItemField
kwargs['size'] = len(value)
form.add(
self.widget_class, self.field_key, title=self.label, value=value, readonly='readonly', **kwargs
)
widget = form.get_widget(self.field_key)
widget.transfer_form_value(get_request())
widget.field = self
if self.extra_css_class:
if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
else:
widget.extra_css_class = self.extra_css_class
return widget
def get_display_locations_options(self):
options = [
('validation', _('Validation Page')),
('summary', _('Summary Page')),
('listings', _('Management Listings')),
]
if self.allow_statistics:
options.append(('statistics', _('Statistics')))
return options
def get_anonymise_options(self):
if get_publisher().has_site_option('enable-intermediate-anonymisation', True):
return [
('final', _('Data deleted on final anonymisation'), 'final'),
(
'intermediate',
_('Data deleted on both intermediate and final anonymisation'),
'intermediate',
),
('no', _('Data kept after anonymisation'), 'no'),
]
else:
return [('final', _('Yes'), 'final'), ('no', _('No'), 'no')]
def fill_admin_form(self, form):
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
form.add(
CheckboxWidget,
'required',
title=_('Required'),
value=self.required,
default_value=self.__class__.required,
)
form.add(TextWidget, 'hint', title=_('Hint'), value=self.hint, cols=60, rows=3)
form.add(
VarnameWidget,
'varname',
title=_('Identifier'),
value=self.varname,
size=30,
advanced=False,
hint=_('This is used as suffix for variable names.'),
)
form.add(
CheckboxesWidget,
'display_locations',
title=_('Display Locations'),
options=self.get_display_locations_options(),
value=self.display_locations,
tab=('display', _('Display')),
default_value=self.__class__.display_locations,
)
form.add(
StringWidget,
'extra_css_class',
title=_('Extra classes for CSS styling'),
value=self.extra_css_class,
size=30,
tab=('display', _('Display')),
)
form.add(
PrefillSelectionWidget,
'prefill',
title=_('Prefill'),
value=self.prefill,
advanced=True,
field=self,
)
form.add(
ConditionWidget,
'condition',
title=_('Display Condition'),
value=self.condition,
required=False,
size=50,
allow_python=False,
tab=('display', _('Display')),
)
if 'anonymise' in self.get_admin_attributes():
# override anonymise flag default value
form.add(
RadiobuttonsWidget,
'anonymise',
title=_('Anonymisation'),
options=self.get_anonymise_options(),
value=self.anonymise,
advanced=True,
hint=_('Marks the field data for removal in the anonymisation processes.'),
default_value=self.__class__.anonymise,
)
def check_admin_form(self, form):
display_locations = form.get_widget('display_locations').parse()
varname = form.get_widget('varname').parse()
if 'statistics' in display_locations and not varname:
form.set_error(
'display_locations', _('Field must have a varname in order to be displayed in statistics.')
)
def get_admin_attributes(self):
return Field.get_admin_attributes(self) + [
'required',
'hint',
'varname',
'display_locations',
'extra_css_class',
'prefill',
]
def get_csv_heading(self):
return [self.label]
def get_value_info(self, data):
# return the selected value and an optional dictionary that will be
# passed to get_view_value() to provide additional details.
value_details = {}
if self.id not in data:
value = None
else:
if self.store_display_value and ('%s_display' % self.id) in data:
value = data['%s_display' % self.id]
value_details['value_id'] = data[self.id]
else:
value = data[self.id]
if value is None or value == '':
value = None
return (value, value_details)
def get_view_value(self, value, **kwargs):
return str(value) if value else ''
def get_view_short_value(self, value, max_len=30, **kwargs):
return self.get_view_value(value)
def get_csv_value(self, element, **kwargs):
if self.convert_value_to_str:
return [self.convert_value_to_str(element)]
return [element]
def get_fts_value(self, data, **kwargs):
if self.store_display_value:
return data.get('%s_display' % self.id)
return data.get(str(self.id))
field_classes = []
field_types = []
def register_field_class(klass):
if klass not in field_classes:
field_classes.append(klass)
field_types.append((klass.key, klass.description))
klass.init()
def get_field_class_by_type(type):
from .block import BlockField
for k in field_classes:
if k.key == type:
return k
if type.startswith('block:'):
# make sure block type exists (raises KeyError on missing data)
BlockDef.get_on_index(type[6:], 'slug')
return BlockField
raise KeyError()
def get_field_options(blacklisted_types):
from .computed import ComputedField
widgets, non_widgets = [], []
disabled_fields = (get_publisher().get_site_option('disabled-fields') or '').split(',')
disabled_fields = [f.strip() for f in disabled_fields if f.strip()]
order = [
'string',
'text',
'email',
'bool',
'file',
'date',
'item',
'items',
'table',
'table-select',
'tablerows',
'map',
'ranked-items',
'password',
'title',
'subtitle',
'comment',
'page',
'computed',
]
for klass in sorted(field_classes, key=lambda x: order.index(x.key)):
if klass is ComputedField:
continue
if klass.key in blacklisted_types:
continue
if klass.key in disabled_fields:
continue
if issubclass(klass, WidgetField):
widgets.append((klass.key, klass.description, klass.key))
else:
non_widgets.append((klass.key, klass.description, klass.key))
options = widgets + [('', '', '')] + non_widgets
if 'computed' not in blacklisted_types:
# add computed field in its own "section"
options.extend([('', '', ''), (ComputedField.key, ComputedField.description, ComputedField.key)])
if not blacklisted_types or 'blocks' not in blacklisted_types:
position = len(options)
for blockdef in BlockDef.select(order_by='name'):
options.append(('block:%s' % blockdef.slug, blockdef.name, 'block:%s' % blockdef.slug))
if len(options) != position:
# add separator
options.insert(position, ('', '', ''))
return options