Browse Source

Merge branch 'develop' into feature

Jeremy Stretch 1 year ago
parent
commit
51bd98bdfc

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -26,7 +26,7 @@ body:
     attributes:
     attributes:
       label: NetBox Version
       label: NetBox Version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v3.7.7
+      placeholder: v3.7.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
     attributes:
       label: NetBox version
       label: NetBox version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v3.7.7
+      placeholder: v3.7.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 17 - 0
docs/release-notes/version-3.7.md

@@ -1,5 +1,22 @@
 # NetBox v3.7
 # NetBox v3.7
 
 
+## v3.7.8 (2024-05-06)
+
+### Enhancements
+
+* [#12127](https://github.com/netbox-community/netbox/issues/12127) - Enable adding new cables directly from navigation menu
+
+### Bug Fixes
+
+* [#15877](https://github.com/netbox-community/netbox/issues/15877) - Account for virtual chassis membership when assigning related interfaces via bulk edit
+* [#15917](https://github.com/netbox-community/netbox/issues/15917) - Fix pagination through search results within dropdown fields
+* [#15925](https://github.com/netbox-community/netbox/issues/15925) - Fix SVG rendering of cable traces to circuit terminations
+* [#15948](https://github.com/netbox-community/netbox/issues/15948) - Fix cable trace SVG generation for cables with multiple terminations at both ends
+* [#15960](https://github.com/netbox-community/netbox/issues/15960) - Replace CSV export formatting for several many-to-many fields
+* [#15961](https://github.com/netbox-community/netbox/issues/15961) - Fix secret toggle button for IKE policies
+
+---
+
 ## v3.7.7 (2024-05-01)
 ## v3.7.7 (2024-05-01)
 
 
 ### Enhancements
 ### Enhancements

+ 3 - 3
netbox/dcim/forms/bulk_edit.py

@@ -1420,9 +1420,9 @@ class InterfaceBulkEditForm(
             device = Device.objects.filter(pk=self.initial['device']).first()
             device = Device.objects.filter(pk=self.initial['device']).first()
 
 
             # Restrict parent/bridge/LAG interface assignment by device
             # Restrict parent/bridge/LAG interface assignment by device
-            self.fields['parent'].widget.add_query_param('device_id', device.pk)
-            self.fields['bridge'].widget.add_query_param('device_id', device.pk)
-            self.fields['lag'].widget.add_query_param('device_id', device.pk)
+            self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
+            self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
+            self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
 
 
             # Limit VLAN choices by device
             # Limit VLAN choices by device
             self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
             self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)

+ 16 - 5
netbox/dcim/svg/cables.py

@@ -17,7 +17,7 @@ PADDING = 10
 LINE_HEIGHT = 20
 LINE_HEIGHT = 20
 FANOUT_HEIGHT = 35
 FANOUT_HEIGHT = 35
 FANOUT_LEG_HEIGHT = 15
 FANOUT_LEG_HEIGHT = 15
-CABLE_HEIGHT = 4 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
+CABLE_HEIGHT = 5 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
 
 
 
 
 class Node(Hyperlink):
 class Node(Hyperlink):
@@ -223,7 +223,7 @@ class CableTraceSVG:
         nodes_height = 0
         nodes_height = 0
         nodes = []
         nodes = []
         # Sort them by name to make renders more readable
         # Sort them by name to make renders more readable
-        for i, term in enumerate(sorted(terminations, key=lambda x: x.name)):
+        for i, term in enumerate(sorted(terminations, key=lambda x: str(x))):
             node = Node(
             node = Node(
                 position=(offset_x + i * width, self.cursor),
                 position=(offset_x + i * width, self.cursor),
                 width=width,
                 width=width,
@@ -266,7 +266,7 @@ class CableTraceSVG:
         Draw the far-end objects and its terminations and return all created nodes
         Draw the far-end objects and its terminations and return all created nodes
         """
         """
         # Make sure elements are sorted by name for readability
         # Make sure elements are sorted by name for readability
-        objects = sorted(obj_list, key=lambda x: x.name)
+        objects = sorted(obj_list, key=lambda x: str(x))
         width = self.width / len(objects)
         width = self.width / len(objects)
 
 
         # Max-height of created terminations
         # Max-height of created terminations
@@ -361,7 +361,8 @@ class CableTraceSVG:
             # Connector (a Cable or WirelessLink)
             # Connector (a Cable or WirelessLink)
             if links:
             if links:
 
 
-                parent_object_nodes, far_terminations = self.draw_far_objects(set(end.parent_object for end in far_ends), far_ends)
+                obj_list = {end.parent_object for end in far_ends}
+                parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
                 for cable in links:
                 for cable in links:
                     # Fill in labels and description with all available data
                     # Fill in labels and description with all available data
                     description = [
                     description = [
@@ -404,7 +405,17 @@ class CableTraceSVG:
                     end = far[0].top_center
                     end = far[0].top_center
                     text_offset = 0
                     text_offset = 0
 
 
-                    if len(near) > 1:
+                    if len(near) > 1 and len(far) > 1:
+                        start_center = sum([pos.bottom_center[0] for pos in near]) / len(near)
+                        end_center = sum([pos.bottom_center[0] for pos in far]) / len(far)
+                        center_x = (start_center + end_center) / 2
+
+                        start = (center_x, start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
+                        end = (center_x, end[1] - FANOUT_HEIGHT - FANOUT_LEG_HEIGHT)
+                        text_offset -= (FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
+                        self.draw_fanin(start, near, color)
+                        self.draw_fanout(end, far, color)
+                    elif len(near) > 1:
                         # Handle Fan-In - change start position to be directly below start
                         # Handle Fan-In - change start position to be directly below start
                         start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
                         start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
                         self.draw_fanin(start, near, color)
                         self.draw_fanin(start, near, color)

+ 1 - 1
netbox/dcim/tables/devices.py

@@ -618,7 +618,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
         verbose_name=_('VRF'),
         verbose_name=_('VRF'),
         linkify=True
         linkify=True
     )
     )
-    inventory_items = tables.ManyToManyColumn(
+    inventory_items = columns.ManyToManyColumn(
         linkify_item=True,
         linkify_item=True,
         verbose_name=_('Inventory Items'),
         verbose_name=_('Inventory Items'),
     )
     )

+ 63 - 0
netbox/dcim/tests/test_cablepaths.py

@@ -394,6 +394,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
         path1 = self.assertPathExists(
         path1 = self.assertPathExists(
@@ -450,6 +453,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
         path1 = self.assertPathExists(
         path1 = self.assertPathExists(
@@ -558,6 +564,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 3
         # Delete cable 3
         cable3.delete()
         cable3.delete()
 
 
@@ -673,6 +682,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 3
         # Delete cable 3
         cable3.delete()
         cable3.delete()
 
 
@@ -804,6 +816,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 3
         # Delete cable 3
         cable3.delete()
         cable3.delete()
 
 
@@ -931,6 +946,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 5
         # Delete cable 5
         cable5.delete()
         cable5.delete()
 
 
@@ -1034,6 +1052,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 3
         # Delete cable 3
         cable3.delete()
         cable3.delete()
 
 
@@ -1093,6 +1114,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 3)
         self.assertEqual(CablePath.objects.count(), 3)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 1
         # Delete cable 1
         cable1.delete()
         cable1.delete()
 
 
@@ -1135,6 +1159,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 1)
         self.assertEqual(CablePath.objects.count(), 1)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
     def test_210_interface_to_circuittermination(self):
     def test_210_interface_to_circuittermination(self):
         """
         """
         [IF1] --C1-- [CT1]
         [IF1] --C1-- [CT1]
@@ -1156,6 +1183,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 1)
         self.assertEqual(CablePath.objects.count(), 1)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 1
         # Delete cable 1
         cable1.delete()
         cable1.delete()
         self.assertEqual(CablePath.objects.count(), 0)
         self.assertEqual(CablePath.objects.count(), 0)
@@ -1212,6 +1242,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
         path1 = self.assertPathExists(
         path1 = self.assertPathExists(
@@ -1277,6 +1310,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
         path1 = self.assertPathExists(
         path1 = self.assertPathExists(
@@ -1314,6 +1350,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 1)
         self.assertEqual(CablePath.objects.count(), 1)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 1
         # Delete cable 1
         cable1.delete()
         cable1.delete()
         self.assertEqual(CablePath.objects.count(), 0)
         self.assertEqual(CablePath.objects.count(), 0)
@@ -1342,6 +1381,9 @@ class CablePathTestCase(TestCase):
         self.assertEqual(CablePath.objects.count(), 1)
         self.assertEqual(CablePath.objects.count(), 1)
         self.assertTrue(CablePath.objects.first().is_complete)
         self.assertTrue(CablePath.objects.first().is_complete)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 1
         # Delete cable 1
         cable1.delete()
         cable1.delete()
         self.assertEqual(CablePath.objects.count(), 0)
         self.assertEqual(CablePath.objects.count(), 0)
@@ -1439,6 +1481,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cables 3-4
         # Delete cables 3-4
         cable3.delete()
         cable3.delete()
         cable4.delete()
         cable4.delete()
@@ -1495,6 +1540,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
         path1 = self.assertPathExists(
         path1 = self.assertPathExists(
@@ -1578,6 +1626,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 2
         # Delete cable 2
         cable2.delete()
         cable2.delete()
 
 
@@ -1697,6 +1748,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 4)
         self.assertEqual(CablePath.objects.count(), 4)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
         # Delete cable 3
         # Delete cable 3
         cable3.delete()
         cable3.delete()
 
 
@@ -1784,6 +1838,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 2)
         self.assertEqual(CablePath.objects.count(), 2)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
     def test_220_interface_to_interface_duplex_via_multiple_front_and_rear_ports(self):
     def test_220_interface_to_interface_duplex_via_multiple_front_and_rear_ports(self):
         """
         """
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
@@ -1877,6 +1934,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 3)
         self.assertEqual(CablePath.objects.count(), 3)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
     def test_221_non_symmetric_paths(self):
     def test_221_non_symmetric_paths(self):
         """
         """
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- -------------------------------------- [IF2]
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- -------------------------------------- [IF2]
@@ -1997,6 +2057,9 @@ class CablePathTestCase(TestCase):
         )
         )
         self.assertEqual(CablePath.objects.count(), 3)
         self.assertEqual(CablePath.objects.count(), 3)
 
 
+        # Test SVG generation
+        CableTraceSVG(interface1).render()
+
     def test_301_create_path_via_existing_cable(self):
     def test_301_create_path_via_existing_cable(self):
         """
         """
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
         [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]

+ 0 - 6
netbox/dcim/views.py

@@ -3160,12 +3160,6 @@ class CableListView(generic.ObjectListView):
     filterset = filtersets.CableFilterSet
     filterset = filtersets.CableFilterSet
     filterset_form = forms.CableFilterForm
     filterset_form = forms.CableFilterForm
     table = tables.CableTable
     table = tables.CableTable
-    actions = {
-        'import': {'add'},
-        'export': {'view'},
-        'bulk_edit': {'change'},
-        'bulk_delete': {'delete'},
-    }
 
 
 
 
 @register_model_view(Cable)
 @register_model_view(Cable)

+ 1 - 1
netbox/ipam/tables/ip.py

@@ -378,7 +378,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
         orderable=False,
         orderable=False,
         verbose_name=_('NAT (Inside)')
         verbose_name=_('NAT (Inside)')
     )
     )
-    nat_outside = tables.ManyToManyColumn(
+    nat_outside = columns.ManyToManyColumn(
         linkify_item=True,
         linkify_item=True,
         orderable=False,
         orderable=False,
         verbose_name=_('NAT (Outside)')
         verbose_name=_('NAT (Outside)')

+ 1 - 1
netbox/netbox/navigation/menu.py

@@ -101,7 +101,7 @@ CONNECTIONS_MENU = Menu(
         MenuGroup(
         MenuGroup(
             label=_('Connections'),
             label=_('Connections'),
             items=(
             items=(
-                get_model_item('dcim', 'cable', _('Cables'), actions=['import']),
+                get_model_item('dcim', 'cable', _('Cables')),
                 get_model_item('wireless', 'wirelesslink', _('Wireless Links')),
                 get_model_item('wireless', 'wirelesslink', _('Wireless Links')),
                 MenuItem(
                 MenuItem(
                     link='dcim:interface_connections_list',
                     link='dcim:interface_connections_list',

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 6 - 7
netbox/project-static/src/buttons/secretToggle.ts

@@ -60,18 +60,17 @@ function handleSecretToggle(state: StateManager<SecretState>, button: HTMLButton
   toggleSecretButton(hidden, button);
   toggleSecretButton(hidden, button);
 }
 }
 
 
+function toggleCallback(event: MouseEvent) {
+  handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
+}
+
 /**
 /**
  * Initialize secret toggle button.
  * Initialize secret toggle button.
  */
  */
 export function initSecretToggle(): void {
 export function initSecretToggle(): void {
   hideSecret();
   hideSecret();
   for (const button of getElements<HTMLButtonElement>('button.toggle-secret')) {
   for (const button of getElements<HTMLButtonElement>('button.toggle-secret')) {
-    button.addEventListener(
-      'click',
-      event => {
-        handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
-      },
-      false,
-    );
+    button.removeEventListener('click', toggleCallback);
+    button.addEventListener('click', toggleCallback);
   }
   }
 }
 }

BIN
netbox/translations/ja/LC_MESSAGES/django.mo


File diff suppressed because it is too large
+ 180 - 135
netbox/translations/ja/LC_MESSAGES/django.po


+ 2 - 2
netbox/vpn/tables/crypto.py

@@ -63,7 +63,7 @@ class IKEPolicyTable(NetBoxTable):
     mode = tables.Column(
     mode = tables.Column(
         verbose_name=_('Mode')
         verbose_name=_('Mode')
     )
     )
-    proposals = tables.ManyToManyColumn(
+    proposals = columns.ManyToManyColumn(
         linkify_item=True,
         linkify_item=True,
         verbose_name=_('Proposals')
         verbose_name=_('Proposals')
     )
     )
@@ -129,7 +129,7 @@ class IPSecPolicyTable(NetBoxTable):
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
     )
     )
-    proposals = tables.ManyToManyColumn(
+    proposals = columns.ManyToManyColumn(
         linkify_item=True,
         linkify_item=True,
         verbose_name=_('Proposals')
         verbose_name=_('Proposals')
     )
     )

+ 1 - 1
netbox/vpn/tables/tunnels.py

@@ -91,7 +91,7 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Tunnel interface'),
         verbose_name=_('Tunnel interface'),
         linkify=True
         linkify=True
     )
     )
-    ip_addresses = tables.ManyToManyColumn(
+    ip_addresses = columns.ManyToManyColumn(
         accessor=tables.A('termination__ip_addresses'),
         accessor=tables.A('termination__ip_addresses'),
         orderable=False,
         orderable=False,
         linkify_item=True,
         linkify_item=True,

+ 3 - 3
requirements.txt

@@ -18,10 +18,10 @@ drf-spectacular==0.27.2
 drf-spectacular-sidecar==2024.5.1
 drf-spectacular-sidecar==2024.5.1
 feedparser==6.0.11
 feedparser==6.0.11
 gunicorn==22.0.0
 gunicorn==22.0.0
-Jinja2==3.1.3
+Jinja2==3.1.4
 Markdown==3.6
 Markdown==3.6
-mkdocs-material==9.5.20
-mkdocstrings[python-legacy]==0.25.0
+mkdocs-material==9.5.21
+mkdocstrings[python-legacy]==0.25.1
 netaddr==1.2.1
 netaddr==1.2.1
 nh3==0.2.17
 nh3==0.2.17
 Pillow==10.3.0
 Pillow==10.3.0

Some files were not shown because too many files changed in this diff