1
1
# coding: utf-8
2
2
import copy
3
3
import os
4
+ import re
4
5
import time
5
6
import uuid
6
7
from datetime import datetime
12
13
from dicttoxml import dicttoxml
13
14
from django .conf import settings
14
15
from django .urls import reverse
16
+ from django .utils .translation import gettext as t
17
+ from lxml import etree
15
18
from rest_framework import status
16
19
17
20
from kpi .constants import (
29
32
)
30
33
from kpi .interfaces .sync_backend_media import SyncBackendMediaInterface
31
34
from kpi .models .asset_file import AssetFile
32
- from kpi .utils .mongo_helper import MongoHelper , drop_mock_only
33
35
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
34
38
from .base_backend import BaseDeploymentBackend
39
+ from ..exceptions import KobocatBulkUpdateSubmissionsClientException
35
40
36
41
37
42
class MockDeploymentBackend (BaseDeploymentBackend ):
38
43
"""
39
44
Only used for unit testing and interface testing.
40
45
"""
41
46
47
+ PROTECTED_XML_FIELDS = [
48
+ '__version__' ,
49
+ 'formhub' ,
50
+ 'meta' ,
51
+ ]
52
+
42
53
def bulk_assign_mapped_perms (self ):
43
54
pass
44
55
45
56
def bulk_update_submissions (
46
57
self , data : dict , user : 'auth.User'
47
58
) -> dict :
48
-
49
59
submission_ids = self .validate_access_with_partial_perms (
50
60
user = user ,
51
61
perm = PERM_CHANGE_SUBMISSIONS ,
52
62
submission_ids = data ['submission_ids' ],
53
63
query = data ['query' ],
54
64
)
55
65
56
- if not submission_ids :
66
+ if submission_ids :
67
+ data ['query' ] = {}
68
+ else :
57
69
submission_ids = data ['submission_ids' ]
58
70
59
71
submissions = self .get_submissions (
60
72
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' ],
63
76
)
64
77
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
+ )
66
82
67
- responses = []
83
+ update_data = self .__prepare_bulk_update_data (data ['data' ])
84
+ kc_responses = []
68
85
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 )
87
115
88
116
def calculated_submission_count (self , user : 'auth.User' , ** kwargs ) -> int :
89
117
params = self .validate_submission_list_params (user ,
@@ -529,6 +557,16 @@ def set_validation_statuses(self, user: 'auth.User', data: dict) -> dict:
529
557
}
530
558
}
531
559
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
+
532
570
@property
533
571
def submission_list_url (self ):
534
572
# This doesn't really need to be implemented.
@@ -545,6 +583,22 @@ def sync_media_files(self, file_type: str = AssetFile.FORM_MEDIA):
545
583
for obj in queryset :
546
584
assert issubclass (obj .__class__ , SyncBackendMediaInterface )
547
585
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
+
548
602
@staticmethod
549
603
def __prepare_bulk_update_response (kc_responses : list ) -> dict :
550
604
total_update_attempts = len (kc_responses )
0 commit comments