Skip to content

Instantly share code, notes, and snippets.

@esoergel
Created January 23, 2026 15:30
Show Gist options
  • Select an option

  • Save esoergel/21303f9e9be4dbec2f7185ee9f83e181 to your computer and use it in GitHub Desktop.

Select an option

Save esoergel/21303f9e9be4dbec2f7185ee9f83e181 to your computer and use it in GitHub Desktop.
import csv
import io
import json
from corehq import toggles
from corehq.util.log import with_progress_bar
from corehq.apps.hqadmin.utils import get_download_url
from corehq.apps.accounting.models import Subscription
from corehq.apps.app_manager.dbaccessors import get_app_doc, get_app_ids_in_domain
from corehq.apps.case_search.models import CaseSearchConfig
from corehq.apps.domain.models import Domain
domain_headers = [
'Env',
'Domain Name',
'Service Type',
'Plan Name',
'Domain Active',
'Case Search Enabled',
'synchronous_web_apps',
'sync_cases_on_form_entry',
'Fuzzy Search setting',
'Remove Special Characters',
'uses fuzzy_prefix_length',
]
app_feature_high_headers = [
'Application',
'Has Search Property',
'Has Default Search',
'Xpath Default',
'Skip to Default Search',
'Normal Case List',
'Search First',
'See More',
'Related Property',
'Inline Search',
'Include Related Cases',
'Ancestor Exists',
'Subcase Exists',
'Subcase Count'
]
csql_fns = [
"date",
"today",
"date-add",
"datetime",
"now",
"datetime-add",
"not",
"starts-with",
"selected",
"selected-any",
"selected-all",
"within-distance",
"fuzzy-match",
"phonetic-match",
"match-all",
"match-none",
]
app_feature_medium_headers = [
"property hidden",
"property exclude",
"claim condition",
"blacklisted_owner_ids_expression",
"custom_related_case_property",
"search_filter",
"instance_name",
"custom_sort_properties",
] + [
f"CSQL {fn}" for fn in csql_fns
]
app_feature_low_headers = [
'Application',
'Search Field Default',
'Search Field Lookup Table',
'Search Field Mobile Report',
'Search Field Checkbox',
'Search Field Free Text',
'Search Field Allow Blank',
'Search Field Single Date',
'Search Field Date Range',
'Search Field Help Text',
'Search Field Geocoder',
'Search Field Required',
'Search Field Validation',
'Search Title',
'Search Subtitle',
'Search On Clear'
]
def get_case_list_modules(app_doc):
return [m for m in app_doc.get('modules', []) if m['module_type'] == 'basic' and m['case_type']]
def get_app_features_high(domain_name, app):
has_search_properties = False
has_default_properties = False
has_skip_default_search = False
has_normal_case_list = False
has_search_first = False
has_see_more = False
uses_xpath_default = False
uses_related_property = False
uses_inline_search = False
uses_include_all_related_cases = False
uses_ancestor_exists = False
uses_subcase_exists = False
uses_subcase_count = False
for m in get_case_list_modules(app):
search_config = m.get('search_config', None)
if search_config is None:
continue
properties = search_config.get('properties', [])
default_properties = search_config.get('default_properties', [])
has_search_properties |= bool(properties)
has_default_properties |= bool(default_properties)
uses_xpath_default |= any([p['property'] == '_xpath_query' for p in default_properties])
uses_related_property |= any([p['name'].startswith('parent/') for p in properties])
auto_launch = search_config.get('auto_launch', False)
default_search = search_config.get('default_search', False)
has_skip_default_search |= auto_launch and default_search
has_normal_case_list |= not auto_launch and not default_search
has_search_first |= auto_launch and not default_search
has_see_more |= not auto_launch and default_search
uses_ancestor_exists |= any([p['property'] == '_xpath_query' and 'ancestor-exists' in p['defaultValue'] for p in default_properties])
uses_subcase_exists |= any([p['property'] == '_xpath_query' and 'subcase-exists' in p['defaultValue'] for p in default_properties])
uses_subcase_count |= any([p['property'] == '_xpath_query' and 'subcase-count' in p['defaultValue'] for p in default_properties])
uses_inline_search |= search_config.get('inline_search', False)
uses_include_all_related_cases |= search_config.get('include_all_related_cases', False)
return {
'Application': f"{app['_id']} ({app.get('name', None)})",
'Has Search Property': has_search_properties,
'Has Default Search': has_default_properties,
'Xpath Default': uses_xpath_default,
'Skip to Default Search': has_skip_default_search,
'Normal Case List': has_normal_case_list,
'Search First': has_search_first,
'See More': has_see_more,
'Related Property': uses_related_property,
'Inline Search': uses_inline_search,
'Include Related Cases': uses_include_all_related_cases,
'Ancestor Exists': uses_ancestor_exists,
'Subcase Exists': uses_subcase_exists,
'Subcase Count': uses_subcase_count,
}
def get_app_features_medium(domain_name, app):
ret = {'Application': f"{app['_id']} ({app.get('name', None)})",}
ret['property hidden'] = False
ret['property exclude'] = False
ret['claim condition'] = False
ret['blacklisted_owner_ids_expression'] = False
ret['custom_related_case_property'] = False
ret['search_filter'] = False
ret['instance_name'] = False
ret['custom_sort_properties'] = False
ret.update({
f"CSQL {fn}": False for fn in csql_fns
})
for m in get_case_list_modules(app):
search_config = m.get('search_config', {})
ret['claim condition'] |= bool(search_config.get('additional_relevant'))
ret['blacklisted_owner_ids_expression'] |= bool(search_config.get('blacklisted_owner_ids_expression'))
ret['custom_related_case_property'] |= bool(search_config.get('custom_related_case_property'))
ret['search_filter'] |= bool(search_config.get('search_filter'))
ret['instance_name'] |= bool(search_config.get('instance_name'))
ret['custom_sort_properties'] |= bool(len(search_config.get('custom_sort_properties', [])))
for prop in search_config.get('properties', []):
ret['property hidden'] |= prop.get('hidden', False)
ret['property exclude'] |= prop.get('exclude', False)
for prop in search_config.get('default_properties', []):
if prop.get('property') == '_xpath_query' and prop.get('defaultValue'):
for fn in csql_fns:
ret[f"CSQL {fn}"] |= fn in prop['defaultValue']
return ret
def get_app_features_low(domain_name, app):
uses_default_for_search_field = False
uses_lookup_table_for_search_field = False
uses_mobile_report_for_search_field = False
uses_checkbox_for_search_field = False
uses_free_text_for_search_field = False
uses_allow_blank_search_field = False
uses_single_date_search_field = False
uses_date_range_search_field = False
uses_help_text = False
uses_geocoder = False
uses_required = False
uses_validation = False
uses_search_title = False
uses_search_sub_title = False
uses_search_on_clear = False
for m in get_case_list_modules(app):
search_config = m.get('search_config', {})
properties = search_config.get('properties', [])
# default_properties = search_config.get('default_properties', [])
uses_default_for_search_field |= any([bool(p.get('default_value')) for p in properties])
uses_lookup_table_for_search_field |= any([
p.get('input_', '') in ['select', 'select1'] and
p.get('itemset', {}).get('instance_id', '').startswith('item-list:')
for p in properties
])
uses_checkbox_for_search_field |= any([
p.get('input_', '') == 'checkbox' and
p.get('itemset', {}).get('instance_id', '').startswith('item-list:')
for p in properties
])
uses_mobile_report_for_search_field |= any([
p.get('input_', '') in ['select', 'select1'] and
p.get('itemset', {}).get('instance_id', '').startswith('commcare-reports:')
for p in properties
])
uses_free_text_for_search_field |= any([
p.get('input_') is None and
not p.get('hidden', False) and
p.get('appearance') is None
for p in properties
])
uses_allow_blank_search_field |= any([
p.get('allow_blank_value', False)
for p in properties
])
uses_single_date_search_field |= any([
p.get('input_', '') == 'date'
for p in properties
])
uses_date_range_search_field |= any([
p.get('input_', '') == 'daterange'
for p in properties
])
uses_help_text |= any([
bool(p.get('hint', {}))
for p in properties
])
uses_geocoder |= any([
p.get('input_') is None and
not p.get('hidden', False) and
p.get('appearance') == 'address'
for p in properties
])
uses_required |= any([
bool(p.get('required', {}).get('test'))
for p in properties
])
uses_validation |= any([
bool(p.get('validations', []))
for p in properties
])
def first_property_is_not_empty(d):
return bool(next(iter(d.values())) if d else None)
uses_search_title |= first_property_is_not_empty(search_config.get('title_label', {}))
uses_search_sub_title |= first_property_is_not_empty(search_config.get('description', {}))
uses_search_on_clear |= search_config.get('search_on_clear', False)
return {
'Application': f"{app['_id']} ({app.get('name', None)})",
'Search Field Default': uses_default_for_search_field,
'Search Field Lookup Table': uses_lookup_table_for_search_field,
'Search Field Mobile Report': uses_mobile_report_for_search_field,
'Search Field Checkbox': uses_checkbox_for_search_field,
'Search Field Free Text': uses_free_text_for_search_field,
'Search Field Allow Blank': uses_allow_blank_search_field,
'Search Field Single Date': uses_single_date_search_field,
'Search Field Date Range': uses_date_range_search_field,
'Search Field Help Text': uses_help_text,
'Search Field Geocoder': uses_geocoder,
'Search Field Required': uses_required,
'Search Field Validation': uses_validation,
'Search Title': uses_search_title,
'Search Subtitle': uses_search_sub_title,
'Search On Clear': uses_search_on_clear,
}
def get_domain_settings(domain_name):
csc = CaseSearchConfig.objects.get_or_none(pk=domain_name)
if not csc:
return {}
return {
"Case Search Enabled": csc.enabled,
"synchronous_web_apps": csc.synchronous_web_apps,
"sync_cases_on_form_entry": csc.sync_cases_on_form_entry,
"Fuzzy Search setting": csc.fuzzy_properties.exists(),
"Remove Special Characters": csc.ignore_patterns.exists(),
"uses fuzzy_prefix_length": csc.fuzzy_prefix_length is not None,
}
def write_features_for_case_search_apps_to_csv(
env,
download_url=True,
include_high=False,
include_medium=False,
include_low=False
):
output = io.StringIO()
header = domain_headers
if include_high:
header += app_feature_high_headers
if include_medium:
header += app_feature_medium_headers
if include_low:
header += app_feature_low_headers
writer = csv.DictWriter(output, fieldnames=header)
writer.writeheader()
domains = toggles.SYNC_SEARCH_CASE_CLAIM.get_enabled_domains()
for domain_name in with_progress_bar(domains, prefix="Domains"):
domain = Domain.get_by_name(domain_name)
subscription = Subscription.get_active_subscription_by_domain(domain_name)
domain_settings = get_domain_settings(domain_name)
for app_id in get_app_ids_in_domain(domain_name):
app = get_app_doc(domain_name, app_id)
app_features_high = {}
if include_high:
app_features_high = get_app_features_high(domain_name, app)
app_features_medium = {}
if include_medium:
app_features_medium = get_app_features_medium(domain_name, app)
app_features_low = {}
if include_low:
app_features_low = get_app_features_low(domain_name, app)
row = {
'Env': env,
'Domain Name': domain_name,
'Service Type': subscription and subscription.service_type,
'Plan Name': subscription and subscription.plan_version.plan.name,
'Domain Active': domain and domain.is_active,
**domain_settings,
**app_features_high,
**app_features_medium,
**app_features_low,
}
writer.writerow(row)
output.seek(0)
if download_url:
return get_download_url(io.BytesIO(output.read().encode('utf-8')), "test.csv", content_type="text")
else:
return output.getvalue()
print(write_features_for_case_search_apps_to_csv('local', download_url=True, include_high=True, include_medium=True, include_low=True))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment