Skip to content

Commit 9a94226

Browse files
committed
option to auto-map foreign keys (see #14)
1 parent 1a21263 commit 9a94226

File tree

7 files changed

+177
-9
lines changed

7 files changed

+177
-9
lines changed

data_wizard/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.utils.module_loading import autodiscover_modules
22
from .registry import registry
3+
from . import idmap
34

45

56
__all__ = (
@@ -9,6 +10,7 @@
910
"register",
1011
"set_loader",
1112
"default_app_config",
13+
"idmap",
1214
)
1315

1416

data_wizard/idmap.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from rest_framework.serializers import ValidationError
2+
3+
4+
def never(value, field):
5+
return None
6+
7+
8+
def existing(value, field):
9+
try:
10+
field.to_internal_value(value)
11+
except ValidationError:
12+
return None
13+
else:
14+
return value
15+
16+
17+
def always(value, field):
18+
return value

data_wizard/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.conf import settings
55
from . import registry
66
from .compat import reverse
7+
from .settings import import_setting
78

89

910
class Run(models.Model):
@@ -86,6 +87,12 @@ def get_serializer_options(self):
8687
else:
8788
raise Exception("No serializer specified!")
8889

90+
def get_idmap(self):
91+
idmap = self.get_serializer_options().get('idmap')
92+
if not idmap:
93+
idmap = import_setting('IDMAP')
94+
return idmap
95+
8996
def already_parsed(self):
9097
return self.range_set.count()
9198

data_wizard/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
DEFAULTS = {
66
'BACKEND': 'data_wizard.backends.threading',
77
'LOADER': 'data_wizard.loaders.FileLoader',
8+
'IDMAP': 'data_wizard.idmap.never', # FIXME: Change to 'existing' in 2.0
89
'AUTHENTICATION': 'rest_framework.authentication.SessionAuthentication',
910
'PERMISSION': 'rest_framework.permissions.IsAdminUser',
1011
}

data_wizard/tasks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,13 +508,15 @@ def read_row_identifiers(run, user=None):
508508
def parse_row_identifiers(run):
509509
run.add_event('parse_row_identifiers')
510510

511+
idmap = run.get_idmap()
511512
lookup_cols = get_lookup_columns(run)
512513
lookup_fields = OrderedDict()
513514
for col in lookup_cols:
514515
field_name = col['field_name']
515516
lookup_fields.setdefault(field_name, {
516517
'cols': [],
517518
'ids': OrderedDict(),
519+
'serializer_field': col['serializer_field'],
518520
'start_col': 1e10,
519521
'end_col': -1,
520522
})
@@ -568,10 +570,13 @@ def parse_row_identifiers(run):
568570
).first()
569571

570572
if not ident:
573+
value = idmap(name, info['serializer_field'])
571574
ident = Identifier.objects.create(
572575
serializer=run.serializer,
573576
field=field_name,
574577
name=name,
578+
value=value,
579+
resolved=bool(value),
575580
)
576581

577582
run.range_set.create(

tests/data_app/wizard.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ class Meta:
1717
}
1818

1919

20+
# FKModelSerializer = Data Wizard default
21+
22+
23+
class FKMapExistingSerializer(serializers.ModelSerializer):
24+
class Meta:
25+
model = FKModel
26+
fields = "__all__"
27+
data_wizard = {
28+
'idmap': data_wizard.idmap.existing
29+
}
30+
31+
32+
class FKMapAlwaysSerializer(serializers.ModelSerializer):
33+
class Meta:
34+
model = FKModel
35+
fields = "__all__"
36+
data_wizard = {
37+
'idmap': data_wizard.idmap.always
38+
}
39+
40+
2041
class SlugSerializer(serializers.ModelSerializer):
2142
type = serializers.SlugRelatedField(
2243
queryset=Type.objects.all(),
@@ -27,6 +48,12 @@ class Meta:
2748
model = FKModel
2849
fields = "__all__"
2950

51+
class SlugMapExistingSerializer(SlugSerializer):
52+
class Meta(SlugSerializer.Meta):
53+
data_wizard = {
54+
'idmap': data_wizard.idmap.existing
55+
}
56+
3057

3158
class NestedFKSerializer(serializers.ModelSerializer):
3259
class Meta:
@@ -67,7 +94,12 @@ class Meta:
6794
data_wizard.register(SimpleModel)
6895
data_wizard.register('Simple Model - Incomplete', IncompleteSerializer)
6996
data_wizard.register(FKModel)
97+
data_wizard.register('FK Model - Use existing FKs', FKMapExistingSerializer)
98+
data_wizard.register('FK Model - Use FKs always', FKMapAlwaysSerializer)
7099
data_wizard.register('FK Model By Name', SlugSerializer)
100+
data_wizard.register(
101+
'FK Model By Name - Use existing', SlugMapExistingSerializer,
102+
)
71103
data_wizard.register('New Type + FK Model', NestedSerializer)
72104
data_wizard.register(Address)
73105
data_wizard.register('Address with Zip Code', AddressSerializer)

tests/test_foreignkey.py

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@ def check_data(self, run, expect_last_record=None, extra_ranges=[]):
3030

3131
class ForeignKeyTestCase(BaseFKTestCase):
3232
serializer_name = 'data_wizard.registry.FKModelSerializer'
33+
row4_error = '{"type": ["Invalid pk \\"100\\" - object does not exist."]}'
3334

3435
def test_manual(self):
3536
run = self.upload_file('fkid.csv')
36-
row4_error = ('{"type": ["Invalid pk \\"100\\"'
37-
' - object does not exist."]}')
3837

3938
# Start data import process, wait for completion
4039
self.start_import(run, [{
4140
'row': 5,
42-
'reason': row4_error,
41+
'reason': self.row4_error,
4342
}])
4443

4544
# Verify results
4645
self.check_data(
47-
run, expect_last_record="Failed at row 4: %s" % row4_error
46+
run,
47+
expect_last_record="Failed at row 4: %s" % self.row4_error,
4848
)
4949
self.assert_log(run, [
5050
'created',
@@ -53,8 +53,27 @@ def test_manual(self):
5353
'import_complete',
5454
])
5555

56-
def test_auto(self):
57-
# Should abort due to unknown type id
56+
def test_auto_idmap_never(self):
57+
# Should abort due to previously unmapped type ids
58+
run = self.upload_file('fkid.csv')
59+
self.auto_import(run, expect_input_required=True)
60+
self.assert_log(run, [
61+
'created',
62+
'auto_import',
63+
'parse_columns',
64+
'parse_row_identifiers',
65+
])
66+
self.assert_ranges(run, [
67+
"Data Column 'type -> type' at Rows 1-4, Column 0",
68+
"Data Column 'notes -> notes' at Rows 1-4, Column 1",
69+
"Cell value 'Unresolved: 1' at Rows 1-2, Column 0",
70+
"Cell value 'Unresolved: 2' at Row 3, Column 0",
71+
"Cell value 'Unresolved: 100' at Row 4, Column 0",
72+
])
73+
74+
def test_auto_idmap_existing(self):
75+
# Should auto-map existing ids 1 & 2, but abort due to unknown id 100
76+
self.serializer_name = 'tests.data_app.wizard.FKMapExistingSerializer'
5877
run = self.upload_file('fkid.csv')
5978
self.auto_import(run, expect_input_required=True)
6079
self.assert_log(run, [
@@ -63,6 +82,40 @@ def test_auto(self):
6382
'parse_columns',
6483
'parse_row_identifiers',
6584
])
85+
self.assert_ranges(run, [
86+
"Data Column 'type -> type' at Rows 1-4, Column 0",
87+
"Data Column 'notes -> notes' at Rows 1-4, Column 1",
88+
"Cell value '1 -> type=1' at Rows 1-2, Column 0",
89+
"Cell value '2 -> type=2' at Row 3, Column 0",
90+
"Cell value 'Unresolved: 100' at Row 4, Column 0",
91+
])
92+
93+
def test_auto_idmap_always(self):
94+
# Map all ids without checking, producing same result as test_manual
95+
96+
self.serializer_name = 'tests.data_app.wizard.FKMapAlwaysSerializer'
97+
run = self.upload_file('fkid.csv')
98+
99+
self.auto_import(run, expect_input_required=False)
100+
101+
self.assert_log(run, [
102+
'created',
103+
'auto_import',
104+
'parse_columns',
105+
'parse_row_identifiers',
106+
'do_import',
107+
'import_complete',
108+
])
109+
110+
self.check_data(
111+
run,
112+
expect_last_record="Failed at row 4: %s" % self.row4_error,
113+
extra_ranges=[
114+
"Cell value '1 -> type=1' at Rows 1-2, Column 0",
115+
"Cell value '2 -> type=2' at Row 3, Column 0",
116+
"Cell value '100 -> type=100' at Row 4, Column 0",
117+
]
118+
)
66119

67120

68121
class SlugTestCase(BaseFKTestCase):
@@ -90,8 +143,8 @@ def test_manual(self):
90143
'import_complete',
91144
])
92145

93-
def test_auto(self):
94-
# Should abort due to unknown type id
146+
def test_auto_idmap_never(self):
147+
# Should abort due to previously unmapped type ids
95148
run = self.upload_file('fkname.csv')
96149
self.auto_import(run, expect_input_required=True)
97150
self.assert_log(run, [
@@ -131,7 +184,7 @@ def test_auto(self):
131184
'import_complete',
132185
])
133186

134-
def test_auto_preset(self):
187+
def test_auto_idmap_never_preset(self):
135188
self.create_identifier('Type #1', 'type', 'Type #1')
136189
self.create_identifier('Type #2', 'type', 'Type #2')
137190
self.create_identifier('Type #2 - Alternate Name', 'type', 'Type #2')
@@ -158,6 +211,56 @@ def test_auto_preset(self):
158211
'import_complete',
159212
])
160213

214+
def test_auto_idmap_existing(self):
215+
# Should auto-map 2 existing slugs, but abort due to unknown Alt Name
216+
self.serializer_name = (
217+
'tests.data_app.wizard.SlugMapExistingSerializer'
218+
)
219+
run = self.upload_file('fkname.csv')
220+
self.auto_import(run, expect_input_required=True)
221+
self.assert_log(run, [
222+
'created',
223+
'auto_import',
224+
'parse_columns',
225+
'parse_row_identifiers',
226+
])
227+
self.assert_ranges(run, [
228+
"Data Column 'type -> type' at Rows 1-4, Column 0",
229+
"Data Column 'notes -> notes' at Rows 1-4, Column 1",
230+
"Cell value 'Type #1 -> type=Type #1' at Rows 1-2, Column 0",
231+
"Cell value 'Type #2 -> type=Type #2' at Row 3, Column 0",
232+
"Cell value 'Unresolved: Type #2 - Alternate Name'"
233+
" at Row 4, Column 0"
234+
])
235+
236+
# Update identifiers and try again
237+
self.update_row_identifiers(run, {
238+
'data_app.type': {
239+
'Type #2 - Alternate Name': 'Type #2',
240+
}
241+
})
242+
self.auto_import(run, expect_input_required=False)
243+
self.check_data(
244+
run,
245+
expect_last_record="Imported 'Type #2 (Test Note 4)' at row 4",
246+
extra_ranges=[
247+
"Cell value 'Type #1 -> type=Type #1' at Rows 1-2, Column 0",
248+
"Cell value 'Type #2 -> type=Type #2' at Row 3, Column 0",
249+
"Cell value 'Type #2 - Alternate Name -> type=Type #2'"
250+
" at Row 4, Column 0",
251+
]
252+
)
253+
self.assert_log(run, [
254+
'created',
255+
'auto_import',
256+
'parse_columns',
257+
'parse_row_identifiers',
258+
'update_row_identifiers',
259+
'auto_import',
260+
'do_import',
261+
'import_complete',
262+
])
263+
161264

162265
class SplitTestCase(BaseFKTestCase):
163266
serializer_name = 'tests.data_app.wizard.SlugSerializer'

0 commit comments

Comments
 (0)