wcs/wcs/fields/page.py

255 lines
9.8 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.conditions import Condition
from wcs.qommon import _
from wcs.qommon.form import CompositeWidget, ConditionWidget, StringWidget, VarnameWidget, WidgetListAsTable
from wcs.qommon.misc import get_dependencies_from_template, xml_node_text
from .base import Field, register_field_class
class PostConditionsRowWidget(CompositeWidget):
def __init__(self, name, value=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
if not value:
value = {}
self.add(
ConditionWidget, name='condition', title=_('Condition'), value=value.get('condition'), size=50
)
self.add(
StringWidget,
name='error_message',
title=_('Error message if condition is not met'),
value=value.get('error_message'),
size=50,
)
def _parse(self, request):
if self.get('condition') or self.get('error_message'):
self.value = {'condition': self.get('condition'), 'error_message': self.get('error_message')}
else:
self.value = None
class PostConditionsTableWidget(WidgetListAsTable):
readonly = False
def __init__(self, name, **kwargs):
super().__init__(name, element_type=PostConditionsRowWidget, **kwargs)
def parse(self, request=None):
super().parse(request=request)
for post_condition in self.value or []:
if not (post_condition.get('error_message') and post_condition.get('condition')):
self.set_error(_('Both condition and error message are required.'))
break
return self.value
class PageCondition(Condition):
record_errors = False
def get_data(self):
dict_vars = self.context['dict_vars']
formdef = self.context['formdef']
# create variables with values currently being evaluated, not yet
# available in the formdata.
from wcs.formdata import get_dict_with_varnames
live_data = {}
form_live_data = {}
if dict_vars is not None and formdef:
live_data = get_dict_with_varnames(formdef.fields, dict_vars)
form_live_data = {'form_' + x: y for x, y in live_data.items()}
# 1) feed the form_var_* variables in the global substitution system,
# they will shadow formdata context variables with their new "live"
# value, this may be useful when evaluating data sources.
class ConditionVars:
def __init__(self, id_dict_var):
# keep track of reference dictionary
self.id_dict_var = id_dict_var
def get_substitution_variables(self):
return {}
def get_static_substitution_variables(self):
# only for backward compatibility with python evaluations
return form_live_data
def __eq__(self, other):
# Assume all ConditionVars are equal when initialized with
# the same live data dictionary; this avoids filling
# the substitution sources with duplicates and invalidating its
# cache.
return self.id_dict_var == getattr(other, 'id_dict_var', None)
if dict_vars is not None:
# Add them only if there is a real dict_vars in context,
# ie do nothing on first page condition
get_publisher().substitutions.feed(ConditionVars(id(dict_vars)))
# alter top-of-stack formdata with data from submitted form
from wcs.formdata import FormData
for source in reversed(get_publisher().substitutions.sources):
if isinstance(source, FormData):
source.data.update(dict_vars)
break
data = super().get_data()
# 2) add live data as var_ variables for local evaluation only, for
# backward compatibility. They are not added globally as they would
# interfere with the var_ prefixed variables used in dynamic jsonp
# fields. (#9786)
data = copy.copy(data)
data.update(live_data)
if dict_vars is None:
# ConditionsVars is not set when evaluating first page condition,
# but we need to have form_var_* variables already; add them from
# form_live_data (where all variables will have been set to None).
data.update(form_live_data)
return data
class PageField(Field):
key = 'page'
description = _('Page')
post_conditions = None
def post_conditions_init_with_xml(self, node, charset, include_id=False, snapshot=False):
if node is None:
return
self.post_conditions = []
for post_condition_node in node.findall('post_condition'):
if post_condition_node.findall('condition/type'):
condition = {
'type': xml_node_text(post_condition_node.find('condition/type')),
'value': xml_node_text(post_condition_node.find('condition/value')),
}
elif post_condition_node.find('condition').text:
condition = {
'type': 'python',
'value': xml_node_text(post_condition_node.find('condition')),
}
else:
continue
self.post_conditions.append(
{
'condition': condition,
'error_message': xml_node_text(post_condition_node.find('error_message')),
}
)
def post_conditions_export_to_xml(self, node, charset, include_id=False):
if not self.post_conditions:
return
conditions_node = ET.SubElement(node, 'post_conditions')
for post_condition in self.post_conditions:
post_condition_node = ET.SubElement(conditions_node, 'post_condition')
condition_node = ET.SubElement(post_condition_node, 'condition')
ET.SubElement(condition_node, 'type').text = force_str(
(post_condition['condition'] or {}).get('type') or '', charset, errors='replace'
)
ET.SubElement(condition_node, 'value').text = force_str(
(post_condition['condition'] or {}).get('value') or '', charset, errors='replace'
)
ET.SubElement(post_condition_node, 'error_message').text = force_str(
post_condition['error_message'] or '', charset, errors='replace'
)
def fill_admin_form(self, form):
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
form.add(
ConditionWidget,
'condition',
title=_('Display Condition'),
value=self.condition,
required=False,
size=50,
)
form.add(
PostConditionsTableWidget,
'post_conditions',
title=_('Post Conditions'),
value=self.post_conditions,
advanced=True,
)
form.add(
VarnameWidget,
'varname',
title=_('Identifier'),
value=self.varname,
size=30,
advanced=True,
hint=_('This is used as reference in workflow edition action.'),
)
def get_admin_attributes(self):
return Field.get_admin_attributes(self) + ['post_conditions', 'varname']
def add_to_view_form(self, *args, **kwargs):
pass
def get_conditions(self):
if self.condition:
yield self.condition
for post_condition in self.post_conditions or []:
yield post_condition.get('condition')
def get_post_conditions_parameter_view_value(self, widget):
if not self.post_conditions:
return
r = TemplateIO(html=True)
r += htmltext('<ul>')
for post_condition in self.post_conditions:
r += htmltext('<li>%s <span class="condition-type">(%s)</span> - %s</li>') % (
post_condition.get('condition').get('value'),
{'django': 'Django', 'python': 'Python'}.get(post_condition.get('condition').get('type')),
post_condition.get('error_message'),
)
r += htmltext('</ul>')
return r.getvalue()
def get_dependencies(self):
yield from super().get_dependencies()
if getattr(self, 'post_conditions', None):
post_conditions = self.post_conditions or []
for post_condition in post_conditions:
condition = post_condition.get('condition') or {}
if condition.get('type') == 'django':
yield from get_dependencies_from_template(condition.get('value'))
def i18n_scan(self, base_location):
location = '%s%s/' % (base_location, self.id)
yield location, None, self.label
for post_condition in self.post_conditions or []:
yield location, None, post_condition.get('error_message')
register_field_class(PageField)