Skip to content

Commit

Permalink
Merge pull request #33828 from dimagi/jt/visually-segmented-query-sea…
Browse files Browse the repository at this point in the history
…rch-categories

Jt/visually segmented query search categories
  • Loading branch information
Jtang-1 authored Dec 19, 2023
2 parents 3328359 + 0b0d35e commit 5bc3206
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 8 deletions.
6 changes: 6 additions & 0 deletions corehq/apps/app_manager/feature_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,9 @@ def supports_detail_field_action(self):
toggles.CASE_LIST_CLICKABLE_ICON.enabled(self.domain)
and self._require_minimum_version('2.54')
)

@property
def supports_grouped_case_search_properties(self):
return (
self._require_minimum_version('2.54')
)
9 changes: 9 additions & 0 deletions corehq/apps/app_manager/helpers/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,15 @@ def validate_search_config(self):
"module": self.get_module_info(),
"details": search_config.get_instance_name()
}
module_contains_grouping_property = any(prop.is_group for prop in search_config.properties)
if module_contains_grouping_property:
ungrouped_properties = [prop for prop in search_config.properties if not prop.group_key]
for prop in ungrouped_properties:
yield {
"type": "invalid grouping from ungrouped search property",
"module": self.get_module_info(),
"property": prop.name,
}

def validate_case_list_field_actions(self):
if hasattr(self.module, 'case_details'):
Expand Down
3 changes: 3 additions & 0 deletions corehq/apps/app_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2119,6 +2119,9 @@ class CaseSearchProperty(DocumentSchema):
receiver_expression = StringProperty(exclude_if_none=True)
itemset = SchemaProperty(Itemset)

is_group = BooleanProperty(default=False)
group_key = StringProperty(exclude_if_none=True)


class DefaultCaseSearchProperty(DocumentSchema):
"""Case Properties with fixed value to search on"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ hqDefine("app_manager/js/details/case_claim", function () {
requiredText: '',
validationTest: '',
validationText: '',
isGroup: false,
groupKey: '',
});
var self = {};
self.uniqueId = generateSemiRandomId();
Expand Down Expand Up @@ -153,6 +155,8 @@ hqDefine("app_manager/js/details/case_claim", function () {
);
});
self.itemset = itemsetModel(options.itemsetOptions, saveButton);
self.isGroup = options.isGroup;
self.groupKey = options.groupKey;

subscribeToSave(self, [
'name', 'label', 'hint', 'appearance', 'defaultValue', 'hidden',
Expand Down Expand Up @@ -352,16 +356,36 @@ hqDefine("app_manager/js/details/case_claim", function () {
hidden: searchProperty.hidden,
receiverExpression: searchProperty.receiver_expression,
itemsetOptions: searchProperty.itemset,
isGroup: searchProperty.is_group,
groupKey: searchProperty.group_key,
}, saveButton);
});

self.search_properties = ko.observableArray(
wrappedSearchProperties.length > 0 ? wrappedSearchProperties : [searchPropertyModel({}, saveButton)]
);

self.search_properties.subscribe(function (newProperties) {
let groupKey = '';
ko.utils.arrayForEach(newProperties, function (property, index) {
if (property.isGroup) {
groupKey = `group_header_${index}`;
if (property.name !== groupKey) {
property.name(groupKey);
}
}
if (property.groupKey !== groupKey) {
property.groupKey = groupKey;
}
});
});

self.addProperty = function () {
self.search_properties.push(searchPropertyModel({}, saveButton));
};
self.addGroupProperty = function () {
self.search_properties.push(searchPropertyModel({isGroup: true}, saveButton));
};
self.removeProperty = function (property) {
self.search_properties.remove(property);
};
Expand All @@ -370,15 +394,15 @@ hqDefine("app_manager/js/details/case_claim", function () {
return _.map(
_.filter(
self.search_properties(),
function (p) { return p.name().length > 0; } // Skip properties where name is blank
function (p) { return p.name().length > 0;} // Skip properties where name is blank
),
function (p) {
var ifSupportsValidation = function (val) {
return p.hidden() || p.appearance() === "address" ? "" : val;
};
return {
name: p.name(),
label: p.label().length ? p.label() : p.name(), // If label isn't set, use name
label: (p.label().length || p.isGroup) ? p.label() : p.name(), // If label isn't set, use name
hint: p.hint(),
appearance: p.appearanceFinal(),
is_multiselect: p.isMultiselect(),
Expand All @@ -392,6 +416,8 @@ hqDefine("app_manager/js/details/case_claim", function () {
hidden: p.hidden(),
receiver_expression: p.receiverExpression(),
fixture: ko.toJSON(p.itemset),
is_group: p.isGroup,
group_key: p.groupKey,
};
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
PushFrame,
QueryData,
QueryPrompt,
QueryPromptGroup,
RemoteRequest,
RemoteRequestPost,
RemoteRequestQuery,
Expand Down Expand Up @@ -215,6 +216,7 @@ def build_remote_request_queries(self):
description=self.build_description() if self.module.search_config.description != {} else None,
data=self._remote_request_query_datums,
prompts=self.build_query_prompts(),
prompt_groups=self.build_query_prompt_groups(),
default_search=self.module.search_config.default_search,
dynamic_search=self.app.split_screen_dynamic_search and not self.module.is_auto_select(),
)
Expand Down Expand Up @@ -315,8 +317,10 @@ def _remote_request_query_datums(self):

def build_query_prompts(self):
prompts = []
for prop in self.module.search_config.properties:
prompt_properties = [prop for prop in self.module.search_config.properties if not prop.is_group]
for prop in prompt_properties:
text = Text(locale_id=id_strings.search_property_locale(self.module, prop.name))

if prop.hint:
display = Display(
text=text,
Expand Down Expand Up @@ -369,9 +373,22 @@ def build_query_prompts(self):
)
for i, validation in enumerate(prop.validations)
]
if prop.group_key:
kwargs['group_key'] = prop.group_key
prompts.append(QueryPrompt(**kwargs))
return prompts

def build_query_prompt_groups(self):
prompts = []
prompt_group_properties = [prop for prop in self.module.search_config.properties if prop.is_group]
for prop in prompt_group_properties:
text = Text(locale_id=id_strings.search_property_locale(self.module, prop.group_key))
prompts.append(QueryPromptGroup(**{
'key': prop.group_key,
'display': Display(text=text)
}))
return prompts

def build_stack(self):
stack = Stack()
rewind_if = None
Expand Down
9 changes: 9 additions & 0 deletions corehq/apps/app_manager/suite_xml/xml_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,14 @@ class QueryPrompt(DisplayNode):

itemset = NodeField('itemset', Itemset)

group_key = StringField('@group_key', required=False)


class QueryPromptGroup(DisplayNode):
ROOT_NAME = 'group'

key = StringField('@key')


class RemoteRequestQuery(OrderedXmlObject, XmlObject):
ROOT_NAME = 'query'
Expand All @@ -582,6 +590,7 @@ class RemoteRequestQuery(OrderedXmlObject, XmlObject):
description = NodeField('description', DisplayNode)
data = NodeListField('data', QueryData)
prompts = NodeListField('prompt', QueryPrompt)
prompt_groups = NodeListField('group', QueryPromptGroup)
default_search = BooleanField("@default_search")
dynamic_search = BooleanField("@dynamic_search")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,11 @@ <h4 class="alert-heading">
{% if not not_actual_build %}The shadow form is {% include "app_manager/partials/form_error_message.html" %}{% endif %}
{% case "password_format" %}
{# Do nothing; the full message is contained in error.message #}
{% case "invalid grouping from ungrouped search property" %}
{% blocktrans with module_name=error.module.name|trans:langs property=error.property %}
<a href="{{ module_url }}">{{ module_name }}</a> has a case search property
<code>{{ property }}</code> that is not assigned to a group.
{% endblocktrans %}
{% case "case search nodeset invalid" %}
{% blocktrans with module_name=error.module.name|trans:langs property=error.property %}
<a href="{{ module_url }}">{{ module_name }}</a> has a case search property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,21 @@ <h4 class="panel-title panel-title-nolink">{% trans "Search Properties" %}</h4>
{% include "app_manager/partials/modules/case_search_property.html" %}
</tbody>
</table>
<p>
<div class="btn-group">
<button type="button"
class="btn btn-default"
data-bind="click: addProperty">
<i class="fa fa-plus"></i> {% trans "Add search property" %}
<i class="fa fa-plus"></i> {% trans "Add Search Property" %}
</button>
</p>
{% if app.supports_grouped_case_search_properties %}
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li data-bind="click: addGroupProperty"><a>{% trans "Add Group" %}</a></li>
</ul>
{% endif %}
</div>
</div>
</div>
<div class="panel panel-appmanager">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{% load i18n %}
{% load hq_shared_tags %}

<tr>
<tr data-bind="css: {'info': isGroup}">
<td class="text-center">
<i class="grip sortable-handle hq-icon-full fa fa-arrows-v"></i>
</td>
<!--ko if: !isGroup -->
<td data-bind="css: {'has-error': $parent.isCommon(ko.unwrap(name()))}">
<input class="form-control" type="text" data-bind="value: name"/>
<div data-bind="visible: $parent.isCommon(ko.unwrap(name()))" class="help-block">
Expand Down Expand Up @@ -243,6 +244,14 @@ <h4 class="modal-title">
</div>
</td>
{% endif %}
<!-- /ko -->
<!--ko if: isGroup -->
<td>
<input class="form-control" type="text" placeholder="{% trans "Group Name" %}" data-bind="value: label"/>
</td>
<td></td>
<td></td>
<!--/ko-->
<td>
<i style="cursor: pointer;" class="fa fa-remove"
data-bind="click: $parent.removeProperty"></i>
Expand Down
4 changes: 4 additions & 0 deletions corehq/apps/app_manager/tests/test_app_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ def test_modules_case_search_app_strings(self):
label={'en': 'Get them all'}
),
properties=[
CaseSearchProperty(is_group=True, name='group_header_0',
group_key='group_header_0', label={'en': 'Personal Information'}),
CaseSearchProperty(name="name", label={'en': 'Name'})
]
)
Expand Down Expand Up @@ -203,6 +205,8 @@ def test_modules_case_search_app_strings(self):
self.assertEqual(en_app_strings['case_search.m0.icon'], 'jr://file/commcare/image/1.png')
self.assertEqual(en_app_strings['case_search.m0.audio'], 'jr://file/commcare/image/2.mp3')
self.assertEqual(en_app_strings['case_search.m0.again'], 'Get them all')
self.assertEqual(en_app_strings['search_property.m0.name'], 'Name')
self.assertEqual(en_app_strings['search_property.m0.group_header_0'], 'Personal Information')

# non-default language
es_app_strings = self._generate_app_strings(app, 'es', build_profile_id='es')
Expand Down
62 changes: 61 additions & 1 deletion corehq/apps/app_manager/tests/test_suite_remote_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def setUp(self):
properties=[
CaseSearchProperty(name='name', label={'en': 'Name'}),
CaseSearchProperty(name='dob', label={'en': 'Date of birth'}, input_="date"),
CaseSearchProperty(name='consent', label={'en': 'Consent to search'}, input_="checkbox")
CaseSearchProperty(name='consent', label={'en': 'Consent to search'}, input_="checkbox"),
],
additional_relevant="instance('groups')/groups/group",
search_filter="name = instance('item-list:trees')/trees_list/trees[favorite='yes']/name",
Expand Down Expand Up @@ -984,3 +984,63 @@ def test_case_search_validation_conditions(self):
</partial>
"""
self.assertXmlPartialEqual(expected, suite, "./remote-request[1]/session/query/prompt")

def test_group(self):
self.module.search_config.properties = [
CaseSearchProperty(is_group=True, group_key='group_header_0', label={'en': 'Personal Information'}),
CaseSearchProperty(name='name', group_key='group_header_0', label={'en': 'Name'}),
CaseSearchProperty(name='dob', group_key='group_header_0',
label={'en': 'Date of birth'}, input_="date"),
CaseSearchProperty(is_group=True, group_key='group_header_3', label={'en': 'Authorization'}),
CaseSearchProperty(name='consent', group_key='group_header_3',
label={'en': 'Consent to search'}, input_="checkbox"),
]
suite = self.app.create_suite()
expected = """
<partial>
<group key="group_header_0">
<display>
<text>
<locale id="search_property.m0.group_header_0"/>
</text>
</display>
</group>
<group key="group_header_3">
<display>
<text>
<locale id="search_property.m0.group_header_3"/>
</text>
</display>
</group>
</partial>
"""
self.assertXmlPartialEqual(expected, suite,
"./remote-request[1]/session/query/group")

expected = """
<partial>
<prompt key="name" group_key="group_header_0">
<display>
<text>
<locale id="search_property.m0.name"/>
</text>
</display>
</prompt>
<prompt group_key="group_header_0" input="date" key="dob">
<display>
<text>
<locale id="search_property.m0.dob"/>
</text>
</display>
</prompt>
<prompt group_key="group_header_3" input="checkbox" key="consent">
<display>
<text>
<locale id="search_property.m0.consent"/>
</text>
</display>
</prompt>
</partial>
"""
self.assertXmlPartialEqual(expected, suite,
"./remote-request[1]/session/query/prompt")
4 changes: 4 additions & 0 deletions corehq/apps/app_manager/views/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,10 @@ def _get_itemset(prop):
'text': _update_translation(current.validations[0] if current and current.validations else None,
prop, "text", "validation_text"),
}]
if prop.get('is_group'):
ret['is_group'] = prop['is_group']
if prop.get('group_key'):
ret['group_key'] = prop['group_key']
if prop.get('appearance', '') == 'fixture':
if prop.get('is_multiselect', False):
ret['input_'] = 'select'
Expand Down

0 comments on commit 5bc3206

Please sign in to comment.