Skip to content

Commit

Permalink
add: support offline entities spec v2024.1.0 via opt-in setting (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
lindsay-stevens committed Sep 5, 2024
1 parent 66283b6 commit d209a84
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 16 deletions.
4 changes: 3 additions & 1 deletion pyxform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@

# The ODK entities spec version that generated forms comply to
ENTITIES_CREATE_VERSION = "2022.1.0"
CURRENT_ENTITIES_VERSION = "2023.1.0"
ENTITIES_UPDATE_VERSION = "2023.1.0"
ENTITIES_OFFLINE_VERSION = "2024.1.0"
ENTITY = "entity"
ENTITY_FEATURES = "entity_features"
ENTITIES_RESERVED_PREFIX = "__"
Expand All @@ -126,6 +127,7 @@ class EntityColumns(StrEnum):
CREATE_IF = "create_if"
UPDATE_IF = "update_if"
LABEL = "label"
OFFLINE = "offline"


DEPRECATED_DEVICE_ID_METADATA_FIELDS = ["subscriberid", "simserial"]
Expand Down
3 changes: 3 additions & 0 deletions pyxform/entities/entities_parsing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from pyxform import constants as const
from pyxform.aliases import yes_no
from pyxform.errors import PyXFormError
from pyxform.xlsparseutils import find_sheet_misspellings, is_valid_xml_tag

Expand Down Expand Up @@ -28,6 +29,7 @@ def get_entity_declaration(
create_condition = entity_row.get(EC.CREATE_IF, None)
update_condition = entity_row.get(EC.UPDATE_IF, None)
entity_label = entity_row.get(EC.LABEL, None)
offline = yes_no.get(entity_row.get(EC.OFFLINE, None), None)

if update_condition and not entity_id:
raise PyXFormError(
Expand All @@ -53,6 +55,7 @@ def get_entity_declaration(
EC.CREATE_IF: create_condition,
EC.UPDATE_IF: update_condition,
EC.LABEL: entity_label,
EC.OFFLINE: offline,
},
}

Expand Down
39 changes: 27 additions & 12 deletions pyxform/entities/entity_declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,28 @@ class EntityDeclaration(SurveyElement):
"""

def xml_instance(self, **kwargs):
parameters = self.get(const.PARAMETERS, {})

attributes = {
EC.DATASET.value: self.get(const.PARAMETERS, {}).get(EC.DATASET, ""),
EC.DATASET.value: parameters.get(EC.DATASET, ""),
"id": "",
}

entity_id_expression = self.get(const.PARAMETERS, {}).get(EC.ENTITY_ID, None)
create_condition = self.get(const.PARAMETERS, {}).get(EC.CREATE_IF, None)
update_condition = self.get(const.PARAMETERS, {}).get(EC.UPDATE_IF, None)
entity_id_expression = parameters.get(EC.ENTITY_ID, None)
create_condition = parameters.get(EC.CREATE_IF, None)
update_condition = parameters.get(EC.UPDATE_IF, None)

if entity_id_expression:
attributes["update"] = "1"
attributes["baseVersion"] = ""
if parameters.get(EC.OFFLINE, None):
attributes["trunkVersion"] = ""
attributes["branchId"] = ""

if create_condition or (not update_condition and not entity_id_expression):
attributes["create"] = "1"

if self.get(const.PARAMETERS, {}).get(EC.LABEL, None):
if parameters.get(EC.LABEL, None):
return node(const.ENTITY, node(const.LABEL), **attributes)
else:
return node(const.ENTITY, **attributes)
Expand All @@ -50,10 +55,11 @@ def xml_bindings(self):
See the class comment for an explanation of the logic for generating bindings.
"""
survey = self.get_root()
entity_id_expression = self.get(const.PARAMETERS, {}).get(EC.ENTITY_ID, None)
create_condition = self.get(const.PARAMETERS, {}).get(EC.CREATE_IF, None)
update_condition = self.get(const.PARAMETERS, {}).get(EC.UPDATE_IF, None)
label_expression = self.get(const.PARAMETERS, {}).get(EC.LABEL, None)
parameters = self.get(const.PARAMETERS, {})
entity_id_expression = parameters.get(EC.ENTITY_ID, None)
create_condition = parameters.get(EC.CREATE_IF, None)
update_condition = parameters.get(EC.UPDATE_IF, None)
label_expression = parameters.get(EC.LABEL, None)

bind_nodes = []

Expand All @@ -69,11 +75,20 @@ def xml_bindings(self):
bind_nodes.append(self._get_bind_node(survey, update_condition, "/@update"))

if entity_id_expression:
dataset_name = self.get(const.PARAMETERS, {}).get(EC.DATASET, "")
base_version_expression = f"instance('{dataset_name}')/root/item[name={entity_id_expression}]/__version"
dataset_name = parameters.get(EC.DATASET, "")
entity = f"instance('{dataset_name}')/root/item[name={entity_id_expression}]"
bind_nodes.append(
self._get_bind_node(survey, base_version_expression, "/@baseVersion")
self._get_bind_node(survey, f"{entity}/__version", "/@baseVersion")
)
if parameters.get(EC.OFFLINE, None):
bind_nodes.append(
self._get_bind_node(
survey, f"{entity}/__trunkVersion", "/@trunkVersion"
)
)
bind_nodes.append(
self._get_bind_node(survey, f"{entity}/__branchId", "/@branchId")
)

if label_expression:
bind_nodes.append(self._get_bind_node(survey, label_expression, "/label"))
Expand Down
8 changes: 6 additions & 2 deletions pyxform/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,13 @@ def xml_model(self):

entity_features = getattr(self, constants.ENTITY_FEATURES, [])
if len(entity_features) > 0:
if "update" in entity_features:
if "offline" in entity_features:
model_kwargs["entities:entities-version"] = (
constants.CURRENT_ENTITIES_VERSION
constants.ENTITIES_OFFLINE_VERSION
)
elif "update" in entity_features:
model_kwargs["entities:entities-version"] = (
constants.ENTITIES_UPDATE_VERSION
)
else:
model_kwargs["entities:entities-version"] = (
Expand Down
6 changes: 5 additions & 1 deletion pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1538,10 +1538,14 @@ def workbook_to_json(

if len(entity_declaration) > 0:
json_dict[constants.ENTITY_FEATURES] = ["create"]
entity_parameters = entity_declaration.get(constants.PARAMETERS, {})

if entity_declaration.get("parameters", {}).get("entity_id", None):
if entity_parameters.get(constants.EntityColumns.ENTITY_ID, None):
json_dict[constants.ENTITY_FEATURES].append("update")

if entity_parameters.get(constants.EntityColumns.OFFLINE, None):
json_dict[constants.ENTITY_FEATURES].append("offline")

meta_children.append(entity_declaration)

if len(meta_children) > 0:
Expand Down
42 changes: 42 additions & 0 deletions tests/test_entities_create.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pyxform import constants as co

from tests.pyxform_test_case import PyxformTestCase


Expand Down Expand Up @@ -438,3 +440,43 @@ def test_entities_columns__multiple_unexpected(self):
"'why'",
],
)

def test_entities_offline_opt_in__yes(self):
"""Should find offline spec version, if opted-in."""
self.assertPyxformXform(
md="""
| survey |
| | type | name | label |
| | text | a | A |
| entities |
| | dataset | label | offline |
| | trees | a | yes |
""",
xml__xpath_match=[
f"""
/h:html/h:head/x:model[
@entities:entities-version = "{co.ENTITIES_OFFLINE_VERSION}"
]
""",
],
)

def test_entities_offline_opt_in__no(self):
"""Should find create spec version, if not opted-in."""
self.assertPyxformXform(
md="""
| survey |
| | type | name | label |
| | text | a | A |
| entities |
| | dataset | label | offline |
| | trees | a | no |
""",
xml__xpath_match=[
f"""
/h:html/h:head/x:model[
@entities:entities-version = "{co.ENTITIES_CREATE_VERSION}"
]
""",
],
)
94 changes: 94 additions & 0 deletions tests/test_entities_update.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pyxform import constants as co

from tests.pyxform_test_case import PyxformTestCase


Expand Down Expand Up @@ -174,3 +176,95 @@ def test_save_to_with_entity_id__puts_save_tos_on_bind(self):
'/h:html/h:head/x:model/x:bind[@nodeset = "/data/a" and @entities:saveto = "foo"]'
],
)

def test_entities_offline_opt_in__yes(self):
"""Should find offline spec version and trunk/branch props/binds, if opted-in."""
self.assertPyxformXform(
md="""
| survey |
| | type | name | label |
| | text | id | Tree id |
| | text | q1 | Q1 |
| entities |
| | dataset | entity_id | offline |
| | trees | ${id} | yes |
""",
xml__xpath_match=[
f"""
/h:html/h:head/x:model[
@entities:entities-version = "{co.ENTITIES_OFFLINE_VERSION}"
]
""",
"""
/h:html/h:head/x:model/x:instance/x:test_name/x:meta/x:entity[
@trunkVersion = ''
and @branchId = ''
]
""",
"""
/h:html/h:head/x:model/x:bind[
@nodeset = '/test_name/meta/entity/@trunkVersion'
and @calculate = "instance('trees')/root/item[name= /test_name/id ]/__trunkVersion"
and @type = 'string'
and @readonly = 'true()'
]
""",
"""
/h:html/h:head/x:model/x:bind[
@nodeset = '/test_name/meta/entity/@branchId'
and @calculate = "instance('trees')/root/item[name= /test_name/id ]/__branchId"
and @type = 'string'
and @readonly = 'true()'
]
""",
],
)

def test_entities_offline_opt_in__no(self):
"""Should not find update spec version and trunk/branch props/binds, if not opted-in."""
cases = (
"""
| entities |
| | dataset | entity_id |
| | trees | ${id} |
""",
"""
| entities |
| | dataset | entity_id | offline |
| | trees | ${id} | no |
""",
)
survey = """
| survey |
| | type | name | label |
| | text | id | Tree id |
| | text | q1 | Q1 |
"""
for i, case in enumerate(cases):
with self.subTest(msg=i):
self.assertPyxformXform(
md=survey + case,
xml__xpath_match=[
f"""
/h:html/h:head/x:model[
@entities:entities-version = "{co.ENTITIES_UPDATE_VERSION}"
]
""",
"""
/h:html/h:head/x:model/x:instance/x:test_name/x:meta/x:entity[
not(@trunkVersion)
and not(@branchId)
]
""",
"""
/h:html/h:head/x:model[
not(x:bind[@nodeset = '/test_name/meta/entity/@trunkVersion'])
]
""",
"""
/h:html/h:head/x:model[
not(x:bind[@nodeset = '/test_name/meta/entity/@branchId'])
]
""",
],
)

0 comments on commit d209a84

Please sign in to comment.