From 6aed2af0e80020d1afda91d149ae8f8b10acf12e Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 10:17:46 -0500 Subject: [PATCH 1/7] Added hqdefine_to_esm --- .../management/commands/hqdefine_to_esm.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py new file mode 100644 index 000000000000..36dbfb4242dd --- /dev/null +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -0,0 +1,139 @@ +import logging +import os +import re + +from django.core.management import BaseCommand, CommandError + + +logger = logging.getLogger('hqdefine_to_esm') + +IMPORT_PATTERN = r'\s*["\']([^,]*)["\'],$' +ARGUMENT_PATTERN = r'\s*([^,]*),?$' + + +class Command(BaseCommand): + help = ''' + Attempts to migrate a JavaScript file from hqDefine to ESM syntax. + Expects input file to be formatted with hqDefine on one line, + then one line per import, then one line per hqDefine argument. + + Also attempts to remove "use strict" directive, because modules automatically use strict. + ''' + dedent = 4 + + def add_arguments(self, parser): + parser.add_argument('filename', help='File to migrate') + + def handle(self, filename, **options): + if not os.path.exists(filename): + raise CommandError(f"Could not find {filename}") + + with open(filename, 'r') as fin: + lines = fin.readlines() + + # Parse imports + self._init_parser() + imports = [] + arguments = [] + line_index = 0 + while self.in_hqdefine_block: + if line_index >= len(lines): + self._fail_parsing() + + line = lines[line_index] + line_index += 1 + + if self._update_parser_location(line): + continue + if self.in_imports: + imports.append(self._parse_import(line)) + elif self.in_arguments: + arguments.append(self._parse_argument(line)) + + # Rewrite file + with open(filename, 'w') as fout: + # Move commcarehq to the top + if "commcarehq" in imports: + fout.write('import "commcarehq";\n') + + # Add imports, ESM-style + for index, dependency in enumerate(imports): + if dependency is None or dependency == "commcarehq": + continue + + if index < len(arguments): + out = f'import {arguments[index]} from "{dependency}";\n' + else: + out = f'import "{dependency}";\n' + + fout.write(out) + fout.write("\n") + + # Write remaining file + for line in lines[line_index:]: + if self._is_use_strict(line) or self._is_hqdefine_close(line): + continue + + fout.write(self._dedent(line)) + + logger.info(f"Rewrote {filename}") + + def _is_use_strict(self, line): + return 'use strict' in line + + def _is_hqdefine_open(self, line): + return 'hqDefine' in line + + def _is_hqdefine_close(self, line): + return line.startswith("});") + + def _parse_import(self, line): + return self._parse_item(IMPORT_PATTERN, line, "import") + + def _parse_argument(self, line): + return self._parse_item(ARGUMENT_PATTERN, line, "argument") + + def _parse_item(self, pattern, line, description=""): + match = re.search(pattern, line) + if match: + item = match.group(1) + logger.info(f"Found {description}: {item}") + if item.endswith("\n"): + item = item[:-1] + return item + logger.warning(f"Could not parse {description} from line: {line}") + + def _init_parser(self): + self.in_hqdefine_block = True + self.in_imports = False + self.in_arguments = False + + def _fail_parsing(self): + if self.in_arguments: + status = "in arguments block" + elif self.in_imports: + status = "in imports block" + else: + status = "before imports block" + raise CommandError(f"Could not parse file. Ran out of code {status}.") + + def _update_parser_location(self, line): + if self._is_use_strict(line): + return True + if self._is_hqdefine_open(line): + self.in_imports = True + return True + if self.in_imports and 'function' in line: + self.in_imports = False + self.in_arguments = True + return True + if self.in_arguments and ')' in line: + self.in_arguments = False + self.in_hqdefine_block = False + return True + return False + + def _dedent(self, line): + if line.startswith(" " * self.dedent): + line = line[self.dedent:] + return line From b1e0fde703a7b81318d3193ec43c10861db2e621 Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 10:18:03 -0500 Subject: [PATCH 2/7] Ran hqdefine_to_esm over data_dictionary.js --- .../data_dictionary/js/data_dictionary.js | 1283 ++++++++--------- 1 file changed, 635 insertions(+), 648 deletions(-) diff --git a/corehq/apps/data_dictionary/static/data_dictionary/js/data_dictionary.js b/corehq/apps/data_dictionary/static/data_dictionary/js/data_dictionary.js index 5815c5ef8f42..e8d11323a359 100644 --- a/corehq/apps/data_dictionary/static/data_dictionary/js/data_dictionary.js +++ b/corehq/apps/data_dictionary/static/data_dictionary/js/data_dictionary.js @@ -1,714 +1,701 @@ -'use strict'; -hqDefine("data_dictionary/js/data_dictionary", [ - "jquery", - "knockout", - "underscore", - "hqwebapp/js/initial_page_data", - "hqwebapp/js/bootstrap3/main", - "analytix/js/google", - "hqwebapp/js/ui_elements/bootstrap3/ui-element-key-val-list", - "DOMPurify/dist/purify.min", - "hqwebapp/js/toggles", - "hqwebapp/js/bootstrap3/knockout_bindings.ko", - "data_interfaces/js/make_read_only", - 'hqwebapp/js/select2_knockout_bindings.ko', - 'knockout-sortable/build/knockout-sortable', - "commcarehq", -], function ( - $, - ko, - _, - initialPageData, - hqMain, - googleAnalytics, - uiElementKeyValueList, - DOMPurify, - toggles +import "commcarehq"; +import $ from "jquery"; +import ko from "knockout"; +import _ from "underscore"; +import initialPageData from "hqwebapp/js/initial_page_data"; +import hqMain from "hqwebapp/js/bootstrap3/main"; +import googleAnalytics from "analytix/js/google"; +import uiElementKeyValueList from "hqwebapp/js/ui_elements/bootstrap3/ui-element-key-val-list"; +import DOMPurify from "DOMPurify/dist/purify.min"; +import toggles from "hqwebapp/js/toggles"; +import "hqwebapp/js/bootstrap3/knockout_bindings.ko"; +import "data_interfaces/js/make_read_only"; +import "hqwebapp/js/select2_knockout_bindings.ko"; +import "knockout-sortable/build/knockout-sortable"; + +var caseType = function ( + name, + fhirResourceType, + deprecated, + moduleCount, + geoCaseProp, + isSafeToDelete, + changeSaveButton, + resetSaveButton, + dataUrl ) { - var caseType = function ( - name, - fhirResourceType, - deprecated, - moduleCount, - geoCaseProp, - isSafeToDelete, - changeSaveButton, - resetSaveButton, - dataUrl - ) { - var self = {}; - self.name = name || gettext("No Name"); - self.deprecated = deprecated; - self.appCount = moduleCount; // The number of application modules using this case type - self.url = "#" + name; - self.fhirResourceType = ko.observable(fhirResourceType); - self.groups = ko.observableArray(); - self.geoCaseProp = geoCaseProp; - self.canDelete = isSafeToDelete; - self.changeSaveButton = changeSaveButton; - self.resetSaveButton = resetSaveButton; - self.dataUrl = dataUrl; - - self.groups.subscribe(changeSaveButton); - - self.fetchCaseProperties = function () { - if (self.groups().length === 0) { - let caseTypeUrl = self.dataUrl + self.name + '/'; - recurseChunks(caseTypeUrl); - } - }; - - const recurseChunks = function (nextUrl) { - $.getJSON(nextUrl, function (data) { - setCaseProperties(data.groups); - self.resetSaveButton(); - nextUrl = data._links.next; - if (nextUrl) { - recurseChunks(nextUrl); - } - }); - }; + var self = {}; + self.name = name || gettext("No Name"); + self.deprecated = deprecated; + self.appCount = moduleCount; // The number of application modules using this case type + self.url = "#" + name; + self.fhirResourceType = ko.observable(fhirResourceType); + self.groups = ko.observableArray(); + self.geoCaseProp = geoCaseProp; + self.canDelete = isSafeToDelete; + self.changeSaveButton = changeSaveButton; + self.resetSaveButton = resetSaveButton; + self.dataUrl = dataUrl; + + self.groups.subscribe(changeSaveButton); + + self.fetchCaseProperties = function () { + if (self.groups().length === 0) { + let caseTypeUrl = self.dataUrl + self.name + '/'; + recurseChunks(caseTypeUrl); + } + }; - const setCaseProperties = function (groupData) { - for (let group of groupData) { - let groupObj = _.find(self.groups(), function (g) { - return g.id === group.id; - }); - if (!groupObj) { - groupObj = groupsViewModel( - self.name, - group.id, - group.name, - group.description, - group.deprecated, - self.changeSaveButton - ); - self.groups.push(groupObj); - } + const recurseChunks = function (nextUrl) { + $.getJSON(nextUrl, function (data) { + setCaseProperties(data.groups); + self.resetSaveButton(); + nextUrl = data._links.next; + if (nextUrl) { + recurseChunks(nextUrl); + } + }); + }; - for (let prop of group.properties) { - const isGeoCaseProp = (self.geoCaseProp === prop.name && prop.data_type === 'gps'); - if (self.canDelete && !prop.is_safe_to_delete) { - self.canDelete = false; - } + const setCaseProperties = function (groupData) { + for (let group of groupData) { + let groupObj = _.find(self.groups(), function (g) { + return g.id === group.id; + }); + if (!groupObj) { + groupObj = groupsViewModel( + self.name, + group.id, + group.name, + group.description, + group.deprecated, + self.changeSaveButton + ); + self.groups.push(groupObj); + } - var propObj = propertyListItem( - prop, - false, - group.name, - self.name, - isGeoCaseProp, - groupObj.name(), - self.changeSaveButton - ); - groupObj.properties.push(propObj); + for (let prop of group.properties) { + const isGeoCaseProp = (self.geoCaseProp === prop.name && prop.data_type === 'gps'); + if (self.canDelete && !prop.is_safe_to_delete) { + self.canDelete = false; } - } - }; - - return self; - }; - - var groupsViewModel = function ( - caseType, - id, - name, - description, - deprecated, - changeSaveButton - ) { - var self = {}; - self.id = id; - self.name = ko.observable(name); - self.description = ko.observable(description); - self.caseType = caseType; - self.properties = ko.observableArray(); - self.newPropertyName = ko.observable(); - self.expanded = ko.observable(true); - self.toggleExpanded = () => self.expanded(!self.expanded()); - self.deprecated = deprecated; - // Ensures that groups are not directly hidden on clicking the deprecated button - self.toBeDeprecated = ko.observable(deprecated || false); - self.deprecateGroup = function () { - self.toBeDeprecated(true); - }; - - self.restoreGroup = function () { - self.toBeDeprecated(false); - }; - // Show a warning that properties will be transferred to "No Group" section on deprecating a group. - self.showGroupPropertyTransferWarning = ko.computed(function () { - return self.toBeDeprecated() && !deprecated && self.properties().length > 0; - }); - self.newCaseProperty = function () { - if (self.newPropertyName().trim()) { - const prop = { - 'name': self.newPropertyName(), - 'label': self.newPropertyName(), - 'allowedValues': {}, - }; - let propObj = propertyListItem( + var propObj = propertyListItem( prop, false, - self.name(), - self.caseType, - false, - self.name(), - changeSaveButton + group.name, + self.name, + isGeoCaseProp, + groupObj.name(), + self.changeSaveButton ); - self.newPropertyName(undefined); - self.properties.push(propObj); + groupObj.properties.push(propObj); } - }; - - self.name.subscribe(changeSaveButton); - self.description.subscribe(changeSaveButton); - self.toBeDeprecated.subscribe(changeSaveButton); - self.properties.subscribe(changeSaveButton); - - return self; - }; - - var propertyListItem = function ( - prop, - isGroup, - groupName, - caseType, - isGeoCaseProp, - loadedGroup, - changeSaveButton - ) { - var self = {}; - self.id = prop.id; - self.name = prop.name; - self.label = ko.observable(prop.label); - self.expanded = ko.observable(true); - self.isGroup = isGroup; - self.group = ko.observable(groupName); - self.caseType = caseType; - self.dataType = ko.observable(prop.data_type); - self.description = ko.observable(prop.description); - self.fhirResourcePropPath = ko.observable(prop.fhir_resource_prop_path); - self.originalResourcePropPath = prop.fhir_resource_prop_path; - self.deprecated = ko.observable(prop.deprecated || false); - self.isGeoCaseProp = ko.observable(isGeoCaseProp); - self.isSafeToDelete = ko.observable(prop.is_safe_to_delete); - self.deleted = ko.observable(false); - self.hasChanges = false; - self.index = prop.index; - self.loadedGroup = loadedGroup; // The group this case property is part of when page was loaded. Used to identify group changes - - self.trackObservableChange = function (observable) { - // Keep track of old val for observable, and subscribe to new changes. - // We can then identify when the val has changed. - let oldVal = observable(); - observable.subscribe(function (newVal) { - if (!newVal && !oldVal) { - return; - } - if (newVal !== oldVal) { - self.hasChanges = true; - } - oldVal = newVal; - }); - }; - - self.allowedValuesChanged = function () { - // Default to true on any change callback as this is how it is - // done for the save button - self.hasChanges = true; - }; - - self.removeFHIRResourcePropertyPath = ko.observable(prop.removeFHIRResourcePropertyPath || false); - let subTitle; - if (toggles.toggleEnabled("CASE_IMPORT_DATA_DICTIONARY_VALIDATION")) { - subTitle = gettext("When importing data, CommCare will not save a row if its cells don't match these valid values."); - } else { - subTitle = gettext("Help colleagues upload correct data into case properties by listing the valid values here."); } - self.allowedValues = uiElementKeyValueList.new( - String(Math.random()).slice(2), /* guid */ - interpolate('Edit valid values for "%s"', [name]), /* modalTitle */ - subTitle, /* subTitle */ - {"key": gettext("valid value"), "value": gettext("description")}, /* placeholders */ - 10 /* maxDisplay */ - ); - self.allowedValues.val(prop.allowed_values); - if (initialPageData.get('read_only_mode')) { - self.allowedValues.setEdit(false); - } - self.$allowedValues = self.allowedValues.ui; + }; - self.toggle = function () { - self.expanded(!self.expanded()); - }; + return self; +}; - self.deprecateProperty = function () { - if (toggles.toggleEnabled('MICROPLANNING') && self.isGeoCaseProp()) { - self.confirmGeospatialDeprecation(); - } else { - self.deprecated(true); - } - }; +var groupsViewModel = function ( + caseType, + id, + name, + description, + deprecated, + changeSaveButton +) { + var self = {}; + self.id = id; + self.name = ko.observable(name); + self.description = ko.observable(description); + self.caseType = caseType; + self.properties = ko.observableArray(); + self.newPropertyName = ko.observable(); + self.expanded = ko.observable(true); + self.toggleExpanded = () => self.expanded(!self.expanded()); + self.deprecated = deprecated; + // Ensures that groups are not directly hidden on clicking the deprecated button + self.toBeDeprecated = ko.observable(deprecated || false); + self.deprecateGroup = function () { + self.toBeDeprecated(true); + }; - self.confirmGeospatialDeprecation = function () { - const $modal = $("#deprecate-geospatial-prop-modal").modal('show'); - $("#deprecate-geospatial-prop-btn").off('click').on('click', function () { - self.deprecated(true); - $modal.modal('hide'); - }); - }; + self.restoreGroup = function () { + self.toBeDeprecated(false); + }; + // Show a warning that properties will be transferred to "No Group" section on deprecating a group. + self.showGroupPropertyTransferWarning = ko.computed(function () { + return self.toBeDeprecated() && !deprecated && self.properties().length > 0; + }); - self.restoreProperty = function () { - self.deprecated(false); - }; + self.newCaseProperty = function () { + if (self.newPropertyName().trim()) { + const prop = { + 'name': self.newPropertyName(), + 'label': self.newPropertyName(), + 'allowedValues': {}, + }; + let propObj = propertyListItem( + prop, + false, + self.name(), + self.caseType, + false, + self.name(), + changeSaveButton + ); + self.newPropertyName(undefined); + self.properties.push(propObj); + } + }; - self.removePath = function () { - self.removeFHIRResourcePropertyPath(true); - // set back to original to delete the corresponding entry on save - self.fhirResourcePropPath(self.originalResourcePropPath); - }; + self.name.subscribe(changeSaveButton); + self.description.subscribe(changeSaveButton); + self.toBeDeprecated.subscribe(changeSaveButton); + self.properties.subscribe(changeSaveButton); + + return self; +}; + +var propertyListItem = function ( + prop, + isGroup, + groupName, + caseType, + isGeoCaseProp, + loadedGroup, + changeSaveButton +) { + var self = {}; + self.id = prop.id; + self.name = prop.name; + self.label = ko.observable(prop.label); + self.expanded = ko.observable(true); + self.isGroup = isGroup; + self.group = ko.observable(groupName); + self.caseType = caseType; + self.dataType = ko.observable(prop.data_type); + self.description = ko.observable(prop.description); + self.fhirResourcePropPath = ko.observable(prop.fhir_resource_prop_path); + self.originalResourcePropPath = prop.fhir_resource_prop_path; + self.deprecated = ko.observable(prop.deprecated || false); + self.isGeoCaseProp = ko.observable(isGeoCaseProp); + self.isSafeToDelete = ko.observable(prop.is_safe_to_delete); + self.deleted = ko.observable(false); + self.hasChanges = false; + self.index = prop.index; + self.loadedGroup = loadedGroup; // The group this case property is part of when page was loaded. Used to identify group changes + + self.trackObservableChange = function (observable) { + // Keep track of old val for observable, and subscribe to new changes. + // We can then identify when the val has changed. + let oldVal = observable(); + observable.subscribe(function (newVal) { + if (!newVal && !oldVal) { + return; + } + if (newVal !== oldVal) { + self.hasChanges = true; + } + oldVal = newVal; + }); + }; - self.restorePath = function () { - self.removeFHIRResourcePropertyPath(false); - }; + self.allowedValuesChanged = function () { + // Default to true on any change callback as this is how it is + // done for the save button + self.hasChanges = true; + }; - self.canHaveAllowedValues = ko.computed(function () { - return self.dataType() === 'select'; - }); + self.removeFHIRResourcePropertyPath = ko.observable(prop.removeFHIRResourcePropertyPath || false); + let subTitle; + if (toggles.toggleEnabled("CASE_IMPORT_DATA_DICTIONARY_VALIDATION")) { + subTitle = gettext("When importing data, CommCare will not save a row if its cells don't match these valid values."); + } else { + subTitle = gettext("Help colleagues upload correct data into case properties by listing the valid values here."); + } + self.allowedValues = uiElementKeyValueList.new( + String(Math.random()).slice(2), /* guid */ + interpolate('Edit valid values for "%s"', [name]), /* modalTitle */ + subTitle, /* subTitle */ + {"key": gettext("valid value"), "value": gettext("description")}, /* placeholders */ + 10 /* maxDisplay */ + ); + self.allowedValues.val(prop.allowed_values); + if (initialPageData.get('read_only_mode')) { + self.allowedValues.setEdit(false); + } + self.$allowedValues = self.allowedValues.ui; + + self.toggle = function () { + self.expanded(!self.expanded()); + }; - self.confirmDeleteProperty = function () { - const $modal = $("#delete-case-prop-modal").modal('show'); - $("#delete-case-prop-name").text(self.name); - $("#delete-case-prop-btn").off("click").on("click", () => { - self.deleted(true); - $modal.modal('hide'); - }); - }; - - subscribePropObservable(self.description); - subscribePropObservable(self.label); - subscribePropObservable(self.fhirResourcePropPath); - subscribePropObservable(self.dataType); - subscribePropObservable(self.deprecated); - subscribePropObservable(self.deleted); - subscribePropObservable(self.removeFHIRResourcePropertyPath); - self.allowedValues.on('change', changeSaveButton); - self.allowedValues.on('change', self.allowedValuesChanged); - - function subscribePropObservable(prop) { - prop.subscribe(changeSaveButton); - self.trackObservableChange(prop); + self.deprecateProperty = function () { + if (toggles.toggleEnabled('MICROPLANNING') && self.isGeoCaseProp()) { + self.confirmGeospatialDeprecation(); + } else { + self.deprecated(true); } + }; - return self; - }; - - var dataDictionaryModel = function (dataUrl, casePropertyUrl, typeChoices, fhirResourceTypes) { - var self = {}; - self.caseTypes = ko.observableArray(); - self.activeCaseType = ko.observable(); - self.fhirResourceType = ko.observable(); - self.removefhirResourceType = ko.observable(false); - self.newPropertyName = ko.observable(); - self.newGroupName = ko.observable(); - self.showAll = ko.observable(false); - self.availableDataTypes = typeChoices; - self.fhirResourceTypes = ko.observableArray(fhirResourceTypes); - - const params = new URLSearchParams(document.location.search); - self.showDeprecatedCaseTypes = ko.observable(params.get("load_deprecated_case_types") !== null); - - // Elements with this class have a hidden class to hide them on page load. If we don't do this, then the elements - // will flash on the page for a bit while the KO bindings are being applied. - $(".deprecate-case-type").removeClass('hidden'); - - self.saveButton = hqMain.initSaveButton({ - unsavedMessage: gettext("You have unsaved changes to your data dictionary."), - save: function () { - const groups = []; - const properties = []; - _.each(self.activeCaseTypeData(), function (group, index) { - if (group.name() !== "") { - let groupData = { - 'caseType': group.caseType, - 'id': group.id, - 'name': group.name(), - 'description': group.description(), - 'index': index, - 'deprecated': group.toBeDeprecated(), - }; - groups.push(groupData); - } - - _.each(group.properties(), function (element, index) { - if (element.deleted() && !element.id) { - return; - } - const propIndex = group.toBeDeprecated() ? 0 : index; - const propGroup = group.toBeDeprecated() ? "" : group.name(); - if (!element.hasChanges - && propIndex === element.index - && element.loadedGroup === propGroup) { - return; - } - - const allowedValues = element.allowedValues.val(); - let pureAllowedValues = {}; - for (const key in allowedValues) { - pureAllowedValues[DOMPurify.sanitize(key)] = DOMPurify.sanitize(allowedValues[key]); - } - var data = { - 'caseType': element.caseType, - 'name': element.name, - 'label': element.label() || element.name, - 'index': propIndex, - 'data_type': element.dataType(), - 'group': propGroup, - 'description': element.description(), - 'fhir_resource_prop_path': ( - element.fhirResourcePropPath() ? element.fhirResourcePropPath().trim() : element.fhirResourcePropPath()), - 'deprecated': element.deprecated(), - 'deleted': element.deleted(), - 'removeFHIRResourcePropertyPath': element.removeFHIRResourcePropertyPath(), - 'allowed_values': pureAllowedValues, - }; - properties.push(data); - }); - }); - self.saveButton.ajax({ - url: casePropertyUrl, - type: 'POST', - dataType: 'JSON', - data: { - 'groups': JSON.stringify(groups), - 'properties': JSON.stringify(properties), - 'fhir_resource_type': self.fhirResourceType(), - 'remove_fhir_resource_type': self.removefhirResourceType(), - 'case_type': self.activeCaseType(), - }, - success: function () { - window.location.reload(); - }, - // Error handling is managed by SaveButton logic in main.js - }); - }, + self.confirmGeospatialDeprecation = function () { + const $modal = $("#deprecate-geospatial-prop-modal").modal('show'); + $("#deprecate-geospatial-prop-btn").off('click').on('click', function () { + self.deprecated(true); + $modal.modal('hide'); }); + }; - var changeSaveButton = function () { - self.saveButton.fire('change'); - }; - - const resetSaveButton = function () { - self.saveButton.setState('saved'); - }; - - self.init = function (callback) { - // Get list of case types - $.getJSON(dataUrl, {load_deprecated_case_types: self.showDeprecatedCaseTypes()}) - .done(function (data) { - _.each(data.case_types, function (caseTypeData) { - var caseTypeObj = caseType( - caseTypeData.name, - caseTypeData.fhir_resource_type, - caseTypeData.is_deprecated, - caseTypeData.module_count, - data.geo_case_property, - caseTypeData.is_safe_to_delete, - changeSaveButton, - resetSaveButton, - dataUrl - ); - self.caseTypes.push(caseTypeObj); - }); - if ( - self.caseTypes().length - // Check that hash navigation has not already loaded the first case type - && self.caseTypes()[0] !== self.getHashNavigationCaseType() - ) { - // `self.goToCaseType()` calls `caseType.fetchCaseProperties()` - // to fetch the case properties of the first case type - let caseType = self.caseTypes()[0]; - self.goToCaseType(caseType); - } - self.fhirResourceType.subscribe(changeSaveButton); - self.removefhirResourceType.subscribe(changeSaveButton); - callback(); - }); - }; + self.restoreProperty = function () { + self.deprecated(false); + }; - self.getHashNavigationCaseType = function () { - let fullHash = window.location.hash.split('?')[0], - hash = fullHash.substring(1); - return _.find(self.caseTypes(), function (prop) { - return prop.name === hash; - }); - }; + self.removePath = function () { + self.removeFHIRResourcePropertyPath(true); + // set back to original to delete the corresponding entry on save + self.fhirResourcePropPath(self.originalResourcePropPath); + }; - self.getActiveCaseType = function () { - return _.find(self.caseTypes(), function (prop) { - return prop.name === self.activeCaseType(); - }); - }; + self.restorePath = function () { + self.removeFHIRResourcePropertyPath(false); + }; - self.getCaseTypeGroupsObservable = function () { - let caseType = self.getActiveCaseType(); - if (caseType) { - return caseType.groups; // The observable, not its value - } - }; - - self.activeCaseTypeData = function () { - const groupsObs = self.getCaseTypeGroupsObservable(); - return (groupsObs) ? groupsObs() : []; - }; - - self.isActiveCaseTypeDeprecated = function () { - const activeCaseType = self.getActiveCaseType(); - return (activeCaseType) ? activeCaseType.deprecated : false; - }; - - self.canDeleteActiveCaseType = function () { - const activeCaseType = self.getActiveCaseType(); - return (activeCaseType) ? activeCaseType.canDelete : false; - }; - - self.activeCaseTypeModuleCount = function () { - const activeCaseType = self.getActiveCaseType(); - return (activeCaseType) ? activeCaseType.appCount : 0; - }; - - self.deprecateCaseType = function () { - self.deprecateOrRestoreCaseType(true); - }; - - self.restoreCaseType = function () { - self.deprecateOrRestoreCaseType(false); - }; - - self.deprecateOrRestoreCaseType = function (shouldDeprecate) { - let activeCaseType = self.getActiveCaseType(); - if (!activeCaseType) { - return; - } + self.canHaveAllowedValues = ko.computed(function () { + return self.dataType() === 'select'; + }); - activeCaseType.deprecated = shouldDeprecate; - $("#case-type-error").hide(); - $.ajax({ - url: initialPageData.reverse('deprecate_or_restore_case_type', activeCaseType.name), - method: 'POST', + self.confirmDeleteProperty = function () { + const $modal = $("#delete-case-prop-modal").modal('show'); + $("#delete-case-prop-name").text(self.name); + $("#delete-case-prop-btn").off("click").on("click", () => { + self.deleted(true); + $modal.modal('hide'); + }); + }; + + subscribePropObservable(self.description); + subscribePropObservable(self.label); + subscribePropObservable(self.fhirResourcePropPath); + subscribePropObservable(self.dataType); + subscribePropObservable(self.deprecated); + subscribePropObservable(self.deleted); + subscribePropObservable(self.removeFHIRResourcePropertyPath); + self.allowedValues.on('change', changeSaveButton); + self.allowedValues.on('change', self.allowedValuesChanged); + + function subscribePropObservable(prop) { + prop.subscribe(changeSaveButton); + self.trackObservableChange(prop); + } + + return self; +}; + +var dataDictionaryModel = function (dataUrl, casePropertyUrl, typeChoices, fhirResourceTypes) { + var self = {}; + self.caseTypes = ko.observableArray(); + self.activeCaseType = ko.observable(); + self.fhirResourceType = ko.observable(); + self.removefhirResourceType = ko.observable(false); + self.newPropertyName = ko.observable(); + self.newGroupName = ko.observable(); + self.showAll = ko.observable(false); + self.availableDataTypes = typeChoices; + self.fhirResourceTypes = ko.observableArray(fhirResourceTypes); + + const params = new URLSearchParams(document.location.search); + self.showDeprecatedCaseTypes = ko.observable(params.get("load_deprecated_case_types") !== null); + + // Elements with this class have a hidden class to hide them on page load. If we don't do this, then the elements + // will flash on the page for a bit while the KO bindings are being applied. + $(".deprecate-case-type").removeClass('hidden'); + + self.saveButton = hqMain.initSaveButton({ + unsavedMessage: gettext("You have unsaved changes to your data dictionary."), + save: function () { + const groups = []; + const properties = []; + _.each(self.activeCaseTypeData(), function (group, index) { + if (group.name() !== "") { + let groupData = { + 'caseType': group.caseType, + 'id': group.id, + 'name': group.name(), + 'description': group.description(), + 'index': index, + 'deprecated': group.toBeDeprecated(), + }; + groups.push(groupData); + } + + _.each(group.properties(), function (element, index) { + if (element.deleted() && !element.id) { + return; + } + const propIndex = group.toBeDeprecated() ? 0 : index; + const propGroup = group.toBeDeprecated() ? "" : group.name(); + if (!element.hasChanges + && propIndex === element.index + && element.loadedGroup === propGroup) { + return; + } + + const allowedValues = element.allowedValues.val(); + let pureAllowedValues = {}; + for (const key in allowedValues) { + pureAllowedValues[DOMPurify.sanitize(key)] = DOMPurify.sanitize(allowedValues[key]); + } + var data = { + 'caseType': element.caseType, + 'name': element.name, + 'label': element.label() || element.name, + 'index': propIndex, + 'data_type': element.dataType(), + 'group': propGroup, + 'description': element.description(), + 'fhir_resource_prop_path': ( + element.fhirResourcePropPath() ? element.fhirResourcePropPath().trim() : element.fhirResourcePropPath()), + 'deprecated': element.deprecated(), + 'deleted': element.deleted(), + 'removeFHIRResourcePropertyPath': element.removeFHIRResourcePropertyPath(), + 'allowed_values': pureAllowedValues, + }; + properties.push(data); + }); + }); + self.saveButton.ajax({ + url: casePropertyUrl, + type: 'POST', + dataType: 'JSON', data: { - 'is_deprecated': shouldDeprecate, + 'groups': JSON.stringify(groups), + 'properties': JSON.stringify(properties), + 'fhir_resource_type': self.fhirResourceType(), + 'remove_fhir_resource_type': self.removefhirResourceType(), + 'case_type': self.activeCaseType(), }, success: function () { - window.location.reload(true); - }, - error: function () { - $("#case-type-error").show(); + window.location.reload(); }, + // Error handling is managed by SaveButton logic in main.js }); - }; + }, + }); - self.deleteCaseType = function () { - $("#case-type-error").hide(); - $.ajax({ - url: initialPageData.reverse('delete_case_type', self.getActiveCaseType().name), - method: 'POST', - success: function () { - window.location.href = initialPageData.reverse('data_dictionary'); - }, - error: function () { - $("#case-type-error").show(); - }, - }); - }; + var changeSaveButton = function () { + self.saveButton.fire('change'); + }; - self.goToCaseType = function (caseType) { - if (self.saveButton.state === 'save') { - var dialog = confirm(gettext('You have unsaved changes to this case type. Are you sure you would like to continue?')); - if (!dialog) { - return; + const resetSaveButton = function () { + self.saveButton.setState('saved'); + }; + + self.init = function (callback) { + // Get list of case types + $.getJSON(dataUrl, {load_deprecated_case_types: self.showDeprecatedCaseTypes()}) + .done(function (data) { + _.each(data.case_types, function (caseTypeData) { + var caseTypeObj = caseType( + caseTypeData.name, + caseTypeData.fhir_resource_type, + caseTypeData.is_deprecated, + caseTypeData.module_count, + data.geo_case_property, + caseTypeData.is_safe_to_delete, + changeSaveButton, + resetSaveButton, + dataUrl + ); + self.caseTypes.push(caseTypeObj); + }); + if ( + self.caseTypes().length + // Check that hash navigation has not already loaded the first case type + && self.caseTypes()[0] !== self.getHashNavigationCaseType() + ) { + // `self.goToCaseType()` calls `caseType.fetchCaseProperties()` + // to fetch the case properties of the first case type + let caseType = self.caseTypes()[0]; + self.goToCaseType(caseType); } - } - caseType.fetchCaseProperties(); - self.activeCaseType(caseType.name); - self.fhirResourceType(caseType.fhirResourceType()); - self.removefhirResourceType(false); - self.saveButton.setState('saved'); - }; - - function isNameValid(nameStr) { - // First character must be a letter, and the entire name can only contain letters, numbers, '-', and '_' - const pattern = /^[a-zA-Z][a-zA-Z0-9-_]*$/; - return pattern.test(nameStr); + self.fhirResourceType.subscribe(changeSaveButton); + self.removefhirResourceType.subscribe(changeSaveButton); + callback(); + }); + }; + + self.getHashNavigationCaseType = function () { + let fullHash = window.location.hash.split('?')[0], + hash = fullHash.substring(1); + return _.find(self.caseTypes(), function (prop) { + return prop.name === hash; + }); + }; + + self.getActiveCaseType = function () { + return _.find(self.caseTypes(), function (prop) { + return prop.name === self.activeCaseType(); + }); + }; + + self.getCaseTypeGroupsObservable = function () { + let caseType = self.getActiveCaseType(); + if (caseType) { + return caseType.groups; // The observable, not its value } + }; - self.newPropertyNameValid = function (name) { - if (!name) { - return true; - } - return isNameValid(name); - }; + self.activeCaseTypeData = function () { + const groupsObs = self.getCaseTypeGroupsObservable(); + return (groupsObs) ? groupsObs() : []; + }; - self.newPropertyNameUnique = function (name) { - if (!name) { - return true; - } + self.isActiveCaseTypeDeprecated = function () { + const activeCaseType = self.getActiveCaseType(); + return (activeCaseType) ? activeCaseType.deprecated : false; + }; - const propertyNameFormatted = name.toLowerCase().trim(); - const activeCaseTypeData = self.activeCaseTypeData(); - for (const group of activeCaseTypeData) { - if (group.properties().find(v => v.name.toLowerCase() === propertyNameFormatted)) { - return false; - } - } - return true; - }; + self.canDeleteActiveCaseType = function () { + const activeCaseType = self.getActiveCaseType(); + return (activeCaseType) ? activeCaseType.canDelete : false; + }; - self.newGroupNameValid = ko.computed(function () { - if (!self.newGroupName()) { - return true; - } - return isNameValid(self.newGroupName()); - }); + self.activeCaseTypeModuleCount = function () { + const activeCaseType = self.getActiveCaseType(); + return (activeCaseType) ? activeCaseType.appCount : 0; + }; - self.newGroupNameUnique = ko.computed(function () { - if (!self.newGroupName()) { - return true; - } + self.deprecateCaseType = function () { + self.deprecateOrRestoreCaseType(true); + }; - const groupNameFormatted = self.newGroupName().toLowerCase().trim(); - const activeCaseTypeData = self.activeCaseTypeData(); - for (const group of activeCaseTypeData) { - if (group.name().toLowerCase() === groupNameFormatted) { - return false; - } - } - return true; - }); + self.restoreCaseType = function () { + self.deprecateOrRestoreCaseType(false); + }; - self.newGroup = function () { - if (_.isString(self.newGroupName()) && self.newGroupName().trim()) { - var group = groupsViewModel( - self.activeCaseType(), - null, - self.newGroupName(), - '', - false, - changeSaveButton - ); - let groupsObs = self.getCaseTypeGroupsObservable(); - groupsObs.push(group); // TODO: Broken for computed value - self.newGroupName(undefined); - } - }; + self.deprecateOrRestoreCaseType = function (shouldDeprecate) { + let activeCaseType = self.getActiveCaseType(); + if (!activeCaseType) { + return; + } - self.toggleGroup = function (group) { - group.toggle(); - var groupIndex = _.findIndex(self.casePropertyList(), function (element) { - return element.name === group.name; - }); - var i = groupIndex + 1; - var next = self.casePropertyList()[i]; - while (next && !next.isGroup) { - next.toggle(); - i++; - next = self.casePropertyList()[i]; - } - }; - - self.showDeprecated = function () { - self.showAll(true); - }; - - self.hideDeprecated = function () { - self.showAll(false); - }; - - self.removeResourceType = function () { - self.removefhirResourceType(true); - }; - - self.restoreResourceType = function () { - self.removefhirResourceType(false); - }; - - self.toggleShowDeprecatedCaseTypes = function () { - self.showDeprecatedCaseTypes(!self.showDeprecatedCaseTypes()); - const pageUrl = new URL(window.location.href); - if (self.showDeprecatedCaseTypes()) { - pageUrl.searchParams.append('load_deprecated_case_types', true); - } else { - pageUrl.searchParams.delete('load_deprecated_case_types'); - } - window.location.href = pageUrl; - }; + activeCaseType.deprecated = shouldDeprecate; + $("#case-type-error").hide(); + $.ajax({ + url: initialPageData.reverse('deprecate_or_restore_case_type', activeCaseType.name), + method: 'POST', + data: { + 'is_deprecated': shouldDeprecate, + }, + success: function () { + window.location.reload(true); + }, + error: function () { + $("#case-type-error").show(); + }, + }); + }; - // CREATE workflow - self.name = ko.observable("").extend({ - rateLimit: { method: "notifyWhenChangesStop", timeout: 400 }, + self.deleteCaseType = function () { + $("#case-type-error").hide(); + $.ajax({ + url: initialPageData.reverse('delete_case_type', self.getActiveCaseType().name), + method: 'POST', + success: function () { + window.location.href = initialPageData.reverse('data_dictionary'); + }, + error: function () { + $("#case-type-error").show(); + }, }); + }; - self.nameValid = ko.observable(false); - self.nameUnique = ko.observable(false); - self.nameChecked = ko.observable(false); - self.name.subscribe((value) => { - if (!value) { - self.nameChecked(false); + self.goToCaseType = function (caseType) { + if (self.saveButton.state === 'save') { + var dialog = confirm(gettext('You have unsaved changes to this case type. Are you sure you would like to continue?')); + if (!dialog) { return; } - let existing = _.find(self.caseTypes(), function (prop) { - return prop.name === value; - }); - self.nameUnique(!existing); - self.nameValid(isNameValid(self.name())); - self.nameChecked(true); - }); + } + caseType.fetchCaseProperties(); + self.activeCaseType(caseType.name); + self.fhirResourceType(caseType.fhirResourceType()); + self.removefhirResourceType(false); + self.saveButton.setState('saved'); + }; + + function isNameValid(nameStr) { + // First character must be a letter, and the entire name can only contain letters, numbers, '-', and '_' + const pattern = /^[a-zA-Z][a-zA-Z0-9-_]*$/; + return pattern.test(nameStr); + } - self.formCreateCaseTypeSent = ko.observable(false); - self.submitCreate = function () { - self.formCreateCaseTypeSent(true); + self.newPropertyNameValid = function (name) { + if (!name) { return true; - }; + } + return isNameValid(name); + }; - self.clearForm = function () { - $("#create-case-type-form").trigger("reset"); - self.name(""); - self.nameValid(false); - self.nameUnique(false); - self.nameChecked(false); + self.newPropertyNameUnique = function (name) { + if (!name) { return true; - }; + } + + const propertyNameFormatted = name.toLowerCase().trim(); + const activeCaseTypeData = self.activeCaseTypeData(); + for (const group of activeCaseTypeData) { + if (group.properties().find(v => v.name.toLowerCase() === propertyNameFormatted)) { + return false; + } + } + return true; + }; + + self.newGroupNameValid = ko.computed(function () { + if (!self.newGroupName()) { + return true; + } + return isNameValid(self.newGroupName()); + }); + + self.newGroupNameUnique = ko.computed(function () { + if (!self.newGroupName()) { + return true; + } - $(document).on('hide.bs.modal', () => { - return self.clearForm(); + const groupNameFormatted = self.newGroupName().toLowerCase().trim(); + const activeCaseTypeData = self.activeCaseTypeData(); + for (const group of activeCaseTypeData) { + if (group.name().toLowerCase() === groupNameFormatted) { + return false; + } + } + return true; + }); + + self.newGroup = function () { + if (_.isString(self.newGroupName()) && self.newGroupName().trim()) { + var group = groupsViewModel( + self.activeCaseType(), + null, + self.newGroupName(), + '', + false, + changeSaveButton + ); + let groupsObs = self.getCaseTypeGroupsObservable(); + groupsObs.push(group); // TODO: Broken for computed value + self.newGroupName(undefined); + } + }; + + self.toggleGroup = function (group) { + group.toggle(); + var groupIndex = _.findIndex(self.casePropertyList(), function (element) { + return element.name === group.name; }); + var i = groupIndex + 1; + var next = self.casePropertyList()[i]; + while (next && !next.isGroup) { + next.toggle(); + i++; + next = self.casePropertyList()[i]; + } + }; - return self; + self.showDeprecated = function () { + self.showAll(true); }; - $(function () { - var dataUrl = initialPageData.reverse('data_dictionary_json_case_types'), - casePropertyUrl = initialPageData.reverse('update_case_property'), - typeChoices = initialPageData.get('typeChoices'), - fhirResourceTypes = initialPageData.get('fhirResourceTypes'), - viewModel = dataDictionaryModel(dataUrl, casePropertyUrl, typeChoices, fhirResourceTypes); + self.hideDeprecated = function () { + self.showAll(false); + }; - function doHashNavigation() { - let caseType = viewModel.getHashNavigationCaseType(); - if (caseType) { - viewModel.goToCaseType(caseType); - } + self.removeResourceType = function () { + self.removefhirResourceType(true); + }; + + self.restoreResourceType = function () { + self.removefhirResourceType(false); + }; + + self.toggleShowDeprecatedCaseTypes = function () { + self.showDeprecatedCaseTypes(!self.showDeprecatedCaseTypes()); + const pageUrl = new URL(window.location.href); + if (self.showDeprecatedCaseTypes()) { + pageUrl.searchParams.append('load_deprecated_case_types', true); + } else { + pageUrl.searchParams.delete('load_deprecated_case_types'); } + window.location.href = pageUrl; + }; - window.onhashchange = doHashNavigation; + // CREATE workflow + self.name = ko.observable("").extend({ + rateLimit: { method: "notifyWhenChangesStop", timeout: 400 }, + }); - viewModel.init(doHashNavigation); - $('#hq-content').parent().koApplyBindings(viewModel); - $('#download-dict').click(function () { - googleAnalytics.track.event('Data Dictionary', 'downloaded data dictionary'); + self.nameValid = ko.observable(false); + self.nameUnique = ko.observable(false); + self.nameChecked = ko.observable(false); + self.name.subscribe((value) => { + if (!value) { + self.nameChecked(false); + return; + } + let existing = _.find(self.caseTypes(), function (prop) { + return prop.name === value; }); + self.nameUnique(!existing); + self.nameValid(isNameValid(self.name())); + self.nameChecked(true); + }); + + self.formCreateCaseTypeSent = ko.observable(false); + self.submitCreate = function () { + self.formCreateCaseTypeSent(true); + return true; + }; + + self.clearForm = function () { + $("#create-case-type-form").trigger("reset"); + self.name(""); + self.nameValid(false); + self.nameUnique(false); + self.nameChecked(false); + return true; + }; + $(document).on('hide.bs.modal', () => { + return self.clearForm(); }); + + return self; +}; + +$(function () { + var dataUrl = initialPageData.reverse('data_dictionary_json_case_types'), + casePropertyUrl = initialPageData.reverse('update_case_property'), + typeChoices = initialPageData.get('typeChoices'), + fhirResourceTypes = initialPageData.get('fhirResourceTypes'), + viewModel = dataDictionaryModel(dataUrl, casePropertyUrl, typeChoices, fhirResourceTypes); + + function doHashNavigation() { + let caseType = viewModel.getHashNavigationCaseType(); + if (caseType) { + viewModel.goToCaseType(caseType); + } + } + + window.onhashchange = doHashNavigation; + + viewModel.init(doHashNavigation); + $('#hq-content').parent().koApplyBindings(viewModel); + $('#download-dict').click(function () { + googleAnalytics.track.event('Data Dictionary', 'downloaded data dictionary'); + }); + }); From 6b38ca582e1fc0d28adab2feea3103e652357e4e Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 11:27:20 -0500 Subject: [PATCH 3/7] Added support for one-line arguments Handle code like this: https://github.com/dimagi/commcare-hq/blob/3153f40b2abf3c3f832fa9f0d7eeffcaf3ae36ea/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js#L1-L9 --- .../management/commands/hqdefine_to_esm.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py index 36dbfb4242dd..7ec7c4b2186a 100644 --- a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -44,6 +44,8 @@ def handle(self, filename, **options): line_index += 1 if self._update_parser_location(line): + if self.in_arguments: + arguments.extend(self._parse_one_line_arguments(line)) continue if self.in_imports: imports.append(self._parse_import(line)) @@ -103,6 +105,14 @@ def _parse_item(self, pattern, line, description=""): return item logger.warning(f"Could not parse {description} from line: {line}") + def _parse_one_line_arguments(self, line): + match = re.search(r'function\s*\((.*)\)', line) + if match: + self._update_parser_location(line) + arguments = match.group(1) + return [arg.strip() for arg in arguments.split(',')] + return [] + def _init_parser(self): self.in_hqdefine_block = True self.in_imports = False @@ -119,18 +129,18 @@ def _fail_parsing(self): def _update_parser_location(self, line): if self._is_use_strict(line): - return True + return line if self._is_hqdefine_open(line): self.in_imports = True - return True + return line if self.in_imports and 'function' in line: self.in_imports = False self.in_arguments = True - return True + return line if self.in_arguments and ')' in line: self.in_arguments = False self.in_hqdefine_block = False - return True + return line return False def _dedent(self, line): From d2b84079bfa1578ceecc45cf3889936cf6e3f217 Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 11:33:14 -0500 Subject: [PATCH 4/7] Variable renames Decided there's no point marking all the functions "private". --- .../management/commands/hqdefine_to_esm.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py index 7ec7c4b2186a..e3cc96e9c7cf 100644 --- a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -19,7 +19,7 @@ class Command(BaseCommand): Also attempts to remove "use strict" directive, because modules automatically use strict. ''' - dedent = 4 + dedent_size = 4 def add_arguments(self, parser): parser.add_argument('filename', help='File to migrate') @@ -32,25 +32,25 @@ def handle(self, filename, **options): lines = fin.readlines() # Parse imports - self._init_parser() + self.init_parser() imports = [] arguments = [] line_index = 0 while self.in_hqdefine_block: if line_index >= len(lines): - self._fail_parsing() + self.fail_parsing() line = lines[line_index] line_index += 1 - if self._update_parser_location(line): + if self.update_parser_location(line): if self.in_arguments: - arguments.extend(self._parse_one_line_arguments(line)) + arguments.extend(self.parse_one_line_arguments(line)) continue if self.in_imports: - imports.append(self._parse_import(line)) + imports.append(self.parse_import(line)) elif self.in_arguments: - arguments.append(self._parse_argument(line)) + arguments.append(self.parse_argument(line)) # Rewrite file with open(filename, 'w') as fout: @@ -73,29 +73,29 @@ def handle(self, filename, **options): # Write remaining file for line in lines[line_index:]: - if self._is_use_strict(line) or self._is_hqdefine_close(line): + if self.is_use_strict(line) or self.is_hqdefine_close(line): continue - fout.write(self._dedent(line)) + fout.write(self.dedent(line)) logger.info(f"Rewrote {filename}") - def _is_use_strict(self, line): + def is_use_strict(self, line): return 'use strict' in line - def _is_hqdefine_open(self, line): + def is_hqdefine_open(self, line): return 'hqDefine' in line - def _is_hqdefine_close(self, line): + def is_hqdefine_close(self, line): return line.startswith("});") - def _parse_import(self, line): - return self._parse_item(IMPORT_PATTERN, line, "import") + def parse_import(self, line): + return self.parse_item(IMPORT_PATTERN, line, "import") - def _parse_argument(self, line): - return self._parse_item(ARGUMENT_PATTERN, line, "argument") + def parse_argument(self, line): + return self.parse_item(ARGUMENT_PATTERN, line, "argument") - def _parse_item(self, pattern, line, description=""): + def parse_item(self, pattern, line, description=""): match = re.search(pattern, line) if match: item = match.group(1) @@ -105,20 +105,20 @@ def _parse_item(self, pattern, line, description=""): return item logger.warning(f"Could not parse {description} from line: {line}") - def _parse_one_line_arguments(self, line): + def parse_one_line_arguments(self, line): match = re.search(r'function\s*\((.*)\)', line) if match: - self._update_parser_location(line) + self.update_parser_location(line) arguments = match.group(1) return [arg.strip() for arg in arguments.split(',')] return [] - def _init_parser(self): + def init_parser(self): self.in_hqdefine_block = True self.in_imports = False self.in_arguments = False - def _fail_parsing(self): + def fail_parsing(self): if self.in_arguments: status = "in arguments block" elif self.in_imports: @@ -127,10 +127,10 @@ def _fail_parsing(self): status = "before imports block" raise CommandError(f"Could not parse file. Ran out of code {status}.") - def _update_parser_location(self, line): - if self._is_use_strict(line): + def update_parser_location(self, line): + if self.is_use_strict(line): return line - if self._is_hqdefine_open(line): + if self.is_hqdefine_open(line): self.in_imports = True return line if self.in_imports and 'function' in line: @@ -143,7 +143,7 @@ def _update_parser_location(self, line): return line return False - def _dedent(self, line): - if line.startswith(" " * self.dedent): - line = line[self.dedent:] + def dedent(self, line): + if line.startswith(" " * self.dedent_size): + line = line[self.dedent_size:] return line From 61773cb5fdad3dc39a186e27546b171bebafdaec Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 11:48:30 -0500 Subject: [PATCH 5/7] Added support for arguments list without final trailing comma --- corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py index e3cc96e9c7cf..b0e7d8cf1b1b 100644 --- a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -7,7 +7,7 @@ logger = logging.getLogger('hqdefine_to_esm') -IMPORT_PATTERN = r'\s*["\']([^,]*)["\'],$' +IMPORT_PATTERN = r'\s*["\']([^,]*)["\'],?$' ARGUMENT_PATTERN = r'\s*([^,]*),?$' From 69035c4210c7a0ffbef42112af4ebb65e3445390 Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 12:13:03 -0500 Subject: [PATCH 6/7] Added support for comments --- .../management/commands/hqdefine_to_esm.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py index b0e7d8cf1b1b..99372833c469 100644 --- a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -55,20 +55,30 @@ def handle(self, filename, **options): # Rewrite file with open(filename, 'w') as fout: # Move commcarehq to the top - if "commcarehq" in imports: + if "commcarehq" in [i[0] for i in imports]: fout.write('import "commcarehq";\n') # Add imports, ESM-style - for index, dependency in enumerate(imports): + for index, dependency_pair in enumerate(imports): + (dependency, comment) = dependency_pair if dependency is None or dependency == "commcarehq": continue if index < len(arguments): - out = f'import {arguments[index]} from "{dependency}";\n' + (argument, argument_comment) = arguments[index] + if argument_comment: + if comment: + comment = f"{comment}; {argument_comment}" + else: + comment = argument_comment + out = f'import {argument} from "{dependency}";' else: - out = f'import "{dependency}";\n' + out = f'import "{dependency}";' - fout.write(out) + if comment: + out += ' // ' + comment + + fout.write(out + '\n') fout.write("\n") # Write remaining file @@ -96,13 +106,20 @@ def parse_argument(self, line): return self.parse_item(ARGUMENT_PATTERN, line, "argument") def parse_item(self, pattern, line, description=""): + match = re.search(r'^(.*\S)\s*\/\/\s*(.*)$', line) + if match: + line = match.group(1) + comment = match.group(2) + else: + comment = '' + match = re.search(pattern, line) if match: item = match.group(1) logger.info(f"Found {description}: {item}") if item.endswith("\n"): item = item[:-1] - return item + return (item, comment) logger.warning(f"Could not parse {description} from line: {line}") def parse_one_line_arguments(self, line): From 9d9266181313d8474564dcf8202f59487a98b4ea Mon Sep 17 00:00:00 2001 From: Jenny Schweers Date: Tue, 28 Jan 2025 12:13:16 -0500 Subject: [PATCH 7/7] Added support for modules without arguments Code like this: https://github.com/dimagi/commcare-hq/blob/b9bb425c9615bda43bf48e5498ad5a259307081c/corehq/messaging/scheduling/static/scheduling/js/create_schedule_main.js#L1-L6 --- corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py index 99372833c469..59e86ff2653b 100644 --- a/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py +++ b/corehq/apps/hqwebapp/management/commands/hqdefine_to_esm.py @@ -152,7 +152,10 @@ def update_parser_location(self, line): return line if self.in_imports and 'function' in line: self.in_imports = False - self.in_arguments = True + if re.search(r'function\s*\(\s*\)', line): + self.in_hqdefine_block = False + else: + self.in_arguments = True return line if self.in_arguments and ')' in line: self.in_arguments = False