Parcourir la source

Merge branch 'develop' into feature

jeremystretch il y a 4 ans
Parent
commit
ca59cd1eb8
43 fichiers modifiés avec 343 ajouts et 317 suppressions
  1. 1 1
      .github/ISSUE_TEMPLATE/bug_report.yaml
  2. 1 1
      .github/ISSUE_TEMPLATE/feature_request.yaml
  3. 9 2
      docs/administration/housekeeping.md
  4. 2 2
      docs/installation/3-netbox.md
  5. 2 2
      docs/installation/upgrading.md
  6. 20 1
      docs/release-notes/version-3.0.md
  7. 5 5
      netbox/circuits/api/serializers.py
  8. 14 8
      netbox/circuits/tests/test_api.py
  9. 56 70
      netbox/dcim/filtersets.py
  10. 11 7
      netbox/dcim/svg.py
  11. 27 9
      netbox/dcim/tests/test_filtersets.py
  12. 1 0
      netbox/dcim/views.py
  13. 8 0
      netbox/extras/api/serializers.py
  14. 1 1
      netbox/netbox/settings.py
  15. 0 0
      netbox/project-static/dist/netbox-dark.css
  16. 0 0
      netbox/project-static/dist/netbox-light.css
  17. 0 0
      netbox/project-static/dist/netbox-print.css
  18. 17 17
      netbox/project-static/styles/netbox.scss
  19. 1 0
      netbox/project-static/styles/select.scss
  20. 1 0
      netbox/project-static/styles/theme-light.scss
  21. 2 2
      netbox/project-static/styles/variables.scss
  22. 2 3
      netbox/templates/circuits/circuittermination_edit.html
  23. 3 7
      netbox/templates/dcim/cable_connect.html
  24. 73 80
      netbox/templates/dcim/device_edit.html
  25. 0 1
      netbox/templates/dcim/inc/cable_form.html
  26. 3 5
      netbox/templates/dcim/interface_edit.html
  27. 6 0
      netbox/templates/dcim/platform.html
  28. 11 15
      netbox/templates/dcim/rack_edit.html
  29. 3 5
      netbox/templates/dcim/virtualchassis_add.html
  30. 2 4
      netbox/templates/dcim/virtualchassis_edit.html
  31. 6 0
      netbox/templates/generic/object.html
  32. 16 22
      netbox/templates/generic/object_edit.html
  33. 3 1
      netbox/templates/generic/object_list.html
  34. 2 2
      netbox/templates/home.html
  35. 2 2
      netbox/templates/inc/table_controls.html
  36. 3 5
      netbox/templates/ipam/ipaddress_bulk_add.html
  37. 5 9
      netbox/templates/ipam/ipaddress_edit.html
  38. 1 2
      netbox/templates/ipam/service_edit.html
  39. 4 7
      netbox/templates/ipam/vlan_edit.html
  40. 1 1
      netbox/templates/tenancy/tenantgroup.html
  41. 3 5
      netbox/templates/virtualization/vminterface_edit.html
  42. 11 9
      netbox/utilities/tables.py
  43. 4 4
      requirements.txt

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

@@ -17,7 +17,7 @@ body:
         What version of NetBox are you currently running? (If you don't have access to the most
         recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
         before opening a bug report to see if your issue has already been addressed.)
-      placeholder: v3.0.5
+      placeholder: v3.0.6
     validations:
       required: true
   - type: dropdown

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

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

+ 9 - 2
docs/administration/housekeeping.md

@@ -5,6 +5,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
 * Clearing expired authentication sessions from the database
 * Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
 
-This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be copied into your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
+This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
 
-The `housekeeping` command can also be run manually at any time: Running the command outside of scheduled execution times will not interfere with its operation.
+```shell
+ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
+```
+
+!!! note
+    On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
+
+The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.

+ 2 - 2
docs/installation/3-netbox.md

@@ -259,10 +259,10 @@ python3 manage.py createsuperuser
 
 NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
 
-A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
+A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
 
 ```shell
-cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
+ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
 ```
 
 See the [housekeeping documentation](../administration/housekeeping.md) for further details.

+ 2 - 2
docs/installation/upgrading.md

@@ -111,10 +111,10 @@ sudo systemctl restart netbox netbox-rq
 
 ## Verify Housekeeping Scheduling
 
-If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
+If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
 
 ```shell
-cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
+ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
 ```
 
 See the [housekeeping documentation](../administration/housekeeping.md) for further details.

+ 20 - 1
docs/release-notes/version-3.0.md

@@ -1,5 +1,24 @@
 # NetBox v3.0
 
+## v3.0.6 (2021-10-06)
+
+### Enhancements
+
+* [#6850](https://github.com/netbox-community/netbox/issues/6850) - Default to current user when creating journal entries via REST API
+* [#6955](https://github.com/netbox-community/netbox/issues/6955) - Include type, ID, and slug on object view
+* [#7394](https://github.com/netbox-community/netbox/issues/7394) - Enable filtering cables by termination type & ID in REST API
+* [#7462](https://github.com/netbox-community/netbox/issues/7462) - Include count of assigned virtual machines under platform view
+
+### Bug Fixes
+
+* [#7442](https://github.com/netbox-community/netbox/issues/7442) - Fix missing actions column on user-configured tables
+* [#7446](https://github.com/netbox-community/netbox/issues/7446) - Fix exception when viewing a large number of child IPs within a prefix
+* [#7455](https://github.com/netbox-community/netbox/issues/7455) - Fix site/provider network validation for circuit termination API serializer
+* [#7459](https://github.com/netbox-community/netbox/issues/7459) - Pre-populate location data when adding a device to a rack
+* [#7460](https://github.com/netbox-community/netbox/issues/7460) - Fix filtering connections by site ID
+
+---
+
 ## v3.0.5 (2021-10-04)
 
 ### Enhancements
@@ -8,7 +27,6 @@
 * [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications
 * [#6708](https://github.com/netbox-community/netbox/issues/6708) - Add image attachment support for circuits, power panels
 * [#7387](https://github.com/netbox-community/netbox/issues/7387) - Enable arbitrary ordering of custom scripts
-* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
 
 ### Bug Fixes
 
@@ -23,6 +41,7 @@
 * [#7412](https://github.com/netbox-community/netbox/issues/7412) - Fix exception in UI when adding child device to device bay
 * [#7417](https://github.com/netbox-community/netbox/issues/7417) - Prevent exception when filtering objects list by invalid tag
 * [#7425](https://github.com/netbox-community/netbox/issues/7425) - Housekeeping command should honor zero verbosity
+* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
 
 ---
 

+ 5 - 5
netbox/circuits/api/serializers.py

@@ -3,10 +3,10 @@ from rest_framework import serializers
 from circuits.choices import CircuitStatusChoices
 from circuits.models import *
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
-from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
+from dcim.api.serializers import CableTerminationSerializer
 from netbox.api import ChoiceField
 from netbox.api.serializers import (
-    BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
+    OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
 )
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from .nested_serializers import *
@@ -90,11 +90,11 @@ class CircuitSerializer(PrimaryModelSerializer):
         ]
 
 
-class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
+class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     circuit = NestedCircuitSerializer()
-    site = NestedSiteSerializer(required=False)
-    provider_network = NestedProviderNetworkSerializer(required=False)
+    site = NestedSiteSerializer(required=False, allow_null=True)
+    provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
     cable = NestedCableSerializer(read_only=True)
 
     class Meta:

+ 14 - 8
netbox/circuits/tests/test_api.py

@@ -136,14 +136,20 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
         SIDE_A = CircuitTerminationSideChoices.SIDE_A
         SIDE_Z = CircuitTerminationSideChoices.SIDE_Z
 
+        provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+        circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+
         sites = (
             Site(name='Site 1', slug='site-1'),
             Site(name='Site 2', slug='site-2'),
         )
         Site.objects.bulk_create(sites)
 
-        provider = Provider.objects.create(name='Provider 1', slug='provider-1')
-        circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+        provider_networks = (
+            ProviderNetwork(provider=provider, name='Provider Network 1'),
+            ProviderNetwork(provider=provider, name='Provider Network 2'),
+        )
+        ProviderNetwork.objects.bulk_create(provider_networks)
 
         circuits = (
             Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
@@ -153,10 +159,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
         Circuit.objects.bulk_create(circuits)
 
         circuit_terminations = (
-            CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A),
-            CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z),
-            CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A),
-            CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z),
+            CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
+            CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
+            CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
+            CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
         )
         CircuitTermination.objects.bulk_create(circuit_terminations)
 
@@ -164,13 +170,13 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
             {
                 'circuit': circuits[2].pk,
                 'term_side': SIDE_A,
-                'site': sites[1].pk,
+                'site': sites[0].pk,
                 'port_speed': 200000,
             },
             {
                 'circuit': circuits[2].pk,
                 'term_side': SIDE_Z,
-                'site': sites[1].pk,
+                'provider_network': provider_networks[0].pk,
                 'port_speed': 200000,
             },
         ]

+ 56 - 70
netbox/dcim/filtersets.py

@@ -10,14 +10,14 @@ from tenancy.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from utilities.choices import ColorChoices
 from utilities.filters import (
-    MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
+    ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
+    TreeNodeMultipleChoiceFilter,
 )
 from virtualization.models import Cluster
 from .choices import *
 from .constants import *
 from .models import *
 
-
 __all__ = (
     'CableFilterSet',
     'CableTerminationFilterSet',
@@ -1184,6 +1184,10 @@ class CableFilterSet(PrimaryModelFilterSet):
         method='search',
         label='Search',
     )
+    termination_a_type = ContentTypeFilter()
+    termination_a_id = MultiValueNumberFilter()
+    termination_b_type = ContentTypeFilter()
+    termination_b_id = MultiValueNumberFilter()
     type = django_filters.MultipleChoiceFilter(
         choices=CableTypeChoices
     )
@@ -1228,7 +1232,7 @@ class CableFilterSet(PrimaryModelFilterSet):
 
     class Meta:
         model = Cable
-        fields = ['id', 'label', 'length', 'length_unit']
+        fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -1243,73 +1247,6 @@ class CableFilterSet(PrimaryModelFilterSet):
         return queryset
 
 
-class ConnectionFilterSet(BaseFilterSet):
-
-    def filter_site(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(device__site__slug=value)
-
-    def filter_device(self, queryset, name, value):
-        if not value:
-            return queryset
-        return queryset.filter(**{f'{name}__in': value})
-
-
-class ConsoleConnectionFilterSet(ConnectionFilterSet):
-    site = django_filters.CharFilter(
-        method='filter_site',
-        label='Site (slug)',
-    )
-    device_id = MultiValueNumberFilter(
-        method='filter_device'
-    )
-    device = MultiValueCharFilter(
-        method='filter_device',
-        field_name='device__name'
-    )
-
-    class Meta:
-        model = ConsolePort
-        fields = ['name']
-
-
-class PowerConnectionFilterSet(ConnectionFilterSet):
-    site = django_filters.CharFilter(
-        method='filter_site',
-        label='Site (slug)',
-    )
-    device_id = MultiValueNumberFilter(
-        method='filter_device'
-    )
-    device = MultiValueCharFilter(
-        method='filter_device',
-        field_name='device__name'
-    )
-
-    class Meta:
-        model = PowerPort
-        fields = ['name']
-
-
-class InterfaceConnectionFilterSet(ConnectionFilterSet):
-    site = django_filters.CharFilter(
-        method='filter_site',
-        label='Site (slug)',
-    )
-    device_id = MultiValueNumberFilter(
-        method='filter_device'
-    )
-    device = MultiValueCharFilter(
-        method='filter_device',
-        field_name='device__name'
-    )
-
-    class Meta:
-        model = Interface
-        fields = []
-
-
 class PowerPanelFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
@@ -1441,3 +1378,52 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
             Q(comments__icontains=value)
         )
         return queryset.filter(qs_filter)
+
+
+#
+# Connection filter sets
+#
+
+class ConnectionFilterSet(BaseFilterSet):
+    site_id = MultiValueNumberFilter(
+        method='filter_connections',
+        field_name='device__site_id'
+    )
+    site = MultiValueCharFilter(
+        method='filter_connections',
+        field_name='device__site__slug'
+    )
+    device_id = MultiValueNumberFilter(
+        method='filter_connections',
+        field_name='device_id'
+    )
+    device = MultiValueCharFilter(
+        method='filter_connections',
+        field_name='device__name'
+    )
+
+    def filter_connections(self, queryset, name, value):
+        if not value:
+            return queryset
+        return queryset.filter(**{f'{name}__in': value})
+
+
+class ConsoleConnectionFilterSet(ConnectionFilterSet):
+
+    class Meta:
+        model = ConsolePort
+        fields = ['name']
+
+
+class PowerConnectionFilterSet(ConnectionFilterSet):
+
+    class Meta:
+        model = PowerPort
+        fields = ['name']
+
+
+class InterfaceConnectionFilterSet(ConnectionFilterSet):
+
+    class Meta:
+        model = Interface
+        fields = []

+ 11 - 7
netbox/dcim/svg.py

@@ -132,14 +132,18 @@ class RackElevationSVG:
 
     @staticmethod
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
+        link_url = '{}?{}'.format(
+            reverse('dcim:device_add'),
+            urlencode({
+                'site': rack.site.pk,
+                'location': rack.location.pk if rack.location else '',
+                'rack': rack.pk,
+                'face': face_id,
+                'position': id_
+            })
+        )
         link = drawing.add(
-            drawing.a(
-                href='{}?{}'.format(
-                    reverse('dcim:device_add'),
-                    urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
-                ),
-                target='_top'
-            )
+            drawing.a(href=link_url, target='_top')
         )
         if reservation:
             link.set_desc('{} — {} · {}'.format(

+ 27 - 9
netbox/dcim/tests/test_filtersets.py

@@ -2851,6 +2851,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         Interface.objects.bulk_create(interfaces)
 
+        console_port = ConsolePort.objects.create(device=devices[0], name='Console Port 1')
+        console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
+
         # Cables
         Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
         Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
@@ -2858,6 +2861,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
         Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
         Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
         Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
+        Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
 
     def test_label(self):
         params = {'label': ['Cable 1', 'Cable 2']}
@@ -2877,7 +2881,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
 
     def test_status(self):
         params = {'status': [CableStatusChoices.STATUS_CONNECTED]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'status': [CableStatusChoices.STATUS_PLANNED]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
@@ -2888,30 +2892,44 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
     def test_device(self):
         devices = Device.objects.all()[:2]
         params = {'device_id': [devices[0].pk, devices[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'device': [devices[0].name, devices[1].name]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
     def test_rack(self):
         racks = Rack.objects.all()[:2]
         params = {'rack_id': [racks[0].pk, racks[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
         params = {'rack': [racks[0].name, racks[1].name]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
 
     def test_site(self):
         site = Site.objects.all()[:2]
         params = {'site_id': [site[0].pk, site[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
         params = {'site': [site[0].slug, site[1].slug]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
 
     def test_tenant(self):
         tenant = Tenant.objects.all()[:2]
         params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
         params = {'tenant': [tenant[0].slug, tenant[1].slug]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+
+    def test_termination_types(self):
+        params = {'termination_a_type': 'dcim.consoleport'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'termination_b_type': 'dcim.consoleserverport'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_termination_ids(self):
+        interface_ids = Cable.objects.values_list('termination_a_id', flat=True)[:3]
+        params = {
+            'termination_a_type': 'dcim.interface',
+            'termination_a_id': list(interface_ids),
+        }
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
 
 class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):

+ 1 - 0
netbox/dcim/views.py

@@ -1229,6 +1229,7 @@ class PlatformView(generic.ObjectView):
 
         return {
             'devices_table': devices_table,
+            'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count()
         }
 
 

+ 8 - 0
netbox/extras/api/serializers.py

@@ -1,3 +1,4 @@
+from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from drf_yasg.utils import swagger_serializer_method
@@ -30,6 +31,7 @@ __all__ = (
     'ExportTemplateSerializer',
     'ImageAttachmentSerializer',
     'JobResultSerializer',
+    'JournalEntrySerializer',
     'ObjectChangeSerializer',
     'ReportDetailSerializer',
     'ReportSerializer',
@@ -192,6 +194,12 @@ class JournalEntrySerializer(ValidatedModelSerializer):
         queryset=ContentType.objects.all()
     )
     assigned_object = serializers.SerializerMethodField(read_only=True)
+    created_by = serializers.PrimaryKeyRelatedField(
+        allow_null=True,
+        queryset=User.objects.all(),
+        required=False,
+        default=serializers.CurrentUserDefault()
+    )
     kind = ChoiceField(
         choices=JournalEntryKindChoices,
         required=False

+ 1 - 1
netbox/netbox/settings.py

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
 # Environment setup
 #
 
-VERSION = '3.0.6-dev'
+VERSION = '3.0.7-dev'
 
 # Hostname
 HOSTNAME = platform.node()

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
netbox/project-static/dist/netbox-print.css


+ 17 - 17
netbox/project-static/styles/netbox.scss

@@ -73,16 +73,6 @@
       color: color-contrast($value);
     }
   }
-
-  // Use proper foreground color in the alert body. Note: this is applied to p, & small because
-  // we *don't* want to override the h1-h6 colors for alerts, since those are set to a color
-  // similar to the alert color.
-  .alert.alert-#{$color} {
-    p,
-    small {
-      color: color-contrast($value);
-    }
-  }
 }
 
 // Ensure progress bars (utilization graph) in tables aren't too narrow to display the percentage.
@@ -200,16 +190,21 @@ div#advanced-search-content div.card div.card-body div.col:not(:last-child) {
 }
 
 table {
-  a {
-    text-decoration: none;
-    &:hover {
-      text-decoration: underline;
+  td {
+    a {
+      text-decoration: none;
+      &:hover {
+        text-decoration: underline;
+      }
     }
   }
-  &.table > :not(caption) > * > * {
-    padding-right: $table-cell-padding-x-sm !important;
-    padding-left: $table-cell-padding-x-sm !important;
+  th {
+    a, a:hover {
+      color: $body-color;
+      text-decoration: none;
+    }
   }
+
   td,
   th {
     font-size: $font-size-sm;
@@ -234,6 +229,11 @@ table {
     }
   }
 
+  &.table > :not(caption) > * > * {
+    padding-right: $table-cell-padding-x-sm !important;
+    padding-left: $table-cell-padding-x-sm !important;
+  }
+
   &.object-list {
     th {
       font-size: $font-size-xs;

+ 1 - 0
netbox/project-static/styles/select.scss

@@ -70,6 +70,7 @@ $spacing-s: $input-padding-x;
       span.arrow-down,
       span.arrow-up {
         border-color: currentColor;
+        color: $text-muted;
       }
     }
     // Don't show the depth indicator outside of the menu.

+ 1 - 0
netbox/project-static/styles/theme-light.scss

@@ -7,6 +7,7 @@ $input-border-color: $gray-200;
 $theme-colors: map-merge(
   $theme-colors,
   (
+    'primary': #337ab7,
     'red': $red-500,
     'yellow': $yellow-500,
     'green': $green-500,

+ 2 - 2
netbox/project-static/styles/variables.scss

@@ -23,7 +23,7 @@
   --nbx-color-mode-toggle-color: #{$primary};
   --nbx-sidenav-link-color: #{$gray-800};
   --nbx-sidenav-pin-color: #{$orange};
-  --nbx-sidenav-parent-color: #{$gray-900};
+  --nbx-sidenav-parent-color: #{$gray-800};
   --nbx-sidenav-group-color: #{$gray-800};
 
   &[data-netbox-color-mode='dark'] {
@@ -49,7 +49,7 @@
     --nbx-color-mode-toggle-color: #{$yellow-300};
     --nbx-sidenav-link-color: #{$gray-200};
     --nbx-sidenav-pin-color: #{$yellow};
-    --nbx-sidenav-parent-color: #{$gray-100};
+    --nbx-sidenav-parent-color: #{$gray-200};
     --nbx-sidenav-group-color: #{$gray-600};
   }
 }

+ 2 - 3
netbox/templates/circuits/circuittermination_edit.html

@@ -5,7 +5,7 @@
 {% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %}
 
 {% block form %}
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Circuit Termination</h5>
     </div>
@@ -53,9 +53,8 @@
       </div>
     {% endwith %}
   </div>
-  <hr />
 
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Termination Details</h5>
     </div>

+ 3 - 7
netbox/templates/dcim/cable_connect.html

@@ -17,9 +17,7 @@
       <div class="row my-3">
           <div class="col col-md-5">
               <div class="card h-100">
-                  <h5 class="card-header">
-                      A Side
-                  </h5>
+                  <h5 class="card-header offset-sm-3">A Side</h5>
                   <div class="card-body">
                       {% if termination_a.device %}
                           {# Device component #}
@@ -100,9 +98,7 @@
           </div>
           <div class="col col-md-5">
               <div class="card h-100">
-                  <h5 class="card-header">
-                      B Side
-                  </h5>
+                  <h5 class="card-header offset-sm-3">B Side</h5>
                   <div class="card-body">
                       {% if tabs %}
                           <ul class="nav nav-tabs">
@@ -154,7 +150,7 @@
       <div class="row my-3 justify-content-center">
         <div class="col col-md-8">
           <div class="card">
-            <h5 class="card-header">Cable</h5>
+            <h5 class="card-header offset-sm-3">Cable</h5>
             <div class="card-body">
               {% include 'dcim/inc/cable_form.html' %}
             </div>

+ 73 - 80
netbox/templates/dcim/device_edit.html

@@ -4,111 +4,104 @@
 {% block form %}
     {% render_errors form %}
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Device</h5>
-        </div>
-        {% render_field form.name %}
-        {% render_field form.device_role %}
-        {% render_field form.tags %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Device</h5>
+      </div>
+      {% render_field form.name %}
+      {% render_field form.device_role %}
+      {% render_field form.tags %}
     </div>
-    <hr />
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Hardware</h5>
-        </div>
-        {% render_field form.manufacturer %}
-        {% render_field form.device_type %}
-        {% render_field form.serial %}
-        {% render_field form.asset_tag %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Hardware</h5>
+      </div>
+      {% render_field form.manufacturer %}
+      {% render_field form.device_type %}
+      {% render_field form.serial %}
+      {% render_field form.asset_tag %}
     </div>
-    <hr />
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Location</h5>
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Location</h5>
+      </div>
+      {% render_field form.region %}
+      {% render_field form.site_group %}
+      {% render_field form.site %}
+      {% render_field form.location %}
+      {% render_field form.rack %}
+
+      {% if obj.device_type.is_child_device and obj.parent_bay %}
+        <div class="row mb-3">
+          <label class="col-sm-3 col-form-label">Parent Device</label>
+          <div class="col">
+            <input class="form-control" value="{{ obj.parent_bay.device }}" disabled />
+          </div>
         </div>
-        {% render_field form.region %}
-        {% render_field form.site_group %}
-        {% render_field form.site %}
-        {% render_field form.location %}
-        {% render_field form.rack %}
-        
-        {% if obj.device_type.is_child_device and obj.parent_bay %}
-            <div class="row mb-3">
-                <label class="col-sm-3 col-form-label">Parent Device</label>
-                <div class="col">
-                    <input class="form-control" value="{{ obj.parent_bay.device }}" disabled />
-                </div>
+        <div class="row mb-3">
+          <label class="col-sm-3 col-form-label">Parent Bay</label>
+          <div class="col">
+            <div class="input-group">
+              <input class="form-control" value="{{ obj.parent_bay.name }}" disabled />
+              <a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" title="Regenerate Slug" class="btn btn-danger d-inline-flex align-items-center">
+                <i class="mdi mdi-close-thick"></i>&nbsp;Remove
+              </a>
             </div>
-            <div class="row mb-3">
-                <label class="col-sm-3 col-form-label">Parent Bay</label>
-                <div class="col">
-                    <div class="input-group">
-                        <input class="form-control" value="{{ obj.parent_bay.name }}" disabled />
-                        <a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" title="Regenerate Slug" class="btn btn-danger d-inline-flex align-items-center">
-                            <i class="mdi mdi-close-thick"></i>&nbsp;Remove
-                        </a>
-                    </div>
-                </div>
             </div>
-        {% else %}
-            {% render_field form.face %}
-            {% render_field form.position %}
-        {% endif %}
+          </div>
+      {% else %}
+        {% render_field form.face %}
+        {% render_field form.position %}
+      {% endif %}
     </div>
-    <hr />
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Management</h5>
-        </div>
-        {% render_field form.status %}
-        {% render_field form.platform %}
-        {% if obj.pk %}
-            {% render_field form.primary_ip4 %}
-            {% render_field form.primary_ip6 %}
-        {% endif %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Management</h5>
+      </div>
+      {% render_field form.status %}
+      {% render_field form.platform %}
+      {% if obj.pk %}
+        {% render_field form.primary_ip4 %}
+        {% render_field form.primary_ip6 %}
+      {% endif %}
     </div>
-    <hr />
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Virtualization</h5>
-        </div>
-        {% render_field form.cluster_group %}
-        {% render_field form.cluster %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Virtualization</h5>
+      </div>
+      {% render_field form.cluster_group %}
+      {% render_field form.cluster %}
     </div>
-    <hr />
     
-    <div class="field-group my-4">
-        <div class="row mb-2">
-          <h5 class="offset-sm-3">Tenancy</h5>
-        </div>
-        {% render_field form.tenant_group %}
-        {% render_field form.tenant %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Tenancy</h5>
+      </div>
+      {% render_field form.tenant_group %}
+      {% render_field form.tenant %}
     </div>
-    <hr />
 
     {% if form.custom_fields %}
-      <div class="field-group my-4">
+      <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Custom Fields</h5>
         </div>
         {% render_custom_fields form %}
       </div>
-      <hr />
     {% endif %}
 
-    <div class="field-group my-4">
-        <h5 class="text-center">Local Config Context Data</h5>
-        {% render_field form.local_context_data %}
+    <div class="field-group my-5">
+      <h5 class="text-center">Local Config Context Data</h5>
+      {% render_field form.local_context_data %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
-        {% render_field form.comments label='Comments' %}
+    <div class="field-group mb-5">
+      <h5 class="text-center">Comments</h5>
+      {% render_field form.comments %}
     </div>
 
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/inc/cable_form.html

@@ -16,7 +16,6 @@
 </div>
 {% render_field form.tags %}
 {% if form.custom_fields %}
-  <hr />
   <div class="field-group">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Custom Fields</h5>

+ 3 - 5
netbox/templates/dcim/interface_edit.html

@@ -2,7 +2,7 @@
 {% load form_helpers %}
 
 {% block form %}
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Interface</h5>
         </div>
@@ -27,9 +27,8 @@
         {% render_field form.mgmt_only %}
         {% render_field form.mark_connected %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">802.1Q Switching</h5>
         </div>
@@ -40,8 +39,7 @@
     </div>
 
     {% if form.custom_fields %}
-      <hr />
-      <div class="field-group my-4">
+      <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Custom Fields</h5>
         </div>

+ 6 - 0
netbox/templates/dcim/platform.html

@@ -46,6 +46,12 @@
               <a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
             </td>
           </tr>
+          <tr>
+            <th scope="row">Virtual Machines</th>
+            <td>
+              <a href="{% url 'virtualization:virtualmachine_list' %}?platform_id={{ object.pk }}">{{ virtualmachine_count }}</a>
+            </td>
+          </tr>
         </table>
       </div>
     </div>

+ 11 - 15
netbox/templates/dcim/rack_edit.html

@@ -2,7 +2,7 @@
 {% load form_helpers %}
 
 {% block form %}
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Rack</h5>
         </div>
@@ -15,9 +15,8 @@
         {% render_field form.role %}
         {% render_field form.tags %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Inventory Control</h5>
         </div>
@@ -25,18 +24,16 @@
         {% render_field form.serial %}
         {% render_field form.asset_tag %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Tenancy</h5>
         </div>
         {% render_field form.tenant_group %}
         {% render_field form.tenant %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Dimensions</h5>
         </div>
@@ -45,34 +42,33 @@
         {% render_field form.u_height %}
         <div class="row mb-3">
             <label class="col col-md-3 col-form-label text-lg-end">Outer Dimensions</label>
-            <div class="col col-md-3">
+            <div class="col col-md-3 mb-1">
                 {{ form.outer_width }}
                 <div class="form-text">Width</div>
             </div>
-            <div class="col col-md-3">
+            <div class="col col-md-3 mb-1">
                 {{ form.outer_depth }}
                 <div class="form-text">Depth</div>
             </div>
-            <div class="col col-md-3">
+            <div class="col col-md-3 mb-1">
                 {{ form.outer_unit }}
                 <div class="form-text">Unit</div>
             </div>
         </div>
         {% render_field form.desc_units %}
     </div>
-    <hr />
 
     {% if form.custom_fields %}
-      <div class="field-group my-4">
+      <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Custom Fields</h5>
         </div>
           {% render_custom_fields form %}
       </div>
-      <hr />
     {% endif %}
 
-    <div class="field-group my-4">
-        {% render_field form.comments label='Comments' %}
+    <div class="field-group my-5">
+      <h5 class="text-center">Comments</h5>
+      {% render_field form.comments %}
     </div>
 {% endblock %}

+ 3 - 5
netbox/templates/dcim/virtualchassis_add.html

@@ -2,7 +2,7 @@
 {% load form_helpers %}
 
 {% block form %}
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Virtual Chassis</h5>
     </div>
@@ -10,9 +10,8 @@
     {% render_field form.domain %}
     {% render_field form.tags %}
   </div>
-  <hr />
 
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Member Devices</h5>
     </div>
@@ -25,8 +24,7 @@
   </div>
 
   {% if form.custom_fields %}
-    <hr />
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Custom Fields</h5>
       </div>

+ 2 - 4
netbox/templates/dcim/virtualchassis_edit.html

@@ -11,7 +11,7 @@
         {% csrf_token %}
         {{ pk_form.pk }}
         {{ formset.management_form }}
-        <div class="field-group my-4">
+        <div class="field-group my-5">
           <div class="row mb-2">
             <h5 class="offset-sm-3">Virtual Chassis</h5>
           </div>
@@ -20,16 +20,14 @@
           {% render_field vc_form.master %}
           {% render_field vc_form.tags %}
         </div>
-        <hr />
 
         {% if vc_form.custom_fields %}
-          <div class="field-group my-4">
+          <div class="field-group my-5">
             <div class="row mb-2">
               <h5 class="offset-sm-3">Custom Fields</h5>
             </div>
             {% render_custom_fields vc_form %}
           </div>
-          <hr />
         {% endif %}
 
         <div class="field-group mb-5">

+ 6 - 0
netbox/templates/generic/object.html

@@ -8,6 +8,12 @@
 {% block header %}
   {# Breadcrumbs #}
   <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
+    <div class="float-end">
+      <code class="text-muted" title="Object type and ID">
+        {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
+        {% if object.slug %}({{ object.slug }}){% endif %}
+      </code>
+    </div>
     <ol class="breadcrumb">
       {% block breadcrumbs %}
         <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>

+ 16 - 22
netbox/templates/generic/object_edit.html

@@ -6,18 +6,6 @@
   {% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
 {% endblock title %}
 
-{% block controls %}
-  {% if obj and settings.DOCS_ROOT %}
-    <div class="controls">
-      <div class="control-group">
-        <a href="{{ obj|get_docs_url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="View model documentation">
-          <i class="mdi mdi-help-circle"></i> Help
-        </a>
-      </div>
-    </div>
-  {% endif %}
-{% endblock controls %}
-
 {% block tabs %}
   <ul class="nav nav-tabs px-3">
     <li class="nav-item" role="presentation">
@@ -31,6 +19,16 @@
 {% block content-wrapper %}
   <div class="tab-content">
     <div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
+
+      {# Link to model documentation #}
+      {% if obj and settings.DOCS_ROOT %}
+        <div class="float-end">
+          <a href="{{ obj|get_docs_url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="View model documentation">
+            <i class="mdi mdi-help-circle"></i> Help
+          </a>
+        </div>
+      {% endif %}
+
       <form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
         {% csrf_token %}
         {% for field in form.hidden_fields %}
@@ -42,7 +40,7 @@
 
             {# Render grouped fields according to Form #}
             {% for group, fields in form.Meta.fieldsets %}
-              <div class="field-group my-4">
+              <div class="field-group my-5">
                 <div class="row mb-2">
                   <h5 class="offset-sm-3">{{ group }}</h5>
                 </div>
@@ -50,14 +48,10 @@
                     {% render_field form|getfield:name %}
                 {% endfor %}
               </div>
-              {% if not forloop.last %}
-                <hr />
-              {% endif %}
             {% endfor %}
 
             {% if form.custom_fields %}
-              <hr />
-              <div class="field-group my-4">
+              <div class="field-group my-5">
                 <div class="row mb-2">
                   <h5 class="offset-sm-3">Custom Fields</h5>
                 </div>
@@ -66,15 +60,15 @@
             {% endif %}
 
             {% if form.comments %}
-              <hr />
-              <div class="field-group my-4">
-                {% render_field form.comments label='Comments' %}
+              <div class="field-group my-5">
+                <h5 class="text-center">Comments</h5>
+                {% render_field form.comments %}
               </div>
             {% endif %}
 
           {% else %}
             {# Render all fields in a single group #}
-            <div class="field-group my-4">
+            <div class="field-group my-5">
               {% block form_fields %}{% render_form form %}{% endblock %}
             </div>
           {% endif %}

+ 3 - 1
netbox/templates/generic/object_list.html

@@ -4,6 +4,8 @@
 {% load render_table from django_tables2 %}
 {% load static %}
 
+{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
+
 {% block controls %}
   <div class="controls">
     <div class="control-group">
@@ -26,7 +28,7 @@
     {% block tab_items %}
       <li class="nav-item" role="presentation">
         <button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
-          {% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
+          Records
           {% badge table.page.paginator.count %}
         </button>
       </li>

+ 2 - 2
netbox/templates/home.html

@@ -30,7 +30,7 @@
       {% for section, items, icon in stats %}
         <div class="col col-sm-12 col-lg-6 col-xl-4 my-2 masonry-item">
           <div class="card">
-            <h6 class="card-header text-primary text-center">
+            <h6 class="card-header text-center">
               <i class="mdi mdi-{{ icon }}"></i>
               <span class="ms-1">{{ section }}</span>
             </h6>
@@ -67,7 +67,7 @@
       <div class="row my-4 flex-grow-1 changelog-container">
         <div class="col">
           <div class="card">
-            <h6 class="card-header text-primary text-center">
+            <h6 class="card-header text-center">
               <i class="mdi mdi-clipboard-clock"></i>
               <span class="ms-1">Change Log</span>
             </h6>

+ 2 - 2
netbox/templates/inc/table_controls.html

@@ -4,8 +4,8 @@
       <input
         type="text"
         class="form-control object-filter"
-        placeholder="Filter"
-        title="Filter text (regular expressions supported)"
+        placeholder="Quick find"
+        title="Find in the results below (regular expressions supported)"
       />
     </div>
   </div>

+ 3 - 5
netbox/templates/ipam/ipaddress_bulk_add.html

@@ -9,7 +9,7 @@
 {% endblock %}
 
 {% block form %}
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">IP Addresses</h5>
         </div>
@@ -20,9 +20,8 @@
         {% render_field model_form.description %}
         {% render_field model_form.tags %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Tenancy</h5>
         </div>
@@ -30,8 +29,7 @@
         {% render_field model_form.tenant %}
     </div>
     {% if model_form.custom_fields %}
-        <hr />
-        <div class="field-group my-4">
+        <div class="field-group my-5">
             <div class="row mb-2">
               <h5 class="offset-sm-3">Custom Fields</h5>
             </div>

+ 5 - 9
netbox/templates/ipam/ipaddress_edit.html

@@ -8,7 +8,7 @@
 {% endblock tabs %}
 
 {% block form %}
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">IP Address</h5>
       </div>
@@ -20,18 +20,16 @@
       {% render_field form.description %}
       {% render_field form.tags %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Tenancy</h5>
       </div>
       {% render_field form.tenant_group %}
       {% render_field form.tenant %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Interface Assignment</h5>
       </div>
@@ -81,9 +79,8 @@
         </div>
       {% endwith %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">NAT IP (Inside)</h5>
       </div>
@@ -152,8 +149,7 @@
     </div>
 
     {% if form.custom_fields %}
-      <hr />
-      <div class="field-group my-4">
+      <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Custom Fields</h5>
         </div>

+ 1 - 2
netbox/templates/ipam/service_edit.html

@@ -2,7 +2,7 @@
 {% load form_helpers %}
 
 {% block form %}
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Service</h5>
     </div>
@@ -43,7 +43,6 @@
   </div>
 
   {% if form.custom_fields %}
-    <hr />
     <div class="row mb-2">
       <h5 class="offset-sm-3">Custom Fields</h5>
     </div>

+ 4 - 7
netbox/templates/ipam/vlan_edit.html

@@ -4,7 +4,7 @@
 {% load helpers %}
 
 {% block form %}
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">VLAN</h5>
     </div>
@@ -15,18 +15,16 @@
     {% render_field form.description %}
     {% render_field form.tags %}
   </div>
-  <hr />
 
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Tenancy</h5>
     </div>
     {% render_field form.tenant_group %}
     {% render_field form.tenant %}
   </div>
-  <hr />
 
-  <div class="field-group my-4">
+  <div class="field-group my-5">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Assignment</h5>
     </div>
@@ -58,8 +56,7 @@
   </div>
 
   {% if form.custom_fields %}
-    <hr />
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Custom Fields</h5>
       </div>

+ 1 - 1
netbox/templates/tenancy/tenantgroup.html

@@ -37,7 +37,7 @@
             </td>
           </tr>
           <tr>
-            <th scope="row">Sites</th>
+            <th scope="row">Tenants</th>
             <td>
               <a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a>
             </td>

+ 3 - 5
netbox/templates/virtualization/vminterface_edit.html

@@ -2,7 +2,7 @@
 {% load form_helpers %}
 
 {% block form %}
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">Interface</h5>
       </div>
@@ -22,9 +22,8 @@
       {% render_field form.description %}
       {% render_field form.tags %}
     </div>
-    <hr />
 
-    <div class="field-group my-4">
+    <div class="field-group my-5">
       <div class="row mb-2">
         <h5 class="offset-sm-3">802.1Q Switching</h5>
       </div>
@@ -35,8 +34,7 @@
     </div>
 
     {% if form.custom_fields %}
-      <hr />
-      <div class="field-group my-4">
+      <div class="field-group my-5">
         <div class="row mb-2">
           <h5 class="offset-sm-3">Custom Fields</h5>
         </div>

+ 11 - 9
netbox/utilities/tables.py

@@ -57,14 +57,14 @@ class BaseTable(tables.Table):
         if user is not None and not isinstance(user, AnonymousUser):
             selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
             if selected_columns:
-                pk = self.base_columns.pop('pk', None)
-                actions = self.base_columns.pop('actions', None)
 
+                # Show only persistent or selected columns
                 for name, column in self.columns.items():
-                    if name in selected_columns:
+                    if name in ['pk', 'actions', *selected_columns]:
                         self.columns.show(name)
                     else:
                         self.columns.hide(name)
+
                 # Rearrange the sequence to list selected columns first, followed by all remaining columns
                 # TODO: There's probably a more clever way to accomplish this
                 self.sequence = [
@@ -72,12 +72,14 @@ class BaseTable(tables.Table):
                     *[c for c in self.columns.names() if c not in selected_columns]
                 ]
 
-                # Always include PK and actions column, if defined on the table
-                if pk:
-                    self.base_columns['pk'] = pk
+                # PK column should always come first
+                if 'pk' in self.sequence:
+                    self.sequence.remove('pk')
                     self.sequence.insert(0, 'pk')
-                if actions:
-                    self.base_columns['actions'] = actions
+
+                # Actions column should always come last
+                if 'actions' in self.sequence:
+                    self.sequence.remove('actions')
                     self.sequence.append('actions')
 
         # Dynamically update the table's QuerySet to ensure related fields are pre-fetched
@@ -128,7 +130,7 @@ class BaseTable(tables.Table):
         prefixes/IP addresses/etc., where some table rows may represent available address space.
         """
         if not hasattr(self, '_objects_count'):
-            self._objects_count = sum(1 for obj in self.data if getattr(obj, 'pk'))
+            self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
         return self._objects_count
 
 

+ 4 - 4
requirements.txt

@@ -1,5 +1,5 @@
-Django==3.2.7
-django-cors-headers==3.9.0
+Django==3.2.8
+django-cors-headers==3.10.0
 django-debug-toolbar==3.2.2
 django-filter==21.1
 django-graphiql-debug-toolbar==0.2.0
@@ -8,14 +8,14 @@ django-pglocks==1.0.4
 django-prometheus==2.1.0
 django-redis==5.0.0
 django-rq==2.4.1
-django-tables2==2.4.0
+django-tables2==2.4.1
 django-taggit==1.5.1
 django-timezone-field==4.2.1
 djangorestframework==3.12.4
 drf-yasg[validation]==1.20.0
 graphene_django==2.15.0
 gunicorn==20.1.0
-Jinja2==3.0.1
+Jinja2==3.0.2
 Markdown==3.3.4
 markdown-include==0.6.0
 mkdocs-material==7.3.1

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff