mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-23 12:08:43 -06:00
Split base form classes into separate modules under netbox.forms
This commit is contained in:
@@ -6,7 +6,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from netbox.events import get_event_type_choices
|
||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.models import Group, User
|
||||
|
||||
@@ -1,57 +1,5 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.search import LookupTypes
|
||||
from netbox.search.backends import search_backend
|
||||
|
||||
from .base import *
|
||||
|
||||
LOOKUP_CHOICES = (
|
||||
('', _('Partial match')),
|
||||
(LookupTypes.EXACT, _('Exact match')),
|
||||
(LookupTypes.STARTSWITH, _('Starts with')),
|
||||
(LookupTypes.ENDSWITH, _('Ends with')),
|
||||
(LookupTypes.REGEX, _('Regex')),
|
||||
)
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
q = forms.CharField(
|
||||
label=_('Search'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'hx-get': '',
|
||||
'hx-target': '#object_list',
|
||||
'hx-trigger': 'keyup[target.value.length >= 3] changed delay:500ms',
|
||||
}
|
||||
)
|
||||
)
|
||||
obj_types = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
required=False,
|
||||
label=_('Object type(s)')
|
||||
)
|
||||
lookup = forms.ChoiceField(
|
||||
choices=LOOKUP_CHOICES,
|
||||
initial=LookupTypes.PARTIAL,
|
||||
required=False,
|
||||
label=_('Lookup')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['obj_types'].choices = search_backend.get_object_types()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate regular expressions
|
||||
if self.cleaned_data['lookup'] == LookupTypes.REGEX:
|
||||
try:
|
||||
re.compile(self.cleaned_data['q'])
|
||||
except re.error as e:
|
||||
raise forms.ValidationError({
|
||||
'q': f'Invalid regular expression: {e}'
|
||||
})
|
||||
from .model_forms import *
|
||||
from .bulk_import import *
|
||||
from .bulk_edit import *
|
||||
from .filtersets import *
|
||||
from .search import *
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, Tag
|
||||
from users.models import Owner
|
||||
from utilities.forms import BulkEditForm, CSVModelForm
|
||||
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField, CSVModelChoiceField
|
||||
from utilities.forms.mixins import CheckLastUpdatedMixin
|
||||
from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, SavedFiltersMixin, TagsMixin
|
||||
|
||||
__all__ = (
|
||||
'NetBoxModelForm',
|
||||
'NetBoxModelImportForm',
|
||||
'NetBoxModelBulkEditForm',
|
||||
'NetBoxModelFilterSetForm',
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelForm(
|
||||
ChangelogMessageMixin,
|
||||
CheckLastUpdatedMixin,
|
||||
CustomFieldsMixin,
|
||||
OwnerMixin,
|
||||
TagsMixin,
|
||||
forms.ModelForm
|
||||
):
|
||||
"""
|
||||
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
|
||||
|
||||
Attributes:
|
||||
fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
"""
|
||||
fieldsets = ()
|
||||
|
||||
def _get_content_type(self):
|
||||
return ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
if self.instance.pk:
|
||||
form_field = customfield.to_form_field(set_initial=False)
|
||||
initial = self.instance.custom_field_data.get(customfield.name)
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON:
|
||||
form_field.initial = json.dumps(initial)
|
||||
else:
|
||||
form_field.initial = initial
|
||||
return form_field
|
||||
|
||||
return customfield.to_form_field()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Save custom field data on instance
|
||||
for cf_name, customfield in self.custom_fields.items():
|
||||
if cf_name not in self.fields:
|
||||
# Custom fields may be absent when performing bulk updates via import
|
||||
continue
|
||||
key = cf_name[3:] # Strip "cf_" from field name
|
||||
value = self.cleaned_data.get(cf_name)
|
||||
|
||||
# Convert "empty" values to null
|
||||
if value in self.fields[cf_name].empty_values:
|
||||
self.instance.custom_field_data[key] = None
|
||||
else:
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
|
||||
value = json.loads(value)
|
||||
self.instance.custom_field_data[key] = customfield.serialize(value)
|
||||
|
||||
return super().clean()
|
||||
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
||||
"""
|
||||
self.instance._m2m_values = {}
|
||||
for field in self.instance._meta.local_many_to_many:
|
||||
if field.name in self.cleaned_data:
|
||||
self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
|
||||
|
||||
return super()._post_clean()
|
||||
|
||||
|
||||
class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
||||
"""
|
||||
Base form for creating NetBox objects from CSV data. Used for bulk importing.
|
||||
"""
|
||||
owner = CSVModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_("Name of the object's owner")
|
||||
)
|
||||
tags = CSVModelMultipleChoiceField(
|
||||
label=_('Tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
to_field_name='slug',
|
||||
help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")')
|
||||
)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(
|
||||
object_types=content_type,
|
||||
ui_editable=CustomFieldUIEditableChoices.YES
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(for_csv_import=True)
|
||||
|
||||
|
||||
class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, 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.
|
||||
|
||||
Attributes:
|
||||
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
"""
|
||||
fieldsets = None
|
||||
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=None, # Set from self.model on init
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
add_tags = DynamicModelMultipleChoiceField(
|
||||
label=_('Add tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
remove_tags = DynamicModelMultipleChoiceField(
|
||||
label=_('Remove tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['pk'].queryset = self.model.objects.all()
|
||||
|
||||
# Restrict tag fields by model
|
||||
object_type = ObjectType.objects.get_for_model(self.model)
|
||||
self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
|
||||
self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
|
||||
|
||||
self._extend_nullable_fields()
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||
|
||||
def _extend_nullable_fields(self):
|
||||
nullable_common_fields = ['owner']
|
||||
nullable_custom_fields = [
|
||||
name for name, customfield in self.custom_fields.items()
|
||||
if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
|
||||
]
|
||||
self.nullable_fields = (
|
||||
*self.nullable_fields,
|
||||
*nullable_common_fields,
|
||||
*nullable_custom_fields,
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
|
||||
"""
|
||||
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
||||
corresponding FilterSet *must* provide a `q` filter.
|
||||
|
||||
Attributes:
|
||||
model: The model class associated with the form
|
||||
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
selector_fields: An iterable of names of fields to display by default when rendering the form as
|
||||
a selector widget
|
||||
"""
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
owner_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
selector_fields = ('filter_id', 'q')
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return super()._get_custom_fields(content_type).exclude(
|
||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)
|
||||
67
netbox/netbox/forms/bulk_edit.py
Normal file
67
netbox/netbox/forms/bulk_edit.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.models import Tag
|
||||
from utilities.forms import BulkEditForm
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin
|
||||
|
||||
__all__ = (
|
||||
'NetBoxModelBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, 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.
|
||||
|
||||
Attributes:
|
||||
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
"""
|
||||
fieldsets = None
|
||||
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=None, # Set from self.model on init
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
add_tags = DynamicModelMultipleChoiceField(
|
||||
label=_('Add tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
remove_tags = DynamicModelMultipleChoiceField(
|
||||
label=_('Remove tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['pk'].queryset = self.model.objects.all()
|
||||
|
||||
# Restrict tag fields by model
|
||||
object_type = ObjectType.objects.get_for_model(self.model)
|
||||
self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
|
||||
self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk)
|
||||
|
||||
self._extend_nullable_fields()
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False)
|
||||
|
||||
def _extend_nullable_fields(self):
|
||||
nullable_common_fields = ['owner']
|
||||
nullable_custom_fields = [
|
||||
name for name, customfield in self.custom_fields.items()
|
||||
if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
|
||||
]
|
||||
self.nullable_fields = (
|
||||
*self.nullable_fields,
|
||||
*nullable_common_fields,
|
||||
*nullable_custom_fields,
|
||||
)
|
||||
40
netbox/netbox/forms/bulk_import.py
Normal file
40
netbox/netbox/forms/bulk_import.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, Tag
|
||||
from users.models import Owner
|
||||
from utilities.forms import CSVModelForm
|
||||
from utilities.forms.fields import CSVModelMultipleChoiceField, CSVModelChoiceField
|
||||
from .model_forms import NetBoxModelForm
|
||||
|
||||
__all__ = (
|
||||
'NetBoxModelImportForm',
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
||||
"""
|
||||
Base form for creating NetBox objects from CSV data. Used for bulk importing.
|
||||
"""
|
||||
owner = CSVModelChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_("Name of the object's owner")
|
||||
)
|
||||
tags = CSVModelMultipleChoiceField(
|
||||
label=_('Tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
to_field_name='slug',
|
||||
help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")')
|
||||
)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(
|
||||
object_types=content_type,
|
||||
ui_editable=CustomFieldUIEditableChoices.YES
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(for_csv_import=True)
|
||||
46
netbox/netbox/forms/filtersets.py
Normal file
46
netbox/netbox/forms/filtersets.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.choices import *
|
||||
from users.models import Owner
|
||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||
from .mixins import CustomFieldsMixin, SavedFiltersMixin
|
||||
|
||||
__all__ = (
|
||||
'NetBoxModelFilterSetForm',
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
|
||||
"""
|
||||
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
||||
corresponding FilterSet *must* provide a `q` filter.
|
||||
|
||||
Attributes:
|
||||
model: The model class associated with the form
|
||||
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
selector_fields: An iterable of names of fields to display by default when rendering the form as
|
||||
a selector widget
|
||||
"""
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
owner_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Owner.objects.all(),
|
||||
required=False,
|
||||
label=_('Owner'),
|
||||
)
|
||||
|
||||
selector_fields = ('filter_id', 'q')
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return super()._get_custom_fields(content_type).exclude(
|
||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||
Q(type=CustomFieldTypeChoices.TYPE_JSON)
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)
|
||||
76
netbox/netbox/forms/model_forms.py
Normal file
76
netbox/netbox/forms/model_forms.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from extras.choices import *
|
||||
from utilities.forms.mixins import CheckLastUpdatedMixin
|
||||
from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, TagsMixin
|
||||
|
||||
__all__ = (
|
||||
'NetBoxModelForm',
|
||||
)
|
||||
|
||||
|
||||
class NetBoxModelForm(
|
||||
ChangelogMessageMixin,
|
||||
CheckLastUpdatedMixin,
|
||||
CustomFieldsMixin,
|
||||
OwnerMixin,
|
||||
TagsMixin,
|
||||
forms.ModelForm
|
||||
):
|
||||
"""
|
||||
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
|
||||
|
||||
Attributes:
|
||||
fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
|
||||
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
|
||||
"""
|
||||
fieldsets = ()
|
||||
|
||||
def _get_content_type(self):
|
||||
return ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
if self.instance.pk:
|
||||
form_field = customfield.to_form_field(set_initial=False)
|
||||
initial = self.instance.custom_field_data.get(customfield.name)
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON:
|
||||
form_field.initial = json.dumps(initial)
|
||||
else:
|
||||
form_field.initial = initial
|
||||
return form_field
|
||||
|
||||
return customfield.to_form_field()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Save custom field data on instance
|
||||
for cf_name, customfield in self.custom_fields.items():
|
||||
if cf_name not in self.fields:
|
||||
# Custom fields may be absent when performing bulk updates via import
|
||||
continue
|
||||
key = cf_name[3:] # Strip "cf_" from field name
|
||||
value = self.cleaned_data.get(cf_name)
|
||||
|
||||
# Convert "empty" values to null
|
||||
if value in self.fields[cf_name].empty_values:
|
||||
self.instance.custom_field_data[key] = None
|
||||
else:
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
|
||||
value = json.loads(value)
|
||||
self.instance.custom_field_data[key] = customfield.serialize(value)
|
||||
|
||||
return super().clean()
|
||||
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
||||
"""
|
||||
self.instance._m2m_values = {}
|
||||
for field in self.instance._meta.local_many_to_many:
|
||||
if field.name in self.cleaned_data:
|
||||
self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
|
||||
|
||||
return super()._post_clean()
|
||||
55
netbox/netbox/forms/search.py
Normal file
55
netbox/netbox/forms/search.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.search import LookupTypes
|
||||
from netbox.search.backends import search_backend
|
||||
|
||||
LOOKUP_CHOICES = (
|
||||
('', _('Partial match')),
|
||||
(LookupTypes.EXACT, _('Exact match')),
|
||||
(LookupTypes.STARTSWITH, _('Starts with')),
|
||||
(LookupTypes.ENDSWITH, _('Ends with')),
|
||||
(LookupTypes.REGEX, _('Regex')),
|
||||
)
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
q = forms.CharField(
|
||||
label=_('Search'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'hx-get': '',
|
||||
'hx-target': '#object_list',
|
||||
'hx-trigger': 'keyup[target.value.length >= 3] changed delay:500ms',
|
||||
}
|
||||
)
|
||||
)
|
||||
obj_types = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
required=False,
|
||||
label=_('Object type(s)')
|
||||
)
|
||||
lookup = forms.ChoiceField(
|
||||
choices=LOOKUP_CHOICES,
|
||||
initial=LookupTypes.PARTIAL,
|
||||
required=False,
|
||||
label=_('Lookup')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['obj_types'].choices = search_backend.get_object_types()
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate regular expressions
|
||||
if self.cleaned_data['lookup'] == LookupTypes.REGEX:
|
||||
try:
|
||||
re.compile(self.cleaned_data['q'])
|
||||
except re.error as e:
|
||||
raise forms.ValidationError({
|
||||
'q': f'Invalid regular expression: {e}'
|
||||
})
|
||||
Reference in New Issue
Block a user