mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Merge ac26665f29
into 6df0a02d8d
This commit is contained in:
commit
596336947a
@ -44,7 +44,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'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))
|
||||
|
@ -186,7 +186,8 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
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,
|
||||
editable=False
|
||||
)
|
||||
message = models.CharField(
|
||||
verbose_name=_('message'),
|
||||
max_length=200,
|
||||
editable=False,
|
||||
blank=True
|
||||
)
|
||||
prechange_data = models.JSONField(
|
||||
verbose_name=_('pre-change data'),
|
||||
editable=False,
|
||||
|
@ -41,6 +41,9 @@ class ObjectChangeTable(NetBoxTable):
|
||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||
verbose_name=_('Request ID')
|
||||
)
|
||||
message = tables.Column(
|
||||
verbose_name=_('Message'),
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=()
|
||||
)
|
||||
@ -49,5 +52,8 @@ class ObjectChangeTable(NetBoxTable):
|
||||
model = ObjectChange
|
||||
fields = (
|
||||
'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):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = ObjectChangeFilterSet
|
||||
ignore_fields = ('prechange_data', 'postchange_data')
|
||||
ignore_fields = ('message', 'prechange_data', 'postchange_data')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -10,7 +10,12 @@ from .nested import *
|
||||
# Base model serializers
|
||||
#
|
||||
|
||||
class NetBoxModelSerializer(TaggableModelSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
|
||||
class NetBoxModelSerializer(
|
||||
ChangeLogMessageSerializer,
|
||||
TaggableModelSerializer,
|
||||
CustomFieldModelSerializer,
|
||||
ValidatedModelSerializer
|
||||
):
|
||||
"""
|
||||
Adds support for custom fields and tags.
|
||||
"""
|
||||
@ -24,5 +29,5 @@ class NestedGroupModelSerializer(NetBoxModelSerializer):
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
|
||||
class BulkOperationSerializer(serializers.Serializer):
|
||||
class BulkOperationSerializer(ChangeLogMessageSerializer):
|
||||
id = serializers.IntegerField()
|
||||
|
@ -5,6 +5,7 @@ from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultVal
|
||||
from .nested import NestedTagSerializer
|
||||
|
||||
__all__ = (
|
||||
'ChangeLogMessageSerializer',
|
||||
'CustomFieldModelSerializer',
|
||||
'TaggableModelSerializer',
|
||||
)
|
||||
@ -54,3 +55,22 @@ class TaggableModelSerializer(serializers.Serializer):
|
||||
instance.tags.clear()
|
||||
|
||||
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 netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from rest_framework import mixins as drf_mixins
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
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.exceptions import AbortRequest
|
||||
from utilities.query import reapply_model_ordering
|
||||
@ -199,9 +201,16 @@ class NetBoxModelViewSet(
|
||||
# Deletes
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Hotwire get_object() to ensure we save a pre-change snapshot
|
||||
self.get_object = self.get_object_with_snapshot
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
instance = self.get_object_with_snapshot()
|
||||
|
||||
# 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):
|
||||
model = self.queryset.model
|
||||
|
@ -11,7 +11,7 @@ from extras.models import CustomField, Tag
|
||||
from utilities.forms import BulkEditForm, CSVModelForm
|
||||
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.mixins import CheckLastUpdatedMixin
|
||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
||||
from .mixins import ChangeLoggingMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin
|
||||
|
||||
__all__ = (
|
||||
'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.
|
||||
|
||||
@ -100,7 +100,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
||||
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
|
||||
fields and adding/removing tags.
|
||||
|
@ -7,12 +7,23 @@ from extras.models import *
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
|
||||
__all__ = (
|
||||
'ChangeLoggingMixin',
|
||||
'CustomFieldsMixin',
|
||||
'SavedFiltersMixin',
|
||||
'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:
|
||||
"""
|
||||
Extend a Form to include custom field support.
|
||||
|
@ -66,6 +66,11 @@ class ChangeLoggingMixin(DeleteMixin, models.Model):
|
||||
class Meta:
|
||||
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):
|
||||
"""
|
||||
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(
|
||||
changed_object=self,
|
||||
object_repr=str(self)[:200],
|
||||
action=action
|
||||
action=action,
|
||||
message=self._changelog_message or '',
|
||||
)
|
||||
if hasattr(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 extras.choices import CustomFieldUIEditableChoices
|
||||
from extras.models import CustomField, ExportTemplate
|
||||
from netbox.forms.mixins 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, AbortTransaction, PermissionsViolation
|
||||
@ -423,7 +424,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
} if prefetch_ids else {}
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
instance = None
|
||||
object_id = int(record.pop('id')) if record.get('id') else None
|
||||
|
||||
# Determine whether this object is being created or updated
|
||||
@ -439,6 +439,8 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
instance.snapshot()
|
||||
|
||||
else:
|
||||
instance = self.queryset.model()
|
||||
|
||||
# For newly created objects, apply any default custom field values
|
||||
custom_fields = CustomField.objects.filter(
|
||||
object_types=ContentType.objects.get_for_model(self.queryset.model),
|
||||
@ -449,6 +451,9 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if field_name not in record:
|
||||
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
|
||||
model_form_kwargs = {
|
||||
'data': record,
|
||||
@ -622,6 +627,9 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if hasattr(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.
|
||||
for name, model_field in model_fields.items():
|
||||
# Handle nullification
|
||||
@ -892,7 +900,7 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
"""
|
||||
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)
|
||||
|
||||
return BulkDeleteForm
|
||||
@ -939,9 +947,15 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
try:
|
||||
with transaction.atomic(using=router.db_for_write(model)):
|
||||
for obj in queryset:
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(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()
|
||||
|
||||
except (ProtectedError, RestrictedError) as e:
|
||||
|
@ -19,7 +19,7 @@ from netbox.object_actions import (
|
||||
)
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
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.permissions import get_permission_for_model
|
||||
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
||||
@ -288,6 +288,9 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
if form.is_valid():
|
||||
logger.debug("Form validation was successful")
|
||||
|
||||
# Record changelog message (if any)
|
||||
obj._changelog_message = form.cleaned_data.pop('changelog_message', '')
|
||||
|
||||
try:
|
||||
with transaction.atomic(using=router.db_for_write(model)):
|
||||
object_created = form.instance.pk is None
|
||||
@ -422,7 +425,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
||||
request: The current request
|
||||
"""
|
||||
obj = self.get_object(**kwargs)
|
||||
form = ConfirmationForm(initial=request.GET)
|
||||
form = DeleteForm(initial=request.GET)
|
||||
|
||||
try:
|
||||
dependent_objects = self._get_dependent_objects(obj)
|
||||
@ -461,23 +464,25 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
|
||||
"""
|
||||
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
||||
obj = self.get_object(**kwargs)
|
||||
form = ConfirmationForm(request.POST)
|
||||
|
||||
# Take a snapshot of change-logged models
|
||||
if hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
form = DeleteForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
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:
|
||||
obj.delete()
|
||||
|
||||
except (ProtectedError, RestrictedError) as e:
|
||||
logger.info(f"Caught {type(e)} while attempting to delete objects")
|
||||
handle_protectederror([obj], request, e)
|
||||
return redirect(obj.get_absolute_url())
|
||||
|
||||
except AbortRequest as e:
|
||||
logger.debug(e.message)
|
||||
messages.error(request, mark_safe(e.message))
|
||||
|
@ -64,10 +64,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Message" %}</th>
|
||||
<td>
|
||||
{{ object.message|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Request ID" %}</th>
|
||||
<td>
|
||||
{{ object.request_id }}
|
||||
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -66,7 +66,10 @@ Context:
|
||||
{% endfor %}
|
||||
|
||||
{# 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 %}
|
||||
</div>
|
||||
|
||||
|
@ -103,7 +103,10 @@ Context:
|
||||
{% endif %}
|
||||
|
||||
{# 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 %}
|
||||
</div>
|
||||
|
||||
|
@ -42,32 +42,31 @@ Context:
|
||||
|
||||
{# Data Import Form #}
|
||||
<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">
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="import_method" value="direct" />
|
||||
<div class="col col-md-12 col-lg-10 offset-lg-1">
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="import_method" value="direct" />
|
||||
|
||||
{# Form fields #}
|
||||
{% render_field form.data %}
|
||||
{% render_field form.format %}
|
||||
{% render_field form.csv_delimiter %}
|
||||
{# Form fields #}
|
||||
{% render_field form.data %}
|
||||
{% render_field form.format %}
|
||||
{% render_field form.csv_delimiter %}
|
||||
|
||||
{# Meta fields #}
|
||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||
{% render_field form.background_job %}
|
||||
{# Meta fields #}
|
||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||
{% 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 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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -83,6 +82,11 @@ Context:
|
||||
{% render_field form.format %}
|
||||
{% 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="col col-md-12 text-end">
|
||||
{% if return_url %}
|
||||
@ -110,6 +114,7 @@ Context:
|
||||
|
||||
{# Meta fields #}
|
||||
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 mb-3">
|
||||
{% render_field form.changelog_message %}
|
||||
{% render_field form.background_job %}
|
||||
</div>
|
||||
|
||||
|
@ -20,7 +20,7 @@ Context:
|
||||
{% endblock %}
|
||||
|
||||
{% 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-content border-1 border-danger">
|
||||
{% include 'htmx/delete_form.html' %}
|
||||
|
@ -2,10 +2,16 @@
|
||||
{% load i18n %}
|
||||
|
||||
<form action="{{ form_url }}" method="post">
|
||||
{# Render hidden fields #}
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Confirm Deletion" %}</h5>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
@ -16,10 +22,10 @@
|
||||
<p>
|
||||
{% trans "The following objects will be deleted as a result of this action." %}
|
||||
</p>
|
||||
<div class="accordion" id="deleteAccordion">
|
||||
<div class="accordion mb-3" id="deleteAccordion">
|
||||
{% for model, instances in dependent_objects.items %}
|
||||
<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 }}">
|
||||
{% with object_count=instances|length %}
|
||||
{{ object_count }}
|
||||
@ -46,8 +52,15 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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 class="modal-footer">
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||
|
@ -28,6 +28,13 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
{# 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-content" id="htmx-modal-content">
|
||||
{# Dynamic content goes here #}
|
||||
|
@ -8,12 +8,13 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
|
||||
from netbox.forms.mixins import ChangeLoggingMixin
|
||||
from utilities.constants import CSV_DELIMITERS
|
||||
from utilities.forms.mixins import BackgroundJobMixin
|
||||
from utilities.forms.utils import parse_csv
|
||||
|
||||
|
||||
class BulkImportForm(BackgroundJobMixin, SyncedDataMixin, forms.Form):
|
||||
class BulkImportForm(ChangeLoggingMixin, BackgroundJobMixin, SyncedDataMixin, forms.Form):
|
||||
import_method = forms.ChoiceField(
|
||||
choices=ImportMethodChoices,
|
||||
required=False
|
||||
|
@ -10,6 +10,7 @@ __all__ = (
|
||||
'BulkRenameForm',
|
||||
'ConfirmationForm',
|
||||
'CSVModelForm',
|
||||
'DeleteForm',
|
||||
'FilterForm',
|
||||
'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):
|
||||
"""
|
||||
Provides bulk edit support for objects.
|
||||
|
Loading…
Reference in New Issue
Block a user