Przeglądaj źródła

Merge pull request #8257 from netbox-community/develop

Release v3.1.5
Jeremy Stretch 4 lat temu
rodzic
commit
d3e2241ff7
59 zmienionych plików z 468 dodań i 309 usunięć
  1. 1 1
      .github/ISSUE_TEMPLATE/bug_report.yaml
  2. 1 1
      .github/ISSUE_TEMPLATE/feature_request.yaml
  3. 18 0
      docs/release-notes/version-3.1.md
  4. 17 10
      netbox/dcim/forms/filtersets.py
  5. 2 1
      netbox/dcim/views.py
  6. 14 4
      netbox/extras/forms/models.py
  7. 10 4
      netbox/extras/scripts.py
  8. 1 0
      netbox/ipam/constants.py
  9. 2 3
      netbox/ipam/forms/models.py
  10. 77 0
      netbox/ipam/tests/test_views.py
  11. 1 4
      netbox/ipam/views.py
  12. 1 1
      netbox/netbox/settings.py
  13. 16 4
      netbox/netbox/views/generic.py
  14. 0 0
      netbox/project-static/dist/netbox-dark.css
  15. 0 0
      netbox/project-static/dist/netbox-light.css
  16. 0 0
      netbox/project-static/dist/netbox-print.css
  17. 8 10
      netbox/project-static/styles/netbox.scss
  18. 0 89
      netbox/project-static/styles/theme-base.scss
  19. 14 7
      netbox/project-static/styles/theme-dark.scss
  20. 37 18
      netbox/project-static/styles/theme-light.scss
  21. 4 1
      netbox/templates/base/layout.html
  22. 4 1
      netbox/templates/dcim/device/consoleports.html
  23. 4 1
      netbox/templates/dcim/device/consoleserverports.html
  24. 4 1
      netbox/templates/dcim/device/devicebays.html
  25. 4 1
      netbox/templates/dcim/device/frontports.html
  26. 4 1
      netbox/templates/dcim/device/interfaces.html
  27. 4 1
      netbox/templates/dcim/device/inventory.html
  28. 4 1
      netbox/templates/dcim/device/poweroutlets.html
  29. 4 1
      netbox/templates/dcim/device/powerports.html
  30. 4 1
      netbox/templates/dcim/device/rearports.html
  31. 1 1
      netbox/templates/dcim/devicebay_populate.html
  32. 2 0
      netbox/templates/dcim/rack_elevation_list.html
  33. 35 26
      netbox/templates/extras/report.html
  34. 1 1
      netbox/templates/extras/report_result.html
  35. 47 49
      netbox/templates/extras/script.html
  36. 5 1
      netbox/templates/generic/object.html
  37. 13 6
      netbox/templates/generic/object_delete.html
  38. 4 2
      netbox/templates/generic/object_list.html
  39. 20 0
      netbox/templates/htmx/delete_form.html
  40. 7 0
      netbox/templates/inc/htmx_modal.html
  41. 4 1
      netbox/templates/ipam/aggregate/prefixes.html
  42. 4 1
      netbox/templates/ipam/iprange/ip_addresses.html
  43. 4 1
      netbox/templates/ipam/prefix/ip_addresses.html
  44. 4 1
      netbox/templates/ipam/prefix/ip_ranges.html
  45. 4 1
      netbox/templates/ipam/prefix/prefixes.html
  46. 0 5
      netbox/templates/ipam/prefix_delete.html
  47. 0 6
      netbox/templates/ipam/vlan/base.html
  48. 4 3
      netbox/templates/ipam/vlan/interfaces.html
  49. 4 3
      netbox/templates/ipam/vlan/vminterfaces.html
  50. 4 3
      netbox/templates/virtualization/cluster/devices.html
  51. 4 3
      netbox/templates/virtualization/cluster/virtual_machines.html
  52. 4 1
      netbox/templates/virtualization/virtualmachine/interfaces.html
  53. 2 2
      netbox/utilities/forms/fields.py
  54. 0 10
      netbox/utilities/forms/widgets.py
  55. 8 2
      netbox/utilities/templates/buttons/delete.html
  56. 0 1
      netbox/utilities/templates/widgets/select_contenttype.html
  57. 6 0
      netbox/virtualization/tables.py
  58. 15 10
      netbox/wireless/forms/bulk_edit.py
  59. 2 2
      requirements.txt

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.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.1.4
+      placeholder: v3.1.5
     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.1.4
+      placeholder: v3.1.5
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 18 - 0
docs/release-notes/version-3.1.md

@@ -1,5 +1,23 @@
 # NetBox v3.1
 # NetBox v3.1
 
 
+## v3.1.5 (2022-01-06)
+
+### Enhancements
+
+* [#8231](https://github.com/netbox-community/netbox/issues/8231) - Use in-page dialogs for confirming object deletion
+* [#8244](https://github.com/netbox-community/netbox/issues/8244) - Add length & length unit fields to cable filter form
+* [#8252](https://github.com/netbox-community/netbox/issues/8252) - Linkify type and group columns in clusters table
+
+### Bug Fixes
+
+* [#8213](https://github.com/netbox-community/netbox/issues/8213) - Fix ValueError exception under prefix IP addresses view
+* [#8224](https://github.com/netbox-community/netbox/issues/8224) - Fix KeyError exception when creating FHRP group with IP address and protocol "other"
+* [#8226](https://github.com/netbox-community/netbox/issues/8226) - Honor return URL after populating a device bay
+* [#8228](https://github.com/netbox-community/netbox/issues/8228) - Optional ChoiceVar fields should not force a selection
+* [#8255](https://github.com/netbox-community/netbox/issues/8255) - Fix bulk editing of authentication parameters for wireless LANs and links
+
+---
+
 ## v3.1.4 (2022-01-03)
 ## v3.1.4 (2022-01-03)
 
 
 ### Enhancements
 ### Enhancements

+ 17 - 10
netbox/dcim/forms/filtersets.py

@@ -578,7 +578,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
     field_groups = [
     field_groups = [
         ['q', 'tag'],
         ['q', 'tag'],
         ['site_id', 'rack_id', 'device_id'],
         ['site_id', 'rack_id', 'device_id'],
-        ['type', 'status', 'color'],
+        ['type', 'status', 'color', 'length', 'length_unit'],
         ['tenant_group_id', 'tenant_id'],
         ['tenant_group_id', 'tenant_id'],
     ]
     ]
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -603,6 +603,16 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
             'site_id': '$site_id'
             'site_id': '$site_id'
         }
         }
     )
     )
+    device_id = DynamicModelMultipleChoiceField(
+        queryset=Device.objects.all(),
+        required=False,
+        query_params={
+            'site_id': '$site_id',
+            'tenant_id': '$tenant_id',
+            'rack_id': '$rack_id',
+        },
+        label=_('Device')
+    )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         choices=add_blank_choice(CableTypeChoices),
         choices=add_blank_choice(CableTypeChoices),
         required=False,
         required=False,
@@ -616,15 +626,12 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
     color = ColorField(
     color = ColorField(
         required=False
         required=False
     )
     )
-    device_id = DynamicModelMultipleChoiceField(
-        queryset=Device.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site_id',
-            'tenant_id': '$tenant_id',
-            'rack_id': '$rack_id',
-        },
-        label=_('Device')
+    length = forms.IntegerField(
+        required=False
+    )
+    length_unit = forms.ChoiceField(
+        choices=add_blank_choice(CableLengthUnitChoices),
+        required=False
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 

+ 2 - 1
netbox/dcim/views.py

@@ -2035,8 +2035,9 @@ class DeviceBayPopulateView(generic.ObjectEditView):
             device_bay.installed_device = form.cleaned_data['installed_device']
             device_bay.installed_device = form.cleaned_data['installed_device']
             device_bay.save()
             device_bay.save()
             messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
             messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
+            return_url = self.get_return_url(request)
 
 
-            return redirect('dcim:device', pk=device_bay.device.pk)
+            return redirect(return_url)
 
 
         return render(request, 'dcim/devicebay_populate.html', {
         return render(request, 'dcim/devicebay_populate.html', {
             'device_bay': device_bay,
             'device_bay': device_bay,

+ 14 - 4
netbox/extras/forms/models.py

@@ -7,8 +7,8 @@ from extras.models import *
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
 from utilities.forms import (
-    add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
-    ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
+    add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField,
+    DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
 )
 )
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 
 
@@ -41,6 +41,10 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
             ('Values', ('default', 'choices')),
             ('Values', ('default', 'choices')),
             ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
             ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
         )
         )
+        widgets = {
+            'type': StaticSelect(),
+            'filter_logic': StaticSelect(),
+        }
 
 
 
 
 class CustomLinkForm(BootstrapMixin, forms.ModelForm):
 class CustomLinkForm(BootstrapMixin, forms.ModelForm):
@@ -57,6 +61,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
             ('Templates', ('link_text', 'link_url')),
             ('Templates', ('link_text', 'link_url')),
         )
         )
         widgets = {
         widgets = {
+            'button_class': StaticSelect(),
             'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
             'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
             'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
             'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
         }
         }
@@ -96,8 +101,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
         model = Webhook
         model = Webhook
         fields = '__all__'
         fields = '__all__'
         fieldsets = (
         fieldsets = (
-            ('Webhook', ('name', 'enabled')),
-            ('Assigned Models', ('content_types',)),
+            ('Webhook', ('name', 'content_types', 'enabled')),
             ('Events', ('type_create', 'type_update', 'type_delete')),
             ('Events', ('type_create', 'type_update', 'type_delete')),
             ('HTTP Request', (
             ('HTTP Request', (
                 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
                 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
@@ -105,7 +109,13 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
             ('Conditions', ('conditions',)),
             ('Conditions', ('conditions',)),
             ('SSL', ('ssl_verification', 'ca_file_path')),
             ('SSL', ('ssl_verification', 'ca_file_path')),
         )
         )
+        labels = {
+            'type_create': 'Creations',
+            'type_update': 'Updates',
+            'type_delete': 'Deletions',
+        }
         widgets = {
         widgets = {
+            'http_method': StaticSelect(),
             'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
             'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
             'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
             'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
         }
         }

+ 10 - 4
netbox/extras/scripts.py

@@ -21,7 +21,7 @@ from extras.models import JobResult
 from ipam.formfields import IPAddressFormField, IPNetworkFormField
 from ipam.formfields import IPAddressFormField, IPNetworkFormField
 from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
 from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
 from utilities.exceptions import AbortTransaction
 from utilities.exceptions import AbortTransaction
-from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from .context_managers import change_logging
 from .context_managers import change_logging
 from .forms import ScriptForm
 from .forms import ScriptForm
 
 
@@ -164,16 +164,22 @@ class ChoiceVar(ScriptVariable):
     def __init__(self, choices, *args, **kwargs):
     def __init__(self, choices, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
-        # Set field choices
-        self.field_attrs['choices'] = choices
+        # Set field choices, adding a blank choice to avoid forced selections
+        self.field_attrs['choices'] = add_blank_choice(choices)
 
 
 
 
-class MultiChoiceVar(ChoiceVar):
+class MultiChoiceVar(ScriptVariable):
     """
     """
     Like ChoiceVar, but allows for the selection of multiple choices.
     Like ChoiceVar, but allows for the selection of multiple choices.
     """
     """
     form_field = forms.MultipleChoiceField
     form_field = forms.MultipleChoiceField
 
 
+    def __init__(self, choices, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set field choices
+        self.field_attrs['choices'] = choices
+
 
 
 class ObjectVar(ScriptVariable):
 class ObjectVar(ScriptVariable):
     """
     """

+ 1 - 0
netbox/ipam/constants.py

@@ -65,6 +65,7 @@ FHRP_PROTOCOL_ROLE_MAPPINGS = {
     FHRPGroupProtocolChoices.PROTOCOL_HSRP: IPAddressRoleChoices.ROLE_HSRP,
     FHRPGroupProtocolChoices.PROTOCOL_HSRP: IPAddressRoleChoices.ROLE_HSRP,
     FHRPGroupProtocolChoices.PROTOCOL_GLBP: IPAddressRoleChoices.ROLE_GLBP,
     FHRPGroupProtocolChoices.PROTOCOL_GLBP: IPAddressRoleChoices.ROLE_GLBP,
     FHRPGroupProtocolChoices.PROTOCOL_CARP: IPAddressRoleChoices.ROLE_CARP,
     FHRPGroupProtocolChoices.PROTOCOL_CARP: IPAddressRoleChoices.ROLE_CARP,
+    FHRPGroupProtocolChoices.PROTOCOL_OTHER: IPAddressRoleChoices.ROLE_VIP,
 }
 }
 
 
 
 

+ 2 - 3
netbox/ipam/forms/models.py

@@ -580,7 +580,7 @@ class FHRPGroupForm(CustomFieldModelForm):
                 vrf=self.cleaned_data['ip_vrf'],
                 vrf=self.cleaned_data['ip_vrf'],
                 address=self.cleaned_data['ip_address'],
                 address=self.cleaned_data['ip_address'],
                 status=self.cleaned_data['ip_status'],
                 status=self.cleaned_data['ip_status'],
-                role=FHRP_PROTOCOL_ROLE_MAPPINGS[self.cleaned_data['protocol']],
+                role=FHRP_PROTOCOL_ROLE_MAPPINGS.get(self.cleaned_data['protocol'], IPAddressRoleChoices.ROLE_VIP),
                 assigned_object=instance
                 assigned_object=instance
             )
             )
             ipaddress.save()
             ipaddress.save()
@@ -628,8 +628,7 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
 class VLANGroupForm(CustomFieldModelForm):
 class VLANGroupForm(CustomFieldModelForm):
     scope_type = ContentTypeChoiceField(
     scope_type = ContentTypeChoiceField(
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
-        required=False,
-        widget=StaticSelect
+        required=False
     )
     )
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),

+ 77 - 0
netbox/ipam/tests/test_views.py

@@ -1,5 +1,7 @@
 import datetime
 import datetime
 
 
+from django.test import override_settings
+from django.urls import reverse
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
@@ -222,6 +224,21 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'description': 'New description',
             'description': 'New description',
         }
         }
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_aggregate_prefixes(self):
+        rir = RIR.objects.first()
+        aggregate = Aggregate.objects.create(prefix=IPNetwork('192.168.0.0/16'), rir=rir)
+        prefixes = (
+            Prefix(prefix=IPNetwork('192.168.1.0/24')),
+            Prefix(prefix=IPNetwork('192.168.2.0/24')),
+            Prefix(prefix=IPNetwork('192.168.3.0/24')),
+        )
+        Prefix.objects.bulk_create(prefixes)
+        self.assertEqual(aggregate.get_child_prefixes().count(), 3)
+
+        url = reverse('ipam:aggregate_prefixes', kwargs={'pk': aggregate.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
 
 
 class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
     model = Role
     model = Role
@@ -319,6 +336,48 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'description': 'New description',
             'description': 'New description',
         }
         }
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_prefix_prefixes(self):
+        prefixes = (
+            Prefix(prefix=IPNetwork('192.168.0.0/16')),
+            Prefix(prefix=IPNetwork('192.168.1.0/24')),
+            Prefix(prefix=IPNetwork('192.168.2.0/24')),
+            Prefix(prefix=IPNetwork('192.168.3.0/24')),
+        )
+        Prefix.objects.bulk_create(prefixes)
+        self.assertEqual(prefixes[0].get_child_prefixes().count(), 3)
+
+        url = reverse('ipam:prefix_prefixes', kwargs={'pk': prefixes[0].pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_prefix_ipranges(self):
+        prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
+        ip_ranges = (
+            IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
+            IPRange(start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
+            IPRange(start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
+        )
+        IPRange.objects.bulk_create(ip_ranges)
+        self.assertEqual(prefix.get_child_ranges().count(), 3)
+
+        url = reverse('ipam:prefix_ipranges', kwargs={'pk': prefix.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_prefix_ipaddresses(self):
+        prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
+        ip_addresses = (
+            IPAddress(address=IPNetwork('192.168.0.1/16')),
+            IPAddress(address=IPNetwork('192.168.0.2/16')),
+            IPAddress(address=IPNetwork('192.168.0.3/16')),
+        )
+        IPAddress.objects.bulk_create(ip_addresses)
+        self.assertEqual(prefix.get_child_ips().count(), 3)
+
+        url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
 
 
 class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     model = IPRange
     model = IPRange
@@ -377,6 +436,24 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'description': 'New description',
             'description': 'New description',
         }
         }
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_iprange_ipaddresses(self):
+        iprange = IPRange.objects.create(
+            start_address=IPNetwork('192.168.0.1/24'),
+            end_address=IPNetwork('192.168.0.100/24'),
+            size=99
+        )
+        ip_addresses = (
+            IPAddress(address=IPNetwork('192.168.0.1/24')),
+            IPAddress(address=IPNetwork('192.168.0.2/24')),
+            IPAddress(address=IPNetwork('192.168.0.3/24')),
+        )
+        IPAddress.objects.bulk_create(ip_addresses)
+        self.assertEqual(iprange.get_child_ips().count(), 3)
+
+        url = reverse('ipam:iprange_ipaddresses', kwargs={'pk': iprange.pk})
+        self.assertHttpStatus(self.client.get(url), 200)
+
 
 
 class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     model = IPAddress
     model = IPAddress

+ 1 - 4
netbox/ipam/views.py

@@ -505,9 +505,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
     template_name = 'ipam/prefix/ip_addresses.html'
     template_name = 'ipam/prefix/ip_addresses.html'
 
 
     def get_children(self, request, parent):
     def get_children(self, request, parent):
-        return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
-            'vrf', 'role', 'tenant',
-        )
+        return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant')
 
 
     def prep_table_data(self, request, queryset, parent):
     def prep_table_data(self, request, queryset, parent):
         show_available = bool(request.GET.get('show_available', 'true') == 'true')
         show_available = bool(request.GET.get('show_available', 'true') == 'true')
@@ -531,7 +529,6 @@ class PrefixEditView(generic.ObjectEditView):
 
 
 class PrefixDeleteView(generic.ObjectDeleteView):
 class PrefixDeleteView(generic.ObjectDeleteView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
-    template_name = 'ipam/prefix_delete.html'
 
 
 
 
 class PrefixBulkImportView(generic.BulkImportView):
 class PrefixBulkImportView(generic.BulkImportView):

+ 1 - 1
netbox/netbox/settings.py

@@ -19,7 +19,7 @@ from netbox.config import PARAMS
 # Environment setup
 # Environment setup
 #
 #
 
 
-VERSION = '3.1.4'
+VERSION = '3.1.5'
 
 
 # Hostname
 # Hostname
 HOSTNAME = platform.node()
 HOSTNAME = platform.node()

+ 16 - 4
netbox/netbox/views/generic.py

@@ -10,6 +10,7 @@ from django.db.models import ManyToManyField, ProtectedError
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
@@ -430,10 +431,21 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
         obj = self.get_object(kwargs)
         obj = self.get_object(kwargs)
         form = ConfirmationForm(initial=request.GET)
         form = ConfirmationForm(initial=request.GET)
 
 
+        # If this is an HTMX request, return only the rendered deletion form as modal content
+        if is_htmx(request):
+            viewname = f'{self.queryset.model._meta.app_label}:{self.queryset.model._meta.model_name}_delete'
+            form_url = reverse(viewname, kwargs={'pk': obj.pk})
+            return render(request, 'htmx/delete_form.html', {
+                'object': obj,
+                'object_type': self.queryset.model._meta.verbose_name,
+                'form': form,
+                'form_url': form_url,
+            })
+
         return render(request, self.template_name, {
         return render(request, self.template_name, {
-            'obj': obj,
+            'object': obj,
+            'object_type': self.queryset.model._meta.verbose_name,
             'form': form,
             'form': form,
-            'obj_type': self.queryset.model._meta.verbose_name,
             'return_url': self.get_return_url(request, obj),
             'return_url': self.get_return_url(request, obj),
         })
         })
 
 
@@ -466,9 +478,9 @@ class ObjectDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
             logger.debug("Form validation failed")
             logger.debug("Form validation failed")
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
-            'obj': obj,
+            'object': obj,
+            'object_type': self.queryset.model._meta.verbose_name,
             'form': form,
             'form': form,
-            'obj_type': self.queryset.model._meta.verbose_name,
             'return_url': self.get_return_url(request, obj),
             'return_url': self.get_return_url(request, obj),
         })
         })
 
 

Plik diff jest za duży
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Plik diff jest za duży
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Plik diff jest za duży
+ 0 - 0
netbox/project-static/dist/netbox-print.css


+ 8 - 10
netbox/project-static/styles/netbox.scss

@@ -358,7 +358,7 @@ nav.search {
   // Don't overtake dropdowns
   // Don't overtake dropdowns
   z-index: 999;
   z-index: 999;
   justify-content: center;
   justify-content: center;
-  background-color: var(--nbx-body-bg);
+  background-color: $navbar-light-color;
 
 
   .search-container {
   .search-container {
     display: flex;
     display: flex;
@@ -452,8 +452,8 @@ main.login-container {
 }
 }
 
 
 .footer {
 .footer {
+  background-color: $tab-content-bg;
   padding: 0;
   padding: 0;
-
   .nav-link {
   .nav-link {
     padding: 0.5rem;
     padding: 0.5rem;
   }
   }
@@ -517,6 +517,10 @@ h6.accordion-item-title {
   }
   }
 }
 }
 
 
+.navbar {
+  border-bottom: 1px solid $border-color;
+}
+
 .navbar-brand {
 .navbar-brand {
   padding-top: 0.75rem;
   padding-top: 0.75rem;
   padding-bottom: 0.75rem;
   padding-bottom: 0.75rem;
@@ -554,6 +558,7 @@ div.content-container {
   }
   }
 
 
   div.content {
   div.content {
+    background-color: $tab-content-bg;
     flex: 1;
     flex: 1;
   }
   }
 
 
@@ -898,6 +903,7 @@ div.card-overlay {
 
 
 // Tabbed content
 // Tabbed content
 .nav-tabs {
 .nav-tabs {
+  background-color: $body-bg;
   .nav-link {
   .nav-link {
     &:hover {
     &:hover {
       // Don't show a bottom-border on a hovered nav link because it overlaps with the .nav-tab border.
       // Don't show a bottom-border on a hovered nav link because it overlaps with the .nav-tab border.
@@ -919,14 +925,6 @@ div.card-overlay {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   padding: $spacer;
   padding: $spacer;
-  background-color: $tab-content-bg;
-  border-bottom: 1px solid $nav-tabs-border-color;
-
-  // Remove background and border when printing.
-  @media print {
-    background-color: var(--nbx-body-bg) !important;
-    border-bottom: none !important;
-  }
 }
 }
 
 
 // Override masonry-layout styles when printing.
 // Override masonry-layout styles when printing.

+ 0 - 89
netbox/project-static/styles/theme-base.scss

@@ -33,95 +33,6 @@ $darkest: #171b1d;
 
 
 @import '../node_modules/bootstrap/scss/variables';
 @import '../node_modules/bootstrap/scss/variables';
 
 
-// Make color palette colors available as theme colors.
-// For example, you could use `.bg-red-100`, if needed.
-$theme-color-addons: (
-  'darker': $darker,
-  'darkest': $darkest,
-  'gray': $gray-400,
-  'gray-100': $gray-100,
-  'gray-200': $gray-200,
-  'gray-300': $gray-300,
-  'gray-400': $gray-400,
-  'gray-500': $gray-500,
-  'gray-600': $gray-600,
-  'gray-700': $gray-700,
-  'gray-800': $gray-800,
-  'gray-900': $gray-900,
-  'red-100': $red-100,
-  'red-200': $red-200,
-  'red-300': $red-300,
-  'red-400': $red-400,
-  'red-500': $red-500,
-  'red-600': $red-600,
-  'red-700': $red-700,
-  'red-800': $red-800,
-  'red-900': $red-900,
-  'yellow-100': $yellow-100,
-  'yellow-200': $yellow-200,
-  'yellow-300': $yellow-300,
-  'yellow-400': $yellow-400,
-  'yellow-500': $yellow-500,
-  'yellow-600': $yellow-600,
-  'yellow-700': $yellow-700,
-  'yellow-800': $yellow-800,
-  'yellow-900': $yellow-900,
-  'green-100': $green-100,
-  'green-200': $green-200,
-  'green-300': $green-300,
-  'green-400': $green-400,
-  'green-500': $green-500,
-  'green-600': $green-600,
-  'green-700': $green-700,
-  'green-800': $green-800,
-  'green-900': $green-900,
-  'blue-100': $blue-100,
-  'blue-200': $blue-200,
-  'blue-300': $blue-300,
-  'blue-400': $blue-400,
-  'blue-500': $blue-500,
-  'blue-600': $blue-600,
-  'blue-700': $blue-700,
-  'blue-800': $blue-800,
-  'blue-900': $blue-900,
-  'cyan-100': $cyan-100,
-  'cyan-200': $cyan-200,
-  'cyan-300': $cyan-300,
-  'cyan-400': $cyan-400,
-  'cyan-500': $cyan-500,
-  'cyan-600': $cyan-600,
-  'cyan-700': $cyan-700,
-  'cyan-800': $cyan-800,
-  'cyan-900': $cyan-900,
-  'indigo-100': $indigo-100,
-  'indigo-200': $indigo-200,
-  'indigo-300': $indigo-300,
-  'indigo-400': $indigo-400,
-  'indigo-500': $indigo-500,
-  'indigo-600': $indigo-600,
-  'indigo-700': $indigo-700,
-  'indigo-800': $indigo-800,
-  'indigo-900': $indigo-900,
-  'purple-100': $purple-100,
-  'purple-200': $purple-200,
-  'purple-300': $purple-300,
-  'purple-400': $purple-400,
-  'purple-500': $purple-500,
-  'purple-600': $purple-600,
-  'purple-700': $purple-700,
-  'purple-800': $purple-800,
-  'purple-900': $purple-900,
-  'pink-100': $pink-100,
-  'pink-200': $pink-200,
-  'pink-300': $pink-300,
-  'pink-400': $pink-400,
-  'pink-500': $pink-500,
-  'pink-600': $pink-600,
-  'pink-700': $pink-700,
-  'pink-800': $pink-800,
-  'pink-900': $pink-900,
-);
-
 // This is the same value as the default from Bootstrap, but it needs to be in scope prior to
 // This is the same value as the default from Bootstrap, but it needs to be in scope prior to
 // importing _variables.scss from Bootstrap.
 // importing _variables.scss from Bootstrap.
 $btn-close-width: 1em;
 $btn-close-width: 1em;

+ 14 - 7
netbox/project-static/styles/theme-dark.scss

@@ -3,6 +3,7 @@
 @use 'sass:map';
 @use 'sass:map';
 @import './theme-base';
 @import './theme-base';
 
 
+// Theme colors (BS5 classes)
 $primary: $blue-300;
 $primary: $blue-300;
 $secondary: $gray-500;
 $secondary: $gray-500;
 $success: $green-300;
 $success: $green-300;
@@ -13,6 +14,7 @@ $light: $gray-300;
 $dark: $gray-500;
 $dark: $gray-500;
 
 
 $theme-colors: (
 $theme-colors: (
+  // BS5 theme colors
   'primary': $primary,
   'primary': $primary,
   'secondary': $secondary,
   'secondary': $secondary,
   'success': $success,
   'success': $success,
@@ -21,18 +23,23 @@ $theme-colors: (
   'danger': $danger,
   'danger': $danger,
   'light': $light,
   'light': $light,
   'dark': $dark,
   'dark': $dark,
-  'red': $red-300,
-  'yellow': $yellow-300,
-  'green': $green-300,
+
+  // General-purpose palette
   'blue': $blue-300,
   'blue': $blue-300,
-  'cyan': $cyan-300,
   'indigo': $indigo-300,
   'indigo': $indigo-300,
   'purple': $purple-300,
   'purple': $purple-300,
   'pink': $pink-300,
   'pink': $pink-300,
+  'red': $red-300,
+  'orange': $orange-300,
+  'yellow': $yellow-300,
+  'green': $green-300,
+  'teal': $teal-300,
+  'cyan': $cyan-300,
+  'gray': $gray-300,
+  'black': $black,
+  'white': $white,
 );
 );
 
 
-$theme-colors: map-merge($theme-colors, $theme-color-addons);
-
 // Gradient
 // Gradient
 $gradient: linear-gradient(180deg, rgba($white, 0.15), rgba($white, 0));
 $gradient: linear-gradient(180deg, rgba($white, 0.15), rgba($white, 0));
 
 
@@ -139,7 +146,7 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg
 $nav-pills-link-active-color: $component-active-color;
 $nav-pills-link-active-color: $component-active-color;
 $nav-pills-link-active-bg: $component-active-bg;
 $nav-pills-link-active-bg: $component-active-bg;
 
 
-$navbar-light-color: $gray-500;
+$navbar-light-color: $darkest;
 $navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
 $navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
 $navbar-light-toggler-border-color: $gray-700;
 $navbar-light-toggler-border-color: $gray-700;
 
 

+ 37 - 18
netbox/project-static/styles/theme-light.scss

@@ -2,28 +2,47 @@
 
 
 @import './theme-base.scss';
 @import './theme-base.scss';
 
 
-$input-border-color: $gray-200;
-
-$theme-colors: map-merge(
-  $theme-colors,
-  (
-    'primary': #337ab7,
-    'info': #54d6f0,
-    'red': $red-500,
-    'yellow': $yellow-500,
-    'green': $green-500,
-    'blue': $blue-500,
-    'cyan': $cyan-500,
-    'indigo': $indigo-500,
-    'purple': $purple-500,
-    'pink': $pink-500,
-  )
-);
+// Theme colors (BS5 classes)
+$primary: #337ab7;
+$secondary: $gray-600;
+$success: $green-500;
+$info: #54d6f0;
+$warning: $yellow-500;
+$danger: $red-500;
+$light: $gray-200;
+$dark: $gray-800;
 
 
-$theme-colors: map-merge($theme-colors, $theme-color-addons);
+$theme-colors: (
+  // BS5 theme colors
+  'primary': $primary,
+  'secondary': $secondary,
+  'success': $success,
+  'info': $info,
+  'warning': $warning,
+  'danger': $danger,
+  'light': $light,
+  'dark': $dark,
+
+  // General-purpose palette
+  'blue': $blue-500,
+  'indigo': $indigo-500,
+  'purple': $purple-500,
+  'pink': $pink-500,
+  'red': $red-500,
+  'orange': $orange-500,
+  'yellow': $yellow-500,
+  'green': $green-500,
+  'teal': $teal-500,
+  'cyan': $cyan-500,
+  'gray': $gray-500,
+  'black': $black,
+  'white': $white,
+);
 
 
 $light: $gray-200;
 $light: $gray-200;
 
 
+$navbar-light-color: $gray-100;
+
 $card-cap-color: $gray-800;
 $card-cap-color: $gray-800;
 
 
 $accordion-bg: transparent;
 $accordion-bg: transparent;

+ 4 - 1
netbox/templates/base/layout.html

@@ -20,7 +20,7 @@
         </div>
         </div>
 
 
         {# Top bar #}
         {# Top bar #}
-        <nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid border-bottom noprint">
+        <nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid noprint">
 
 
             {# Mobile Navigation #}
             {# Mobile Navigation #}
             <div class="nav-mobile">
             <div class="nav-mobile">
@@ -103,6 +103,9 @@
           </div>
           </div>
         {% endif %}
         {% endif %}
 
 
+        {# BS5 pop-up modals #}
+        {% block modals %}{% endblock %}
+
         {# Page footer #}
         {# Page footer #}
         <footer class="footer container-fluid">
         <footer class="footer container-fluid">
           <div class="row align-items-center justify-content-between mx-0">
           <div class="row align-items-center justify-content-between mx-0">

+ 4 - 1
netbox/templates/dcim/device/consoleports.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/consoleserverports.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/devicebays.html

@@ -39,5 +39,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/frontports.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/interfaces.html

@@ -77,5 +77,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/inventory.html

@@ -39,5 +39,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/poweroutlets.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/powerports.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/dcim/device/rearports.html

@@ -42,5 +42,8 @@
         {% endif %}
         {% endif %}
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 1 - 1
netbox/templates/dcim/devicebay_populate.html

@@ -4,7 +4,7 @@
 {% render_errors form %}
 {% render_errors form %}
 
 
 {% block content %}
 {% block content %}
-<form action="." method="post">
+<form action="" method="post">
     {% csrf_token %}
     {% csrf_token %}
     <div class="row mb-3">
     <div class="row mb-3">
         <div class="col col-md-6 offset-md-3">
         <div class="col col-md-6 offset-md-3">

+ 2 - 0
netbox/templates/dcim/rack_elevation_list.html

@@ -73,3 +73,5 @@
 
 
   </div>
   </div>
 {% endblock content-wrapper %}
 {% endblock content-wrapper %}
+
+{% block modals %}{% endblock %}

+ 35 - 26
netbox/templates/extras/report.html

@@ -10,7 +10,7 @@
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>
-{% endblock %}
+{% endblock breadcrumbs %}
 
 
 {% block subtitle %}
 {% block subtitle %}
   {% if report.description %}
   {% if report.description %}
@@ -18,33 +18,42 @@
       <div class="text-muted">{{ report.description|render_markdown }}</div>
       <div class="text-muted">{{ report.description|render_markdown }}</div>
     </div>
     </div>
   {% endif %}
   {% endif %}
-{% endblock %}
+{% endblock subtitle %}
 
 
 {% block controls %}{% endblock %}
 {% block controls %}{% endblock %}
-{% block tabs %}{% endblock %}
 
 
-{% block content-wrapper %}
-  {% if perms.extras.run_report %}
-    <div class="px-3 float-end noprint">
-        <form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
-            {% csrf_token %}
-            <button type="submit" name="_run" class="btn btn-primary">
-                {% if report.result %}
-                    <i class="mdi mdi-replay"></i> Run Again
-                {% else %}
-                    <i class="mdi mdi-play"></i> Run Report
-                {% endif %}
-            </button>
-        </form>
-    </div>
-  {% endif %}
-  <div class="row px-3">
-      <div class="col col-md-12">
-          {% if report.result %}
-              Last run: <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">
-                  <strong>{{ report.result.created|annotated_date }}</strong>
-              </a>
-          {% endif %}
+{% block tabs %}
+  <ul class="nav nav-tabs px-3">
+    <li class="nav-item" role="presentation">
+      <a href="#report" role="tab" data-bs-toggle="tab" class="nav-link active">Report</a>
+    </li>
+  </ul>
+{% endblock tabs %}
+
+{% block content %}
+  <div role="tabpanel" class="tab-pane active" id="report">
+    {% if perms.extras.run_report %}
+      <div class="float-end noprint">
+          <form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
+              {% csrf_token %}
+              <button type="submit" name="_run" class="btn btn-primary">
+                  {% if report.result %}
+                      <i class="mdi mdi-replay"></i> Run Again
+                  {% else %}
+                      <i class="mdi mdi-play"></i> Run Report
+                  {% endif %}
+              </button>
+          </form>
       </div>
       </div>
+    {% endif %}
+    <div class="row">
+        <div class="col col-md-12">
+            {% if report.result %}
+                Last run: <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">
+                    <strong>{{ report.result.created|annotated_date }}</strong>
+                </a>
+            {% endif %}
+        </div>
+    </div>
   </div>
   </div>
-{% endblock %}
+{% endblock content %}

+ 1 - 1
netbox/templates/extras/report_result.html

@@ -1,7 +1,7 @@
 {% extends 'extras/report.html' %}
 {% extends 'extras/report.html' %}
 
 
 {% block content-wrapper %}
 {% block content-wrapper %}
-  <div class="row px-3">
+  <div class="row p-3">
     <div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
     <div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
       {% include 'extras/htmx/report_result.html' %}
       {% include 'extras/htmx/report_result.html' %}
     </div>
     </div>

+ 47 - 49
netbox/templates/extras/script.html

@@ -7,69 +7,67 @@
 
 
 {% block object_identifier %}
 {% block object_identifier %}
   {{ script.full_name }}
   {{ script.full_name }}
-{% endblock %}
+{% endblock object_identifier %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
-{% endblock %}
+{% endblock breadcrumbs %}
 
 
 {% block subtitle %}
 {% block subtitle %}
   <div class="object-subtitle">
   <div class="object-subtitle">
     <div class="text-muted">{{ script.Meta.description|render_markdown }}</div>
     <div class="text-muted">{{ script.Meta.description|render_markdown }}</div>
   </div>
   </div>
-{% endblock %}
+{% endblock subtitle %}
 
 
 {% block controls %}{% endblock %}
 {% block controls %}{% endblock %}
 
 
 {% block tabs %}
 {% block tabs %}
-<ul class="nav nav-tabs px-3">
-  <li class="nav-item" role="presentation">
-    <a href="#run" role="tab" data-bs-toggle="tab" class="nav-link active">Run</a>
-  </li>
-  <li class="nav-item" role="presentation">
-    <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
-  </li>
-</ul>
-{% endblock %}
+  <ul class="nav nav-tabs px-3">
+    <li class="nav-item" role="presentation">
+      <a href="#run" role="tab" data-bs-toggle="tab" class="nav-link active">Run</a>
+    </li>
+    <li class="nav-item" role="presentation">
+      <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
+    </li>
+  </ul>
+{% endblock tabs %}
 
 
-{% block content-wrapper %}
-  <div class="tab-content">
-    <div role="tabpanel" class="tab-pane active" id="run">
-      <div class="row">
-        <div class="col">
-          {% if not perms.extras.run_script %}
-            <div class="alert alert-warning">
-              <i class="mdi mdi-alert"></i>
-              You do not have permission to run scripts.
-            </div>
-          {% endif %}
-          <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
-            {% csrf_token %}
-            <div class="field-group my-4">
-              {% if form.requires_input %}
-                <div class="row mb-2">
-                  <h5 class="offset-sm-3">Script Data</h5>
-                </div>
-              {% else %}
-                <div class="alert alert-info">
-                  <i class="mdi mdi-information"></i>
-                  This script does not require any input to run.
-                </div>
-              {% endif %}
-              {% render_form form %}
-            </div>
-            <div class="float-end">
-              <a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">Cancel</a>
-              <button type="submit" name="_run" class="btn btn-primary"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
-            </div>
-          </form>
-        </div>
+{% block content %}
+  <div role="tabpanel" class="tab-pane active" id="run">
+    <div class="row">
+      <div class="col">
+        {% if not perms.extras.run_script %}
+          <div class="alert alert-warning">
+            <i class="mdi mdi-alert"></i>
+            You do not have permission to run scripts.
+          </div>
+        {% endif %}
+        <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
+          {% csrf_token %}
+          <div class="field-group my-4">
+            {% if form.requires_input %}
+              <div class="row mb-2">
+                <h5 class="offset-sm-3">Script Data</h5>
+              </div>
+            {% else %}
+              <div class="alert alert-info">
+                <i class="mdi mdi-information"></i>
+                This script does not require any input to run.
+              </div>
+            {% endif %}
+            {% render_form form %}
+          </div>
+          <div class="float-end">
+            <a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">Cancel</a>
+            <button type="submit" name="_run" class="btn btn-primary"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
+          </div>
+        </form>
       </div>
       </div>
     </div>
     </div>
-    <div role="tabpanel" class="tab-pane" id="source">
-      <code class="h6 my-3 d-block">{{ script.filename }}</code>
-      <pre class="block">{{ script.source }}</pre>
-    </div>
   </div>
   </div>
-{% endblock content-wrapper %}
+  <div role="tabpanel" class="tab-pane" id="source">
+    <code class="h6 my-3 d-block">{{ script.filename }}</code>
+    <pre class="block">{{ script.source }}</pre>
+  </div>
+{% endblock content %}

+ 5 - 1
netbox/templates/generic/object.html

@@ -100,4 +100,8 @@
   <div class="tab-content">
   <div class="tab-content">
     {% block content %}{% endblock %}
     {% block content %}{% endblock %}
   </div>
   </div>
-{% endblock %}
+{% endblock content-wrapper %}
+
+{% block modals %}
+  {% include 'inc/htmx_modal.html' %}
+{% endblock modals %}

+ 13 - 6
netbox/templates/generic/object_delete.html

@@ -1,9 +1,16 @@
-{% extends 'generic/confirmation_form.html' %}
+{% extends 'base/layout.html' %}
 {% load form_helpers %}
 {% load form_helpers %}
 
 
-{% block title %}Delete {{ obj_type }}?{% endblock %}
+{% block title %}Delete {{ object_type }}?{% endblock %}
 
 
-{% block message %}
-  <p>Are you sure you want to <strong class="text-danger">delete</strong> {{ obj_type }} <strong>{{ obj }}</strong>?</p>
-  {% block message_extra %}{% endblock %}
-{% endblock message %}
+{% block header %}{% endblock %}
+
+{% block content %}
+  <div class="modal" tabindex="-1" style="display: block; position: static">
+    <div class="modal-dialog">
+      <div class="modal-content" >
+        {% include 'htmx/delete_form.html' %}
+      </div>
+    </div>
+  </div>
+{% endblock %}

+ 4 - 2
netbox/templates/generic/object_list.html

@@ -133,6 +133,8 @@
     {% endif %}
     {% endif %}
   </div>
   </div>
 
 
-  {# Table config form #}
-  {% table_config_form table table_name="ObjectTable" %}
 {% endblock content-wrapper %}
 {% endblock content-wrapper %}
+
+{% block modals %}
+  {% table_config_form table table_name="ObjectTable" %}
+{% endblock modals %}

+ 20 - 0
netbox/templates/htmx/delete_form.html

@@ -0,0 +1,20 @@
+{% load form_helpers %}
+
+<form action="{{ form_url }}" method="post">
+  {% csrf_token %}
+  <div class="modal-header">
+    <h5 class="modal-title">Confirm Deletion</h5>
+  </div>
+  <div class="modal-body">
+    <p>Are you sure you want to <strong class="text-danger">delete</strong> {{ object_type }} <strong>{{ object }}</strong>?</p>
+    {% render_form form %}
+  </div>
+  <div class="modal-footer">
+    {% if return_url %}
+      <a href="{{ return_url }}" class="btn btn-outline-secondary">Cancel</a>
+    {% else %}
+      <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
+    {% endif %}
+    <button type="submit" class="btn btn-danger">Delete</button>
+  </div>
+</form>

+ 7 - 0
netbox/templates/inc/htmx_modal.html

@@ -0,0 +1,7 @@
+<div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
+  <div class="modal-dialog modal-dialog-centered">
+    <div class="modal-content" id="htmx-modal-content">
+      {# Dynamic content goes here #}
+    </div>
+  </div>
+</div>

+ 4 - 1
netbox/templates/ipam/aggregate/prefixes.html

@@ -37,5 +37,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/ipam/iprange/ip_addresses.html

@@ -35,5 +35,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/ipam/prefix/ip_addresses.html

@@ -35,5 +35,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/ipam/prefix/ip_ranges.html

@@ -35,5 +35,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/ipam/prefix/prefixes.html

@@ -37,5 +37,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
-  {% table_config_form table %}
 {% endblock %}
 {% endblock %}
+
+{% block modals %}
+  {% table_config_form table %}
+{% endblock modals %}

+ 0 - 5
netbox/templates/ipam/prefix_delete.html

@@ -1,5 +0,0 @@
-{% extends 'generic/object_delete.html' %}
-
-{% block message_extra %}
-    <p>Note: This will <strong>not</strong> delete any child prefixes or IP addresses.</p>
-{% endblock %}

+ 0 - 6
netbox/templates/ipam/vlan/base.html

@@ -37,9 +37,3 @@
     {% endif %}
     {% endif %}
   </ul>
   </ul>
 {% endblock %}
 {% endblock %}
-
-{% block content-wrapper %}
-  <div class="tab-content">
-    {% block content %}{% endblock %}
-  </div>
-{% endblock %}

+ 4 - 3
netbox/templates/ipam/vlan/interfaces.html

@@ -5,13 +5,14 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
-
     <div class="card">
     <div class="card">
       <div class="card-body" id="object_list">
       <div class="card-body" id="object_list">
         {% include 'htmx/table.html' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>
-
   </form>
   </form>
+{% endblock content %}
+
+{% block modals %}
   {% table_config_form table %}
   {% table_config_form table %}
-{% endblock %}
+{% endblock modals %}

+ 4 - 3
netbox/templates/ipam/vlan/vminterfaces.html

@@ -5,13 +5,14 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
-
     <div class="card">
     <div class="card">
       <div class="card-body" id="object_list">
       <div class="card-body" id="object_list">
         {% include 'htmx/table.html' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>
-
   </form>
   </form>
+{% endblock content %}
+
+{% block modals %}
   {% table_config_form table %}
   {% table_config_form table %}
-{% endblock %}
+{% endblock modals %}

+ 4 - 3
netbox/templates/virtualization/cluster/devices.html

@@ -6,13 +6,11 @@
   <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
   <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
     {% csrf_token %}
     {% csrf_token %}
     {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
     {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
-
     <div class="card">
     <div class="card">
       <div class="card-body" id="object_list">
       <div class="card-body" id="object_list">
         {% include 'htmx/table.html' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>
-
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
         {% if perms.virtualization.change_cluster %}
         {% if perms.virtualization.change_cluster %}
@@ -23,5 +21,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
+{% endblock content %}
+
+{% block modals %}
   {% table_config_form table %}
   {% table_config_form table %}
-{% endblock %}
+{% endblock modals %}

+ 4 - 3
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -6,13 +6,11 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
     {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
-
     <div class="card">
     <div class="card">
       <div class="card-body" id="object_list">
       <div class="card-body" id="object_list">
         {% include 'htmx/table.html' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>
-
     <div class="noprint bulk-buttons">
     <div class="noprint bulk-buttons">
       <div class="bulk-button-group">
       <div class="bulk-button-group">
         {% if perms.virtualization.change_virtualmachine %}
         {% if perms.virtualization.change_virtualmachine %}
@@ -28,5 +26,8 @@
       </div>
       </div>
     </div>
     </div>
   </form>
   </form>
+{% endblock content %}
+
+{% block modals %}
   {% table_config_form table %}
   {% table_config_form table %}
-{% endblock %}
+{% endblock modals %}

+ 4 - 1
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -37,5 +37,8 @@
         <div class="clearfix"></div>
         <div class="clearfix"></div>
      </div>
      </div>
   </form>
   </form>
+{% endblock content %}
+
+{% block modals %}
   {% table_config_form table %}
   {% table_config_form table %}
-{% endblock %}
+{% endblock modals %}

+ 2 - 2
netbox/utilities/forms/fields.py

@@ -168,11 +168,11 @@ class ContentTypeChoiceMixin:
 
 
 
 
 class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField):
 class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField):
-    pass
+    widget = widgets.StaticSelect
 
 
 
 
 class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField):
 class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField):
-    pass
+    widget = widgets.StaticSelectMultiple
 
 
 
 
 #
 #

+ 0 - 10
netbox/utilities/forms/widgets.py

@@ -14,7 +14,6 @@ __all__ = (
     'BulkEditNullBooleanSelect',
     'BulkEditNullBooleanSelect',
     'ClearableFileInput',
     'ClearableFileInput',
     'ColorSelect',
     'ColorSelect',
-    'ContentTypeSelect',
     'DatePicker',
     'DatePicker',
     'DateTimePicker',
     'DateTimePicker',
     'NumericArrayField',
     'NumericArrayField',
@@ -110,15 +109,6 @@ class SelectWithPK(StaticSelect):
     option_template_name = 'widgets/select_option_with_pk.html'
     option_template_name = 'widgets/select_option_with_pk.html'
 
 
 
 
-class ContentTypeSelect(StaticSelect):
-    """
-    Appends an `api-value` attribute equal to the slugified model name for each ContentType. For example:
-        <option value="37" api-value="console-server-port">console server port</option>
-    This attribute can be used to reference the relevant API endpoint for a particular ContentType.
-    """
-    option_template_name = 'widgets/select_contenttype.html'
-
-
 class SelectSpeedWidget(forms.NumberInput):
 class SelectSpeedWidget(forms.NumberInput):
     """
     """
     Speed field with dropdown selections for convenience.
     Speed field with dropdown selections for convenience.

+ 8 - 2
netbox/utilities/templates/buttons/delete.html

@@ -1,3 +1,9 @@
-<a href="{{ url }}" class="btn btn-sm btn-danger" role="button">
-    <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>&nbsp;Delete
+<a href="#"
+  hx-get="{{ url }}"
+  hx-target="#htmx-modal-content"
+  class="btn btn-sm btn-danger"
+  data-bs-toggle="modal"
+  data-bs-target="#htmx-modal"
+>
+  <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>&nbsp;Delete
 </a>
 </a>

+ 0 - 1
netbox/utilities/templates/widgets/select_contenttype.html

@@ -1 +0,0 @@
-<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}{% if widget.value %} api-value="{{ widget.label|slugify }}"{% endif %}>{{ widget.label.label|default:widget.label|capfirst }}</option>

+ 6 - 0
netbox/virtualization/tables.py

@@ -80,6 +80,12 @@ class ClusterTable(BaseTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
+    type = tables.Column(
+        linkify=True
+    )
+    group = tables.Column(
+        linkify=True
+    )
     tenant = tables.Column(
     tenant = tables.Column(
         linkify=True
         linkify=True
     )
     )

+ 15 - 10
netbox/wireless/forms/bulk_edit.py

@@ -3,7 +3,7 @@ from django import forms
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
 from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
 from ipam.models import VLAN
 from ipam.models import VLAN
-from utilities.forms import DynamicModelChoiceField
+from utilities.forms import add_blank_choice, DynamicModelChoiceField
 from wireless.choices import *
 from wireless.choices import *
 from wireless.constants import SSID_MAX_LENGTH
 from wireless.constants import SSID_MAX_LENGTH
 from wireless.models import *
 from wireless.models import *
@@ -45,24 +45,27 @@ class WirelessLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
     vlan = DynamicModelChoiceField(
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False,
         required=False,
+        label='VLAN'
     )
     )
     ssid = forms.CharField(
     ssid = forms.CharField(
         max_length=SSID_MAX_LENGTH,
         max_length=SSID_MAX_LENGTH,
-        required=False
+        required=False,
+        label='SSID'
     )
     )
     description = forms.CharField(
     description = forms.CharField(
         required=False
         required=False
     )
     )
     auth_type = forms.ChoiceField(
     auth_type = forms.ChoiceField(
-        choices=WirelessAuthTypeChoices,
+        choices=add_blank_choice(WirelessAuthTypeChoices),
         required=False
         required=False
     )
     )
     auth_cipher = forms.ChoiceField(
     auth_cipher = forms.ChoiceField(
-        choices=WirelessAuthCipherChoices,
+        choices=add_blank_choice(WirelessAuthCipherChoices),
         required=False
         required=False
     )
     )
     auth_psk = forms.CharField(
     auth_psk = forms.CharField(
-        required=False
+        required=False,
+        label='Pre-shared key'
     )
     )
 
 
     class Meta:
     class Meta:
@@ -76,25 +79,27 @@ class WirelessLinkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
     )
     )
     ssid = forms.CharField(
     ssid = forms.CharField(
         max_length=SSID_MAX_LENGTH,
         max_length=SSID_MAX_LENGTH,
-        required=False
+        required=False,
+        label='SSID'
     )
     )
     status = forms.ChoiceField(
     status = forms.ChoiceField(
-        choices=LinkStatusChoices,
+        choices=add_blank_choice(LinkStatusChoices),
         required=False
         required=False
     )
     )
     description = forms.CharField(
     description = forms.CharField(
         required=False
         required=False
     )
     )
     auth_type = forms.ChoiceField(
     auth_type = forms.ChoiceField(
-        choices=WirelessAuthTypeChoices,
+        choices=add_blank_choice(WirelessAuthTypeChoices),
         required=False
         required=False
     )
     )
     auth_cipher = forms.ChoiceField(
     auth_cipher = forms.ChoiceField(
-        choices=WirelessAuthCipherChoices,
+        choices=add_blank_choice(WirelessAuthCipherChoices),
         required=False
         required=False
     )
     )
     auth_psk = forms.CharField(
     auth_psk = forms.CharField(
-        required=False
+        required=False,
+        label='Pre-shared key'
     )
     )
 
 
     class Meta:
     class Meta:

+ 2 - 2
requirements.txt

@@ -1,4 +1,4 @@
-Django==3.2.10
+Django==3.2.11
 django-cors-headers==3.10.1
 django-cors-headers==3.10.1
 django-debug-toolbar==3.2.4
 django-debug-toolbar==3.2.4
 django-filter==21.1
 django-filter==21.1
@@ -18,7 +18,7 @@ gunicorn==20.1.0
 Jinja2==3.0.3
 Jinja2==3.0.3
 Markdown==3.3.6
 Markdown==3.3.6
 markdown-include==0.6.0
 markdown-include==0.6.0
-mkdocs-material==8.1.3
+mkdocs-material==8.1.4
 netaddr==0.8.0
 netaddr==0.8.0
 Pillow==8.4.0
 Pillow==8.4.0
 psycopg2-binary==2.9.3
 psycopg2-binary==2.9.3

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików