mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-27 01:36:11 -06:00
Merge branch 'develop' into 11091-token-permission
This commit is contained in:
commit
989d6fda64
@ -58,8 +58,6 @@ as the cornerstone for network automation in thousands of organizations.
|
|||||||
[](https://netboxlabs.com)
|
[](https://netboxlabs.com)
|
||||||
|
|
||||||
[](https://try.digitalocean.com/developer-cloud)
|
[](https://try.digitalocean.com/developer-cloud)
|
||||||
|
|
||||||
[](https://ns1.com)
|
|
||||||
<br />
|
<br />
|
||||||
[](https://sentry.io)
|
[](https://sentry.io)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN
|
||||||
* [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type
|
* [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@ -14,6 +15,9 @@
|
|||||||
* [#12084](https://github.com/netbox-community/netbox/issues/12084) - Fix exception when attempting to create a saved filter for applied filters
|
* [#12084](https://github.com/netbox-community/netbox/issues/12084) - Fix exception when attempting to create a saved filter for applied filters
|
||||||
* [#12087](https://github.com/netbox-community/netbox/issues/12087) - Fix bulk editing of many-to-many relationships
|
* [#12087](https://github.com/netbox-community/netbox/issues/12087) - Fix bulk editing of many-to-many relationships
|
||||||
* [#12117](https://github.com/netbox-community/netbox/issues/12117) - Hide clone button for objects with no clonable attributes
|
* [#12117](https://github.com/netbox-community/netbox/issues/12117) - Hide clone button for objects with no clonable attributes
|
||||||
|
* [#12118](https://github.com/netbox-community/netbox/issues/12118) - Fix instantiation of nested inventory item templates when creating a device
|
||||||
|
* [#12184](https://github.com/netbox-community/netbox/issues/12184) - Fix filtered bulk deletion for various models
|
||||||
|
* [#12190](https://github.com/netbox-community/netbox/issues/12190) - Fix form layout for plugin textarea fields
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -196,6 +196,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = CircuitType.objects.annotate(
|
queryset = CircuitType.objects.annotate(
|
||||||
circuit_count=count_related(Circuit, 'type')
|
circuit_count=count_related(Circuit, 'type')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.CircuitTypeFilterSet
|
||||||
table = tables.CircuitTypeTable
|
table = tables.CircuitTypeTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ __all__ = (
|
|||||||
'CableFilterSet',
|
'CableFilterSet',
|
||||||
'CabledObjectFilterSet',
|
'CabledObjectFilterSet',
|
||||||
'CableTerminationFilterSet',
|
'CableTerminationFilterSet',
|
||||||
|
'CommonInterfaceFilterSet',
|
||||||
'ConsoleConnectionFilterSet',
|
'ConsoleConnectionFilterSet',
|
||||||
'ConsolePortFilterSet',
|
'ConsolePortFilterSet',
|
||||||
'ConsolePortTemplateFilterSet',
|
'ConsolePortTemplateFilterSet',
|
||||||
@ -1321,11 +1322,45 @@ class PowerOutletFilterSet(
|
|||||||
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
|
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
|
class CommonInterfaceFilterSet(django_filters.FilterSet):
|
||||||
|
vlan_id = django_filters.CharFilter(
|
||||||
|
method='filter_vlan_id',
|
||||||
|
label=_('Assigned VLAN')
|
||||||
|
)
|
||||||
|
vlan = django_filters.CharFilter(
|
||||||
|
method='filter_vlan',
|
||||||
|
label=_('Assigned VID')
|
||||||
|
)
|
||||||
|
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vrf',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
label=_('VRF'),
|
||||||
|
)
|
||||||
|
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='vrf__rd',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
to_field_name='rd',
|
||||||
|
label=_('VRF (RD)'),
|
||||||
|
)
|
||||||
|
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
label=_('L2VPN (ID)'),
|
||||||
|
)
|
||||||
|
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='l2vpn_terminations__l2vpn__identifier',
|
||||||
|
queryset=L2VPN.objects.all(),
|
||||||
|
to_field_name='identifier',
|
||||||
|
label=_('L2VPN'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterSet(
|
class InterfaceFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CabledObjectFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet,
|
||||||
|
CommonInterfaceFilterSet
|
||||||
):
|
):
|
||||||
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
||||||
# members
|
# members
|
||||||
@ -1370,14 +1405,6 @@ class InterfaceFilterSet(
|
|||||||
poe_type = django_filters.MultipleChoiceFilter(
|
poe_type = django_filters.MultipleChoiceFilter(
|
||||||
choices=InterfacePoETypeChoices
|
choices=InterfacePoETypeChoices
|
||||||
)
|
)
|
||||||
vlan_id = django_filters.CharFilter(
|
|
||||||
method='filter_vlan_id',
|
|
||||||
label=_('Assigned VLAN')
|
|
||||||
)
|
|
||||||
vlan = django_filters.CharFilter(
|
|
||||||
method='filter_vlan',
|
|
||||||
label=_('Assigned VID')
|
|
||||||
)
|
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -1388,17 +1415,6 @@ class InterfaceFilterSet(
|
|||||||
rf_channel = django_filters.MultipleChoiceFilter(
|
rf_channel = django_filters.MultipleChoiceFilter(
|
||||||
choices=WirelessChannelChoices
|
choices=WirelessChannelChoices
|
||||||
)
|
)
|
||||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='vrf',
|
|
||||||
queryset=VRF.objects.all(),
|
|
||||||
label=_('VRF'),
|
|
||||||
)
|
|
||||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='vrf__rd',
|
|
||||||
queryset=VRF.objects.all(),
|
|
||||||
to_field_name='rd',
|
|
||||||
label=_('VRF (RD)'),
|
|
||||||
)
|
|
||||||
vdc_id = django_filters.ModelMultipleChoiceFilter(
|
vdc_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='vdcs',
|
field_name='vdcs',
|
||||||
queryset=VirtualDeviceContext.objects.all(),
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
@ -1416,17 +1432,6 @@ class InterfaceFilterSet(
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Virtual Device Context',
|
label='Virtual Device Context',
|
||||||
)
|
)
|
||||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='l2vpn_terminations__l2vpn',
|
|
||||||
queryset=L2VPN.objects.all(),
|
|
||||||
label=_('L2VPN (ID)'),
|
|
||||||
)
|
|
||||||
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='l2vpn_terminations__l2vpn__identifier',
|
|
||||||
queryset=L2VPN.objects.all(),
|
|
||||||
to_field_name='identifier',
|
|
||||||
label=_('L2VPN'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -774,8 +774,10 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
bulk_create: If True, bulk_create() will be called to create all components in a single query
|
bulk_create: If True, bulk_create() will be called to create all components in a single query
|
||||||
(default). Otherwise, save() will be called on each instance individually.
|
(default). Otherwise, save() will be called on each instance individually.
|
||||||
"""
|
"""
|
||||||
|
if bulk_create:
|
||||||
components = [obj.instantiate(device=self) for obj in queryset]
|
components = [obj.instantiate(device=self) for obj in queryset]
|
||||||
if components and bulk_create:
|
if not components:
|
||||||
|
return
|
||||||
model = components[0]._meta.model
|
model = components[0]._meta.model
|
||||||
model.objects.bulk_create(components)
|
model.objects.bulk_create(components)
|
||||||
# Manually send the post_save signal for each of the newly created components
|
# Manually send the post_save signal for each of the newly created components
|
||||||
@ -788,8 +790,9 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
using='default',
|
using='default',
|
||||||
update_fields=None
|
update_fields=None
|
||||||
)
|
)
|
||||||
elif components:
|
else:
|
||||||
for component in components:
|
for obj in queryset:
|
||||||
|
component = obj.instantiate(device=self)
|
||||||
component.save()
|
component.save()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -628,6 +628,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = RackRole.objects.annotate(
|
queryset = RackRole.objects.annotate(
|
||||||
rack_count=count_related(Rack, 'role')
|
rack_count=count_related(Rack, 'role')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.RackRoleFilterSet
|
||||||
table = tables.RackRoleTable
|
table = tables.RackRoleTable
|
||||||
|
|
||||||
|
|
||||||
@ -909,6 +910,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = Manufacturer.objects.annotate(
|
queryset = Manufacturer.objects.annotate(
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.ManufacturerFilterSet
|
||||||
table = tables.ManufacturerTable
|
table = tables.ManufacturerTable
|
||||||
|
|
||||||
|
|
||||||
@ -1808,6 +1810,7 @@ class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
device_count=count_related(Device, 'device_role'),
|
device_count=count_related(Device, 'device_role'),
|
||||||
vm_count=count_related(VirtualMachine, 'role')
|
vm_count=count_related(VirtualMachine, 'role')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.DeviceRoleFilterSet
|
||||||
table = tables.DeviceRoleTable
|
table = tables.DeviceRoleTable
|
||||||
|
|
||||||
|
|
||||||
@ -1868,6 +1871,7 @@ class PlatformBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
|
filterset = filtersets.PlatformFilterSet
|
||||||
table = tables.PlatformTable
|
table = tables.PlatformTable
|
||||||
|
|
||||||
|
|
||||||
@ -2981,6 +2985,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
|
|||||||
|
|
||||||
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = InventoryItem.objects.all()
|
queryset = InventoryItem.objects.all()
|
||||||
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
template_name = 'dcim/inventoryitem_bulk_delete.html'
|
||||||
|
|
||||||
@ -3038,6 +3043,7 @@ class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = InventoryItemRole.objects.annotate(
|
queryset = InventoryItemRole.objects.annotate(
|
||||||
inventoryitem_count=count_related(InventoryItem, 'role'),
|
inventoryitem_count=count_related(InventoryItem, 'role'),
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.InventoryItemRoleFilterSet
|
||||||
table = tables.InventoryItemRoleTable
|
table = tables.InventoryItemRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from unittest import skipIf
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django_rq.queues import get_connection
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rq import Worker
|
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||||
from extras.api.views import ReportViewSet, ScriptViewSet
|
from extras.api.views import ReportViewSet, ScriptViewSet
|
||||||
@ -16,8 +13,6 @@ from extras.reports import Report
|
|||||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
rq_worker_running = Worker.count(get_connection('default'))
|
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
@ -539,16 +534,6 @@ class ReportTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.data['name'], self.TestReport.__name__)
|
self.assertEqual(response.data['name'], self.TestReport.__name__)
|
||||||
|
|
||||||
@skipIf(not rq_worker_running, "RQ worker not running")
|
|
||||||
def test_run_report(self):
|
|
||||||
self.add_permissions('extras.run_script')
|
|
||||||
|
|
||||||
url = reverse('extras-api:report-run', kwargs={'pk': None})
|
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
self.assertEqual(response.data['result']['status']['value'], 'pending')
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptTest(APITestCase):
|
class ScriptTest(APITestCase):
|
||||||
|
|
||||||
@ -589,27 +574,6 @@ class ScriptTest(APITestCase):
|
|||||||
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
|
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
|
||||||
self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
|
self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
|
||||||
|
|
||||||
@skipIf(not rq_worker_running, "RQ worker not running")
|
|
||||||
def test_run_script(self):
|
|
||||||
self.add_permissions('extras.run_script')
|
|
||||||
|
|
||||||
script_data = {
|
|
||||||
'var1': 'FooBar',
|
|
||||||
'var2': 123,
|
|
||||||
'var3': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'data': script_data,
|
|
||||||
'commit': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
url = reverse('extras-api:script-detail', kwargs={'pk': None})
|
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
self.assertEqual(response.data['result']['status']['value'], 'pending')
|
|
||||||
|
|
||||||
|
|
||||||
class CreatedUpdatedFilterTest(APITestCase):
|
class CreatedUpdatedFilterTest(APITestCase):
|
||||||
|
|
||||||
|
@ -414,6 +414,7 @@ class ConfigContextDeleteView(generic.ObjectDeleteView):
|
|||||||
|
|
||||||
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
|
filterset = filtersets.ConfigContextFilterSet
|
||||||
table = tables.ConfigContextTable
|
table = tables.ConfigContextTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -836,7 +836,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
self.add_permissions('ipam.delete_vlan')
|
self.add_permissions('ipam.delete_vlan')
|
||||||
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
|
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
|
||||||
with disable_warnings('django.request'):
|
with disable_warnings('netbox.api.views.ModelViewSet'):
|
||||||
response = self.client.delete(url, **self.header)
|
response = self.client.delete(url, **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
||||||
|
@ -431,6 +431,7 @@ class RoleBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
class RoleBulkDeleteView(generic.BulkDeleteView):
|
class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Role.objects.all()
|
queryset = Role.objects.all()
|
||||||
|
filterset = filtersets.RoleFilterSet
|
||||||
table = tables.RoleTable
|
table = tables.RoleTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ from django_tables2.export import TableExport
|
|||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.choices import ImportFormatChoices
|
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
|
from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
|
||||||
|
@ -111,7 +111,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
{% render_field vc_form.comments %}
|
{% render_field vc_form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ Context:
|
|||||||
|
|
||||||
{% if form.comments %}
|
{% if form.comments %}
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -33,9 +33,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -139,9 +139,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -66,9 +66,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -53,9 +53,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -56,9 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="text-center">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
{% render_field form.comments %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
{% extends 'generic/object_edit.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block form %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Side A</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.device_a %}
|
|
||||||
{% render_field form.interface_a %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Side B</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.device_b %}
|
|
||||||
{% render_field form.interface_b %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Comments</h5>
|
|
||||||
</div>
|
|
||||||
{% render_field form.comments %}
|
|
||||||
</div>
|
|
||||||
{% if form.custom_fields %}
|
|
||||||
<div class="field-group my-5">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
|
||||||
</div>
|
|
||||||
{% render_custom_fields form %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
@ -84,6 +84,7 @@ class TenantGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
'tenant_count',
|
'tenant_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.TenantGroupFilterSet
|
||||||
table = tables.TenantGroupTable
|
table = tables.TenantGroupTable
|
||||||
|
|
||||||
|
|
||||||
@ -247,6 +248,7 @@ class ContactGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
'contact_count',
|
'contact_count',
|
||||||
cumulative=True
|
cumulative=True
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.ContactGroupFilterSet
|
||||||
table = tables.ContactGroupTable
|
table = tables.ContactGroupTable
|
||||||
|
|
||||||
|
|
||||||
@ -305,6 +307,7 @@ class ContactRoleBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
class ContactRoleBulkDeleteView(generic.BulkDeleteView):
|
class ContactRoleBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = ContactRole.objects.all()
|
queryset = ContactRole.objects.all()
|
||||||
|
filterset = filtersets.ContactRoleFilterSet
|
||||||
table = tables.ContactRoleTable
|
table = tables.ContactRoleTable
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class AppTest(APITestCase):
|
|||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
|
||||||
url = reverse('users-api:api-root')
|
url = reverse('users-api:api-root')
|
||||||
response = self.client.get('{}?format=api'.format(url), **self.header)
|
response = self.client.get(f'{url}?format=api', **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -36,14 +36,17 @@ class UserTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'password': 'password6',
|
'password': 'password6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
bulk_update_data = {
|
||||||
|
'email': 'test@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
users = (
|
users = (
|
||||||
User(username='User_1'),
|
User(username='User_1', password='password1'),
|
||||||
User(username='User_2'),
|
User(username='User_2', password='password2'),
|
||||||
User(username='User_3'),
|
User(username='User_3', password='password3'),
|
||||||
)
|
)
|
||||||
User.objects.bulk_create(users)
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
@ -74,6 +77,12 @@ class GroupTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
Group.objects.bulk_create(users)
|
Group.objects.bulk_create(users)
|
||||||
|
|
||||||
|
def test_bulk_update_objects(self):
|
||||||
|
"""
|
||||||
|
Disabled test. There's no attribute we can set in bulk for Groups.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class TokenTest(
|
class TokenTest(
|
||||||
# No GraphQL support for Token
|
# No GraphQL support for Token
|
||||||
|
@ -34,8 +34,8 @@ class CommentField(forms.CharField):
|
|||||||
Markdown</a> syntax is supported
|
Markdown</a> syntax is supported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *, label='', help_text=help_text, required=False, **kwargs):
|
def __init__(self, *, help_text=help_text, required=False, **kwargs):
|
||||||
super().__init__(label=label, help_text=help_text, required=required, **kwargs)
|
super().__init__(help_text=help_text, required=required, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SlugField(forms.SlugField):
|
class SlugField(forms.SlugField):
|
||||||
|
@ -3,11 +3,8 @@
|
|||||||
|
|
||||||
<div class="row mb-3{% if field.errors %} has-errors{% endif %}">
|
<div class="row mb-3{% if field.errors %} has-errors{% endif %}">
|
||||||
|
|
||||||
{# Render the field label, except for: #}
|
{# Render the field label, except for checkboxes #}
|
||||||
{# 1. Checkboxes (label appears to the right of the field #}
|
{% if field|widget_type != 'checkboxinput' %}
|
||||||
{# 2. Textareas with no label set (will expand across entire row) #}
|
|
||||||
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' or field|widget_type == 'markdownwidget' and not label %}
|
|
||||||
{% else %}
|
|
||||||
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
|
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -2,9 +2,9 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from dcim.filtersets import CommonInterfaceFilterSet
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import L2VPN, VRF
|
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||||
@ -250,7 +250,7 @@ class VirtualMachineFilterSet(
|
|||||||
return queryset.exclude(params)
|
return queryset.exclude(params)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceFilterSet(NetBoxModelFilterSet):
|
class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet):
|
||||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_machine__cluster',
|
field_name='virtual_machine__cluster',
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
@ -286,28 +286,6 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet):
|
|||||||
mac_address = MultiValueMACAddressFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
label=_('MAC address'),
|
label=_('MAC address'),
|
||||||
)
|
)
|
||||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='vrf',
|
|
||||||
queryset=VRF.objects.all(),
|
|
||||||
label=_('VRF'),
|
|
||||||
)
|
|
||||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='vrf__rd',
|
|
||||||
queryset=VRF.objects.all(),
|
|
||||||
to_field_name='rd',
|
|
||||||
label=_('VRF (RD)'),
|
|
||||||
)
|
|
||||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='l2vpn_terminations__l2vpn',
|
|
||||||
queryset=L2VPN.objects.all(),
|
|
||||||
label=_('L2VPN (ID)'),
|
|
||||||
)
|
|
||||||
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='l2vpn_terminations__l2vpn__identifier',
|
|
||||||
queryset=L2VPN.objects.all(),
|
|
||||||
to_field_name='identifier',
|
|
||||||
label=_('L2VPN'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
@ -80,6 +80,7 @@ class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = ClusterType.objects.annotate(
|
queryset = ClusterType.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'type')
|
cluster_count=count_related(Cluster, 'type')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.ClusterTypeFilterSet
|
||||||
table = tables.ClusterTypeTable
|
table = tables.ClusterTypeTable
|
||||||
|
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
queryset = ClusterGroup.objects.annotate(
|
queryset = ClusterGroup.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'group')
|
cluster_count=count_related(Cluster, 'group')
|
||||||
)
|
)
|
||||||
|
filterset = filtersets.ClusterGroupFilterSet
|
||||||
table = tables.ClusterGroupTable
|
table = tables.ClusterGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user