Remove rear_ports M2M fields from FrontPort & FrontPortTemplate

This commit is contained in:
Jeremy Stretch
2025-11-21 13:15:57 -05:00
parent bfff2d7658
commit 66bbfa7a88
9 changed files with 156 additions and 67 deletions

View File

@@ -5,7 +5,8 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
InventoryItemTemplate, ModuleBayTemplate, PortAssignmentTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
)
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.gfk_fields import GFKSerializerField
@@ -205,6 +206,16 @@ class InterfaceTemplateSerializer(ComponentTemplateSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class RearPortTemplateAssignmentSerializer(serializers.ModelSerializer):
front_port = serializers.PrimaryKeyRelatedField(
queryset=FrontPortTemplate.objects.all(),
)
class Meta:
model = PortAssignmentTemplate
fields = ('id', 'rear_port_position', 'front_port', 'front_port_position')
class RearPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
required=False,
@@ -219,15 +230,52 @@ class RearPortTemplateSerializer(ComponentTemplateSerializer):
default=None
)
type = ChoiceField(choices=PortTypeChoices)
front_ports = RearPortTemplateAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta:
model = RearPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
'positions', 'description', 'created', 'last_updated',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
'front_ports', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
def create(self, validated_data):
assignments = validated_data.pop('assignments', [])
instance = super().create(validated_data)
# Create FrontPort assignments
for assignment_data in assignments:
PortAssignmentTemplate.objects.create(rear_port=instance, **assignment_data)
return instance
def update(self, instance, validated_data):
assignments = validated_data.pop('assignments', None)
instance = super().update(instance, validated_data)
if assignments is not None:
# Update FrontPort assignments
PortAssignmentTemplate.objects.filter(rear_port=instance).delete()
for assignment_data in assignments:
PortAssignmentTemplate.objects.create(rear_port=instance, **assignment_data)
return instance
class FrontPortTemplateAssignmentSerializer(serializers.ModelSerializer):
rear_port = serializers.PrimaryKeyRelatedField(
queryset=RearPortTemplate.objects.all(),
)
class Meta:
model = PortAssignmentTemplate
fields = ('id', 'front_port_position', 'rear_port', 'rear_port_position')
class FrontPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
@@ -243,7 +291,11 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
default=None
)
type = ChoiceField(choices=PortTypeChoices)
rear_ports = RearPortTemplateSerializer(nested=True, many=True)
rear_ports = FrontPortTemplateAssignmentSerializer(
source='assignments',
many=True,
required=False,
)
class Meta:
model = FrontPortTemplate
@@ -253,6 +305,28 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
def create(self, validated_data):
assignments = validated_data.pop('assignments', [])
instance = super().create(validated_data)
# Create RearPort assignments
for assignment_data in assignments:
PortAssignmentTemplate.objects.create(front_port=instance, **assignment_data)
return instance
def update(self, instance, validated_data):
assignments = validated_data.pop('assignments', None)
instance = super().update(instance, validated_data)
if assignments is not None:
# Update RearPort assignments
PortAssignmentTemplate.objects.filter(front_port=instance).delete()
for assignment_data in assignments:
PortAssignmentTemplate.objects.create(front_port=instance, **assignment_data)
return instance
class ModuleBayTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(

View File

@@ -884,10 +884,11 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
choices=PortTypeChoices,
null_value=None
)
rear_port_template_id = django_filters.ModelMultipleChoiceFilter(
field_name='rear_ports',
queryset=FrontPortTemplate.objects.all(),
label=_('Rear port template (ID)'),
rear_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='assignments__rear_port',
queryset=RearPort.objects.all(),
to_field_name='rear_port',
label=_('Rear port (ID)'),
)
class Meta:
@@ -900,10 +901,11 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
choices=PortTypeChoices,
null_value=None
)
front_port_template_id = django_filters.ModelMultipleChoiceFilter(
field_name='front_ports',
queryset=FrontPortTemplate.objects.all(),
label=_('Front port template (ID)'),
front_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='assignments__front_port',
queryset=FrontPort.objects.all(),
to_field_name='front_port',
label=_('Front port (ID)'),
)
class Meta:
@@ -2109,8 +2111,9 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
null_value=None
)
rear_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='rear_ports',
field_name='assignments__rear_port',
queryset=RearPort.objects.all(),
to_field_name='rear_port',
label=_('Rear port (ID)'),
)
@@ -2128,8 +2131,9 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
null_value=None
)
front_port_id = django_filters.ModelMultipleChoiceFilter(
field_name='front_ports',
field_name='assignments__front_port',
queryset=FrontPort.objects.all(),
to_field_name='front_port',
label=_('Front port (ID)'),
)

View File

@@ -133,8 +133,6 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
# positions
print(f"name: {self.cleaned_data['name']}")
print(f"rear_ports: {self.cleaned_data['rear_ports']}")
frontport_count = len(self.cleaned_data['name'])
rearport_count = len(self.cleaned_data['rear_ports'])
if frontport_count != rearport_count:

View File

@@ -385,7 +385,8 @@ class DeviceTypeType(PrimaryObjectType):
)
class FrontPortType(ModularComponentType, CabledObjectMixin):
color: str
# rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
assignments: List[Annotated["PortAssignmentType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -396,7 +397,8 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
)
class FrontPortTemplateType(ModularComponentTemplateType):
color: str
# rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
assignments: List[Annotated["PortAssignmentTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -636,6 +638,28 @@ class PlatformType(NestedGroupObjectType):
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
models.PortAssignment,
fields='__all__',
# filters=PortAssignmentFilter,
pagination=True
)
class PortAssignmentType(ModularComponentTemplateType):
front_port: Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]
rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
models.PortAssignmentTemplate,
fields='__all__',
# filters=PortAssignmentTemplateFilter,
pagination=True
)
class PortAssignmentTemplateType(ModularComponentTemplateType):
front_port_template: Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]
rear_port_template: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(
models.PowerFeed,
exclude=['_path'],
@@ -768,7 +792,7 @@ class RackRoleType(OrganizationalObjectType):
class RearPortType(ModularComponentType, CabledObjectMixin):
color: str
front_ports: List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
assignments: List[Annotated["PortAssignmentType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -780,7 +804,7 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
class RearPortTemplateType(ModularComponentTemplateType):
color: str
front_ports: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
assignments: List[Annotated["PortAssignmentTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(

View File

@@ -118,17 +118,6 @@ class Migration(migrations.Migration):
),
),
# Add rear_ports ManyToManyField on FrontPortTemplate
migrations.AddField(
model_name='frontporttemplate',
name='rear_ports',
field=models.ManyToManyField(
related_name='front_ports',
through='dcim.PortAssignmentTemplate',
to='dcim.rearporttemplate'
),
),
# Create PortAssignment model (for Devices)
migrations.CreateModel(
name='PortAssignment',
@@ -187,17 +176,6 @@ class Migration(migrations.Migration):
),
),
# Add rear_ports ManyToManyField on FrontPort
migrations.AddField(
model_name='frontport',
name='rear_ports',
field=models.ManyToManyField(
related_name='front_ports',
through='dcim.PortAssignment',
to='dcim.rearport'
),
),
# Data migration
migrations.RunPython(
code=populate_port_template_assignments,

View File

@@ -568,11 +568,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
MaxValueValidator(PORT_POSITION_MAX)
],
)
rear_ports = models.ManyToManyField(
to='dcim.RearPortTemplate',
through='dcim.PortAssignmentTemplate',
related_name='front_ports',
)
component_model = FrontPort

View File

@@ -1119,13 +1119,8 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
MaxValueValidator(PORT_POSITION_MAX)
],
)
rear_ports = models.ManyToManyField(
to='dcim.RearPort',
through='dcim.PortAssignment',
related_name='front_ports',
)
clone_fields = ('device', 'type', 'color')
clone_fields = ('device', 'type', 'color', 'positions')
class Meta(ModularComponentModel.Meta):
constraints = (
@@ -1159,6 +1154,7 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
MaxValueValidator(PORT_POSITION_MAX)
],
)
clone_fields = ('device', 'type', 'color', 'positions')
class Meta(ModularComponentModel.Meta):
@@ -1170,13 +1166,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
# Check that positions count is greater than or equal to the number of associated FrontPorts
if not self._state.adding:
frontport_count = self.front_ports.count()
if self.positions < frontport_count:
assignment_count = self.assignments.count()
if self.positions < assignment_count:
raise ValidationError({
"positions": _(
"The number of positions cannot be less than the number of mapped front ports "
"({frontport_count})"
).format(frontport_count=frontport_count)
"({count})"
).format(count=assignment_count)
})

View File

@@ -156,8 +156,8 @@ def extend_rearport_cable_paths(instance, created, raw, **kwargs):
When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort.
"""
if created and not raw:
for rear_port in instance.rear_ports.all():
for cablepath in CablePath.objects.filter(_nodes__contains=rear_port):
for assignment in instance.assignments.prefetch_related('rear_port'):
for cablepath in CablePath.objects.filter(_nodes__contains=assignment.rear_port):
cablepath.retrace()

View File

@@ -999,29 +999,49 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
'device_type': devicetype.pk,
'name': 'Front Port Template 3',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[2].pk,
'rear_port_position': 1,
'rear_ports': [
{
'front_port_position': 1,
'rear_port': rear_port_templates[2].pk,
'rear_port_position': 1,
},
],
},
{
'device_type': devicetype.pk,
'name': 'Front Port Template 4',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[3].pk,
'rear_port_position': 1,
'rear_ports': [
{
'front_port_position': 1,
'rear_port': rear_port_templates[3].pk,
'rear_port_position': 1,
},
],
},
{
'module_type': moduletype.pk,
'name': 'Front Port Template 7',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[6].pk,
'rear_port_position': 1,
'rear_ports': [
{
'front_port_position': 1,
'rear_port': rear_port_templates[6].pk,
'rear_port_position': 1,
},
],
},
{
'module_type': moduletype.pk,
'name': 'Front Port Template 8',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port': rear_port_templates[7].pk,
'rear_port_position': 1,
'rear_ports': [
{
'front_port_position': 1,
'rear_port': rear_port_templates[7].pk,
'rear_port_position': 1,
},
],
},
]