mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
@ -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.
|
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.)
|
||||||
|
@ -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.
|
* `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.
|
* `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.
|
* `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
|
### Default Request Body
|
||||||
|
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
# NetBox v3.2
|
# 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ from netbox.api.serializers import (
|
|||||||
WritableNestedSerializer,
|
WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -57,7 +58,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Return serialized peer termination objects
|
# 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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.link_peers, context=context, many=True).data
|
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.
|
Return the appropriate serializer for the type of connected object.
|
||||||
"""
|
"""
|
||||||
if endpoints := obj.connected_endpoints:
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(endpoints, many=True, context=context).data
|
return serializer(endpoints, many=True, context=context).data
|
||||||
|
|
||||||
@ -572,7 +573,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
|||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return 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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.component, context=context).data
|
return serializer(obj.component, context=context).data
|
||||||
|
|
||||||
@ -968,7 +969,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
|||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return 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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.component, context=context).data
|
return serializer(obj.component, context=context).data
|
||||||
|
|
||||||
@ -1037,7 +1038,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_termination(self, obj):
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.termination, context=context).data
|
return serializer(obj.termination, context=context).data
|
||||||
|
|
||||||
@ -1053,7 +1054,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
|||||||
def get_path(self, obj):
|
def get_path(self, obj):
|
||||||
ret = []
|
ret = []
|
||||||
for nodes in obj.path_objects:
|
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']}
|
context = {'request': self.context['request']}
|
||||||
ret.append(serializer(nodes, context=context, many=True).data)
|
ret.append(serializer(nodes, context=context, many=True).data)
|
||||||
return ret
|
return ret
|
||||||
|
@ -24,6 +24,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
|||||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
@ -65,7 +66,7 @@ class PathEndpointMixin(object):
|
|||||||
# Serialize path objects, iterating over each three-tuple in the path
|
# Serialize path objects, iterating over each three-tuple in the path
|
||||||
for near_end, cable, far_end in obj.trace():
|
for near_end, cable, far_end in obj.trace():
|
||||||
if near_end is not None:
|
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
|
near_end = serializer_a(near_end, many=True, context={'request': request}).data
|
||||||
else:
|
else:
|
||||||
# Path is split; stop here
|
# Path is split; stop here
|
||||||
@ -73,7 +74,7 @@ class PathEndpointMixin(object):
|
|||||||
if cable is not None:
|
if cable is not None:
|
||||||
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||||
if far_end is not None:
|
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
|
far_end = serializer_b(far_end, many=True, context={'request': request}).data
|
||||||
|
|
||||||
path.append((near_end, cable, far_end))
|
path.append((near_end, cable, far_end))
|
||||||
|
@ -312,7 +312,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
serial = django_filters.CharFilter(
|
serial = MultiValueCharFilter(
|
||||||
lookup_expr='iexact'
|
lookup_expr='iexact'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1007,10 +1007,13 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
|
serial = MultiValueCharFilter(
|
||||||
|
lookup_expr='iexact'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
fields = ['id', 'serial', 'asset_tag']
|
fields = ['id', 'asset_tag']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -1411,7 +1414,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
component_type = ContentTypeFilter()
|
component_type = ContentTypeFilter()
|
||||||
component_id = MultiValueNumberFilter()
|
component_id = MultiValueNumberFilter()
|
||||||
serial = django_filters.CharFilter(
|
serial = MultiValueCharFilter(
|
||||||
lookup_expr='iexact'
|
lookup_expr='iexact'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -998,8 +998,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
)
|
)
|
||||||
speed = forms.IntegerField(
|
speed = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Select Speed',
|
label='Speed',
|
||||||
widget=SelectSpeedWidget(attrs={'readonly': None})
|
widget=SelectSpeedWidget()
|
||||||
)
|
)
|
||||||
duplex = MultipleChoiceField(
|
duplex = MultipleChoiceField(
|
||||||
choices=InterfaceDuplexChoices,
|
choices=InterfaceDuplexChoices,
|
||||||
|
@ -163,8 +163,9 @@ class RackElevationSVG:
|
|||||||
|
|
||||||
# Embed device type image if provided
|
# Embed device type image if provided
|
||||||
if self.include_images and image:
|
if self.include_images and image:
|
||||||
|
url = f'{self.base_url}{image.url}' if image.url.startswith('/') else image.url
|
||||||
image = Image(
|
image = Image(
|
||||||
href=f'{self.base_url}{image.url}',
|
href=url,
|
||||||
insert=coords,
|
insert=coords,
|
||||||
size=size,
|
size=size,
|
||||||
class_=f'device-image{css_extra}'
|
class_=f'device-image{css_extra}'
|
||||||
|
@ -498,10 +498,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_serial(self):
|
def test_serial(self):
|
||||||
params = {'serial': 'ABC'}
|
params = {'serial': ['ABC', 'DEF']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'serial': 'abc'}
|
params = {'serial': ['abc', 'def']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_tenant(self):
|
def test_tenant(self):
|
||||||
tenants = Tenant.objects.all()[:2]
|
tenants = Tenant.objects.all()[:2]
|
||||||
@ -1864,7 +1864,9 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
def test_serial(self):
|
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)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_asset_tag(self):
|
def test_asset_tag(self):
|
||||||
@ -3520,10 +3522,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_serial(self):
|
def test_serial(self):
|
||||||
params = {'serial': 'ABC'}
|
params = {'serial': ['ABC', 'DEF']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
params = {'serial': 'abc'}
|
params = {'serial': ['abc', 'def']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_component_type(self):
|
def test_component_type(self):
|
||||||
params = {'component_type': 'dcim.interface'}
|
params = {'component_type': 'dcim.interface'}
|
||||||
|
@ -3,6 +3,7 @@ from rest_framework.fields import Field
|
|||||||
|
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField
|
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():
|
for cf in self._get_custom_fields():
|
||||||
value = cf.deserialize(obj.get(cf.name))
|
value = cf.deserialize(obj.get(cf.name))
|
||||||
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
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
|
value = serializer(value, context=self.parent.context).data
|
||||||
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
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
|
value = serializer(value, many=True, context=self.parent.context).data
|
||||||
data[cf.name] = value
|
data[cf.name] = value
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from extras.utils import FeatureQuery
|
|||||||
from netbox.api.exceptions import SerializerNotFound
|
from netbox.api.exceptions import SerializerNotFound
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
|
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.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from users.api.nested_serializers import NestedUserSerializer
|
from users.api.nested_serializers import NestedUserSerializer
|
||||||
@ -193,7 +194,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_parent(self, obj):
|
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
|
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)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_assigned_object(self, instance):
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(instance.assigned_object, context=context).data
|
return serializer(instance.assigned_object, context=context).data
|
||||||
|
|
||||||
@ -469,7 +470,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
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:
|
except SerializerNotFound:
|
||||||
return obj.object_repr
|
return obj.object_repr
|
||||||
context = {
|
context = {
|
||||||
|
@ -10,6 +10,7 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
|||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||||
@ -148,7 +149,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
def get_interface(self, obj):
|
def get_interface(self, obj):
|
||||||
if obj.interface is None:
|
if obj.interface is None:
|
||||||
return 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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.interface, context=context).data
|
return serializer(obj.interface, context=context).data
|
||||||
|
|
||||||
@ -194,7 +195,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
|||||||
def get_scope(self, obj):
|
def get_scope(self, obj):
|
||||||
if obj.scope_id is None:
|
if obj.scope_id is None:
|
||||||
return 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']}
|
context = {'request': self.context['request']}
|
||||||
|
|
||||||
return serializer(obj.scope, context=context).data
|
return serializer(obj.scope, context=context).data
|
||||||
@ -378,7 +379,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
|||||||
def get_assigned_object(self, obj):
|
def get_assigned_object(self, obj):
|
||||||
if obj.assigned_object is None:
|
if obj.assigned_object is None:
|
||||||
return 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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.assigned_object, context=context).data
|
return serializer(obj.assigned_object, context=context).data
|
||||||
|
|
||||||
@ -485,6 +486,6 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_assigned_object(self, instance):
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(instance.assigned_object, context=context).data
|
return serializer(instance.assigned_object, context=context).data
|
||||||
|
@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from netbox.api.exceptions import SerializerNotFound
|
from netbox.api.exceptions import SerializerNotFound
|
||||||
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from .mixins import *
|
from .mixins import *
|
||||||
@ -61,7 +62,7 @@ class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectVali
|
|||||||
if self.brief:
|
if self.brief:
|
||||||
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
||||||
try:
|
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}")
|
logger.debug(f"Using serializer {serializer}")
|
||||||
return serializer
|
return serializer
|
||||||
except SerializerNotFound:
|
except SerializerNotFound:
|
||||||
|
@ -1,256 +1,5 @@
|
|||||||
from collections import OrderedDict
|
# Prefix for nested serializers
|
||||||
from typing import Dict
|
NESTED_SERIALIZER_PREFIX = 'Nested'
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
# Max results per object type
|
||||||
SEARCH_MAX_RESULTS = 15
|
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()
|
|
||||||
|
@ -125,7 +125,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Skip nonstandard lookup expressions
|
# 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 {}
|
return {}
|
||||||
|
|
||||||
# Choose the lookup expression map based on the filter type
|
# Choose the lookup expression map based on the filter type
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from netbox.constants import SEARCH_TYPE_HIERARCHY
|
from netbox.search import SEARCH_TYPE_HIERARCHY
|
||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
|
261
netbox/netbox/search.py
Normal file
261
netbox/netbox/search.py
Normal file
@ -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()
|
@ -478,13 +478,6 @@ if SENTRY_ENABLED:
|
|||||||
# Django social auth
|
# 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_AUTH_PIPELINE = (
|
||||||
'social_core.pipeline.social_auth.social_details',
|
'social_core.pipeline.social_auth.social_details',
|
||||||
'social_core.pipeline.social_auth.social_uid',
|
'social_core.pipeline.social_auth.social_uid',
|
||||||
@ -498,6 +491,14 @@ SOCIAL_AUTH_PIPELINE = (
|
|||||||
'social_core.pipeline.user.user_details',
|
'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
|
# Django Prometheus
|
||||||
|
@ -21,8 +21,9 @@ from dcim.models import (
|
|||||||
from extras.models import ObjectChange
|
from extras.models import ObjectChange
|
||||||
from extras.tables import ObjectChangeTable
|
from extras.tables import ObjectChangeTable
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
|
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.forms import SearchForm
|
||||||
|
from netbox.search import SEARCH_TYPES
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from virtualization.models import Cluster, VirtualMachine
|
from virtualization.models import Cluster, VirtualMachine
|
||||||
from wireless.models import WirelessLAN, WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
|
14
netbox/project-static/dist/netbox.js
vendored
14
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js.map
vendored
2
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@ -38,7 +38,9 @@ export function initReslug(): void {
|
|||||||
slugLength = Number(slugLengthAttr);
|
slugLength = Number(slugLengthAttr);
|
||||||
}
|
}
|
||||||
sourceField.addEventListener('blur', () => {
|
sourceField.addEventListener('blur', () => {
|
||||||
|
if (!slugField.value) {
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
slugButton.addEventListener('click', () => {
|
slugButton.addEventListener('click', () => {
|
||||||
slugField.value = slugify(sourceField.value, slugLength);
|
slugField.value = slugify(sourceField.value, slugLength);
|
||||||
|
@ -1,32 +1,4 @@
|
|||||||
import { getElements, scrollTo, isTruthy } from '../util';
|
import { getElements, scrollTo } 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||||
// Track the names of each invalid field.
|
// 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
|
* 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
|
* 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));
|
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initReturnUrlSubmitButtons();
|
|
||||||
}
|
}
|
||||||
|
@ -411,7 +411,6 @@ export class APISelect {
|
|||||||
} finally {
|
} finally {
|
||||||
this.setOptionStyles();
|
this.setOptionStyles();
|
||||||
this.enable();
|
this.enable();
|
||||||
this.slim.slim.search.input.focus();
|
|
||||||
this.base.dispatchEvent(this.loadEvent);
|
this.base.dispatchEvent(this.loadEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,17 +56,3 @@
|
|||||||
{% render_custom_fields form %}
|
{% render_custom_fields form %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
@ -99,14 +99,3 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
@ -36,8 +36,8 @@ Context:
|
|||||||
{{ field }}
|
{{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="text-end">
|
<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>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,8 +118,8 @@ Context:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-end">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,10 +45,10 @@ Context:
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col col-md-12 text-end">
|
<div class="col col-md-12 text-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
{% if return_url %}
|
{% if return_url %}
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
{{ field }}
|
{{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="text-center">
|
<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>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,11 +34,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-12 my-3 text-end">
|
<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>
|
<button type="submit" name="_preview" class="btn btn-primary">Preview</button>
|
||||||
{% if '_preview' in request.POST and not form.errors %}
|
{% if '_preview' in request.POST and not form.errors %}
|
||||||
<button type="submit" name="_apply" class="btn btn-primary">Apply</button>
|
<button type="submit" name="_apply" class="btn btn-primary">Apply</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,32 +3,23 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
|
|
||||||
<div class="col col-md-6 offset-md-3">
|
<div class="col col-md-6 offset-md-3">
|
||||||
|
|
||||||
<form action="" method="post" class="form">
|
<form action="" method="post" class="form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form.hidden_fields %}
|
{% for field in form.hidden_fields %}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="card border-danger">
|
<div class="card border-danger">
|
||||||
<h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
|
<h5 class="card-header">{% block confirmation_title %}{% endblock %}</h5>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-end">
|
<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>
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -94,19 +94,19 @@ Context:
|
|||||||
|
|
||||||
<div class="text-end my-3">
|
<div class="text-end my-3">
|
||||||
{% block buttons %}
|
{% block buttons %}
|
||||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
|
||||||
{% if object.pk %}
|
{% if object.pk %}
|
||||||
<button type="submit" name="_update" class="btn btn-primary">
|
<button type="submit" name="_update" class="btn btn-primary">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
|
||||||
Create & Add Another
|
|
||||||
</button>
|
|
||||||
<button type="submit" name="_create" class="btn btn-primary">
|
<button type="submit" name="_create" class="btn btn-primary">
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
|
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||||
|
Create & Add Another
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||||
{% endblock buttons %}
|
{% endblock buttons %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -11,11 +11,11 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="col col-md-12 text-end">
|
<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 %}
|
{% if return_url %}
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
{% endif %}
|
{% 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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,14 +55,3 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||||
|
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||||
from tenancy.choices import ContactPriorityChoices
|
from tenancy.choices import ContactPriorityChoices
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -108,6 +109,6 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_object(self, instance):
|
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']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(instance.object, context=context).data
|
return serializer(instance.object, context=context).data
|
||||||
|
@ -20,7 +20,7 @@ from netbox.authentication import get_auth_backend_display
|
|||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||||
from .models import Token
|
from .models import Token, UserConfig
|
||||||
from .tables import TokenTable
|
from .tables import TokenTable
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +70,13 @@ class LoginView(View):
|
|||||||
# Authenticate user
|
# Authenticate user
|
||||||
auth_login(request, form.get_user())
|
auth_login(request, form.get_user())
|
||||||
logger.info(f"User {request.user} successfully authenticated")
|
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)
|
return self.redirect_to_next(request, logger)
|
||||||
|
|
||||||
|
@ -144,6 +144,8 @@ def render_markdown(value):
|
|||||||
|
|
||||||
{{ md_source_text|markdown }}
|
{{ md_source_text|markdown }}
|
||||||
"""
|
"""
|
||||||
|
if not value:
|
||||||
|
return ''
|
||||||
|
|
||||||
# Render Markdown
|
# Render Markdown
|
||||||
html = markdown(value, extensions=['def_list', 'fenced_code', 'tables', StrikethroughExtension()])
|
html = markdown(value, extensions=['def_list', 'fenced_code', 'tables', StrikethroughExtension()])
|
||||||
|
@ -471,6 +471,7 @@ class VMInterfaceBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
class VMInterfaceBulkEditView(generic.BulkEditView):
|
class VMInterfaceBulkEditView(generic.BulkEditView):
|
||||||
queryset = VMInterface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
|
filterset = filtersets.VMInterfaceFilterSet
|
||||||
table = tables.VMInterfaceTable
|
table = tables.VMInterfaceTable
|
||||||
form = forms.VMInterfaceBulkEditForm
|
form = forms.VMInterfaceBulkEditForm
|
||||||
|
|
||||||
@ -482,6 +483,7 @@ class VMInterfaceBulkRenameView(generic.BulkRenameView):
|
|||||||
|
|
||||||
class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
|
class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VMInterface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
|
filterset = filtersets.VMInterfaceFilterSet
|
||||||
table = tables.VMInterfaceTable
|
table = tables.VMInterfaceTable
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user