Skip to content

Commit e41ba34

Browse files
feat(organizations): Add mongo_uuid field to XForm model and populate it TASK-957 (#5196)
## Checklist 1. [x] If you've added code that should be tested, add tests 2. [ ] If you've changed APIs, update (or create!) the documentation 3. [x] Ensure the tests pass 4. [x] Run `./python-format.sh` to make sure that your code lints and that you've followed [our coding style](https://github.com/kobotoolbox/kpi/blob/main/CONTRIBUTING.md) 5. [x] Write a title and, if necessary, a description of your work suitable for publishing in our [release notes](https://community.kobotoolbox.org/tag/release-notes) 6. [ ] Mention any related issues in this repository (as #ISSUE) and in other repositories (as kobotoolbox/other#ISSUE) 7. [ ] Open an issue in the [docs](https://github.com/kobotoolbox/docs/issues/new) if there are UI/UX changes 8. [ ] Create a testing plan for the reviewer and add it to the Testing section ## Description This PR introduces a new field, mongo_uuid, to the XForm model in KoboToolbox. This unique identifier is designed to enhance the ownership transfer process of XForms by ensuring that each form is linked to a distinct UUID in MongoDB. The addition of mongo_uuid will streamline how XForms are managed, especially during ownership transfers, by reducing unnecessary updates to the MongoDB documents related to the XForm. ## Notes - A new column (mongo_uuid) has been added to the XForm model, which will serve as a unique identifier. Ownership Transfer: The logic for transferring an XForm's ownership has been updated to ensure that the mongo_uuid is populated if it is currently null. - All existing MongoDB documents that match the _userform_id will be updated to include the new mongo_uuid value, ensuring consistency across the database. - The ParsedInstance.to_dict_for_mongo() method has been modified to prioritize the mongo_uuid over _userform_id when exporting data. If mongo_uuid is not null, _userform_id will be set to mongo_uuid; otherwise, the original logic will be maintained. - The mongo_userform_id property in the OpenRosaDeploymentBackend class has been updated to return the mongo_uuid when available, ensuring that the correct ID is used during operations. --------- Co-authored-by: Olivier Leger <[email protected]>
1 parent 1574ad2 commit e41ba34

File tree

9 files changed

+144
-50
lines changed

9 files changed

+144
-50
lines changed

kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_viewset.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def test_xform_serializer_none(self):
287287
'num_of_submissions': 0,
288288
'attachment_storage_bytes': 0,
289289
'kpi_asset_uid': '',
290+
'mongo_uuid': '',
290291
}
291292
self.assertEqual(data, XFormSerializer(None).data)
292293

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.2.15 on 2024-10-24 20:16
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
('logger', '0037_remove_xform_has_kpi_hooks_and_instance_posted_to_kpi'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='xform',
17+
name='mongo_uuid',
18+
field=models.CharField(
19+
db_index=True, max_length=100, null=True, unique=True
20+
),
21+
),
22+
]

kobo/apps/openrosa/apps/logger/models/xform.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class XForm(AbstractTimeStampedModel):
7777
last_submission_time = models.DateTimeField(blank=True, null=True)
7878
has_start_time = models.BooleanField(default=False)
7979
uuid = models.CharField(max_length=32, default='', db_index=True)
80+
mongo_uuid = models.CharField(
81+
max_length=100, null=True, unique=True, db_index=True
82+
)
8083

8184
uuid_regex = re.compile(r'(<instance>.*?id="[^"]+">)(.*</instance>)(.*)',
8285
re.DOTALL)

kobo/apps/openrosa/apps/viewer/models/parsed_instance.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,17 @@ def _get_paginated_and_sorted_cursor(cls, cursor, start, limit, sort):
259259

260260
def to_dict_for_mongo(self):
261261
d = self.to_dict()
262+
263+
userform_id = (
264+
self.instance.xform.mongo_uuid
265+
if self.instance.xform.mongo_uuid
266+
else f'{self.instance.xform.user.username}_{self.instance.xform.id_string}'
267+
)
268+
262269
data = {
263270
UUID: self.instance.uuid,
264271
ID: self.instance.id,
265-
self.USERFORM_ID: '%s_%s' % (
266-
self.instance.xform.user.username,
267-
self.instance.xform.id_string),
272+
self.USERFORM_ID: userform_id,
268273
ATTACHMENTS: _get_attachments_from_instance(self.instance),
269274
self.STATUS: self.instance.status,
270275
GEOLOCATION: [self.lat, self.lng],

kobo/apps/project_ownership/tests/api/v2/test_api.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,17 +500,79 @@ def test_data_accessible_to_new_user(self):
500500
for attachment in response.data['results'][0]['_attachments']:
501501
assert attachment['filename'].startswith('anotheruser/')
502502

503+
# Get the mongo_uuid for the transferred asset (XForm)
504+
self.asset.deployment.xform.refresh_from_db()
505+
mongo_uuid = self.asset.deployment.xform.mongo_uuid
506+
503507
assert (
504508
settings.MONGO_DB.instances.count_documents(
505509
{'_userform_id': f'someuser_{self.asset.uid}'}
506510
) == 0
507511
)
508512
assert (
509513
settings.MONGO_DB.instances.count_documents(
510-
{'_userform_id': f'anotheruser_{self.asset.uid}'}
514+
{'_userform_id': mongo_uuid}
515+
) == 1
516+
)
517+
518+
@patch(
519+
'kobo.apps.project_ownership.models.transfer.reset_kc_permissions',
520+
MagicMock()
521+
)
522+
@patch(
523+
'kobo.apps.project_ownership.tasks.move_attachments',
524+
MagicMock()
525+
)
526+
@patch(
527+
'kobo.apps.project_ownership.tasks.move_media_files',
528+
MagicMock()
529+
)
530+
@override_config(PROJECT_OWNERSHIP_AUTO_ACCEPT_INVITES=True)
531+
def test_mongo_uuid_after_transfer(self):
532+
"""
533+
Test that after an ownership transfer, the XForm's MongoDB document
534+
updates to use the `mongo_uuid` as the `_userform_id` instead of the
535+
original owner's identifier
536+
"""
537+
self.client.login(username='someuser', password='someuser')
538+
original_userform_id = f'someuser_{self.asset.uid}'
539+
assert (
540+
settings.MONGO_DB.instances.count_documents(
541+
{'_userform_id': original_userform_id}
542+
) == 1
543+
)
544+
545+
# Transfer the project from someuser to anotheruser
546+
payload = {
547+
'recipient': self.absolute_reverse(
548+
self._get_endpoint('user-kpi-detail'),
549+
args=[self.anotheruser.username]
550+
),
551+
'assets': [self.asset.uid]
552+
}
553+
554+
with immediate_on_commit():
555+
response = self.client.post(self.invite_url, data=payload, format='json')
556+
assert response.status_code == status.HTTP_201_CREATED
557+
558+
# Get the mongo_uuid for the transferred asset (XForm)
559+
self.asset.deployment.xform.refresh_from_db()
560+
mongo_uuid = self.asset.deployment.xform.mongo_uuid
561+
562+
# Verify MongoDB now uses mongo_uuid as the identifier
563+
assert (
564+
settings.MONGO_DB.instances.count_documents(
565+
{'_userform_id': mongo_uuid}
511566
) == 1
512567
)
513568

569+
# Confirm the original `_userform_id` is no longer used
570+
assert (
571+
settings.MONGO_DB.instances.count_documents(
572+
{'_userform_id': original_userform_id}
573+
) == 0
574+
)
575+
514576

515577
class ProjectOwnershipInAppMessageAPITestCase(KpiTestCase):
516578

kobo/apps/project_ownership/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ def rewrite_mongo_userform_id(transfer: 'project_ownership.Transfer'):
172172
if not transfer.asset.has_deployment:
173173
return
174174

175+
transfer.asset.deployment.set_mongo_uuid()
176+
175177
if not transfer.asset.deployment.transfer_submissions_ownership(
176178
old_owner.username
177179
):

kpi/deployment_backends/openrosa_backend.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
SubmissionNotFoundException,
6262
XPathNotFoundException,
6363
)
64+
from kpi.fields import KpiUidField
6465
from kpi.interfaces.sync_backend_media import SyncBackendMediaInterface
6566
from kpi.models.asset_file import AssetFile
6667
from kpi.models.object_permission import ObjectPermission
@@ -731,7 +732,10 @@ def get_validation_status(
731732

732733
@property
733734
def mongo_userform_id(self):
734-
return '{}_{}'.format(self.asset.owner.username, self.xform_id_string)
735+
return (
736+
self.xform.mongo_uuid
737+
or f'{self.asset.owner.username}_{self.xform_id_string}'
738+
)
735739

736740
@staticmethod
737741
def nlp_tracking_data(
@@ -959,6 +963,14 @@ def set_enketo_open_rosa_server(self, require_auth: bool, enketo_id: str = None)
959963
server_url,
960964
)
961965

966+
def set_mongo_uuid(self):
967+
"""
968+
Set the `mongo_uuid` for the associated XForm if it's not already set.
969+
"""
970+
if not self.xform.mongo_uuid:
971+
self.xform.mongo_uuid = KpiUidField.generate_unique_id()
972+
self.xform.save(update_fields=['mongo_uuid'])
973+
962974
def set_validation_status(
963975
self,
964976
submission_id: int,
@@ -1218,6 +1230,7 @@ def xform(self):
12181230
'attachment_storage_bytes',
12191231
'require_auth',
12201232
'uuid',
1233+
'mongo_uuid',
12211234
)
12221235
.select_related('user') # Avoid extra query to validate username below
12231236
.first()

kpi/fixtures/conflicting_versions.json

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@
130130
"_deployment_data": {
131131
"active": true,
132132
"version": "vqu4EaVFry5MirsJcBbhsQ",
133-
"backend": "mock",
134-
"identifier": "mock://axD3Wc8ZnfgLXBcURRt5fM"
133+
"backend": "mock"
135134
},
136135
"asset": 713928,
137136
"deployed": true,
@@ -221,8 +220,7 @@
221220
"_deployment_data": {
222221
"active": true,
223222
"version": "vnPWjPhx4VP6V24Cvn2EhG",
224-
"backend": "mock",
225-
"identifier": "mock://axD3Wc8ZnfgLXBcURRt5fM"
223+
"backend": "mock"
226224
},
227225
"asset": 713928,
228226
"deployed": true,
@@ -330,8 +328,7 @@
330328
"_deployment_data": {
331329
"active": true,
332330
"version": "vGcztYhnRo9CCLv7biByAt",
333-
"backend": "mock",
334-
"identifier": "mock://axD3Wc8ZnfgLXBcURRt5fM"
331+
"backend": "mock"
335332
},
336333
"asset": 713928,
337334
"deployed": true,
@@ -496,26 +493,23 @@
496493
"active": true,
497494
"version": "vKYFpP4BG5593cJrsYSUgg",
498495
"backend": "mock",
499-
"identifier": "mock://axD3Wc8ZnfgLXBcURRt5fM",
496+
"backend_response": {
497+
"hash": "md5:d41d8cd98f00b204e9800998ecf8427e",
498+
"formid": 713928,
499+
"uuid": "d9e0d163e34c4524b5fd35b394f7aa26",
500+
"id_string": "axD3Wc8ZnfgLXBcURRt5fM",
501+
"kpi_asset_uid": "axD3Wc8ZnfgLXBcURRt5fM"
502+
},
500503
"submissions": [
501504
{
502-
"_id" : 713934,
503-
"_notes" : [ ],
504505
"meta/instanceID" : "uuid:be5817e1-dd67-42b0-a6bd-30bf5ecfe4d3",
505506
"end" : "2017-12-18T17:47:09.000-05:00",
506507
"created_in_second_version" : "submitted to second version (2)",
507-
"_bamboo_dataset_id" : "",
508508
"_uuid" : "be5817e1-dd67-42b0-a6bd-30bf5ecfe4d3",
509509
"created_in_first_version" : "submitted to second version (1)",
510-
"_tags" : [ ],
511-
"_attachments" : [ ],
512510
"_submission_time" : "2017-12-18T22:47:10",
513511
"start" : "2017-12-18T17:46:22.000-05:00",
514-
"_submitted_by" : null,
515-
"_geolocation" : [
516-
null,
517-
null
518-
],
512+
"_submitted_by" : "someuser",
519513
"_xform_id_string" : "axD3Wc8ZnfgLXBcURRt5fM",
520514
"_userform_id" : "someuser_axD3Wc8ZnfgLXBcURRt5fM",
521515
"_version_" : "vnPWjPhx4VP6V24Cvn2EhG",
@@ -524,13 +518,9 @@
524518
"formhub/uuid" : "d9e0d163e34c4524b5fd35b394f7aa26"
525519
},
526520
{
527-
"_id" : 713935,
528-
"_notes" : [ ],
529521
"_version__001" : "vGcztYhnRo9CCLv7biByAt",
530522
"_uuid" : "a95a0de5-06d2-43ab-b78f-fb2885ceac61",
531-
"_bamboo_dataset_id" : "",
532-
"_tags" : [ ],
533-
"_submitted_by" : null,
523+
"_submitted_by" : "someuser",
534524
"_xform_id_string" : "axD3Wc8ZnfgLXBcURRt5fM",
535525
"created_in_third_version" : "submitted to third version (3)",
536526
"meta/instanceID" : "uuid:a95a0de5-06d2-43ab-b78f-fb2885ceac61",
@@ -539,26 +529,18 @@
539529
"start" : "2017-12-18T17:48:09.000-05:00",
540530
"_submission_time" : "2017-12-18T22:48:21",
541531
"created_in_first_version" : "submitted to third version (1)",
542-
"_attachments" : [ ],
543532
"created_in_second_version" : "submitted to third version (2)",
544-
"_geolocation" : [
545-
null,
546-
null
547-
],
548533
"_userform_id" : "someuser_axD3Wc8ZnfgLXBcURRt5fM",
549534
"_version_" : "vnPWjPhx4VP6V24Cvn2EhG",
550535
"_status" : "submitted_via_web",
551536
"__version__" : "vqu4EaVFry5MirsJcBbhsQ"
552537
},
553538
{
554-
"_id" : 713936,
555-
"_notes" : [ ],
556539
"_version__001" : "vGcztYhnRo9CCLv7biByAt",
557540
"_version__002" : "vKYFpP4BG5593cJrsYSUgg",
558541
"_uuid" : "bcbfcdfc-891a-4513-80b3-d9b98bd87553",
559542
"_bamboo_dataset_id" : "",
560-
"_tags" : [ ],
561-
"_submitted_by" : null,
543+
"_submitted_by" : "someuser",
562544
"_xform_id_string" : "axD3Wc8ZnfgLXBcURRt5fM",
563545
"created_in_third_version" : "submitted to fourth version (3)",
564546
"meta/instanceID" : "uuid:bcbfcdfc-891a-4513-80b3-d9b98bd87553",
@@ -567,37 +549,24 @@
567549
"start" : "2017-12-18T17:49:19.000-05:00",
568550
"_submission_time" : "2017-12-18T22:49:38",
569551
"created_in_first_version" : "submitted to fourth version (1)",
570-
"_attachments" : [ ],
571552
"created_in_second_version" : "submitted to fourth version (2)",
572-
"_geolocation" : [
573-
null,
574-
null
575-
],
576553
"_userform_id" : "someuser_axD3Wc8ZnfgLXBcURRt5fM",
577554
"_version_" : "vnPWjPhx4VP6V24Cvn2EhG",
578555
"_status" : "submitted_via_web",
579556
"__version__" : "vqu4EaVFry5MirsJcBbhsQ",
580557
"created_in_fourth_version" : "submitted to fourth version (4)"
581558
},
582559
{
583-
"_id" : 713937,
584-
"_notes" : [ ],
585560
"_uuid" : "a6bac3ac-c9aa-4556-a1c0-b1d44d51ca65",
586561
"_bamboo_dataset_id" : "",
587-
"_tags" : [ ],
588-
"_submitted_by" : null,
562+
"_submitted_by" : "someuser",
589563
"_xform_id_string" : "axD3Wc8ZnfgLXBcURRt5fM",
590564
"meta/instanceID" : "uuid:a6bac3ac-c9aa-4556-a1c0-b1d44d51ca65",
591565
"formhub/uuid" : "d9e0d163e34c4524b5fd35b394f7aa26",
592566
"end" : "2017-12-18T17:45:26.000-05:00",
593567
"_submission_time" : "2017-12-18T22:45:26",
594568
"created_in_first_version" : "submitted to first version",
595-
"_attachments" : [ ],
596569
"start" : "2017-12-18T17:45:15.000-05:00",
597-
"_geolocation" : [
598-
null,
599-
null
600-
],
601570
"_userform_id" : "someuser_axD3Wc8ZnfgLXBcURRt5fM",
602571
"_status" : "submitted_via_web",
603572
"__version__" : "vqu4EaVFry5MirsJcBbhsQ"
@@ -611,5 +580,17 @@
611580
},
612581
"model": "kpi.asset",
613582
"pk": 713928
583+
},
584+
{
585+
"fields": {
586+
"title": "conflicting versions",
587+
"user": 2,
588+
"xml": "",
589+
"id_string": "axD3Wc8ZnfgLXBcURRt5fM",
590+
"date_created": "2017-12-18T22:42:59.233Z",
591+
"kpi_asset_uid": "axD3Wc8ZnfgLXBcURRt5fM"
592+
},
593+
"model": "logger.xform",
594+
"pk": 713928
614595
}
615596
]

kpi/tests/test_mock_data_conflicting_version_exports.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ def setUp(self):
2727
self.maxDiff = None
2828
self.user = User.objects.get(username='someuser')
2929
self.asset = Asset.objects.get(uid='axD3Wc8ZnfgLXBcURRt5fM')
30+
# To avoid cluttering the fixture, redeploy asset to set related XForm properly
31+
self.asset.deployment.redeploy(active=True)
3032
# To avoid cluttering the fixture, assign permissions here
3133
self.asset.assign_perm(self.user, PERM_VIEW_SUBMISSIONS)
34+
self.asset.deployment.mock_submissions(
35+
submissions=self.asset._deployment_data['submissions'],
36+
)
3237
self.submissions = self.asset.deployment.get_submissions(self.asset.owner)
3338
self.submission_id_field = '_id'
3439
self.formpack, self.submission_stream = report_data.build_formpack(

0 commit comments

Comments
 (0)