diff --git a/corehq/apps/app_manager/models.py b/corehq/apps/app_manager/models.py
index 408bd3447f37..4f9354488536 100644
--- a/corehq/apps/app_manager/models.py
+++ b/corehq/apps/app_manager/models.py
@@ -546,6 +546,21 @@ def _apply_updates_to_mappings(current_mappings, updates):
if all_missing_mappings:
raise MissingPropertyMapException(*all_missing_mappings)
+ def get_mappings(self):
+ mappings = {}
+ self.make_multi()
+
+ for (case_property, updates) in self.update_multi.items():
+ mappings[case_property] = [self.update_to_json(update) for update in updates]
+
+ return mappings
+
+ def update_to_json(self, update):
+ json = update.to_json()
+ if 'doc_type' in json:
+ del json['doc_type']
+ return json
+
class PreloadAction(FormAction):
@@ -655,6 +670,34 @@ def has_name_update(self):
def _update_has_name(self, update):
return bool(update.question_path)
+ def get_assigned_names(self):
+ questions = []
+ if self.name_update_multi:
+ questions.extend(self.name_update_multi)
+
+ if self.name_update:
+ questions.append(self.name_update)
+
+ return [question.question_path for question in questions if question.question_path]
+
+ def assign_name_update(self, question_path):
+ self.name_update = ConditionalCaseUpdate(question_path=question_path)
+ self.name_update_multi = []
+
+ def get_mappings(self):
+ mappings = {}
+ self.make_multi()
+
+ mappings['name'] = [self.update_to_json(update) for update in self.name_update_multi]
+
+ return mappings
+
+ def update_to_json(self, update):
+ json = update.to_json()
+ if 'doc_type' in json:
+ del json['doc_type']
+ return json
+
class OpenSubCaseAction(FormAction, IndexedSchema):
@@ -697,6 +740,33 @@ class FormActionsDiff(DocumentSchema):
open_case = SchemaProperty(OpenCaseDiff)
update_case = SchemaProperty(UpdateCaseDiff)
+ @classmethod
+ def parse_universal_diff(cls, universal_diff_json, is_registration=False):
+ open_diff = OpenCaseDiff()
+ update_diff = UpdateCaseDiff(universal_diff_json)
+
+ if is_registration:
+ if 'name' in update_diff.add:
+ name_additions = update_diff.add['name']
+ open_diff.add = name_additions
+ del update_diff.add['name']
+
+ if 'name' in update_diff.update:
+ name_updates = update_diff.update['name']
+ open_diff.update = name_updates
+ del update_diff.update['name']
+
+ if 'name' in update_diff.delete:
+ name_deletions = update_diff.delete['name']
+ open_diff.delete = name_deletions
+ del update_diff.delete['name']
+
+ diff = FormActionsDiff()
+ diff.open_case = open_diff
+ diff.update_case = update_diff
+
+ return diff
+
class FormActions(UpdateableDocument):
open_case = SchemaProperty(OpenCaseAction)
@@ -767,6 +837,17 @@ def make_single(self, allow_conflicts=True):
else:
self.update_case.make_single()
+ def get_mappings(self):
+ mappings = {}
+
+ if self.open_case:
+ mappings.update(self.open_case.get_mappings())
+
+ if self.update_case:
+ mappings.update(self.update_case.get_mappings())
+
+ return mappings
+
class CaseIndex(DocumentSchema):
tag = StringProperty()
@@ -1726,6 +1807,9 @@ def get_case_updates_for_case_type(self, case_type):
"""
return self.get_case_updates().get(case_type, [])
+ def get_source_with_mappings(self):
+ return self.source
+
class JRResourceProperty(StringProperty):
@@ -2113,6 +2197,14 @@ def get_contributed_case_relationships(self):
(parent_case_type, subcase.reference_id or 'parent'))
return case_relationships_by_child_type
+ def get_source_with_mappings(self):
+ if not self._parent.case_type:
+ return self.source
+
+ xform = XForm(self.source)
+ xform.add_case_mappings(self)
+ return xform.render_pretty().decode('utf-8')
+
class GraphAnnotations(IndexedSchema):
display_text = DictProperty()
diff --git a/corehq/apps/app_manager/tests/test_models.py b/corehq/apps/app_manager/tests/test_models.py
index 6b4572d8a3ec..19dafb481233 100644
--- a/corehq/apps/app_manager/tests/test_models.py
+++ b/corehq/apps/app_manager/tests/test_models.py
@@ -160,6 +160,48 @@ def test_has_name_update_requires_name_update_multi_to_have_a_path(self):
self.assertFalse(action.has_name_update())
+ def test_get_assigned_names_spans_update_and_update_multi(self):
+ action = OpenCaseAction({
+ 'name_update': {'question_path': 'one'},
+ 'name_update_multi': [{'question_path': 'two'}]
+ })
+
+ self.assertEqual(set(action.get_assigned_names()), {'one', 'two'})
+
+ def test_get_assigned_names_ignores_empty_values(self):
+ action = OpenCaseAction({
+ 'name_update': {'question_path': None}
+ })
+
+ self.assertEqual(set(action.get_assigned_names()), set())
+
+ def test_assign_name_update_sets_name_update(self):
+ action = OpenCaseAction()
+ action.assign_name_update('name_question')
+
+ self.assertEqual(action.name_update.question_path, 'name_question')
+
+ def test_assign_name_update_removes_update_multi(self):
+ action = OpenCaseAction({
+ 'name_update_multi': [{'question_path': 'one'}, {'question_path': 'two'}]
+ })
+ action.assign_name_update('three')
+
+ self.assertEqual(action.name_update_multi, [])
+
+ def test_get_mappings_serializes_name_updates(self):
+ action = OpenCaseAction({
+ 'name_update_multi': [{'question_path': 'one'}, {'question_path': 'two'}]
+ })
+
+ json = action.get_mappings()
+ self.assertEqual(json, {
+ 'name': [
+ {'question_path': 'one', 'update_mode': 'always'},
+ {'question_path': 'two', 'update_mode': 'always'}
+ ]
+ })
+
class OpenCaseAction_ApplyUpdates_Tests(SimpleTestCase):
def test_no_changes(self):
@@ -391,6 +433,37 @@ def test_get_property_names_returns_keys_from_update_multi(self):
self.assertEqual(action.get_property_names(), {'one'})
+ def test_get_mappings_serializes_updates(self):
+ action = UpdateCaseAction({
+ 'update_multi': {
+ 'one': [{'question_path': '/A/'}, {'question_path': '/B/'}],
+ 'two': [{'question_path': '/C/'}],
+ }
+ })
+
+ json = action.get_mappings()
+
+ self.assertEqual(json, {
+ 'one': [
+ {'question_path': '/A/', 'update_mode': 'always'},
+ {'question_path': '/B/', 'update_mode': 'always'}
+ ],
+ 'two': [{'question_path': '/C/', 'update_mode': 'always'}]
+ })
+
+ def test_get_mappings_removes_doc_type(self):
+ action = UpdateCaseAction({
+ 'update': {
+ 'one': {'question_path': '/A/', 'update_mode': 'edit', 'doc_type': 'TestDoc'},
+ }
+ })
+
+ json = action.get_mappings()
+
+ self.assertEqual(json, {
+ 'one': [{'question_path': '/A/', 'update_mode': 'edit'}]
+ })
+
class UpdateCaseAction_ApplyUpdates_Tests(SimpleTestCase):
def test_no_changes(self):
@@ -759,6 +832,54 @@ def test_json_construction(self):
assert diff.open_case.add[0].question_path == 'one'
assert diff.update_case.delete['case_two'][0].question_path == 'two'
+ def test_parse_universal_diff_creates_diff_object(self):
+ universal_json = {
+ 'add': {
+ 'prop1': [{'question_path': 'one'}]
+ },
+ 'update': {
+ 'prop2': [{'question_path': 'two'}]
+ },
+ 'delete': {
+ 'prop3': [{'question_path': 'three'}]
+ }
+ }
+
+ diff = FormActionsDiff.parse_universal_diff(universal_json)
+
+ assert len(diff.update_case.add['prop1']) == 1
+ assert diff.update_case.add['prop1'][0].question_path == 'one'
+
+ assert len(diff.update_case.update['prop2']) == 1
+ assert diff.update_case.update['prop2'][0].question_path == 'two'
+
+ assert len(diff.update_case.delete['prop3']) == 1
+ assert diff.update_case.delete['prop3'][0].question_path == 'three'
+
+ def test_parse_universal_diff_non_registration_name_stays_in_update(self):
+ universal_json = {
+ 'add': {
+ 'name': [{'question_path': 'one'}]
+ }
+ }
+
+ diff = FormActionsDiff.parse_universal_diff(universal_json)
+
+ assert diff.update_case.add['name'][0].question_path == 'one'
+ assert 'name' not in diff.open_case.add
+
+ def test_parse_universal_diff_registration_name_is_in_open_case(self):
+ universal_json = {
+ 'add': {
+ 'name': [{'question_path': 'one'}]
+ }
+ }
+
+ diff = FormActionsDiff.parse_universal_diff(universal_json, is_registration=True)
+
+ assert diff.open_case.add[0].question_path == 'one'
+ assert 'name' not in diff.update_case.add
+
class FormActionTests(SimpleTestCase):
def test_get_action_properties_for_name_update(self):
diff --git a/corehq/apps/app_manager/tests/test_xform.py b/corehq/apps/app_manager/tests/test_xform.py
index 799b42270244..da605f107ea1 100644
--- a/corehq/apps/app_manager/tests/test_xform.py
+++ b/corehq/apps/app_manager/tests/test_xform.py
@@ -1,4 +1,5 @@
from unittest.mock import patch
+from lxml import etree
from django.test import SimpleTestCase
@@ -9,7 +10,8 @@
XFormValidationError,
XFormValidationFailed,
)
-from ..xform import parse_xml, validate_xform
+from corehq.apps.app_manager.models import Form, FormActions
+from ..xform import parse_xml, validate_xform, XForm
class ParseXMLTests(SimpleTestCase):
@@ -106,3 +108,225 @@ def test_successful(self, mock_validate_form):
validate_xform(xml)
except XFormValidationFailed as e:
self.fail(f"validate_xform raised {e} unexpectedly")
+
+
+class XForm_CreateCaseMappingsTests(SimpleTestCase):
+ def test_creates_mappings(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {
+ 'one': {'question_path': 'q1'},
+ 'two': {'question_path': 'q2'}
+ }
+ }
+ })
+
+ form = Form(actions=actions)
+
+ tree = XForm.create_case_mappings(form)
+ rendered_tree = etree.tostring(tree, encoding='unicode', pretty_print=True).strip()
+
+ assert rendered_tree == """
+
+
+
+
+
+
+
+
+""".strip()
+
+ def test_creates_open_case_mappings(self):
+ actions = FormActions({
+ 'open_case': {
+ 'name_update': {'question_path': 'name'}
+ }
+ })
+
+ form = Form(actions=actions)
+
+ tree = XForm.create_case_mappings(form)
+ rendered_tree = etree.tostring(tree, encoding='unicode', pretty_print=True).strip()
+
+ assert rendered_tree == """
+
+
+
+
+
+""".strip()
+
+ def test_uses_name_from_open_case(self):
+ actions = FormActions({
+ 'open_case': {
+ 'name_update': {'question_path': 'open_case_name'}
+ },
+ 'update_case': {
+ 'update': {
+ 'name': {'question_path': 'update_case_name'}
+ }
+ }
+ })
+
+ form = Form(actions=actions)
+
+ tree = XForm.create_case_mappings(form)
+ rendered_tree = etree.tounicode(tree, pretty_print=True).strip()
+
+ assert rendered_tree == """
+
+
+
+
+
+""".strip()
+
+
+class XForm_AddCaseMappingsTests(SimpleTestCase):
+ def test_adds_case_mappings(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {
+ 'one': {'question_path': 'q1'}
+ }
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ assert xform.find('case_mappings') is not None
+
+ def test_handles_no_mappings(self):
+ actions = FormActions()
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ mappings_node = xform.find('case_mappings')
+ assert len(mappings_node) == 0
+
+ def test_does_not_create_duplicate_mapping_nodes(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {
+ 'one': {'question_path': 'q1'}
+ }
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+ xform.add_case_mappings(form)
+
+ case_mapping_nodes = xform.findall('{f}case_mappings')
+ assert len(case_mapping_nodes) == 1
+
+ def _create_minimal_xform(self):
+ return XForm('''
+
+
+
+
+
+
+
+
+
+''')
+
+
+class XForm_GetFormActionsTests(SimpleTestCase):
+ def test_no_mappings_returns_empty_dict(self):
+ xform = self._create_minimal_xform()
+ assert xform.get_form_actions() == {}
+
+ def test_returns_update_mappings(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {
+ 'one': {'question_path': 'q1'},
+ 'two': {'question_path': 'q2'},
+ }
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ mappings = xform.get_form_actions()
+
+ case_updates = mappings['update_case']['update_multi']
+
+ assert set(case_updates.keys()) == {'one', 'two'}
+ assert case_updates['one'] == [{'question_path': 'q1', 'update_mode': 'always'}]
+ assert case_updates['two'] == [{'question_path': 'q2', 'update_mode': 'always'}]
+
+ def test_name_updates_for_registration_forms_use_open_case(self):
+ actions = FormActions({
+ 'open_case': {
+ 'name_update': {'question_path': 'test_name'}
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ mappings = xform.get_form_actions(for_registration_form=True)
+
+ name_updates = mappings['open_case']['name_update_multi']
+
+ assert name_updates == [{'question_path': 'test_name', 'update_mode': 'always'}]
+ assert 'update_case' not in mappings
+
+ def test_name_updates_for_update_forms_use_update_case(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {'name': {'question_path': 'test_name'}},
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ mappings = xform.get_form_actions(for_registration_form=False)
+
+ case_updates = mappings['update_case']['update_multi']
+
+ assert case_updates['name'] == [{'question_path': 'test_name', 'update_mode': 'always'}]
+ assert 'open_case' not in mappings
+
+ def test_can_be_turned_into_form_actions_object(self):
+ actions = FormActions({
+ 'update_case': {
+ 'update': {'name': {'question_path': 'test_name'}},
+ }
+ })
+ form = Form(actions=actions)
+ xform = self._create_minimal_xform()
+
+ xform.add_case_mappings(form)
+
+ mappings = xform.get_form_actions()
+ result = FormActions(mappings)
+ assert result.update_case.update_multi['name'][0].question_path == 'test_name'
+
+ def _create_minimal_xform(self):
+ return XForm('''
+
+
+
+
+
+
+
+
+
+''')
diff --git a/corehq/apps/app_manager/util.py b/corehq/apps/app_manager/util.py
index 55b7acc8e59e..4910e3994fef 100644
--- a/corehq/apps/app_manager/util.py
+++ b/corehq/apps/app_manager/util.py
@@ -151,26 +151,32 @@ def generate_xmlns():
return str(uuid.uuid4()).upper()
-def save_xform(app, form, xml):
-
- def change_xmlns(xform, old_xmlns, new_xmlns):
+def save_xform(app, form, xml, mapping_diff=None):
+ def update_xmlns(xform, old_xmlns, new_xmlns):
data = xform.data_node.render().decode('utf-8')
data = data.replace(old_xmlns, new_xmlns, 1)
xform.instance_node.remove(xform.data_node.xml)
xform.instance_node.append(parse_xml(data))
- return xform.render()
+
+ source_xml = xml
try:
xform = XForm(xml, domain=app.domain)
except XFormException:
pass
else:
+ changed = False
+ if mapping_diff:
+ form.actions = form.actions.with_updates({}, mapping_diff)
+ changed = True
+
GENERIC_XMLNS = "http://www.w3.org/2002/xforms"
uid = generate_xmlns()
tag_xmlns = xform.data_node.tag_xmlns
new_xmlns = form.xmlns or "http://openrosa.org/formdesigner/%s" % uid
if not tag_xmlns or tag_xmlns == GENERIC_XMLNS: # no xmlns
- xml = change_xmlns(xform, GENERIC_XMLNS, new_xmlns)
+ update_xmlns(xform, GENERIC_XMLNS, new_xmlns)
+ changed = True
else:
forms = [form_
for form_ in app.get_xmlns_map().get(tag_xmlns, [])
@@ -180,23 +186,28 @@ def change_xmlns(xform, old_xmlns, new_xmlns):
new_xmlns = "http://openrosa.org/formdesigner/%s" % uid
# form most likely created by app.copy_form(...)
# or form is being updated with source copied from other form
- xml = change_xmlns(xform, tag_xmlns, new_xmlns)
+ update_xmlns(xform, tag_xmlns, new_xmlns)
+ changed = True
+
+ if changed:
+ # form.source needs to persist the XML without case mappings (because the mappings
+ # are persisted in FormActions, instead),
+ # but keep a separate copy of the xml with mapping data for SHA comparisons
+ xml = xform.render_pretty()
+ xform.remove_case_mappings()
+ source_xml = xform.render_pretty()
- form.source = xml.decode('utf-8')
+ form.source = source_xml.decode('utf-8')
- from corehq.apps.app_manager.models import ConditionalCaseUpdate
if form.is_registration_form():
# For registration forms, assume that the first question is the
# case name unless something else has been specified
questions = form.get_questions([app.default_language])
- if hasattr(form.actions, 'open_case'):
- path = getattr(form.actions.open_case.name_update, 'question_path', None)
- if path:
- name_questions = [q for q in questions if q['value'] == path]
- if not len(name_questions):
- path = None
- if not path and len(questions):
- form.actions.open_case.name_update = ConditionalCaseUpdate(question_path=questions[0]['value'])
+ if questions and hasattr(form.actions, 'open_case'):
+ names = form.actions.open_case.get_assigned_names()
+ assigns_name = any(q for q in questions if q['value'] in names)
+ if not assigns_name:
+ form.actions.open_case.assign_name_update(questions[0]['value'])
return xml
diff --git a/corehq/apps/app_manager/views/formdesigner.py b/corehq/apps/app_manager/views/formdesigner.py
index 182cf83e1791..f1120fe3cec0 100644
--- a/corehq/apps/app_manager/views/formdesigner.py
+++ b/corehq/apps/app_manager/views/formdesigner.py
@@ -20,6 +20,7 @@
send_hubspot_form,
)
from corehq.apps.app_manager import add_ons
+from corehq.apps.app_manager.app_schemas.case_properties import get_all_case_properties_for_case_type
from corehq.apps.app_manager.app_schemas.casedb_schema import get_casedb_schema, get_registry_schema
from corehq.apps.app_manager.app_schemas.session_schema import (
get_session_schema,
@@ -36,7 +37,8 @@
AppManagerException,
FormNotFoundException,
)
-from corehq.apps.app_manager.models import ModuleNotFoundException
+from corehq.apps.app_manager.helpers.validators import load_case_reserved_words
+from corehq.apps.app_manager.models import ModuleNotFoundException, AdvancedForm
from corehq.apps.app_manager.templatetags.xforms_extras import translate
from corehq.apps.app_manager.util import (
app_callout_templates,
@@ -234,7 +236,7 @@ def _get_base_vellum_options(request, domain, form, displayLang):
:param displayLang: --> derived from the base context
"""
app = form.get_app()
- return {
+ options = {
'intents': {
'templates': next(app_callout_templates),
},
@@ -254,6 +256,22 @@ def _get_base_vellum_options(request, domain, form, displayLang):
},
}
+ has_vellum_case_mapping = toggles.FORMBUILDER_SAVE_TO_CASE.enabled_for_request(request)
+ is_advanced_form = isinstance(form, AdvancedForm)
+
+ case_type = form.get_module().case_type
+ if case_type and has_vellum_case_mapping and not is_advanced_form:
+ case_properties = get_all_case_properties_for_case_type(domain, case_type)
+ mappings = form.actions.get_mappings()
+ options['caseManagement'] = {
+ 'mappings': mappings,
+ 'properties': case_properties,
+ 'view_form_url': reverse('view_form', args=[domain, app.id, form.unique_id]),
+ 'reserved_words': load_case_reserved_words(),
+ }
+
+ return options
+
def _get_vellum_core_context(request, domain, app, module, form, lang):
"""
diff --git a/corehq/apps/app_manager/views/forms.py b/corehq/apps/app_manager/views/forms.py
index 3a240075cbf4..c63c34945f87 100644
--- a/corehq/apps/app_manager/views/forms.py
+++ b/corehq/apps/app_manager/views/forms.py
@@ -304,7 +304,7 @@ def should_edit(attribute):
return attribute in request.POST
if 'sha1' in request.POST and (should_edit("xform") or "xform" in request.FILES):
- conflict = _get_xform_conflict_response(form, request.POST['sha1'])
+ conflict = _get_xform_conflict_response(form.source, request.POST['sha1'])
if conflict is not None:
return conflict
@@ -336,7 +336,9 @@ def should_edit(attribute):
if xform:
if isinstance(xform, str):
xform = xform.encode('utf-8')
- save_xform(app, form, xform)
+
+ case_update_diff = _get_case_update_diff(request, form)
+ save_xform(app, form, xform, case_update_diff)
else:
raise Exception("You didn't select a form to upload")
except Exception as e:
@@ -523,15 +525,18 @@ def patch_xform(request, domain, app_id, form_unique_id):
app = get_app(domain, app_id)
form = app.get_form(form_unique_id)
+ existing_xml = form.source
- conflict = _get_xform_conflict_response(form, sha1_checksum)
+ conflict = _get_xform_conflict_response(existing_xml, sha1_checksum)
if conflict is not None:
return conflict
- xml = apply_patch(patch, form.source)
+ xml = apply_patch(patch, existing_xml)
+
+ case_update_diff = _get_case_update_diff(request, form)
try:
- xml = save_xform(app, form, xml.encode('utf-8'))
+ xml = save_xform(app, form, xml.encode('utf-8'), case_update_diff)
except XFormException:
return JsonResponse({'status': 'error'}, status=HttpResponseBadRequest.status_code)
@@ -551,8 +556,19 @@ def apply_patch(patch, text):
return dmp.patch_apply(dmp.patch_fromText(patch), text)[0]
-def _get_xform_conflict_response(form, sha1_checksum):
- form_xml = form.source
+def _get_case_update_diff(request, form):
+ update_diff = None
+
+ uses_vellum_case_mapping = toggles.FORMBUILDER_SAVE_TO_CASE.enabled_for_request(request)
+ if uses_vellum_case_mapping and 'mapping_diff' in request.POST:
+ diff_json = json.loads(request.POST['mapping_diff'])
+ is_registration_form = form.is_registration_form()
+ update_diff = FormActionsDiff.parse_universal_diff(diff_json, is_registration=is_registration_form)
+
+ return update_diff
+
+
+def _get_xform_conflict_response(form_xml, sha1_checksum):
if hashlib.sha1(form_xml.encode('utf-8')).hexdigest() != sha1_checksum:
return json_response({'status': 'conflict', 'xform': form_xml})
return None
diff --git a/corehq/apps/app_manager/xform.py b/corehq/apps/app_manager/xform.py
index 03437fdafd5a..1e8f16322a1e 100644
--- a/corehq/apps/app_manager/xform.py
+++ b/corehq/apps/app_manager/xform.py
@@ -2147,9 +2147,120 @@ def get_scheduler_case_updates(self):
raise Exception('Scheduler case updates have not yet been populated')
return self._scheduler_case_updates
+ @property
+ def case_mappings_xml_node(self):
+ path = '{f}case_mappings'.format(**self.namespaces)
+ return self.xml.find(path)
+
def _add_scheduler_case_update(self, case_type, case_property):
self._scheduler_case_updates[case_type].add(case_property)
+ def add_case_mappings(self, form):
+ mapping_element = self.create_case_mappings(form)
+ existing_mappings = self.case_mappings_xml_node
+ if existing_mappings is None:
+ self.xml.append(mapping_element)
+ else:
+ self.xml.replace(existing_mappings, mapping_element)
+
+ @classmethod
+ def create_case_mappings(cls, form):
+ root = _make_elem('{f}case_mappings')
+
+ # TODO: add non-mutating functionality to form actions to grab
+ # the multi version without mutating the base object
+ form.actions.open_case.make_multi()
+ name_mapping = _make_elem('{f}mapping', {'property': 'name'})
+ for question in form.actions.open_case.name_update_multi:
+ if question.question_path:
+ name_mapping.append(cls._get_update_xml(question))
+ if len(name_mapping):
+ root.append(name_mapping)
+
+ form.actions.update_case.make_multi()
+ for case_property, questions in form.actions.update_case.update_multi.items():
+ if case_property == 'name' and len(name_mapping):
+ # skip name mappings if already provided by open_case
+ continue
+ mapping = _make_elem('{f}mapping', {'property': case_property})
+ root.append(mapping)
+ for question in questions:
+ mapping.append(cls._get_update_xml(question))
+
+ return root
+
+ def get_form_actions(self, for_registration_form=False):
+ root = self.case_mappings_xml_node
+ if root is None:
+ return {}
+
+ name_update_multi = []
+ update_case_mappings = {}
+
+ for mapping_node in root:
+ prop = mapping_node.attrib['property']
+ questions = []
+
+ for question_node in mapping_node:
+ question = question_node.attrib
+ question_dict = dict(question.items())
+ questions.append(question_dict)
+
+ if prop == 'name' and for_registration_form:
+ name_update_multi = questions
+ else:
+ update_case_mappings[prop] = questions
+
+ actions = {}
+ if name_update_multi:
+ actions['open_case'] = {
+ 'name_update_multi': name_update_multi
+ }
+
+ if update_case_mappings:
+ actions['update_case'] = {
+ 'update_multi': update_case_mappings
+ }
+
+ return actions
+
+ def remove_case_mappings(self):
+ '''
+ Removes the case mappings node and returns a boolean indicating whether
+ the tree was changed
+ '''
+ mappings_node = self.case_mappings_xml_node
+ if mappings_node is not None:
+ self.xml.remove(mappings_node)
+ return True
+
+ return False
+
+ @classmethod
+ def _get_update_xml(cls, update):
+ EXCLUDED_PROPERTIES = ['doc_type']
+ properties = {k: v for (k, v) in update.items() if k not in EXCLUDED_PROPERTIES}
+ # return ET.Element('question', **properties)
+ return _make_elem('{f}question', properties)
+
+ def render_pretty(self):
+ '''
+ This returns a tab-indented version of the xml as a string.
+ This is mostly a hackish method of creating a string that exactly mimics
+ Vellum's formatting. If the Vellum formatting were to change, this would
+ also need to change.
+ '''
+ ET.indent(self.xml, space='\t')
+ encoding = "UTF-8"
+ output = ET.tostring(self.xml, encoding=encoding).decode(encoding)
+
+ # Vellum uses an additional space after self-closing elements
+ output = re.sub(r'(?', ' />', output)
+
+ xml_declaration = f''
+
+ return (xml_declaration + '\n' + output).encode(encoding)
+
VELLUM_TYPES = {
"AndroidIntent": {