353 lines
13 KiB
Python
353 lines
13 KiB
Python
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2023 Entr'ouvert
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import copy
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from django.utils.encoding import force_str
|
|
from quixote import get_publisher
|
|
from quixote.html import TemplateIO, htmltext
|
|
|
|
from wcs.blocks import BlockDef, BlockWidget
|
|
from wcs.qommon import _
|
|
from wcs.qommon.form import CheckboxWidget, IntWidget, SingleSelectWidget, StringWidget
|
|
from wcs.qommon.ods import NS as OD_NS
|
|
from wcs.qommon.ods import clean_text as od_clean_text
|
|
|
|
from .base import SetValueError, WidgetField
|
|
from .item import UnknownCardValueError
|
|
|
|
|
|
class MissingBlockFieldError(Exception):
|
|
def __init__(self, block_slug):
|
|
self.block_slug = block_slug
|
|
|
|
def __str__(self):
|
|
return force_str(_('Missing block field: %s') % self.block_slug)
|
|
|
|
|
|
class BlockRowValue:
|
|
# a container for a value that will be added as a "line" of a block
|
|
def __init__(self, append=False, merge=False, existing=None, **kwargs):
|
|
self.append = append
|
|
self.merge = merge
|
|
self.attributes = kwargs
|
|
self.rows = None
|
|
if append is True:
|
|
self.rows = getattr(existing, 'rows', None) or []
|
|
self.rows.append(kwargs)
|
|
|
|
def check_current_value(self, current_block_value):
|
|
return (
|
|
isinstance(current_block_value, dict)
|
|
and 'data' in current_block_value
|
|
and isinstance(current_block_value['data'], list)
|
|
)
|
|
|
|
def make_value(self, block, field, data):
|
|
def make_row_data(attributes):
|
|
row_data = {}
|
|
for sub_field in block.fields:
|
|
if sub_field.varname and sub_field.varname in attributes:
|
|
sub_value = attributes.get(sub_field.varname)
|
|
if sub_field.convert_value_from_anything:
|
|
sub_value = sub_field.convert_value_from_anything(sub_value)
|
|
sub_field.set_value(row_data, sub_value)
|
|
return row_data
|
|
|
|
try:
|
|
row_data = make_row_data(self.attributes)
|
|
except UnknownCardValueError as e:
|
|
get_publisher().record_error(_('invalid value when creating block: %s') % str(e), exception=e)
|
|
return None
|
|
|
|
current_block_value = data.get(field.id)
|
|
if not self.check_current_value(current_block_value):
|
|
current_block_value = None
|
|
if self.append and current_block_value:
|
|
block_value = current_block_value
|
|
block_value['data'].append(row_data)
|
|
elif self.merge is not False and field.id in data:
|
|
block_value = current_block_value
|
|
try:
|
|
merge_index = -1 if self.merge is True else int(self.merge)
|
|
block_value['data'][merge_index].update(row_data)
|
|
except (ValueError, IndexError):
|
|
# ValueError if self.merge is not an integer,
|
|
# IndexError if merge_index is out of range.
|
|
pass # ignore
|
|
elif self.rows:
|
|
rows_data = [make_row_data(x) for x in self.rows if x]
|
|
block_value = {'data': rows_data, 'schema': {x.id: x.key for x in block.fields}}
|
|
else:
|
|
block_value = {'data': [row_data], 'schema': {x.id: x.key for x in block.fields}}
|
|
return block_value
|
|
|
|
|
|
class BlockField(WidgetField):
|
|
key = 'block'
|
|
allow_complex = True
|
|
|
|
widget_class = BlockWidget
|
|
default_items_count = 1
|
|
max_items = 1
|
|
extra_attributes = [
|
|
'block',
|
|
'default_items_count',
|
|
'max_items',
|
|
'add_element_label',
|
|
'label_display',
|
|
'remove_button',
|
|
]
|
|
add_element_label = ''
|
|
label_display = 'normal'
|
|
remove_button = False
|
|
block_slug = None
|
|
|
|
# cache
|
|
_block = None
|
|
|
|
def migrate(self):
|
|
changed = False
|
|
if not self.block_slug: # 2023-05-21
|
|
self.block_slug = self.type.removeprefix('block:')
|
|
changed = True
|
|
return changed
|
|
|
|
@property
|
|
def block(self):
|
|
if self._block:
|
|
return self._block
|
|
self._block = BlockDef.get_on_index(self.block_slug, 'slug')
|
|
return self._block
|
|
|
|
def get_type_label(self):
|
|
try:
|
|
return _('Field Block (%s)') % self.block.name
|
|
except KeyError:
|
|
return _('Field Block (%s, missing)') % self.block_slug
|
|
|
|
def get_dependencies(self):
|
|
yield from super().get_dependencies()
|
|
yield self.block
|
|
|
|
def add_to_form(self, form, value=None):
|
|
try:
|
|
self.block
|
|
except KeyError:
|
|
raise MissingBlockFieldError(self.block_slug)
|
|
return super().add_to_form(form, value=value)
|
|
|
|
def fill_admin_form(self, form):
|
|
super().fill_admin_form(form)
|
|
form.add(
|
|
IntWidget,
|
|
'default_items_count',
|
|
title=_('Number of items to display by default'),
|
|
value=self.default_items_count,
|
|
)
|
|
form.add(
|
|
IntWidget, 'max_items', title=_('Maximum number of items'), value=self.max_items, required=True
|
|
)
|
|
form.add(
|
|
StringWidget, 'add_element_label', title=_('Label of "Add" button'), value=self.add_element_label
|
|
)
|
|
display_options = [
|
|
('normal', _('Normal')),
|
|
('subtitle', _('Subtitle')),
|
|
('hidden', _('Hidden')),
|
|
]
|
|
form.add(
|
|
SingleSelectWidget,
|
|
'label_display',
|
|
title=_('Label display'),
|
|
value=self.label_display or 'normal',
|
|
options=display_options,
|
|
)
|
|
form.add(CheckboxWidget, 'remove_button', title=_('Include remove button'), value=self.remove_button)
|
|
|
|
def get_admin_attributes(self):
|
|
return super().get_admin_attributes() + [
|
|
'default_items_count',
|
|
'max_items',
|
|
'add_element_label',
|
|
'label_display',
|
|
'remove_button',
|
|
'block_slug', # only mentioned for xml export/import
|
|
]
|
|
|
|
def store_display_value(self, data, field_id, raise_on_error=False):
|
|
value = data.get(field_id)
|
|
parts = []
|
|
if value and value.get('data'):
|
|
for subvalue in value.get('data'):
|
|
parts.append(self.block.get_display_value(subvalue) or '')
|
|
return ', '.join(parts)
|
|
|
|
def get_view_value(self, value, summary=False, include_unset_required_fields=False, **kwargs):
|
|
from wcs.workflows import template_on_formdata
|
|
|
|
if 'value_id' not in kwargs:
|
|
# when called from get_rst_view_value()
|
|
return str(value or '')
|
|
value = kwargs['value_id']
|
|
if value is None:
|
|
return ''
|
|
r = TemplateIO(html=True)
|
|
for i, row_value in enumerate(value['data']):
|
|
try:
|
|
block = self.block
|
|
except KeyError:
|
|
# block was deleted, ignore
|
|
continue
|
|
context = block.get_substitution_counter_variables(i)
|
|
for field in block.fields:
|
|
if summary and not field.include_in_summary_page:
|
|
continue
|
|
if not hasattr(field, 'get_value_info'):
|
|
# inert field
|
|
if field.include_in_summary_page:
|
|
with get_publisher().substitutions.temporary_feed(context):
|
|
if field.key == 'title':
|
|
label = template_on_formdata(None, field.label, autoescape=False)
|
|
r += htmltext('<div class="title %s"><h3>%s</h3></div>') % (
|
|
field.extra_css_class or '',
|
|
label,
|
|
)
|
|
elif field.key == 'subtitle':
|
|
label = template_on_formdata(None, field.label, autoescape=False)
|
|
r += htmltext('<div class="subtitle %s"><h4>%s</h4></div>') % (
|
|
field.extra_css_class or '',
|
|
label,
|
|
)
|
|
elif field.key == 'comment':
|
|
r += htmltext(
|
|
'<div class="comment-field %s">%s</div>'
|
|
% (field.extra_css_class or '', field.get_text())
|
|
)
|
|
continue
|
|
css_classes = ['field', 'field-type-%s' % field.key]
|
|
if field.extra_css_class:
|
|
css_classes.append(field.extra_css_class)
|
|
sub_value, sub_value_details = field.get_value_info(row_value)
|
|
if sub_value is None and not (field.required and include_unset_required_fields):
|
|
continue
|
|
label_id = f'form-field-label-f{self.id}-r{i}-s{field.id}'
|
|
r += htmltext('<div class="%s">' % ' '.join(css_classes))
|
|
r += htmltext('<p id="%s" class="label">%s</p> ') % (label_id, field.label)
|
|
if sub_value is None:
|
|
r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set')
|
|
else:
|
|
r += htmltext('<div class="value">')
|
|
kwargs = {'parent_field': self, 'parent_field_index': i, 'label_id': label_id}
|
|
kwargs.update(**sub_value_details)
|
|
r += field.get_view_value(sub_value, **kwargs)
|
|
r += htmltext('</div>')
|
|
r += htmltext('</div>\n')
|
|
return r.getvalue()
|
|
|
|
def get_value_info(self, data):
|
|
value = data.get(self.id)
|
|
if value and not any(x for x in value.get('data') or []):
|
|
# skip if there are no values
|
|
return (None, {})
|
|
value_info, value_details = super().get_value_info(data)
|
|
if value_info is None and value_details not in (None, {'value_id': None}):
|
|
# buggy digest template created an empty value, switch it to an empty string
|
|
# so it's not considered empty in summary page.
|
|
value_info = ''
|
|
return (value_info, value_details)
|
|
|
|
def get_csv_heading(self, subfield=None):
|
|
nb_items = self.max_items or 1
|
|
label = self.label
|
|
if subfield:
|
|
headings = [f'{label} - {x}' for x in subfield.get_csv_heading()]
|
|
label += ' - %s' % subfield.label
|
|
else:
|
|
headings = [label]
|
|
if nb_items == 1:
|
|
return headings
|
|
base_headings = headings[:]
|
|
headings = []
|
|
for i in range(nb_items):
|
|
headings.extend([f'{x} - {i + 1}' for x in base_headings])
|
|
return headings
|
|
|
|
def get_csv_value(self, element, **kwargs):
|
|
nb_items = self.max_items or 1
|
|
cells = [''] * nb_items
|
|
if element and element.get('data'):
|
|
for i, subvalue in enumerate(element.get('data')[:nb_items]):
|
|
if subvalue:
|
|
cells[i] = self.block.get_display_value(subvalue)
|
|
return cells
|
|
|
|
def set_value(self, data, value, **kwargs):
|
|
if value == '':
|
|
value = None
|
|
if isinstance(value, BlockRowValue):
|
|
value = value.make_value(block=self.block, field=self, data=data)
|
|
elif value and not (isinstance(value, dict) and 'data' in value and 'schema' in value):
|
|
raise SetValueError(_('invalid value for block (field id: %s)') % self.id)
|
|
elif value:
|
|
value = copy.deepcopy(value)
|
|
super().set_value(data, value, **kwargs)
|
|
|
|
def get_json_value(self, value, **kwargs):
|
|
from wcs.formdata import FormData
|
|
|
|
result = []
|
|
if not value or not value.get('data'):
|
|
return result
|
|
for subvalue_data in value.get('data'):
|
|
result.append(
|
|
FormData.get_json_data_dict(
|
|
subvalue_data,
|
|
self.block.fields,
|
|
formdata=kwargs.get('formdata'),
|
|
include_files=kwargs.get('include_file_content'),
|
|
include_unnamed_fields=True,
|
|
)
|
|
)
|
|
return result
|
|
|
|
def from_json_value(self, value):
|
|
from wcs.api import posted_json_data_to_formdata_data
|
|
|
|
result = []
|
|
if isinstance(value, list):
|
|
for subvalue_data in value or []:
|
|
result.append(posted_json_data_to_formdata_data(self.block, subvalue_data))
|
|
|
|
return {'data': result, 'schema': {x.id: x.key for x in self.block.fields}}
|
|
|
|
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
|
node = ET.Element('{%s}span' % OD_NS['text'])
|
|
node.text = od_clean_text(force_str(value))
|
|
return node
|
|
|
|
def __getstate__(self):
|
|
# do not store _block cache
|
|
odict = super().__getstate__()
|
|
odict.pop('_block', None)
|
|
return odict
|
|
|
|
def __setstate__(self, ndict):
|
|
# make sure a cached copy of _block is not restored
|
|
self.__dict__ = ndict
|
|
self._block = None
|