diff --git a/pyodk/_endpoints/forms.py b/pyodk/_endpoints/forms.py index 87dcb22..5e3e4a4 100644 --- a/pyodk/_endpoints/forms.py +++ b/pyodk/_endpoints/forms.py @@ -137,8 +137,8 @@ def update( * form attachments only * form attachments with `version_updater` - If a definition is provided, the new version name must be specified in the definition. - If no definition is provided, a default version will be set using + If a definition is provided, the new version name must be specified in the + definition. If no definition is provided, a default version will be set using the current datetime is ISO format. The default datetime version can be overridden by providing a `version_updater` @@ -150,12 +150,12 @@ def update( :param form_id: The xmlFormId of the Form being referenced. :param project_id: The id of the project this form belongs to. - :param definition: The path to a form definition file to upload. The form definition - must include an updated version string. + :param definition: The path to a form definition file to upload. The form + definition must include an updated version string. :param attachments: The paths of the form attachment file(s) to upload. :param version_updater: A function that accepts a version name string and returns - a version name string, which is used for the new form version. Not allowed if a form - definition is specified. + a version name string, which is used for the new form version. Not allowed if a + form definition is specified. """ if definition is None and attachments is None: raise PyODKError("Must specify a form definition and/or attachments.") diff --git a/pyodk/_endpoints/submissions.py b/pyodk/_endpoints/submissions.py index 26f1489..192d6c2 100644 --- a/pyodk/_endpoints/submissions.py +++ b/pyodk/_endpoints/submissions.py @@ -196,6 +196,7 @@ def create( form_id: Optional[str] = None, project_id: Optional[int] = None, device_id: Optional[str] = None, + encoding: str = "utf-8", ) -> Submission: """ Create a Submission. @@ -216,6 +217,7 @@ def create( :param form_id: The xmlFormId of the Form being referenced. :param project_id: The id of the project this form belongs to. :param device_id: An optional deviceID associated with the submission. + :param encoding: The encoding of the submission XML, default "utf-8". """ try: pid = pv.validate_project_id(project_id, self.default_project_id) @@ -233,7 +235,7 @@ def create( logger=log, headers={"Content-Type": "application/xml"}, params=params, - data=xml, + data=xml.encode(encoding=encoding), ) data = response.json() return Submission(**data) @@ -244,27 +246,16 @@ def _put( xml: str, form_id: Optional[str] = None, project_id: Optional[int] = None, + encoding: str = "utf-8", ) -> Submission: """ Update Submission data. - Example submission XML structure: - - ``` - - - uuid:85cb9aff-005e-4edd-9739-dc9c1a829c44 - uuid:315c2f74-c8fc-4606-ae3f-22f8983e441e - - Alice - 36 - - ``` - :param instance_id: The instanceId of the Submission being referenced. :param xml: The submission XML. :param form_id: The xmlFormId of the Form being referenced. :param project_id: The id of the project this form belongs to. + :param encoding: The encoding of the submission XML, default "utf-8". """ try: pid = pv.validate_project_id(project_id, self.default_project_id) @@ -281,7 +272,7 @@ def _put( ), logger=log, headers={"Content-Type": "application/xml"}, - data=xml, + data=xml.encode(encoding=encoding), ) data = response.json() return Submission(**data) @@ -330,18 +321,37 @@ def edit( form_id: Optional[str] = None, project_id: Optional[int] = None, comment: Optional[str] = None, + encoding: str = "utf-8", ) -> None: """ Edit a submission and optionally comment on it. - :param instance_id: The instanceId of the Submission being referenced. + Example edited submission XML structure: + + ``` + + + uuid:85cb9aff-005e-4edd-9739-dc9c1a829c44 + uuid:315c2f74-c8fc-4606-ae3f-22f8983e441e + + Alice + 36 + + ``` + + :param instance_id: The instanceId of the Submission being referenced. The + `instance_id` each Submission is first submitted with will always represent + that Submission as a whole. Each version of the Submission, though, has its own + `instance_id`. So `instance_id` will not necessarily match the values in + the XML elements named `instanceID` and `deprecatedID`. :param xml: The submission XML. :param form_id: The xmlFormId of the Form being referenced. :param project_id: The id of the project this form belongs to. :param comment: The text of the comment. + :param encoding: The encoding of the submission XML, default "utf-8". """ fp_ids = {"form_id": form_id, "project_id": project_id} - self._put(instance_id=instance_id, xml=xml, **fp_ids) + self._put(instance_id=instance_id, xml=xml, encoding=encoding, **fp_ids) if comment is not None: self.add_comment(instance_id=instance_id, comment=comment, **fp_ids) diff --git a/tests/resources/forms/non_ascii_form_id.xlsx b/tests/resources/forms/non_ascii_form_id.xlsx new file mode 100644 index 0000000..fb488cb Binary files /dev/null and b/tests/resources/forms/non_ascii_form_id.xlsx differ diff --git "a/tests/resources/forms/\342\234\205.xlsx" "b/tests/resources/forms/\342\234\205.xlsx" new file mode 100644 index 0000000..ce3274c Binary files /dev/null and "b/tests/resources/forms/\342\234\205.xlsx" differ diff --git a/tests/test_client.py b/tests/test_client.py index 13a4e68..dcce9d5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest import TestCase, skip from pyodk.client import Client @@ -49,6 +50,26 @@ def test_form_update__new_definition_and_attachments(self): attachments=[(RESOURCES / "forms" / "fruits.csv").as_posix()], ) + def test_form_update__new_definition_and_attachments__non_ascii_dingbat(self): + """Should create a new version with new definition and attachment.""" + with Client() as client: + client.forms.update( + form_id="✅", + definition=(RESOURCES / "forms" / "✅.xlsx").as_posix(), + attachments=[(RESOURCES / "forms" / "fruits.csv").as_posix()], + ) + form = client.forms.get("✅") + self.assertEqual(form.xmlFormId, "✅") + + def test_form_update__with_version_updater__non_ascii_specials(self): + """Should create a new version with new definition and attachment.""" + with Client() as client: + client.forms.update( + form_id="'=+/*-451%/%", + attachments=[], + version_updater=lambda v: datetime.now().isoformat(), + ) + def test_form_update__attachments(self): """Should create a new version with new attachment.""" with Client() as client: @@ -78,3 +99,40 @@ def test_project_create_app_users__names_and_forms(self): display_names=["test_assign3", "test_assign_23"], forms=["range", "pull_data"], ) + + def test_submission_create__non_ascii(self): + """Should create an instance of the form, encoded to utf-8.""" + client = Client() + xml = """ + + + ~!@#$%^&*()_+=-✅✅ + + Banana + + + """ + client.submissions.create(xml=xml, form_id="'=+/*-451%/%") + submission = client.submissions.get( + form_id="'=+/*-451%/%", instance_id="~!@#$%^&*()_+=-✅✅" + ) + self.assertEqual("~!@#$%^&*()_+=-✅✅", submission.instanceId) + + def test_submission_edit__non_ascii(self): + """Should edit an existing instance of the form, encoded to utf-8.""" + client = Client() + # The "instance_id" remains the id of the first submission, not the + # instanceID/deprecatedID used in the XML. + xml = """ + + + ~!@#$%^&*()_+=-✘✘ + ~!@#$%^&*()_+=-✘✘✘ + + Papaya + + + """ + client.submissions.edit( + xml=xml, form_id="'=+/*-451%/%", instance_id="~!@#$%^&*()_+=-✅✅" + )