Update FrontPort model form
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run

This commit is contained in:
Jeremy Stretch
2025-11-18 10:41:47 -05:00
parent c09b0771b2
commit 6a7027aebb
7 changed files with 124 additions and 39 deletions

View File

@@ -333,14 +333,14 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
rear_ports = FrontPortRearPortSerializer(many=True)
class Meta:
model = FrontPort
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
'custom_fields', 'created', 'last_updated', '_occupied',
'rear_ports', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')

View File

@@ -1578,16 +1578,15 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
query_params={
'device_id': '$device',
}
rear_ports = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
widget=forms.SelectMultiple(attrs={'size': 6})
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
'description', 'tags',
),
)
@@ -1599,6 +1598,59 @@ class FrontPortForm(ModularDeviceComponentForm):
'tags',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if device_id := (self.data.get('device') or self.initial.get('device')):
device = Device.objects.get(pk=device_id)
else:
return
# Populate rear port choices
choices = []
for rear_port in RearPort.objects.filter(device=device):
for i in range(1, rear_port.positions + 1):
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
self.fields['rear_ports'].choices = choices
# Set initial rear port assignments
if self.instance.pk:
self.initial['rear_ports'] = [
f'{assignment.rear_port_id}:{assignment.rear_port_position}'
for assignment in PortAssignment.objects.filter(front_port_id=self.instance.pk)
]
def clean(self):
# Count of selected rear port & position pairs much match the assigned number of positions
if len(self.cleaned_data['rear_ports']) != self.cleaned_data['positions']:
raise forms.ValidationError(
_("The number of rear port/position pairs selected must match the number of positions assigned.")
)
def _save_m2m(self):
super()._save_m2m()
# TODO: Can this be made more efficient?
# Delete existing rear port assignments
PortAssignment.objects.filter(front_port_id=self.instance.pk).delete()
# Create new rear port assignments
assignments = []
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
rear_port_id, rear_port_position = rp_position.split(':')
assignments.append(
PortAssignment(
front_port_id=self.instance.pk,
front_port_position=i,
rear_port_id=rear_port_id,
rear_port_position=rear_port_position,
)
)
PortAssignment.objects.bulk_create(assignments)
class RearPortForm(ModularDeviceComponentForm):
fieldsets = (

View File

@@ -9,6 +9,10 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveConstraint(
model_name='frontport',
name='dcim_frontport_unique_rear_port_position',
),
migrations.RemoveField(
model_name='frontport',
name='rear_port',

View File

@@ -35,6 +35,7 @@ __all__ = (
'InventoryItemRole',
'ModuleBay',
'PathEndpoint',
'PortAssignment',
'PowerOutlet',
'PowerPort',
'RearPort',
@@ -1106,6 +1107,29 @@ class PortAssignment(models.Model):
),
)
def clean(self):
# Validate rear port assignment
if self.front_port.device_id != self.rear_port.device_id:
raise ValidationError({
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(
rear_port=self.rear_port
)
})
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": _(
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
})
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
"""
@@ -1142,40 +1166,10 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
fields=('device', 'name'),
name='%(app_label)s_%(class)s_unique_device_name'
),
models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='%(app_label)s_%(class)s_unique_rear_port_position'
),
)
verbose_name = _('front port')
verbose_name_plural = _('front ports')
def clean(self):
super().clean()
if hasattr(self, 'rear_port'):
# Validate rear port assignment
if self.rear_port.device != self.device:
raise ValidationError({
"rear_port": _(
"Rear port ({rear_port}) must belong to the same device"
).format(rear_port=self.rear_port)
})
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": _(
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
})
class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
"""

View File

@@ -42,6 +42,7 @@ from wireless.models import WirelessLAN
from . import filtersets, forms, tables
from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import *
from .models.device_components import PortAssignment
from .object_actions import BulkAddComponents, BulkDisconnect
CABLE_TERMINATION_TYPES = {
@@ -3242,6 +3243,11 @@ class FrontPortListView(generic.ObjectListView):
class FrontPortView(generic.ObjectView):
queryset = FrontPort.objects.all()
def get_extra_context(self, request, instance):
return {
'rear_port_assignments': PortAssignment.objects.filter(front_port=instance).prefetch_related('rear_port'),
}
@register_model_view(FrontPort, 'add', detail=False)
class FrontPortCreateView(generic.ComponentCreateView):
@@ -3313,6 +3319,11 @@ class RearPortListView(generic.ObjectListView):
class RearPortView(generic.ObjectView):
queryset = RearPort.objects.all()
def get_extra_context(self, request, instance):
return {
'front_port_assignments': PortAssignment.objects.filter(rear_port=instance).prefetch_related('front_port'),
}
@register_model_view(RearPort, 'add', detail=False)
class RearPortCreateView(generic.ComponentCreateView):

View File

@@ -61,6 +61,18 @@
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Ports" %}</h2>
<table class="table table-hover">
{% for assignment in rear_port_assignments %}
<tr>
<td>{{ assignment.front_port_position }}</td>
<td>{{ assignment.rear_port|linkify }}</td>
<td>{{ assignment.rear_port_position }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}

View File

@@ -61,6 +61,18 @@
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Ports" %}</h2>
<table class="table table-hover">
{% for assignment in front_port_assignments %}
<tr>
<td>{{ assignment.rear_port_position }}</td>
<td>{{ assignment.front_port|linkify }}</td>
<td>{{ assignment.front_port_position }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}