Merge branch 'develop' into 11091-token-permission

This commit is contained in:
Arthur 2023-04-07 11:53:22 -07:00
commit 989d6fda64
28 changed files with 82 additions and 169 deletions

View File

@ -58,8 +58,6 @@ as the cornerstone for network automation in thousands of organizations.
[![NetBox Labs](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/netbox_labs.png)](https://netboxlabs.com)
          
[![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud)
          
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com)
<br />
[![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -4,6 +4,7 @@
### 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
### 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
* [#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
* [#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
---

View File

@ -196,6 +196,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.CircuitTypeFilterSet
table = tables.CircuitTypeTable

View File

@ -24,6 +24,7 @@ __all__ = (
'CableFilterSet',
'CabledObjectFilterSet',
'CableTerminationFilterSet',
'CommonInterfaceFilterSet',
'ConsoleConnectionFilterSet',
'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet',
@ -1321,11 +1322,45 @@ class PowerOutletFilterSet(
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(
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CabledObjectFilterSet,
PathEndpointFilterSet
PathEndpointFilterSet,
CommonInterfaceFilterSet
):
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
# members
@ -1370,14 +1405,6 @@ class InterfaceFilterSet(
poe_type = django_filters.MultipleChoiceFilter(
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(
choices=InterfaceTypeChoices,
null_value=None
@ -1388,17 +1415,6 @@ class InterfaceFilterSet(
rf_channel = django_filters.MultipleChoiceFilter(
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(
field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(),
@ -1416,17 +1432,6 @@ class InterfaceFilterSet(
to_field_name='name',
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:
model = Interface

View File

@ -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
(default). Otherwise, save() will be called on each instance individually.
"""
components = [obj.instantiate(device=self) for obj in queryset]
if components and bulk_create:
if bulk_create:
components = [obj.instantiate(device=self) for obj in queryset]
if not components:
return
model = components[0]._meta.model
model.objects.bulk_create(components)
# Manually send the post_save signal for each of the newly created components
@ -788,8 +790,9 @@ class Device(PrimaryModel, ConfigContextModel):
using='default',
update_fields=None
)
elif components:
for component in components:
else:
for obj in queryset:
component = obj.instantiate(device=self)
component.save()
def save(self, *args, **kwargs):

View File

@ -628,6 +628,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role')
)
filterset = filtersets.RackRoleFilterSet
table = tables.RackRoleTable
@ -909,6 +910,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer')
)
filterset = filtersets.ManufacturerFilterSet
table = tables.ManufacturerTable
@ -1808,6 +1810,7 @@ class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
device_count=count_related(Device, 'device_role'),
vm_count=count_related(VirtualMachine, 'role')
)
filterset = filtersets.DeviceRoleFilterSet
table = tables.DeviceRoleTable
@ -1868,6 +1871,7 @@ class PlatformBulkEditView(generic.BulkEditView):
class PlatformBulkDeleteView(generic.BulkDeleteView):
queryset = Platform.objects.all()
filterset = filtersets.PlatformFilterSet
table = tables.PlatformTable
@ -2981,6 +2985,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItem.objects.all()
filterset = filtersets.InventoryItemFilterSet
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'
@ -3038,6 +3043,7 @@ class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItemRole.objects.annotate(
inventoryitem_count=count_related(InventoryItem, 'role'),
)
filterset = filtersets.InventoryItemRoleFilterSet
table = tables.InventoryItemRoleTable

View File

@ -1,13 +1,10 @@
import datetime
from unittest import skipIf
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.timezone import make_aware
from django_rq.queues import get_connection
from rest_framework import status
from rq import Worker
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
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 utilities.testing import APITestCase, APIViewTestCases
rq_worker_running = Worker.count(get_connection('default'))
class AppTest(APITestCase):
@ -539,16 +534,6 @@ class ReportTest(APITestCase):
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):
@ -589,27 +574,6 @@ class ScriptTest(APITestCase):
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
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):

View File

@ -414,6 +414,7 @@ class ConfigContextDeleteView(generic.ObjectDeleteView):
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigContext.objects.all()
filterset = filtersets.ConfigContextFilterSet
table = tables.ConfigContextTable

View File

@ -836,7 +836,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
self.add_permissions('ipam.delete_vlan')
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)
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)

View File

@ -431,6 +431,7 @@ class RoleBulkEditView(generic.BulkEditView):
class RoleBulkDeleteView(generic.BulkDeleteView):
queryset = Role.objects.all()
filterset = filtersets.RoleFilterSet
table = tables.RoleTable

View File

@ -16,7 +16,6 @@ from django_tables2.export import TableExport
from extras.models import ExportTemplate
from extras.signals import clear_webhooks
from utilities.choices import ImportFormatChoices
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields

View File

@ -98,7 +98,7 @@
<div class="card">
<h5 class="card-header text-center">Comments</h5>
<div class="card-body">
{% render_field form.comments %}
{% render_field form.comments %}
</div>
</div>
{% if form.custom_fields %}

View File

@ -111,7 +111,6 @@
</div>
<div class="field-group mb-5">
<h5 class="text-center">Comments</h5>
{% render_field form.comments %}
</div>

View File

@ -85,7 +85,6 @@
{% endif %}
<div class="field-group my-5">
<h5 class="text-center">Comments</h5>
{% render_field form.comments %}
</div>
{% endblock %}

View File

@ -27,7 +27,6 @@
</div>
<div class="field-group my-5">
<h5 class="text-center">Comments</h5>
{% render_field vc_form.comments %}
</div>

View File

@ -85,7 +85,6 @@ Context:
{% if form.comments %}
<div class="field-group mb-5">
<h5 class="text-center">Comments</h5>
{% render_field form.comments %}
</div>
{% endif %}

View File

@ -33,9 +33,6 @@
{% endif %}
<div class="field-group mb-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Comments</h5>
</div>
{% render_field form.comments %}
</div>

View File

@ -139,9 +139,6 @@
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="text-center">Comments</h5>
</div>
{% render_field form.comments %}
</div>

View File

@ -66,9 +66,6 @@
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="text-center">Comments</h5>
</div>
{% render_field form.comments %}
</div>

View File

@ -53,9 +53,6 @@
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="text-center">Comments</h5>
</div>
{% render_field form.comments %}
</div>

View File

@ -56,9 +56,6 @@
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="text-center">Comments</h5>
</div>
{% render_field form.comments %}
</div>

View File

@ -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 %}

View File

@ -84,6 +84,7 @@ class TenantGroupBulkDeleteView(generic.BulkDeleteView):
'tenant_count',
cumulative=True
)
filterset = filtersets.TenantGroupFilterSet
table = tables.TenantGroupTable
@ -247,6 +248,7 @@ class ContactGroupBulkDeleteView(generic.BulkDeleteView):
'contact_count',
cumulative=True
)
filterset = filtersets.ContactGroupFilterSet
table = tables.ContactGroupTable
@ -305,6 +307,7 @@ class ContactRoleBulkEditView(generic.BulkEditView):
class ContactRoleBulkDeleteView(generic.BulkDeleteView):
queryset = ContactRole.objects.all()
filterset = filtersets.ContactRoleFilterSet
table = tables.ContactRoleTable

View File

@ -12,7 +12,7 @@ class AppTest(APITestCase):
def test_root(self):
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)
@ -36,14 +36,17 @@ class UserTest(APIViewTestCases.APIViewTestCase):
'password': 'password6',
},
]
bulk_update_data = {
'email': 'test@example.com',
}
@classmethod
def setUpTestData(cls):
users = (
User(username='User_1'),
User(username='User_2'),
User(username='User_3'),
User(username='User_1', password='password1'),
User(username='User_2', password='password2'),
User(username='User_3', password='password3'),
)
User.objects.bulk_create(users)
@ -74,6 +77,12 @@ class GroupTest(APIViewTestCases.APIViewTestCase):
)
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(
# No GraphQL support for Token

View File

@ -34,8 +34,8 @@ class CommentField(forms.CharField):
Markdown</a> syntax is supported
"""
def __init__(self, *, label='', help_text=help_text, required=False, **kwargs):
super().__init__(label=label, help_text=help_text, required=required, **kwargs)
def __init__(self, *, help_text=help_text, required=False, **kwargs):
super().__init__(help_text=help_text, required=required, **kwargs)
class SlugField(forms.SlugField):

View File

@ -3,11 +3,8 @@
<div class="row mb-3{% if field.errors %} has-errors{% endif %}">
{# Render the field label, except for: #}
{# 1. Checkboxes (label appears to the right of the field #}
{# 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 %}
{# Render the field label, except for checkboxes #}
{% if field|widget_type != 'checkboxinput' %}
<label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
{{ label }}
</label>

View File

@ -2,9 +2,9 @@ import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.filtersets import CommonInterfaceFilterSet
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import L2VPN, VRF
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
@ -250,7 +250,7 @@ class VirtualMachineFilterSet(
return queryset.exclude(params)
class VMInterfaceFilterSet(NetBoxModelFilterSet):
class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet):
cluster_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_machine__cluster',
queryset=Cluster.objects.all(),
@ -286,28 +286,6 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet):
mac_address = MultiValueMACAddressFilter(
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:
model = VMInterface

View File

@ -80,6 +80,7 @@ class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ClusterType.objects.annotate(
cluster_count=count_related(Cluster, 'type')
)
filterset = filtersets.ClusterTypeFilterSet
table = tables.ClusterTypeTable
@ -147,6 +148,7 @@ class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
queryset = ClusterGroup.objects.annotate(
cluster_count=count_related(Cluster, 'group')
)
filterset = filtersets.ClusterGroupFilterSet
table = tables.ClusterGroupTable