2020-05-19 15:00:02 +02:00
|
|
|
# w.c.s. - web application for online forms
|
|
|
|
# Copyright (C) 2005-2020 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/>.
|
|
|
|
|
2023-05-04 15:14:25 +02:00
|
|
|
import collections
|
2021-11-14 19:30:43 +01:00
|
|
|
import itertools
|
2023-01-10 17:01:38 +01:00
|
|
|
import types
|
2020-05-19 15:00:02 +02:00
|
|
|
import uuid
|
2023-06-21 17:00:49 +02:00
|
|
|
import xml.etree.ElementTree as ET
|
2022-04-19 07:53:49 +02:00
|
|
|
from contextlib import contextmanager
|
2020-05-19 15:00:02 +02:00
|
|
|
|
2023-02-14 18:15:04 +01:00
|
|
|
from quixote import get_publisher, get_request, get_response
|
2020-10-23 09:40:28 +02:00
|
|
|
from quixote.html import htmltag, htmltext
|
2020-05-19 15:00:02 +02:00
|
|
|
|
2024-03-12 08:37:26 +01:00
|
|
|
from . import conditions, data_sources, fields
|
2021-12-13 15:14:34 +01:00
|
|
|
from .categories import BlockCategory
|
2023-01-10 17:01:38 +01:00
|
|
|
from .formdata import FormData
|
2021-05-15 15:34:16 +02:00
|
|
|
from .qommon import _, misc
|
2023-05-04 15:14:25 +02:00
|
|
|
from .qommon.errors import UnknownReferencedErrorMixin
|
2023-05-16 16:40:26 +02:00
|
|
|
from .qommon.form import CompositeWidget, SingleSelectHintWidget, WidgetList
|
2023-05-04 15:14:25 +02:00
|
|
|
from .qommon.storage import Equal, StorableObject
|
2022-02-25 15:49:26 +01:00
|
|
|
from .qommon.substitution import CompatibilityNamesDict
|
2020-05-19 15:00:02 +02:00
|
|
|
from .qommon.template import Template
|
2024-03-12 08:07:51 +01:00
|
|
|
from .qommon.xml_storage import PostConditionsXmlMixin
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BlockdefImportError(Exception):
|
2023-05-04 15:14:25 +02:00
|
|
|
def __init__(self, msg, msg_args=None, details=None):
|
2020-05-19 15:00:02 +02:00
|
|
|
self.msg = msg
|
2023-05-04 15:14:25 +02:00
|
|
|
self.msg_args = msg_args or ()
|
2020-05-19 15:00:02 +02:00
|
|
|
self.details = details
|
|
|
|
|
|
|
|
|
2023-05-04 15:14:25 +02:00
|
|
|
class BlockdefImportUnknownReferencedError(UnknownReferencedErrorMixin, BlockdefImportError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2024-03-12 08:07:51 +01:00
|
|
|
class BlockDef(StorableObject, PostConditionsXmlMixin):
|
2020-05-19 15:00:02 +02:00
|
|
|
_names = 'blockdefs'
|
|
|
|
_indexes = ['slug']
|
2022-01-09 15:30:27 +01:00
|
|
|
backoffice_class = 'wcs.admin.blocks.BlockDirectory'
|
2020-05-19 15:00:02 +02:00
|
|
|
xml_root_node = 'block'
|
2024-04-08 13:38:55 +02:00
|
|
|
verbose_name = _('Block of fields')
|
|
|
|
verbose_name_plural = _('Blocks of fields')
|
2022-04-19 07:53:49 +02:00
|
|
|
var_prefixes = ['block']
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
name = None
|
|
|
|
slug = None
|
|
|
|
fields = None
|
|
|
|
digest_template = None
|
2021-12-13 15:14:34 +01:00
|
|
|
category_id = None
|
2024-04-07 10:33:22 +02:00
|
|
|
documentation = None
|
2024-03-12 08:07:51 +01:00
|
|
|
post_conditions = None
|
2020-05-19 15:00:02 +02:00
|
|
|
|
2021-12-31 14:58:15 +01:00
|
|
|
SLUG_DASH = '_'
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
# declarations for serialization
|
2024-04-07 10:33:22 +02:00
|
|
|
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template', 'documentation']
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
def __init__(self, name=None, **kwargs):
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self.name = name
|
|
|
|
self.fields = []
|
|
|
|
|
2021-12-13 15:14:34 +01:00
|
|
|
@property
|
|
|
|
def category(self):
|
|
|
|
return BlockCategory.get(self.category_id, ignore_errors=True)
|
|
|
|
|
|
|
|
@category.setter
|
|
|
|
def category(self, category):
|
|
|
|
if category:
|
|
|
|
self.category_id = category.id
|
|
|
|
elif self.category_id:
|
|
|
|
self.category_id = None
|
|
|
|
|
2023-09-05 12:29:49 +02:00
|
|
|
def migrate(self):
|
|
|
|
changed = False
|
|
|
|
for f in self.fields or []:
|
|
|
|
changed |= f.migrate()
|
|
|
|
if changed:
|
|
|
|
self.store(comment=_('Automatic update'), snapshot_store_user=False)
|
|
|
|
|
|
|
|
def store(self, comment=None, snapshot_store_user=True, application=None, *args, **kwargs):
|
2021-11-14 19:30:43 +01:00
|
|
|
from wcs.carddef import CardDef
|
|
|
|
from wcs.formdef import FormDef
|
|
|
|
|
2020-08-15 17:00:39 +02:00
|
|
|
assert not self.is_readonly()
|
2020-05-19 15:00:02 +02:00
|
|
|
if self.slug is None:
|
|
|
|
# set slug if it's not yet there
|
|
|
|
self.slug = self.get_new_slug()
|
|
|
|
|
2021-07-01 11:32:31 +02:00
|
|
|
super().store(*args, **kwargs)
|
2020-08-10 13:10:04 +02:00
|
|
|
if get_publisher().snapshot_class:
|
2023-09-05 12:29:49 +02:00
|
|
|
get_publisher().snapshot_class.snap(
|
|
|
|
instance=self, comment=comment, store_user=snapshot_store_user, application=application
|
|
|
|
)
|
2020-05-19 15:00:02 +02:00
|
|
|
|
2021-11-14 19:30:43 +01:00
|
|
|
# update relations
|
|
|
|
for objdef in itertools.chain(
|
|
|
|
FormDef.select(ignore_errors=True, ignore_migration=True),
|
|
|
|
CardDef.select(ignore_errors=True, ignore_migration=True),
|
|
|
|
):
|
|
|
|
for field in objdef.get_all_fields():
|
2023-05-21 18:05:31 +02:00
|
|
|
if field.key == 'block' and field.block_slug == self.slug:
|
2021-11-14 19:30:43 +01:00
|
|
|
objdef.store()
|
2023-02-14 18:15:04 +01:00
|
|
|
|
2023-07-19 14:54:47 +02:00
|
|
|
if get_response():
|
2023-02-14 18:15:04 +01:00
|
|
|
from wcs.admin.tests import TestsAfterJob
|
|
|
|
|
|
|
|
context = _('in field block "%s"') % field.label
|
|
|
|
get_response().add_after_job(
|
|
|
|
TestsAfterJob(objdef, reason='%s (%s)' % (comment, context))
|
|
|
|
)
|
2021-11-14 19:30:43 +01:00
|
|
|
break
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def get_new_field_id(self):
|
|
|
|
return 'bf%s' % str(uuid.uuid4())
|
|
|
|
|
2020-08-11 15:15:47 +02:00
|
|
|
def get_admin_url(self):
|
|
|
|
base_url = get_publisher().get_backoffice_url()
|
|
|
|
return '%s/forms/blocks/%s/' % (base_url, self.id)
|
|
|
|
|
2022-03-28 21:23:25 +02:00
|
|
|
def get_field_admin_url(self, field):
|
|
|
|
return self.get_admin_url() + '%s/' % field.id
|
|
|
|
|
2023-12-04 21:19:53 +01:00
|
|
|
def get_widget_fields(self):
|
|
|
|
return [field for field in self.fields or [] if isinstance(field, fields.WidgetField)]
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def get_display_value(self, value):
|
|
|
|
if not self.digest_template:
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
from .variables import LazyBlockDataVar
|
|
|
|
|
2022-04-29 09:38:39 +02:00
|
|
|
context = CompatibilityNamesDict({'block_var': LazyBlockDataVar(self.fields, value)})
|
|
|
|
# for backward compatibility it is also possible to use <slug>_var_<whatever>
|
|
|
|
context[self.slug.replace('-', '_') + '_var'] = context['block_var']
|
2020-05-19 15:00:02 +02:00
|
|
|
return Template(self.digest_template, autoescape=False).render(context)
|
|
|
|
|
2022-02-25 15:49:26 +01:00
|
|
|
def get_substitution_counter_variables(self, index):
|
|
|
|
return CompatibilityNamesDict(
|
|
|
|
{
|
|
|
|
'block_counter': {
|
|
|
|
'index0': index,
|
|
|
|
'index': index + 1,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2021-12-31 14:59:14 +01:00
|
|
|
def get_dependencies(self):
|
|
|
|
yield self.category
|
|
|
|
for field in self.fields or []:
|
|
|
|
yield from field.get_dependencies()
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def export_to_xml(self, include_id=False):
|
|
|
|
root = ET.Element(self.xml_root_node)
|
|
|
|
if include_id and self.id:
|
|
|
|
root.attrib['id'] = str(self.id)
|
|
|
|
for text_attribute in list(self.TEXT_ATTRIBUTES):
|
|
|
|
if not hasattr(self, text_attribute) or not getattr(self, text_attribute):
|
|
|
|
continue
|
|
|
|
ET.SubElement(root, text_attribute).text = getattr(self, text_attribute)
|
|
|
|
|
2022-01-09 20:28:02 +01:00
|
|
|
BlockCategory.object_category_xml_export(self, root, include_id=include_id)
|
2021-12-13 15:14:34 +01:00
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
fields = ET.SubElement(root, 'fields')
|
|
|
|
for field in self.fields or []:
|
2024-01-13 16:08:33 +01:00
|
|
|
fields.append(field.export_to_xml(include_id=True))
|
2020-05-19 15:00:02 +02:00
|
|
|
|
2024-03-12 08:07:51 +01:00
|
|
|
self.post_conditions_export_to_xml(root, include_id=include_id)
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
return root
|
|
|
|
|
|
|
|
@classmethod
|
2024-04-05 16:03:32 +02:00
|
|
|
def import_from_xml(cls, fd, include_id=False, check_datasources=True, check_deprecated=False):
|
2020-05-19 15:00:02 +02:00
|
|
|
try:
|
|
|
|
tree = ET.parse(fd)
|
2021-03-12 16:24:45 +01:00
|
|
|
except Exception:
|
2020-05-19 15:00:02 +02:00
|
|
|
raise ValueError()
|
2024-03-04 17:02:17 +01:00
|
|
|
blockdef = cls.import_from_xml_tree(
|
|
|
|
tree,
|
|
|
|
include_id=include_id,
|
|
|
|
check_datasources=check_datasources,
|
|
|
|
check_deprecated=check_deprecated,
|
|
|
|
)
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
if blockdef.slug:
|
|
|
|
try:
|
|
|
|
cls.get_on_index(blockdef.slug, 'slug', ignore_migration=True)
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
blockdef.slug = blockdef.get_new_slug()
|
|
|
|
|
|
|
|
return blockdef
|
|
|
|
|
|
|
|
@classmethod
|
2024-03-04 17:02:17 +01:00
|
|
|
def import_from_xml_tree(
|
2024-04-05 16:03:32 +02:00
|
|
|
cls, tree, include_id=False, check_datasources=True, check_deprecated=False, **kwargs
|
2024-03-04 17:02:17 +01:00
|
|
|
):
|
2024-03-04 17:23:41 +01:00
|
|
|
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
2024-03-04 17:02:17 +01:00
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
blockdef = cls()
|
|
|
|
if tree.find('name') is None or not tree.find('name').text:
|
2021-05-15 15:34:16 +02:00
|
|
|
raise BlockdefImportError(_('Missing name'))
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
# if the tree we get is actually a ElementTree for real, we get its
|
|
|
|
# root element and go on happily.
|
|
|
|
if not ET.iselement(tree):
|
|
|
|
tree = tree.getroot()
|
|
|
|
|
|
|
|
if tree.tag != cls.xml_root_node:
|
2022-02-25 16:52:39 +01:00
|
|
|
raise BlockdefImportError(
|
|
|
|
_('Provided XML file is invalid, it starts with a <%(seen)s> tag instead of <%(expected)s>')
|
|
|
|
% {'seen': tree.tag, 'expected': cls.xml_root_node}
|
|
|
|
)
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
if include_id and tree.attrib.get('id'):
|
|
|
|
blockdef.id = tree.attrib.get('id')
|
|
|
|
for text_attribute in list(cls.TEXT_ATTRIBUTES):
|
|
|
|
value = tree.find(text_attribute)
|
|
|
|
if value is None or value.text is None:
|
|
|
|
continue
|
|
|
|
setattr(blockdef, text_attribute, misc.xml_node_text(value))
|
|
|
|
|
2023-09-24 18:51:31 +02:00
|
|
|
from wcs.fields.base import get_field_class_by_type
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
blockdef.fields = []
|
2023-05-04 15:14:25 +02:00
|
|
|
unknown_field_types = set()
|
2021-03-19 22:49:33 +01:00
|
|
|
for field in tree.find('fields'):
|
2020-05-19 15:00:02 +02:00
|
|
|
try:
|
2023-09-24 18:51:31 +02:00
|
|
|
field_o = get_field_class_by_type(field.findtext('type'))()
|
2020-05-19 15:00:02 +02:00
|
|
|
except KeyError:
|
2023-05-04 15:14:25 +02:00
|
|
|
field_type = field.findtext('type')
|
|
|
|
unknown_field_types.add(field_type)
|
|
|
|
continue
|
2024-01-13 16:08:33 +01:00
|
|
|
field_o.init_with_xml(field, include_id=True)
|
2020-05-19 15:00:02 +02:00
|
|
|
blockdef.fields.append(field_o)
|
|
|
|
|
2022-01-09 20:28:02 +01:00
|
|
|
BlockCategory.object_category_xml_import(blockdef, tree, include_id=include_id)
|
2021-12-13 15:14:34 +01:00
|
|
|
|
2024-03-12 08:07:51 +01:00
|
|
|
post_conditions_node = tree.find('post_conditions')
|
|
|
|
blockdef.post_conditions_init_with_xml(post_conditions_node, include_id=include_id)
|
|
|
|
|
2023-05-04 15:14:25 +02:00
|
|
|
unknown_datasources = set()
|
|
|
|
if check_datasources:
|
|
|
|
from wcs.carddef import CardDef
|
|
|
|
|
|
|
|
# check if datasources are defined
|
|
|
|
for field in blockdef.fields:
|
|
|
|
data_source = getattr(field, 'data_source', None)
|
|
|
|
if data_source:
|
|
|
|
data_source_id = data_source.get('type')
|
|
|
|
if isinstance(data_sources.get_object(data_source), data_sources.StubNamedDataSource):
|
|
|
|
unknown_datasources.add(data_source_id)
|
|
|
|
elif data_source_id and data_source_id.startswith('carddef:'):
|
|
|
|
parts = data_source_id.split(':')
|
|
|
|
# check if carddef exists
|
|
|
|
url_name = parts[1]
|
|
|
|
try:
|
|
|
|
CardDef.get_by_urlname(url_name)
|
|
|
|
except KeyError:
|
|
|
|
unknown_datasources.add(data_source_id)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if len(parts) == 2 or parts[2] == '_with_user_filter':
|
|
|
|
continue
|
|
|
|
|
|
|
|
lookup_criterias = [
|
|
|
|
Equal('formdef_type', 'carddef'),
|
|
|
|
Equal('visibility', 'datasource'),
|
|
|
|
Equal('slug', parts[2]),
|
|
|
|
]
|
|
|
|
try:
|
|
|
|
get_publisher().custom_view_class.select(lookup_criterias)[0]
|
|
|
|
except IndexError:
|
|
|
|
unknown_datasources.add(data_source_id)
|
|
|
|
|
|
|
|
if unknown_field_types or unknown_datasources:
|
|
|
|
details = collections.defaultdict(set)
|
|
|
|
if unknown_field_types:
|
|
|
|
details[_('Unknown field types')].update(unknown_field_types)
|
|
|
|
if unknown_datasources:
|
|
|
|
details[_('Unknown datasources')].update(unknown_datasources)
|
|
|
|
raise BlockdefImportUnknownReferencedError(_('Unknown referenced objects'), details=details)
|
|
|
|
|
2024-03-04 17:02:17 +01:00
|
|
|
if check_deprecated:
|
|
|
|
# check for deprecated elements
|
2024-03-04 17:23:41 +01:00
|
|
|
job = DeprecationsScan()
|
2024-03-04 17:02:17 +01:00
|
|
|
try:
|
|
|
|
job.check_deprecated_elements_in_object(blockdef)
|
|
|
|
except DeprecatedElementsDetected as e:
|
|
|
|
raise BlockdefImportError(str(e))
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
return blockdef
|
|
|
|
|
2023-10-08 10:20:52 +02:00
|
|
|
def get_usage_fields(self):
|
|
|
|
from wcs.formdef import get_formdefs_of_all_kinds
|
|
|
|
|
|
|
|
for formdef in get_formdefs_of_all_kinds():
|
|
|
|
for field in formdef.fields:
|
|
|
|
if field.key == 'block' and field.block_slug == self.slug:
|
|
|
|
field.formdef = formdef
|
|
|
|
yield field
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def get_usage_formdefs(self):
|
|
|
|
from wcs.formdef import get_formdefs_of_all_kinds
|
|
|
|
|
|
|
|
for formdef in get_formdefs_of_all_kinds():
|
|
|
|
for field in formdef.fields:
|
2023-05-21 18:05:31 +02:00
|
|
|
if field.key == 'block' and field.block_slug == self.slug:
|
2020-05-19 15:00:02 +02:00
|
|
|
yield formdef
|
|
|
|
break
|
|
|
|
|
|
|
|
def is_used(self):
|
|
|
|
return any(self.get_usage_formdefs())
|
|
|
|
|
2022-04-19 07:53:49 +02:00
|
|
|
@contextmanager
|
|
|
|
def visibility_context(self, value, row_index):
|
|
|
|
from .variables import LazyBlockDataVar
|
|
|
|
|
|
|
|
context = self.get_substitution_counter_variables(row_index)
|
|
|
|
context['block_var'] = LazyBlockDataVar(self.fields, value)
|
|
|
|
with get_publisher().substitutions.temporary_feed(context):
|
|
|
|
yield
|
|
|
|
|
2022-10-22 10:51:41 +02:00
|
|
|
def i18n_scan(self):
|
|
|
|
location = 'forms/blocks/%s/' % self.id
|
|
|
|
for field in self.fields or []:
|
|
|
|
yield from field.i18n_scan(base_location=location)
|
2024-03-12 08:07:51 +01:00
|
|
|
for post_condition in self.post_conditions or []:
|
|
|
|
yield location, None, post_condition.get('error_message')
|
2022-10-22 10:51:41 +02:00
|
|
|
|
2023-01-10 17:01:38 +01:00
|
|
|
def get_all_fields(self):
|
|
|
|
return self.fields
|
|
|
|
|
|
|
|
def data_class(self):
|
|
|
|
return types.ClassType('fake_formdata', (FormData,), {'_formdef': self})
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
class BlockSubWidget(CompositeWidget):
|
2021-02-01 19:37:42 +01:00
|
|
|
template_name = 'qommon/forms/widgets/block_sub.html'
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def __init__(self, name, value=None, *args, **kwargs):
|
|
|
|
self.block = kwargs.pop('block')
|
|
|
|
self.readonly = kwargs.get('readonly')
|
2021-02-01 19:37:42 +01:00
|
|
|
self.remove_button = kwargs.pop('remove_button', False)
|
2022-02-25 15:49:26 +01:00
|
|
|
self.index = kwargs.pop('index', 0)
|
2020-05-19 15:00:02 +02:00
|
|
|
super().__init__(name, value, *args, **kwargs)
|
2022-02-25 15:49:26 +01:00
|
|
|
|
|
|
|
def add_to_form(field):
|
2020-05-19 15:00:02 +02:00
|
|
|
if 'readonly' in kwargs:
|
2020-07-12 13:58:01 +02:00
|
|
|
field_value = None
|
|
|
|
if value is not None:
|
|
|
|
field_value = value.get(field.id)
|
2022-04-19 07:53:49 +02:00
|
|
|
return field.add_to_view_form(form=self, value=field_value)
|
2020-05-19 15:00:02 +02:00
|
|
|
else:
|
2023-04-20 18:16:22 +02:00
|
|
|
widget = field.add_to_form(form=self)
|
2023-05-21 18:05:31 +02:00
|
|
|
if field.key in ['title', 'subtitle', 'comment']:
|
2023-04-20 18:16:22 +02:00
|
|
|
return widget
|
2020-07-20 10:17:35 +02:00
|
|
|
widget = self.get_widget('f%s' % field.id)
|
|
|
|
if widget:
|
2022-03-22 07:30:20 +01:00
|
|
|
widget.div_id = None
|
2020-07-20 10:17:35 +02:00
|
|
|
widget.prefill_attributes = field.get_prefill_attributes()
|
2022-04-19 07:53:49 +02:00
|
|
|
return widget
|
2022-02-25 15:49:26 +01:00
|
|
|
|
2022-04-19 07:53:49 +02:00
|
|
|
self.fields = {}
|
|
|
|
|
|
|
|
live_sources = []
|
2022-05-24 09:12:46 +02:00
|
|
|
live_condition_fields = {}
|
2022-02-25 15:49:26 +01:00
|
|
|
for field in self.block.fields:
|
|
|
|
context = self.block.get_substitution_counter_variables(self.index)
|
2023-05-21 18:05:31 +02:00
|
|
|
if field.key in ['title', 'subtitle', 'comment']:
|
2022-02-25 15:49:26 +01:00
|
|
|
with get_publisher().substitutions.temporary_feed(context):
|
2022-04-19 07:53:49 +02:00
|
|
|
widget = add_to_form(field)
|
2022-02-25 15:49:26 +01:00
|
|
|
else:
|
2022-04-19 07:53:49 +02:00
|
|
|
widget = add_to_form(field)
|
|
|
|
|
|
|
|
if field.condition:
|
2022-05-24 09:12:46 +02:00
|
|
|
varnames = field.get_condition_varnames(formdef=self.block)
|
|
|
|
live_sources.extend(varnames)
|
|
|
|
for varname in varnames:
|
|
|
|
if varname not in live_condition_fields:
|
|
|
|
live_condition_fields[varname] = []
|
|
|
|
live_condition_fields[varname].append(field)
|
2022-04-19 07:53:49 +02:00
|
|
|
field.widget = widget
|
|
|
|
self.fields[field.id] = widget
|
|
|
|
|
|
|
|
for field in self.block.fields:
|
|
|
|
if field.varname in live_sources:
|
|
|
|
field.widget.live_condition_source = True
|
2022-05-24 09:12:46 +02:00
|
|
|
field.widget.live_condition_fields = live_condition_fields[field.varname]
|
2022-04-19 07:53:49 +02:00
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
if value:
|
|
|
|
self.set_value(value)
|
|
|
|
|
2022-04-19 07:53:49 +02:00
|
|
|
self.set_visibility(value)
|
|
|
|
|
|
|
|
def set_visibility(self, value):
|
|
|
|
with self.block.visibility_context(value, self.index):
|
|
|
|
for field in self.block.fields:
|
|
|
|
widget = self.fields.get(field.id)
|
|
|
|
if not widget:
|
|
|
|
continue
|
|
|
|
visible = field.is_visible({}, formdef=None)
|
|
|
|
widget.is_hidden = not (visible)
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def set_value(self, value):
|
2020-12-20 11:14:52 +01:00
|
|
|
self.value = value
|
2020-05-19 15:00:02 +02:00
|
|
|
for widget in self.get_widgets():
|
2020-07-12 13:58:01 +02:00
|
|
|
if hasattr(widget, 'set_value') and not getattr(widget, 'secondary', False):
|
2020-07-03 15:59:12 +02:00
|
|
|
widget.set_value(value.get(widget.field.id))
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
def get_field_data(self, field, widget):
|
|
|
|
from wcs.formdef import FormDef
|
|
|
|
|
|
|
|
return FormDef.get_field_data(field, widget)
|
|
|
|
|
|
|
|
def _parse(self, request):
|
|
|
|
value = {}
|
|
|
|
empty = True
|
2023-05-16 16:40:26 +02:00
|
|
|
all_lists = True
|
2022-04-19 07:53:49 +02:00
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
for widget in self.get_widgets():
|
2023-05-16 16:40:26 +02:00
|
|
|
if widget.field.key in ['title', 'subtitle', 'comment']:
|
|
|
|
continue
|
2020-05-19 15:00:02 +02:00
|
|
|
widget_value = self.get_field_data(widget.field, widget)
|
2022-04-19 07:53:49 +02:00
|
|
|
with self.block.visibility_context(value, self.index):
|
|
|
|
if not widget.field.is_visible({}, formdef=None):
|
|
|
|
widget.clear_error()
|
|
|
|
continue
|
2020-05-19 15:00:02 +02:00
|
|
|
value.update(widget_value)
|
2023-05-16 16:40:26 +02:00
|
|
|
empty_values = [None]
|
|
|
|
if (
|
|
|
|
widget.field.key == 'item'
|
|
|
|
and isinstance(widget, SingleSelectHintWidget)
|
|
|
|
and widget.separate_hint()
|
|
|
|
):
|
|
|
|
# <select> will have its first option automatically selected,
|
|
|
|
# do not consider it to mark the field as filled.
|
|
|
|
empty_values.append(widget.options[0][0])
|
|
|
|
else:
|
|
|
|
all_lists = False
|
|
|
|
if widget_value.get(widget.field.id) not in empty_values:
|
2020-05-19 15:00:02 +02:00
|
|
|
empty = False
|
2024-03-12 08:37:26 +01:00
|
|
|
|
|
|
|
if not empty and self.block.post_conditions:
|
|
|
|
error_messages = []
|
|
|
|
with self.block.visibility_context(value, self.index):
|
|
|
|
for i, post_condition in enumerate(self.block.post_conditions):
|
|
|
|
condition = post_condition.get('condition')
|
|
|
|
try:
|
|
|
|
if conditions.Condition(condition, record_errors=False).evaluate():
|
|
|
|
continue
|
|
|
|
except RuntimeError:
|
|
|
|
pass
|
|
|
|
error_message = post_condition.get('error_message')
|
|
|
|
error_messages.append(get_publisher().translate(error_message))
|
|
|
|
if error_messages:
|
|
|
|
self.set_error(' '.join(error_messages))
|
|
|
|
|
2023-09-29 19:43:39 +02:00
|
|
|
if empty and not all_lists and not get_publisher().keep_all_block_rows_mode:
|
2020-05-19 15:00:02 +02:00
|
|
|
value = None
|
2024-01-04 16:24:42 +01:00
|
|
|
for widget in self.get_widgets(): # reset "required" errors
|
|
|
|
if widget.error == self.REQUIRED_ERROR:
|
|
|
|
widget.clear_error()
|
2020-05-19 15:00:02 +02:00
|
|
|
self.value = value
|
|
|
|
|
|
|
|
def add_media(self):
|
|
|
|
for widget in self.get_widgets():
|
2020-07-03 15:54:58 +02:00
|
|
|
if hasattr(widget, 'add_media'):
|
|
|
|
widget.add_media()
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BlockWidget(WidgetList):
|
2021-02-01 19:37:42 +01:00
|
|
|
template_name = 'qommon/forms/widgets/block.html'
|
|
|
|
always_include_add_button = True
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def __init__(
|
2021-11-12 10:06:16 +01:00
|
|
|
self,
|
|
|
|
name,
|
|
|
|
value=None,
|
|
|
|
title=None,
|
|
|
|
block=None,
|
|
|
|
default_items_count=None,
|
|
|
|
max_items=None,
|
|
|
|
add_element_label=None,
|
|
|
|
**kwargs,
|
2020-05-19 15:00:02 +02:00
|
|
|
):
|
|
|
|
self.block = block
|
|
|
|
self.readonly = kwargs.get('readonly')
|
2020-10-23 09:40:28 +02:00
|
|
|
self.label_display = kwargs.pop('label_display') or 'normal'
|
2021-02-01 19:37:42 +01:00
|
|
|
self.remove_button = kwargs.pop('remove_button', False)
|
2020-05-19 15:00:02 +02:00
|
|
|
element_values = None
|
|
|
|
if value:
|
|
|
|
element_values = value.get('data')
|
2021-11-12 10:06:16 +01:00
|
|
|
max_items = max_items or 1
|
|
|
|
default_items_count = min(default_items_count or 1, max_items)
|
2020-08-03 12:49:51 +02:00
|
|
|
hint = kwargs.pop('hint', None)
|
2021-02-01 19:37:42 +01:00
|
|
|
element_kwargs = {'block': self.block, 'render_br': False, 'remove_button': self.remove_button}
|
2020-05-19 15:00:02 +02:00
|
|
|
element_kwargs.update(kwargs)
|
|
|
|
super().__init__(
|
|
|
|
name,
|
|
|
|
value=element_values,
|
|
|
|
title=title,
|
2021-11-12 10:06:16 +01:00
|
|
|
default_items_count=default_items_count,
|
2020-05-19 15:00:02 +02:00
|
|
|
max_items=max_items,
|
|
|
|
element_type=BlockSubWidget,
|
|
|
|
element_kwargs=element_kwargs,
|
|
|
|
add_element_label=add_element_label or _('Add another'),
|
2020-08-03 12:49:51 +02:00
|
|
|
hint=hint,
|
2020-05-19 15:00:02 +02:00
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
2024-03-25 12:58:41 +01:00
|
|
|
@property
|
|
|
|
def a11y_labelledby(self):
|
|
|
|
return bool(self.a11y_role)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def a11y_role(self):
|
|
|
|
# don't mark block as a group if it has no label
|
|
|
|
if self.label_display != 'hidden':
|
|
|
|
return 'group'
|
|
|
|
return None
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def set_value(self, value):
|
2023-09-24 18:51:31 +02:00
|
|
|
from .fields.block import BlockRowValue
|
|
|
|
|
|
|
|
if isinstance(value, BlockRowValue):
|
2023-01-14 13:39:19 +01:00
|
|
|
value = value.make_value(block=self.block, field=self.field, data={})
|
2023-02-14 14:38:29 +01:00
|
|
|
if isinstance(value, dict) and 'data' in value:
|
|
|
|
super().set_value(value['data'])
|
|
|
|
self.value = value
|
|
|
|
else:
|
|
|
|
self.value = None
|
2020-05-19 15:00:02 +02:00
|
|
|
|
|
|
|
def _parse(self, request):
|
|
|
|
# iterate over existing form keys to get actual list of elements.
|
|
|
|
# (maybe this could be moved to WidgetList)
|
|
|
|
prefix = '%s$element' % self.name
|
|
|
|
known_prefixes = {x.split('$', 2)[1] for x in request.form.keys() if x.startswith(prefix)}
|
2021-02-01 19:37:42 +01:00
|
|
|
for prefix in known_prefixes:
|
|
|
|
if prefix not in self.element_names:
|
|
|
|
self.add_element(element_name=prefix)
|
2020-05-19 15:00:02 +02:00
|
|
|
super()._parse(request)
|
|
|
|
if self.value:
|
|
|
|
self.value = {'data': self.value}
|
|
|
|
# keep "schema" next to data, this allows custom behaviour for
|
|
|
|
# date fields (time.struct_time) when writing/reading from
|
|
|
|
# database in JSON.
|
|
|
|
self.value['schema'] = {x.id: x.key for x in self.block.fields}
|
|
|
|
|
2021-01-19 10:20:50 +01:00
|
|
|
def unparse(self):
|
|
|
|
self._parsed = False
|
|
|
|
for widget in self.widgets:
|
|
|
|
widget._parsed = False
|
|
|
|
|
2020-05-19 15:00:02 +02:00
|
|
|
def parse(self, request=None):
|
|
|
|
if not self._parsed:
|
|
|
|
self._parsed = True
|
|
|
|
if request is None:
|
|
|
|
request = get_request()
|
|
|
|
self._parse(request)
|
|
|
|
if self.required and self.value is None:
|
|
|
|
self.set_error(_(self.REQUIRED_ERROR))
|
2024-03-12 08:37:26 +01:00
|
|
|
for widget in self.widgets:
|
|
|
|
# mark required rows with a special attribute, to avoid doubling the
|
|
|
|
# error messages in the template.
|
|
|
|
widget.is_required_error = bool(widget.error == self.REQUIRED_ERROR)
|
2020-05-19 15:00:02 +02:00
|
|
|
return self.value
|
|
|
|
|
|
|
|
def add_media(self):
|
|
|
|
for widget in self.get_widgets():
|
|
|
|
if hasattr(widget, 'add_media'):
|
|
|
|
widget.add_media()
|
|
|
|
|
|
|
|
def get_error(self, request=None):
|
|
|
|
request = request or get_request()
|
|
|
|
if request.get_method() == 'POST':
|
|
|
|
self.parse(request=request)
|
|
|
|
return self.error
|
|
|
|
|
|
|
|
def has_error(self, request=None):
|
|
|
|
if self.get_error():
|
|
|
|
return True
|
|
|
|
# we know subwidgets have been parsed
|
|
|
|
has_error = False
|
|
|
|
for widget in self.widgets:
|
|
|
|
if widget.value is None:
|
|
|
|
continue
|
|
|
|
if widget.has_error():
|
|
|
|
has_error = True
|
|
|
|
return has_error
|
2020-10-23 09:40:28 +02:00
|
|
|
|
|
|
|
def render_title(self, title):
|
2023-07-09 08:09:36 +02:00
|
|
|
attrs = {'id': 'form_label_%s' % self.get_name_for_id()}
|
2020-10-23 09:40:28 +02:00
|
|
|
if not title or self.label_display == 'hidden':
|
2024-03-25 12:58:41 +01:00
|
|
|
# add a tag even if there's no label to display as it's used as an anchor point
|
|
|
|
# for links to errors.
|
|
|
|
return htmltag('div', **attrs) + htmltext('</div>')
|
2020-10-23 09:40:28 +02:00
|
|
|
|
|
|
|
if self.label_display == 'normal':
|
|
|
|
return super().render_title(title)
|
|
|
|
|
|
|
|
if self.required:
|
|
|
|
title += htmltext('<span title="%s" class="required">*</span>') % _('This field is required.')
|
|
|
|
hint = self.get_hint()
|
|
|
|
if hint:
|
|
|
|
attrs['aria-describedby'] = 'form_hint_%s' % self.name
|
|
|
|
title_tag = htmltag('h4', **attrs)
|
|
|
|
return title_tag + htmltext('%s</h4>') % title
|
2021-02-02 13:50:32 +01:00
|
|
|
|
|
|
|
def had_add_clicked(self):
|
|
|
|
add_widget = self.get_widget('add_element')
|
2021-02-16 14:06:26 +01:00
|
|
|
request = get_request()
|
|
|
|
request_form = getattr(request, 'orig_form', request.form)
|
|
|
|
return request_form.get(add_widget.name) if add_widget else False
|