Kaynağa Gözat

Merge branch 'develop' into feature

jeremystretch 3 yıl önce
ebeveyn
işleme
abf11fbcb8
39 değiştirilmiş dosya ile 410 ekleme ve 436 silme
  1. 1 1
      docs/administration/authentication/overview.md
  2. 1 1
      docs/models/extras/webhook.md
  3. 22 1
      docs/release-notes/version-3.2.md
  4. 7 6
      netbox/dcim/api/serializers.py
  5. 3 2
      netbox/dcim/api/views.py
  6. 6 3
      netbox/dcim/filtersets.py
  7. 2 2
      netbox/dcim/forms/filtersets.py
  8. 2 1
      netbox/dcim/svg/racks.py
  9. 11 9
      netbox/dcim/tests/test_filtersets.py
  10. 3 2
      netbox/extras/api/customfields.py
  11. 4 3
      netbox/extras/api/serializers.py
  12. 5 4
      netbox/ipam/api/serializers.py
  13. 2 1
      netbox/netbox/api/viewsets/__init__.py
  14. 3 254
      netbox/netbox/constants.py
  15. 1 1
      netbox/netbox/filtersets.py
  16. 1 1
      netbox/netbox/forms/__init__.py
  17. 261 0
      netbox/netbox/search.py
  18. 8 7
      netbox/netbox/settings.py
  19. 2 1
      netbox/netbox/views/__init__.py
  20. 0 0
      netbox/project-static/dist/netbox.js
  21. 0 0
      netbox/project-static/dist/netbox.js.map
  22. 3 1
      netbox/project-static/src/buttons/reslug.ts
  23. 1 39
      netbox/project-static/src/forms/elements.ts
  24. 0 1
      netbox/project-static/src/select/api/apiSelect.ts
  25. 0 14
      netbox/templates/circuits/circuittermination_edit.html
  26. 0 11
      netbox/templates/dcim/interface_edit.html
  27. 1 1
      netbox/templates/generic/bulk_delete.html
  28. 1 1
      netbox/templates/generic/bulk_edit.html
  29. 6 6
      netbox/templates/generic/bulk_import.html
  30. 1 1
      netbox/templates/generic/bulk_remove.html
  31. 1 1
      netbox/templates/generic/bulk_rename.html
  32. 18 27
      netbox/templates/generic/confirmation_form.html
  33. 4 4
      netbox/templates/generic/object_edit.html
  34. 15 15
      netbox/templates/generic/object_import.html
  35. 0 11
      netbox/templates/virtualization/vminterface_edit.html
  36. 2 1
      netbox/tenancy/api/serializers.py
  37. 8 2
      netbox/users/views.py
  38. 2 0
      netbox/utilities/templatetags/builtins/filters.py
  39. 2 0
      netbox/virtualization/views.py

+ 1 - 1
docs/administration/authentication/overview.md

@@ -34,4 +34,4 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
 
 NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
 
-Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter.
+Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.)

+ 1 - 1
docs/models/extras/webhook.md

@@ -43,7 +43,7 @@ The following data is available as context for Jinja2 templates:
 * `username` - The name of the user account associated with the change.
 * `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
 * `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
-* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
+* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
 
 ### Default Request Body
 

+ 22 - 1
docs/release-notes/version-3.2.md

@@ -1,6 +1,27 @@
 # NetBox v3.2
 
-## v3.2.7 (FUTURE)
+## v3.2.8 (FUTURE)
+
+---
+
+## v3.2.7 (2022-07-20)
+
+### Enhancements
+
+* [#9705](https://github.com/netbox-community/netbox/issues/9705) - Support filter expressions for the `serial` field on racks, devices, and inventory items
+* [#9741](https://github.com/netbox-community/netbox/issues/9741) - Check for UserConfig instance during user login
+* [#9745](https://github.com/netbox-community/netbox/issues/9745) - Add wireless LANs and links to global search
+
+### Bug Fixes
+
+* [#9437](https://github.com/netbox-community/netbox/issues/9437) - Standardize form submission buttons and behavior when using enter key
+* [#9499](https://github.com/netbox-community/netbox/issues/9499) - Fix filtered bulk deletion of VM Interfaces
+* [#9634](https://github.com/netbox-community/netbox/issues/9634) - Fix image URLs in rack elevations when using external storage
+* [#9715](https://github.com/netbox-community/netbox/issues/9715) - Fix `SOCIAL_AUTH_PIPELINE` config parameter not taking effect
+* [#9754](https://github.com/netbox-community/netbox/issues/9754) - Fix regression introduced by #9632
+* [#9746](https://github.com/netbox-community/netbox/issues/9746) - Permit filtering interfaces by arbitrary speed value in UI
+* [#9749](https://github.com/netbox-community/netbox/issues/9749) - Retain original slug values when modifying object names
+* [#9775](https://github.com/netbox-community/netbox/issues/9775) - Fix exception when viewing a report with no description
 
 ---
 

+ 7 - 6
netbox/dcim/api/serializers.py

@@ -19,6 +19,7 @@ from netbox.api.serializers import (
     WritableNestedSerializer,
 )
 from netbox.config import ConfigItem
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from users.api.nested_serializers import NestedUserSerializer
 from utilities.api import get_serializer_for_model
@@ -57,7 +58,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
             return []
 
         # Return serialized peer termination objects
-        serializer = get_serializer_for_model(obj.link_peers[0], prefix='Nested')
+        serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.link_peers, context=context, many=True).data
 
@@ -84,7 +85,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
         Return the appropriate serializer for the type of connected object.
         """
         if endpoints := obj.connected_endpoints:
-            serializer = get_serializer_for_model(endpoints[0], prefix='Nested')
+            serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX)
             context = {'request': self.context['request']}
             return serializer(endpoints, many=True, context=context).data
 
@@ -572,7 +573,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
     def get_component(self, obj):
         if obj.component is None:
             return None
-        serializer = get_serializer_for_model(obj.component, prefix='Nested')
+        serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.component, context=context).data
 
@@ -968,7 +969,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
     def get_component(self, obj):
         if obj.component is None:
             return None
-        serializer = get_serializer_for_model(obj.component, prefix='Nested')
+        serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.component, context=context).data
 
@@ -1037,7 +1038,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_termination(self, obj):
-        serializer = get_serializer_for_model(obj.termination, prefix='Nested')
+        serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.termination, context=context).data
 
@@ -1053,7 +1054,7 @@ class CablePathSerializer(serializers.ModelSerializer):
     def get_path(self, obj):
         ret = []
         for nodes in obj.path_objects:
-            serializer = get_serializer_for_model(nodes[0], prefix='Nested')
+            serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX)
             context = {'request': self.context['request']}
             ret.append(serializer(nodes, context=context, many=True).data)
         return ret

+ 3 - 2
netbox/dcim/api/views.py

@@ -24,6 +24,7 @@ from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.pagination import StripCountAnnotationsPaginator
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.config import get_config
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from utilities.api import get_serializer_for_model
 from utilities.utils import count_related
 from virtualization.models import VirtualMachine
@@ -65,7 +66,7 @@ class PathEndpointMixin(object):
         # Serialize path objects, iterating over each three-tuple in the path
         for near_end, cable, far_end in obj.trace():
             if near_end is not None:
-                serializer_a = get_serializer_for_model(near_end[0], prefix='Nested')
+                serializer_a = get_serializer_for_model(near_end[0], prefix=NESTED_SERIALIZER_PREFIX)
                 near_end = serializer_a(near_end, many=True, context={'request': request}).data
             else:
                 # Path is split; stop here
@@ -73,7 +74,7 @@ class PathEndpointMixin(object):
             if cable is not None:
                 cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
             if far_end is not None:
-                serializer_b = get_serializer_for_model(far_end[0], prefix='Nested')
+                serializer_b = get_serializer_for_model(far_end[0], prefix=NESTED_SERIALIZER_PREFIX)
                 far_end = serializer_b(far_end, many=True, context={'request': request}).data
 
             path.append((near_end, cable, far_end))

+ 6 - 3
netbox/dcim/filtersets.py

@@ -312,7 +312,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
         to_field_name='slug',
         label='Role (slug)',
     )
-    serial = django_filters.CharFilter(
+    serial = MultiValueCharFilter(
         lookup_expr='iexact'
     )
 
@@ -1007,10 +1007,13 @@ class ModuleFilterSet(NetBoxModelFilterSet):
         queryset=Device.objects.all(),
         label='Device (ID)',
     )
+    serial = MultiValueCharFilter(
+        lookup_expr='iexact'
+    )
 
     class Meta:
         model = Module
-        fields = ['id', 'serial', 'asset_tag']
+        fields = ['id', 'asset_tag']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -1411,7 +1414,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
     )
     component_type = ContentTypeFilter()
     component_id = MultiValueNumberFilter()
-    serial = django_filters.CharFilter(
+    serial = MultiValueCharFilter(
         lookup_expr='iexact'
     )
 

+ 2 - 2
netbox/dcim/forms/filtersets.py

@@ -998,8 +998,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     )
     speed = forms.IntegerField(
         required=False,
-        label='Select Speed',
-        widget=SelectSpeedWidget(attrs={'readonly': None})
+        label='Speed',
+        widget=SelectSpeedWidget()
     )
     duplex = MultipleChoiceField(
         choices=InterfaceDuplexChoices,

+ 2 - 1
netbox/dcim/svg/racks.py

@@ -163,8 +163,9 @@ class RackElevationSVG:
 
         # Embed device type image if provided
         if self.include_images and image:
+            url = f'{self.base_url}{image.url}' if image.url.startswith('/') else image.url
             image = Image(
-                href=f'{self.base_url}{image.url}',
+                href=url,
                 insert=coords,
                 size=size,
                 class_=f'device-image{css_extra}'

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

@@ -498,10 +498,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_serial(self):
-        params = {'serial': 'ABC'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-        params = {'serial': 'abc'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'serial': ['ABC', 'DEF']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'serial': ['abc', 'def']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_tenant(self):
         tenants = Tenant.objects.all()[:2]
@@ -1864,7 +1864,9 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
 
     def test_serial(self):
-        params = {'asset_tag': ['A', 'B']}
+        params = {'serial': ['A', 'B']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'serial': ['a', 'b']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_asset_tag(self):
@@ -3520,10 +3522,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_serial(self):
-        params = {'serial': 'ABC'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-        params = {'serial': 'abc'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'serial': ['ABC', 'DEF']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'serial': ['abc', 'def']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_component_type(self):
         params = {'component_type': 'dcim.interface'}

+ 3 - 2
netbox/extras/api/customfields.py

@@ -3,6 +3,7 @@ from rest_framework.fields import Field
 
 from extras.choices import CustomFieldTypeChoices
 from extras.models import CustomField
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 
 
 #
@@ -51,10 +52,10 @@ class CustomFieldsDataField(Field):
         for cf in self._get_custom_fields():
             value = cf.deserialize(obj.get(cf.name))
             if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
-                serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
+                serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
                 value = serializer(value, context=self.parent.context).data
             elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
-                serializer = get_serializer_for_model(cf.object_type.model_class(), prefix='Nested')
+                serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
                 value = serializer(value, many=True, context=self.parent.context).data
             data[cf.name] = value
 

+ 4 - 3
netbox/extras/api/serializers.py

@@ -15,6 +15,7 @@ from extras.utils import FeatureQuery
 from netbox.api.exceptions import SerializerNotFound
 from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
 from users.api.nested_serializers import NestedUserSerializer
@@ -193,7 +194,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_parent(self, obj):
-        serializer = get_serializer_for_model(obj.parent, prefix='Nested')
+        serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
         return serializer(obj.parent, context={'request': self.context['request']}).data
 
 
@@ -243,7 +244,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_assigned_object(self, instance):
-        serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix='Nested')
+        serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(instance.assigned_object, context=context).data
 
@@ -469,7 +470,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
             return None
 
         try:
-            serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
+            serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
         except SerializerNotFound:
             return obj.object_repr
         context = {

+ 5 - 4
netbox/ipam/api/serializers.py

@@ -10,6 +10,7 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.models import *
 from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import get_serializer_for_model
 from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
@@ -148,7 +149,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
     def get_interface(self, obj):
         if obj.interface is None:
             return None
-        serializer = get_serializer_for_model(obj.interface, prefix='Nested')
+        serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.interface, context=context).data
 
@@ -194,7 +195,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
     def get_scope(self, obj):
         if obj.scope_id is None:
             return None
-        serializer = get_serializer_for_model(obj.scope, prefix='Nested')
+        serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
 
         return serializer(obj.scope, context=context).data
@@ -378,7 +379,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
     def get_assigned_object(self, obj):
         if obj.assigned_object is None:
             return None
-        serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
+        serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(obj.assigned_object, context=context).data
 
@@ -485,6 +486,6 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_assigned_object(self, instance):
-        serializer = get_serializer_for_model(instance.assigned_object, prefix='Nested')
+        serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(instance.assigned_object, context=context).data

+ 2 - 1
netbox/netbox/api/viewsets/__init__.py

@@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
 
 from extras.models import ExportTemplate
 from netbox.api.exceptions import SerializerNotFound
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from utilities.api import get_serializer_for_model
 from utilities.exceptions import AbortRequest
 from .mixins import *
@@ -61,7 +62,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
         if self.brief:
             logger.debug("Request is for 'brief' format; initializing nested serializer")
             try:
-                serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
+                serializer = get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)
                 logger.debug(f"Using serializer {serializer}")
                 return serializer
             except SerializerNotFound:

+ 3 - 254
netbox/netbox/constants.py

@@ -1,256 +1,5 @@
-from collections import OrderedDict
-from typing import Dict
-
-import circuits.filtersets
-import circuits.tables
-import dcim.filtersets
-import dcim.tables
-import ipam.filtersets
-import ipam.tables
-import tenancy.filtersets
-import tenancy.tables
-import virtualization.filtersets
-import virtualization.tables
-from circuits.models import Circuit, ProviderNetwork, Provider
-from dcim.models import (
-    Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis,
-)
-from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
-from tenancy.models import Contact, Tenant, ContactAssignment
-from utilities.utils import count_related
-from virtualization.models import Cluster, VirtualMachine
+# Prefix for nested serializers
+NESTED_SERIALIZER_PREFIX = 'Nested'
 
+# Max results per object type
 SEARCH_MAX_RESULTS = 15
-
-CIRCUIT_TYPES = OrderedDict(
-    (
-        ('provider', {
-            'queryset': Provider.objects.annotate(
-                count_circuits=count_related(Circuit, 'provider')
-            ),
-            'filterset': circuits.filtersets.ProviderFilterSet,
-            'table': circuits.tables.ProviderTable,
-            'url': 'circuits:provider_list',
-        }),
-        ('circuit', {
-            'queryset': Circuit.objects.prefetch_related(
-                'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
-            ),
-            'filterset': circuits.filtersets.CircuitFilterSet,
-            'table': circuits.tables.CircuitTable,
-            'url': 'circuits:circuit_list',
-        }),
-        ('providernetwork', {
-            'queryset': ProviderNetwork.objects.prefetch_related('provider'),
-            'filterset': circuits.filtersets.ProviderNetworkFilterSet,
-            'table': circuits.tables.ProviderNetworkTable,
-            'url': 'circuits:providernetwork_list',
-        }),
-    )
-)
-
-
-DCIM_TYPES = OrderedDict(
-    (
-        ('site', {
-            'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
-            'filterset': dcim.filtersets.SiteFilterSet,
-            'table': dcim.tables.SiteTable,
-            'url': 'dcim:site_list',
-        }),
-        ('rack', {
-            'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
-                device_count=count_related(Device, 'rack')
-            ),
-            'filterset': dcim.filtersets.RackFilterSet,
-            'table': dcim.tables.RackTable,
-            'url': 'dcim:rack_list',
-        }),
-        ('rackreservation', {
-            'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
-            'filterset': dcim.filtersets.RackReservationFilterSet,
-            'table': dcim.tables.RackReservationTable,
-            'url': 'dcim:rackreservation_list',
-        }),
-        ('location', {
-            'queryset': Location.objects.add_related_count(
-                Location.objects.add_related_count(
-                    Location.objects.all(),
-                    Device,
-                    'location',
-                    'device_count',
-                    cumulative=True
-                ),
-                Rack,
-                'location',
-                'rack_count',
-                cumulative=True
-            ).prefetch_related('site'),
-            'filterset': dcim.filtersets.LocationFilterSet,
-            'table': dcim.tables.LocationTable,
-            'url': 'dcim:location_list',
-        }),
-        ('devicetype', {
-            'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
-                instance_count=count_related(Device, 'device_type')
-            ),
-            'filterset': dcim.filtersets.DeviceTypeFilterSet,
-            'table': dcim.tables.DeviceTypeTable,
-            'url': 'dcim:devicetype_list',
-        }),
-        ('device', {
-            'queryset': Device.objects.prefetch_related(
-                'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
-            ),
-            'filterset': dcim.filtersets.DeviceFilterSet,
-            'table': dcim.tables.DeviceTable,
-            'url': 'dcim:device_list',
-        }),
-        ('moduletype', {
-            'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
-                instance_count=count_related(Module, 'module_type')
-            ),
-            'filterset': dcim.filtersets.ModuleTypeFilterSet,
-            'table': dcim.tables.ModuleTypeTable,
-            'url': 'dcim:moduletype_list',
-        }),
-        ('module', {
-            'queryset': Module.objects.prefetch_related(
-                'module_type__manufacturer', 'device', 'module_bay',
-            ),
-            'filterset': dcim.filtersets.ModuleFilterSet,
-            'table': dcim.tables.ModuleTable,
-            'url': 'dcim:module_list',
-        }),
-        ('virtualchassis', {
-            'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
-                member_count=count_related(Device, 'virtual_chassis')
-            ),
-            'filterset': dcim.filtersets.VirtualChassisFilterSet,
-            'table': dcim.tables.VirtualChassisTable,
-            'url': 'dcim:virtualchassis_list',
-        }),
-        ('cable', {
-            'queryset': Cable.objects.all(),
-            'filterset': dcim.filtersets.CableFilterSet,
-            'table': dcim.tables.CableTable,
-            'url': 'dcim:cable_list',
-        }),
-        ('powerfeed', {
-            'queryset': PowerFeed.objects.all(),
-            'filterset': dcim.filtersets.PowerFeedFilterSet,
-            'table': dcim.tables.PowerFeedTable,
-            'url': 'dcim:powerfeed_list',
-        }),
-    )
-)
-
-IPAM_TYPES = OrderedDict(
-    (
-        ('vrf', {
-            'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
-            'filterset': ipam.filtersets.VRFFilterSet,
-            'table': ipam.tables.VRFTable,
-            'url': 'ipam:vrf_list',
-        }),
-        ('aggregate', {
-            'queryset': Aggregate.objects.prefetch_related('rir'),
-            'filterset': ipam.filtersets.AggregateFilterSet,
-            'table': ipam.tables.AggregateTable,
-            'url': 'ipam:aggregate_list',
-        }),
-        ('prefix', {
-            'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
-            'filterset': ipam.filtersets.PrefixFilterSet,
-            'table': ipam.tables.PrefixTable,
-            'url': 'ipam:prefix_list',
-        }),
-        ('ipaddress', {
-            'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
-            'filterset': ipam.filtersets.IPAddressFilterSet,
-            'table': ipam.tables.IPAddressTable,
-            'url': 'ipam:ipaddress_list',
-        }),
-        ('vlan', {
-            'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
-            'filterset': ipam.filtersets.VLANFilterSet,
-            'table': ipam.tables.VLANTable,
-            'url': 'ipam:vlan_list',
-        }),
-        ('asn', {
-            'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
-            'filterset': ipam.filtersets.ASNFilterSet,
-            'table': ipam.tables.ASNTable,
-            'url': 'ipam:asn_list',
-        }),
-        ('service', {
-            'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
-            'filterset': ipam.filtersets.ServiceFilterSet,
-            'table': ipam.tables.ServiceTable,
-            'url': 'ipam:service_list',
-        }),
-    )
-)
-
-TENANCY_TYPES = OrderedDict(
-    (
-        ('tenant', {
-            'queryset': Tenant.objects.prefetch_related('group'),
-            'filterset': tenancy.filtersets.TenantFilterSet,
-            'table': tenancy.tables.TenantTable,
-            'url': 'tenancy:tenant_list',
-        }),
-        ('contact', {
-            'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
-                assignment_count=count_related(ContactAssignment, 'contact')),
-            'filterset': tenancy.filtersets.ContactFilterSet,
-            'table': tenancy.tables.ContactTable,
-            'url': 'tenancy:contact_list',
-        }),
-    )
-)
-
-VIRTUALIZATION_TYPES = OrderedDict(
-    (
-        ('cluster', {
-            'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
-                device_count=count_related(Device, 'cluster'),
-                vm_count=count_related(VirtualMachine, 'cluster')
-            ),
-            'filterset': virtualization.filtersets.ClusterFilterSet,
-            'table': virtualization.tables.ClusterTable,
-            'url': 'virtualization:cluster_list',
-        }),
-        ('virtualmachine', {
-            'queryset': VirtualMachine.objects.prefetch_related(
-                'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
-            ),
-            'filterset': virtualization.filtersets.VirtualMachineFilterSet,
-            'table': virtualization.tables.VirtualMachineTable,
-            'url': 'virtualization:virtualmachine_list',
-        }),
-    )
-)
-
-SEARCH_TYPE_HIERARCHY = OrderedDict(
-    (
-        ("Circuits", CIRCUIT_TYPES),
-        ("DCIM", DCIM_TYPES),
-        ("IPAM", IPAM_TYPES),
-        ("Tenancy", TENANCY_TYPES),
-        ("Virtualization", VIRTUALIZATION_TYPES),
-    )
-)
-
-
-def build_search_types() -> Dict[str, Dict]:
-    result = dict()
-
-    for app_types in SEARCH_TYPE_HIERARCHY.values():
-        for name, items in app_types.items():
-            result[name] = items
-
-    return result
-
-
-SEARCH_TYPES = build_search_types()

+ 1 - 1
netbox/netbox/filtersets.py

@@ -125,7 +125,7 @@ class BaseFilterSet(django_filters.FilterSet):
             return {}
 
         # Skip nonstandard lookup expressions
-        if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
+        if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']:
             return {}
 
         # Choose the lookup expression map based on the filter type

+ 1 - 1
netbox/netbox/forms/__init__.py

@@ -1,6 +1,6 @@
 from django import forms
 
-from netbox.constants import SEARCH_TYPE_HIERARCHY
+from netbox.search import SEARCH_TYPE_HIERARCHY
 from utilities.forms import BootstrapMixin
 from .base import *
 

+ 261 - 0
netbox/netbox/search.py

@@ -0,0 +1,261 @@
+import circuits.filtersets
+import circuits.tables
+import dcim.filtersets
+import dcim.tables
+import ipam.filtersets
+import ipam.tables
+import tenancy.filtersets
+import tenancy.tables
+import virtualization.filtersets
+import wireless.tables
+import wireless.filtersets
+import virtualization.tables
+from circuits.models import Circuit, ProviderNetwork, Provider
+from dcim.models import (
+    Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site,
+    VirtualChassis,
+)
+from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
+from tenancy.models import Contact, Tenant, ContactAssignment
+from utilities.utils import count_related
+from wireless.models import WirelessLAN, WirelessLink
+from virtualization.models import Cluster, VirtualMachine
+
+CIRCUIT_TYPES = {
+    'provider': {
+        'queryset': Provider.objects.annotate(
+            count_circuits=count_related(Circuit, 'provider')
+        ),
+        'filterset': circuits.filtersets.ProviderFilterSet,
+        'table': circuits.tables.ProviderTable,
+        'url': 'circuits:provider_list',
+    },
+    'circuit': {
+        'queryset': Circuit.objects.prefetch_related(
+            'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
+        ),
+        'filterset': circuits.filtersets.CircuitFilterSet,
+        'table': circuits.tables.CircuitTable,
+        'url': 'circuits:circuit_list',
+    },
+    'providernetwork': {
+        'queryset': ProviderNetwork.objects.prefetch_related('provider'),
+        'filterset': circuits.filtersets.ProviderNetworkFilterSet,
+        'table': circuits.tables.ProviderNetworkTable,
+        'url': 'circuits:providernetwork_list',
+    },
+}
+
+DCIM_TYPES = {
+    'site': {
+        'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
+        'filterset': dcim.filtersets.SiteFilterSet,
+        'table': dcim.tables.SiteTable,
+        'url': 'dcim:site_list',
+    },
+    'rack': {
+        'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
+            device_count=count_related(Device, 'rack')
+        ),
+        'filterset': dcim.filtersets.RackFilterSet,
+        'table': dcim.tables.RackTable,
+        'url': 'dcim:rack_list',
+    },
+    'rackreservation': {
+        'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
+        'filterset': dcim.filtersets.RackReservationFilterSet,
+        'table': dcim.tables.RackReservationTable,
+        'url': 'dcim:rackreservation_list',
+    },
+    'location': {
+        'queryset': Location.objects.add_related_count(
+            Location.objects.add_related_count(
+                Location.objects.all(),
+                Device,
+                'location',
+                'device_count',
+                cumulative=True
+            ),
+            Rack,
+            'location',
+            'rack_count',
+            cumulative=True
+        ).prefetch_related('site'),
+        'filterset': dcim.filtersets.LocationFilterSet,
+        'table': dcim.tables.LocationTable,
+        'url': 'dcim:location_list',
+    },
+    'devicetype': {
+        'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
+            instance_count=count_related(Device, 'device_type')
+        ),
+        'filterset': dcim.filtersets.DeviceTypeFilterSet,
+        'table': dcim.tables.DeviceTypeTable,
+        'url': 'dcim:devicetype_list',
+    },
+    'device': {
+        'queryset': Device.objects.prefetch_related(
+            'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4',
+            'primary_ip6',
+        ),
+        'filterset': dcim.filtersets.DeviceFilterSet,
+        'table': dcim.tables.DeviceTable,
+        'url': 'dcim:device_list',
+    },
+    'moduletype': {
+        'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
+            instance_count=count_related(Module, 'module_type')
+        ),
+        'filterset': dcim.filtersets.ModuleTypeFilterSet,
+        'table': dcim.tables.ModuleTypeTable,
+        'url': 'dcim:moduletype_list',
+    },
+    'module': {
+        'queryset': Module.objects.prefetch_related(
+            'module_type__manufacturer', 'device', 'module_bay',
+        ),
+        'filterset': dcim.filtersets.ModuleFilterSet,
+        'table': dcim.tables.ModuleTable,
+        'url': 'dcim:module_list',
+    },
+    'virtualchassis': {
+        'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
+            member_count=count_related(Device, 'virtual_chassis')
+        ),
+        'filterset': dcim.filtersets.VirtualChassisFilterSet,
+        'table': dcim.tables.VirtualChassisTable,
+        'url': 'dcim:virtualchassis_list',
+    },
+    'cable': {
+        'queryset': Cable.objects.all(),
+        'filterset': dcim.filtersets.CableFilterSet,
+        'table': dcim.tables.CableTable,
+        'url': 'dcim:cable_list',
+    },
+    'powerfeed': {
+        'queryset': PowerFeed.objects.all(),
+        'filterset': dcim.filtersets.PowerFeedFilterSet,
+        'table': dcim.tables.PowerFeedTable,
+        'url': 'dcim:powerfeed_list',
+    },
+}
+
+IPAM_TYPES = {
+    'vrf': {
+        'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
+        'filterset': ipam.filtersets.VRFFilterSet,
+        'table': ipam.tables.VRFTable,
+        'url': 'ipam:vrf_list',
+    },
+    'aggregate': {
+        'queryset': Aggregate.objects.prefetch_related('rir'),
+        'filterset': ipam.filtersets.AggregateFilterSet,
+        'table': ipam.tables.AggregateTable,
+        'url': 'ipam:aggregate_list',
+    },
+    'prefix': {
+        'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
+        'filterset': ipam.filtersets.PrefixFilterSet,
+        'table': ipam.tables.PrefixTable,
+        'url': 'ipam:prefix_list',
+    },
+    'ipaddress': {
+        'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
+        'filterset': ipam.filtersets.IPAddressFilterSet,
+        'table': ipam.tables.IPAddressTable,
+        'url': 'ipam:ipaddress_list',
+    },
+    'vlan': {
+        'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
+        'filterset': ipam.filtersets.VLANFilterSet,
+        'table': ipam.tables.VLANTable,
+        'url': 'ipam:vlan_list',
+    },
+    'asn': {
+        'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
+        'filterset': ipam.filtersets.ASNFilterSet,
+        'table': ipam.tables.ASNTable,
+        'url': 'ipam:asn_list',
+    },
+    'service': {
+        'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
+        'filterset': ipam.filtersets.ServiceFilterSet,
+        'table': ipam.tables.ServiceTable,
+        'url': 'ipam:service_list',
+    },
+}
+
+TENANCY_TYPES = {
+    'tenant': {
+        'queryset': Tenant.objects.prefetch_related('group'),
+        'filterset': tenancy.filtersets.TenantFilterSet,
+        'table': tenancy.tables.TenantTable,
+        'url': 'tenancy:tenant_list',
+    },
+    'contact': {
+        'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
+            assignment_count=count_related(ContactAssignment, 'contact')),
+        'filterset': tenancy.filtersets.ContactFilterSet,
+        'table': tenancy.tables.ContactTable,
+        'url': 'tenancy:contact_list',
+    },
+}
+
+VIRTUALIZATION_TYPES = {
+    'cluster': {
+        'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
+            device_count=count_related(Device, 'cluster'),
+            vm_count=count_related(VirtualMachine, 'cluster')
+        ),
+        'filterset': virtualization.filtersets.ClusterFilterSet,
+        'table': virtualization.tables.ClusterTable,
+        'url': 'virtualization:cluster_list',
+    },
+    'virtualmachine': {
+        'queryset': VirtualMachine.objects.prefetch_related(
+            'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
+        ),
+        'filterset': virtualization.filtersets.VirtualMachineFilterSet,
+        'table': virtualization.tables.VirtualMachineTable,
+        'url': 'virtualization:virtualmachine_list',
+    },
+}
+
+WIRELESS_TYPES = {
+    'wirelesslan': {
+        'queryset': WirelessLAN.objects.prefetch_related('group', 'vlan').annotate(
+            interface_count=count_related(Interface, 'wireless_lans')
+        ),
+        'filterset': wireless.filtersets.WirelessLANFilterSet,
+        'table': wireless.tables.WirelessLANTable,
+        'url': 'wireless:wirelesslan_list',
+    },
+    'wirelesslink': {
+        'queryset': WirelessLink.objects.prefetch_related('interface_a__device', 'interface_b__device'),
+        'filterset': wireless.filtersets.WirelessLinkFilterSet,
+        'table': wireless.tables.WirelessLinkTable,
+        'url': 'wireless:wirelesslink_list',
+    },
+}
+
+SEARCH_TYPE_HIERARCHY = {
+    'Circuits': CIRCUIT_TYPES,
+    'DCIM': DCIM_TYPES,
+    'IPAM': IPAM_TYPES,
+    'Tenancy': TENANCY_TYPES,
+    'Virtualization': VIRTUALIZATION_TYPES,
+    'Wireless': WIRELESS_TYPES,
+}
+
+
+def build_search_types():
+    result = dict()
+
+    for app_types in SEARCH_TYPE_HIERARCHY.values():
+        for name, items in app_types.items():
+            result[name] = items
+
+    return result
+
+
+SEARCH_TYPES = build_search_types()

+ 8 - 7
netbox/netbox/settings.py

@@ -478,13 +478,6 @@ if SENTRY_ENABLED:
 # Django social auth
 #
 
-# Load all SOCIAL_AUTH_* settings from the user configuration
-for param in dir(configuration):
-    if param.startswith('SOCIAL_AUTH_'):
-        globals()[param] = getattr(configuration, param)
-
-SOCIAL_AUTH_JSONFIELD_ENABLED = True
-
 SOCIAL_AUTH_PIPELINE = (
     'social_core.pipeline.social_auth.social_details',
     'social_core.pipeline.social_auth.social_uid',
@@ -498,6 +491,14 @@ SOCIAL_AUTH_PIPELINE = (
     'social_core.pipeline.user.user_details',
 )
 
+# Load all SOCIAL_AUTH_* settings from the user configuration
+for param in dir(configuration):
+    if param.startswith('SOCIAL_AUTH_'):
+        globals()[param] = getattr(configuration, param)
+
+# Force usage of PostgreSQL's JSONB field for extra data
+SOCIAL_AUTH_JSONFIELD_ENABLED = True
+
 
 #
 # Django Prometheus

+ 2 - 1
netbox/netbox/views/__init__.py

@@ -21,8 +21,9 @@ from dcim.models import (
 from extras.models import ObjectChange
 from extras.tables import ObjectChangeTable
 from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
-from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
+from netbox.constants import SEARCH_MAX_RESULTS
 from netbox.forms import SearchForm
+from netbox.search import SEARCH_TYPES
 from tenancy.models import Tenant
 from virtualization.models import Cluster, VirtualMachine
 from wireless.models import WirelessLAN, WirelessLink

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 3 - 1
netbox/project-static/src/buttons/reslug.ts

@@ -38,7 +38,9 @@ export function initReslug(): void {
     slugLength = Number(slugLengthAttr);
   }
   sourceField.addEventListener('blur', () => {
-    slugField.value = slugify(sourceField.value, slugLength);
+    if (!slugField.value) {
+      slugField.value = slugify(sourceField.value, slugLength);
+    }
   });
   slugButton.addEventListener('click', () => {
     slugField.value = slugify(sourceField.value, slugLength);

+ 1 - 39
netbox/project-static/src/forms/elements.ts

@@ -1,32 +1,4 @@
-import { getElements, scrollTo, isTruthy } from '../util';
-
-/**
- * When editing an object, it is sometimes desirable to customize the form action *without*
- * overriding the form's `submit` event. For example, the 'Save & Continue' button. We don't want
- * to use the `formaction` attribute on that element because it will be included on the form even
- * if the button isn't clicked.
- *
- * @example
- * ```html
- * <button type="button" return-url="/special-url/">
- *   Save & Continue
- * </button>
- * ```
- *
- * @param event Click event.
- */
-function handleSubmitWithReturnUrl(event: MouseEvent): void {
-  const element = event.target as HTMLElement;
-  if (element.tagName === 'BUTTON') {
-    const button = element as HTMLButtonElement;
-    const action = button.getAttribute('return-url');
-    const form = button.form;
-    if (form !== null && isTruthy(action)) {
-      form.action = action;
-      form.submit();
-    }
-  }
-}
+import { getElements, scrollTo } from '../util';
 
 function handleFormSubmit(event: Event, form: HTMLFormElement): void {
   // Track the names of each invalid field.
@@ -57,15 +29,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
   }
 }
 
-/**
- * Attach event listeners to form buttons with the `return-url` attribute present.
- */
-function initReturnUrlSubmitButtons(): void {
-  for (const button of getElements<HTMLButtonElement>('button[return-url]')) {
-    button.addEventListener('click', handleSubmitWithReturnUrl);
-  }
-}
-
 /**
  * Attach an event listener to each form's submitter (button[type=submit]). When called, the
  * callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
@@ -82,5 +45,4 @@ export function initFormElements(): void {
       submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
     }
   }
-  initReturnUrlSubmitButtons();
 }

+ 0 - 1
netbox/project-static/src/select/api/apiSelect.ts

@@ -411,7 +411,6 @@ export class APISelect {
     } finally {
       this.setOptionStyles();
       this.enable();
-      this.slim.slim.search.input.focus();
       this.base.dispatchEvent(this.loadEvent);
     }
   }

+ 0 - 14
netbox/templates/circuits/circuittermination_edit.html

@@ -56,17 +56,3 @@
     {% render_custom_fields form %}
   </div>
 {% endblock %}
-
-{# Override buttons block, 'Create & Add Another'/'_addanother' is not needed on a circuit. #}
-{% block buttons %}
-    <a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
-    {% if object.pk %}
-        <button type="submit" name="_update" class="btn btn-primary">
-            Save
-        </button>
-    {% else %}
-        <button type="submit" name="_create" class="btn btn-primary">
-            Create
-        </button>
-    {% endif %}
-{% endblock buttons %}

+ 0 - 11
netbox/templates/dcim/interface_edit.html

@@ -99,14 +99,3 @@
       </div>
     {% endif %}
 {% endblock %}
-
-{% block buttons %}
-    <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
-    {% if object.pk %}
-        <button type="button" return-url="?return_url={% url 'dcim:interface_edit' pk=object.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
-        <button type="submit" name="_update" class="btn btn-primary">Save</button>
-    {% else %}
-        <button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>
-        <button type="submit" name="_create" class="btn btn-primary">Create</button>
-    {% endif %}
-{% endblock %}

+ 1 - 1
netbox/templates/generic/bulk_delete.html

@@ -36,8 +36,8 @@ Context:
           {{ field }}
         {% endfor %}
         <div class="text-end">
-          <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
           <button type="submit" name="_confirm" class="btn btn-danger">Delete {{ table.rows|length }} {{ model|meta:"verbose_name_plural" }}</button>
+          <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
         </div>
       </form>
     </div>

+ 1 - 1
netbox/templates/generic/bulk_edit.html

@@ -118,8 +118,8 @@ Context:
               </div>
 
               <div class="text-end">
-                <a href="{{ return_url }}" class="btn btn-sm btn-outline-danger">Cancel</a>
                 <button type="submit" name="_apply" class="btn btn-sm btn-primary">Apply</button>
+                <a href="{{ return_url }}" class="btn btn-sm btn-outline-danger">Cancel</a>
               </div>
             </div>
           </div>

+ 6 - 6
netbox/templates/generic/bulk_import.html

@@ -44,12 +44,12 @@ Context:
                     </div>
                   </div>
                   <div class="form-group">
-                      <div class="col col-md-12 text-end">
-                          {% if return_url %}
-                              <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
-                          {% endif %}
-                          <button type="submit" class="btn btn-primary">Submit</button>
-                      </div>
+                    <div class="col col-md-12 text-end">
+                      <button type="submit" class="btn btn-primary">Submit</button>
+                      {% if return_url %}
+                        <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
+                      {% endif %}
+                    </div>
                   </div>
                 </form>
                 {% if fields %}

+ 1 - 1
netbox/templates/generic/bulk_remove.html

@@ -23,8 +23,8 @@
       {{ field }}
     {% endfor %}
     <div class="text-center">
-      <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
       <button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
+      <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
     </div>
   </form>
 </div>

+ 1 - 1
netbox/templates/generic/bulk_rename.html

@@ -34,11 +34,11 @@
                 </div>
             </div>
             <div class="col col-md-12 my-3 text-end">
-                <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
                 <button type="submit" name="_preview" class="btn btn-primary">Preview</button>
                 {% if '_preview' in request.POST and not form.errors %}
                     <button type="submit" name="_apply" class="btn btn-primary">Apply</button>
                 {% endif %}
+                <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
             </div>
         </form>
     </div>

+ 18 - 27
netbox/templates/generic/confirmation_form.html

@@ -2,33 +2,24 @@
 {% load form_helpers %}
 
 {% block content %}
-    <div class="row mt-5">
-
-        <div class="col col-md-6 offset-md-3">
-
-            <form action="" method="post" class="form">
-                {% csrf_token %}
-                {% for field in form.hidden_fields %}
-                    {{ field }}
-                {% endfor %}
-
-                <div class="card border-danger">
-                    <h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
-            
-                    <div class="card-body">
-                        {% block message %}<p>Are you sure?</p>{% endblock %}
-                    </div>
-
-                    <div class="card-footer text-end">
-                        <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
-                        <button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
-                    </div>
-
-                </div>
-
-            </form>
-
+  <div class="row mt-5">
+    <div class="col col-md-6 offset-md-3">
+      <form action="" method="post" class="form">
+        {% csrf_token %}
+        {% for field in form.hidden_fields %}
+          {{ field }}
+        {% endfor %}
+        <div class="card border-danger">
+          <h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
+          <div class="card-body">
+            {% block message %}<p>Are you sure?</p>{% endblock %}
+          </div>
+          <div class="card-footer text-end">
+            <button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
+            <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
+          </div>
         </div>
-
+      </form>
     </div>
+  </div>
 {% endblock %}

+ 4 - 4
netbox/templates/generic/object_edit.html

@@ -94,19 +94,19 @@ Context:
 
         <div class="text-end my-3">
           {% block buttons %}
-            <a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
             {% if object.pk %}
               <button type="submit" name="_update" class="btn btn-primary">
                 Save
               </button>
             {% else %}
-              <button type="submit" name="_addanother" class="btn btn-outline-primary">
-                Create & Add Another
-              </button>
               <button type="submit" name="_create" class="btn btn-primary">
                 Create
               </button>
+              <button type="submit" name="_addanother" class="btn btn-outline-primary">
+                Create & Add Another
+              </button>
             {% endif %}
+            <a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
           {% endblock buttons %}
         </div>
       </form>

+ 15 - 15
netbox/templates/generic/object_import.html

@@ -5,19 +5,19 @@
 {% block title %}{{ obj_type|bettertitle }} Import{% endblock %}
 
 {% block content %}
-<div class="row mb-3">
-	<div class="col col-md-12 col-xl-8 offset-xl-2">
-		<form action="" method="post" class="form form-horizontal">
-		    {% csrf_token %}
-		    {% render_form form %}
-            <div class="col col-md-12 text-end">
-                {% if return_url %}
-                    <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
-                {% endif %}
-                <button type="submit" name="_addanother" class="btn btn-outline-primary">Submit & Import Another</button>
-                <button type="submit" name="_create" class="btn btn-primary">Submit</button>
-            </div>
-		</form>
-	</div>
-</div>
+  <div class="row mb-3">
+    <div class="col col-md-12 col-xl-8 offset-xl-2">
+      <form action="" method="post" class="form form-horizontal">
+        {% csrf_token %}
+        {% render_form form %}
+        <div class="col col-md-12 text-end">
+          <button type="submit" name="_create" class="btn btn-primary">Submit</button>
+          <button type="submit" name="_addanother" class="btn btn-outline-primary">Submit & Import Another</button>
+          {% if return_url %}
+            <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
+          {% endif %}
+        </div>
+      </form>
+    </div>
+  </div>
 {% endblock content %}

+ 0 - 11
netbox/templates/virtualization/vminterface_edit.html

@@ -55,14 +55,3 @@
       </div>
     {% endif %}
 {% endblock %}
-
-{% block buttons %}
-  <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
-  {% if object.pk %}
-    <button type="button" return-url="?return_url={% url 'virtualization:vminterface_edit' pk=object.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
-    <button type="submit" name="_update" class="btn btn-primary">Save</button>
-  {% else %}
-    <button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>
-    <button type="submit" name="_create" class="btn btn-primary">Create</button>
-  {% endif %}
-{% endblock %}

+ 2 - 1
netbox/tenancy/api/serializers.py

@@ -4,6 +4,7 @@ from rest_framework import serializers
 
 from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.choices import ContactPriorityChoices
 from tenancy.models import *
 from utilities.api import get_serializer_for_model
@@ -108,6 +109,6 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_object(self, instance):
-        serializer = get_serializer_for_model(instance.content_type.model_class(), prefix='Nested')
+        serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
         context = {'request': self.context['request']}
         return serializer(instance.object, context=context).data

+ 8 - 2
netbox/users/views.py

@@ -20,7 +20,7 @@ from netbox.authentication import get_auth_backend_display
 from netbox.config import get_config
 from utilities.forms import ConfirmationForm
 from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
-from .models import Token
+from .models import Token, UserConfig
 from .tables import TokenTable
 
 
@@ -70,7 +70,13 @@ class LoginView(View):
             # Authenticate user
             auth_login(request, form.get_user())
             logger.info(f"User {request.user} successfully authenticated")
-            messages.info(request, "Logged in as {}.".format(request.user))
+            messages.info(request, f"Logged in as {request.user}.")
+
+            # Ensure the user has a UserConfig defined. (This should normally be handled by
+            # create_userconfig() on user creation.)
+            if not hasattr(request.user, 'config'):
+                config = get_config()
+                UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
 
             return self.redirect_to_next(request, logger)
 

+ 2 - 0
netbox/utilities/templatetags/builtins/filters.py

@@ -144,6 +144,8 @@ def render_markdown(value):
 
         {{ md_source_text|markdown }}
     """
+    if not value:
+        return ''
 
     # Render Markdown
     html = markdown(value, extensions=['def_list', 'fenced_code', 'tables', StrikethroughExtension()])

+ 2 - 0
netbox/virtualization/views.py

@@ -471,6 +471,7 @@ class VMInterfaceBulkImportView(generic.BulkImportView):
 
 class VMInterfaceBulkEditView(generic.BulkEditView):
     queryset = VMInterface.objects.all()
+    filterset = filtersets.VMInterfaceFilterSet
     table = tables.VMInterfaceTable
     form = forms.VMInterfaceBulkEditForm
 
@@ -482,6 +483,7 @@ class VMInterfaceBulkRenameView(generic.BulkRenameView):
 
 class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
     queryset = VMInterface.objects.all()
+    filterset = filtersets.VMInterfaceFilterSet
     table = tables.VMInterfaceTable
 
 

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor