Skip to content

Commit c261022

Browse files
committed
Migrate legacy data from django reversion
- Migrate Per Overview version datetime data to date to avoid django serialization error - Add helper function for future
1 parent acd0bd6 commit c261022

File tree

4 files changed

+229
-1
lines changed

4 files changed

+229
-1
lines changed

main/utils.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import requests
2+
import typing
3+
import json
4+
import datetime
25

36
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
47
from collections import defaultdict
58

9+
from reversion.models import Version
10+
from reversion.revisions import _get_options
11+
from django.utils.dateparse import parse_datetime, parse_date
12+
from django.db import models, router
13+
from django.contrib.contenttypes.models import ContentType
14+
615

716
def is_tableau(request):
817
""" Checking the request for the 'tableau' parameter
@@ -60,3 +69,94 @@ def __enter__(self) -> _TemporaryFileWrapper:
6069
def __exit__(self, *_):
6170
if self.downloaded_file:
6271
self.downloaded_file.close()
72+
73+
74+
class DjangoReversionDataFixHelper:
75+
@staticmethod
76+
def _get_content_type(
77+
content_type_model: typing.Type[ContentType],
78+
model: typing.Type[models.Model],
79+
using
80+
):
81+
version_options = _get_options(model)
82+
return content_type_model.objects.db_manager(using).get_for_model(
83+
model,
84+
for_concrete_model=version_options.for_concrete_model,
85+
)
86+
87+
@classmethod
88+
def get_for_model(
89+
cls,
90+
content_type_model: typing.Type[ContentType],
91+
version_model: typing.Type[Version],
92+
model: typing.Type[models.Model],
93+
):
94+
model_db = router.db_for_write(model)
95+
content_type = cls._get_content_type(content_type_model, model, version_model.objects.db)
96+
return version_model.objects.filter(
97+
content_type=content_type,
98+
db=model_db,
99+
)
100+
101+
@classmethod
102+
def _date_field_adjust(
103+
cls,
104+
content_type_model: typing.Type[ContentType],
105+
version_model: typing.Type[Version],
106+
model: typing.Type[models.Model],
107+
fields: typing.List[str],
108+
parser: typing.Callable,
109+
renderer: typing.Callable,
110+
):
111+
updated_versions = []
112+
for version in cls.get_for_model(content_type_model, version_model, model):
113+
updated_serialized_data = json.loads(version.serialized_data)
114+
has_changed = False
115+
for field in fields:
116+
if field not in updated_serialized_data[0]['fields']:
117+
continue
118+
updated_value = parser(updated_serialized_data[0]['fields'][field])
119+
if updated_value is None:
120+
# For other format, parser should return None
121+
continue
122+
updated_serialized_data[0]['fields'][field] = renderer(updated_value)
123+
has_changed = True
124+
if has_changed:
125+
version.serialized_data = json.dumps(updated_serialized_data)
126+
updated_versions.append(version)
127+
128+
version_model.objects.bulk_update(updated_versions, fields=('serialized_data',))
129+
130+
@classmethod
131+
def date_fields_to_datetime(
132+
cls,
133+
content_type_model: typing.Type[ContentType],
134+
version_model: typing.Type[Version],
135+
model: typing.Type[models.Model],
136+
fields: typing.List[str],
137+
):
138+
return cls._date_field_adjust(
139+
content_type_model,
140+
version_model,
141+
model,
142+
fields,
143+
parse_date,
144+
lambda x: datetime.datetime.combine(x, datetime.datetime.min.time()).isoformat(),
145+
)
146+
147+
@classmethod
148+
def datetime_fields_to_date(
149+
cls,
150+
content_type_model: typing.Type[ContentType],
151+
version_model: typing.Type[Version],
152+
model: typing.Type[models.Model],
153+
fields: typing.List[str],
154+
):
155+
return cls._date_field_adjust(
156+
content_type_model,
157+
version_model,
158+
model,
159+
fields,
160+
parse_datetime,
161+
lambda x: x.date().isoformat(),
162+
)

per/factories.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import factory
22
import datetime
3+
from factory import fuzzy
34

45
from per.models import (
56
Overview,
@@ -22,7 +23,7 @@ class Meta:
2223

2324

2425
class OverviewFactory(factory.django.DjangoModelFactory):
25-
date_of_assessment = factory.fuzzy.FuzzyNaiveDateTime(datetime.datetime(2023, 1, 1))
26+
date_of_assessment = fuzzy.FuzzyNaiveDateTime(datetime.datetime(2023, 1, 1))
2627
type_of_assessment = factory.SubFactory(AssessmentTypeFactory)
2728

2829
class Meta:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 3.2.23 on 2024-02-08 05:02
2+
3+
from django.db import migrations
4+
from main.utils import DjangoReversionDataFixHelper
5+
6+
7+
def modify_per_datetime_data_to_date(self, schema_editor):
8+
ContentType = self.get_model('contenttypes', 'ContentType')
9+
Version = self.get_model('reversion', 'Version')
10+
Overview = self.get_model('per', 'Overview')
11+
DjangoReversionDataFixHelper.datetime_fields_to_date(ContentType, Version, Overview, ['date_of_assessment'])
12+
13+
14+
def modify_per_date_data_to_datetime(self, schema_editor):
15+
ContentType = self.get_model('contenttypes', 'ContentType')
16+
Version = self.get_model('reversion', 'Version')
17+
Overview = self.get_model('per', 'Overview')
18+
DjangoReversionDataFixHelper.date_fields_to_datetime(ContentType, Version, Overview, ['date_of_assessment'])
19+
20+
21+
class Migration(migrations.Migration):
22+
23+
dependencies = [
24+
('reversion', '0001_squashed_0004_auto_20160611_1202'),
25+
('contenttypes', '0002_remove_content_type_name'),
26+
('per', '0097_alter_opslearning_appeal_code'),
27+
]
28+
29+
operations = [
30+
migrations.RunPython(
31+
modify_per_datetime_data_to_date, # Time data is lost here
32+
reverse_code=modify_per_date_data_to_datetime
33+
),
34+
]

utils/test_utils.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import erp
2+
import json
3+
from collections import Counter
24
from django.test import TestCase
5+
6+
import reversion
7+
from reversion.errors import RevertError
8+
from reversion.models import Version
9+
from django.contrib.contenttypes.models import ContentType
10+
311
from api.models import Event, FieldReport, Region, Country, DisasterType, ERPGUID
412
from main.mock import erp_request_side_effect_mock
13+
from main.utils import DjangoReversionDataFixHelper
514
from unittest.mock import patch
615

16+
from per.models import Overview as PerOverview
17+
from per.factories import OverviewFactory as PerOverviewFactory
718
from api.factories import disaster_type as dtFactory
819
from api.factories import country as countryFactory
920
from api.factories import event as eventFactory
@@ -44,3 +55,85 @@ def test_successful(self, erp_request_side_effect_mock):
4455
self.assertEqual(ERP.api_guid, 'FindThisGUID')
4556
self.assertEqual(ERP.field_report_id, report.id)
4657
self.assertEqual(erp_request_side_effect_mock.called, True)
58+
59+
60+
class DjangoReversionDataFixHelperTest(TestCase):
61+
def get_version_qs(self):
62+
return Version.objects.get_for_model(PerOverview)
63+
64+
def update_serialized_data(self, raw_data, new_value):
65+
new_data = json.loads(raw_data)
66+
new_data[0]['fields'][self.field_name] = new_value
67+
return json.dumps(new_data)
68+
69+
def get_version_data_snapshot(self, field_name):
70+
# Version data snapshot excluding self.field_name
71+
version_data_snapshot = []
72+
for _id, data_raw in self.get_version_qs().values_list('id', 'serialized_data').order_by('id'):
73+
data = json.loads(data_raw)
74+
data[0]['fields'].pop(field_name)
75+
version_data_snapshot.append({
76+
'id': _id,
77+
'data': data,
78+
})
79+
return version_data_snapshot
80+
81+
def assert_values(self, values: dict):
82+
# Make sure other values are not changed
83+
assert self.version_data_snapshot == self.get_version_data_snapshot(self.field_name)
84+
# Check count
85+
assert self.get_version_qs().count() == sum(values.values())
86+
# Check count by value
87+
assert dict(Counter([
88+
json.loads(data)[0]['fields'][self.field_name]
89+
for data in self.get_version_qs().values_list('serialized_data', flat=True)
90+
])) == values
91+
92+
def confirm_version_data_serialization(self):
93+
for version in self.get_version_qs().all():
94+
version._local_field_dict
95+
96+
def setUp(self):
97+
super().setUp()
98+
self.field_name = 'date_of_assessment'
99+
for _ in range(95):
100+
reversion.create_revision()(PerOverviewFactory.create)()
101+
102+
versions = self.get_version_qs().all()
103+
# Create dataset with different formats
104+
for index, version in enumerate(versions):
105+
new_value = '2022-01-01'
106+
if (index % 2) == 0:
107+
new_value = '2022-01-01T00:00:00'
108+
version.serialized_data = self.update_serialized_data(
109+
version.serialized_data,
110+
new_value,
111+
)
112+
version.save()
113+
Version.objects.bulk_update(versions, fields=('serialized_data',))
114+
# Version data snapshot excluding self.field_name
115+
self.version_data_snapshot = self.get_version_data_snapshot(self.field_name)
116+
self.assert_values({'2022-01-01': 47, '2022-01-01T00:00:00': 48})
117+
118+
def test_date_fields_to_datetime(self):
119+
self.assert_values({'2022-01-01': 47, '2022-01-01T00:00:00': 48})
120+
DjangoReversionDataFixHelper.date_fields_to_datetime(
121+
ContentType,
122+
Version,
123+
PerOverview,
124+
[self.field_name]
125+
)
126+
self.assert_values({'2022-01-01T00:00:00': 95})
127+
128+
def test_datetime_fields_to_date(self):
129+
with self.assertRaises(RevertError):
130+
self.confirm_version_data_serialization()
131+
self.assert_values({'2022-01-01': 47, '2022-01-01T00:00:00': 48})
132+
DjangoReversionDataFixHelper.datetime_fields_to_date(
133+
ContentType,
134+
Version,
135+
PerOverview,
136+
[self.field_name]
137+
)
138+
self.assert_values({'2022-01-01': 95})
139+
self.confirm_version_data_serialization()

0 commit comments

Comments
 (0)