Fix IntegrityError merging real racks: airflow/face can be NULL on a Rack/Device
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>
This commit is contained in:
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.choices import RackWidthChoices
|
||||
from dcim.choices import DeviceFaceChoices, RackWidthChoices
|
||||
from dcim.models import Device, Rack
|
||||
|
||||
from . import components
|
||||
@@ -113,7 +113,7 @@ def copy_rack_to_concept(rack, name, status=ConceptStatusChoices.STATUS_DRAFT, t
|
||||
platform=device.platform,
|
||||
tenant=device.tenant,
|
||||
position=device.position,
|
||||
face=device.face,
|
||||
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||
width_fraction=fraction,
|
||||
horizontal_position=horizontal,
|
||||
serial=device.serial if include_device_names else '',
|
||||
@@ -187,7 +187,7 @@ def merge_concepts(concept_bottom, concept_top, name, status=ConceptStatusChoice
|
||||
platform=device.platform,
|
||||
tenant=device.tenant,
|
||||
position=(device.position + shift) if device.position is not None else None,
|
||||
face=device.face,
|
||||
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||
width_fraction=device.width_fraction,
|
||||
horizontal_position=device.horizontal_position,
|
||||
u_height=device.u_height,
|
||||
@@ -245,7 +245,7 @@ def merge_racks_to_concept(rack_bottom, rack_top, name, status=ConceptStatusChoi
|
||||
u_height=total_height,
|
||||
starting_unit=starting_unit,
|
||||
form_factor=_rack_geometry(rack_bottom, 'form_factor') or '',
|
||||
airflow=rack_bottom.airflow,
|
||||
airflow=rack_bottom.airflow or '',
|
||||
default_site=rack_bottom.site,
|
||||
default_location=rack_bottom.location,
|
||||
rack_role=rack_bottom.role,
|
||||
@@ -274,7 +274,7 @@ def merge_racks_to_concept(rack_bottom, rack_top, name, status=ConceptStatusChoi
|
||||
platform=device.platform,
|
||||
tenant=device.tenant,
|
||||
position=device.position + shift,
|
||||
face=device.face,
|
||||
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||
width_fraction=fraction,
|
||||
horizontal_position=horizontal,
|
||||
serial=device.serial if include_device_names else '',
|
||||
@@ -338,7 +338,7 @@ def clone_concept(concept, name, status=ConceptStatusChoices.STATUS_DRAFT, tenan
|
||||
platform=device.platform,
|
||||
tenant=device.tenant,
|
||||
position=device.position,
|
||||
face=device.face,
|
||||
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||
width_fraction=device.width_fraction,
|
||||
horizontal_position=device.horizontal_position,
|
||||
u_height=device.u_height,
|
||||
|
||||
Reference in New Issue
Block a user