Skip to content

Commit

Permalink
feat: allow uploading attachments alongside submission
Browse files Browse the repository at this point in the history
  • Loading branch information
spwoodcock committed Feb 11, 2025
1 parent 755c32e commit 9dc61fe
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 2 deletions.
27 changes: 26 additions & 1 deletion pyodk/_endpoints/submissions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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,
Expand Down
51 changes: 50 additions & 1 deletion tests/endpoints/test_submissions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9dc61fe

Please # to comment.