Skip to content

Commit 2bd2cea

Browse files
committed
Merge remote-tracking branch 'origin/master' into public-beta
2 parents a66a3b2 + 8960bac commit 2bd2cea

File tree

3 files changed

+87
-37
lines changed

3 files changed

+87
-37
lines changed

kpi/deployment_backends/kobocat_backend.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import requests
1616
from django.conf import settings
1717
from django.core.exceptions import ImproperlyConfigured
18+
from lxml import etree
1819
from django.core.files import File
1920
from django.db.models.query import QuerySet
2021
from django.utils.translation import gettext_lazy as t
@@ -44,7 +45,7 @@
4445
from kpi.utils.log import logging
4546
from kpi.utils.mongo_helper import MongoHelper
4647
from kpi.utils.permissions import is_user_anonymous
47-
from kpi.utils.xml import edit_submission_xml, strip_nodes
48+
from kpi.utils.xml import edit_submission_xml
4849
from .base_backend import BaseDeploymentBackend
4950
from .kc_access.shadow_models import (
5051
KobocatOneTimeAuthToken,
@@ -152,7 +153,7 @@ def bulk_update_submissions(
152153
update_data = self.__prepare_bulk_update_data(data['data'])
153154
kc_responses = []
154155
for submission in submissions:
155-
xml_parsed = ET.fromstring(submission)
156+
xml_parsed = etree.fromstring(submission)
156157

157158
_uuid, uuid_formatted = self.generate_new_instance_id()
158159

@@ -166,7 +167,7 @@ def bulk_update_submissions(
166167
deprecated_id_or_new = (
167168
deprecated_id
168169
if deprecated_id is not None
169-
else ET.SubElement(xml_parsed.find('meta'), 'deprecatedID')
170+
else etree.SubElement(xml_parsed.find('meta'), 'deprecatedID')
170171
)
171172
deprecated_id_or_new.text = instance_id.text
172173
instance_id.text = uuid_formatted
@@ -181,7 +182,7 @@ def bulk_update_submissions(
181182

182183
# TODO: Might be worth refactoring this as it is also used when
183184
# duplicating a submission
184-
file_tuple = (_uuid, io.BytesIO(ET.tostring(xml_parsed)))
185+
file_tuple = (_uuid, io.BytesIO(etree.tostring(xml_parsed)))
185186
files = {'xml_submission_file': file_tuple}
186187
# `POST` is required by OpenRosa spec https://docs.getodk.org/openrosa-form-submission
187188
headers = {}
@@ -666,12 +667,6 @@ def get_data_download_links(self):
666667
'reports',
667668
self.backend_response['id_string']
668669
))
669-
forms_base_url = '/'.join((
670-
settings.KOBOCAT_URL.rstrip('/'),
671-
self.asset.owner.username,
672-
'forms',
673-
self.backend_response['id_string']
674-
))
675670
links = {
676671
# To be displayed in iframes
677672
'xls_legacy': '/'.join((exports_base_url, 'xls/')),
@@ -700,7 +695,7 @@ def get_enketo_survey_links(self):
700695
data=data
701696
)
702697
response.raise_for_status()
703-
except requests.exceptions.RequestException as e:
698+
except requests.exceptions.RequestException:
704699
# Don't 500 the entire asset view if Enketo is unreachable
705700
logging.error(
706701
'Failed to retrieve links from Enketo', exc_info=True)

kpi/deployment_backends/mock_backend.py

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# coding: utf-8
22
import copy
33
import os
4+
import re
45
import time
56
import uuid
67
from datetime import datetime
@@ -12,6 +13,8 @@
1213
from dicttoxml import dicttoxml
1314
from django.conf import settings
1415
from django.urls import reverse
16+
from django.utils.translation import gettext as t
17+
from lxml import etree
1518
from rest_framework import status
1619

1720
from kpi.constants import (
@@ -29,61 +32,86 @@
2932
)
3033
from kpi.interfaces.sync_backend_media import SyncBackendMediaInterface
3134
from kpi.models.asset_file import AssetFile
32-
from kpi.utils.mongo_helper import MongoHelper, drop_mock_only
3335
from kpi.tests.utils.mock import MockAttachment
36+
from kpi.utils.mongo_helper import MongoHelper, drop_mock_only
37+
from kpi.utils.xml import edit_submission_xml
3438
from .base_backend import BaseDeploymentBackend
39+
from ..exceptions import KobocatBulkUpdateSubmissionsClientException
3540

3641

3742
class MockDeploymentBackend(BaseDeploymentBackend):
3843
"""
3944
Only used for unit testing and interface testing.
4045
"""
4146

47+
PROTECTED_XML_FIELDS = [
48+
'__version__',
49+
'formhub',
50+
'meta',
51+
]
52+
4253
def bulk_assign_mapped_perms(self):
4354
pass
4455

4556
def bulk_update_submissions(
4657
self, data: dict, user: 'auth.User'
4758
) -> dict:
48-
4959
submission_ids = self.validate_access_with_partial_perms(
5060
user=user,
5161
perm=PERM_CHANGE_SUBMISSIONS,
5262
submission_ids=data['submission_ids'],
5363
query=data['query'],
5464
)
5565

56-
if not submission_ids:
66+
if submission_ids:
67+
data['query'] = {}
68+
else:
5769
submission_ids = data['submission_ids']
5870

5971
submissions = self.get_submissions(
6072
user=user,
61-
format_type=SUBMISSION_FORMAT_TYPE_JSON,
62-
submission_ids=submission_ids
73+
format_type=SUBMISSION_FORMAT_TYPE_XML,
74+
submission_ids=submission_ids,
75+
query=data['query'],
6376
)
6477

65-
submission_ids = [int(id_) for id_ in submission_ids]
78+
if not self.current_submissions_count:
79+
raise KobocatBulkUpdateSubmissionsClientException(
80+
detail=t('No submissions match the given `submission_ids`')
81+
)
6682

67-
responses = []
83+
update_data = self.__prepare_bulk_update_data(data['data'])
84+
kc_responses = []
6885
for submission in submissions:
69-
if submission['_id'] in submission_ids:
70-
_uuid = uuid.uuid4()
71-
submission['meta/deprecatedID'] = submission['meta/instanceID']
72-
submission['meta/instanceID'] = f'uuid:{_uuid}'
73-
for k, v in data['data'].items():
74-
submission[k] = v
75-
76-
# Mirror KobocatDeploymentBackend responses
77-
responses.append(
78-
{
79-
'uuid': _uuid,
80-
'status_code': status.HTTP_201_CREATED,
81-
'message': 'Successful submission'
82-
}
83-
)
84-
85-
self.mock_submissions(submissions)
86-
return self.__prepare_bulk_update_response(responses)
86+
# Remove XML declaration from submission
87+
submission = re.sub(r'(<\?.*\?>)', '', submission)
88+
xml_parsed = etree.fromstring(submission)
89+
90+
_uuid, uuid_formatted = self.generate_new_instance_id()
91+
92+
instance_id = xml_parsed.find('meta/instanceID')
93+
deprecated_id = xml_parsed.find('meta/deprecatedID')
94+
deprecated_id_or_new = (
95+
deprecated_id
96+
if deprecated_id is not None
97+
else etree.SubElement(xml_parsed.find('meta'), 'deprecatedID')
98+
)
99+
deprecated_id_or_new.text = instance_id.text
100+
instance_id.text = uuid_formatted
101+
102+
for path, value in update_data.items():
103+
edit_submission_xml(xml_parsed, path, value)
104+
105+
kc_responses.append(
106+
{
107+
'uuid': _uuid,
108+
'status_code': status.HTTP_201_CREATED,
109+
'message': 'Successful submission',
110+
'updated_submission': etree.tostring(xml_parsed) # only for testing
111+
}
112+
)
113+
114+
return self.__prepare_bulk_update_response(kc_responses)
87115

88116
def calculated_submission_count(self, user: 'auth.User', **kwargs) -> int:
89117
params = self.validate_submission_list_params(user,
@@ -529,6 +557,16 @@ def set_validation_statuses(self, user: 'auth.User', data: dict) -> dict:
529557
}
530558
}
531559

560+
@staticmethod
561+
def generate_new_instance_id() -> (str, str):
562+
"""
563+
Returns:
564+
- Generated uuid
565+
- Formatted uuid for OpenRosa xml
566+
"""
567+
_uuid = str(uuid.uuid4())
568+
return _uuid, f'uuid:{_uuid}'
569+
532570
@property
533571
def submission_list_url(self):
534572
# This doesn't really need to be implemented.
@@ -545,6 +583,22 @@ def sync_media_files(self, file_type: str = AssetFile.FORM_MEDIA):
545583
for obj in queryset:
546584
assert issubclass(obj.__class__, SyncBackendMediaInterface)
547585

586+
@classmethod
587+
def __prepare_bulk_update_data(cls, updates: dict) -> dict:
588+
"""
589+
Preparing the request payload for bulk updating of submissions
590+
"""
591+
# Sanitizing the payload of potentially destructive keys
592+
sanitized_updates = copy.deepcopy(updates)
593+
for key in updates:
594+
if (
595+
key in cls.PROTECTED_XML_FIELDS
596+
or '/' in key and key.split('/')[0] in cls.PROTECTED_XML_FIELDS
597+
):
598+
sanitized_updates.pop(key)
599+
600+
return sanitized_updates
601+
548602
@staticmethod
549603
def __prepare_bulk_update_response(kc_responses: list) -> dict:
550604
total_update_attempts = len(kc_responses)

kpi/tests/api/v2/test_api_submissions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,8 @@ def setUp(self):
13851385
self.updated_submission_data = {
13861386
'submission_ids': [rs['_id'] for rs in random_submissions],
13871387
'data': {
1388-
'q1': '🕺',
1388+
'q1': 'Updated value',
1389+
'q_new': 'A new question and value'
13891390
},
13901391
}
13911392

0 commit comments

Comments
 (0)