From 96e8d1582607a3a7042db7aec15bb95fe8362c8a Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Jul 2024 15:02:35 +1000 Subject: [PATCH] add: form creation with attachments --- pyodk/_endpoints/forms.py | 24 ++++++++++++++++++++++-- tests/endpoints/test_forms.py | 25 ++++++++++++++++++++++++- tests/test_client.py | 12 +++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/pyodk/_endpoints/forms.py b/pyodk/_endpoints/forms.py index 74caec8..c47e10a 100644 --- a/pyodk/_endpoints/forms.py +++ b/pyodk/_endpoints/forms.py @@ -124,6 +124,7 @@ def get( def create( self, definition: PathLike | str | bytes, + attachments: Iterable[PathLike | str] | None = None, ignore_warnings: bool | None = True, form_id: str | None = None, project_id: int | None = None, @@ -133,6 +134,7 @@ def create( :param definition: The path to the file to upload (string or PathLike), or the form definition in memory (string (XML) or bytes (XLS/XLSX)). + :param attachments: The paths of the form attachment file(s) to upload. :param ignore_warnings: If True, create the form if there are XLSForm warnings. :param form_id: The xmlFormId of the Form being referenced. :param project_id: The id of the project this form belongs to. @@ -145,7 +147,9 @@ def create( form_id=form_id, project_id=project_id, ) - params["publish"] = True + + # Create the new Form definition, in draft state. + params["publish"] = False response = self.session.response_or_error( method="POST", url=self.session.urlformat(self.urls.forms, project_id=pid), @@ -155,7 +159,23 @@ def create( data=form_def, ) data = response.json() - return Form(**data) + + # In case the form_id parameter was None, use the (maybe generated) response value. + form = Form(**data) + fp_ids = {"form_id": form.xmlFormId, "project_id": project_id} + + # Upload the attachments, if any. + if attachments is not None: + fda = FormDraftAttachmentService(session=self.session, **self._default_kw()) + for attach in attachments: + if not fda.upload(file_path=attach, **fp_ids): + raise PyODKError("Form create (attachment upload) failed.") + + # Publish the draft. + if not fd.publish(**fp_ids): + raise PyODKError("Form create (draft publish) failed.") + + return form def update( self, diff --git a/tests/endpoints/test_forms.py b/tests/endpoints/test_forms.py index 119c343..ef7dda6 100644 --- a/tests/endpoints/test_forms.py +++ b/tests/endpoints/test_forms.py @@ -105,7 +105,8 @@ def test_get__ok(self): ) self.assertIsInstance(observed, Form) - def test_create__ok(self): + @get_mock_context + def test_create__ok(self, ctx: MockContext): """Should return a FormType object.""" fixture = forms_data.test_forms with patch.object(Session, "request") as mock_session: @@ -126,6 +127,28 @@ def test_create__ok(self): ) self.assertIsInstance(observed, Form) + @get_mock_context + def test_create__with_attachments__ok(self, ctx: MockContext): + """Should return a FormType object.""" + fixture = forms_data.test_forms + with patch.object(Session, "request") as mock_session: + mock_session.return_value.status_code = 200 + mock_session.return_value.json.return_value = fixture["response_data"][1] + with Client() as client, utils.get_temp_file(suffix=".xml") as fp: + fp.write_text(forms_data.get_xml__range_draft()) + observed = client.forms.create( + definition=fp, + project_id=fixture["project_id"], + form_id=fixture["response_data"][1]["xmlFormId"], + attachments=["/some/path/a.jpg", "/some/path/b.jpg"], + ) + self.assertIsInstance(observed, Form) + self.assertEqual(2, ctx.fda_upload.call_count) + ctx.fd_publish.assert_called_once_with( + form_id=fixture["response_data"][1]["xmlFormId"], + project_id=fixture["project_id"], + ) + def test_update__def_or_attach_required(self): """Should raise an error if both 'definition' and 'attachments' are None.""" with self.assertRaises(PyODKError) as err: diff --git a/tests/test_client.py b/tests/test_client.py index 45e7629..a480e3a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -139,6 +139,16 @@ def test_form_create__new_definition_xlsx(self): form = self.client.forms.create(definition=wb) self.assertTrue(form.xmlFormId.startswith("uuid:")) + def test_form_create__new_definition_xlsx_and_attachments(self): + """Should create a new form with the new definition and attachment.""" + form_def = forms_data.get_md__pull_data() + wb = md_table_to_bytes(mdstr=form_def) + form = self.client.forms.create( + definition=wb, + attachments=[(RESOURCES / "forms" / "fruits.csv").as_posix()], + ) + self.assertTrue(form.xmlFormId.startswith("uuid:")) + # Below tests assume project has forms by these names already published. def test_form_update__new_definition(self): """Should create a new version with the new definition.""" @@ -175,7 +185,7 @@ def test_form_update__new_definition_and_attachments__non_ascii_dingbat(self): 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.""" + """Should create a new version with new definition.""" self.client.forms.update( form_id="'=+/*-451%/%", attachments=[],