Created
January 23, 2026 15:30
-
-
Save esoergel/21303f9e9be4dbec2f7185ee9f83e181 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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