Skip to content

Commit

Permalink
feat: generate mandatory XLS forms programmatically
Browse files Browse the repository at this point in the history
  • Loading branch information
Sujanadh committed Dec 18, 2024
1 parent ea3e48c commit 9f155cc
Show file tree
Hide file tree
Showing 5 changed files with 465 additions and 114 deletions.
5 changes: 5 additions & 0 deletions osm_fieldwork/form_components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""OSM Fieldwork Form Builder Package."""

import os

package_root = os.path.dirname(os.path.abspath(__file__))
69 changes: 69 additions & 0 deletions osm_fieldwork/form_components/choice_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/python3

# Copyright (c) 2020, 2021, 2022, 2023, 2024 Humanitarian OpenStreetMap Team
#
# This file is part of OSM-Fieldwork.
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OSM-Fieldwork. If not, see <https:#www.gnu.org/licenses/>.
#

"""This creates DataFrames for choice lists used in the survey form.
These include predefined options for fields such as yes/no responses
and issues related to digitization problems. Each choice contains
multilingual labels to support various languages.
Returns:
tuple: Two pandas DataFrames containing the `choices` data for yes/no
and digitisation problems respectively.
"""

import pandas as pd

# Define the choices sheet
choices_data = [
{"list_name": "yes_no", "name": "yes", "label::english(en)": "Yes"},
{"list_name": "yes_no", "name": "no", "label::english(en)": "No"},
]

choices_df = pd.DataFrame(choices_data)

digitisation_choices = [
{
"list_name": "digitisation_problem",
"name": "lumped",
"label::english(en)": "Lumped - one polygon (more than one building digitized as one)",
"label::swahili(sw)": "Lumped - poligoni moja (zaidi ya jengo moja limewekwa dijiti kuwa moja)",
"label::french(fr)": "Lumped - un polygone (plus d'un bâtiment numérisé en un seul)",
"label::spanish(es)": "Agrupado - un polígono (más de un edificio digitalizado como uno)",
},
{
"list_name": "digitisation_problem",
"name": "split",
"label::english(en)": "Split - one building (one building digitized as more than one polygon)",
"label::swahili(sw)": "Mgawanyiko - jengo moja (jengo moja limebadilishwa kuwa zaidi ya poligoni moja)",
"label::french(fr)": "Fractionnement - un bâtiment (un bâtiment numérisé sous la forme de plusieurs polygones)",
"label::spanish(es)": "Split - un edificio (un edificio digitalizado como más de un polígono)",
},
{
"list_name": "digitisation_problem",
"name": "other",
"label::english(en)": "OTHER",
"label::swahili(sw)": "MENGINEYO",
"label::french(fr)": "AUTRES",
"label::spanish(es)": "OTROS",
},
]

digitisation_choices_df = pd.DataFrame(digitisation_choices)
78 changes: 78 additions & 0 deletions osm_fieldwork/form_components/digitisation_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/python3

# Copyright (c) 2020, 2021, 2022, 2023, 2024 Humanitarian OpenStreetMap Team
#
# This file is part of OSM-Fieldwork.
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OSM-Fieldwork. If not, see <https:#www.gnu.org/licenses/>.
#

"""This script creates a DataFrame for digitization-related fields in the survey form.
These fields focus on verifying the accuracy of digitized locations,
capturing any issues with digitization, and adding additional notes or
images if required. The fields include logic for relevance, mandatory
requirements, and conditions based on user responses.
Returns:
pd.DataFrame: A DataFrame containing the digitization-related fields.
"""

import pandas as pd

digitisation_fields = [
{
"type": "begin group",
"name": "verification",
"label::english(en)": "Verification",
"relevant": "(${new_feature} != '') or (${building_exists} = 'yes')",
},
{
"type": "select_one yes_no",
"name": "digitisation_correct",
"label::english(en)": "Is the digitized location for this feature correct?",
"relevant": "(${new_feature} != '') or (${building_exists} = 'yes')",
"calculation": "once(if(${new_feature} != '', 'yes', ''))",
"read_only": "${new_feature} != ''",
"required": "yes",
},
{
"type": "select_one digitisation_problem",
"name": "digitisation_problem",
"label::english(en)": "What is wrong with the digitization?",
"relevant": "${digitisation_correct}='no'",
},
{
"type": "text",
"name": "digitisation_problem_other",
"label::english(en)": "You said “Other.” Please tell us what went wrong with the digitization!",
"relevant": "${digitisation_problem}='other' ",
},
{"type": "end group"},
{
"type": "image",
"name": "image",
"label::english(en)": "Take a Picture",
"apperance": "minimal",
"parameters": "max-pixels=1000",
},
{
"type": "note",
"name": "end_note",
"label::english(en)": "You can't proceed with data acquisition, if the building doesn't exist.",
"relevant": "${building_exists} = 'no'",
},
]

digitisation_df = pd.DataFrame(digitisation_fields)
175 changes: 175 additions & 0 deletions osm_fieldwork/form_components/mandatory_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/python3

# Copyright (c) 2020, 2021, 2022, 2023, 2024 Humanitarian OpenStreetMap Team
#
# This file is part of OSM-Fieldwork.
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OSM-Fieldwork. If not, see <https:#www.gnu.org/licenses/>.
#

"""This script generates an XLS form with mandatory fields.
For use in data collection and mapping tasks.
The generated form includes metadata, survey questions, and settings
required for compatibility with HOT's FMTM tools.
It programmatically organizes form sections into metadata,
mandatory fields, and entities, and outputs them in a structured format.
Modules and functionalities:
- **Metadata Sheet**: Includes default metadata fields
such as `start`, `end`, `username`, and `deviceid`.
- **Survey Sheet**: Combines metadata with mandatory fields required for FMTM workflows.
- `warmup` for collecting initial location.
- `feature` for selecting map geometry from predefined options.
- `new_feature` for capturing GPS coordinates of new map features.
- Calculated fields such as `form_category`, `xid`, `xlocation`, `status`, and others.
- **Entities Sheet**: Defines entity management rules to handle mapping tasks dynamically.
- Includes rules for entity creation and updates with user-friendly labels.
- **Settings Sheet**: Sets the form ID, version, and configuration options.
"""

from datetime import datetime

import pandas as pd

current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

meta_data = [
{"type": "start", "name": "start"},
{"type": "end", "name": "end"},
{"type": "today", "name": "today"},
{"type": "phonenumber", "name": "phonenumber"},
{"type": "deviceid", "name": "deviceid"},
{"type": "username", "name": "username"},
{
"type": "email",
"name": "email",
},
]

meta_df = pd.DataFrame(meta_data)

mandatory_data = [
{
"type": "note",
"name": "instructions",
"label::english(en)": """Welcome ${username}. This survey form was generated
by HOT's FMTM to record ${form_category} map features.""",
},
{"notes": "Fields essential to FMTM"},
{"type": "start-geopoint", "name": "warmup", "notes": "collects location on form start"},
{"type": "select_one_from_file features.csv", "name": "feature", "label::english(en)": "Geometry", "appearance": "map"},
{
"type": "geopoint",
"name": "new_feature",
"label::english(en)": "Alternatively, take a gps coordinates of a new feature",
"appearance": "placement-map",
"relevant": "${feature}= ''",
"required": "yes",
},
{
"type": "calculate",
"name": "form_category",
"label::english(en)": "FMTM form category",
"appearance": "minimal",
"calculation": "once('Unkown')",
},
{
"type": "calculate",
"name": "xid",
"notes": "e.g. OSM ID",
"label::english(en)": "Feature ID",
"appearance": "minimal",
"calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/osm_id, '')",
},
{
"type": "calculate",
"name": "xlocation",
"notes": "e.g. OSM Geometry",
"label::english(en)": "Feature Geometry",
"appearance": "minimal",
"calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/geometry, ${new_feature})",
"save_to": "geometry",
},
{
"type": "calculate",
"name": "task_id",
"notes": "e.g. FMTM Task ID",
"label::english(en)": "Task ID",
"appearance": "minimal",
"calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/task_id, '')",
"save_to": "task_id",
},
{
"type": "calculate",
"name": "status",
"notes": "Update the Entity 'status' field",
"label::english(en)": "Mapping Status",
"appearance": "minimal",
"calculation": """if(${new_feature} != '', 2,
if(${building_exists} = 'no', 5,
if(${digitisation_correct} = 'no', 6,
${status})))""",
"default": "2",
"trigger": "${new_feature}",
"save_to": "status",
},
{
"type": "select_one yes_no",
"name": "building_exists",
"label::english(en)": "Does this feature exist?",
"relevant": "${feature} != '' ",
},
{
"type": "calculate",
"name": "submission_id",
"notes": "Update the submission id",
"label::english(en)": "Submission id",
"appearance": "minimal",
"calculation": "once(${instanceID})",
"save_to": "submission_id",
},
]

mandatory_df = pd.DataFrame(mandatory_data)

# Define the survey sheet
survey_df = pd.concat([meta_df, mandatory_df])

# Define entities sheet
entities_data = [
{
"list_name": "features",
"entity_id": "coalesce(${feature}, uuid())",
"create_if": "if(${new_feature}, true(), false())",
"update_if": "if(${new_feature}, false(), true())",
"label": """concat(if(${status} = '1', "🔒 ",
if(${status} = '2', "✅ ", if(${status} = '5', "❌ ",
if(${status} = '6', "❌ ", '')))), "Task ", ${task_id},
" Feature ", if(${xid} != ' ', ${xid}, ' '))""",
}
]
entities_df = pd.DataFrame(entities_data)

# Define the settings sheet
settings_data = [
{
"form_id": "mandatory_fields",
"version": current_datetime,
"form_title": "Mandatory Fields Form",
"allow_choice_duplicates": "yes",
}
]

settings_df = pd.DataFrame(settings_data)
Loading

0 comments on commit 9f155cc

Please sign in to comment.