mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Merge f600429b7e
into 6df0a02d8d
This commit is contained in:
commit
7c811d5eb2
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -63,6 +63,8 @@ class ChangeLoggingMixin(DeleteMixin, models.Model):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_changelog_message = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@ -103,7 +105,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