3414 lines
120 KiB
Python
3414 lines
120 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# 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
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from django.utils.encoding import force_bytes
|
|
from django.utils.encoding import force_text
|
|
from django.utils.encoding import smart_text
|
|
from django.utils.formats import date_format as django_date_format
|
|
from django.utils.html import urlize
|
|
from quixote import get_publisher
|
|
from quixote import get_request
|
|
from quixote import get_session
|
|
from quixote.html import TemplateIO
|
|
from quixote.html import htmlescape
|
|
from quixote.html import htmltag
|
|
from quixote.html import htmltext
|
|
|
|
from . import data_sources
|
|
from . import portfolio
|
|
from .blocks import BlockDef
|
|
from .blocks import BlockWidget
|
|
from .conditions import Condition
|
|
from .qommon import N_
|
|
from .qommon import _
|
|
from .qommon import evalutils
|
|
from .qommon import force_str
|
|
from .qommon import get_cfg
|
|
from .qommon import misc
|
|
from .qommon.form import AutocompleteStringWidget
|
|
from .qommon.form import CheckboxesWidget
|
|
from .qommon.form import CheckboxWidget
|
|
from .qommon.form import CommentWidget
|
|
from .qommon.form import CompositeWidget
|
|
from .qommon.form import ComputedExpressionWidget
|
|
from .qommon.form import ConditionWidget
|
|
from .qommon.form import DateWidget
|
|
from .qommon.form import EmailWidget
|
|
from .qommon.form import FileSizeWidget
|
|
from .qommon.form import FileWithPreviewWidget
|
|
from .qommon.form import HiddenWidget
|
|
from .qommon.form import HtmlWidget
|
|
from .qommon.form import IntWidget
|
|
from .qommon.form import JsonpSingleSelectWidget
|
|
from .qommon.form import MapMarkerSelectionWidget
|
|
from .qommon.form import MapWidget
|
|
from .qommon.form import PasswordEntryWidget
|
|
from .qommon.form import RadiobuttonsWidget
|
|
from .qommon.form import RankedItemsWidget
|
|
from .qommon.form import SingleSelectHintWidget
|
|
from .qommon.form import SingleSelectTableWidget
|
|
from .qommon.form import SingleSelectWidget
|
|
from .qommon.form import StringWidget
|
|
from .qommon.form import TableListRowsWidget
|
|
from .qommon.form import TableWidget
|
|
from .qommon.form import TextWidget
|
|
from .qommon.form import ValidationWidget
|
|
from .qommon.form import VarnameWidget
|
|
from .qommon.form import WcsExtraStringWidget
|
|
from .qommon.form import WidgetList
|
|
from .qommon.form import WidgetListAsTable
|
|
from .qommon.form import WysiwygTextWidget
|
|
from .qommon.misc import date_format
|
|
from .qommon.misc import ellipsize
|
|
from .qommon.misc import get_as_datetime
|
|
from .qommon.misc import get_document_type_value_options
|
|
from .qommon.misc import get_document_types
|
|
from .qommon.misc import strftime
|
|
from .qommon.misc import 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
|
|
from .qommon.template import TemplateError
|
|
from .qommon.upload_storage import PicklableUpload
|
|
|
|
|
|
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')),
|
|
('formula', _('Python Expression')),
|
|
('user', _('User Field')),
|
|
('geolocation', _('Geolocation')),
|
|
]
|
|
|
|
if field and field.type == 'items':
|
|
# limit choices to python as items field are prefilled with list
|
|
# of strings
|
|
options = [x for x in options if x[0] in ('none', 'formula')]
|
|
elif field and field.type == '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'),
|
|
attrs={'data-dynamic-display-parent': 'true'},
|
|
)
|
|
|
|
self.parse()
|
|
if not self.value:
|
|
self.value = {}
|
|
|
|
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.type == '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')),
|
|
]
|
|
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(
|
|
[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_:
|
|
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
|
|
type = 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
|
|
display_locations = []
|
|
prefill = None
|
|
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
|
|
|
|
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 unhtmled_label(self):
|
|
return force_str(html.unescape(force_text(re.sub('<.*?>', ' ', self.label or ''))).strip())
|
|
|
|
def get_admin_attributes(self):
|
|
return ['label', 'type', 'condition']
|
|
|
|
def export_to_json(self, include_id=False, anonymise=True):
|
|
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
|
|
if not 'type' in field:
|
|
field['type'] = self.key
|
|
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 = []
|
|
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 type(val) is dict and not val:
|
|
continue
|
|
el = ET.SubElement(field, attribute)
|
|
if type(val) is dict:
|
|
for k, v in sorted(val.items()):
|
|
if isinstance(v, str):
|
|
text_value = force_text(v, charset, errors='replace')
|
|
else:
|
|
# field having non str value in dictionnary field must overload
|
|
# import_to_xml to handle import
|
|
text_value = force_text(v)
|
|
ET.SubElement(el, k).text = text_value
|
|
elif type(val) is list:
|
|
if attribute[-1] == 's':
|
|
atname = attribute[:-1]
|
|
else:
|
|
atname = 'item'
|
|
for v in val:
|
|
ET.SubElement(el, atname).text = force_text(v, charset, errors='replace')
|
|
elif isinstance(val, str):
|
|
el.text = force_text(val, charset, errors='replace')
|
|
else:
|
|
el.text = str(val)
|
|
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 type(getattr(self, attribute)) is list:
|
|
v = [xml_node_text(x) for x in el]
|
|
elif type(getattr(self, attribute)) is 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 el.text is None:
|
|
if isinstance(getattr(self, attribute), list):
|
|
setattr(self, attribute, [])
|
|
else:
|
|
setattr(self, attribute, None)
|
|
elif el.text in ('False', 'True'): # bools
|
|
setattr(self, attribute, el.text == 'True')
|
|
elif type(getattr(self, attribute)) is 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_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)
|
|
|
|
context = get_publisher().substitutions.get_context_variables(mode='lazy')
|
|
try:
|
|
return (Template(value, autoescape=False, raises=True).render(context), 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 == '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:
|
|
return (
|
|
user.form_data.get(x),
|
|
explicit_lock or str(userfield.id) in (user.verified_fields or []),
|
|
)
|
|
|
|
elif t == 'formula':
|
|
formula = self.prefill.get('value')
|
|
try:
|
|
ret = eval(
|
|
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.prefill:
|
|
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) is not None:
|
|
if self.in_listing:
|
|
self.display_locations = self.display_locations[:]
|
|
self.display_locations.append('listings')
|
|
changed = True
|
|
self.in_listing = None
|
|
# repair dictionary attributes that may have been kept as bytes in
|
|
# the initial python 2 -> 3 conversion.
|
|
from wcs.qommon.storage import deep_bytes2str
|
|
|
|
for key in ('prefill', 'data_source'):
|
|
value = getattr(self, key, None)
|
|
if not value:
|
|
continue
|
|
if b'type' in value:
|
|
setattr(self, key, deep_bytes2str(getattr(self, key)))
|
|
return changed
|
|
|
|
def evaluate_condition(self, dict_vars, formdef, condition):
|
|
return PageCondition(condition, {'dict_vars': dict_vars, 'formdef': formdef}).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)' % formdef.var_prefix,
|
|
value or '',
|
|
)
|
|
|
|
def get_condition_varnames(self, formdef):
|
|
return self.get_referenced_varnames(formdef, self.condition['value'])
|
|
|
|
def has_live_conditions(self, formdef):
|
|
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].type == 'page':
|
|
break
|
|
else:
|
|
field_position = -1 # form with no page
|
|
# start from there
|
|
for field in formdef.fields[field_position + 1 :]:
|
|
if field.type == 'page':
|
|
# stop at next page
|
|
break
|
|
if field.varname in varnames:
|
|
return True
|
|
return False
|
|
|
|
def set_value(self, data, value):
|
|
data['%s' % self.id] = value
|
|
if self.store_display_value:
|
|
display_value = self.store_display_value(data, 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)
|
|
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 __repr__(self):
|
|
return '<%s %s %r>' % (self.__class__.__name__, self.id, self.label and self.label[:64])
|
|
|
|
|
|
class WidgetField(Field):
|
|
hint = None
|
|
required = True
|
|
display_locations = ['validation', 'summary']
|
|
extra_attributes = []
|
|
prefill = {}
|
|
|
|
widget_class = None
|
|
|
|
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(self.hint)
|
|
else:
|
|
hint = self.hint
|
|
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
|
|
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 not 'size' 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
|
|
|
|
def get_display_locations_options(self):
|
|
return [
|
|
('validation', _('Validation Page')),
|
|
('summary', _('Summary Page')),
|
|
('listings', _('Management Listings')),
|
|
]
|
|
|
|
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)
|
|
form.add(
|
|
VarnameWidget,
|
|
'varname',
|
|
title=_('Identifier'),
|
|
value=self.varname,
|
|
size=30,
|
|
advanced=(not self.varname),
|
|
hint=_('This is used as suffix for variable names.'),
|
|
)
|
|
form.add(TextWidget, 'hint', title=_('Hint'), value=self.hint, cols=60, rows=3)
|
|
form.add(
|
|
CheckboxesWidget,
|
|
'display_locations',
|
|
title=_('Display Locations'),
|
|
options=self.get_display_locations_options(),
|
|
value=self.display_locations,
|
|
advanced=True,
|
|
)
|
|
form.add(
|
|
StringWidget,
|
|
'extra_css_class',
|
|
title=_('Extra classes for CSS styling'),
|
|
value=self.extra_css_class,
|
|
size=30,
|
|
advanced=(not self.extra_css_class),
|
|
)
|
|
prefill_in_advanced = not self.prefill or self.prefill.get('type') == 'none'
|
|
if prefill_in_advanced and (
|
|
get_request().form.get('prefill$apply') or get_request().form.get('prefill$value')
|
|
):
|
|
# check if the apply button has been clicked, and get the field out
|
|
# of the "advanced parameters" section if that's the case.
|
|
prefill_in_advanced = False
|
|
form.add(
|
|
PrefillSelectionWidget,
|
|
'prefill',
|
|
title=_('Prefill'),
|
|
value=self.prefill,
|
|
advanced=prefill_in_advanced,
|
|
field=self,
|
|
)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
django_only=True,
|
|
advanced=not (bool(self.condition)),
|
|
)
|
|
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.'),
|
|
)
|
|
|
|
def check_admin_form(self, form):
|
|
return
|
|
|
|
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]
|
|
|
|
|
|
field_classes = []
|
|
field_types = []
|
|
|
|
|
|
def register_field_class(klass):
|
|
if not klass 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 = N_('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, 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,
|
|
advanced=(not self.extra_css_class),
|
|
)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
django_only=True,
|
|
advanced=not (bool(self.condition)),
|
|
)
|
|
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']
|
|
|
|
|
|
register_field_class(TitleField)
|
|
|
|
|
|
class SubtitleField(TitleField):
|
|
key = 'subtitle'
|
|
description = N_('Subtitle')
|
|
html_tag = 'h4'
|
|
|
|
|
|
register_field_class(SubtitleField)
|
|
|
|
|
|
class CommentField(Field):
|
|
key = 'comment'
|
|
description = N_('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 ''
|
|
if self.label.startswith('<'):
|
|
return self.label
|
|
if '\n\n' in self.label:
|
|
# blank lines to paragraphs
|
|
label = '</p>\n<p>'.join([str(htmlescape(x)) for x in re.split('\n\n+', self.label)])
|
|
return '<p>' + label + '</p>'
|
|
return '<p>%s</p>' % str(htmlescape(self.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=(not self.extra_css_class),
|
|
)
|
|
form.add(
|
|
ConditionWidget,
|
|
'condition',
|
|
title=_('Display Condition'),
|
|
value=self.condition,
|
|
required=False,
|
|
size=50,
|
|
django_only=True,
|
|
advanced=not (bool(self.condition)),
|
|
)
|
|
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']
|
|
|
|
|
|
register_field_class(CommentField)
|
|
|
|
|
|
def is_datasource_advanced(value):
|
|
data_source_in_advanced = not value
|
|
if data_source_in_advanced and (
|
|
get_request().form.get('data_source$apply') or get_request().form.get('data_source$value')
|
|
):
|
|
data_source_in_advanced = False
|
|
return data_source_in_advanced
|
|
|
|
|
|
class StringField(WidgetField):
|
|
key = 'string'
|
|
description = N_('Text (line)')
|
|
|
|
widget_class = WcsExtraStringWidget
|
|
size = None
|
|
extra_attributes = ['size']
|
|
validation = {}
|
|
data_source = {}
|
|
|
|
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
|
|
|
|
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=(not self.validation),
|
|
)
|
|
form.add(
|
|
data_sources.DataSourceSelectionWidget,
|
|
'data_source',
|
|
value=self.data_source,
|
|
title=_('Data Source'),
|
|
hint=_('This will allow autocompletion from an external source.'),
|
|
advanced=is_datasource_advanced(self.data_source),
|
|
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_text(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_text(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
|
|
|
|
@classmethod
|
|
def convert_value_from_anything(cls, value):
|
|
if value is None:
|
|
return None
|
|
return str(value)
|
|
|
|
def migrate(self):
|
|
changed = super().migrate()
|
|
if isinstance(self.validation, str):
|
|
self.validation = {'type': 'regex', 'value': self.validation}
|
|
changed = True
|
|
return changed
|
|
|
|
def init_with_xml(self, element, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(element, charset, include_id=include_id)
|
|
self.migrate()
|
|
|
|
|
|
register_field_class(StringField)
|
|
|
|
|
|
class TextField(WidgetField):
|
|
key = 'text'
|
|
description = N_('Long Text')
|
|
|
|
widget_class = TextWidget
|
|
cols = None
|
|
rows = None
|
|
pre = False
|
|
maxlength = None
|
|
extra_attributes = ['cols', 'rows', 'maxlength']
|
|
|
|
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)
|
|
form.add(CheckboxWidget, 'pre', title=_('Preformatted Text'), value=self.pre, advanced=not (self.pre))
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + ['cols', 'rows', 'pre', 'maxlength']
|
|
|
|
def convert_value_from_str(self, value):
|
|
return value
|
|
|
|
def get_view_value(self, value, **kwargs):
|
|
if self.pre:
|
|
return htmltext('<pre>') + value + htmltext('</pre>')
|
|
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):
|
|
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):
|
|
return ellipsize(str(value), max_len)
|
|
|
|
|
|
register_field_class(TextField)
|
|
|
|
|
|
class EmailField(WidgetField):
|
|
key = 'email'
|
|
description = N_('Email')
|
|
|
|
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 = N_('Check Box (single choice)')
|
|
allow_complex = 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 _('Yes')
|
|
elif value is False or value == 'False':
|
|
return _('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 += _('Yes')
|
|
value = True
|
|
else:
|
|
r += _('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()
|
|
|
|
|
|
register_field_class(BoolField)
|
|
|
|
|
|
class FileField(WidgetField):
|
|
key = 'file'
|
|
description = N_('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=not (self.document_type),
|
|
)
|
|
form.add(
|
|
FileSizeWidget,
|
|
'max_file_size',
|
|
title=_('Max file size'),
|
|
value=self.max_file_size,
|
|
advanced=not (self.max_file_size),
|
|
)
|
|
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=(self.allow_portfolio_picking is FileField.allow_portfolio_picking),
|
|
)
|
|
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=bool(not self.storage or self.storage == 'default'),
|
|
)
|
|
|
|
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']))
|
|
if 'filename' in value and 'content' in value:
|
|
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_download_query_string(self, **kwargs):
|
|
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 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 '%sdownload?%s' % (formdata.get_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_text(value))
|
|
return node
|
|
|
|
def get_csv_value(self, value, **kwargs):
|
|
return [str(value) if value 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, **kwargs)
|
|
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 migrate(self):
|
|
changed = super().migrate()
|
|
if 'file_type' in self.__dict__:
|
|
self.document_type = {}
|
|
if self.__dict__['file_type']:
|
|
file_type = self.__dict__['file_type']
|
|
document_types = get_document_types(self.document_type)
|
|
parts = []
|
|
for key, value in document_types.items():
|
|
if file_type == value.get('mimetypes'):
|
|
self.document_type = value.copy()
|
|
self.document_type['id'] = key
|
|
break
|
|
if not value.get('mimetypes'):
|
|
continue
|
|
if ','.join(value['mimetypes']) in file_type:
|
|
parts.append(value['label'])
|
|
else:
|
|
# self.file_type is a combination of file type, we create a
|
|
# virtual one from them
|
|
if parts and len(parts) > 1:
|
|
label = ', '.join(parts)
|
|
else:
|
|
label = ','.join(file_type)
|
|
self.document_type = {
|
|
'id': '_legacy',
|
|
'label': label,
|
|
'mimetypes': file_type,
|
|
}
|
|
del self.__dict__['file_type']
|
|
changed = True
|
|
return changed
|
|
|
|
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, element, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(element, 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 = N_('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',
|
|
]
|
|
|
|
@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 ''
|
|
try:
|
|
return strftime(date_format(), value)
|
|
except TypeError:
|
|
return ''
|
|
|
|
def add_to_form(self, form, value=None):
|
|
if value and type(value) is not 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.type == '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
|
|
|
|
def fill_zoom_admin_form(self, form, **kwargs):
|
|
# 0: whole world, 9: wide area, 11: area, 13: town, 16: small road
|
|
zoom_levels = [
|
|
(None, '---'),
|
|
('0', _('Whole world')),
|
|
('6', _('Country')),
|
|
('9', _('Wide area')),
|
|
('11', _('Area')),
|
|
('13', _('Town')),
|
|
('16', _('Small road')),
|
|
('18', _('Neighbourhood')),
|
|
('19', _('Ant')),
|
|
]
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'initial_zoom',
|
|
title=_('Initial zoom level'),
|
|
value=self.initial_zoom or '13',
|
|
options=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.')
|
|
)
|
|
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 add_items_fields_admin_form(self, form):
|
|
real_data_source = data_sources.get_real(self.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'},
|
|
)
|
|
form.add(
|
|
data_sources.DataSourceSelectionWidget,
|
|
'data_source',
|
|
value=self.data_source,
|
|
required=False,
|
|
hint=_('This will get the available items from an external source.'),
|
|
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())
|
|
|
|
|
|
class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
|
|
key = 'item'
|
|
description = N_('List')
|
|
allow_complex = True
|
|
|
|
items = []
|
|
show_as_radio = None
|
|
anonymise = False
|
|
widget_class = SingleSelectHintWidget
|
|
data_source = {}
|
|
in_filters = False
|
|
display_disabled_items = False
|
|
display_mode = 'list'
|
|
initial_date_alignment = 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):
|
|
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, element, charset, include_id=False, snapshot=False):
|
|
super().init_with_xml(element, charset, include_id=include_id)
|
|
if getattr(element.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', '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)]
|
|
if self.items:
|
|
return [(x, x) for x in self.items]
|
|
return []
|
|
|
|
def get_extended_options(self):
|
|
if self.data_source:
|
|
return data_sources.get_structured_items(self.data_source, mode='lazy')
|
|
if self.items:
|
|
return [{'id': x, 'text': x} for x in self.items]
|
|
return []
|
|
|
|
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():
|
|
self.url = kwargs['url'] = data_source.get_jsonp_url()
|
|
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 == 'radio':
|
|
self.widget_class = RadiobuttonsWidget
|
|
if type(kwargs['options'][0]) is 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_display_value(self, value):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source is None:
|
|
return value or ''
|
|
|
|
if data_source.type == 'jsonp':
|
|
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)
|
|
if self.display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
|
|
# store display value in session to be used by select2
|
|
url = data_source.get_jsonp_url()
|
|
if not get_session().jsonp_display_values:
|
|
get_session().jsonp_display_values = {}
|
|
get_session().jsonp_display_values['%s_%s' % (url, value)] = display_value
|
|
|
|
return display_value
|
|
|
|
def get_view_value(self, value, value_id=None, **kwargs):
|
|
value = super().get_view_value(value)
|
|
if not (
|
|
value_id
|
|
and get_request()
|
|
and get_request().is_in_backoffice()
|
|
and self.data_source
|
|
and self.data_source.get('type', '').startswith('carddef:')
|
|
):
|
|
return value
|
|
from wcs.carddef import CardDef
|
|
|
|
try:
|
|
carddef = CardDef.get_by_urlname(self.data_source['type'][8:])
|
|
carddata = carddef.data_class().get(value_id)
|
|
except KeyError:
|
|
return value
|
|
if not 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_text(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
|
|
|
|
def store_display_value(self, data, field_id):
|
|
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
|
|
return self.get_display_value(value)
|
|
|
|
def store_structured_value(self, data, field_id):
|
|
data_source = data_sources.get_object(self.data_source)
|
|
if data_source is None:
|
|
return
|
|
|
|
if data_source.type == 'jsonp':
|
|
return
|
|
|
|
value = data_source.get_structured_value(data.get(field_id))
|
|
if value is None or set(value.keys()) == set(['id', 'text']):
|
|
return
|
|
return value
|
|
|
|
def convert_value_from_anything(self, value):
|
|
if value is None:
|
|
return None
|
|
return str(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=not (self.display_disabled_items),
|
|
)
|
|
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'}
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'items',
|
|
'display_mode',
|
|
'data_source',
|
|
'in_filters',
|
|
'anonymise',
|
|
'display_disabled_items',
|
|
'initial_zoom',
|
|
'min_zoom',
|
|
'max_zoom',
|
|
'initial_date_alignment',
|
|
]
|
|
|
|
def check_admin_form(self, 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 = data_sources.get_real(self.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, value, display_value=None, **kwargs):
|
|
values = [value]
|
|
if self.data_source:
|
|
values.append(display_value)
|
|
return values
|
|
|
|
def export_to_json(self, include_id=False, anonymise=True):
|
|
field = super().export_to_json(include_id=include_id, anonymise=anonymise)
|
|
if self.data_source and not anonymise:
|
|
structured_items = data_sources.get_structured_items(self.data_source)
|
|
if structured_items:
|
|
field['structured_items'] = structured_items
|
|
if not field.get('items'):
|
|
field['items'] = [item['text'] for item in structured_items]
|
|
return field
|
|
|
|
|
|
register_field_class(ItemField)
|
|
|
|
|
|
class ItemsField(WidgetField, ItemFieldMixin):
|
|
key = 'items'
|
|
description = N_('Multiple choice list')
|
|
allow_complex = True
|
|
|
|
items = []
|
|
min_choices = 0
|
|
max_choices = 0
|
|
data_source = {}
|
|
in_filters = False
|
|
display_disabled_items = False
|
|
|
|
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, 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
|
|
|
|
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,
|
|
)
|
|
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=not (self.display_disabled_items),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return WidgetField.get_admin_attributes(self) + [
|
|
'items',
|
|
'min_choices',
|
|
'max_choices',
|
|
'data_source',
|
|
'in_filters',
|
|
'anonymise',
|
|
'display_disabled_items',
|
|
]
|
|
|
|
def check_admin_form(self, form):
|
|
self.check_items_admin_form(form)
|
|
|
|
def get_prefill_value(self, user=None, force_string=True):
|
|
return super().get_prefill_value(user=user, force_string=False)
|
|
|
|
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 [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 type(value) is 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):
|
|
labels = [self.label]
|
|
if self.max_choices:
|
|
labels.extend([''] * (self.max_choices - 1))
|
|
elif len(self.get_options()):
|
|
labels.extend([''] * (len(self.get_options()) - 1))
|
|
return labels
|
|
|
|
def get_csv_value(self, value, **kwargs):
|
|
values = []
|
|
for one_value in value:
|
|
values.append(one_value)
|
|
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 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):
|
|
options = self.get_options()
|
|
if not options:
|
|
return ''
|
|
choices = []
|
|
for choice in data.get(field_id) or []:
|
|
if type(options[0]) is 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
|
|
return ', '.join(choices)
|
|
|
|
def store_structured_value(self, data, field_id):
|
|
if not self.data_source:
|
|
return
|
|
structured_options = data_sources.get_structured_items(self.data_source)
|
|
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, anonymise=True):
|
|
field = super().export_to_json(include_id=include_id, anonymise=True)
|
|
if self.data_source and not anonymise:
|
|
structured_items = data_sources.get_structured_items(self.data_source)
|
|
if structured_items:
|
|
field['structured_items'] = structured_items
|
|
if not field.get('items'):
|
|
field['items'] = [item['text'] for item in structured_items]
|
|
return field
|
|
|
|
|
|
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):
|
|
log_errors = True
|
|
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:
|
|
live_data = get_dict_with_varnames(formdef.fields, dict_vars)
|
|
form_live_data = dict(('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 = N_('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_text(
|
|
post_condition['condition'].get('type') or '', charset, errors='replace'
|
|
)
|
|
ET.SubElement(condition_node, 'value').text = force_text(
|
|
post_condition['condition'].get('value') or '', charset, errors='replace'
|
|
)
|
|
ET.SubElement(post_condition_node, 'error_message').text = force_text(
|
|
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=not (self.post_conditions),
|
|
)
|
|
|
|
def get_admin_attributes(self):
|
|
return Field.get_admin_attributes(self) + ['post_conditions']
|
|
|
|
def migrate(self):
|
|
changed = super().migrate()
|
|
if isinstance(self.condition, str):
|
|
if self.condition:
|
|
self.condition = {'type': 'python', 'value': self.condition}
|
|
else:
|
|
self.condition = {}
|
|
changed = True
|
|
for post_condition in self.post_conditions or []:
|
|
condition = post_condition.get('condition')
|
|
if isinstance(condition, str):
|
|
if condition:
|
|
post_condition['condition'] = {'type': 'python', 'value': condition}
|
|
else:
|
|
post_condition['condition'] = {}
|
|
changed = True
|
|
return changed
|
|
|
|
def add_to_view_form(self, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
register_field_class(PageField)
|
|
|
|
|
|
class TableField(WidgetField):
|
|
key = 'table'
|
|
description = N_('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_text(column)))
|
|
|
|
for i, row in enumerate(value):
|
|
value[i] = [x or '' for x in row]
|
|
|
|
def get_value(i, j):
|
|
try:
|
|
return smart_text(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_text(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_text(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, value, **kwargs):
|
|
if not self.columns:
|
|
return ['']
|
|
values = []
|
|
for i in range(len(self.columns)):
|
|
for j in range(len(self.rows)):
|
|
try:
|
|
values.append(value[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
|
|
|
|
|
|
register_field_class(TableField)
|
|
|
|
|
|
class TableSelectField(TableField):
|
|
key = 'table-select'
|
|
description = N_('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 = N_('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)
|
|
|
|
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_text(column)))
|
|
|
|
for i, row in enumerate(value):
|
|
value[i] = [x or '' for x in row]
|
|
|
|
def get_value(i, j):
|
|
try:
|
|
return smart_text(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_text(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_text(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 = N_('Map')
|
|
|
|
default_position = None
|
|
init_with_geoloc = False
|
|
|
|
widget_class = MapWidget
|
|
extra_attributes = ['initial_zoom', 'min_zoom', 'max_zoom', 'default_position', 'init_with_geoloc']
|
|
|
|
def fill_admin_form(self, form):
|
|
WidgetField.fill_admin_form(self, form)
|
|
self.fill_zoom_admin_form(form)
|
|
form.add(
|
|
MapWidget,
|
|
'default_position',
|
|
title=_('Initial Position'),
|
|
value=self.default_position,
|
|
default_zoom='9',
|
|
required=False,
|
|
)
|
|
form.add(
|
|
CheckboxWidget,
|
|
'init_with_geoloc',
|
|
title=_('Initialize position using device geolocation'),
|
|
value=self.init_with_geoloc,
|
|
required=False,
|
|
)
|
|
|
|
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',
|
|
'default_position',
|
|
'init_with_geoloc',
|
|
]
|
|
|
|
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))
|
|
|
|
|
|
register_field_class(MapField)
|
|
|
|
|
|
class RankedItemsField(WidgetField):
|
|
key = 'ranked-items'
|
|
description = N_('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, value, **kwargs):
|
|
if not self.items:
|
|
return ['']
|
|
if type(value) is not dict:
|
|
value = {}
|
|
items = [x for x in value.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 = N_('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,
|
|
)
|
|
form.add(CheckboxWidget, 'confirmation', title=_('Add a confirmation input'), value=self.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, value, **kwargs):
|
|
return [self.get_view_value(value)]
|
|
|
|
def get_rst_view_value(self, value, indent=''):
|
|
return indent + self.get_view_value(value)
|
|
|
|
|
|
register_field_class(PasswordField)
|
|
|
|
|
|
class BlockField(WidgetField):
|
|
key = 'block'
|
|
allow_complex = True
|
|
|
|
widget_class = BlockWidget
|
|
max_items = 1
|
|
extra_attributes = ['block', 'max_items', 'add_element_label', 'label_display', 'remove_button']
|
|
add_element_label = ''
|
|
label_display = 'normal'
|
|
remove_button = False
|
|
|
|
# cache
|
|
_block = None
|
|
|
|
@property
|
|
def block(self):
|
|
if self._block:
|
|
return self._block
|
|
self._block = BlockDef.get_on_index(self.type[6:], 'slug')
|
|
return self._block
|
|
|
|
def get_type_label(self):
|
|
return _('Field Block (%s)') % self.block.name
|
|
|
|
def fill_admin_form(self, form):
|
|
super().fill_admin_form(form)
|
|
if form.get_widget('prefill'):
|
|
form.remove('prefill')
|
|
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() + [
|
|
'max_items',
|
|
'add_element_label',
|
|
'label_display',
|
|
'remove_button',
|
|
]
|
|
|
|
def store_display_value(self, data, field_id):
|
|
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, **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']
|
|
r = TemplateIO(html=True)
|
|
for i, row_value in enumerate(value['data']):
|
|
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:
|
|
if field.type == '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.type == '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.type == '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)
|
|
r += htmltext('<div class="%s">' % ' '.join(css_classes))
|
|
r += htmltext('<span class="label">%s</span> ') % field.label
|
|
sub_value, sub_value_details = field.get_value_info(row_value)
|
|
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}
|
|
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):
|
|
nb_items = self.max_items or 1
|
|
if nb_items == 1:
|
|
return [self.label]
|
|
headings = ['%s - %s' % (self.label, x + 1) for x in range(nb_items)]
|
|
return headings
|
|
|
|
def get_csv_value(self, value, **kwargs):
|
|
nb_items = self.max_items or 1
|
|
cells = [''] * nb_items
|
|
if value and value.get('data'):
|
|
for i, subvalue in enumerate(value.get('data')[:nb_items]):
|
|
if subvalue:
|
|
cells[i] = self.block.get_display_value(subvalue)
|
|
return cells
|
|
|
|
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, include_files=kwargs.get('include_file_content')
|
|
)
|
|
)
|
|
return result
|
|
|
|
|
|
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_types():
|
|
return field_types
|
|
|
|
|
|
def get_field_options(blacklisted_types):
|
|
widgets, non_widgets = [], []
|
|
for klass in field_classes:
|
|
if klass.key in blacklisted_types:
|
|
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 get_publisher().has_site_option('fields-blocks') and (
|
|
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
|