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.db import transaction
|
||||||
from django.utils.translation import gettext as _
|
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 dcim.models import Device, Rack
|
||||||
|
|
||||||
from . import components
|
from . import components
|
||||||
@@ -113,7 +113,7 @@ def copy_rack_to_concept(rack, name, status=ConceptStatusChoices.STATUS_DRAFT, t
|
|||||||
platform=device.platform,
|
platform=device.platform,
|
||||||
tenant=device.tenant,
|
tenant=device.tenant,
|
||||||
position=device.position,
|
position=device.position,
|
||||||
face=device.face,
|
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||||
width_fraction=fraction,
|
width_fraction=fraction,
|
||||||
horizontal_position=horizontal,
|
horizontal_position=horizontal,
|
||||||
serial=device.serial if include_device_names else '',
|
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,
|
platform=device.platform,
|
||||||
tenant=device.tenant,
|
tenant=device.tenant,
|
||||||
position=(device.position + shift) if device.position is not None else None,
|
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,
|
width_fraction=device.width_fraction,
|
||||||
horizontal_position=device.horizontal_position,
|
horizontal_position=device.horizontal_position,
|
||||||
u_height=device.u_height,
|
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,
|
u_height=total_height,
|
||||||
starting_unit=starting_unit,
|
starting_unit=starting_unit,
|
||||||
form_factor=_rack_geometry(rack_bottom, 'form_factor') or '',
|
form_factor=_rack_geometry(rack_bottom, 'form_factor') or '',
|
||||||
airflow=rack_bottom.airflow,
|
airflow=rack_bottom.airflow or '',
|
||||||
default_site=rack_bottom.site,
|
default_site=rack_bottom.site,
|
||||||
default_location=rack_bottom.location,
|
default_location=rack_bottom.location,
|
||||||
rack_role=rack_bottom.role,
|
rack_role=rack_bottom.role,
|
||||||
@@ -274,7 +274,7 @@ def merge_racks_to_concept(rack_bottom, rack_top, name, status=ConceptStatusChoi
|
|||||||
platform=device.platform,
|
platform=device.platform,
|
||||||
tenant=device.tenant,
|
tenant=device.tenant,
|
||||||
position=device.position + shift,
|
position=device.position + shift,
|
||||||
face=device.face,
|
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||||
width_fraction=fraction,
|
width_fraction=fraction,
|
||||||
horizontal_position=horizontal,
|
horizontal_position=horizontal,
|
||||||
serial=device.serial if include_device_names else '',
|
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,
|
platform=device.platform,
|
||||||
tenant=device.tenant,
|
tenant=device.tenant,
|
||||||
position=device.position,
|
position=device.position,
|
||||||
face=device.face,
|
face=device.face or DeviceFaceChoices.FACE_FRONT,
|
||||||
width_fraction=device.width_fraction,
|
width_fraction=device.width_fraction,
|
||||||
horizontal_position=device.horizontal_position,
|
horizontal_position=device.horizontal_position,
|
||||||
u_height=device.u_height,
|
u_height=device.u_height,
|
||||||
|
|||||||
Reference in New Issue
Block a user