4343 lines
154 KiB
Python
4343 lines
154 KiB
Python
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2010 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 base64
|
|
import collections
|
|
import copy
|
|
import datetime
|
|
import html
|
|
import os
|
|
import random
|
|
import re
|
|
import sys
|
|
import time
|
|
|
|
from django.utils.encoding import force_bytes, force_str, smart_str
|
|
from django.utils.formats import date_format as django_date_format
|
|
from django.utils.html import strip_tags, urlize
|
|
from lxml import etree as ET
|
|
from quixote import get_publisher, get_request, get_session
|
|
from quixote.html import TemplateIO, htmlescape, htmltag, htmltext
|
|
|
|
from wcs.sql_criterias import Contains
|
|
|
|
from . import data_sources, portfolio
|
|
from .blocks import BlockDef, BlockWidget
|
|
from .conditions import Condition
|
|
from .qommon import N_, _, evalutils, force_str, get_cfg, misc
|
|
from .qommon.form import (
|
|
AutocompleteStringWidget,
|
|
CheckboxesWidget,
|
|
CheckboxWidget,
|
|
CommentWidget,
|
|
CompositeWidget,
|
|
ComputedExpressionWidget,
|
|
ConditionWidget,
|
|
DateWidget,
|
|
EmailWidget,
|
|
FileSizeWidget,
|
|
FileWithPreviewWidget,
|
|
Form,
|
|
HiddenWidget,
|
|
HtmlWidget,
|
|
IntWidget,
|
|
JsonpSingleSelectWidget,
|
|
MapMarkerSelectionWidget,
|
|
MapWidget,
|
|
MultiSelectWidget,
|
|
PasswordEntryWidget,
|
|
RadiobuttonsWidget,
|
|
RankedItemsWidget,
|
|
RichTextWidget,
|
|
SingleSelectHintWidget,
|
|
SingleSelectTableWidget,
|
|
SingleSelectWidget,
|
|
StringWidget,
|
|
TableListRowsWidget,
|
|
TableWidget,
|
|
TextWidget,
|
|
ValidationWidget,
|
|
VarnameWidget,
|
|
WcsExtraStringWidget,
|
|
WidgetList,
|
|
WidgetListAsTable,
|
|
WysiwygTextWidget,
|
|
)
|
|
from .qommon.misc import (
|
|
check_carddefs,
|
|
check_formdefs,
|
|
check_wscalls,
|
|
date_format,
|
|
ellipsize,
|
|
get_as_datetime,
|
|
get_document_type_value_options,
|
|
strftime,
|
|
strip_some_tags,
|
|
xml_node_text,
|
|
)
|
|
from .qommon.ods import NS as OD_NS
|
|
from .qommon.ods import clean_text as od_clean_text
|
|
from .qommon.template import Template, TemplateError
|
|
from .qommon.upload_storage import PicklableUpload
|
|
from .sessions import BasicSession
|
|
|
|
|
|
class SetValueError(Exception):
|
|
pass
|
|
|
|
|
|
class MissingBlockFieldError(Exception):
|
|
def __init__(self, block_slug):
|
|
self.block_slug = block_slug
|
|
|
|
def __str__(self):
|
|
return force_str(_('Missing block field: %s') % self.block_slug)
|
|
|
|
|
|
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 value.get('type') == 'formula':
|
|
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
|
|
# can be overriden in field' settings
|
|
anonymise = True
|
|
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')
|
|
if include_id:
|
|
extra_fields = ['id']
|
|
else:
|
|
extra_fields = []
|
|
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):
|
|
for attribute in self.get_admin_attributes():
|
|
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
|
|
return changed
|
|
|
|
@staticmethod
|
|
def evaluate_condition(dict_vars, formdef, condition, record_errors=True):
|
|
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),
|
|
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)
|
|
if display_value:
|
|
data['%s_display' % self.id] = display_value
|
|
elif '%s_display' % self.id in data:
|
|
del data['%s_display' % self.id]
|
|
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
|
|
elif '%s_structured' % self.id in data:
|
|
del data['%s_structured' % self.id]
|
|
elif self.store_structured_value and '%s_structured' % self.id in data:
|
|
del data['%s_structured' % self.id]
|
|
|
|
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 .carddef import CardDef
|
|
|
|
carddef_slug = data_source_type.split(':')[1]
|
|
try:
|
|
yield CardDef.get_by_urlname(carddef_slug)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
from .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 check_wscalls(prefill.get('value'))
|
|
yield from check_carddefs(prefill.get('value'))
|
|
yield from check_formdefs(prefill.get('value'))
|
|
if getattr(self, 'condition', None):
|
|
condition = self.condition
|
|
if condition:
|
|
if condition.get('type') == 'django':
|
|
yield from check_wscalls(condition.get('value'))
|
|
yield from check_carddefs(condition.get('value'))
|
|
yield from check_formdefs(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 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(
|
|
CheckboxWidget,
|
|
'anonymise',
|
|
title=_('Anonymise'),
|
|
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)
|
|
if not issubclass(klass, WidgetField):
|
|
field_types.append((klass.key, klass.description))
|
|
else:
|
|
non_widgets = [x for x in field_classes if not issubclass(x, WidgetField)]
|
|
if not non_widgets:
|
|
field_types.append((klass.key, klass.description))
|
|
else:
|
|
idx = field_types.index([x for x in field_types if x[0] == non_widgets[0].key][0])
|
|
field_types.insert(idx, (klass.key, klass.description))
|
|
klass.init()
|
|
|
|
|
|
class TitleField(Field):
|
|
key = 'title'
|
|
description = _('Title')
|
|
html_tag = 'h3'
|
|
display_locations = ['validation', 'summary']
|
|
|
|
def add_to_form(self, form, value=None):
|
|
import wcs.workflows
|
|
|
|
extra_attributes = ' data-field-id="%s"' % self.id
|
|
if self.extra_css_class:
|
|
extra_attributes += ' class="%s"' % self.extra_css_class
|
|
title_markup = '<{html_tag}{extra_attributes}>%s</{html_tag}>'.format(
|
|
html_tag=self.html_tag,
|
|
extra_attributes=extra_attributes,
|
|
)
|
|
label = wcs.workflows.template_on_formdata(
|
|
None, get_publisher().translate(self.label), autoescape=False
|
|
)
|
|
widget = HtmlWidget(htmltext(title_markup) % label)
|
|
widget.field = self
|
|
form.widgets.append(widget)
|
|
return widget
|
|
|
|
add_to_view_form = add_to_form
|
|
|
|
def get_display_locations_options(self):
|
|
return [('validation', _('Validation Page')), ('summary', _('Summary Page'))]
|
|
|
|
def fill_admin_form(self, form):
|
|
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
|
|
form.add(
|
|
StringWidget,
|
|
'extra_css_class',
|
|
title=_('Extra classes for CSS styling'),
|
|
value=self.extra_css_class,
|
|
size=30,
|
|
tab=('display', _('Display')),
|
|
)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
allow_python=False,
|
|
tab=('display', _('Display')),
|
|
)
|
|
form.add(
|
|
CheckboxesWidget,
|
|
'display_locations',
|
|
title=_('Display Locations'),
|
|
options=self.get_display_locations_options(),
|
|
value=self.display_locations,
|
|
default_value=self.__class__.display_locations,
|
|
tab=('display', _('Display')),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return Field.get_admin_attributes(self) + ['extra_css_class', 'display_locations']
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield from check_wscalls(self.label)
|
|
yield from check_carddefs(self.label)
|
|
yield from check_formdefs(self.label)
|
|
|
|
|
|
register_field_class(TitleField)
|
|
|
|
|
|
class SubtitleField(TitleField):
|
|
key = 'subtitle'
|
|
description = _('Subtitle')
|
|
html_tag = 'h4'
|
|
|
|
|
|
register_field_class(SubtitleField)
|
|
|
|
|
|
class CommentField(Field):
|
|
key = 'comment'
|
|
description = _('Comment')
|
|
display_locations = []
|
|
|
|
def get_text(self):
|
|
import wcs.workflows
|
|
|
|
label = self.get_html_content()
|
|
return wcs.workflows.template_on_html_string(label)
|
|
|
|
def add_to_form(self, form, value=None):
|
|
widget = CommentWidget(content=self.get_text(), extra_css_class=self.extra_css_class)
|
|
form.widgets.append(widget)
|
|
widget.field = self
|
|
return widget
|
|
|
|
def add_to_view_form(self, *args, **kwargs):
|
|
if self.include_in_validation_page:
|
|
return self.add_to_form(*args, **kwargs)
|
|
return None
|
|
|
|
def get_html_content(self):
|
|
if not self.label:
|
|
return ''
|
|
label = get_publisher().translate(self.label)
|
|
if label.startswith('<'):
|
|
return label
|
|
if '\n\n' in label:
|
|
# blank lines to paragraphs
|
|
label = '</p>\n<p>'.join([str(htmlescape(x)) for x in re.split('\n\n+', label)])
|
|
return '<p>' + label + '</p>'
|
|
return '<p>%s</p>' % str(htmlescape(label))
|
|
|
|
def get_display_locations_options(self):
|
|
return [('validation', _('Validation Page')), ('summary', _('Summary Page'))]
|
|
|
|
def fill_admin_form(self, form):
|
|
if self.label and (self.label[0] != '<' and '[end]' in self.label):
|
|
form.add(
|
|
TextWidget,
|
|
'label',
|
|
title=_('Label'),
|
|
value=self.label,
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
required=True,
|
|
cols=70,
|
|
rows=3,
|
|
render_br=False,
|
|
)
|
|
else:
|
|
form.add(
|
|
WysiwygTextWidget,
|
|
'label',
|
|
title=_('Label'),
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
value=self.get_html_content(),
|
|
required=True,
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'extra_css_class',
|
|
title=_('Extra classes for CSS styling'),
|
|
value=self.extra_css_class,
|
|
size=30,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
allow_python=False,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
CheckboxesWidget,
|
|
'display_locations',
|
|
title=_('Display Locations'),
|
|
options=self.get_display_locations_options(),
|
|
value=self.display_locations,
|
|
advanced=True,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return Field.get_admin_attributes(self) + ['extra_css_class', 'display_locations']
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield from check_wscalls(self.label)
|
|
yield from check_carddefs(self.label)
|
|
yield from check_formdefs(self.label)
|
|
|
|
|
|
register_field_class(CommentField)
|
|
|
|
|
|
class StringField(WidgetField):
|
|
key = 'string'
|
|
description = _('Text (line)')
|
|
|
|
widget_class = WcsExtraStringWidget
|
|
size = None
|
|
extra_attributes = ['size']
|
|
validation = {}
|
|
data_source = {}
|
|
keep_raw_value = False
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
if self.data_source:
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source.can_jsonp():
|
|
kwargs['url'] = data_source.get_jsonp_url()
|
|
self.widget_class = AutocompleteStringWidget
|
|
|
|
@property
|
|
def use_live_server_validation(self):
|
|
if self.validation and self.validation['type']:
|
|
return bool(ValidationWidget.validation_methods.get(self.validation['type']))
|
|
return False
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
if self.size:
|
|
form.add(
|
|
StringWidget,
|
|
'size',
|
|
title=_('Line length'),
|
|
hint=_(
|
|
'Deprecated option, it is advised to use CSS classes '
|
|
'to size the fields in a manner compatible with all devices.'
|
|
),
|
|
value=self.size,
|
|
)
|
|
else:
|
|
form.add(HiddenWidget, 'size', value=None)
|
|
form.add(
|
|
ValidationWidget,
|
|
'validation',
|
|
title=_('Validation'),
|
|
value=self.validation,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
data_sources.DataSourceSelectionWidget,
|
|
'data_source',
|
|
value=self.data_source,
|
|
title=_('Data Source'),
|
|
hint=_('This will allow autocompletion from an external source.'),
|
|
disallowed_source_types={'geojson'},
|
|
advanced=True,
|
|
required=False,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + ['size', 'validation', 'data_source', 'anonymise']
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
value = value or ''
|
|
if value.startswith('http://') or value.startswith('https://'):
|
|
charset = get_publisher().site_charset
|
|
value = force_str(value, charset)
|
|
return htmltext(force_str(urlize(value, nofollow=True, autoescape=True)))
|
|
return str(value)
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
if value.startswith('http://') or value.startswith('https://'):
|
|
node = ET.Element('{%s}a' % OD_NS['text'])
|
|
node.attrib['{%s}href' % OD_NS['xlink']] = value
|
|
else:
|
|
node = ET.Element('{%s}span' % OD_NS['text'])
|
|
node.text = od_clean_text(force_str(value))
|
|
return node
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
return indent + str(value or '')
|
|
|
|
def convert_value_from_str(self, value):
|
|
return value
|
|
|
|
def convert_value_to_str(self, value):
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, (time.struct_time, datetime.date)):
|
|
return strftime(date_format(), value)
|
|
return str(value)
|
|
|
|
@classmethod
|
|
def convert_value_from_anything(cls, value):
|
|
if value is None:
|
|
return None
|
|
return str(value)
|
|
|
|
def get_fts_value(self, data, **kwargs):
|
|
value = super().get_fts_value(data, **kwargs)
|
|
if value and self.validation and self.validation['type']:
|
|
validation_method = ValidationWidget.validation_methods.get(self.validation['type'])
|
|
if validation_method and validation_method.get('normalize_for_fts'):
|
|
# index both original and normalized value
|
|
# in the case of phone numbers this makes sure the "international/E164"
|
|
# format (ex: +33199001234) is indexed.
|
|
value = '%s %s' % (value, validation_method.get('normalize_for_fts')(value))
|
|
return value
|
|
|
|
def migrate(self):
|
|
changed = super().migrate()
|
|
if isinstance(self.validation, str): # 2019-08-10
|
|
self.validation = {'type': 'regex', 'value': self.validation}
|
|
changed = True
|
|
return changed
|
|
|
|
def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(elem, charset, include_id=include_id)
|
|
self.migrate()
|
|
|
|
def get_validation_parameter_view_value(self, widget):
|
|
if not self.validation:
|
|
return
|
|
validation_type = self.validation['type']
|
|
validation_types = {x: y['title'] for x, y in ValidationWidget.validation_methods.items()}
|
|
if validation_type in ('regex', 'django'):
|
|
validation_value = self.validation.get('value')
|
|
if not validation_value:
|
|
return
|
|
return '%s - %s' % (validation_types.get(validation_type), validation_value)
|
|
return str(validation_types.get(validation_type))
|
|
|
|
def i18n_scan(self, base_location):
|
|
location = '%s%s/' % (base_location, self.id)
|
|
yield from super().i18n_scan(base_location)
|
|
if self.validation and self.validation.get('error_message'):
|
|
yield location, None, self.validation.get('error_message')
|
|
|
|
|
|
register_field_class(StringField)
|
|
|
|
|
|
class TextField(WidgetField):
|
|
key = 'text'
|
|
description = _('Long Text')
|
|
|
|
widget_class = TextWidget
|
|
cols = None
|
|
rows = None
|
|
pre = None
|
|
display_mode = 'plain'
|
|
maxlength = None
|
|
extra_attributes = ['cols', 'rows', 'maxlength']
|
|
|
|
def migrate(self):
|
|
changed = super().migrate()
|
|
if isinstance(getattr(self, 'pre', None), bool): # 2022-09-16
|
|
if self.pre:
|
|
self.display_mode = 'pre'
|
|
else:
|
|
self.display_mode = 'plain'
|
|
self.pre = None
|
|
changed = True
|
|
return changed
|
|
|
|
def perform_more_widget_changes(self, *args, **kwargs):
|
|
if self.display_mode == 'rich':
|
|
self.widget_class = RichTextWidget
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
if self.cols:
|
|
form.add(
|
|
StringWidget,
|
|
'cols',
|
|
title=_('Line length'),
|
|
hint=_(
|
|
'Deprecated option, it is advised to use CSS classes '
|
|
'to size the fields in a manner compatible with all devices.'
|
|
),
|
|
value=self.cols,
|
|
)
|
|
else:
|
|
form.add(HiddenWidget, 'cols', value=None)
|
|
form.add(StringWidget, 'rows', title=_('Number of rows'), value=self.rows)
|
|
form.add(StringWidget, 'maxlength', title=_('Maximum number of characters'), value=self.maxlength)
|
|
display_options = []
|
|
if get_publisher().get_site_option('enable-richtext-field'):
|
|
display_options += [
|
|
('rich', _('Rich Text')),
|
|
]
|
|
display_options += [
|
|
('plain', _('Plain Text (with automatic paragraphs on blank lines)')),
|
|
('pre', _('Plain Text (with linebreaks as typed)')),
|
|
]
|
|
form.add(
|
|
RadiobuttonsWidget,
|
|
'display_mode',
|
|
title=_('Text display'),
|
|
value=self.display_mode,
|
|
default_value='plain',
|
|
options=display_options,
|
|
advanced=True,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'cols',
|
|
'rows',
|
|
'display_mode',
|
|
'maxlength',
|
|
'anonymise',
|
|
]
|
|
|
|
def convert_value_from_str(self, value):
|
|
return value
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
if self.display_mode == 'pre':
|
|
return htmltext('<p class="plain-text-pre">') + value + htmltext('</p>')
|
|
elif self.display_mode == 'rich':
|
|
return htmltext(strip_some_tags(value, RichTextWidget.ALL_TAGS))
|
|
else:
|
|
try:
|
|
return (
|
|
htmltext('<p>')
|
|
+ htmltext('\n').join([(x or htmltext('</p><p>')) for x in value.splitlines()])
|
|
+ htmltext('</p>')
|
|
)
|
|
except Exception:
|
|
return ''
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
if self.display_mode == 'rich':
|
|
return
|
|
paragraphs = []
|
|
for paragraph in value.splitlines():
|
|
if paragraph.strip():
|
|
p = ET.Element('{%s}p' % OD_NS['text'])
|
|
p.text = paragraph
|
|
paragraphs.append(p)
|
|
return paragraphs
|
|
|
|
def get_view_short_value(self, value, max_len=30, **kwargs):
|
|
if self.display_mode == 'rich':
|
|
return ellipsize(str(strip_tags(value)), max_len)
|
|
return ellipsize(str(value), max_len)
|
|
|
|
def get_json_value(self, value, **kwargs):
|
|
if self.display_mode == 'rich':
|
|
return str(self.get_view_value(value))
|
|
return value
|
|
|
|
|
|
register_field_class(TextField)
|
|
|
|
|
|
class EmailField(WidgetField):
|
|
key = 'email'
|
|
description = _('Email')
|
|
use_live_server_validation = True
|
|
|
|
widget_class = EmailWidget
|
|
|
|
def convert_value_from_str(self, value):
|
|
return value
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
return htmltext('<a href="mailto:%s">%s</a>') % (value, value)
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
return indent + value
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
a = ET.Element('{%s}a' % OD_NS['text'])
|
|
a.text = od_clean_text(value)
|
|
a.attrib['{%s}href' % OD_NS['xlink']] = 'mailto:' + a.text
|
|
return a
|
|
|
|
|
|
register_field_class(EmailField)
|
|
|
|
|
|
class BoolField(WidgetField):
|
|
key = 'bool'
|
|
description = _('Check Box (single choice)')
|
|
allow_complex = True
|
|
allow_statistics = True
|
|
|
|
widget_class = CheckboxWidget
|
|
required = False
|
|
anonymise = False
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
if not edit:
|
|
kwargs['disabled'] = 'disabled'
|
|
value = get_request().get_field(self.field_key)
|
|
form.add_hidden(self.field_key, value=str(value))
|
|
widget = form.get_widget(self.field_key)
|
|
widget.field = self
|
|
if value and not value == 'False':
|
|
self.field_key = 'f%sdisabled' % self.id
|
|
get_request().form[self.field_key] = 'yes'
|
|
self.field_key = 'f%sdisabled' % self.id
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
if value is True or value == 'True':
|
|
return str(_('Yes'))
|
|
elif value is False or value == 'False':
|
|
return str(_('No'))
|
|
else:
|
|
return ''
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
span = ET.Element('{%s}span' % OD_NS['text'])
|
|
span.text = od_clean_text(self.get_view_value(value))
|
|
return span
|
|
|
|
def convert_value_from_anything(self, value):
|
|
if isinstance(value, str):
|
|
return self.convert_value_from_str(value)
|
|
return bool(value)
|
|
|
|
def convert_value_from_str(self, value):
|
|
if value is None:
|
|
return None
|
|
for true_word in (N_('True'), N_('Yes')):
|
|
if str(value).lower() in (true_word.lower(), _(true_word).lower()):
|
|
return True
|
|
return False
|
|
|
|
def convert_value_to_str(self, value):
|
|
if value is True:
|
|
return 'True'
|
|
elif value is False:
|
|
return 'False'
|
|
return value
|
|
|
|
def stats(self, values):
|
|
no_records = len(values)
|
|
if not no_records:
|
|
return
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<table class="stats">')
|
|
r += htmltext('<thead><tr><th colspan="4">')
|
|
r += self.label
|
|
r += htmltext('</th></tr></thead>')
|
|
options = (True, False)
|
|
r += htmltext('<tbody>')
|
|
for o in options:
|
|
r += htmltext('<tr>')
|
|
r += htmltext('<td class="label">')
|
|
if o is True:
|
|
r += str(_('Yes'))
|
|
value = True
|
|
else:
|
|
r += str(_('No'))
|
|
value = False
|
|
r += htmltext('</td>')
|
|
no = len([None for x in values if self.convert_value_from_str(x.data.get(self.id)) is value])
|
|
|
|
r += htmltext('<td class="percent">')
|
|
r += htmltext(' %.2f %%') % (100.0 * no / no_records)
|
|
r += htmltext('</td>')
|
|
r += htmltext('<td class="total">')
|
|
r += '(%d/%d)' % (no, no_records)
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('<tr>')
|
|
r += htmltext('<td class="bar" colspan="3">')
|
|
r += htmltext('<span style="width: %d%%"></span>' % (100 * no / no_records))
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('</tbody>')
|
|
r += htmltext('</table>')
|
|
return r.getvalue()
|
|
|
|
def from_json_value(self, value):
|
|
if value is None:
|
|
return value
|
|
return bool(value)
|
|
|
|
|
|
register_field_class(BoolField)
|
|
|
|
|
|
class FileField(WidgetField):
|
|
key = 'file'
|
|
description = _('File Upload')
|
|
allow_complex = True
|
|
|
|
document_type = None
|
|
max_file_size = None
|
|
automatic_image_resize = False
|
|
allow_portfolio_picking = False
|
|
storage = 'default'
|
|
|
|
widget_class = FileWithPreviewWidget
|
|
extra_attributes = [
|
|
'file_type',
|
|
'max_file_size',
|
|
'allow_portfolio_picking',
|
|
'automatic_image_resize',
|
|
'storage',
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.document_type = self.document_type or {}
|
|
|
|
@property
|
|
def file_type(self):
|
|
return (self.document_type or {}).get('mimetypes', [])
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
options = get_document_type_value_options(self.document_type)
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'document_type',
|
|
title=_('File type suggestion'),
|
|
value=self.document_type,
|
|
options=options,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
FileSizeWidget,
|
|
'max_file_size',
|
|
title=_('Max file size'),
|
|
value=self.max_file_size,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'automatic_image_resize',
|
|
title=_('Automatically resize uploaded images'),
|
|
value=self.automatic_image_resize,
|
|
advanced=True,
|
|
)
|
|
if portfolio.has_portfolio():
|
|
form.add(
|
|
CheckboxWidget,
|
|
'allow_portfolio_picking',
|
|
title=_('Allow user to pick a file from a portfolio'),
|
|
value=self.allow_portfolio_picking,
|
|
advanced=True,
|
|
)
|
|
storages = get_publisher().get_site_storages()
|
|
if storages:
|
|
storage_options = [('default', '---', {})]
|
|
storage_options += [(key, value['label'], key) for key, value in storages.items()]
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'storage',
|
|
title=_('File storage system'),
|
|
value=self.storage,
|
|
options=storage_options,
|
|
advanced=True,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'document_type',
|
|
'max_file_size',
|
|
'allow_portfolio_picking',
|
|
'automatic_image_resize',
|
|
'storage',
|
|
]
|
|
|
|
@classmethod
|
|
def convert_value_from_anything(cls, value):
|
|
if not value:
|
|
return None
|
|
from wcs.variables import LazyFieldVarFile
|
|
|
|
if isinstance(value, LazyFieldVarFile):
|
|
value = value.get_value() # unbox
|
|
if hasattr(value, 'base_filename'):
|
|
upload = PicklableUpload(value.base_filename, value.content_type or 'application/octet-stream')
|
|
upload.receive([value.get_content()])
|
|
return upload
|
|
from wcs.workflows import NamedAttachmentsSubstitutionProxy
|
|
|
|
if isinstance(value, NamedAttachmentsSubstitutionProxy):
|
|
upload = PicklableUpload(value.filename, value.content_type)
|
|
upload.receive([value.content])
|
|
return upload
|
|
if isinstance(value, dict):
|
|
# if value is a dictionary we expect it to have a content or
|
|
# b64_content key and a filename keys and an optional
|
|
# content_type key.
|
|
if 'b64_content' in value:
|
|
value_content = base64.decodebytes(force_bytes(value['b64_content']))
|
|
else:
|
|
value_content = value.get('content')
|
|
if 'filename' in value and value_content:
|
|
content_type = value.get('content_type') or 'application/octet-stream'
|
|
if content_type.startswith('text/'):
|
|
charset = 'utf-8'
|
|
else:
|
|
charset = None
|
|
upload = PicklableUpload(value['filename'], content_type, charset)
|
|
upload.receive([force_bytes(value_content)])
|
|
return upload
|
|
raise ValueError('invalid data for file type (%r)' % value)
|
|
|
|
def get_view_short_value(self, value, max_len=30, **kwargs):
|
|
return self.get_view_value(value, include_image_thumbnail=False, max_len=max_len, **kwargs)
|
|
|
|
def get_prefill_value(self, user=None, force_string=True):
|
|
return super().get_prefill_value(user=user, force_string=False)
|
|
|
|
def get_download_query_string(self, **kwargs):
|
|
if kwargs.get('file_value'):
|
|
return 'hash=%s' % kwargs.get('file_value').file_digest()
|
|
if kwargs.get('parent_field'):
|
|
return 'f=%s$%s$%s' % (kwargs['parent_field'].id, kwargs['parent_field_index'], self.id)
|
|
return 'f=%s' % self.id
|
|
|
|
def get_view_value(self, value, include_image_thumbnail=True, max_len=None, **kwargs):
|
|
show_link = True
|
|
if value.has_redirect_url():
|
|
is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
|
|
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
|
|
t = TemplateIO(html=True)
|
|
t += htmltext('<div class="file-field">')
|
|
if show_link or include_image_thumbnail:
|
|
download_qs = self.get_download_query_string(**kwargs)
|
|
if show_link:
|
|
attrs = {
|
|
'href': '[download]?%s' % download_qs,
|
|
}
|
|
if kwargs.get('label_id'):
|
|
attrs['aria-describedby'] = kwargs.get('label_id')
|
|
if max_len:
|
|
attrs['title'] = value
|
|
t += htmltag('a', **attrs)
|
|
if include_image_thumbnail and value.can_thumbnail():
|
|
t += htmltext('<img alt="" src="[download]?%s&thumbnail=1"/>') % download_qs
|
|
filename = str(value)
|
|
if max_len and len(filename) > max_len:
|
|
basename, ext = os.path.splitext(filename)
|
|
basename = ellipsize(basename, max_len - 5)
|
|
filename = basename + ext
|
|
t += htmltext('<span>%s</span>') % filename
|
|
if show_link:
|
|
t += htmltext('</a>')
|
|
t += htmltext('</div>')
|
|
return t.getvalue()
|
|
|
|
def get_download_url(self, formdata, **kwargs):
|
|
return '%s?%s' % (formdata.get_file_base_url(), self.get_download_query_string(**kwargs))
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
show_link = True
|
|
if value.has_redirect_url():
|
|
is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
|
|
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
|
|
if show_link and formdata:
|
|
node = ET.Element('{%s}a' % OD_NS['text'])
|
|
node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata, **kwargs)
|
|
else:
|
|
node = ET.Element('{%s}span' % OD_NS['text'])
|
|
node.text = od_clean_text(force_str(value))
|
|
return node
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
return [str(element) if element else '']
|
|
|
|
def get_json_value(self, value, formdata=None, include_file_content=True, **kwargs):
|
|
out = value.get_json_value(include_file_content=include_file_content)
|
|
if formdata:
|
|
out['url'] = self.get_download_url(formdata, file_value=value, **kwargs)
|
|
if value and misc.can_thumbnail(value.content_type):
|
|
out['thumbnail_url'] = out['url'] + '&thumbnail=1'
|
|
out['field_id'] = self.id
|
|
return out
|
|
|
|
def from_json_value(self, value):
|
|
if value and 'filename' in value and 'content' in value:
|
|
content = base64.b64decode(value['content'])
|
|
content_type = value.get('content_type', 'application/octet-stream')
|
|
if content_type.startswith('text/'):
|
|
charset = 'utf-8'
|
|
else:
|
|
charset = None
|
|
upload = PicklableUpload(value['filename'], content_type, charset)
|
|
upload.receive([content])
|
|
return upload
|
|
return None
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
if not edit:
|
|
value = get_request().get_field(self.field_key)
|
|
if value and hasattr(value, 'token'):
|
|
get_request().form[self.field_key + '$token'] = value.token
|
|
|
|
def export_to_xml(self, charset, include_id=False):
|
|
# convert some sub-fields to strings as export_to_xml() only supports
|
|
# dictionnaries with strings values
|
|
if self.document_type and self.document_type.get('mimetypes'):
|
|
old_value = self.document_type['mimetypes']
|
|
self.document_type['mimetypes'] = '|'.join(self.document_type['mimetypes'])
|
|
result = super().export_to_xml(charset, include_id=include_id)
|
|
if self.document_type and self.document_type.get('mimetypes'):
|
|
self.document_type['mimetypes'] = old_value
|
|
return result
|
|
|
|
def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(elem, charset, include_id=include_id)
|
|
# translate fields flattened to strings
|
|
if self.document_type and self.document_type.get('mimetypes'):
|
|
self.document_type['mimetypes'] = self.document_type['mimetypes'].split('|')
|
|
if self.document_type and self.document_type.get('fargo'):
|
|
self.document_type['fargo'] = self.document_type['fargo'] == 'True'
|
|
|
|
|
|
register_field_class(FileField)
|
|
|
|
|
|
class DateField(WidgetField):
|
|
key = 'date'
|
|
description = _('Date')
|
|
|
|
widget_class = DateWidget
|
|
minimum_date = None
|
|
maximum_date = None
|
|
minimum_is_future = False
|
|
date_in_the_past = False
|
|
date_can_be_today = False
|
|
extra_attributes = [
|
|
'minimum_date',
|
|
'minimum_is_future',
|
|
'maximum_date',
|
|
'date_in_the_past',
|
|
'date_can_be_today',
|
|
]
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
form.add(DateWidget, 'minimum_date', title=_('Minimum Date'), value=self.minimum_date)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'minimum_is_future',
|
|
title=_('Date must be in the future'),
|
|
value=self.minimum_is_future,
|
|
hint=_('This option is obviously not compatible with setting a minimum date'),
|
|
)
|
|
form.add(DateWidget, 'maximum_date', title=_('Maximum Date'), value=self.maximum_date)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'date_in_the_past',
|
|
title=_('Date must be in the past'),
|
|
value=self.date_in_the_past,
|
|
hint=_('This option is obviously not compatible with setting a maximum date'),
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'date_can_be_today',
|
|
title=_('Date can be present day'),
|
|
value=self.date_can_be_today,
|
|
hint=_('This option is only useful combined with one of the previous checkboxes.'),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'minimum_date',
|
|
'minimum_is_future',
|
|
'maximum_date',
|
|
'date_in_the_past',
|
|
'date_can_be_today',
|
|
'anonymise',
|
|
]
|
|
|
|
@classmethod
|
|
def convert_value_from_anything(cls, value):
|
|
if value is None or value == '':
|
|
return None
|
|
date_value = evalutils.make_date(value).timetuple() # could raise ValueError
|
|
return date_value
|
|
|
|
def convert_value_from_str(self, value):
|
|
if not value:
|
|
return None
|
|
try:
|
|
return get_as_datetime(value).timetuple()
|
|
except ValueError:
|
|
return None
|
|
|
|
def convert_value_to_str(self, value):
|
|
if value is None:
|
|
return ''
|
|
if isinstance(value, str):
|
|
return value
|
|
try:
|
|
return strftime(date_format(), value)
|
|
except TypeError:
|
|
return ''
|
|
|
|
def add_to_form(self, form, value=None):
|
|
if value and not isinstance(value, str):
|
|
value = self.convert_value_to_str(value)
|
|
return WidgetField.add_to_form(self, form, value=value)
|
|
|
|
def add_to_view_form(self, form, value=None):
|
|
value = strftime(misc.date_format(), value)
|
|
return super().add_to_view_form(form, value=value)
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
try:
|
|
return strftime(misc.date_format(), value)
|
|
except TypeError:
|
|
return value
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
span = ET.Element('{%s}span' % OD_NS['text'])
|
|
span.text = od_clean_text(self.get_view_value(value))
|
|
return span
|
|
|
|
def get_json_value(self, value, **kwargs):
|
|
try:
|
|
return strftime('%Y-%m-%d', value)
|
|
except TypeError:
|
|
return ''
|
|
|
|
def from_json_value(self, value):
|
|
try:
|
|
return time.strptime(value, '%Y-%m-%d')
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
register_field_class(DateField)
|
|
|
|
|
|
def item_items_stats(field, values):
|
|
if field.data_source:
|
|
options = data_sources.get_items(field.data_source)
|
|
else:
|
|
options = field.items or []
|
|
if len(options) == 0:
|
|
return None
|
|
no_records = len(values)
|
|
if no_records == 0:
|
|
return None
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<table class="stats">')
|
|
r += htmltext('<thead><tr><th colspan="4">')
|
|
r += field.label
|
|
r += htmltext('</th></tr></thead>')
|
|
r += htmltext('<tbody>')
|
|
for option in options:
|
|
if type(option) in (tuple, list):
|
|
option_label = option[1]
|
|
option_value = str(option[0])
|
|
else:
|
|
option_label = option
|
|
option_value = option
|
|
if field.key == 'item':
|
|
no = len([None for x in values if x.data.get(field.id) == option_value])
|
|
else:
|
|
no = len([None for x in values if option_value in (x.data.get(field.id) or [])])
|
|
r += htmltext('<tr>')
|
|
r += htmltext('<td class="label">')
|
|
r += option_label
|
|
r += htmltext('</td>')
|
|
|
|
r += htmltext('<td class="percent">')
|
|
r += htmltext(' %.2f %%') % (100.0 * no / no_records)
|
|
r += htmltext('</td>')
|
|
r += htmltext('<td class="total">')
|
|
r += '(%d/%d)' % (no, no_records)
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('<tr>')
|
|
r += htmltext('<td class="bar" colspan="3">')
|
|
r += htmltext('<span style="width: %d%%"></span>' % (100 * no / no_records))
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('</tbody>')
|
|
r += htmltext('</table>')
|
|
return r.getvalue()
|
|
|
|
|
|
class MapOptionsMixin:
|
|
initial_zoom = None
|
|
min_zoom = None
|
|
max_zoom = None
|
|
|
|
@classmethod
|
|
def get_zoom_levels(cls):
|
|
zoom_levels = [
|
|
(None, '---'),
|
|
('0', _('Whole world')),
|
|
('6', _('Country')),
|
|
('9', _('Wide area')),
|
|
('11', _('Area')),
|
|
('13', _('Town')),
|
|
('16', _('Small road')),
|
|
('18', _('Neighbourhood')),
|
|
('19', _('Ant')),
|
|
]
|
|
return zoom_levels
|
|
|
|
def fill_zoom_admin_form(self, form, **kwargs):
|
|
zoom_levels = self.get_zoom_levels()
|
|
zoom_levels_dict = dict(zoom_levels)
|
|
default_zoom_level = get_publisher().get_default_zoom_level()
|
|
initial_zoom_levels = zoom_levels[:]
|
|
initial_zoom_levels[0] = (None, _('Default (%s)') % zoom_levels_dict[default_zoom_level])
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'initial_zoom',
|
|
title=_('Initial zoom level'),
|
|
value=self.initial_zoom,
|
|
options=initial_zoom_levels,
|
|
**kwargs,
|
|
)
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'min_zoom',
|
|
title=_('Minimal zoom level'),
|
|
value=self.min_zoom,
|
|
options=zoom_levels,
|
|
required=False,
|
|
**kwargs,
|
|
)
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'max_zoom',
|
|
title=_('Maximal zoom level'),
|
|
value=self.max_zoom,
|
|
options=zoom_levels,
|
|
required=False,
|
|
**kwargs,
|
|
)
|
|
|
|
def check_zoom_admin_form(self, form):
|
|
initial_zoom = form.get_widget('initial_zoom').parse()
|
|
min_zoom = form.get_widget('min_zoom').parse()
|
|
max_zoom = form.get_widget('max_zoom').parse()
|
|
if min_zoom and max_zoom:
|
|
if int(min_zoom) > int(max_zoom):
|
|
form.get_widget('min_zoom').set_error(
|
|
_('Minimal zoom level cannot be greater than maximal zoom level.')
|
|
)
|
|
# noqa pylint: disable=too-many-boolean-expressions
|
|
if (initial_zoom and min_zoom and int(initial_zoom) < int(min_zoom)) or (
|
|
initial_zoom and max_zoom and int(initial_zoom) > int(max_zoom)
|
|
):
|
|
form.get_widget('initial_zoom').set_error(
|
|
_('Initial zoom level must be between minimal and maximal zoom levels.')
|
|
)
|
|
|
|
|
|
class ItemFieldMixin:
|
|
def get_real_data_source(self):
|
|
return data_sources.get_real(self.data_source)
|
|
|
|
def add_items_fields_admin_form(self, form):
|
|
real_data_source = self.get_real_data_source()
|
|
form.add(
|
|
RadiobuttonsWidget,
|
|
'data_mode',
|
|
title=_('Data'),
|
|
options=[
|
|
('simple-list', _('Simple List'), 'simple-list'),
|
|
('data-source', _('Data Source'), 'data-source'),
|
|
],
|
|
value='data-source' if real_data_source else 'simple-list',
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
extra_css_class='widget-inline-radio no-bottom-margin',
|
|
)
|
|
form.add(
|
|
WidgetList,
|
|
'items',
|
|
element_type=StringWidget,
|
|
value=self.items,
|
|
required=False,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add item'),
|
|
attrs={'data-dynamic-display-child-of': 'data_mode', 'data-dynamic-display-value': 'simple-list'},
|
|
extra_css_class='sortable',
|
|
)
|
|
form.add(
|
|
data_sources.DataSourceSelectionWidget,
|
|
'data_source',
|
|
value=self.data_source,
|
|
required=False,
|
|
hint=_('This will get the available items from an external source.'),
|
|
disallowed_source_types={'geojson'},
|
|
attrs={'data-dynamic-display-child-of': 'data_mode', 'data-dynamic-display-value': 'data-source'},
|
|
)
|
|
|
|
def check_items_admin_form(self, form):
|
|
data_mode = form.get_widget('data_mode').parse()
|
|
if data_mode == 'simple-list':
|
|
items = form.get_widget('items').parse()
|
|
d = {}
|
|
for v in items or []:
|
|
if v in d:
|
|
form.set_error('items', _('Duplicated Items'))
|
|
return
|
|
d[v] = None
|
|
|
|
data_source_type = form.get_widget('data_source').get_widget('type')
|
|
data_source_type.set_value(None)
|
|
data_source_type.transfer_form_value(get_request())
|
|
|
|
def get_items_parameter_view_label(self):
|
|
if self.data_source:
|
|
# skip field if there's a data source
|
|
return None
|
|
return str(_('Choices'))
|
|
|
|
def get_data_source_parameter_view_label(self):
|
|
return str(_('Data source'))
|
|
|
|
def get_carddef(self):
|
|
from wcs.carddef import CardDef
|
|
|
|
if (
|
|
not self.data_source
|
|
or not self.data_source.get('type')
|
|
or not self.data_source['type'].startswith('carddef:')
|
|
):
|
|
return None
|
|
|
|
carddef_slug = self.data_source['type'].split(':')[1]
|
|
try:
|
|
return CardDef.get_by_urlname(carddef_slug)
|
|
except KeyError:
|
|
return None
|
|
|
|
def get_extended_options(self):
|
|
if self.data_source:
|
|
return data_sources.get_structured_items(
|
|
self.data_source, mode='lazy', include_disabled=self.display_disabled_items
|
|
)
|
|
if self.items:
|
|
return [{'id': x, 'text': x} for x in self.items]
|
|
return []
|
|
|
|
def export_to_json_data_source(self, field):
|
|
if 'items' in field:
|
|
del field['items']
|
|
data_source_type = self.data_source.get('type')
|
|
if data_source_type and data_source_type.startswith('carddef:'):
|
|
carddef_slug = data_source_type.split(':')[1]
|
|
url = None
|
|
if data_source_type.count(':') == 1:
|
|
url = '/api/cards/%s/list' % carddef_slug
|
|
else:
|
|
custom_view_slug = data_source_type.split(':')[2]
|
|
if not custom_view_slug.startswith('_'):
|
|
url = '/api/cards/%s/%s/list' % (carddef_slug, custom_view_slug)
|
|
if url:
|
|
field['items_url'] = get_request().build_absolute_uri(url)
|
|
return field
|
|
|
|
def i18n_scan(self, base_location):
|
|
location = '%s%s/' % (base_location, self.id)
|
|
real_data_source = self.get_real_data_source()
|
|
if not real_data_source:
|
|
for item in self.items or []:
|
|
yield location, None, item
|
|
|
|
def get_display_value(self, value):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source is None:
|
|
return get_publisher().translate(value) or ''
|
|
|
|
if data_source.type == 'jsonp':
|
|
if not get_session():
|
|
return value
|
|
if not get_session().jsonp_display_values:
|
|
get_session().jsonp_display_values = {}
|
|
return get_session().jsonp_display_values.get('%s_%s' % (data_source.get_jsonp_url(), value))
|
|
|
|
display_value = data_source.get_display_value(value)
|
|
session = get_session()
|
|
if (
|
|
isinstance(session, BasicSession)
|
|
and self.display_mode == 'autocomplete'
|
|
and data_source
|
|
and data_source.can_jsonp()
|
|
):
|
|
carddef = self.get_carddef()
|
|
url_kwargs = {}
|
|
if self.key == 'item' and get_request().is_in_backoffice() and carddef:
|
|
url_kwargs['with_related'] = True
|
|
# store display value in session to be used by select2
|
|
url = data_source.get_jsonp_url(**url_kwargs)
|
|
if not session.jsonp_display_values:
|
|
session.jsonp_display_values = {}
|
|
session.jsonp_display_values['%s_%s' % (url, value)] = display_value
|
|
|
|
return display_value
|
|
|
|
|
|
class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|
key = 'item'
|
|
description = _('List')
|
|
allow_complex = True
|
|
allow_statistics = True
|
|
|
|
items = []
|
|
show_as_radio = None
|
|
anonymise = False
|
|
widget_class = SingleSelectHintWidget
|
|
data_source = {}
|
|
in_filters = False
|
|
display_disabled_items = False
|
|
display_mode = 'list'
|
|
|
|
# <select> option
|
|
use_hint_as_first_option = True
|
|
|
|
# timetable option
|
|
initial_date_alignment = None
|
|
|
|
# map options
|
|
initial_position = None
|
|
default_position = None
|
|
position_template = None
|
|
|
|
def __init__(self, **kwargs):
|
|
self.items = []
|
|
WidgetField.__init__(self, **kwargs)
|
|
|
|
def migrate(self):
|
|
changed = super().migrate()
|
|
if isinstance(getattr(self, 'show_as_radio', None), bool): # 2019-03-19
|
|
if self.show_as_radio:
|
|
self.display_mode = 'radio'
|
|
else:
|
|
self.display_mode = 'list'
|
|
self.show_as_radio = None
|
|
changed = True
|
|
return changed
|
|
|
|
def init_with_xml(self, elem, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(elem, charset, include_id=include_id)
|
|
if getattr(elem.find('show_as_radio'), 'text', None) == 'True':
|
|
self.display_mode = 'radio'
|
|
|
|
@property
|
|
def extra_attributes(self):
|
|
if self.display_mode == 'map':
|
|
return [
|
|
'initial_zoom',
|
|
'min_zoom',
|
|
'max_zoom',
|
|
'initial_position',
|
|
'position_template',
|
|
'data_source',
|
|
]
|
|
return []
|
|
|
|
def get_options(self, mode=None):
|
|
if self.data_source:
|
|
return [
|
|
x[:3]
|
|
for x in data_sources.get_items(
|
|
self.data_source, mode=mode, include_disabled=self.display_disabled_items
|
|
)
|
|
]
|
|
if self.items:
|
|
return [(x, get_publisher().translate(x), x) for x in self.items]
|
|
return []
|
|
|
|
def get_id_by_option_text(self, text_value):
|
|
if self.data_source:
|
|
return data_sources.get_id_by_option_text(self.data_source, text_value)
|
|
return text_value
|
|
|
|
def get_display_mode(self, data_source=None):
|
|
if not data_source:
|
|
data_source = data_sources.get_object(self.data_source)
|
|
|
|
if data_source and data_source.type == 'jsonp':
|
|
# a source defined as JSONP can only be used in autocomplete mode
|
|
return 'autocomplete'
|
|
|
|
return self.display_mode
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
display_mode = self.get_display_mode(data_source)
|
|
|
|
if display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
|
|
carddef = self.get_carddef()
|
|
url_kwargs = {}
|
|
if get_request().is_in_backoffice() and carddef:
|
|
if carddef.can_user_add_cards(get_request().user):
|
|
kwargs['add_related_url'] = carddef.get_backoffice_submission_url()
|
|
kwargs['with_related'] = True
|
|
url_kwargs['with_related'] = True
|
|
self.url = kwargs['url'] = data_source.get_jsonp_url(**url_kwargs)
|
|
self.widget_class = JsonpSingleSelectWidget
|
|
return
|
|
|
|
if self.display_mode != 'map':
|
|
if self.data_source:
|
|
items = data_sources.get_items(self.data_source, include_disabled=self.display_disabled_items)
|
|
kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
|
|
kwargs['options_with_attributes'] = items[:]
|
|
else:
|
|
kwargs['options'] = self.get_options()
|
|
if not kwargs.get('options'):
|
|
kwargs['options'] = [(None, '---', None)]
|
|
if display_mode == 'list':
|
|
kwargs['use_hint_as_first_option'] = self.use_hint_as_first_option
|
|
elif display_mode == 'radio':
|
|
self.widget_class = RadiobuttonsWidget
|
|
if isinstance(kwargs['options'][0], str):
|
|
first_items = [x for x in kwargs['options'][:3]]
|
|
else:
|
|
first_items = [x[1] for x in kwargs['options'][:3]]
|
|
length_first_items = sum(len(x) for x in first_items)
|
|
# display radio buttons on a single line if there's just a few
|
|
# short options.
|
|
# TODO: absence/presence of delimitor should be an option
|
|
self.inline = bool(len(kwargs['options']) <= 3 and length_first_items <= 40)
|
|
elif display_mode == 'autocomplete':
|
|
kwargs['select2'] = True
|
|
elif display_mode == 'map':
|
|
self.widget_class = MapMarkerSelectionWidget
|
|
elif display_mode == 'timetable':
|
|
# SingleSelectHintWidget with custom template
|
|
kwargs['template-name'] = 'qommon/forms/widgets/select-timetable.html'
|
|
|
|
def get_view_value(self, value, value_id=None, **kwargs):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if value and data_source is None:
|
|
return get_publisher().translate(value) or ''
|
|
value = super().get_view_value(value)
|
|
if not (value_id and self.data_source and self.data_source.get('type', '').startswith('carddef:')):
|
|
return value
|
|
carddef = self.get_carddef()
|
|
if not carddef:
|
|
return value
|
|
try:
|
|
carddata = carddef.data_class().get(value_id)
|
|
except KeyError:
|
|
return value
|
|
parts = data_source.data_source['type'].split(':')
|
|
digest_key = 'default'
|
|
value = (carddata.digests or {}).get(digest_key) or value
|
|
if len(parts) == 3:
|
|
digest_key = 'custom-view:%s' % parts[-1]
|
|
value = (carddata.digests or {}).get(digest_key) or value
|
|
if get_publisher().has_i18n_enabled():
|
|
digest_key += ':' + get_publisher().current_language
|
|
value = (carddata.digests or {}).get(digest_key) or value
|
|
if not (
|
|
get_request()
|
|
and get_request().is_in_backoffice()
|
|
and carddef.is_user_allowed_read(get_request().user, carddata)
|
|
):
|
|
return value
|
|
return htmltext('<a href="%s">' % carddata.get_url(backoffice=True)) + htmltext('%s</a>') % value
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
span = ET.Element('{%s}span' % OD_NS['text'])
|
|
span.text = od_clean_text(force_str(value))
|
|
return span
|
|
|
|
def add_to_view_form(self, form, value=None):
|
|
real_value = value
|
|
label_value = ''
|
|
if value is not None:
|
|
label_value = self.get_display_value(value)
|
|
self.field_key = 'f%s' % self.id
|
|
|
|
form.add(
|
|
StringWidget,
|
|
self.field_key + '_label',
|
|
title=self.label,
|
|
value=label_value,
|
|
readonly='readonly',
|
|
size=len(label_value or '') + 2,
|
|
render_br=False,
|
|
)
|
|
label_widget = form.get_widget(self.field_key + '_label')
|
|
# don't let subwidget overwrite label widget value
|
|
label_widget.secondary = True
|
|
get_request().form[label_widget.name] = label_value
|
|
label_widget.field = self
|
|
form.add(HiddenWidget, self.field_key, value=real_value)
|
|
form.get_widget(self.field_key).field = self
|
|
widget = form.get_widget(self.field_key + '_label')
|
|
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 store_display_value(self, data, field_id, raise_on_error=False):
|
|
value = data.get(field_id)
|
|
if not value:
|
|
return ''
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source and data_source.type == 'jsonp':
|
|
if get_request():
|
|
display_value = get_request().form.get('f%s_display' % field_id)
|
|
real_data_source = data_source.data_source
|
|
if display_value is None:
|
|
if not get_session().jsonp_display_values:
|
|
get_session().jsonp_display_values = {}
|
|
display_value = get_session().jsonp_display_values.get(
|
|
'%s_%s' % (real_data_source.get('value'), value)
|
|
)
|
|
else:
|
|
if not get_session().jsonp_display_values:
|
|
get_session().jsonp_display_values = {}
|
|
get_session().jsonp_display_values[
|
|
'%s_%s' % (real_data_source.get('value'), value)
|
|
] = display_value
|
|
return display_value
|
|
with get_publisher().with_language('default'):
|
|
return self.get_display_value(value)
|
|
|
|
def store_structured_value(self, data, field_id, raise_on_error=False):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source is None:
|
|
return
|
|
|
|
if data_source.type == 'jsonp':
|
|
return
|
|
|
|
with get_publisher().with_language('default'):
|
|
value = data_source.get_structured_value(data.get(field_id))
|
|
if value is None and raise_on_error:
|
|
raise SetValueError('a datasource is unavailable (field id: %s)' % field_id)
|
|
|
|
if value is None or set(value.keys()) == {'id', 'text'}:
|
|
return
|
|
return value
|
|
|
|
def convert_value_from_anything(self, value):
|
|
if not value:
|
|
return None
|
|
value = str(value)
|
|
if self.data_source:
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source.type and data_source.type.startswith('carddef:'):
|
|
card_value = data_source.get_card_structured_value_by_id(value)
|
|
if card_value:
|
|
value = str(card_value['id'])
|
|
else:
|
|
raise ValueError('unknown card value (%r)' % value)
|
|
return value
|
|
|
|
def convert_value_from_str(self, value):
|
|
# caller should also call store_display_value and store_structured_value
|
|
return value
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'in_filters',
|
|
title=_('Display in default filters'),
|
|
value=self.in_filters,
|
|
advanced=True,
|
|
)
|
|
options = [
|
|
('list', _('List'), 'list'),
|
|
('radio', _('Radio buttons'), 'radio'),
|
|
('autocomplete', _('Autocomplete'), 'autocomplete'),
|
|
('map', _('Map (requires geographical data)'), 'map'),
|
|
('timetable', _('Timetable'), 'timetable'),
|
|
]
|
|
form.add(
|
|
RadiobuttonsWidget,
|
|
'display_mode',
|
|
title=_('Display Mode'),
|
|
options=options,
|
|
value=self.display_mode,
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
extra_css_class='widget-inline-radio',
|
|
)
|
|
self.add_items_fields_admin_form(form)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'display_disabled_items',
|
|
title=_('Display disabled items'),
|
|
value=self.display_disabled_items,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'initial_date_alignment',
|
|
title=_('Initial date alignment'),
|
|
value=self.initial_date_alignment,
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
attrs={
|
|
'data-dynamic-display-child-of': 'display_mode',
|
|
'data-dynamic-display-value': 'timetable',
|
|
},
|
|
)
|
|
self.fill_zoom_admin_form(
|
|
form, attrs={'data-dynamic-display-child-of': 'display_mode', 'data-dynamic-display-value': 'map'}
|
|
)
|
|
initial_position_widget = form.add(
|
|
RadiobuttonsWidget,
|
|
'initial_position',
|
|
title=_('Initial Position'),
|
|
options=(
|
|
('', _('Default position (from markers)'), ''),
|
|
('template', _('From template'), 'template'),
|
|
),
|
|
value=self.initial_position or '',
|
|
extra_css_class='widget-inline-radio',
|
|
attrs={
|
|
'data-dynamic-display-child-of': 'display_mode',
|
|
'data-dynamic-display-value': 'map',
|
|
'data-dynamic-display-parent': 'true',
|
|
},
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'position_template',
|
|
value=self.position_template,
|
|
size=80,
|
|
required=False,
|
|
hint=_('Positions (using latitute;longitude format) and addresses are supported.'),
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
attrs={
|
|
'data-dynamic-display-child-of': initial_position_widget.get_name(),
|
|
'data-dynamic-display-value': 'template',
|
|
},
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'use_hint_as_first_option',
|
|
title=_('Use hint as first option'),
|
|
value=self.use_hint_as_first_option,
|
|
attrs={
|
|
'data-dynamic-display-child-of': 'display_mode',
|
|
'data-dynamic-display-value': 'list',
|
|
},
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'display_mode',
|
|
'items',
|
|
'data_source',
|
|
'in_filters',
|
|
'anonymise',
|
|
'display_disabled_items',
|
|
'initial_zoom',
|
|
'min_zoom',
|
|
'max_zoom',
|
|
'initial_position',
|
|
'position_template',
|
|
'initial_date_alignment',
|
|
'use_hint_as_first_option',
|
|
]
|
|
|
|
def check_admin_form(self, form):
|
|
super().check_admin_form(form)
|
|
self.check_items_admin_form(form)
|
|
self.check_zoom_admin_form(form)
|
|
|
|
def stats(self, values):
|
|
return item_items_stats(self, values)
|
|
|
|
def get_initial_date_alignment(self):
|
|
if not self.initial_date_alignment:
|
|
return
|
|
import wcs.workflows
|
|
|
|
try:
|
|
date = wcs.workflows.template_on_formdata(None, self.initial_date_alignment, autoescape=False)
|
|
except TemplateError:
|
|
return
|
|
try:
|
|
return misc.get_as_datetime(date)
|
|
except ValueError:
|
|
return
|
|
|
|
def feed_session(self, value, display_value):
|
|
real_data_source = self.get_real_data_source()
|
|
if real_data_source and real_data_source.get('type') == 'jsonp':
|
|
if not get_session().jsonp_display_values:
|
|
get_session().jsonp_display_values = {}
|
|
get_session().jsonp_display_values[
|
|
'%s_%s' % (real_data_source.get('value'), value)
|
|
] = display_value
|
|
|
|
def get_csv_heading(self):
|
|
if self.data_source:
|
|
return ['%s (%s)' % (self.label, _('identifier')), self.label]
|
|
return [self.label]
|
|
|
|
def get_csv_value(self, element, display_value=None, **kwargs):
|
|
values = [element]
|
|
if self.data_source:
|
|
values.append(display_value)
|
|
return values
|
|
|
|
def export_to_json(self, include_id=False):
|
|
field = super().export_to_json(include_id=include_id)
|
|
if self.data_source:
|
|
self.export_to_json_data_source(field)
|
|
return field
|
|
|
|
def i18n_scan(self, base_location):
|
|
yield from super(WidgetField, self).i18n_scan(base_location)
|
|
yield from ItemFieldMixin.i18n_scan(self, base_location)
|
|
|
|
|
|
register_field_class(ItemField)
|
|
|
|
|
|
class ItemsField(WidgetField, ItemFieldMixin):
|
|
key = 'items'
|
|
description = _('Multiple choice list')
|
|
allow_complex = True
|
|
allow_statistics = True
|
|
|
|
items = []
|
|
min_choices = 0
|
|
max_choices = 0
|
|
data_source = {}
|
|
in_filters = False
|
|
display_disabled_items = False
|
|
display_mode = 'checkboxes'
|
|
|
|
widget_class = CheckboxesWidget
|
|
|
|
_cached_data_source = None
|
|
|
|
def __init__(self, **kwargs):
|
|
self.items = []
|
|
WidgetField.__init__(self, **kwargs)
|
|
|
|
def get_options(self):
|
|
if self.data_source:
|
|
if self._cached_data_source:
|
|
return self._cached_data_source
|
|
self._cached_data_source = [x[:3] for x in data_sources.get_items(self.data_source)]
|
|
return self._cached_data_source[:]
|
|
elif self.items:
|
|
return [(x, get_publisher().translate(x)) for x in self.items]
|
|
else:
|
|
return []
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
kwargs['options'] = self.get_options()
|
|
kwargs['min_choices'] = self.min_choices
|
|
kwargs['max_choices'] = self.max_choices
|
|
if self.data_source:
|
|
items = data_sources.get_items(self.data_source, include_disabled=self.display_disabled_items)
|
|
kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
|
|
kwargs['options_with_attributes'] = items[:]
|
|
|
|
if len(kwargs['options']) > 3:
|
|
kwargs['inline'] = False
|
|
|
|
if self.display_mode == 'autocomplete':
|
|
self.widget_class = MultiSelectWidget
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'in_filters',
|
|
title=_('Display in default filters'),
|
|
value=self.in_filters,
|
|
advanced=True,
|
|
)
|
|
options = [
|
|
('checkboxes', _('Checkboxes'), 'checkboxes'),
|
|
('autocomplete', _('Autocomplete'), 'autocomplete'),
|
|
]
|
|
form.add(
|
|
RadiobuttonsWidget,
|
|
'display_mode',
|
|
title=_('Display Mode'),
|
|
options=options,
|
|
value=self.display_mode,
|
|
extra_css_class='widget-inline-radio',
|
|
)
|
|
self.add_items_fields_admin_form(form)
|
|
form.add(
|
|
IntWidget,
|
|
'min_choices',
|
|
title=_('Minimum number of choices'),
|
|
value=self.min_choices,
|
|
required=False,
|
|
size=4,
|
|
)
|
|
form.add(
|
|
IntWidget,
|
|
'max_choices',
|
|
title=_('Maximum number of choices'),
|
|
value=self.max_choices,
|
|
required=False,
|
|
size=4,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'display_disabled_items',
|
|
title=_('Display disabled items'),
|
|
value=self.display_disabled_items,
|
|
advanced=True,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'items',
|
|
'display_mode',
|
|
'min_choices',
|
|
'max_choices',
|
|
'data_source',
|
|
'in_filters',
|
|
'anonymise',
|
|
'display_disabled_items',
|
|
]
|
|
|
|
def check_admin_form(self, form):
|
|
super().check_admin_form(form)
|
|
self.check_items_admin_form(form)
|
|
|
|
def get_prefill_value(self, user=None, force_string=True):
|
|
value, explicit_lock = super().get_prefill_value(user=user, force_string=False)
|
|
if value is not None and (
|
|
not isinstance(value, (str, tuple, list)) or not all(isinstance(x, (int, str)) for x in value)
|
|
):
|
|
get_publisher().record_error(
|
|
_('Invalid value for items prefill on field "%s"') % self.label,
|
|
formdef=getattr(self, 'formdef', None),
|
|
)
|
|
return (None, explicit_lock)
|
|
return (value, explicit_lock)
|
|
|
|
def convert_value_to_str(self, value):
|
|
return value
|
|
|
|
def convert_value_from_str(self, value):
|
|
if not isinstance(value, str):
|
|
return value
|
|
if not value.strip():
|
|
return None
|
|
return [x.strip() for x in value.split('|') if x.strip()]
|
|
|
|
def convert_value_from_anything(self, value):
|
|
if isinstance(value, str):
|
|
return self.convert_value_from_str(value)
|
|
if isinstance(value, int):
|
|
return [str(value)]
|
|
if not value:
|
|
return None
|
|
try:
|
|
return list(value)
|
|
except TypeError:
|
|
raise ValueError('invalid data for items type (%r)' % value)
|
|
|
|
def get_value_info(self, data):
|
|
value, value_details = super().get_value_info(data)
|
|
labels = []
|
|
if not self.data_source:
|
|
value_id = value_details.get('value_id')
|
|
if value_id:
|
|
labels = value_id.copy()
|
|
else:
|
|
structured_values = self.get_structured_value(data)
|
|
if structured_values:
|
|
labels = [x['text'] for x in structured_values]
|
|
value_details['labels'] = labels
|
|
return (value, value_details)
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
if kwargs.get('labels'):
|
|
# summary page and labels are available
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<ul>')
|
|
for x in kwargs['labels']:
|
|
r += htmltext('<li>%s</li>' % x)
|
|
r += htmltext('</ul>')
|
|
return r.getvalue()
|
|
|
|
if isinstance(value, str): # == display_value
|
|
return value
|
|
if value:
|
|
try:
|
|
return ', '.join([(x) for x in value])
|
|
except TypeError:
|
|
pass
|
|
return ''
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
span = ET.Element('{%s}span' % OD_NS['text'])
|
|
span.text = od_clean_text(self.get_view_value(value))
|
|
return span
|
|
|
|
def stats(self, values):
|
|
return item_items_stats(self, values)
|
|
|
|
def get_csv_heading(self):
|
|
if self.data_source:
|
|
labels = ['%s (%s)' % (self.label, _('identifier')), '%s (%s)' % (self.label, _('label'))]
|
|
next_columns = [_('(continued)')] * 2
|
|
else:
|
|
labels = [self.label]
|
|
next_columns = [_('(continued)')]
|
|
if self.max_choices:
|
|
labels.extend(next_columns * (self.max_choices - 1))
|
|
elif len(self.get_options()):
|
|
labels.extend(next_columns * (len(self.get_options()) - 1))
|
|
return labels
|
|
|
|
def get_csv_value(self, element, structured_value=None, **kwargs):
|
|
values = []
|
|
|
|
if self.max_choices:
|
|
nb_columns = self.max_choices
|
|
elif len(self.get_options()):
|
|
nb_columns = len(self.get_options())
|
|
else:
|
|
nb_columns = 1
|
|
|
|
if self.data_source:
|
|
nb_columns *= 2
|
|
for one_value in structured_value or []:
|
|
values.append(one_value.get('id'))
|
|
values.append(one_value.get('text'))
|
|
else:
|
|
for one_value in element:
|
|
values.append(one_value)
|
|
|
|
if len(values) > nb_columns:
|
|
# this would happen if max_choices is set after forms were already
|
|
# filled with more values
|
|
values = values[:nb_columns]
|
|
elif len(values) < nb_columns:
|
|
values.extend([''] * (nb_columns - len(values)))
|
|
|
|
return values
|
|
|
|
def store_display_value(self, data, field_id, raise_on_error=False):
|
|
if not data.get(field_id):
|
|
return ''
|
|
options = self.get_options()
|
|
if not options:
|
|
return ''
|
|
choices = []
|
|
for choice in data.get(field_id) or []:
|
|
if isinstance(options[0], str):
|
|
choices.append(choice)
|
|
elif type(options[0]) in (tuple, list):
|
|
if len(options[0]) == 2:
|
|
for key, option_value in options:
|
|
if str(key) == str(choice):
|
|
choices.append(option_value)
|
|
break
|
|
elif len(options[0]) == 3:
|
|
for key, option_value, dummy in options:
|
|
if str(key) == str(choice):
|
|
choices.append(option_value)
|
|
break
|
|
else:
|
|
if raise_on_error:
|
|
raise SetValueError('datasource is unavailable')
|
|
return ', '.join(choices)
|
|
|
|
def store_structured_value(self, data, field_id, raise_on_error=False):
|
|
if not data.get(field_id):
|
|
return
|
|
if not self.data_source:
|
|
return
|
|
try:
|
|
structured_options = data_sources.get_structured_items(
|
|
self.data_source, raise_on_error=raise_on_error
|
|
)
|
|
except data_sources.DataSourceError as e:
|
|
raise SetValueError(str(e))
|
|
if not structured_options:
|
|
return
|
|
structured_value = []
|
|
for structured_option in structured_options:
|
|
for choice in data.get(field_id) or []:
|
|
if str(structured_option.get('id')) == str(choice):
|
|
structured_value.append(structured_option)
|
|
return structured_value
|
|
|
|
def export_to_json(self, include_id=False):
|
|
field = super().export_to_json(include_id=include_id)
|
|
if self.data_source:
|
|
self.export_to_json_data_source(field)
|
|
return field
|
|
|
|
def from_json_value(self, value):
|
|
if isinstance(value, list):
|
|
return value
|
|
return []
|
|
|
|
def get_exploded_options(self, options):
|
|
carddef = self.get_carddef()
|
|
if not carddef:
|
|
# unnest key/values
|
|
exploded_options = {}
|
|
for option_keys, option_label in options:
|
|
if option_keys and option_label:
|
|
for option_key, option_label in zip(option_keys, option_label.split(', ')):
|
|
exploded_options[option_key] = option_label
|
|
return exploded_options.items()
|
|
|
|
options_ids = set()
|
|
for option in options:
|
|
if option[0]:
|
|
options_ids.update(set(option[0]))
|
|
|
|
options_ids = [x for x in options_ids if misc.is_ascii_digit(str(x))]
|
|
cards = carddef.data_class().select([Contains('id', options_ids)])
|
|
|
|
return [(str(x.id), x.get_display_label()) for x in cards]
|
|
|
|
def i18n_scan(self, base_location):
|
|
yield from super(WidgetField, self).i18n_scan(base_location)
|
|
yield from ItemFieldMixin.i18n_scan(self, base_location)
|
|
|
|
|
|
register_field_class(ItemsField)
|
|
|
|
|
|
class PostConditionsRowWidget(CompositeWidget):
|
|
def __init__(self, name, value=None, **kwargs):
|
|
CompositeWidget.__init__(self, name, value, **kwargs)
|
|
if not value:
|
|
value = {}
|
|
self.add(
|
|
ConditionWidget, name='condition', title=_('Condition'), value=value.get('condition'), size=50
|
|
)
|
|
self.add(
|
|
StringWidget,
|
|
name='error_message',
|
|
title=_('Error message if condition is not met'),
|
|
value=value.get('error_message'),
|
|
size=50,
|
|
)
|
|
|
|
def _parse(self, request):
|
|
if self.get('condition') or self.get('error_message'):
|
|
self.value = {'condition': self.get('condition'), 'error_message': self.get('error_message')}
|
|
else:
|
|
self.value = None
|
|
|
|
|
|
class PostConditionsTableWidget(WidgetListAsTable):
|
|
readonly = False
|
|
|
|
def __init__(self, name, **kwargs):
|
|
super().__init__(name, element_type=PostConditionsRowWidget, **kwargs)
|
|
|
|
def parse(self, request=None):
|
|
super().parse(request=request)
|
|
for post_condition in self.value or []:
|
|
if not (post_condition.get('error_message') and post_condition.get('condition')):
|
|
self.set_error(_('Both condition and error message are required.'))
|
|
break
|
|
return self.value
|
|
|
|
|
|
class PageCondition(Condition):
|
|
record_errors = False
|
|
|
|
def get_data(self):
|
|
dict_vars = self.context['dict_vars']
|
|
formdef = self.context['formdef']
|
|
|
|
# create variables with values currently being evaluated, not yet
|
|
# available in the formdata.
|
|
from .formdata import get_dict_with_varnames
|
|
|
|
live_data = {}
|
|
form_live_data = {}
|
|
if dict_vars is not None and formdef:
|
|
live_data = get_dict_with_varnames(formdef.fields, dict_vars)
|
|
form_live_data = {'form_' + x: y for x, y in live_data.items()}
|
|
|
|
# 1) feed the form_var_* variables in the global substitution system,
|
|
# they will shadow formdata context variables with their new "live"
|
|
# value, this may be useful when evaluating data sources.
|
|
class ConditionVars:
|
|
def __init__(self, id_dict_var):
|
|
# keep track of reference dictionary
|
|
self.id_dict_var = id_dict_var
|
|
|
|
def get_substitution_variables(self):
|
|
return {}
|
|
|
|
def get_static_substitution_variables(self):
|
|
# only for backward compatibility with python evaluations
|
|
return form_live_data
|
|
|
|
def __eq__(self, other):
|
|
# Assume all ConditionVars are equal when initialized with
|
|
# the same live data dictionary; this avoids filling
|
|
# the substitution sources with duplicates and invalidating its
|
|
# cache.
|
|
return self.id_dict_var == getattr(other, 'id_dict_var', None)
|
|
|
|
if dict_vars is not None:
|
|
# Add them only if there is a real dict_vars in context,
|
|
# ie do nothing on first page condition
|
|
get_publisher().substitutions.feed(ConditionVars(id(dict_vars)))
|
|
|
|
# alter top-of-stack formdata with data from submitted form
|
|
from wcs.formdata import FormData
|
|
|
|
for source in reversed(get_publisher().substitutions.sources):
|
|
if isinstance(source, FormData):
|
|
source.data.update(dict_vars)
|
|
break
|
|
|
|
data = super().get_data()
|
|
# 2) add live data as var_ variables for local evaluation only, for
|
|
# backward compatibility. They are not added globally as they would
|
|
# interfere with the var_ prefixed variables used in dynamic jsonp
|
|
# fields. (#9786)
|
|
data = copy.copy(data)
|
|
data.update(live_data)
|
|
if dict_vars is None:
|
|
# ConditionsVars is not set when evaluating first page condition,
|
|
# but we need to have form_var_* variables already; add them from
|
|
# form_live_data (where all variables will have been set to None).
|
|
data.update(form_live_data)
|
|
return data
|
|
|
|
|
|
class PageField(Field):
|
|
key = 'page'
|
|
description = _('Page')
|
|
|
|
post_conditions = None
|
|
|
|
def post_conditions_init_with_xml(self, node, charset, include_id=False, snapshot=False):
|
|
if node is None:
|
|
return
|
|
self.post_conditions = []
|
|
for post_condition_node in node.findall('post_condition'):
|
|
if post_condition_node.findall('condition/type'):
|
|
condition = {
|
|
'type': xml_node_text(post_condition_node.find('condition/type')),
|
|
'value': xml_node_text(post_condition_node.find('condition/value')),
|
|
}
|
|
elif post_condition_node.find('condition').text:
|
|
condition = {
|
|
'type': 'python',
|
|
'value': xml_node_text(post_condition_node.find('condition')),
|
|
}
|
|
else:
|
|
continue
|
|
self.post_conditions.append(
|
|
{
|
|
'condition': condition,
|
|
'error_message': xml_node_text(post_condition_node.find('error_message')),
|
|
}
|
|
)
|
|
|
|
def post_conditions_export_to_xml(self, node, charset, include_id=False):
|
|
if not self.post_conditions:
|
|
return
|
|
|
|
conditions_node = ET.SubElement(node, 'post_conditions')
|
|
for post_condition in self.post_conditions:
|
|
post_condition_node = ET.SubElement(conditions_node, 'post_condition')
|
|
condition_node = ET.SubElement(post_condition_node, 'condition')
|
|
ET.SubElement(condition_node, 'type').text = force_str(
|
|
(post_condition['condition'] or {}).get('type') or '', charset, errors='replace'
|
|
)
|
|
ET.SubElement(condition_node, 'value').text = force_str(
|
|
(post_condition['condition'] or {}).get('value') or '', charset, errors='replace'
|
|
)
|
|
ET.SubElement(post_condition_node, 'error_message').text = force_str(
|
|
post_condition['error_message'] or '', charset, errors='replace'
|
|
)
|
|
|
|
def fill_admin_form(self, form):
|
|
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
)
|
|
form.add(
|
|
PostConditionsTableWidget,
|
|
'post_conditions',
|
|
title=_('Post Conditions'),
|
|
value=self.post_conditions,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
VarnameWidget,
|
|
'varname',
|
|
title=_('Identifier'),
|
|
value=self.varname,
|
|
size=30,
|
|
advanced=True,
|
|
hint=_('This is used as reference in workflow edition action.'),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return Field.get_admin_attributes(self) + ['post_conditions', 'varname']
|
|
|
|
def add_to_view_form(self, *args, **kwargs):
|
|
pass
|
|
|
|
def get_conditions(self):
|
|
if self.condition:
|
|
yield self.condition
|
|
for post_condition in self.post_conditions or []:
|
|
yield post_condition.get('condition')
|
|
|
|
def get_post_conditions_parameter_view_value(self, widget):
|
|
if not self.post_conditions:
|
|
return
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<ul>')
|
|
for post_condition in self.post_conditions:
|
|
r += htmltext('<li>%s <span class="condition-type">(%s)</span> - %s</li>') % (
|
|
post_condition.get('condition').get('value'),
|
|
{'django': 'Django', 'python': 'Python'}.get(post_condition.get('condition').get('type')),
|
|
post_condition.get('error_message'),
|
|
)
|
|
r += htmltext('</ul>')
|
|
return r.getvalue()
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
if getattr(self, 'post_conditions', None):
|
|
post_conditions = self.post_conditions or []
|
|
for post_condition in post_conditions:
|
|
condition = post_condition.get('condition') or {}
|
|
if condition.get('type') == 'django':
|
|
yield from check_wscalls(condition.get('value'))
|
|
yield from check_carddefs(condition.get('value'))
|
|
yield from check_formdefs(condition.get('value'))
|
|
|
|
def i18n_scan(self, base_location):
|
|
location = '%s%s/' % (base_location, self.id)
|
|
yield location, None, self.label
|
|
for post_condition in self.post_conditions or []:
|
|
yield location, None, post_condition.get('error_message')
|
|
|
|
|
|
register_field_class(PageField)
|
|
|
|
|
|
class TableField(WidgetField):
|
|
key = 'table'
|
|
description = _('Table')
|
|
allow_complex = True
|
|
|
|
rows = None
|
|
columns = None
|
|
|
|
widget_class = TableWidget
|
|
|
|
def __init__(self, **kwargs):
|
|
self.rows = []
|
|
self.columns = []
|
|
WidgetField.__init__(self, **kwargs)
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
kwargs['rows'] = self.rows
|
|
kwargs['columns'] = self.columns
|
|
|
|
def get_display_locations_options(self):
|
|
return [('validation', _('Validation Page')), ('summary', _('Summary Page'))]
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
try:
|
|
form.remove('prefill')
|
|
except KeyError: # perhaps it was already removed
|
|
pass
|
|
form.add(
|
|
WidgetList,
|
|
'rows',
|
|
title=_('Rows'),
|
|
element_type=StringWidget,
|
|
value=self.rows,
|
|
required=True,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add row'),
|
|
)
|
|
form.add(
|
|
WidgetList,
|
|
'columns',
|
|
title=_('Columns'),
|
|
element_type=StringWidget,
|
|
value=self.columns,
|
|
required=True,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add column'),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
t = WidgetField.get_admin_attributes(self) + ['rows', 'columns']
|
|
try:
|
|
t.remove('prefill')
|
|
except ValueError:
|
|
pass
|
|
return t
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<table><thead><tr><td></td>')
|
|
for column in self.columns:
|
|
r += htmltext('<th>%s</th>') % column
|
|
r += htmltext('</tr></thead><tbody>')
|
|
for i, row in enumerate(self.rows):
|
|
r += htmltext('<tr><th>%s</th>') % row
|
|
for j, column in enumerate(self.columns):
|
|
r += htmltext('<td>')
|
|
if value:
|
|
try:
|
|
r += value[i][j]
|
|
except IndexError:
|
|
pass
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('</tbody></table>')
|
|
|
|
return r.getvalue()
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
if not value:
|
|
return indent
|
|
r = []
|
|
max_width = 0
|
|
for column in self.columns:
|
|
max_width = max(max_width, len(smart_str(column)))
|
|
|
|
for i, row in enumerate(value):
|
|
value[i] = [x or '' for x in row]
|
|
|
|
def get_value(i, j):
|
|
try:
|
|
return smart_str(value[i][j])
|
|
except IndexError:
|
|
return '-'
|
|
|
|
for i, row in enumerate(self.rows):
|
|
max_width = max(max_width, len(row))
|
|
for j, column in enumerate(self.columns):
|
|
max_width = max(max_width, len(get_value(i, j)))
|
|
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns) + 1)))
|
|
r.append(' '.join([smart_str(column).center(max_width) for column in ['/'] + self.columns]))
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns) + 1)))
|
|
for i, row in enumerate(self.rows):
|
|
r.append(
|
|
' '.join(
|
|
[
|
|
cell.center(max_width)
|
|
for cell in [smart_str(row)] + [get_value(i, x) for x in range(len(self.columns))]
|
|
]
|
|
)
|
|
)
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns) + 1)))
|
|
return misc.site_encode('\n'.join([indent + x for x in r]))
|
|
|
|
def get_csv_heading(self):
|
|
if not self.columns:
|
|
return [self.label]
|
|
labels = []
|
|
for col in self.columns:
|
|
for row in self.rows:
|
|
t = '%s / %s' % (col, row)
|
|
if len(labels) == 0:
|
|
labels.append('%s - %s' % (self.label, t))
|
|
else:
|
|
labels.append(t)
|
|
return labels
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
if not self.columns:
|
|
return ['']
|
|
values = []
|
|
for i in range(len(self.columns)):
|
|
for j in range(len(self.rows)):
|
|
try:
|
|
values.append(element[j][i])
|
|
except IndexError:
|
|
values.append('')
|
|
return values
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
table = ET.Element('{%s}table' % OD_NS['table'])
|
|
ET.SubElement(table, '{%s}table-column' % OD_NS['table'])
|
|
for col in self.columns:
|
|
ET.SubElement(table, '{%s}table-column' % OD_NS['table'])
|
|
row = ET.SubElement(table, '{%s}table-row' % OD_NS['table'])
|
|
ET.SubElement(row, '{%s}table-cell' % OD_NS['table'])
|
|
for col in self.columns:
|
|
table_cell = ET.SubElement(row, '{%s}table-cell' % OD_NS['table'])
|
|
cell_value = ET.SubElement(table_cell, '{%s}p' % OD_NS['text'])
|
|
cell_value.text = col
|
|
for i, row_label in enumerate(self.rows):
|
|
row = ET.SubElement(table, '{%s}table-row' % OD_NS['table'])
|
|
table_cell = ET.SubElement(row, '{%s}table-cell' % OD_NS['table'])
|
|
cell_value = ET.SubElement(table_cell, '{%s}p' % OD_NS['text'])
|
|
cell_value.text = row_label
|
|
for j, col in enumerate(self.columns):
|
|
table_cell = ET.SubElement(row, '{%s}table-cell' % OD_NS['table'])
|
|
cell_value = ET.SubElement(table_cell, '{%s}p' % OD_NS['text'])
|
|
try:
|
|
cell_value.text = value[i][j]
|
|
except IndexError:
|
|
pass
|
|
return table
|
|
|
|
def from_json_value(self, value):
|
|
return value
|
|
|
|
|
|
register_field_class(TableField)
|
|
|
|
|
|
class TableSelectField(TableField):
|
|
key = 'table-select'
|
|
description = _('Table of Lists')
|
|
allow_complex = True
|
|
|
|
items = None
|
|
|
|
widget_class = SingleSelectTableWidget
|
|
|
|
def __init__(self, **kwargs):
|
|
self.items = []
|
|
TableField.__init__(self, **kwargs)
|
|
|
|
def fill_admin_form(self, form):
|
|
TableField.fill_admin_form(self, form)
|
|
form.add(
|
|
WidgetList,
|
|
'items',
|
|
title=_('Items'),
|
|
element_type=StringWidget,
|
|
value=self.items,
|
|
required=True,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add item'),
|
|
)
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
TableField.perform_more_widget_changes(self, form, kwargs, edit=edit)
|
|
if edit:
|
|
kwargs['options'] = self.items or [(None, '---')]
|
|
else:
|
|
self.widget_class = TableWidget
|
|
|
|
def get_admin_attributes(self):
|
|
return TableField.get_admin_attributes(self) + ['items']
|
|
|
|
def check_admin_form(self, form):
|
|
items = form.get_widget('items').parse()
|
|
d = {}
|
|
for v in items or []:
|
|
if v in d:
|
|
form.set_error('items', _('Duplicated Items'))
|
|
return
|
|
d[v] = None
|
|
|
|
|
|
register_field_class(TableSelectField)
|
|
|
|
|
|
class TableRowsField(WidgetField):
|
|
key = 'tablerows'
|
|
description = _('Table with rows')
|
|
allow_complex = True
|
|
|
|
total_row = True
|
|
columns = None
|
|
|
|
widget_class = TableListRowsWidget
|
|
|
|
def __init__(self, **kwargs):
|
|
self.columns = []
|
|
WidgetField.__init__(self, **kwargs)
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
kwargs['columns'] = self.columns
|
|
kwargs['add_element_label'] = _('Add row')
|
|
|
|
def get_display_locations_options(self):
|
|
return [('validation', _('Validation Page')), ('summary', _('Summary Page'))]
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
try:
|
|
form.remove('prefill')
|
|
except KeyError: # perhaps it was already removed
|
|
pass
|
|
form.add(
|
|
WidgetList,
|
|
'columns',
|
|
title=_('Columns'),
|
|
element_type=StringWidget,
|
|
value=self.columns,
|
|
required=True,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add column'),
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'total_row',
|
|
title=_('Total Row'),
|
|
value=self.total_row,
|
|
default_value=self.__class__.total_row,
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
t = WidgetField.get_admin_attributes(self) + ['columns', 'total_row']
|
|
try:
|
|
t.remove('prefill')
|
|
except ValueError:
|
|
pass
|
|
return t
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<table><thead><tr>')
|
|
for column in self.columns:
|
|
r += htmltext('<th>%s</th>') % column
|
|
r += htmltext('</tr></thead><tbody>')
|
|
for row in value:
|
|
r += htmltext('<tr>')
|
|
for j, column in enumerate(self.columns):
|
|
r += htmltext('<td>')
|
|
if value:
|
|
try:
|
|
r += row[j]
|
|
except IndexError:
|
|
pass
|
|
r += htmltext('</td>')
|
|
r += htmltext('</tr>')
|
|
r += htmltext('</tbody>')
|
|
|
|
if self.total_row:
|
|
sums_row = []
|
|
for j, column in enumerate(self.columns):
|
|
sum_column = 0
|
|
for row_value in value:
|
|
try:
|
|
cell_value = row_value[j]
|
|
except IndexError:
|
|
continue
|
|
if cell_value in (None, ''):
|
|
continue
|
|
try:
|
|
sum_column += float(cell_value)
|
|
except ValueError:
|
|
sums_row.append(None)
|
|
break
|
|
else:
|
|
sums_row.append(sum_column)
|
|
if [x for x in sums_row if x is not None]:
|
|
r += htmltext('<tfoot><tr>')
|
|
for sum_column in sums_row:
|
|
if sum_column is None:
|
|
r += htmltext('<td></td>')
|
|
else:
|
|
r += htmltext('<td>%.2f</td>' % sum_column)
|
|
r += htmltext('</tr></tfoot>')
|
|
|
|
r += htmltext('</table>')
|
|
|
|
return r.getvalue()
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
if not value:
|
|
return indent
|
|
r = []
|
|
max_width = 0
|
|
for column in self.columns:
|
|
max_width = max(max_width, len(smart_str(column)))
|
|
|
|
for i, row in enumerate(value):
|
|
value[i] = [x or '' for x in row]
|
|
|
|
def get_value(i, j):
|
|
try:
|
|
return smart_str(value[i][j])
|
|
except IndexError:
|
|
return '-'
|
|
|
|
for i, row_value in enumerate(value):
|
|
for j, column in enumerate(self.columns):
|
|
try:
|
|
max_width = max(max_width, len(smart_str(row_value[j])))
|
|
except IndexError:
|
|
# ignore errors for shorter than expected rows, this is
|
|
# typical of the field gaining new columns after some forms
|
|
# were already saved.
|
|
pass
|
|
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns))))
|
|
r.append(' '.join([smart_str(column).center(max_width) for column in self.columns]))
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns))))
|
|
for i, row_value in enumerate(value):
|
|
r.append(
|
|
' '.join(
|
|
[cell.center(max_width) for cell in [get_value(i, x) for x in range(len(self.columns))]]
|
|
)
|
|
)
|
|
r.append(' '.join(['=' * max_width] * (len(self.columns))))
|
|
return misc.site_encode('\n'.join([indent + x for x in r]))
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
return [_('unimplemented')] # XXX
|
|
|
|
|
|
register_field_class(TableRowsField)
|
|
|
|
|
|
class MapField(WidgetField, MapOptionsMixin):
|
|
key = 'map'
|
|
description = _('Map')
|
|
|
|
initial_position = None
|
|
default_position = None
|
|
position_template = None
|
|
|
|
widget_class = MapWidget
|
|
extra_attributes = [
|
|
'initial_zoom',
|
|
'min_zoom',
|
|
'max_zoom',
|
|
'initial_position',
|
|
'default_position',
|
|
'position_template',
|
|
]
|
|
|
|
def migrate(self):
|
|
changed = False
|
|
if not self.initial_position: # 2023-04-20
|
|
if getattr(self, 'init_with_geoloc', False):
|
|
self.initial_position = 'geoloc'
|
|
changed = True
|
|
elif self.default_position:
|
|
self.initial_position = 'point'
|
|
changed = True
|
|
return changed
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
self.fill_zoom_admin_form(form, tab=('position', _('Position')))
|
|
initial_position_widget = form.add(
|
|
RadiobuttonsWidget,
|
|
'initial_position',
|
|
title=_('Initial Position'),
|
|
options=(
|
|
('', _('Default position'), ''),
|
|
('point', _('Specific point'), 'point'),
|
|
('geoloc', _('Device geolocation'), 'geoloc'),
|
|
('template', _('From template'), 'template'),
|
|
),
|
|
value=self.initial_position or '',
|
|
extra_css_class='widget-inline-radio',
|
|
tab=('position', _('Position')),
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
)
|
|
form.add(
|
|
MapWidget,
|
|
'default_position',
|
|
value=self.default_position,
|
|
default_zoom='9',
|
|
required=False,
|
|
tab=('position', _('Position')),
|
|
attrs={
|
|
'data-dynamic-display-child-of': initial_position_widget.get_name(),
|
|
'data-dynamic-display-value': 'point',
|
|
},
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'position_template',
|
|
value=self.position_template,
|
|
size=80,
|
|
required=False,
|
|
hint=_('Positions (using latitute;longitude format) and addresses are supported.'),
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
tab=('position', _('Position')),
|
|
attrs={
|
|
'data-dynamic-display-child-of': initial_position_widget.get_name(),
|
|
'data-dynamic-display-value': 'template',
|
|
},
|
|
)
|
|
|
|
def check_admin_form(self, form):
|
|
self.check_zoom_admin_form(form)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'initial_zoom',
|
|
'min_zoom',
|
|
'max_zoom',
|
|
'initial_position',
|
|
'default_position',
|
|
'position_template',
|
|
]
|
|
|
|
def get_prefill_value(self, user=None, force_string=True):
|
|
if self.prefill.get('type') != 'string' or not self.prefill.get('value'):
|
|
return (None, False)
|
|
# template string must produce lat;lon to be interpreted as coordinates,
|
|
# otherwise it will be interpreted as an address that will be geocoded.
|
|
prefill_value, explicit_lock = super().get_prefill_value()
|
|
if re.match(r'-?\d+(\.\d+)?;-?\d+(\.\d+)?$', prefill_value):
|
|
return (prefill_value, explicit_lock)
|
|
|
|
from wcs.wf.geolocate import GeolocateWorkflowStatusItem
|
|
|
|
geolocate = GeolocateWorkflowStatusItem()
|
|
geolocate.method = 'address_string'
|
|
geolocate.address_string = prefill_value
|
|
coords = geolocate.geolocate_address_string(None, compute_template=False)
|
|
if not coords:
|
|
return (None, False)
|
|
return ('%(lat)s;%(lon)s' % coords, False)
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
widget = self.widget_class('x%s' % random.random(), value, readonly=True)
|
|
return widget.render_widget_content()
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
return indent + value
|
|
|
|
def convert_value_from_str(self, value):
|
|
try:
|
|
dummy, dummy = (float(x) for x in value.split(';'))
|
|
except (AttributeError, ValueError):
|
|
return None
|
|
return value
|
|
|
|
def get_json_value(self, value, **kwargs):
|
|
if not value or ';' not in value:
|
|
return None
|
|
lat, lon = value.split(';')
|
|
try:
|
|
lat = float(lat)
|
|
lon = float(lon)
|
|
except ValueError:
|
|
return None
|
|
return {'lat': lat, 'lon': lon}
|
|
|
|
def from_json_value(self, value):
|
|
if 'lat' in value and 'lon' in value:
|
|
return '%s;%s' % (float(value['lat']), float(value['lon']))
|
|
else:
|
|
return None
|
|
|
|
def get_structured_value(self, data):
|
|
return self.get_json_value(data.get(self.id))
|
|
|
|
def set_value(self, data, value, raise_on_error=False):
|
|
if value == '':
|
|
value = None
|
|
elif value and ';' not in value:
|
|
raise SetValueError('invalid coordinates %r (missing ;)' % value)
|
|
elif value:
|
|
try:
|
|
dummy, dummy = (float(x) for x in value.split(';'))
|
|
except ValueError:
|
|
# will catch both "too many values to unpack" and invalid float values
|
|
raise SetValueError('invalid coordinates %r' % value)
|
|
super().set_value(data, value)
|
|
|
|
|
|
register_field_class(MapField)
|
|
|
|
|
|
class RankedItemsField(WidgetField):
|
|
key = 'ranked-items'
|
|
description = _('Ranked Items')
|
|
allow_complex = True
|
|
|
|
items = []
|
|
randomize_items = False
|
|
widget_class = RankedItemsWidget
|
|
anonymise = False
|
|
|
|
def perform_more_widget_changes(self, form, kwargs, edit=True):
|
|
kwargs['elements'] = self.items or []
|
|
kwargs['randomize_items'] = self.randomize_items
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
try:
|
|
form.remove('prefill')
|
|
except KeyError: # perhaps it was already removed
|
|
pass
|
|
form.add(
|
|
WidgetList,
|
|
'items',
|
|
title=_('Items'),
|
|
element_type=StringWidget,
|
|
value=self.items,
|
|
required=True,
|
|
element_kwargs={'render_br': False, 'size': 50},
|
|
add_element_label=_('Add item'),
|
|
)
|
|
form.add(CheckboxWidget, 'randomize_items', title=_('Randomize Items'), value=self.randomize_items)
|
|
|
|
def get_admin_attributes(self):
|
|
attrs = WidgetField.get_admin_attributes(self) + ['items', 'randomize_items']
|
|
if 'prefill' in attrs:
|
|
attrs.remove('prefill')
|
|
return attrs
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<ul>')
|
|
items = list(value.items())
|
|
items.sort(key=lambda x: x[1] or sys.maxsize)
|
|
counter = 0
|
|
last_it = None
|
|
for it in items:
|
|
if it[1] is not None:
|
|
if last_it != it[1]:
|
|
counter += 1
|
|
last_it = it[1]
|
|
r += htmltext('<li>%s: %s</li>') % (counter, it[0])
|
|
r += htmltext('</ul>')
|
|
return r.getvalue()
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
items = list(value.items())
|
|
items.sort(key=lambda x: x[1] or sys.maxsize)
|
|
counter = 0
|
|
last_it = None
|
|
values = []
|
|
for it in items:
|
|
if it[1] is not None:
|
|
if last_it != it[1]:
|
|
counter += 1
|
|
last_it = it[1]
|
|
values.append('%s: %s' % (counter, it[0]))
|
|
return indent + ' / '.join(values)
|
|
|
|
def get_csv_heading(self):
|
|
if not self.items:
|
|
return [self.label]
|
|
return [self.label] + [''] * (len(self.items) - 1)
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
if not self.items:
|
|
return ['']
|
|
if not isinstance(element, dict):
|
|
element = {}
|
|
items = [x for x in element.items() if x[1] is not None]
|
|
items.sort(key=lambda x: x[1])
|
|
ranked = [x[0] for x in items]
|
|
return ranked + ['' for x in range(len(self.items) - len(ranked))]
|
|
|
|
|
|
register_field_class(RankedItemsField)
|
|
|
|
|
|
class PasswordField(WidgetField):
|
|
key = 'password'
|
|
description = _('Password')
|
|
|
|
min_length = 0
|
|
max_length = 0
|
|
count_uppercase = 0
|
|
count_lowercase = 0
|
|
count_digit = 0
|
|
count_special = 0
|
|
confirmation = True
|
|
confirmation_title = None
|
|
strength_indicator = True
|
|
formats = ['sha1']
|
|
extra_attributes = [
|
|
'formats',
|
|
'min_length',
|
|
'max_length',
|
|
'count_uppercase',
|
|
'count_lowercase',
|
|
'count_digit',
|
|
'count_special',
|
|
'confirmation',
|
|
'confirmation_title',
|
|
'strength_indicator',
|
|
]
|
|
|
|
widget_class = PasswordEntryWidget
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + self.extra_attributes
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
formats = [
|
|
('cleartext', _('Clear text')),
|
|
('md5', _('MD5')),
|
|
('sha1', _('SHA1')),
|
|
]
|
|
form.add(
|
|
CheckboxesWidget,
|
|
'formats',
|
|
title=_('Storage formats'),
|
|
value=self.formats,
|
|
options=formats,
|
|
inline=True,
|
|
)
|
|
form.add(IntWidget, 'min_length', title=_('Minimum length'), value=self.min_length)
|
|
form.add(
|
|
IntWidget,
|
|
'max_length',
|
|
title=_('Maximum password length'),
|
|
value=self.max_length,
|
|
hint=_('0 for unlimited length'),
|
|
)
|
|
form.add(
|
|
IntWidget,
|
|
'count_uppercase',
|
|
title=_('Minimum number of uppercase characters'),
|
|
value=self.count_uppercase,
|
|
)
|
|
form.add(
|
|
IntWidget,
|
|
'count_lowercase',
|
|
title=_('Minimum number of lowercase characters'),
|
|
value=self.count_lowercase,
|
|
)
|
|
form.add(IntWidget, 'count_digit', title=_('Minimum number of digits'), value=self.count_digit)
|
|
form.add(
|
|
IntWidget,
|
|
'count_special',
|
|
title=_('Minimum number of special characters'),
|
|
value=self.count_special,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'strength_indicator',
|
|
title=_('Add a password strength indicator'),
|
|
value=self.strength_indicator,
|
|
default_value=self.__class__.strength_indicator,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'confirmation',
|
|
title=_('Add a confirmation input'),
|
|
value=self.confirmation,
|
|
default_value=self.__class__.confirmation,
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'confirmation_title',
|
|
size=50,
|
|
title=_('Label for confirmation input'),
|
|
value=self.confirmation_title,
|
|
)
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
return '●' * 8
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
return [self.get_view_value(element)]
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
return indent + self.get_view_value(value)
|
|
|
|
|
|
register_field_class(PasswordField)
|
|
|
|
|
|
class BlockRowValue:
|
|
# a container for a value that will be added as a "line" of a block
|
|
def __init__(self, append=False, merge=False, existing=None, **kwargs):
|
|
self.append = append
|
|
self.merge = merge
|
|
self.attributes = kwargs
|
|
self.rows = None
|
|
if append is True:
|
|
self.rows = getattr(existing, 'rows', None) or []
|
|
self.rows.append(kwargs)
|
|
|
|
def check_current_value(self, current_block_value):
|
|
return (
|
|
isinstance(current_block_value, dict)
|
|
and 'data' in current_block_value
|
|
and isinstance(current_block_value['data'], list)
|
|
)
|
|
|
|
def make_value(self, block, field, data):
|
|
def make_row_data(attributes):
|
|
row_data = {}
|
|
for sub_field in block.fields:
|
|
if sub_field.varname and sub_field.varname in attributes:
|
|
sub_value = attributes.get(sub_field.varname)
|
|
if sub_field.convert_value_from_anything:
|
|
sub_value = sub_field.convert_value_from_anything(sub_value)
|
|
sub_field.set_value(row_data, sub_value)
|
|
return row_data
|
|
|
|
row_data = make_row_data(self.attributes)
|
|
|
|
current_block_value = data.get(field.id)
|
|
if not self.check_current_value(current_block_value):
|
|
current_block_value = None
|
|
if self.append and current_block_value:
|
|
block_value = current_block_value
|
|
block_value['data'].append(row_data)
|
|
elif self.merge is not False and field.id in data:
|
|
block_value = current_block_value
|
|
try:
|
|
merge_index = -1 if self.merge is True else int(self.merge)
|
|
block_value['data'][merge_index].update(row_data)
|
|
except (ValueError, IndexError):
|
|
# ValueError if self.merge is not an integer,
|
|
# IndexError if merge_index is out of range.
|
|
pass # ignore
|
|
elif self.rows:
|
|
rows_data = [make_row_data(x) for x in self.rows if x]
|
|
block_value = {'data': rows_data, 'schema': {x.id: x.key for x in block.fields}}
|
|
else:
|
|
block_value = {'data': [row_data], 'schema': {x.id: x.key for x in block.fields}}
|
|
return block_value
|
|
|
|
|
|
class BlockField(WidgetField):
|
|
key = 'block'
|
|
allow_complex = True
|
|
|
|
widget_class = BlockWidget
|
|
default_items_count = 1
|
|
max_items = 1
|
|
extra_attributes = [
|
|
'block',
|
|
'default_items_count',
|
|
'max_items',
|
|
'add_element_label',
|
|
'label_display',
|
|
'remove_button',
|
|
]
|
|
add_element_label = ''
|
|
label_display = 'normal'
|
|
remove_button = False
|
|
block_slug = None
|
|
|
|
# cache
|
|
_block = None
|
|
|
|
def migrate(self):
|
|
changed = False
|
|
if not self.block_slug: # 2023-05-21
|
|
self.block_slug = self.type.removeprefix('block:')
|
|
changed = True
|
|
return changed
|
|
|
|
@property
|
|
def block(self):
|
|
if self._block:
|
|
return self._block
|
|
self._block = BlockDef.get_on_index(self.block_slug, 'slug')
|
|
return self._block
|
|
|
|
def get_type_label(self):
|
|
try:
|
|
return _('Field Block (%s)') % self.block.name
|
|
except KeyError:
|
|
return _('Field Block (%s, missing)') % self.block_slug
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield self.block
|
|
|
|
def add_to_form(self, form, value=None):
|
|
try:
|
|
self.block
|
|
except KeyError:
|
|
raise MissingBlockFieldError(self.block_slug)
|
|
return super().add_to_form(form, value=value)
|
|
|
|
def fill_admin_form(self, form):
|
|
super().fill_admin_form(form)
|
|
form.add(
|
|
IntWidget,
|
|
'default_items_count',
|
|
title=_('Number of items to display by default'),
|
|
value=self.default_items_count,
|
|
)
|
|
form.add(IntWidget, 'max_items', title=_('Maximum number of items'), value=self.max_items)
|
|
form.add(
|
|
StringWidget, 'add_element_label', title=_('Label of "Add" button'), value=self.add_element_label
|
|
)
|
|
display_options = [
|
|
('normal', _('Normal')),
|
|
('subtitle', _('Subtitle')),
|
|
('hidden', _('Hidden')),
|
|
]
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'label_display',
|
|
title=_('Label display'),
|
|
value=self.label_display or 'normal',
|
|
options=display_options,
|
|
)
|
|
form.add(CheckboxWidget, 'remove_button', title=_('Include remove button'), value=self.remove_button)
|
|
|
|
def get_admin_attributes(self):
|
|
return super().get_admin_attributes() + [
|
|
'default_items_count',
|
|
'max_items',
|
|
'add_element_label',
|
|
'label_display',
|
|
'remove_button',
|
|
'block_slug', # only mentioned for xml export/import
|
|
]
|
|
|
|
def store_display_value(self, data, field_id, raise_on_error=False):
|
|
value = data.get(field_id)
|
|
parts = []
|
|
if value and value.get('data'):
|
|
for subvalue in value.get('data'):
|
|
parts.append(self.block.get_display_value(subvalue))
|
|
return ', '.join(parts)
|
|
|
|
def get_view_value(self, value, summary=False, include_unset_required_fields=False, **kwargs):
|
|
from wcs.workflows import template_on_formdata
|
|
|
|
if 'value_id' not in kwargs:
|
|
# when called from get_rst_view_value()
|
|
return str(value or '')
|
|
value = kwargs['value_id']
|
|
if value is None:
|
|
return ''
|
|
r = TemplateIO(html=True)
|
|
for i, row_value in enumerate(value['data']):
|
|
context = self.block.get_substitution_counter_variables(i)
|
|
for field in self.block.fields:
|
|
if summary and not field.include_in_summary_page:
|
|
continue
|
|
if not hasattr(field, 'get_value_info'):
|
|
# inert field
|
|
if field.include_in_summary_page:
|
|
with get_publisher().substitutions.temporary_feed(context):
|
|
if field.key == 'title':
|
|
label = template_on_formdata(None, field.label, autoescape=False)
|
|
r += htmltext('<div class="title %s"><h3>%s</h3></div>') % (
|
|
field.extra_css_class or '',
|
|
label,
|
|
)
|
|
elif field.key == 'subtitle':
|
|
label = template_on_formdata(None, field.label, autoescape=False)
|
|
r += htmltext('<div class="subtitle %s"><h4>%s</h4></div>') % (
|
|
field.extra_css_class or '',
|
|
label,
|
|
)
|
|
elif field.key == 'comment':
|
|
r += htmltext(
|
|
'<div class="comment-field %s">%s</div>'
|
|
% (field.extra_css_class or '', field.get_text())
|
|
)
|
|
continue
|
|
css_classes = ['field', 'field-type-%s' % field.key]
|
|
if field.extra_css_class:
|
|
css_classes.append(field.extra_css_class)
|
|
sub_value, sub_value_details = field.get_value_info(row_value)
|
|
if sub_value is None and not (field.required and include_unset_required_fields):
|
|
continue
|
|
label_id = f'form-field-label-f{self.id}-r{i}-s{field.id}'
|
|
r += htmltext('<div class="%s">' % ' '.join(css_classes))
|
|
r += htmltext('<p id="%s" class="label">%s</p> ') % (label_id, field.label)
|
|
if sub_value is None:
|
|
r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set')
|
|
else:
|
|
r += htmltext('<div class="value">')
|
|
kwargs = {'parent_field': self, 'parent_field_index': i, 'label_id': label_id}
|
|
kwargs.update(**sub_value_details)
|
|
r += field.get_view_value(sub_value, **kwargs)
|
|
r += htmltext('</div>')
|
|
r += htmltext('</div>\n')
|
|
return r.getvalue()
|
|
|
|
def get_csv_heading(self, subfield_label=None):
|
|
nb_items = self.max_items or 1
|
|
label = self.label
|
|
if subfield_label:
|
|
label += ' - %s' % subfield_label
|
|
if nb_items == 1:
|
|
return [label]
|
|
headings = ['%s - %s' % (label, x + 1) for x in range(nb_items)]
|
|
return headings
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
nb_items = self.max_items or 1
|
|
cells = [''] * nb_items
|
|
if element and element.get('data'):
|
|
for i, subvalue in enumerate(element.get('data')[:nb_items]):
|
|
if subvalue:
|
|
cells[i] = self.block.get_display_value(subvalue)
|
|
return cells
|
|
|
|
def set_value(self, data, value, **kwargs):
|
|
if value == '':
|
|
value = None
|
|
if isinstance(value, BlockRowValue):
|
|
value = value.make_value(block=self.block, field=self, data=data)
|
|
elif value and not (isinstance(value, dict) and 'data' in value and 'schema' in value):
|
|
raise SetValueError('invalid value for block')
|
|
super().set_value(data, value, **kwargs)
|
|
|
|
def get_json_value(self, value, **kwargs):
|
|
from wcs.formdata import FormData
|
|
|
|
result = []
|
|
if not value or not value.get('data'):
|
|
return result
|
|
for subvalue_data in value.get('data'):
|
|
result.append(
|
|
FormData.get_json_data_dict(
|
|
subvalue_data,
|
|
self.block.fields,
|
|
formdata=kwargs.get('formdata'),
|
|
include_files=kwargs.get('include_file_content'),
|
|
include_unnamed_fields=True,
|
|
)
|
|
)
|
|
return result
|
|
|
|
def from_json_value(self, value):
|
|
from wcs.api import posted_json_data_to_formdata_data
|
|
|
|
result = []
|
|
if isinstance(value, list):
|
|
for subvalue_data in value or []:
|
|
result.append(posted_json_data_to_formdata_data(self.block, subvalue_data))
|
|
|
|
return {'data': result, 'schema': {x.id: x.key for x in self.block.fields}}
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
node = ET.Element('{%s}span' % OD_NS['text'])
|
|
node.text = od_clean_text(force_str(value))
|
|
return node
|
|
|
|
def __getstate__(self):
|
|
# do not store _block cache
|
|
odict = copy.copy(self.__dict__)
|
|
if '_block' in odict:
|
|
del odict['_block']
|
|
return odict
|
|
|
|
def __setstate__(self, ndict):
|
|
# make sure a cached copy of _block is not restored
|
|
self.__dict__ = ndict
|
|
self._block = None
|
|
|
|
|
|
class ComputedField(Field):
|
|
key = 'computed'
|
|
description = _('Computed Data')
|
|
|
|
value_template = None
|
|
freeze_on_initial_value = False
|
|
data_source = {}
|
|
|
|
add_to_form = None
|
|
add_to_view_form = None
|
|
get_opendocument_node_value = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if not self.varname:
|
|
self.varname = misc.simplify(self.label, space='_')
|
|
|
|
def get_admin_attributes(self):
|
|
attributes = super().get_admin_attributes()
|
|
attributes.remove('condition')
|
|
return attributes + ['varname', 'value_template', 'freeze_on_initial_value', 'data_source']
|
|
|
|
def fill_admin_form(self, form):
|
|
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
|
|
form.add(
|
|
VarnameWidget,
|
|
'varname',
|
|
title=_('Identifier'),
|
|
required=True,
|
|
value=self.varname,
|
|
size=30,
|
|
hint=_('This is used as suffix for variable names.'),
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'value_template',
|
|
title=_('Value'),
|
|
required=True,
|
|
size=80,
|
|
value=self.value_template,
|
|
validation_function=ComputedExpressionWidget.validate_template,
|
|
hint=_('As a Django template'),
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'freeze_on_initial_value',
|
|
title=_('Freeze on initial value'),
|
|
value=self.freeze_on_initial_value,
|
|
)
|
|
form.add(
|
|
data_sources.DataSourceSelectionWidget,
|
|
'data_source',
|
|
value=self.data_source,
|
|
allowed_source_types={'cards'},
|
|
title=_('Data Source (cards only)'),
|
|
hint=_('This will make linked card data available for expressions.'),
|
|
required=False,
|
|
)
|
|
|
|
def get_real_data_source(self):
|
|
return data_sources.get_real(self.data_source)
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield from check_wscalls(self.value_template)
|
|
|
|
|
|
register_field_class(ComputedField)
|
|
|
|
|
|
def get_field_class_by_type(type):
|
|
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):
|
|
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()]
|
|
for klass in field_classes:
|
|
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
|