Skip to content

Commit a15e737

Browse files
authored
fix(submission): Add management command to reassign root_uuid to prevent 409 conflict when re-editing an already edited submission (#5888)
### 📣 Summary Add management command `clean_duplicated_submissions_root_uuid` to reassign `root_uuid` to prevents conflict errors (HTTP 409) when editing a submission multiple times in a row. ### 📖 Description This PR fixes an issue where edited submissions would trigger a 409 Conflict error if a user tried to edit them again. The problem emerged after switching to the new root_uuid field (introduced in the fix for #5852, PR #5861) to identify submissions more reliably. However, submissions that had not yet been fully backfilled with root_uuid data caused conflicts when edited repeatedly
1 parent 128fd66 commit a15e737

File tree

1 file changed

+122
-0
lines changed

1 file changed

+122
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import time
2+
3+
from django.conf import settings
4+
from django.core.management.base import BaseCommand, CommandError
5+
from django.core.management import call_command
6+
from django.db.utils import IntegrityError
7+
8+
from kobo.apps.openrosa.apps.logger.models.instance import Instance
9+
from kobo.apps.openrosa.apps.logger.xform_instance_parser import (
10+
set_meta,
11+
add_uuid_prefix,
12+
)
13+
14+
15+
class Command(BaseCommand):
16+
17+
help = """
18+
Create a unique `root_uuid` for submissions saved before the `2.025.02` release,
19+
when their current `root_uuid` is already used by another submission
20+
within the same project."
21+
"""
22+
23+
def add_arguments(self, parser):
24+
super().add_arguments(parser)
25+
26+
parser.add_argument(
27+
'--xform',
28+
help="Specify a XForm's `id_string`",
29+
)
30+
31+
def handle(self, *args, **options):
32+
self._verbosity = options['verbosity']
33+
34+
if not (xform_id_string := options.get('xform')):
35+
raise CommandError("XForm's `id_string` must be specified")
36+
37+
# First
38+
try:
39+
exit_code = call_command(
40+
'clean_duplicated_submissions',
41+
verbosity=self._verbosity,
42+
xform=xform_id_string,
43+
)
44+
except Exception as e:
45+
exit_code = 1
46+
error = str(e)
47+
else:
48+
error = ''
49+
50+
if exit_code:
51+
raise CommandError(
52+
f'`clean_duplicated_submissions` command has completed '
53+
f'with errors: {error}'
54+
)
55+
56+
# Retrieve all instances with the same `uuid`
57+
queryset = Instance.objects.filter(
58+
xform__id_string=xform_id_string, root_uuid__isnull=True
59+
)
60+
61+
for instance in queryset.iterator():
62+
try:
63+
instance._populate_root_uuid() # noqa
64+
if self._verbosity >= 1:
65+
self.stdout.write(
66+
f'Processing root_uuid `{instance.root_uuid}`…'
67+
)
68+
# Bypass Instance.save() mechanism
69+
Instance.objects.filter(pk=instance.pk).update(
70+
root_uuid=instance.root_uuid
71+
)
72+
except IntegrityError as e:
73+
if 'unique_root_uuid_per_xform' not in str(e):
74+
self.stderr.write(
75+
f'Could not update instance #{instance.pk} '
76+
f'- uuid: {instance.uuid}'
77+
)
78+
79+
if self._verbosity >= 2:
80+
self.stdout.write('\tConflict detected!')
81+
82+
xml_hash = Instance.objects.values_list(
83+
'xml_hash', flat=True
84+
).get(root_uuid=instance.root_uuid)
85+
# Only consider different hashes, because if there are the
86+
# same, they should have been handled by clean_duplicated_submissions
87+
# management command
88+
if xml_hash != instance.xml_hash:
89+
old_uuid = instance.uuid
90+
now = int(time.time() * 1000)
91+
instance.root_uuid = (
92+
f'CONFLICT-{now}-{xform_id_string}-{old_uuid}'
93+
)
94+
instance.xml = set_meta(
95+
instance.xml,
96+
'rootUuid',
97+
add_uuid_prefix(instance.root_uuid),
98+
)
99+
instance.xml_hash = instance.get_hash(instance.xml)
100+
if self._verbosity >= 2:
101+
self.stdout.write(
102+
f'\tOld root_uuid: {old_uuid}, '
103+
f'New UUID: {instance.root_uuid}'
104+
)
105+
# Bypass Instance.save() mechanism
106+
Instance.objects.filter(pk=instance.pk).update(
107+
xml=instance.xml,
108+
xml_hash=instance.xml_hash,
109+
root_uuid=instance.root_uuid
110+
)
111+
doc = settings.MONGO_DB.instances.find_one(
112+
{'_id': instance.pk}
113+
)
114+
doc['meta/rootUuid'] = add_uuid_prefix(instance.root_uuid)
115+
settings.MONGO_DB.instances.replace_one(
116+
{'_id': instance.pk}, doc, upsert=True
117+
)
118+
except AssertionError:
119+
self.stderr.write(
120+
f'Could not update root_uuid of instance #{instance.pk} '
121+
f'- uuid: {instance.uuid}'
122+
)

0 commit comments

Comments
 (0)