ui: place connection VLANs below cable B side

This commit is contained in:
2026-08-25 12:03:45 +02:00
parent 4421000409
commit d245ae0f79
7 changed files with 135 additions and 8 deletions
+4
View File
@@ -22,6 +22,10 @@ mehrwertige Reverse-Relationen. Das behebt unter NetBox 4.6.8 insbesondere den
Fehler `'RelatedManager' object has no attribute '_meta'` beim Öffnen der Fehler `'RelatedManager' object has no attribute '_meta'` beim Öffnen der
Seite zum Anlegen einer VLAN-Gruppe. Seite zum Anlegen einer VLAN-Gruppe.
Ab Version `0.10.2` befindet sich die VLAN-Auswahl beim Anlegen und Bearbeiten
eines Kabels direkt im Abschnitt **B-Seite**, unterhalb des B-seitigen
Verbindungsendes.
## Kompatibilität ## Kompatibilität
- NetBox `>=4.6.5,<4.7` - NetBox `>=4.6.5,<4.7`
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig, get_plugin_config from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.10.1" __version__ = "0.10.2"
class NetBoxUtilitiesConfig(PluginConfig): class NetBoxUtilitiesConfig(PluginConfig):
@@ -1,12 +1,120 @@
{% load static %}
{% load helpers %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% include 'dcim/htmx/cable_edit.html' %} {% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% if form.utilities_vlans %} {# A side termination #}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "A Side" %}</h2>
</div>
{% render_field form.a_terminations_type %}
{% if 'termination_a_device' in form.fields %}
{% render_field form.termination_a_device %}
{% endif %}
{% if 'termination_a_powerpanel' in form.fields %}
{% render_field form.termination_a_powerpanel %}
{% endif %}
{% if 'termination_a_circuit' in form.fields %}
{% render_field form.termination_a_circuit %}
{% endif %}
{% if 'a_terminations' in form.fields %}
{% render_field form.a_terminations %}
{% endif %}
</div>
{# B side termination and connection VLANs #}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "B Side" %}</h2>
</div>
{% render_field form.b_terminations_type %}
{% if 'termination_b_device' in form.fields %}
{% render_field form.termination_b_device %}
{% endif %}
{% if 'termination_b_powerpanel' in form.fields %}
{% render_field form.termination_b_powerpanel %}
{% endif %}
{% if 'termination_b_circuit' in form.fields %}
{% render_field form.termination_b_circuit %}
{% endif %}
{% if 'b_terminations' in form.fields %}
{% render_field form.b_terminations %}
{% endif %}
{% if form.utilities_vlans %}
{% render_field form.utilities_vlans %}
{% endif %}
</div>
{# Cable attributes #}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Cable" %}</h2>
</div>
{% render_field form.status %}
{% render_field form.profile %}
{% render_field form.type %}
{% render_field form.bundle %}
{% render_field form.label %}
{% render_field form.description %}
{% render_field form.color %}
<div class="row mb-3{% if form.length.errors or form.length_unit.errors %} has-errors{% endif %}">
<label class="col-sm-3 col-form-label text-lg-end">{{ form.length.label }}</label>
<div class="col-md-5">
{% render_field_with_aria form.length %}
{% if form.length.errors %}
<div class="form-text text-danger" id="{{ form.length.auto_id }}_errors" role="alert">
{% for error in form.length.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-4">
{% render_field_with_aria form.length_unit %}
{% if form.length_unit.errors %}
<div class="form-text text-danger" id="{{ form.length_unit.auto_id }}_errors" role="alert">
{% for error in form.length_unit.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
</div>
{% endif %}
</div>
</div>
{% render_field form.tags %}
</div>
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Tenancy" %}</h2>
</div>
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Ownership" %}</h2>
</div>
{% render_field form.owner_group %}
{% render_field form.owner %}
</div>
{% if form.custom_fields %}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row"> <div class="row">
<h2 class="col-9 offset-3">VLANs der Verbindung</h2> <h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div> </div>
{% render_field form.utilities_vlans %} {% render_custom_fields form %}
</div> </div>
{% endif %} {% endif %}
<div class="field-group mb-5">
<h2 class="text-center">{% trans "Comments" %}</h2>
{% render_field form.comments %}
</div>
{# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
{% render_field form.changelog_message %}
</div>
@@ -1,8 +1,10 @@
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django import forms from django import forms
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.template.loader import get_template
from django.test import SimpleTestCase from django.test import SimpleTestCase
from netbox_utilities.connection_vlans import ( from netbox_utilities.connection_vlans import (
@@ -14,6 +16,19 @@ from netbox_utilities.models import CableVLANAssignment, WirelessLinkVLANAssignm
class ConnectionVLANTest(SimpleTestCase): class ConnectionVLANTest(SimpleTestCase):
def test_cable_vlan_field_is_rendered_below_b_side(self):
self.assertIsNotNone(get_template("netbox_utilities/cable_edit_form.html"))
template_path = Path(__file__).parents[1] / "templates" / "netbox_utilities" / "cable_edit_form.html"
template = template_path.read_text(encoding="utf-8")
b_side = template.index('{% trans "B Side" %}')
vlan_field = template.index("{% render_field form.utilities_vlans %}")
cable_section = template.index('{% trans "Cable" %}')
self.assertLess(b_side, vlan_field)
self.assertLess(vlan_field, cable_section)
self.assertEqual(template.count("{% render_field form.utilities_vlans %}"), 1)
def test_assignment_models_are_tied_to_their_connection_and_vlans(self): def test_assignment_models_are_tied_to_their_connection_and_vlans(self):
self.assertEqual( self.assertEqual(
CableVLANAssignment._meta.get_field("cable").remote_field.model._meta.label_lower, "dcim.cable" CableVLANAssignment._meta.get_field("cable").remote_field.model._meta.label_lower, "dcim.cable"
+1 -1
View File
@@ -449,7 +449,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertEqual(template_name, "netbox_utilities/reorder_rack.html") self.assertEqual(template_name, "netbox_utilities/reorder_rack.html")
self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox") self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox")
self.assertEqual(context["reorder_devices"][0]["grid_width"], 6) self.assertEqual(context["reorder_devices"][0]["grid_width"], 6)
self.assertEqual(context["asset_version"], "0.10.1") self.assertEqual(context["asset_version"], "0.10.2")
self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value) self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value)
get_width_data.assert_called_once() get_width_data.assert_called_once()
self.assertIs(get_width_data.call_args.kwargs["rack"], rack) self.assertIs(get_width_data.call_args.kwargs["rack"], rack)
@@ -246,7 +246,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html) self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html)
self.assertIn('.rack-device[href="/dcim/devices/334/"]', html) self.assertIn('.rack-device[href="/dcim/devices/334/"]', html)
self.assertIn("left: calc(50% + 3px) !important", html) self.assertIn("left: calc(50% + 3px) !important", html)
self.assertIn("netbox_utilities/topology-rack-width.js?v=0.10.1", html) self.assertIn("netbox_utilities/topology-rack-width.js?v=0.10.2", html)
self.assertIn("left:calc(0% + 3px)!important", html) self.assertIn("left:calc(0% + 3px)!important", html)
self.assertIn("left:calc(50% + 3px)!important", html) self.assertIn("left:calc(50% + 3px)!important", html)
self.assertIn("width:calc(50% - 6px)!important", html) self.assertIn("width:calc(50% - 6px)!important", html)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "netbox-utilities" name = "netbox-utilities"
version = "0.10.1" version = "0.10.2"
description = "Navigation, tenant utilities, connection VLANs, partial-width racks, and bulk operations for NetBox 4.6" description = "Navigation, tenant utilities, connection VLANs, partial-width racks, and bulk operations for NetBox 4.6"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"