Skip to content

Commit

Permalink
Merge pull request #324 from hotosm/feat/form-builder
Browse files Browse the repository at this point in the history
Generate mandatory XLS forms programmatically
  • Loading branch information
Sujanadh authored Dec 25, 2024
2 parents f3078f2 + 84b4ea4 commit bea5a1b
Show file tree
Hide file tree
Showing 6 changed files with 482 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__))
72 changes: 72 additions & 0 deletions osm_fieldwork/form_components/choice_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/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", "label::nepali(ne)": "छ"},
{"list_name": "yes_no", "name": "no", "label::english(en)": "No", "label::nepali(ne)": "छैन"},
]

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::nepali(ne)": "लुम्पेड - एक बहुभुज (एउटा भन्दा बढी भवनहरू एकको रूपमा डिजिटलाइज गरिएको)",
"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::nepali(ne)": "विभाजन - एउटा भवन (एउटा भवन एक भन्दा बढी बहुभुजको रूपमा डिजिटाइज गरिएको)",
"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::nepali(ne)": "अन्य",
"label::swahili(sw)": "MENGINEYO",
"label::french(fr)": "AUTRES",
"label::spanish(es)": "OTROS",
},
]

digitisation_choices_df = pd.DataFrame(digitisation_choices)
84 changes: 84 additions & 0 deletions osm_fieldwork/form_components/digitisation_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/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",
"label::nepali(ne)": "प्रमाणीकरण",
"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?",
"label::nepali(ne)": "के यो डिजिटाइज गरिएको स्थान सही छ?",
"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?",
"label::nepali(en)": "डिजिटलाइजेशनमा के गल्ती छ?",
"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!",
"label::nepali(ne)": "तपाईंले 'अरू' भन्नुभयो। डिजिटलाइजेशनमा के गल्ती भयो! कृपया हामीलाई भन्न सक्नुहुन्छ?",
"relevant": "${digitisation_problem}='other' ",
},
{"type": "end group"},
{
"type": "image",
"name": "image",
"label::english(en)": "Take a Picture",
"label::nepali(ne)": "तस्विर लिनुहोस्",
"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.",
"label::nepali(ne)": "यदि भवन अवस्थित छैन भने, तपाईं डाटा लिन अगाडि बढ्न सक्नुहुन्न।",
"relevant": "${building_exists} = 'no'",
},
]

digitisation_df = pd.DataFrame(digitisation_fields)
183 changes: 183 additions & 0 deletions osm_fieldwork/form_components/mandatory_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/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.""",
"label::nepali(ne)": """स्वागत छ ${username}। ${form_category} नक्सा Data रेकर्ड गर्न HOT को FMTM द्वारा
यो सर्वेक्षण फारम उत्पन्न भएको थियो।""",
},
{"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",
"label::nepali(ne)": "वैकल्पिक रूपमा, नयाँ सुविधाको GPS निर्देशांक लिनुहोस्।",
"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?",
"label::nepali(ne)": "के यो भवन अवस्थित छ?",
"relevant": "${feature} != '' ",
},
{
"type": "calculate",
"name": "submission_ids",
"notes": "Update the submission ids",
"label::english(en)": "Submission ids",
"appearance": "minimal",
"calculation": """if(
instance('features')/root/item[name=${feature}]/submission_ids = '',
${instanceID},
concat(instance('features')/root/item[name=${feature}]/submission_ids, ',', ${instanceID})
)""",
"save_to": "submission_ids",
},
]

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 bea5a1b

Please sign in to comment.