Fix rear-face image-only labels, click-to-add units, cable concept devices
- Fix "images only" leaving plain labels visible on faces where a device has no image for that side (e.g. no rear_image set): the view selector now hides every plain label outright in that mode, not just the ones an image happens to sit on top of. - Remove the "Add planned device" button from the elevation card. Empty units are now click targets themselves - clicking one opens the add-device form pre-filled with that position and face, exactly like a real rack's elevation. Occupancy is computed per unit per face from the existing device blocks. - Add cabling for concept devices: ConceptComponent (interfaces, front/rear ports, power ports/outlets, console ports) and ConceptCable (a connection between two components, restricted to devices in the same concept). components.sync_components() populates a device's components from its device type's templates, including the front/rear port pass-through mapping copied from DeviceType.port_mappings - so a device with front/rear port templates (a patch panel being the common case) behaves exactly like a real one, without hardcoding anything role-specific. Placeholder devices without a device type get components added by hand. Full CRUD views, tables, filtersets, API endpoints and a "Connect" action wired in from the device detail page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Populate a planned device's components from its device type's templates - interfaces,
|
||||
ports, and the front/rear pass-through mapping - so a concept device with a device type
|
||||
can be cabled exactly like a real one. A device type's own component templates are the
|
||||
single source of truth here; nothing about this is specific to any particular role, so
|
||||
a "Patchpanel"-role device gets correct front/rear pass-through behaviour simply because
|
||||
that's what its device type's templates already describe - the same as a real device.
|
||||
|
||||
Placeholder devices without a device type have no templates to sync from; their
|
||||
components, if any, have to be added by hand.
|
||||
"""
|
||||
from django.db import transaction
|
||||
|
||||
from .choices import ConceptComponentTypeChoices
|
||||
from .models import ConceptComponent, ConceptPortMapping
|
||||
|
||||
__all__ = ('sync_components',)
|
||||
|
||||
# (our component type, DeviceType's reverse accessor for that template queryset)
|
||||
TEMPLATE_SOURCES = (
|
||||
(ConceptComponentTypeChoices.TYPE_INTERFACE, 'interfacetemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_CONSOLE_PORT, 'consoleporttemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_CONSOLE_SERVER_PORT, 'consoleserverporttemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_POWER_PORT, 'powerporttemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_POWER_OUTLET, 'poweroutlettemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_FRONT_PORT, 'frontporttemplates'),
|
||||
(ConceptComponentTypeChoices.TYPE_REAR_PORT, 'rearporttemplates'),
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def sync_components(concept_device):
|
||||
"""
|
||||
Create whichever of the device type's components don't already exist on this
|
||||
device (matched by name), and mirror its front/rear port mapping. Existing
|
||||
components - and any cables already attached to them - are left untouched, so this
|
||||
is safe to re-run after adding extra components by hand or after the device type
|
||||
changes. Returns (components_created, mappings_created).
|
||||
"""
|
||||
device_type = concept_device.device_type
|
||||
if device_type is None:
|
||||
return 0, 0
|
||||
|
||||
existing = {component.name: component for component in concept_device.components.all()}
|
||||
created = 0
|
||||
|
||||
for component_type, accessor in TEMPLATE_SOURCES:
|
||||
for template in getattr(device_type, accessor).all():
|
||||
if template.name in existing:
|
||||
continue
|
||||
component = ConceptComponent.objects.create(
|
||||
device=concept_device,
|
||||
component_type=component_type,
|
||||
name=template.name,
|
||||
label=template.label,
|
||||
port_type=getattr(template, 'type', '') or '',
|
||||
positions=getattr(template, 'positions', 1) or 1,
|
||||
color=getattr(template, 'color', '') or '',
|
||||
)
|
||||
existing[component.name] = component
|
||||
created += 1
|
||||
|
||||
mappings_created = 0
|
||||
for mapping in device_type.port_mappings.select_related('front_port', 'rear_port').all():
|
||||
front = existing.get(mapping.front_port.name)
|
||||
rear = existing.get(mapping.rear_port.name)
|
||||
if not (front and rear):
|
||||
# Shouldn't happen - the templates were just synced above - but a
|
||||
# partially-synced device from before a device type change could hit this.
|
||||
continue
|
||||
_, was_created = ConceptPortMapping.objects.get_or_create(
|
||||
front_component=front,
|
||||
front_position=mapping.front_port_position,
|
||||
defaults={'rear_component': rear, 'rear_position': mapping.rear_port_position}
|
||||
)
|
||||
if was_created:
|
||||
mappings_created += 1
|
||||
|
||||
return created, mappings_created
|
||||
Reference in New Issue
Block a user