From 56862ebb423d6080e930bc7e66f5f1779f5c5363 Mon Sep 17 00:00:00 2001 From: Sujan Adhikari Date: Fri, 15 Nov 2024 16:41:36 +0545 Subject: [PATCH] feat: sanitize column names to follow defined standards --- osm_fieldwork/update_xlsform.py | 64 +++++++++++++----- .../xlsforms/common/digitisation_fields.xls | Bin 30208 -> 10752 bytes .../xlsforms/common/mandatory_fields.xls | Bin 16384 -> 16384 bytes 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/osm_fieldwork/update_xlsform.py b/osm_fieldwork/update_xlsform.py index f4a846c3..cd50fcd3 100644 --- a/osm_fieldwork/update_xlsform.py +++ b/osm_fieldwork/update_xlsform.py @@ -21,6 +21,36 @@ SURVEY_GROUP_NAME = "survey_questions" +def standardize_xlsform_sheets(xlsform: dict) -> dict: + """Standardizes column headers in both the 'survey' and 'choices' sheets of an XLSForm. + + - Strips spaces and lowercases all column headers. + - Fixes formatting for columns with '::' (e.g., multilingual labels). + + Args: + xlsform (dict): A dictionary with keys 'survey' and 'choices', each containing a DataFrame. + + Returns: + dict: The updated XLSForm dictionary with standardized column headers. + """ + + def clean_column_name(col_name): + if col_name == "label": + return "label::english(en)" + if "::" in col_name: + # Handle '::' columns (e.g., 'label::english (en)') + parts = col_name.split("::") + language_part = parts[1].replace(" ", "").lower() # Remove spaces and lowercase + return f"{parts[0]}::{language_part}" + return col_name.strip().lower() # General cleanup + + # Apply cleaning to each sheet + for _sheet_name, sheet_df in xlsform.items(): + sheet_df.columns = [clean_column_name(col) for col in sheet_df.columns] + + return xlsform + + def merge_dataframes(mandatory_df: pd.DataFrame, user_question_df: pd.DataFrame, digitisation_df: pd.DataFrame): """Merge multiple Pandas dataframes together, removing duplicate fields.""" # Remove empty rows from dataframes @@ -92,8 +122,8 @@ def handle_translations( if field in user_question_df.columns and not translation_columns: # If user_question_df has only the base field (e.g., 'label'), map English translation from mandatory and digitisation - mandatory_df[field] = mandatory_df.get(f"{field}::English(en)", mandatory_df.get(field)) - digitisation_df[field] = digitisation_df.get(f"{field}::English(en)", digitisation_df.get(field)) + mandatory_df[field] = mandatory_df.get(f"{field}::english(en)", mandatory_df.get(field)) + digitisation_df[field] = digitisation_df.get(f"{field}::english(en)", digitisation_df.get(field)) # Then drop translation columns mandatory_df = mandatory_df.loc[:, ~mandatory_df.columns.str.startswith("label::")] @@ -133,10 +163,10 @@ def create_survey_group(name: str) -> dict[str, pd.DataFrame]: { "type": ["begin group"], "name": [name], - "label::English(en)": [name], - "label::Swahili(sw)": [name], - "label::French(fr)": [name], - "label::Spanish(es)": [name], + "label::english(en)": [name], + "label::swahili(sw)": [name], + "label::french(fr)": [name], + "label::spanish(es)": [name], "relevant": "(${new_feature} != '') or (${building_exists} = 'yes')", } ) @@ -158,20 +188,16 @@ def append_select_one_from_file_row(df: pd.DataFrame, entity_name: str) -> pd.Da # Find the row index after 'feature' row row_index_to_split_on = select_one_from_file_index[0] + 1 - # Strip the 's' from the end for singular form - if entity_name.endswith("s"): - # Plural to singular - entity_name = entity_name[:-1] additional_row = pd.DataFrame( { "type": [f"select_one_from_file {entity_name}.csv"], "name": [entity_name], - "label::English(en)": [entity_name], + "label::english(en)": [entity_name], "appearance": ["map"], - "label::Swahili(sw)": [entity_name], - "label::French(fr)": [entity_name], - "label::Spanish(es)": [entity_name], + "label::swahili(sw)": [entity_name], + "label::french(fr)": [entity_name], + "label::spanish(es)": [entity_name], } ) @@ -205,12 +231,14 @@ async def append_mandatory_fields( custom_sheets = pd.read_excel(custom_form, sheet_name=None, engine="calamine") mandatory_sheets = pd.read_excel(f"{xlsforms_path}/common/mandatory_fields.xls", sheet_name=None, engine="calamine") digitisation_sheets = pd.read_excel(f"{xlsforms_path}/common/digitisation_fields.xls", sheet_name=None, engine="calamine") + custom_sheets = standardize_xlsform_sheets(custom_sheets) # Merge 'survey' and 'choices' sheets if "survey" not in custom_sheets: msg = "Survey sheet is required in XLSForm!" log.error(msg) raise ValueError(msg) + log.debug("Merging survey sheet XLSForm data") custom_sheets["survey"] = merge_dataframes( mandatory_sheets.get("survey"), custom_sheets.get("survey"), digitisation_sheets.get("survey") @@ -223,10 +251,10 @@ async def append_mandatory_fields( if not form_category_row.empty: custom_sheets["survey"].loc[custom_sheets["survey"]["name"] == "form_category", "calculation"] = f"once('{form_category}')" - if "choices" not in custom_sheets: - msg = "Choices sheet is required in XLSForm!" - log.error(msg) - raise ValueError(msg) + # Ensure the 'choices' sheet exists in custom_sheets + if "choices" not in custom_sheets or custom_sheets["choices"] is None: + custom_sheets["choices"] = pd.DataFrame(columns=["list_name", "name", "label::english(en)"]) + log.debug("Merging choices sheet XLSForm data") custom_sheets["choices"] = merge_dataframes( mandatory_sheets.get("choices"), custom_sheets.get("choices"), digitisation_sheets.get("choices") diff --git a/osm_fieldwork/xlsforms/common/digitisation_fields.xls b/osm_fieldwork/xlsforms/common/digitisation_fields.xls index c0820ac1f4b8022de47cdc0811a24e41c13ee619..22005af9c4befbbae9651fc519774a5b2ebf4892 100644 GIT binary patch literal 10752 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 JG0Vsq(l_hvdE)>8