wcs/wcs/fields/block.py

354 lines
14 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 = copy.copy(self.__dict__)
if '_block' in odict:
del odict['_block']
return odict
def __setstate__(self, ndict):
# make sure a cached copy of _block is not restored
self.__dict__ = ndict
self._block = None