Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions netbox/netbox/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .bulk_edit import *
from .bulk_import import *
from .bulk_rename import *
from .filtersets import *
from .model_forms import *
from .search import *
40 changes: 40 additions & 0 deletions netbox/netbox/forms/bulk_rename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import re

from django import forms
from django.utils.translation import gettext as _

from .mixins import ChangelogMessageMixin

__all__ = (
'BulkRenameForm',
)


class BulkRenameForm(ChangelogMessageMixin, forms.Form):
"""
An extendable form to be used for renaming objects in bulk.
"""
find = forms.CharField(
strip=False
)
replace = forms.CharField(
strip=False,
required=False
)
use_regex = forms.BooleanField(
required=False,
initial=True,
label=_('Use regular expressions')
)

def clean(self):
super().clean()

# Validate regular expression in "find" field
if self.cleaned_data['use_regex']:
try:
re.compile(self.cleaned_data['find'])
except re.error:
raise forms.ValidationError({
'find': "Invalid regular expression"
})
10 changes: 9 additions & 1 deletion netbox/netbox/views/generic/bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@
from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate
from netbox.forms.bulk_rename import BulkRenameForm
from netbox.models.features import ChangeLoggingMixin
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.export import TableExport, stream_table_csv_response
from utilities.forms import BulkDeleteForm, BulkRenameForm, restrict_form_fields
from utilities.forms import BulkDeleteForm, restrict_form_fields
from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial
from utilities.jobs import is_background_request, process_request_as_job
Expand Down Expand Up @@ -890,6 +891,11 @@ class _Form(BulkRenameForm):

self.form = _Form

# Remove changelog_message field if model doesn't support change logging.
# Mutating base_fields is safe here because _Form is created fresh per request above.
if not issubclass(self.queryset.model, ChangeLoggingMixin):
self.form.base_fields.pop('changelog_message', None)

def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'change')

Expand Down Expand Up @@ -940,10 +946,12 @@ def post(self, request):
with self.queryset.model.objects.delay_mptt_updates():
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj._changelog_message = form.cleaned_data.get('changelog_message', '')
obj.save()
else:
for obj in selected_objects:
setattr(obj, self.field_name, obj.new_name)
obj._changelog_message = form.cleaned_data.get('changelog_message', '')
obj.save()

# Enforce constrained permissions
Expand Down
33 changes: 0 additions & 33 deletions netbox/utilities/forms/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import re

from django import forms
from django.utils.translation import gettext as _

Expand All @@ -10,7 +8,6 @@
__all__ = (
'BulkDeleteForm',
'BulkEditForm',
'BulkRenameForm',
'CSVModelForm',
'ConfirmationForm',
'DeleteForm',
Expand Down Expand Up @@ -61,36 +58,6 @@ class BulkEditForm(BackgroundJobMixin, forms.Form):
nullable_fields = ()


class BulkRenameForm(forms.Form):
"""
An extendable form to be used for renaming objects in bulk.
"""
find = forms.CharField(
strip=False
)
replace = forms.CharField(
strip=False,
required=False
)
use_regex = forms.BooleanField(
required=False,
initial=True,
label=_('Use regular expressions')
)

def clean(self):
super().clean()

# Validate regular expression in "find" field
if self.cleaned_data['use_regex']:
try:
re.compile(self.cleaned_data['find'])
except re.error:
raise forms.ValidationError({
'find': "Invalid regular expression"
})


class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm):
pk = forms.ModelMultipleChoiceField(
queryset=None,
Expand Down
35 changes: 35 additions & 0 deletions netbox/utilities/testing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,41 @@ def test_bulk_rename_objects_with_permission(self):
for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
self.assertEqual(instance.name, f'{objects[i].name}X')

@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_bulk_rename_objects_with_changelog_message(self):
if not issubclass(self.model, ChangeLoggingMixin):
self.skipTest("Model does not support change logging")
objects = self._get_queryset().all()[:3]
pk_list = [obj.pk for obj in objects]
data = {
'pk': pk_list,
'_apply': True,
'changelog_message': 'Bulk rename test message',
}
data.update(self.rename_data)

# Assign model-level permission
obj_perm = ObjectPermission(
name='Test permission',
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))

self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)

# Verify changelog message was recorded on each renamed object
object_type = ObjectType.objects.get_for_model(self.model)
for pk in pk_list:
oc = ObjectChange.objects.filter(
changed_object_type=object_type,
changed_object_id=pk,
action=ObjectChangeActionChoices.ACTION_UPDATE,
).order_by('-time').first()
self.assertIsNotNone(oc)
self.assertEqual(oc.message, 'Bulk rename test message')

@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_bulk_rename_objects_with_constrained_permission(self):
objects = self._get_queryset().all()[:3]
Expand Down
2 changes: 1 addition & 1 deletion netbox/utilities/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

from dcim.models import Site
from netbox.choices import ImportFormatChoices
from netbox.forms.bulk_rename import BulkRenameForm
from utilities.forms.bulk_import import BulkImportForm
from utilities.forms.fields.csv import CSVSelectWidget
from utilities.forms.forms import BulkRenameForm
from utilities.forms.utils import (
expand_alphanumeric_pattern,
expand_ipnetwork_pattern,
Expand Down
3 changes: 2 additions & 1 deletion netbox/virtualization/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
from extras.models import ConfigTemplate
from ipam.models import VLAN, VRF, VLANGroup, VLANTranslationPolicy
from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
from netbox.forms.bulk_rename import BulkRenameForm
from netbox.forms.mixins import OwnerMixin
from tenancy.models import Tenant
from utilities.forms import BulkRenameForm, add_blank_choice
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.utils import get_capacity_unit_label
Expand Down
Loading