diff --git a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_viewset.py b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_viewset.py index ad9b7a2c3a..8649f9fa61 100644 --- a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_viewset.py +++ b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_viewset.py @@ -287,6 +287,7 @@ def test_xform_serializer_none(self): 'num_of_submissions': 0, 'attachment_storage_bytes': 0, 'kpi_asset_uid': '', + 'mongo_uuid': '', } self.assertEqual(data, XFormSerializer(None).data) diff --git a/kobo/apps/openrosa/apps/logger/migrations/0038_add_mongo_uuid_field_to_xform.py b/kobo/apps/openrosa/apps/logger/migrations/0038_add_mongo_uuid_field_to_xform.py new file mode 100644 index 0000000000..9780598d46 --- /dev/null +++ b/kobo/apps/openrosa/apps/logger/migrations/0038_add_mongo_uuid_field_to_xform.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.15 on 2024-10-24 20:16 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('logger', '0037_remove_xform_has_kpi_hooks_and_instance_posted_to_kpi'), + ] + + operations = [ + migrations.AddField( + model_name='xform', + name='mongo_uuid', + field=models.CharField( + db_index=True, max_length=100, null=True, unique=True + ), + ), + ] diff --git a/kobo/apps/openrosa/apps/logger/models/xform.py b/kobo/apps/openrosa/apps/logger/models/xform.py index f031ef5fd1..9e2e17294b 100644 --- a/kobo/apps/openrosa/apps/logger/models/xform.py +++ b/kobo/apps/openrosa/apps/logger/models/xform.py @@ -78,6 +78,9 @@ class XForm(AbstractTimeStampedModel): last_submission_time = models.DateTimeField(blank=True, null=True) has_start_time = models.BooleanField(default=False) uuid = models.CharField(max_length=32, default='', db_index=True) + mongo_uuid = models.CharField( + max_length=100, null=True, unique=True, db_index=True + ) uuid_regex = re.compile(r'(.*?id="[^"]+">)(.*)(.*)', re.DOTALL) diff --git a/kobo/apps/openrosa/apps/viewer/models/parsed_instance.py b/kobo/apps/openrosa/apps/viewer/models/parsed_instance.py index 655178d7f6..21d3acc45a 100644 --- a/kobo/apps/openrosa/apps/viewer/models/parsed_instance.py +++ b/kobo/apps/openrosa/apps/viewer/models/parsed_instance.py @@ -259,12 +259,17 @@ def _get_paginated_and_sorted_cursor(cls, cursor, start, limit, sort): def to_dict_for_mongo(self): d = self.to_dict() + + userform_id = ( + self.instance.xform.mongo_uuid + if self.instance.xform.mongo_uuid + else f'{self.instance.xform.user.username}_{self.instance.xform.id_string}' + ) + data = { UUID: self.instance.uuid, ID: self.instance.id, - self.USERFORM_ID: '%s_%s' % ( - self.instance.xform.user.username, - self.instance.xform.id_string), + self.USERFORM_ID: userform_id, ATTACHMENTS: _get_attachments_from_instance(self.instance), self.STATUS: self.instance.status, GEOLOCATION: [self.lat, self.lng], diff --git a/kobo/apps/project_ownership/tests/api/v2/test_api.py b/kobo/apps/project_ownership/tests/api/v2/test_api.py index d8144a66ac..d892807863 100644 --- a/kobo/apps/project_ownership/tests/api/v2/test_api.py +++ b/kobo/apps/project_ownership/tests/api/v2/test_api.py @@ -8,6 +8,7 @@ from rest_framework import status from rest_framework.reverse import reverse +from kobo.apps.openrosa.apps.logger.models import XForm from kobo.apps.project_ownership.models import Invite, InviteStatusChoices, Transfer from kobo.apps.trackers.utils import update_nlp_counter from kpi.constants import PERM_VIEW_ASSET @@ -500,6 +501,10 @@ def test_data_accessible_to_new_user(self): for attachment in response.data['results'][0]['_attachments']: assert attachment['filename'].startswith('anotheruser/') + # Get the mongo_uuid for the transferred asset (XForm) + xform = XForm.objects.get(kpi_asset_uid=self.asset.uid) + mongo_uuid = xform.mongo_uuid + assert ( settings.MONGO_DB.instances.count_documents( {'_userform_id': f'someuser_{self.asset.uid}'} @@ -507,7 +512,7 @@ def test_data_accessible_to_new_user(self): ) assert ( settings.MONGO_DB.instances.count_documents( - {'_userform_id': f'anotheruser_{self.asset.uid}'} + {'_userform_id': mongo_uuid} ) == 1 ) diff --git a/kobo/apps/project_ownership/utils.py b/kobo/apps/project_ownership/utils.py index 631331bd47..253dd97fd0 100644 --- a/kobo/apps/project_ownership/utils.py +++ b/kobo/apps/project_ownership/utils.py @@ -172,6 +172,8 @@ def rewrite_mongo_userform_id(transfer: 'project_ownership.Transfer'): if not transfer.asset.has_deployment: return + transfer.asset.deployment.set_mongo_uuid() + if not transfer.asset.deployment.transfer_submissions_ownership( old_owner.username ): diff --git a/kpi/deployment_backends/openrosa_backend.py b/kpi/deployment_backends/openrosa_backend.py index c55a62ca3f..fd5e934025 100644 --- a/kpi/deployment_backends/openrosa_backend.py +++ b/kpi/deployment_backends/openrosa_backend.py @@ -11,6 +11,8 @@ except ImportError: from backports.zoneinfo import ZoneInfo +import secrets +import string import redis.exceptions import requests from defusedxml import ElementTree as DET @@ -731,7 +733,9 @@ def get_validation_status( @property def mongo_userform_id(self): - return '{}_{}'.format(self.asset.owner.username, self.xform_id_string) + if self.xform.mongo_uuid: + return self.xform.mongo_uuid + return f'{self.asset.owner.username}_{self.xform_id_string}' @staticmethod def nlp_tracking_data( @@ -1218,6 +1222,7 @@ def xform(self): 'attachment_storage_bytes', 'require_auth', 'uuid', + 'mongo_uuid', ) .select_related('user') # Avoid extra query to validate username below .first() @@ -1234,6 +1239,20 @@ def xform(self): self._xform = xform return self._xform + def generate_unique_key(self, length=20): + + characters = string.ascii_letters + string.digits + # Generate a secure key with the specified length + return ''.join(secrets.choice(characters) for _ in range(length)) + + def set_mongo_uuid(self): + """ + Set the `mongo_uuid` for the associated XForm if it's not already set. + """ + if not self.xform.mongo_uuid: + self.xform.mongo_uuid = self.generate_unique_key() + self.xform.save(update_fields=['mongo_uuid']) + @property def xform_id(self): return self.xform.pk