diff --git a/pyodk/_endpoints/submissions.py b/pyodk/_endpoints/submissions.py index 3ec4609..c56d2dc 100644 --- a/pyodk/_endpoints/submissions.py +++ b/pyodk/_endpoints/submissions.py @@ -1,10 +1,13 @@ +import builtins import logging from collections.abc import Iterable from datetime import datetime +from pathlib import Path from typing import Any from pyodk._endpoints import bases from pyodk._endpoints.comments import Comment, CommentService +from pyodk._endpoints.submission_attachments import SubmissionAttachmentService from pyodk._utils import validators as pv from pyodk._utils.session import Session from pyodk.errors import PyODKError @@ -198,6 +201,8 @@ def create( project_id: int | None = None, device_id: str | None = None, encoding: str = "utf-8", + # Here we must use imported typing.List to avoid conflict with .list method + attachments: builtins.list[str] | None = None, ) -> Submission: """ Create a Submission. @@ -219,6 +224,7 @@ def create( :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". + :param attachments: A list of file paths to upload as attachments. """ try: pid = pv.validate_project_id(project_id, self.default_project_id) @@ -239,7 +245,26 @@ def create( data=xml.encode(encoding=encoding), ) data = response.json() - return Submission(**data) + submission = Submission(**data) + instance_id = submission.instanceId + + # If there are attachments, upload each one + if attachments: + attachment_svc = SubmissionAttachmentService(session=self.session) + for attachment in attachments: + attachment_path = Path(attachment) + file_name = attachment_path.name + upload_success = attachment_svc.upload( + file_path_or_bytes=attachment, + instance_id=instance_id, + file_name=file_name, + form_id=fid, + project_id=pid, + ) + if not upload_success: + log.error(f"Failed to upload attachment: {attachment}") + + return submission def _put( self, diff --git a/tests/endpoints/test_submissions.py b/tests/endpoints/test_submissions.py index c0543cf..6f4f80b 100644 --- a/tests/endpoints/test_submissions.py +++ b/tests/endpoints/test_submissions.py @@ -1,7 +1,10 @@ from unittest import TestCase from unittest.mock import MagicMock, patch -from pyodk._endpoints.submission_attachments import SubmissionAttachment +from pyodk._endpoints.submission_attachments import ( + SubmissionAttachment, + SubmissionAttachmentService, +) from pyodk._endpoints.submissions import Submission from pyodk._utils.session import Session from pyodk.client import Client @@ -73,6 +76,52 @@ def test_create__ok(self): ) self.assertIsInstance(observed, Submission) + def test_create_with_attachments__ok(self): + """Should return a Submission object, with attachments uploaded.""" + fixture = submissions_data.test_submissions + submission_file_path = ( + RESOURCES / "attachments" / "submission_image.png" + ).as_posix() + + with ( + patch.object(Session, "request") as mock_session, + patch.object( + SubmissionAttachmentService, "upload", return_value={"success": True} + ) as mock_upload, + ): + mock_session.return_value.status_code = 200 + mock_session.return_value.json.return_value = fixture["response_data"][0] + + with Client() as client: + # Upload the same attachment 3 times + observed = client.submissions.create( + project_id=fixture["project_id"], + form_id=fixture["form_id"], + xml=submissions_data.test_xml, + attachments=[ + submission_file_path, + submission_file_path, + submission_file_path, + ], + ) + self.assertIsInstance(observed, Submission) + + # Use default + observed = client.submissions.create( + form_id=fixture["form_id"], + xml=submissions_data.test_xml, + ) + self.assertIsInstance(observed, Submission) + + # Now test if upload success + mock_upload.assert_called_with( + file_path_or_bytes=submission_file_path, + instance_id=observed.instanceId, + file_name="submission_image.png", + form_id=fixture["form_id"], + project_id=fixture["project_id"], + ) + def test__put__ok(self): """Should return a Submission object.""" fixture = submissions_data.test_submissions