+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
18 """Logic for interaction with ODK Central & data."""
+
+
+
+
+
+
24 from io
import BytesIO, StringIO
+
25 from typing
import Optional, Union
+
26 from xml.etree.ElementTree
import Element, SubElement
+
+
+
29 from defusedxml
import ElementTree
+
30 from fastapi
import HTTPException
+
31 from loguru
import logger
as log
+
32 from osm_fieldwork.csvdump
import CSVDump
+
33 from osm_fieldwork.OdkCentral
import OdkAppUser, OdkForm, OdkProject
+
34 from pyxform.builder
import create_survey_element_from_dict
+
35 from pyxform.xls2json
import parse_file_to_json
+
36 from sqlalchemy
import text
+
37 from sqlalchemy.orm
import Session
+
+
+
+
+
42 geojson_to_javarosa_geom,
+
43 javarosa_to_geojson_geom,
+
44 parse_geojson_file_to_featcol,
+
+
+
+
+
+
50 def get_odk_project(odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None):
+
51 """Helper function to get the OdkProject with credentials."""
+
+
53 url = odk_central.odk_central_url
+
54 user = odk_central.odk_central_user
+
55 pw = odk_central.odk_central_password
+
+
57 log.debug(
"ODKCentral connection variables not set in function")
+
58 log.debug(
"Attempting extraction from environment variables")
+
59 url = settings.ODK_CENTRAL_URL
+
60 user = settings.ODK_CENTRAL_USER
+
61 pw = settings.ODK_CENTRAL_PASSWD
+
+
+
64 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
65 project = OdkProject(url, user, pw)
+
+
67 except ValueError
as e:
+
+
+
+
+
72 ODK credentials are invalid, or may have been updated. Please update them.
+
+
+
75 except Exception
as e:
+
+
+
78 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
84 def get_odk_form(odk_central: project_schemas.ODKCentralDecrypted):
+
85 """Helper function to get the OdkForm with credentials."""
+
86 url = odk_central.odk_central_url
+
87 user = odk_central.odk_central_user
+
88 pw = odk_central.odk_central_password
+
+
+
91 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
92 form = OdkForm(url, user, pw)
+
93 except Exception
as e:
+
+
+
96 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
102 def get_odk_app_user(odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None):
+
103 """Helper function to get the OdkAppUser with credentials."""
+
+
105 url = odk_central.odk_central_url
+
106 user = odk_central.odk_central_user
+
107 pw = odk_central.odk_central_password
+
+
109 log.debug(
"ODKCentral connection variables not set in function")
+
110 log.debug(
"Attempting extraction from environment variables")
+
111 url = settings.ODK_CENTRAL_URL
+
112 user = settings.ODK_CENTRAL_USER
+
113 pw = settings.ODK_CENTRAL_PASSWD
+
+
+
116 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
117 form = OdkAppUser(url, user, pw)
+
118 except Exception
as e:
+
+
+
121 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
127 def list_odk_projects(
+
128 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
130 """List all projects on a remote ODK Server."""
+
131 project = get_odk_project(odk_central)
+
132 return project.listProjects()
+
+
+
135 def create_odk_project(
+
136 name: str, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
138 """Create a project on a remote ODK Server.
+
+
140 Appends FMTM to the project name to help identify on shared servers.
+
+
142 project = get_odk_project(odk_central)
+
+
+
145 log.debug(f
"Attempting ODKCentral project creation: FMTM {name}")
+
146 result = project.createProject(f
"FMTM {name}")
+
+
+
149 if isinstance(result, dict):
+
150 if result.get(
"code") == 401.2:
+
+
+
153 detail=
"Could not authenticate to odk central.",
+
+
+
156 log.debug(f
"ODKCentral response: {result}")
+
157 log.info(f
"Project {name} available on the ODK Central server.")
+
+
159 except Exception
as e:
+
+
+
162 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
166 async
def delete_odk_project(
+
167 project_id: int, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
169 """Delete a project from a remote ODK Server."""
+
+
+
+
173 project = get_odk_project(odk_central)
+
174 result = project.deleteProject(project_id)
+
175 log.info(f
"Project {project_id} has been deleted from the ODK Central server.")
+
+
+
178 return "Could not delete project from central odk"
+
+
+
181 def delete_odk_app_user(
+
+
+
184 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
186 """Delete an app-user from a remote ODK Server."""
+
187 odk_app_user = get_odk_app_user(odk_central)
+
188 result = odk_app_user.delete(project_id, name)
+
+
+
+
192 def create_odk_xform(
+
+
+
195 odk_credentials: project_schemas.ODKCentralDecrypted,
+
+
197 """Create an XForm on a remote ODK Central server.
+
+
+
200 odk_id (str): Project ID for ODK Central.
+
201 xform_data (BytesIO): XForm data to set.
+
202 odk_credentials (ODKCentralDecrypted): Creds for ODK Central.
+
+
+
205 form_name (str): ODK Central form name for the API.
+
+
+
208 xform = get_odk_form(odk_credentials)
+
209 except Exception
as e:
+
+
+
212 status_code=500, detail={
"message":
"Connection failed to odk central"}
+
+
+
215 xform_id = xform.createForm(odk_id, xform_data, publish=
True)
+
+
+
218 "h":
"http://www.w3.org/1999/xhtml",
+
219 "odk":
"http://www.opendatakit.org/xforms",
+
220 "xforms":
"http://www.w3.org/2002/xforms",
+
+
+
223 root = ElementTree.fromstring(xform_data.getvalue())
+
224 xml_data = root.findall(
".//xforms:data[@id]", namespaces)
+
225 extracted_name =
"Not Found"
+
+
227 extracted_name = dt.get(
"id")
+
228 msg = f
"Failed to create form on ODK Central: ({extracted_name})"
+
+
+
231 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
+
236 def delete_odk_xform(
+
+
+
239 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
241 """Delete an XForm from a remote ODK Central server."""
+
242 xform = get_odk_form(odk_central)
+
243 result = xform.deleteForm(project_id, xform_id)
+
+
+
+
+
+
+
250 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
251 metadata: bool =
False,
+
+
253 """List all XForms in an ODK Central project."""
+
254 project = get_odk_project(odk_central)
+
255 xforms = project.listForms(project_id, metadata)
+
+
+
+
+
260 def get_form_full_details(
+
+
+
263 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
265 """Get additional metadata for ODK Form."""
+
266 form = get_odk_form(odk_central)
+
267 form_details = form.getFullDetails(odk_project_id, form_id)
+
+
+
+
271 def get_odk_project_full_details(
+
272 odk_project_id: int, odk_central: project_schemas.ODKCentralDecrypted
+
+
274 """Get additional metadata for ODK project."""
+
275 project = get_odk_project(odk_central)
+
276 project_details = project.getFullDetails(odk_project_id)
+
277 return project_details
+
+
+
280 def list_submissions(
+
281 project_id: int, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
283 """List all submissions for a project, aggregated from associated users."""
+
284 project = get_odk_project(odk_central)
+
285 xform = get_odk_form(odk_central)
+
+
287 for user
in project.listAppUsers(project_id):
+
288 for subm
in xform.listSubmissions(project_id, user[
"displayName"]):
+
289 submissions.append(subm)
+
+
+
+
+
294 async
def get_form_list(db: Session) -> list:
+
295 """Returns the list of {id:title} for XLSForms in the database."""
+
+
297 include_categories = [category.value
for category
in XLSFormType]
+
+
+
+
301 SELECT id, title FROM xlsforms
+
+
303 (SELECT UNNEST(:categories));
+
+
+
+
307 result = db.execute(sql_query, {
"categories": include_categories}).fetchall()
+
308 result_list = [{
"id": row.id,
"title": row.title}
for row
in result]
+
+
+
311 except Exception
as e:
+
+
+
314 status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+
+
+
+
+
319 async
def update_project_xform(
+
+
+
+
+
+
+
326 odk_credentials: project_schemas.ODKCentralDecrypted,
+
+
328 """Update and publish the XForm for a project.
+
+
+
331 xform_id (str): The UUID of the existing XForm in ODK Central.
+
332 odk_id (int): ODK Central form ID.
+
333 xform_data (BytesIO): XForm data.
+
334 form_file_ext (str): Extension of the form file.
+
335 category (str): Category of the XForm.
+
336 task_count (int): The number of tasks in a project.
+
337 odk_credentials (project_schemas.ODKCentralDecrypted): ODK Central creds.
+
+
339 xform_data = await read_and_test_xform(
+
+
+
342 return_form_data=
True,
+
+
344 updated_xform_data = await modify_xform_xml(
+
+
+
+
348 existing_id=xform_id,
+
+
+
351 xform_obj = get_odk_form(odk_credentials)
+
+
+
354 xform_obj.createForm(
+
+
+
+
+
+
360 xform_obj.publishForm(odk_id, xform_id)
+
+
+
363 async
def read_and_test_xform(
+
+
+
366 return_form_data: bool =
False,
+
+
368 """Read and validate an XForm.
+
+
+
371 input_data (BytesIO): form to be tested.
+
372 form_file_ext (str): type of form (.xls, .xlsx, or .xml).
+
373 return_form_data (bool): return the XForm data.
+
+
+
376 file_ext = form_file_ext.lower()
+
+
378 if file_ext ==
".xml":
+
379 xform_bytesio = input_data
+
+
+
382 ElementTree.fromstring(xform_bytesio.getvalue())
+
383 except ElementTree.ParseError
as e:
+
+
385 msg = f
"Error parsing XForm XML: Possible reason: {str(e)}"
+
+
387 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
391 log.debug(
"Converting xlsform -> xform")
+
392 json_data = parse_file_to_json(
+
393 path=
"/dummy/path/with/file/ext.xls",
+
394 file_object=input_data,
+
+
396 generated_xform = create_survey_element_from_dict(json_data)
+
+
398 xform_bytesio = BytesIO(
+
399 generated_xform.to_xml(
+
+
+
+
+
404 except Exception
as e:
+
+
406 msg = f
"XLSForm is invalid: {str(e)}"
+
+
408 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
+
+
+
+
416 xform_xml = ElementTree.fromstring(xform_bytesio.getvalue())
+
+
+
+
420 namespaces = {
"xforms":
"http://www.w3.org/2002/xforms"}
+
+
422 os.path.splitext(inst.attrib[
"src"].split(
"/")[-1])[0]
+
423 for inst
in xform_xml.findall(
".//xforms:instance[@src]", namespaces)
+
424 if inst.attrib.get(
"src",
"").endswith(
".csv")
+
+
+
+
+
+
430 "The form has no select_one_from_file or "
+
431 "select_multiple_from_file field defined for a CSV."
+
+
433 raise ValueError(msg)
from None
+
+
435 return {
"required_media": csv_list,
"message":
"Your form is valid"}
+
+
437 except Exception
as e:
+
+
+
440 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=str(e)
+
+
+
+
444 async
def modify_xform_xml(
+
+
+
+
448 existing_id: Optional[str] =
None,
+
+
450 """Update fields in the XForm to work with FMTM.
+
+
452 The 'id' field is set to random UUID (xFormId) unless existing_id is specified
+
453 The 'name' field is set to the category name.
+
454 The upload media must be equal to 'features.csv'.
+
455 The task_filter options are populated as choices in the form.
+
456 The form_category value is also injected to display in the instructions.
+
+
+
459 form_data (str): The input form data.
+
460 category (str): The form category, used to name the dataset (entity list)
+
461 and the .csv file containing the geometries.
+
462 task_count (int): The number of tasks in a project.
+
463 existing_id (str): An existing XForm ID in ODK Central, for updating.
+
+
+
466 BytesIO: The XForm data.
+
+
468 log.debug(f
"Updating XML keys in survey XForm: {category}")
+
+
+
471 xform_id = existing_id
+
+
473 xform_id = uuid.uuid4()
+
+
+
476 "h":
"http://www.w3.org/1999/xhtml",
+
477 "odk":
"http://www.opendatakit.org/xforms",
+
478 "xforms":
"http://www.w3.org/2002/xforms",
+
479 "entities":
"http://www.opendatakit.org/xforms/entities",
+
+
+
+
483 root = ElementTree.fromstring(form_data.getvalue())
+
+
485 xform_data = root.findall(
".//xforms:data[@id]", namespaces)
+
486 for dt
in xform_data:
+
+
488 dt.set(
"id", str(xform_id))
+
+
+
491 existing_title = root.find(
".//h:title", namespaces)
+
492 if existing_title
is not None:
+
493 existing_title.text = category
+
+
+
496 xform_instance_src = root.findall(
".//xforms:instance[@src]", namespaces)
+
497 for inst
in xform_instance_src:
+
498 src_value = inst.get(
"src",
"")
+
499 if src_value.endswith(
".geojson")
or src_value.endswith(
".csv"):
+
+
+
502 inst.set(
"src",
"jr://file-csv/features.csv")
+
+
+
+
506 model_element = root.find(
".//xforms:model", namespaces)
+
+
508 existing_instance = model_element.find(
+
509 ".//xforms:instance[@id='task_filter']", namespaces
+
+
511 if existing_instance
is not None:
+
512 model_element.remove(existing_instance)
+
+
514 instance_task_filters = Element(
"instance", id=
"task_filter")
+
515 root_element = SubElement(instance_task_filters,
"root")
+
+
517 for task_id
in range(1, task_count + 1):
+
518 item = SubElement(root_element,
"item")
+
519 SubElement(item,
"itextId").text = f
"task_filter-{task_id}"
+
520 SubElement(item,
"name").text = str(task_id)
+
521 model_element.append(instance_task_filters)
+
+
+
524 itext_element = root.find(
".//xforms:itext", namespaces)
+
525 if itext_element
is not None:
+
526 existing_translations = itext_element.findall(
+
527 ".//xforms:translation", namespaces
+
+
529 for translation
in existing_translations:
+
+
531 existing_text = translation.find(
+
532 ".//xforms:text[@id='task_filter-0']", namespaces
+
+
534 if existing_text
is not None:
+
535 translation.remove(existing_text)
+
+
+
538 for task_id
in range(1, task_count + 1):
+
539 new_text = Element(
"text", id=f
"task_filter-{task_id}")
+
540 value_element = Element(
"value")
+
541 value_element.text = str(task_id)
+
542 new_text.append(value_element)
+
543 translation.append(new_text)
+
+
+
546 form_category_update = root.find(
+
547 ".//xforms:bind[@nodeset='/data/form_category']", namespaces
+
+
549 if form_category_update
is not None:
+
550 if category.endswith(
"s"):
+
+
552 category = category[:-1]
+
553 form_category_update.set(
"calculate", f
"once('{category.rstrip('s')}')")
+
+
555 return BytesIO(ElementTree.tostring(root))
+
+
+
558 async
def convert_geojson_to_odk_csv(
+
559 input_geojson: BytesIO,
+
+
561 """Convert GeoJSON features to ODK CSV format.
+
+
563 Used for form upload media (dataset) in ODK Central.
+
+
+
566 input_geojson (BytesIO): GeoJSON file to convert.
+
+
+
569 feature_csv (StringIO): CSV of features in XLSForm format for ODK.
+
+
571 parsed_geojson = parse_geojson_file_to_featcol(input_geojson.getvalue())
+
+
573 if not parsed_geojson:
+
+
575 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+
576 detail=
"Conversion GeoJSON --> CSV failed",
+
+
+
579 csv_buffer = StringIO()
+
580 csv_writer = csv.writer(csv_buffer)
+
+
582 header = [
"osm_id",
"tags",
"version",
"changeset",
"timestamp",
"geometry"]
+
583 csv_writer.writerow(header)
+
+
585 features = parsed_geojson.get(
"features", [])
+
586 for feature
in features:
+
587 geometry = feature.get(
"geometry")
+
588 javarosa_geom = await geojson_to_javarosa_geom(geometry)
+
+
590 properties = feature.get(
"properties", {})
+
591 osm_id = properties.get(
"osm_id")
+
592 tags = properties.get(
"tags")
+
593 version = properties.get(
"version")
+
594 changeset = properties.get(
"changeset")
+
595 timestamp = properties.get(
"timestamp")
+
+
597 csv_row = [osm_id, tags, version, changeset, timestamp, javarosa_geom]
+
598 csv_writer.writerow(csv_row)
+
+
+
+
+
+
+
+
606 def flatten_json(data: dict, target: dict):
+
607 """Flatten json properties to a single level.
+
+
609 Removes any existing GeoJSON data from captured GPS coordinates in
+
+
+
+
+
614 flatten_json(original_dict, new_dict)
+
+
616 for k, v
in data.items():
+
617 if isinstance(v, dict):
+
618 if "type" in v
and "coordinates" in v:
+
+
+
621 flatten_json(v, target)
+
+
+
+
+
626 async
def convert_odk_submission_json_to_geojson(
+
627 input_json: Union[BytesIO, list],
+
628 ) -> geojson.FeatureCollection:
+
629 """Convert ODK submission JSON file to GeoJSON.
+
+
631 Used for loading into QGIS.
+
+
+
634 input_json (BytesIO): ODK JSON submission list.
+
+
+
637 geojson (BytesIO): GeoJSON format ODK submission.
+
+
639 if isinstance(input_json, list):
+
640 submission_json = input_json
+
+
642 submission_json = json.loads(input_json.getvalue())
+
+
644 if not submission_json:
+
+
646 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+
647 detail=
"Loading JSON submission failed",
+
+
+
+
651 for submission
in submission_json:
+
652 keys_to_remove = [
"meta",
"__id",
"__system"]
+
653 for key
in keys_to_remove:
+
+
+
+
657 flatten_json(submission, data)
+
+
659 geojson_geom = await javarosa_to_geojson_geom(
+
660 data.pop(
"xlocation", {}), geom_type=
"Polygon"
+
+
+
663 feature = geojson.Feature(geometry=geojson_geom, properties=data)
+
664 all_features.append(feature)
+
+
666 return geojson.FeatureCollection(features=all_features)
+
+
+
669 async
def get_entities_geojson(
+
670 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
672 dataset_name: str =
"features",
+
673 minimal: Optional[bool] =
False,
+
674 ) -> geojson.FeatureCollection:
+
675 """Get the Entity details for a dataset / Entity list.
+
+
677 Uses the OData endpoint from ODK Central.
+
+
679 Currently it is not possible to filter via OData filters on custom params.
+
680 TODO in the future filter by task_id via the URL,
+
681 instead of returning all and filtering.
+
+
683 Response GeoJSON format:
+
+
685 "type": "FeatureCollection",
+
+
+
+
+
+
+
692 "id": uuid_of_entity,
+
+
694 "updated_at": "2024-04-11T18:23:30.787Z",
+
+
+
+
+
+
+
701 "timestamp": "2024-12-20",
+
702 "status": "LOCKED_FOR_MAPPING"
+
+
+
+
+
+
708 Response GeoJSON format, minimal:
+
+
710 "type": "FeatureCollection",
+
+
+
+
+
+
+
717 "id": uuid_of_entity,
+
+
+
720 "updated_at": "2024-04-11T18:23:30.787Z",
+
721 "status": "LOCKED_FOR_MAPPING"
+
+
+
+
+
+
727 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
728 odk_id (str): The project ID in ODK Central.
+
729 dataset_name (str): The dataset / Entity list name in ODK Central.
+
730 minimal (bool): Remove all fields apart from id, updated_at, and status.
+
+
+
733 dict: Entity data in OData JSON format.
+
+
735 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
736 entities = await odk_central.getEntityData(
+
+
+
739 url_params=
"$select=__id, __system/updatedAt, geometry, osm_id, status"
+
+
+
+
+
+
745 for entity
in entities:
+
+
747 flatten_json(entity, flattened_dict)
+
+
749 javarosa_geom = flattened_dict.pop(
"geometry")
or ""
+
750 geojson_geom = await javarosa_to_geojson_geom(
+
751 javarosa_geom, geom_type=
"Polygon"
+
+
+
754 feature = geojson.Feature(
+
755 geometry=geojson_geom,
+
756 id=flattened_dict.pop(
"__id"),
+
757 properties=flattened_dict,
+
+
759 all_features.append(feature)
+
+
761 return geojson.FeatureCollection(features=all_features)
+
+
+
764 async
def get_entities_data(
+
765 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
767 dataset_name: str =
"features",
+
768 fields: str =
"__system/updatedAt, osm_id, status, task_id",
+
+
770 """Get all the entity mapping statuses.
+
+
772 No geometries are included.
+
+
+
775 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
776 odk_id (str): The project ID in ODK Central.
+
777 dataset_name (str): The dataset / Entity list name in ODK Central.
+
778 fields (str): Extra fields to include in $select filter.
+
779 __id is included by default.
+
+
+
782 list: JSON list containing Entity info. If updated_at is included,
+
783 the format is string 2022-01-31T23:59:59.999Z.
+
+
785 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
786 entities = await odk_central.getEntityData(
+
+
+
789 url_params=f
"$select=__id{',' if fields else ''} {fields}",
+
+
+
+
793 for entity
in entities:
+
+
795 flatten_json(entity, flattened_dict)
+
+
+
798 flattened_dict[
"id"] = flattened_dict.pop(
"__id")
+
799 all_entities.append(flattened_dict)
+
+
+
+
+
804 def entity_to_flat_dict(
+
805 entity: Optional[dict],
+
+
+
808 dataset_name: str =
"features",
+
+
810 """Convert returned Entity from ODK Central to flattened dict."""
+
+
+
813 status_code=HTTPStatus.NOT_FOUND,
+
+
815 f
"Entity ({entity_uuid}) not found in ODK project ({odk_id}) "
+
816 f
"and dataset ({dataset_name})"
+
+
+
+
+
821 entity.get(
"currentVersion", {}).pop(
"dataReceived")
+
+
823 flatten_json(entity, flattened_dict)
+
+
+
826 flattened_dict[
"id"] = flattened_dict.pop(
"uuid")
+
+
828 return flattened_dict
+
+
+
831 async
def get_entity_mapping_status(
+
832 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
+
835 dataset_name: str =
"features",
+
+
837 """Get an single entity mapping status.
+
+
839 No geometries are included.
+
+
+
842 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
843 odk_id (str): The project ID in ODK Central.
+
844 dataset_name (str): The dataset / Entity list name in ODK Central.
+
845 entity_uuid (str): The unique entity UUID for ODK Central.
+
+
+
848 dict: JSON containing Entity: id, status, updated_at.
+
849 updated_at is in string format 2022-01-31T23:59:59.999Z.
+
+
851 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
852 entity = await odk_central.getEntity(
+
+
+
+
+
857 return entity_to_flat_dict(entity, odk_id, entity_uuid, dataset_name)
+
+
+
860 async
def update_entity_mapping_status(
+
861 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
+
+
+
866 dataset_name: str =
"features",
+
+
868 """Update the Entity mapping status.
+
+
870 This includes both the 'label' and 'status' data field.
+
+
+
873 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
874 odk_id (str): The project ID in ODK Central.
+
875 entity_uuid (str): The unique entity UUID for ODK Central.
+
876 label (str): New label, with emoji prepended for status.
+
877 status (TaskStatus): New TaskStatus to assign, in string form.
+
878 dataset_name (str): Override the default dataset / Entity list name 'features'.
+
+
+
881 dict: All Entity data in OData JSON format.
+
+
883 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
884 entity = await odk_central.updateEntity(
+
+
+
+
+
+
+
+
+
893 return entity_to_flat_dict(entity, odk_id, entity_uuid, dataset_name)
+
+
+
+
+
+
+
900 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
902 """Upload a data file to Central."""
+
903 xform = get_odk_form(odk_central)
+
904 xform.uploadMedia(project_id, xform_id, filespec)
+
+
+
+
+
+
910 filename: str =
"test",
+
911 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
913 """Upload a data file to Central."""
+
914 xform = get_odk_form(odk_central)
+
915 xform.getMedia(project_id, xform_id, filename)
+
+
+
+
+
+
+
922 """Convert ODK CSV to OSM XML and GeoJson."""
+
923 csvin = CSVDump(
"/xforms.yaml")
+
+
925 osmoutfile = f
"{filespec}.osm"
+
926 csvin.createOSM(osmoutfile)
+
+
928 jsonoutfile = f
"{filespec}.geojson"
+
929 csvin.createGeoJson(jsonoutfile)
+
+
+
932 log.debug(
"Parsing csv file %r" % filespec)
+
+
934 data = csvin.parse(filespec)
+
+
936 csvdata = csvin.parse(filespec, data)
+
937 for entry
in csvdata:
+
938 log.debug(f
"Parsing csv data {entry}")
+
+
+
941 feature = csvin.createEntry(entry)
+
+
+
944 if "tags" not in feature:
+
945 log.warning(
"Bad record! %r" % feature)
+
+
947 if "lat" not in feature[
"attrs"]:
+
+
+
+
951 csvin.writeOSM(feature)
+
+
953 csvin.writeGeoJson(feature)
+
+
+
+
957 csvin.finishGeoJson()
+
+
+
+
+
962 async
def get_appuser_token(
+
+
+
965 odk_credentials: project_schemas.ODKCentralDecrypted,
+
+
+
968 """Get the app user token for a specific project.
+
+
+
971 db: The database session to use.
+
972 odk_credentials: ODK credentials for the project.
+
973 project_odk_id: The ODK ID of the project.
+
974 xform_id: The ID of the XForm.
+
+
+
+
+
+
980 appuser = get_odk_app_user(odk_credentials)
+
981 odk_project = get_odk_project(odk_credentials)
+
982 odk_app_user = odk_project.listAppUsers(project_odk_id)
+
+
+
+
986 app_user_id = odk_app_user[0].get(
"id")
+
987 appuser.delete(project_odk_id, app_user_id)
+
+
+
990 appuser_name =
"fmtm_user"
+
+
992 f
"Creating ODK appuser ({appuser_name}) for ODK project ({project_odk_id})"
+
+
994 appuser_json = appuser.create(project_odk_id, appuser_name)
+
995 appuser_token = appuser_json.get(
"token")
+
996 appuser_id = appuser_json.get(
"id")
+
+
998 odk_url = odk_credentials.odk_central_url
+
+
+
1001 log.info(
"Updating XForm role for appuser in ODK Central")
+
1002 response = appuser.updateRole(
+
1003 projectId=project_odk_id,
+
+
+
+
+
+
1009 json_data = response.json()
+
1010 log.error(json_data)
+
1011 except json.decoder.JSONDecodeError:
+
+
1013 "Could not parse response json during appuser update. "
+
1014 f
"status_code={response.status_code}"
+
+
+
1017 msg = f
"Failed to update appuser for formId: ({xform_id})"
+
+
1019 raise HTTPException(
+
1020 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
1022 odk_token = encrypt_value(
+
1023 f
"{odk_url}/v1/key/{appuser_token}/projects/{project_odk_id}"
+
+
+
+
1027 except Exception
as e:
+
1028 log.error(f
"An error occurred: {str(e)}")
+
1029 raise HTTPException(
+
1030 status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+
1031 detail=
"An error occurred while creating the app user token.",
+
+
+
+
+
+
+