-
-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33999 from dimagi/gh/data-dump/use-natural-foreig…
…n-keys Use natural keys for foreign key relationships during serialization/deserialization
- Loading branch information
Showing
8 changed files
with
240 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,97 @@ | ||
import json | ||
from io import StringIO | ||
from unittest.mock import patch | ||
|
||
from django.contrib.auth.models import User | ||
from django.core.serializers.python import Deserializer | ||
from django.test import SimpleTestCase | ||
|
||
from corehq.apps.dump_reload.sql.dump import SqlDataDumper | ||
from corehq.apps.users.models import SQLUserData | ||
from corehq.apps.users.models_role import Permission, RolePermission, UserRole | ||
from corehq.form_processor.models.cases import CaseTransaction, CommCareCase | ||
from corehq.form_processor.models.forms import XFormInstance, XFormOperation | ||
|
||
|
||
class TestJSONFieldSerialization(SimpleTestCase): | ||
"""See https://github.com/bradjasper/django-jsonfield/pull/173""" | ||
""" | ||
See https://github.com/bradjasper/django-jsonfield/pull/173 | ||
We just need to test that a model that uses jsonfield.JSONField is serialized correctly | ||
""" | ||
|
||
def test(self): | ||
serialized_model_with_primary_key = { | ||
'model': 'form_processor.XFormInstance', 'pk': 1, 'fields': {'auth_context': '{}'} | ||
'model': 'accounting.BillingContactInfo', 'pk': 1, 'fields': {'email_list': '{}'} | ||
} | ||
serialized_model_with_natural_key = { | ||
'model': 'form_processor.XFormInstance', 'fields': {'auth_context': '{}'} | ||
'model': 'accounting.BillingContactInfo', 'fields': {'email_list': '{}'} | ||
} | ||
|
||
def _test_json_field_after_serialization(serialized): | ||
for obj in Deserializer([serialized]): | ||
self.assertIsInstance(obj.object.auth_context, dict) | ||
self.assertIsInstance(obj.object.email_list, dict) | ||
|
||
_test_json_field_after_serialization(serialized_model_with_primary_key) | ||
_test_json_field_after_serialization(serialized_model_with_natural_key) | ||
|
||
|
||
class TestForeignKeyFieldSerialization(SimpleTestCase): | ||
""" | ||
We use natural foreign keys when dumping SQL data, but CommCareCase and XFormInstance have natural_key methods | ||
that intentionally return a string for the case_id or form_id, rather than a tuple as Django recommends for | ||
all natural_key methods. We made this decision to optimize loading deserialized data back into a database. If | ||
the natural_key method returns a tuple, it will use the get_by_natural_key method on the foreign key model's | ||
default object manager to fetch the foreign keyed object, resulting in a database lookup everytime we write | ||
a model that foreign keys to cases or forms in SqlDataLoader. | ||
""" | ||
|
||
def test_natural_foreign_key_returns_iterable_when_serialized(self): | ||
user = User(username='testuser') | ||
user_data = SQLUserData(django_user=user, data={'test': 1}) | ||
|
||
output_stream = StringIO() | ||
with patch('corehq.apps.dump_reload.sql.dump.get_objects_to_dump', return_value=[user_data]): | ||
SqlDataDumper('test', [], []).dump(output_stream) | ||
|
||
deserialized_model = json.loads(output_stream.getvalue()) | ||
fk_field = deserialized_model['fields']['django_user'] | ||
self.assertEqual(fk_field, ['testuser']) | ||
|
||
def test_foreign_key_on_model_without_natural_key_returns_primary_key_when_serialized(self): | ||
role = UserRole(pk=10, domain='test', name='test-role') | ||
permission = Permission(pk=500, value='test') | ||
role_permission = RolePermission(role=role, permission_fk=permission) | ||
|
||
output_stream = StringIO() | ||
with patch('corehq.apps.dump_reload.sql.dump.get_objects_to_dump', return_value=[role_permission]): | ||
SqlDataDumper('test', [], []).dump(output_stream) | ||
|
||
deserialized_model = json.loads(output_stream.getvalue()) | ||
role_field = deserialized_model['fields']['role'] | ||
self.assertEqual(role_field, 10) | ||
permission_field = deserialized_model['fields']['permission_fk'] | ||
self.assertEqual(permission_field, 500) | ||
|
||
def test_natural_foreign_key_for_CommCareCase_returns_str_when_serialized(self): | ||
cc_case = CommCareCase(domain='test', case_id='abc123') | ||
transaction = CaseTransaction(case=cc_case) | ||
|
||
output_stream = StringIO() | ||
with patch('corehq.apps.dump_reload.sql.dump.get_objects_to_dump', return_value=[transaction]): | ||
SqlDataDumper('test', [], []).dump(output_stream) | ||
|
||
deserialized_model = json.loads(output_stream.getvalue()) | ||
fk_field = deserialized_model['fields']['case'] | ||
self.assertEqual(fk_field, 'abc123') | ||
|
||
def test_natural_foreign_key_for_XFormInstance_returns_str_when_serialized(self): | ||
xform = XFormInstance(domain='test', form_id='abc123') | ||
operation = XFormOperation(form=xform) | ||
|
||
output_stream = StringIO() | ||
with patch('corehq.apps.dump_reload.sql.dump.get_objects_to_dump', return_value=[operation]): | ||
SqlDataDumper('test', [], []).dump(output_stream) | ||
|
||
deserialized_model = json.loads(output_stream.getvalue()) | ||
fk_field = deserialized_model['fields']['form'] | ||
self.assertEqual(fk_field, 'abc123') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import json | ||
|
||
from django.contrib.auth.models import User | ||
from django.test import TestCase | ||
|
||
from corehq.apps.dump_reload.sql.load import SqlDataLoader | ||
from corehq.apps.users.models import SQLUserData | ||
from corehq.apps.users.models_role import Permission, RolePermission, UserRole | ||
from corehq.form_processor.models.cases import CaseTransaction | ||
from corehq.form_processor.tests.utils import create_case | ||
|
||
|
||
class TestSqlDataLoader(TestCase): | ||
|
||
def test_loading_foreign_keys_using_iterable_natural_key(self): | ||
user = User.objects.create(username='testuser') | ||
model = { | ||
"model": "users.sqluserdata", | ||
"fields": { | ||
"domain": "test", | ||
"user_id": "testuser", | ||
"django_user": ["testuser"], | ||
"modified_on": "2024-01-01T12:00:00.000000Z", | ||
"profile": None, | ||
"data": {"test": "1"}, | ||
}, | ||
} | ||
serialized_model = json.dumps(model) | ||
|
||
SqlDataLoader().load_objects([serialized_model]) | ||
|
||
user_data = SQLUserData.objects.get(django_user=user) | ||
self.assertEqual(user_data.django_user.pk, user.pk) | ||
|
||
def test_loading_foreign_keys_using_non_iterable_natural_key(self): | ||
# create_case will create a CaseTransaction too so test verifies the serialized one is saved properly | ||
cc_case = create_case('test', case_id='abc123', save=True) | ||
model = { | ||
"model": "form_processor.casetransaction", | ||
"fields": { | ||
"case": "abc123", | ||
"form_id": "fk-test", | ||
"sync_log_id": None, | ||
"server_date": "2024-01-01T12:00:00.000000Z", | ||
"_client_date": None, | ||
"type": 1, | ||
"revoked": False, | ||
"details": {}, | ||
}, | ||
} | ||
serialized_model = json.dumps(model) | ||
|
||
SqlDataLoader().load_objects([serialized_model]) | ||
|
||
transaction = CaseTransaction.objects.partitioned_query('abc123').get(case=cc_case, form_id='fk-test') | ||
self.assertEqual(transaction.case_id, 'abc123') | ||
|
||
def test_loading_foreign_keys_using_primary_key(self): | ||
role = UserRole.objects.create(domain='test', name='test-role') | ||
permission = Permission.objects.create(value='test') | ||
model = { | ||
"model": "users.rolepermission", | ||
"pk": 1, | ||
"fields": {"role": role.pk, "permission_fk": permission.pk, "allow_all": True, "allowed_items": []}, | ||
} | ||
serialized_model = json.dumps(model) | ||
|
||
SqlDataLoader().load_objects([serialized_model]) | ||
|
||
role_permission = RolePermission.objects.get(role=role, permission_fk=permission) | ||
self.assertEqual(role_permission.pk, 1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -360,6 +360,26 @@ def test_users(self): | |
|
||
self._dump_and_load(expected_object_counts) | ||
|
||
def test_sqluserdata(self): | ||
from corehq.apps.users.models import SQLUserData, WebUser | ||
from django.contrib.auth.models import User | ||
|
||
expected_object_counts = Counter({User: 1, SQLUserData: 1}) | ||
|
||
web_user = WebUser.create( | ||
domain=self.domain_name, | ||
username='webuser_t1', | ||
password='secret', | ||
created_by=None, | ||
created_via=None, | ||
email='[email protected]', | ||
) | ||
self.addCleanup(web_user.delete, self.domain_name, deleted_by=None) | ||
user = web_user.get_django_user() | ||
SQLUserData.objects.create(domain=self.domain_name, data={'test': 1}, django_user=user) | ||
|
||
self._dump_and_load(expected_object_counts) | ||
|
||
def test_dump_roles(self): | ||
from corehq.apps.users.models import UserRole, HqPermissions, RoleAssignableBy, RolePermission | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters