merge_racks_to_concept() copied rack_bottom.airflow and device.face
straight from the real objects. Both are nullable on dcim.Rack/Device
(airflow when not set, face on full-depth devices), but the
corresponding RackConcept.airflow and ConceptDevice.face fields are
not nullable. Model.full_clean() doesn't catch this: for a blank=True
field, Django's clean_fields() skips validation entirely when the raw
value is already an "empty" value (including None), so the None
sailed straight through to the database as a raw IntegrityError
instead of a clean form error.
Normalize both: airflow falls back to '' (matching the existing
form_factor fallback in the same function), face falls back to
DeviceFaceChoices.FACE_FRONT (matching ConceptDevice.face's own
default). Applied the face fallback everywhere a ConceptDevice is
built from another device's .face, including the two copy paths that
read from an already-validated ConceptDevice, as a harmless defense
in depth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>