mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-26 02:18:37 -06:00
Merge ac26665f29
into 6df0a02d8d
This commit is contained in:
commit
596336947a
@ -44,7 +44,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
|||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
||||||
'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data',
|
||||||
|
'postchange_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
@ -186,7 +186,8 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(user_name__icontains=value) |
|
Q(user_name__icontains=value) |
|
||||||
Q(object_repr__icontains=value)
|
Q(object_repr__icontains=value) |
|
||||||
|
Q(message__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
16
netbox/core/migrations/0017_objectchange_message.py
Normal file
16
netbox/core/migrations/0017_objectchange_message.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0016_job_log_entries'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='objectchange',
|
||||||
|
name='message',
|
||||||
|
field=models.CharField(blank=True, editable=False, max_length=200),
|
||||||
|
),
|
||||||
|
]
|
@ -82,6 +82,12 @@ class ObjectChange(models.Model):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
|
message = models.CharField(
|
||||||
|
verbose_name=_('message'),
|
||||||
|
max_length=200,
|
||||||
|
editable=False,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
prechange_data = models.JSONField(
|
prechange_data = models.JSONField(
|
||||||
verbose_name=_('pre-change data'),
|
verbose_name=_('pre-change data'),
|
||||||
editable=False,
|
editable=False,
|
||||||
|
@ -41,6 +41,9 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||||
verbose_name=_('Request ID')
|
verbose_name=_('Request ID')
|
||||||
)
|
)
|
||||||
|
message = tables.Column(
|
||||||
|
verbose_name=_('Message'),
|
||||||
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=()
|
actions=()
|
||||||
)
|
)
|
||||||
@ -49,5 +52,8 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
|
'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
|
||||||
'actions',
|
'message', 'actions',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'message', 'actions',
|
||||||
)
|
)
|
||||||
|
@ -150,7 +150,7 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = ObjectChange.objects.all()
|
queryset = ObjectChange.objects.all()
|
||||||
filterset = ObjectChangeFilterSet
|
filterset = ObjectChangeFilterSet
|
||||||
ignore_fields = ('prechange_data', 'postchange_data')
|
ignore_fields = ('message', 'prechange_data', 'postchange_data')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
@ -10,7 +10,12 @@ from .nested import *
|
|||||||
# Base model serializers
|
# Base model serializers
|
||||||
#
|
#
|
||||||
|
|
||||||
class NetBoxModelSerializer(TaggableModelSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
|
class NetBoxModelSerializer(
|
||||||
|
ChangeLogMessageSerializer,
|
||||||
|
TaggableModelSerializer,
|
||||||
|
CustomFieldModelSerializer,
|
||||||
|
ValidatedModelSerializer
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Adds support for custom fields and tags.
|
Adds support for custom fields and tags.
|
||||||
"""
|
"""
|
||||||
@ -24,5 +29,5 @@ class NestedGroupModelSerializer(NetBoxModelSerializer):
|
|||||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class BulkOperationSerializer(serializers.Serializer):
|
class BulkOperationSerializer(ChangeLogMessageSerializer):
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
|
@ -5,6 +5,7 @@ from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultVal
|
|||||||
from .nested import NestedTagSerializer
|
from .nested import NestedTagSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ChangeLogMessageSerializer',
|
||||||
'CustomFieldModelSerializer',
|
'CustomFieldModelSerializer',
|
||||||
'TaggableModelSerializer',
|
'TaggableModelSerializer',
|
||||||
)
|
)
|
||||||
@ -54,3 +55,22 @@ class TaggableModelSerializer(serializers.Serializer):
|
|||||||
instance.tags.clear()
|
instance.tags.clear()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeLogMessageSerializer(serializers.Serializer):
|
||||||
|
changelog_message = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
ret = super().to_internal_value(data)
|
||||||
|
|
||||||
|
# Workaround to bypass requirement to include changelog_message in Meta.fields on every serializer
|
||||||
|
if 'changelog_message' in data and 'changelog_message' not in ret:
|
||||||
|
# TODO: Validation
|
||||||
|
ret['changelog_message'] = data['changelog_message']
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if self.instance is not None:
|
||||||
|
self.instance._changelog_message = self.validated_data.get('changelog_message')
|
||||||
|
return super().save(**kwargs)
|
||||||
|
@ -7,9 +7,11 @@ from django.db.models import ProtectedError, RestrictedError
|
|||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
from rest_framework import mixins as drf_mixins
|
from rest_framework import mixins as drf_mixins
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
from netbox.api.serializers.features import ChangeLogMessageSerializer
|
||||||
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||||
from utilities.exceptions import AbortRequest
|
from utilities.exceptions import AbortRequest
|
||||||
from utilities.query import reapply_model_ordering
|
from utilities.query import reapply_model_ordering
|
||||||
@ -199,9 +201,16 @@ class NetBoxModelViewSet(
|
|||||||
# Deletes
|
# Deletes
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
instance = self.get_object_with_snapshot()
|
||||||
self.get_object = self.get_object_with_snapshot
|
|
||||||
return super().destroy(request, *args, **kwargs)
|
# Attach changelog message (if any)
|
||||||
|
serializer = ChangeLogMessageSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
instance._changelog_message = serializer.validated_data.get('changelog_message')
|
||||||
|
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
model = self.queryset.model
|
model = self.queryset.model
|
||||||
|
@ -11,7 +11,7 @@ from extras.models import CustomField, Tag
|
|||||||
from utilities.forms import BulkEditForm, CSVModelForm
|
from utilities.forms import BulkEditForm, CSVModelForm
|
||||||
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
|
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.mixins import CheckLastUpdatedMixin
|
from utilities.forms.mixins import CheckLastUpdatedMixin
|
||||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
from .mixins import ChangeLoggingMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'NetBoxModelForm',
|
'NetBoxModelForm',
|
||||||
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
|
class NetBoxModelForm(ChangeLoggingMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
|
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
|||||||
return customfield.to_form_field(for_csv_import=True)
|
return customfield.to_form_field(for_csv_import=True)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxModelBulkEditForm(CustomFieldsMixin, BulkEditForm):
|
class NetBoxModelBulkEditForm(ChangeLoggingMixin, CustomFieldsMixin, BulkEditForm):
|
||||||
"""
|
"""
|
||||||
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
|
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
|
||||||
fields and adding/removing tags.
|
fields and adding/removing tags.
|
||||||
|
@ -7,12 +7,23 @@ from extras.models import *
|
|||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ChangeLoggingMixin',
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
'SavedFiltersMixin',
|
'SavedFiltersMixin',
|
||||||
'TagsMixin',
|
'TagsMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeLoggingMixin(forms.Form):
|
||||||
|
"""
|
||||||
|
Adds an optional field for recording a message on the resulting changelog record(s).
|
||||||
|
"""
|
||||||
|
changelog_message = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
max_length=200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldsMixin:
|
class CustomFieldsMixin:
|
||||||
"""
|
"""
|
||||||
Extend a Form to include custom field support.
|
Extend a Form to include custom field support.
|
||||||
|
@ -66,6 +66,11 @@ class ChangeLoggingMixin(DeleteMixin, models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
changelog_message = kwargs.pop('changelog_message', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._changelog_message = changelog_message
|
||||||
|
|
||||||
def serialize_object(self, exclude=None):
|
def serialize_object(self, exclude=None):
|
||||||
"""
|
"""
|
||||||
Return a JSON representation of the instance. Models can override this method to replace or extend the default
|
Return a JSON representation of the instance. Models can override this method to replace or extend the default
|
||||||
@ -103,7 +108,8 @@ class ChangeLoggingMixin(DeleteMixin, models.Model):
|
|||||||
objectchange = ObjectChange(
|
objectchange = ObjectChange(
|
||||||
changed_object=self,
|
changed_object=self,
|
||||||
object_repr=str(self)[:200],
|
object_repr=str(self)[:200],
|
||||||
action=action
|
action=action,
|
||||||
|
message=self._changelog_message or '',
|
||||||
)
|
)
|
||||||
if hasattr(self, '_prechange_snapshot'):
|
if hasattr(self, '_prechange_snapshot'):
|
||||||
objectchange.prechange_data = self._prechange_snapshot
|
objectchange.prechange_data = self._prechange_snapshot
|
||||||
|
@ -21,6 +21,7 @@ from core.models import ObjectType
|
|||||||
from core.signals import clear_events
|
from core.signals import clear_events
|
||||||
from extras.choices import CustomFieldUIEditableChoices
|
from extras.choices import CustomFieldUIEditableChoices
|
||||||
from extras.models import CustomField, ExportTemplate
|
from extras.models import CustomField, ExportTemplate
|
||||||
|
from netbox.forms.mixins import ChangeLoggingMixin
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
|
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
|
||||||
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
|
||||||
@ -423,7 +424,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
} if prefetch_ids else {}
|
} if prefetch_ids else {}
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
for i, record in enumerate(records, start=1):
|
||||||
instance = None
|
|
||||||
object_id = int(record.pop('id')) if record.get('id') else None
|
object_id = int(record.pop('id')) if record.get('id') else None
|
||||||
|
|
||||||
# Determine whether this object is being created or updated
|
# Determine whether this object is being created or updated
|
||||||
@ -439,6 +439,8 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
instance.snapshot()
|
instance.snapshot()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
instance = self.queryset.model()
|
||||||
|
|
||||||
# For newly created objects, apply any default custom field values
|
# For newly created objects, apply any default custom field values
|
||||||
custom_fields = CustomField.objects.filter(
|
custom_fields = CustomField.objects.filter(
|
||||||
object_types=ContentType.objects.get_for_model(self.queryset.model),
|
object_types=ContentType.objects.get_for_model(self.queryset.model),
|
||||||
@ -449,6 +451,9 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
if field_name not in record:
|
if field_name not in record:
|
||||||
record[field_name] = cf.default
|
record[field_name] = cf.default
|
||||||
|
|
||||||
|
# Record changelog message (if any)
|
||||||
|
instance._changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||||
|
|
||||||
# Instantiate the model form for the object
|
# Instantiate the model form for the object
|
||||||
model_form_kwargs = {
|
model_form_kwargs = {
|
||||||
'data': record,
|
'data': record,
|
||||||
@ -622,6 +627,9 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
if hasattr(obj, 'snapshot'):
|
if hasattr(obj, 'snapshot'):
|
||||||
obj.snapshot()
|
obj.snapshot()
|
||||||
|
|
||||||
|
# Attach the changelog message (if any) to the object
|
||||||
|
obj._changelog_message = form.cleaned_data.get('changelog_message')
|
||||||
|
|
||||||
# Update standard fields. If a field is listed in _nullify, delete its value.
|
# Update standard fields. If a field is listed in _nullify, delete its value.
|
||||||
for name, model_field in model_fields.items():
|
for name, model_field in model_fields.items():
|
||||||
# Handle nullification
|
# Handle nullification
|
||||||
@ -892,7 +900,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
"""
|
"""
|
||||||
Provide a standard bulk delete form if none has been specified for the view
|
Provide a standard bulk delete form if none has been specified for the view
|
||||||
"""
|
"""
|
||||||
class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm):
|
class BulkDeleteForm(BackgroundJobMixin, ChangeLoggingMixin, ConfirmationForm):
|
||||||
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
||||||
|
|
||||||
return BulkDeleteForm
|
return BulkDeleteForm
|
||||||
@ -939,9 +947,15 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic(using=router.db_for_write(model)):
|
with transaction.atomic(using=router.db_for_write(model)):
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
|
|
||||||
# Take a snapshot of change-logged models
|
# Take a snapshot of change-logged models
|
||||||
if hasattr(obj, 'snapshot'):
|
if hasattr(obj, 'snapshot'):
|
||||||
obj.snapshot()
|
obj.snapshot()
|
||||||
|
|
||||||
|
# Attach the changelog message (if any) to the object
|
||||||
|
obj._changelog_message = form.cleaned_data.get('changelog_message')
|
||||||
|
|
||||||
|
# Delete the object
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
except (ProtectedError, RestrictedError) as e:
|
except (ProtectedError, RestrictedError) as e:
|
||||||
|
@ -19,7 +19,7 @@ from netbox.object_actions import (
|
|||||||
)
|
)
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
from utilities.forms import DeleteForm, restrict_form_fields
|
||||||
from utilities.htmx import htmx_partial
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
||||||
@ -288,6 +288,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
|
# Record changelog message (if any)
|
||||||
|
obj._changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic(using=router.db_for_write(model)):
|
with transaction.atomic(using=router.db_for_write(model)):
|
||||||
object_created = form.instance.pk is None
|
object_created = form.instance.pk is None
|
||||||
@ -422,7 +425,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
|||||||
request: The current request
|
request: The current request
|
||||||
"""
|
"""
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
form = ConfirmationForm(initial=request.GET)
|
form = DeleteForm(initial=request.GET)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dependent_objects = self._get_dependent_objects(obj)
|
dependent_objects = self._get_dependent_objects(obj)
|
||||||
@ -461,23 +464,25 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
|||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
||||||
obj = self.get_object(**kwargs)
|
obj = self.get_object(**kwargs)
|
||||||
form = ConfirmationForm(request.POST)
|
form = DeleteForm(request.POST)
|
||||||
|
|
||||||
# Take a snapshot of change-logged models
|
|
||||||
if hasattr(obj, 'snapshot'):
|
|
||||||
obj.snapshot()
|
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
|
# Take a snapshot of change-logged models
|
||||||
|
if hasattr(obj, 'snapshot'):
|
||||||
|
obj.snapshot()
|
||||||
|
|
||||||
|
# Record changelog message (if any)
|
||||||
|
obj._changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||||
|
|
||||||
|
# Delete the object
|
||||||
try:
|
try:
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
except (ProtectedError, RestrictedError) as e:
|
except (ProtectedError, RestrictedError) as e:
|
||||||
logger.info(f"Caught {type(e)} while attempting to delete objects")
|
logger.info(f"Caught {type(e)} while attempting to delete objects")
|
||||||
handle_protectederror([obj], request, e)
|
handle_protectederror([obj], request, e)
|
||||||
return redirect(obj.get_absolute_url())
|
return redirect(obj.get_absolute_url())
|
||||||
|
|
||||||
except AbortRequest as e:
|
except AbortRequest as e:
|
||||||
logger.debug(e.message)
|
logger.debug(e.message)
|
||||||
messages.error(request, mark_safe(e.message))
|
messages.error(request, mark_safe(e.message))
|
||||||
|
@ -64,10 +64,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Message" %}</th>
|
||||||
|
<td>
|
||||||
|
{{ object.message|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Request ID" %}</th>
|
<th scope="row">{% trans "Request ID" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.request_id }}
|
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -66,7 +66,10 @@ Context:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{# Meta fields #}
|
{# Meta fields #}
|
||||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
|
||||||
|
{% if form.changelog_message %}
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
|
{% endif %}
|
||||||
{% render_field form.background_job %}
|
{% render_field form.background_job %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -103,7 +103,10 @@ Context:
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Meta fields #}
|
{# Meta fields #}
|
||||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
|
||||||
|
{% if form.changelog_message %}
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
|
{% endif %}
|
||||||
{% render_field form.background_job %}
|
{% render_field form.background_job %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -42,32 +42,31 @@ Context:
|
|||||||
|
|
||||||
{# Data Import Form #}
|
{# Data Import Form #}
|
||||||
<div class="tab-pane show active" id="import-form" role="tabpanel" aria-labelledby="import-form-tab">
|
<div class="tab-pane show active" id="import-form" role="tabpanel" aria-labelledby="import-form-tab">
|
||||||
<div class="row">
|
<div class="col col-md-12 col-lg-10 offset-lg-1">
|
||||||
<div class="col col-md-12 col-lg-10 offset-lg-1">
|
<form action="" method="post" enctype="multipart/form-data" class="form">
|
||||||
<form action="" method="post" enctype="multipart/form-data" class="form">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="hidden" name="import_method" value="direct" />
|
||||||
<input type="hidden" name="import_method" value="direct" />
|
|
||||||
|
|
||||||
{# Form fields #}
|
{# Form fields #}
|
||||||
{% render_field form.data %}
|
{% render_field form.data %}
|
||||||
{% render_field form.format %}
|
{% render_field form.format %}
|
||||||
{% render_field form.csv_delimiter %}
|
{% render_field form.csv_delimiter %}
|
||||||
|
|
||||||
{# Meta fields #}
|
{# Meta fields #}
|
||||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||||
{% render_field form.background_job %}
|
{% render_field form.changelog_message %}
|
||||||
|
{% render_field form.background_job %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col col-md-12 text-end">
|
||||||
|
{% if return_url %}
|
||||||
|
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" name="data_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
</form>
|
||||||
<div class="col col-md-12 text-end">
|
|
||||||
{% if return_url %}
|
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
<button type="submit" name="data_submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -83,6 +82,11 @@ Context:
|
|||||||
{% render_field form.format %}
|
{% render_field form.format %}
|
||||||
{% render_field form.csv_delimiter %}
|
{% render_field form.csv_delimiter %}
|
||||||
|
|
||||||
|
{# Meta fields #}
|
||||||
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
|
</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">
|
||||||
{% if return_url %}
|
{% if return_url %}
|
||||||
@ -110,6 +114,7 @@ Context:
|
|||||||
|
|
||||||
{# Meta fields #}
|
{# Meta fields #}
|
||||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
{% render_field form.background_job %}
|
{% render_field form.background_job %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ Context:
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="modal" tabindex="-1" style="display: block; position: static">
|
<div class="modal modal-lg" tabindex="-1" style="display: block; position: static">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content border-1 border-danger">
|
<div class="modal-content border-1 border-danger">
|
||||||
{% include 'htmx/delete_form.html' %}
|
{% include 'htmx/delete_form.html' %}
|
||||||
|
@ -2,10 +2,16 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<form action="{{ form_url }}" method="post">
|
<form action="{{ form_url }}" method="post">
|
||||||
|
{# Render hidden fields #}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">{% trans "Confirm Deletion" %}</h5>
|
<h5 class="modal-title">{% trans "Confirm Deletion" %}</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
@ -16,10 +22,10 @@
|
|||||||
<p>
|
<p>
|
||||||
{% trans "The following objects will be deleted as a result of this action." %}
|
{% trans "The following objects will be deleted as a result of this action." %}
|
||||||
</p>
|
</p>
|
||||||
<div class="accordion" id="deleteAccordion">
|
<div class="accordion mb-3" id="deleteAccordion">
|
||||||
{% for model, instances in dependent_objects.items %}
|
{% for model, instances in dependent_objects.items %}
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="deleteheading{{ forloop.counter }}">
|
<h2 class="accordion-header h4" id="deleteheading{{ forloop.counter }}">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ forloop.counter }}" aria-expanded="false" aria-controls="collapse{{ forloop.counter }}">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ forloop.counter }}" aria-expanded="false" aria-controls="collapse{{ forloop.counter }}">
|
||||||
{% with object_count=instances|length %}
|
{% with object_count=instances|length %}
|
||||||
{{ object_count }}
|
{{ object_count }}
|
||||||
@ -46,8 +52,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% render_form form %}
|
|
||||||
|
{# Meta fields #}
|
||||||
|
{% if form.changelog_message %}
|
||||||
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3">
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
{% if return_url %}
|
{% if return_url %}
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||||
|
@ -28,6 +28,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Meta fields #}
|
||||||
|
{% if form.changelog_message %}
|
||||||
|
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
|
||||||
|
{% render_field form.changelog_message %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{# Render all fields in a single group #}
|
{# Render all fields in a single group #}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
|
<div class="modal modal-lg fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog{% if size %} modal-{{ size }}{% endif %}">
|
<div class="modal-dialog{% if size %} modal-{{ size }}{% endif %}">
|
||||||
<div class="modal-content" id="htmx-modal-content">
|
<div class="modal-content" id="htmx-modal-content">
|
||||||
{# Dynamic content goes here #}
|
{# Dynamic content goes here #}
|
||||||
|
@ -8,12 +8,13 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from core.forms.mixins import SyncedDataMixin
|
from core.forms.mixins import SyncedDataMixin
|
||||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
|
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
|
||||||
|
from netbox.forms.mixins import ChangeLoggingMixin
|
||||||
from utilities.constants import CSV_DELIMITERS
|
from utilities.constants import CSV_DELIMITERS
|
||||||
from utilities.forms.mixins import BackgroundJobMixin
|
from utilities.forms.mixins import BackgroundJobMixin
|
||||||
from utilities.forms.utils import parse_csv
|
from utilities.forms.utils import parse_csv
|
||||||
|
|
||||||
|
|
||||||
class BulkImportForm(BackgroundJobMixin, SyncedDataMixin, forms.Form):
|
class BulkImportForm(ChangeLoggingMixin, BackgroundJobMixin, SyncedDataMixin, forms.Form):
|
||||||
import_method = forms.ChoiceField(
|
import_method = forms.ChoiceField(
|
||||||
choices=ImportMethodChoices,
|
choices=ImportMethodChoices,
|
||||||
required=False
|
required=False
|
||||||
|
@ -10,6 +10,7 @@ __all__ = (
|
|||||||
'BulkRenameForm',
|
'BulkRenameForm',
|
||||||
'ConfirmationForm',
|
'ConfirmationForm',
|
||||||
'CSVModelForm',
|
'CSVModelForm',
|
||||||
|
'DeleteForm',
|
||||||
'FilterForm',
|
'FilterForm',
|
||||||
'TableConfigForm',
|
'TableConfigForm',
|
||||||
)
|
)
|
||||||
@ -30,6 +31,16 @@ class ConfirmationForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteForm(ConfirmationForm):
|
||||||
|
"""
|
||||||
|
Confirm the deletion of an object, optionally providing a changelog message.
|
||||||
|
"""
|
||||||
|
changelog_message = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
max_length=200
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkEditForm(BackgroundJobMixin, forms.Form):
|
class BulkEditForm(BackgroundJobMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Provides bulk edit support for objects.
|
Provides bulk edit support for objects.
|
||||||
|
Loading…
Reference in New Issue
Block a user