Skip to content

Commit 817e49f

Browse files
committed
Refactor validated_instances to address MR - see HEA-898
1 parent 78bfdf3 commit 817e49f

1 file changed

Lines changed: 49 additions & 35 deletions

File tree

pipelines/assets/fixtures.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,10 @@ def validate_instances(
173173
# The model is not in the fixture, and hasn't been checked already, so use the primary and
174174
# natural keys for already saved instances. Save the keys as a dict mapping to the
175175
# instance, so that we can resolve natural keys later when validating model.clean()
176-
remote_keys = {
177-
related_instance.pk: related_instance
178-
for related_instance in field.related_model.objects.all()
179-
}
180-
if hasattr(field.related_model, "natural_key"):
181-
for related_instance in field.related_model.objects.all():
176+
remote_keys = {}
177+
for related_instance in field.related_model.objects.all():
178+
remote_keys[related_instance.pk] = related_instance
179+
if hasattr(field.related_model, "natural_key"):
182180
remote_keys[related_instance.natural_key()] = related_instance
183181
valid_keys[field.related_model.__name__] = remote_keys
184182

@@ -214,47 +212,63 @@ def validate_instances(
214212
# Replace empty strings with None for optional and numeric fields
215213
if value == "" and (field.null or field.get_internal_type() not in ["CharField", "TextField"]):
216214
value = None
217-
# Handle foreign key fields, resolving natural keys to model instances
215+
# Handle foreign key fields
218216
if isinstance(field, models.ForeignKey):
219-
if isinstance(value, list):
220-
value = tuple(value)
221-
value = valid_keys[field.related_model.__name__].get(value)
217+
# Natural keys need to be converted to tuples for looking up in valid_keys
218+
lookup_value = tuple(value) if isinstance(value, list) else value
219+
related_instance = valid_keys[field.related_model.__name__].get(lookup_value)
220+
if related_instance:
221+
# Assign the resolved model instance to the relationship attribute (e.g. `product`),
222+
setattr(model_instance, field.name, related_instance)
223+
# Also set the attname (e.g. `product_id`) to the primary key
224+
if related_instance.pk:
225+
setattr(model_instance, field.get_attname(), related_instance.pk)
222226
else:
223227
# Use the Django model to validate the fields, so we can apply already defined model
224228
# validations and return informative error messages.
225229
try:
226-
field.clean(value, model_instance)
230+
value = field.clean(value, model_instance)
231+
setattr(model_instance, column, value)
232+
except ValidationError as e:
233+
# Record validation error messages in a similar format to field.clean handling
234+
msgs = []
235+
if hasattr(e, "message_dict"):
236+
for v in e.message_dict.values():
237+
msgs.extend(v if isinstance(v, list) else [v])
238+
elif hasattr(e, "messages"):
239+
msgs = e.messages
240+
else:
241+
msgs = [str(e)]
242+
for msg in msgs:
243+
error = f"Invalid {field.name} value {value}: {msg} for {record_reference}."
244+
model_errors.append(error)
227245
except Exception as e:
228-
error = (
229-
f'Invalid {field.name} value {value}: "{", ".join(e.error_list[0].messages)}" '
230-
f"for {record_reference}."
231-
)
232-
model_errors.append(error)
233-
setattr(model_instance, column, value)
246+
# Record unexpected errors from clean() so the author can investigate
247+
model_errors.append(f"Invalid {field.name} value {value}: {e} for {record_reference}.")
234248
# We don't need to validate LivelihoodActivity, because we will validate the subclass instances anyway.
235249
if model != LivelihoodActivity:
236250
try:
237251
model_instance.clean()
238-
except Exception as e:
252+
except ObjectDoesNotExist:
239253
# Ignore missing related object errors since the related object may be
240-
# loaded by the same fixture. We check class name to handle a possible
241-
# custom RelatedObjectNotFound exception defined elsewhere.
242-
if isinstance(e, ObjectDoesNotExist) or e.__class__.__name__ == "RelatedObjectNotFound":
243-
pass
244-
elif isinstance(e, ValidationError):
245-
# Record validation error messages in a similar format to field.clean handling
246-
msgs = []
247-
if hasattr(e, "message_dict"):
248-
for k, v in e.message_dict.items():
249-
msgs.extend(v if isinstance(v, list) else [v])
250-
else:
251-
msgs = e.messages if hasattr(e, "messages") else [str(e)]
252-
for msg in msgs:
253-
error = f"{msg} for {record_reference}."
254-
model_errors.append(error)
254+
# loaded by the same fixture.
255+
pass
256+
except ValidationError as e:
257+
# Record validation error messages in a similar format to field.clean handling
258+
msgs = []
259+
if hasattr(e, "message_dict"):
260+
for v in e.message_dict.values():
261+
msgs.extend(v if isinstance(v, list) else [v])
262+
elif hasattr(e, "messages"):
263+
msgs = e.messages
255264
else:
256-
# Record unexpected errors from clean() so the author can investigate
257-
model_errors.append(f"Error during clean(): {e} for {record_reference}.")
265+
msgs = [str(e)]
266+
for msg in msgs:
267+
error = f"{msg} for {record_reference}."
268+
model_errors.append(error)
269+
except Exception as e:
270+
# Record unexpected errors from clean() so the author can investigate
271+
model_errors.append(f"Error during clean(): {e} for {record_reference}.")
258272
except Exception as e:
259273
model_errors.append(f"Error creating {model_name} instance: {e} for {record_reference}.")
260274
# Ignore errors creating the instance for clean()

0 commit comments

Comments
 (0)