Skip to content

Commit 049dad8

Browse files
authored
Merge pull request #37298 from dimagi/dm/vellum-case-mapping
Form Builder case mappings
2 parents b2945ba + 96ecd3f commit 049dad8

File tree

6 files changed

+200
-11
lines changed

6 files changed

+200
-11
lines changed

corehq/apps/app_manager/models.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,14 @@ def make_multi(self):
395395
# because update_multi is meant to be exclusive with update, no items need to be moved
396396
return False
397397

398-
self.update_multi = {k: [v] for (k, v) in self.update.items()}
398+
self.update_multi = self._multi_updates()
399399
self.update = {}
400400

401401
return True
402402

403+
def _multi_updates(self):
404+
return self.update_multi or {k: [v] for k, v in self.update.items()}
405+
403406
def normalize_update(self):
404407
'''
405408
Attempt to move `update_multi` to `update`
@@ -548,6 +551,12 @@ def _apply_updates_to_mappings(current_mappings, updates):
548551
if all_missing_mappings:
549552
raise MissingPropertyMapException(*all_missing_mappings)
550553

554+
def get_mappings(self):
555+
return {
556+
case_property: [_to_json_sans_doc_type(update) for update in updates]
557+
for (case_property, updates) in self._multi_updates().items()
558+
}
559+
551560

552561
class PreloadAction(FormAction):
553562

@@ -657,6 +666,19 @@ def has_name_update(self):
657666
def _update_has_name(self, update):
658667
return bool(update.question_path)
659668

669+
def get_mappings(self):
670+
updates = self.name_update_multi or [self.name_update]
671+
return {
672+
'name': [_to_json_sans_doc_type(update) for update in updates]
673+
}
674+
675+
676+
def _to_json_sans_doc_type(obj):
677+
json = obj.to_json()
678+
if 'doc_type' in json:
679+
del json['doc_type']
680+
return json
681+
660682

661683
class OpenSubCaseAction(FormAction, IndexedSchema):
662684

@@ -699,6 +721,33 @@ class FormActionsDiff(DocumentSchema):
699721
open_case = SchemaProperty(OpenCaseDiff)
700722
update_case = SchemaProperty(UpdateCaseDiff)
701723

724+
@classmethod
725+
def from_json(cls, universal_diff_json, is_registration=False):
726+
open_diff = OpenCaseDiff()
727+
update_diff = UpdateCaseDiff(universal_diff_json)
728+
729+
if is_registration:
730+
if 'name' in update_diff.add:
731+
name_additions = update_diff.add['name']
732+
open_diff.add = name_additions
733+
del update_diff.add['name']
734+
735+
if 'name' in update_diff.update:
736+
name_updates = update_diff.update['name']
737+
open_diff.update = name_updates
738+
del update_diff.update['name']
739+
740+
if 'name' in update_diff.delete:
741+
name_deletions = update_diff.delete['name']
742+
open_diff.delete = name_deletions
743+
del update_diff.delete['name']
744+
745+
diff = FormActionsDiff()
746+
diff.open_case = open_diff
747+
diff.update_case = update_diff
748+
749+
return diff
750+
702751

703752
class FormActions(UpdateableDocument):
704753
open_case = SchemaProperty(OpenCaseAction)
@@ -769,6 +818,14 @@ def make_single(self, allow_conflicts=True):
769818
else:
770819
self.update_case.make_single()
771820

821+
def get_mappings(self):
822+
mappings = {}
823+
if self.open_case:
824+
mappings.update(self.open_case.get_mappings())
825+
if self.update_case:
826+
mappings.update(self.update_case.get_mappings())
827+
return mappings
828+
772829

773830
class CaseIndex(DocumentSchema):
774831
tag = StringProperty()

corehq/apps/app_manager/tests/test_models.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ def test_has_name_update_requires_name_update_multi_to_have_a_path(self):
160160

161161
self.assertFalse(action.has_name_update())
162162

163+
def test_get_mappings_serializes_name_updates(self):
164+
action = OpenCaseAction({
165+
'name_update_multi': [{'question_path': 'one'}, {'question_path': 'two'}]
166+
})
167+
168+
json = action.get_mappings()
169+
self.assertEqual(json, {
170+
'name': [
171+
{'question_path': 'one', 'update_mode': 'always'},
172+
{'question_path': 'two', 'update_mode': 'always'}
173+
]
174+
})
175+
163176

164177
class OpenCaseAction_ApplyUpdates_Tests(SimpleTestCase):
165178
def test_no_changes(self):
@@ -391,6 +404,37 @@ def test_get_property_names_returns_keys_from_update_multi(self):
391404

392405
self.assertEqual(action.get_property_names(), {'one'})
393406

407+
def test_get_mappings_serializes_updates(self):
408+
action = UpdateCaseAction({
409+
'update_multi': {
410+
'one': [{'question_path': '/A/'}, {'question_path': '/B/'}],
411+
'two': [{'question_path': '/C/'}],
412+
}
413+
})
414+
415+
json = action.get_mappings()
416+
417+
self.assertEqual(json, {
418+
'one': [
419+
{'question_path': '/A/', 'update_mode': 'always'},
420+
{'question_path': '/B/', 'update_mode': 'always'}
421+
],
422+
'two': [{'question_path': '/C/', 'update_mode': 'always'}]
423+
})
424+
425+
def test_get_mappings_removes_doc_type(self):
426+
action = UpdateCaseAction({
427+
'update': {
428+
'one': {'question_path': '/A/', 'update_mode': 'edit', 'doc_type': 'TestDoc'},
429+
}
430+
})
431+
432+
json = action.get_mappings()
433+
434+
self.assertEqual(json, {
435+
'one': [{'question_path': '/A/', 'update_mode': 'edit'}]
436+
})
437+
394438

395439
class UpdateCaseAction_ApplyUpdates_Tests(SimpleTestCase):
396440
def test_no_changes(self):
@@ -759,6 +803,54 @@ def test_json_construction(self):
759803
assert diff.open_case.add[0].question_path == 'one'
760804
assert diff.update_case.delete['case_two'][0].question_path == 'two'
761805

806+
def test_from_json_creates_diff_object(self):
807+
universal_json = {
808+
'add': {
809+
'prop1': [{'question_path': 'one'}]
810+
},
811+
'update': {
812+
'prop2': [{'question_path': 'two'}]
813+
},
814+
'delete': {
815+
'prop3': [{'question_path': 'three'}]
816+
}
817+
}
818+
819+
diff = FormActionsDiff.from_json(universal_json)
820+
821+
assert len(diff.update_case.add['prop1']) == 1
822+
assert diff.update_case.add['prop1'][0].question_path == 'one'
823+
824+
assert len(diff.update_case.update['prop2']) == 1
825+
assert diff.update_case.update['prop2'][0].question_path == 'two'
826+
827+
assert len(diff.update_case.delete['prop3']) == 1
828+
assert diff.update_case.delete['prop3'][0].question_path == 'three'
829+
830+
def test_from_json_non_registration_name_stays_in_update(self):
831+
universal_json = {
832+
'add': {
833+
'name': [{'question_path': 'one'}]
834+
}
835+
}
836+
837+
diff = FormActionsDiff.from_json(universal_json)
838+
839+
assert diff.update_case.add['name'][0].question_path == 'one'
840+
assert 'name' not in diff.open_case.add
841+
842+
def test_from_json_registration_name_is_in_open_case(self):
843+
universal_json = {
844+
'add': {
845+
'name': [{'question_path': 'one'}]
846+
}
847+
}
848+
849+
diff = FormActionsDiff.from_json(universal_json, is_registration=True)
850+
851+
assert diff.open_case.add[0].question_path == 'one'
852+
assert 'name' not in diff.update_case.add
853+
762854

763855
class FormActionTests(SimpleTestCase):
764856
def test_get_action_properties_for_name_update(self):

corehq/apps/app_manager/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def generate_xmlns():
151151
return str(uuid.uuid4()).upper()
152152

153153

154-
def save_xform(app, form, xml):
154+
def save_xform(app, form, xml, mapping_diff=None):
155155

156156
def change_xmlns(xform, old_xmlns, new_xmlns):
157157
data = xform.data_node.render().decode('utf-8')
@@ -165,6 +165,9 @@ def change_xmlns(xform, old_xmlns, new_xmlns):
165165
except XFormException:
166166
pass
167167
else:
168+
if mapping_diff:
169+
form.actions = form.actions.with_updates({}, mapping_diff)
170+
168171
GENERIC_XMLNS = "http://www.w3.org/2002/xforms"
169172
uid = generate_xmlns()
170173
tag_xmlns = xform.data_node.tag_xmlns

corehq/apps/app_manager/views/formdesigner.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
AppManagerException,
3333
FormNotFoundException,
3434
)
35-
from corehq.apps.app_manager.models import ModuleNotFoundException
35+
from corehq.apps.app_manager.helpers.validators import load_case_reserved_words
36+
from corehq.apps.app_manager.models import ModuleNotFoundException, AdvancedForm
3637
from corehq.apps.app_manager.templatetags.xforms_extras import translate
3738
from corehq.apps.app_manager.util import (
3839
app_callout_templates,
@@ -49,6 +50,7 @@
4950
set_lang_cookie,
5051
)
5152
from corehq.apps.cloudcare.utils import should_show_preview_app
53+
from corehq.apps.data_dictionary.util import get_case_properties
5254
from corehq.apps.domain.decorators import track_domain_request
5355
from corehq.apps.fixtures.fixturegenerators import item_lists_by_domain
5456
from corehq.apps.users.permissions import SUBMISSION_HISTORY_PERMISSION, has_permission_to_view_report
@@ -147,7 +149,7 @@ def _get_form_designer_view(request, domain, app, module, form):
147149

148150
vellum_options = _get_base_vellum_options(request, domain, form, context['lang'])
149151
vellum_options['core'] = _get_vellum_core_context(request, domain, app, module, form, context['lang'])
150-
vellum_options['plugins'] = _get_vellum_plugins(domain, form, module)
152+
vellum_options['plugins'] = _get_vellum_plugins(domain, form, module, vellum_options)
151153
vellum_options['features'] = _get_vellum_features(request, domain, app)
152154
context['vellum_options'] = vellum_options
153155

@@ -228,7 +230,7 @@ def _get_base_vellum_options(request, domain, form, displayLang):
228230
:param displayLang: --> derived from the base context
229231
"""
230232
app = form.get_app()
231-
return {
233+
options = {
232234
'intents': {
233235
'templates': next(app_callout_templates),
234236
},
@@ -248,6 +250,19 @@ def _get_base_vellum_options(request, domain, form, displayLang):
248250
},
249251
}
250252

253+
has_vellum_case_mapping = toggles.FORMBUILDER_SAVE_TO_CASE.enabled_for_request(request)
254+
is_advanced_form = isinstance(form, AdvancedForm)
255+
case_type = form.get_module().case_type
256+
if case_type and has_vellum_case_mapping and not is_advanced_form:
257+
options['caseManagement'] = {
258+
'mappings': form.actions.get_mappings(),
259+
'properties': sorted(get_case_properties(domain, case_type).values_list('name', flat=True)),
260+
'view_form_url': reverse('view_form', args=[domain, app.id, form.unique_id]),
261+
'reserved_words': load_case_reserved_words(),
262+
}
263+
264+
return options
265+
251266

252267
def _get_vellum_core_context(request, domain, app, module, form, lang):
253268
"""
@@ -291,7 +306,7 @@ def _get_vellum_core_context(request, domain, app, module, form, lang):
291306
return core
292307

293308

294-
def _get_vellum_plugins(domain, form, module):
309+
def _get_vellum_plugins(domain, form, module, options):
295310
"""
296311
Returns a list of enabled vellum plugins based on the domain's
297312
privileges.
@@ -300,6 +315,8 @@ def _get_vellum_plugins(domain, form, module):
300315
if (toggles.COMMTRACK.enabled(domain)
301316
or toggles.NON_COMMTRACK_LEDGERS.enabled(domain)):
302317
vellum_plugins.append("commtrack")
318+
if "caseManagement" in options:
319+
vellum_plugins.append("caseManagement")
303320
if toggles.VELLUM_SAVE_TO_CASE.enabled(domain):
304321
vellum_plugins.append("saveToCase")
305322
if toggles.COMMCARE_CONNECT.enabled(domain):

corehq/apps/app_manager/views/forms.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,8 @@ def should_edit(attribute):
336336
if xform:
337337
if isinstance(xform, str):
338338
xform = xform.encode('utf-8')
339-
save_xform(app, form, xform)
339+
case_update_diff = _get_case_update_diff(request, form)
340+
save_xform(app, form, xform, case_update_diff)
340341
else:
341342
raise Exception("You didn't select a form to upload")
342343
except Exception as e:
@@ -529,9 +530,10 @@ def patch_xform(request, domain, app_id, form_unique_id):
529530
return conflict
530531

531532
xml = apply_patch(patch, form.source)
533+
case_update_diff = _get_case_update_diff(request, form)
532534

533535
try:
534-
xml = save_xform(app, form, xml.encode('utf-8'))
536+
xml = save_xform(app, form, xml.encode('utf-8'), case_update_diff)
535537
except XFormException:
536538
return JsonResponse({'status': 'error'}, status=HttpResponseBadRequest.status_code)
537539

@@ -551,6 +553,17 @@ def apply_patch(patch, text):
551553
return dmp.patch_apply(dmp.patch_fromText(patch), text)[0]
552554

553555

556+
def _get_case_update_diff(request, form):
557+
update_diff = None
558+
has_vellum_case_mapping = toggles.FORMBUILDER_SAVE_TO_CASE.enabled_for_request(request)
559+
if has_vellum_case_mapping and 'mapping_diff' in request.POST:
560+
update_diff = FormActionsDiff.from_json(
561+
json.loads(request.POST['mapping_diff']),
562+
is_registration=form.is_registration_form(),
563+
)
564+
return update_diff
565+
566+
554567
def _get_xform_conflict_response(form, sha1_checksum):
555568
form_xml = form.source
556569
if hashlib.sha1(form_xml.encode('utf-8')).hexdigest() != sha1_checksum:

corehq/apps/data_dictionary/util.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,17 @@ def is_case_property_unused(domain, case_type, case_property):
405405
return query.NOT(case_property_missing(case_property)).count() == 0
406406

407407

408+
def get_case_properties(domain, case_type_name):
409+
return CaseProperty.objects.filter(
410+
case_type__name=case_type_name,
411+
case_type__domain=domain,
412+
deprecated=False,
413+
group__deprecated=False,
414+
)
415+
416+
408417
def get_case_property_group_name_for_properties(domain, case_type_name):
409-
return dict(CaseProperty.objects.filter(
410-
case_type__name=case_type_name, case_type__domain=domain, deprecated=False, group__deprecated=False
411-
).values_list('name', 'group__name'))
418+
return dict(get_case_properties(domain, case_type_name).values_list('name', 'group__name'))
412419

413420

414421
def update_url_query_params(url, params):

0 commit comments

Comments
 (0)