+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.OdkCentral
import OdkAppUser, OdkForm, OdkProject
+
33 from pyxform.builder
import create_survey_element_from_dict
+
34 from pyxform.xls2json
import parse_file_to_json
+
35 from sqlalchemy
import text
+
36 from sqlalchemy.orm
import Session
+
+
+
+
+
41 geojson_to_javarosa_geom,
+
42 javarosa_to_geojson_geom,
+
43 parse_geojson_file_to_featcol,
+
+
+
+
+
+
49 def get_odk_project(odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None):
+
50 """Helper function to get the OdkProject with credentials."""
+
+
52 url = odk_central.odk_central_url
+
53 user = odk_central.odk_central_user
+
54 pw = odk_central.odk_central_password
+
+
56 log.debug(
"ODKCentral connection variables not set in function")
+
57 log.debug(
"Attempting extraction from environment variables")
+
58 url = settings.ODK_CENTRAL_URL
+
59 user = settings.ODK_CENTRAL_USER
+
60 pw = settings.ODK_CENTRAL_PASSWD
+
+
+
63 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
64 project = OdkProject(url, user, pw)
+
+
66 except ValueError
as e:
+
+
+
+
+
71 ODK credentials are invalid, or may have been updated. Please update them.
+
+
+
74 except Exception
as e:
+
+
+
77 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
83 def get_odk_form(odk_central: project_schemas.ODKCentralDecrypted):
+
84 """Helper function to get the OdkForm with credentials."""
+
85 url = odk_central.odk_central_url
+
86 user = odk_central.odk_central_user
+
87 pw = odk_central.odk_central_password
+
+
+
90 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
91 form = OdkForm(url, user, pw)
+
92 except Exception
as e:
+
+
+
95 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
101 def get_odk_app_user(odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None):
+
102 """Helper function to get the OdkAppUser with credentials."""
+
+
104 url = odk_central.odk_central_url
+
105 user = odk_central.odk_central_user
+
106 pw = odk_central.odk_central_password
+
+
108 log.debug(
"ODKCentral connection variables not set in function")
+
109 log.debug(
"Attempting extraction from environment variables")
+
110 url = settings.ODK_CENTRAL_URL
+
111 user = settings.ODK_CENTRAL_USER
+
112 pw = settings.ODK_CENTRAL_PASSWD
+
+
+
115 log.debug(f
"Connecting to ODKCentral: url={url} user={user}")
+
116 form = OdkAppUser(url, user, pw)
+
117 except Exception
as e:
+
+
+
120 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
+
+
126 def list_odk_projects(
+
127 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
129 """List all projects on a remote ODK Server."""
+
130 project = get_odk_project(odk_central)
+
131 return project.listProjects()
+
+
+
134 def create_odk_project(
+
135 name: str, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
137 """Create a project on a remote ODK Server.
+
+
139 Appends FMTM to the project name to help identify on shared servers.
+
+
141 project = get_odk_project(odk_central)
+
+
+
144 log.debug(f
"Attempting ODKCentral project creation: FMTM {name}")
+
145 result = project.createProject(f
"FMTM {name}")
+
+
+
148 if isinstance(result, dict):
+
149 if result.get(
"code") == 401.2:
+
+
+
152 detail=
"Could not authenticate to odk central.",
+
+
+
155 log.debug(f
"ODKCentral response: {result}")
+
156 log.info(f
"Project {name} available on the ODK Central server.")
+
+
158 except Exception
as e:
+
+
+
161 status_code=500, detail=f
"Error creating project on ODK Central: {e}"
+
+
+
+
165 async
def delete_odk_project(
+
166 project_id: int, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
168 """Delete a project from a remote ODK Server."""
+
+
+
+
172 project = get_odk_project(odk_central)
+
173 result = project.deleteProject(project_id)
+
174 log.info(f
"Project {project_id} has been deleted from the ODK Central server.")
+
+
+
177 return "Could not delete project from central odk"
+
+
+
180 def delete_odk_app_user(
+
+
+
183 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
185 """Delete an app-user from a remote ODK Server."""
+
186 odk_app_user = get_odk_app_user(odk_central)
+
187 result = odk_app_user.delete(project_id, name)
+
+
+
+
191 def create_odk_xform(
+
+
+
194 odk_credentials: project_schemas.ODKCentralDecrypted,
+
+
196 """Create an XForm on a remote ODK Central server.
+
+
+
199 odk_id (str): Project ID for ODK Central.
+
200 xform_data (BytesIO): XForm data to set.
+
201 odk_credentials (ODKCentralDecrypted): Creds for ODK Central.
+
+
+
204 form_name (str): ODK Central form name for the API.
+
+
+
207 xform = get_odk_form(odk_credentials)
+
208 except Exception
as e:
+
+
+
211 status_code=500, detail={
"message":
"Connection failed to odk central"}
+
+
+
214 xform_id = xform.createForm(odk_id, xform_data, publish=
True)
+
+
+
217 "h":
"http://www.w3.org/1999/xhtml",
+
218 "odk":
"http://www.opendatakit.org/xforms",
+
219 "xforms":
"http://www.w3.org/2002/xforms",
+
+
+
222 root = ElementTree.fromstring(xform_data.getvalue())
+
223 xml_data = root.findall(
".//xforms:data[@id]", namespaces)
+
224 extracted_name =
"Not Found"
+
+
226 extracted_name = dt.get(
"id")
+
227 msg = f
"Failed to create form on ODK Central: ({extracted_name})"
+
+
+
230 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
+
235 def delete_odk_xform(
+
+
+
238 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
240 """Delete an XForm from a remote ODK Central server."""
+
241 xform = get_odk_form(odk_central)
+
242 result = xform.deleteForm(project_id, xform_id)
+
+
+
+
+
+
+
249 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
250 metadata: bool =
False,
+
+
252 """List all XForms in an ODK Central project."""
+
253 project = get_odk_project(odk_central)
+
254 xforms = project.listForms(project_id, metadata)
+
+
+
+
+
259 def get_form_full_details(
+
+
+
262 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
264 """Get additional metadata for ODK Form."""
+
265 form = get_odk_form(odk_central)
+
266 form_details = form.getFullDetails(odk_project_id, form_id)
+
+
+
+
270 def get_odk_project_full_details(
+
271 odk_project_id: int, odk_central: project_schemas.ODKCentralDecrypted
+
+
273 """Get additional metadata for ODK project."""
+
274 project = get_odk_project(odk_central)
+
275 project_details = project.getFullDetails(odk_project_id)
+
276 return project_details
+
+
+
279 def list_submissions(
+
280 project_id: int, odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None
+
+
282 """List all submissions for a project, aggregated from associated users."""
+
283 project = get_odk_project(odk_central)
+
284 xform = get_odk_form(odk_central)
+
+
286 for user
in project.listAppUsers(project_id):
+
287 for subm
in xform.listSubmissions(project_id, user[
"displayName"]):
+
288 submissions.append(subm)
+
+
+
+
+
293 async
def get_form_list(db: Session) -> list:
+
294 """Returns the list of {id:title} for XLSForms in the database."""
+
+
296 include_categories = [category.value
for category
in XLSFormType]
+
+
+
+
300 SELECT id, title FROM xlsforms
+
+
302 (SELECT UNNEST(:categories));
+
+
+
+
306 result = db.execute(sql_query, {
"categories": include_categories}).fetchall()
+
307 result_list = [{
"id": row.id,
"title": row.title}
for row
in result]
+
+
+
310 except Exception
as e:
+
+
+
313 status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+
+
+
+
+
318 async
def update_project_xform(
+
+
+
+
+
+
+
325 odk_credentials: project_schemas.ODKCentralDecrypted,
+
+
327 """Update and publish the XForm for a project.
+
+
+
330 xform_id (str): The UUID of the existing XForm in ODK Central.
+
331 odk_id (int): ODK Central form ID.
+
332 xform_data (BytesIO): XForm data.
+
333 form_file_ext (str): Extension of the form file.
+
334 category (str): Category of the XForm.
+
335 task_count (int): The number of tasks in a project.
+
336 odk_credentials (project_schemas.ODKCentralDecrypted): ODK Central creds.
+
+
338 xform_data = await read_and_test_xform(
+
+
+
341 return_form_data=
True,
+
+
343 updated_xform_data = await modify_xform_xml(
+
+
+
+
347 existing_id=xform_id,
+
+
+
350 xform_obj = get_odk_form(odk_credentials)
+
+
+
353 xform_obj.createForm(
+
+
+
+
+
+
359 xform_obj.publishForm(odk_id, xform_id)
+
+
+
362 async
def read_and_test_xform(
+
+
+
365 return_form_data: bool =
False,
+
+
367 """Read and validate an XForm.
+
+
+
370 input_data (BytesIO): form to be tested.
+
371 form_file_ext (str): type of form (.xls, .xlsx, or .xml).
+
372 return_form_data (bool): return the XForm data.
+
+
+
375 file_ext = form_file_ext.lower()
+
+
377 if file_ext ==
".xml":
+
378 xform_bytesio = input_data
+
+
+
381 ElementTree.fromstring(xform_bytesio.getvalue())
+
382 except ElementTree.ParseError
as e:
+
+
384 msg = f
"Error parsing XForm XML: Possible reason: {str(e)}"
+
+
386 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
390 log.debug(
"Converting xlsform -> xform")
+
391 json_data = parse_file_to_json(
+
392 path=
"/dummy/path/with/file/ext.xls",
+
393 file_object=input_data,
+
+
395 generated_xform = create_survey_element_from_dict(json_data)
+
+
397 xform_bytesio = BytesIO(
+
398 generated_xform.to_xml(
+
+
+
+
+
403 except Exception
as e:
+
+
405 msg = f
"XLSForm is invalid: {str(e)}"
+
+
407 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
+
+
+
+
+
+
+
+
415 xform_xml = ElementTree.fromstring(xform_bytesio.getvalue())
+
+
+
+
419 namespaces = {
"xforms":
"http://www.w3.org/2002/xforms"}
+
+
421 os.path.splitext(inst.attrib[
"src"].split(
"/")[-1])[0]
+
422 for inst
in xform_xml.findall(
".//xforms:instance[@src]", namespaces)
+
423 if inst.attrib.get(
"src",
"").endswith(
".csv")
+
+
+
+
+
+
429 "The form has no select_one_from_file or "
+
430 "select_multiple_from_file field defined for a CSV."
+
+
432 raise ValueError(msg)
from None
+
+
434 return {
"required_media": csv_list,
"message":
"Your form is valid"}
+
+
436 except Exception
as e:
+
+
+
439 status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=str(e)
+
+
+
+
443 async
def modify_xform_xml(
+
+
+
+
447 existing_id: Optional[str] =
None,
+
+
449 """Update fields in the XForm to work with FMTM.
+
+
451 The 'id' field is set to random UUID (xFormId) unless existing_id is specified
+
452 The 'name' field is set to the category name.
+
453 The upload media must be equal to 'features.csv'.
+
454 The task_filter options are populated as choices in the form.
+
455 The form_category value is also injected to display in the instructions.
+
+
+
458 form_data (str): The input form data.
+
459 category (str): The form category, used to name the dataset (entity list)
+
460 and the .csv file containing the geometries.
+
461 task_count (int): The number of tasks in a project.
+
462 existing_id (str): An existing XForm ID in ODK Central, for updating.
+
+
+
465 BytesIO: The XForm data.
+
+
467 log.debug(f
"Updating XML keys in survey XForm: {category}")
+
+
+
470 xform_id = existing_id
+
+
472 xform_id = uuid.uuid4()
+
+
+
475 "h":
"http://www.w3.org/1999/xhtml",
+
476 "odk":
"http://www.opendatakit.org/xforms",
+
477 "xforms":
"http://www.w3.org/2002/xforms",
+
478 "entities":
"http://www.opendatakit.org/xforms/entities",
+
+
+
+
482 root = ElementTree.fromstring(form_data.getvalue())
+
+
484 xform_data = root.findall(
".//xforms:data[@id]", namespaces)
+
485 for dt
in xform_data:
+
+
487 dt.set(
"id", str(xform_id))
+
+
+
490 existing_title = root.find(
".//h:title", namespaces)
+
491 if existing_title
is not None:
+
492 existing_title.text = category
+
+
+
495 xform_instance_src = root.findall(
".//xforms:instance[@src]", namespaces)
+
496 for inst
in xform_instance_src:
+
497 src_value = inst.get(
"src",
"")
+
498 if src_value.endswith(
".geojson")
or src_value.endswith(
".csv"):
+
+
+
501 inst.set(
"src",
"jr://file-csv/features.csv")
+
+
+
+
505 model_element = root.find(
".//xforms:model", namespaces)
+
+
507 existing_instance = model_element.find(
+
508 ".//xforms:instance[@id='task_filter']", namespaces
+
+
510 if existing_instance
is not None:
+
511 model_element.remove(existing_instance)
+
+
513 instance_task_filters = Element(
"instance", id=
"task_filter")
+
514 root_element = SubElement(instance_task_filters,
"root")
+
+
516 for task_id
in range(1, task_count + 1):
+
517 item = SubElement(root_element,
"item")
+
518 SubElement(item,
"itextId").text = f
"task_filter-{task_id}"
+
519 SubElement(item,
"name").text = str(task_id)
+
520 model_element.append(instance_task_filters)
+
+
+
523 itext_element = root.find(
".//xforms:itext", namespaces)
+
524 if itext_element
is not None:
+
525 existing_translations = itext_element.findall(
+
526 ".//xforms:translation", namespaces
+
+
528 for translation
in existing_translations:
+
+
530 existing_text = translation.find(
+
531 ".//xforms:text[@id='task_filter-0']", namespaces
+
+
533 if existing_text
is not None:
+
534 translation.remove(existing_text)
+
+
+
537 for task_id
in range(1, task_count + 1):
+
538 new_text = Element(
"text", id=f
"task_filter-{task_id}")
+
539 value_element = Element(
"value")
+
540 value_element.text = str(task_id)
+
541 new_text.append(value_element)
+
542 translation.append(new_text)
+
+
+
545 form_category_update = root.find(
+
546 ".//xforms:bind[@nodeset='/data/form_category']", namespaces
+
+
548 if form_category_update
is not None:
+
549 if category.endswith(
"s"):
+
+
551 category = category[:-1]
+
552 form_category_update.set(
"calculate", f
"once('{category.rstrip('s')}')")
+
+
554 return BytesIO(ElementTree.tostring(root))
+
+
+
557 async
def convert_geojson_to_odk_csv(
+
558 input_geojson: BytesIO,
+
+
560 """Convert GeoJSON features to ODK CSV format.
+
+
562 Used for form upload media (dataset) in ODK Central.
+
+
+
565 input_geojson (BytesIO): GeoJSON file to convert.
+
+
+
568 feature_csv (StringIO): CSV of features in XLSForm format for ODK.
+
+
570 parsed_geojson = parse_geojson_file_to_featcol(input_geojson.getvalue())
+
+
572 if not parsed_geojson:
+
+
574 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+
575 detail=
"Conversion GeoJSON --> CSV failed",
+
+
+
578 csv_buffer = StringIO()
+
579 csv_writer = csv.writer(csv_buffer)
+
+
581 header = [
"osm_id",
"tags",
"version",
"changeset",
"timestamp",
"geometry"]
+
582 csv_writer.writerow(header)
+
+
584 features = parsed_geojson.get(
"features", [])
+
585 for feature
in features:
+
586 geometry = feature.get(
"geometry")
+
587 javarosa_geom = await geojson_to_javarosa_geom(geometry)
+
+
589 properties = feature.get(
"properties", {})
+
590 osm_id = properties.get(
"osm_id")
+
591 tags = properties.get(
"tags")
+
592 version = properties.get(
"version")
+
593 changeset = properties.get(
"changeset")
+
594 timestamp = properties.get(
"timestamp")
+
+
596 csv_row = [osm_id, tags, version, changeset, timestamp, javarosa_geom]
+
597 csv_writer.writerow(csv_row)
+
+
+
+
+
+
+
+
605 def flatten_json(data: dict, target: dict):
+
606 """Flatten json properties to a single level.
+
+
608 Removes any existing GeoJSON data from captured GPS coordinates in
+
+
+
+
+
613 flatten_json(original_dict, new_dict)
+
+
615 for k, v
in data.items():
+
616 if isinstance(v, dict):
+
617 if "type" in v
and "coordinates" in v:
+
+
+
620 flatten_json(v, target)
+
+
+
+
+
625 async
def convert_odk_submission_json_to_geojson(
+
626 input_json: Union[BytesIO, list],
+
627 ) -> geojson.FeatureCollection:
+
628 """Convert ODK submission JSON file to GeoJSON.
+
+
630 Used for loading into QGIS.
+
+
+
633 input_json (BytesIO): ODK JSON submission list.
+
+
+
636 geojson (BytesIO): GeoJSON format ODK submission.
+
+
638 if isinstance(input_json, list):
+
639 submission_json = input_json
+
+
641 submission_json = json.loads(input_json.getvalue())
+
+
643 if not submission_json:
+
+
645 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
+
646 detail=
"Project contains no submissions yet",
+
+
+
+
650 for submission
in submission_json:
+
651 keys_to_remove = [
"meta",
"__id",
"__system"]
+
652 for key
in keys_to_remove:
+
+
+
+
656 flatten_json(submission, data)
+
+
658 geojson_geom = await javarosa_to_geojson_geom(
+
659 data.pop(
"xlocation", {}), geom_type=
"Polygon"
+
+
+
662 feature = geojson.Feature(geometry=geojson_geom, properties=data)
+
663 all_features.append(feature)
+
+
665 return geojson.FeatureCollection(features=all_features)
+
+
+
668 async
def get_entities_geojson(
+
669 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
671 dataset_name: str =
"features",
+
672 minimal: Optional[bool] =
False,
+
673 ) -> geojson.FeatureCollection:
+
674 """Get the Entity details for a dataset / Entity list.
+
+
676 Uses the OData endpoint from ODK Central.
+
+
678 Currently it is not possible to filter via OData filters on custom params.
+
679 TODO in the future filter by task_id via the URL,
+
680 instead of returning all and filtering.
+
+
682 Response GeoJSON format:
+
+
684 "type": "FeatureCollection",
+
+
+
+
+
+
+
691 "id": uuid_of_entity,
+
+
693 "updated_at": "2024-04-11T18:23:30.787Z",
+
+
+
+
+
+
+
700 "timestamp": "2024-12-20",
+
701 "status": "LOCKED_FOR_MAPPING"
+
+
+
+
+
+
707 Response GeoJSON format, minimal:
+
+
709 "type": "FeatureCollection",
+
+
+
+
+
+
+
716 "id": uuid_of_entity,
+
+
+
719 "updated_at": "2024-04-11T18:23:30.787Z",
+
720 "status": "LOCKED_FOR_MAPPING"
+
+
+
+
+
+
726 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
727 odk_id (str): The project ID in ODK Central.
+
728 dataset_name (str): The dataset / Entity list name in ODK Central.
+
729 minimal (bool): Remove all fields apart from id, updated_at, and status.
+
+
+
732 dict: Entity data in OData JSON format.
+
+
734 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
735 entities = await odk_central.getEntityData(
+
+
+
738 url_params=
"$select=__id, __system/updatedAt, geometry, osm_id, status"
+
+
+
+
+
+
744 for entity
in entities:
+
+
746 flatten_json(entity, flattened_dict)
+
+
748 javarosa_geom = flattened_dict.pop(
"geometry")
or ""
+
749 geojson_geom = await javarosa_to_geojson_geom(
+
750 javarosa_geom, geom_type=
"Polygon"
+
+
+
753 feature = geojson.Feature(
+
754 geometry=geojson_geom,
+
755 id=flattened_dict.pop(
"__id"),
+
756 properties=flattened_dict,
+
+
758 all_features.append(feature)
+
+
760 return geojson.FeatureCollection(features=all_features)
+
+
+
763 async
def get_entities_data(
+
764 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
766 dataset_name: str =
"features",
+
767 fields: str =
"__system/updatedAt, osm_id, status, task_id",
+
+
769 """Get all the entity mapping statuses.
+
+
771 No geometries are included.
+
+
+
774 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
775 odk_id (str): The project ID in ODK Central.
+
776 dataset_name (str): The dataset / Entity list name in ODK Central.
+
777 fields (str): Extra fields to include in $select filter.
+
778 __id is included by default.
+
+
+
781 list: JSON list containing Entity info. If updated_at is included,
+
782 the format is string 2022-01-31T23:59:59.999Z.
+
+
784 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
785 entities = await odk_central.getEntityData(
+
+
+
788 url_params=f
"$select=__id{',' if fields else ''} {fields}",
+
+
+
+
792 for entity
in entities:
+
+
794 flatten_json(entity, flattened_dict)
+
+
+
797 flattened_dict[
"id"] = flattened_dict.pop(
"__id")
+
798 all_entities.append(flattened_dict)
+
+
+
+
+
803 def entity_to_flat_dict(
+
804 entity: Optional[dict],
+
+
+
807 dataset_name: str =
"features",
+
+
809 """Convert returned Entity from ODK Central to flattened dict."""
+
+
+
812 status_code=HTTPStatus.NOT_FOUND,
+
+
814 f
"Entity ({entity_uuid}) not found in ODK project ({odk_id}) "
+
815 f
"and dataset ({dataset_name})"
+
+
+
+
+
820 entity.get(
"currentVersion", {}).pop(
"dataReceived")
+
+
822 flatten_json(entity, flattened_dict)
+
+
+
825 flattened_dict[
"id"] = flattened_dict.pop(
"uuid")
+
+
827 return flattened_dict
+
+
+
830 async
def get_entity_mapping_status(
+
831 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
+
834 dataset_name: str =
"features",
+
+
836 """Get an single entity mapping status.
+
+
838 No geometries are included.
+
+
+
841 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
842 odk_id (str): The project ID in ODK Central.
+
843 dataset_name (str): The dataset / Entity list name in ODK Central.
+
844 entity_uuid (str): The unique entity UUID for ODK Central.
+
+
+
847 dict: JSON containing Entity: id, status, updated_at.
+
848 updated_at is in string format 2022-01-31T23:59:59.999Z.
+
+
850 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
851 entity = await odk_central.getEntity(
+
+
+
+
+
856 return entity_to_flat_dict(entity, odk_id, entity_uuid, dataset_name)
+
+
+
859 async
def update_entity_mapping_status(
+
860 odk_creds: project_schemas.ODKCentralDecrypted,
+
+
+
+
+
865 dataset_name: str =
"features",
+
+
867 """Update the Entity mapping status.
+
+
869 This includes both the 'label' and 'status' data field.
+
+
+
872 odk_creds (ODKCentralDecrypted): ODK credentials for a project.
+
873 odk_id (str): The project ID in ODK Central.
+
874 entity_uuid (str): The unique entity UUID for ODK Central.
+
875 label (str): New label, with emoji prepended for status.
+
876 status (TaskStatus): New TaskStatus to assign, in string form.
+
877 dataset_name (str): Override the default dataset / Entity list name 'features'.
+
+
+
880 dict: All Entity data in OData JSON format.
+
+
882 async
with central_deps.get_odk_entity(odk_creds)
as odk_central:
+
883 entity = await odk_central.updateEntity(
+
+
+
+
+
+
+
+
+
892 return entity_to_flat_dict(entity, odk_id, entity_uuid, dataset_name)
+
+
+
+
+
+
+
899 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
901 """Upload a data file to Central."""
+
902 xform = get_odk_form(odk_central)
+
903 xform.uploadMedia(project_id, xform_id, filespec)
+
+
+
+
+
+
909 filename: str =
"test",
+
910 odk_central: Optional[project_schemas.ODKCentralDecrypted] =
None,
+
+
912 """Upload a data file to Central."""
+
913 xform = get_odk_form(odk_central)
+
914 xform.getMedia(project_id, xform_id, filename)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.",
+
+
+
+
+
+
+