|
|
@@ -1,23 +1,34 @@
|
|
|
import copy
|
|
|
+import json
|
|
|
|
|
|
from django import forms
|
|
|
+from django.conf import settings
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
from core.forms.mixins import SyncedDataMixin
|
|
|
from core.models import *
|
|
|
+from netbox.config import get_config, PARAMS
|
|
|
from netbox.forms import NetBoxModelForm
|
|
|
from netbox.registry import registry
|
|
|
-from utilities.forms import get_field_value
|
|
|
+from netbox.utils import get_data_backend_choices
|
|
|
+from utilities.forms import BootstrapMixin, get_field_value
|
|
|
from utilities.forms.fields import CommentField
|
|
|
from utilities.forms.widgets import HTMXSelect
|
|
|
|
|
|
__all__ = (
|
|
|
+ 'ConfigRevisionForm',
|
|
|
'DataSourceForm',
|
|
|
'ManagedFileForm',
|
|
|
)
|
|
|
|
|
|
+EMPTY_VALUES = ('', None, [], ())
|
|
|
+
|
|
|
|
|
|
class DataSourceForm(NetBoxModelForm):
|
|
|
+ type = forms.ChoiceField(
|
|
|
+ choices=get_data_backend_choices,
|
|
|
+ widget=HTMXSelect()
|
|
|
+ )
|
|
|
comments = CommentField()
|
|
|
|
|
|
class Meta:
|
|
|
@@ -26,7 +37,6 @@ class DataSourceForm(NetBoxModelForm):
|
|
|
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags',
|
|
|
]
|
|
|
widgets = {
|
|
|
- 'type': HTMXSelect(),
|
|
|
'ignore_rules': forms.Textarea(
|
|
|
attrs={
|
|
|
'rows': 5,
|
|
|
@@ -56,12 +66,13 @@ class DataSourceForm(NetBoxModelForm):
|
|
|
|
|
|
# Add backend-specific form fields
|
|
|
self.backend_fields = []
|
|
|
- for name, form_field in backend.parameters.items():
|
|
|
- field_name = f'backend_{name}'
|
|
|
- self.backend_fields.append(field_name)
|
|
|
- self.fields[field_name] = copy.copy(form_field)
|
|
|
- if self.instance and self.instance.parameters:
|
|
|
- self.fields[field_name].initial = self.instance.parameters.get(name)
|
|
|
+ if backend:
|
|
|
+ for name, form_field in backend.parameters.items():
|
|
|
+ field_name = f'backend_{name}'
|
|
|
+ self.backend_fields.append(field_name)
|
|
|
+ self.fields[field_name] = copy.copy(form_field)
|
|
|
+ if self.instance and self.instance.parameters:
|
|
|
+ self.fields[field_name].initial = self.instance.parameters.get(name)
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
|
@@ -106,3 +117,113 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
|
|
|
new_file.write(self.cleaned_data['upload_file'].read())
|
|
|
|
|
|
return super().save(*args, **kwargs)
|
|
|
+
|
|
|
+
|
|
|
+class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
|
|
+
|
|
|
+ def __new__(mcs, name, bases, attrs):
|
|
|
+
|
|
|
+ # Emulate a declared field for each supported configuration parameter
|
|
|
+ param_fields = {}
|
|
|
+ for param in PARAMS:
|
|
|
+ field_kwargs = {
|
|
|
+ 'required': False,
|
|
|
+ 'label': param.label,
|
|
|
+ 'help_text': param.description,
|
|
|
+ }
|
|
|
+ field_kwargs.update(**param.field_kwargs)
|
|
|
+ param_fields[param.name] = param.field(**field_kwargs)
|
|
|
+ attrs.update(param_fields)
|
|
|
+
|
|
|
+ return super().__new__(mcs, name, bases, attrs)
|
|
|
+
|
|
|
+
|
|
|
+class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
|
|
|
+ """
|
|
|
+ Form for creating a new ConfigRevision.
|
|
|
+ """
|
|
|
+
|
|
|
+ fieldsets = (
|
|
|
+ (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
|
|
|
+ (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
|
|
|
+ (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
|
|
|
+ (_('Security'), ('ALLOWED_URL_SCHEMES',)),
|
|
|
+ (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
|
|
|
+ (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
|
|
|
+ (_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
|
|
|
+ (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
|
|
|
+ (_('Miscellaneous'), (
|
|
|
+ 'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
|
|
|
+ )),
|
|
|
+ (_('Config Revision'), ('comment',))
|
|
|
+ )
|
|
|
+
|
|
|
+ class Meta:
|
|
|
+ model = ConfigRevision
|
|
|
+ fields = '__all__'
|
|
|
+ widgets = {
|
|
|
+ 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}),
|
|
|
+ 'comment': forms.Textarea(),
|
|
|
+ }
|
|
|
+
|
|
|
+ def __init__(self, *args, **kwargs):
|
|
|
+ super().__init__(*args, **kwargs)
|
|
|
+
|
|
|
+ # Append current parameter values to form field help texts and check for static configurations
|
|
|
+ config = get_config()
|
|
|
+ for param in PARAMS:
|
|
|
+ value = getattr(config, param.name)
|
|
|
+
|
|
|
+ # Set the field's initial value, if it can be serialized. (This may not be the case e.g. for
|
|
|
+ # CUSTOM_VALIDATORS, which may reference Python objects.)
|
|
|
+ try:
|
|
|
+ json.dumps(value)
|
|
|
+ if type(value) in (tuple, list):
|
|
|
+ self.fields[param.name].initial = ', '.join(value)
|
|
|
+ else:
|
|
|
+ self.fields[param.name].initial = value
|
|
|
+ except TypeError:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # Check whether this parameter is statically configured (e.g. in configuration.py)
|
|
|
+ if hasattr(settings, param.name):
|
|
|
+ self.fields[param.name].disabled = True
|
|
|
+ self.fields[param.name].help_text = _(
|
|
|
+ 'This parameter has been defined statically and cannot be modified.'
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Set the field's help text
|
|
|
+ help_text = self.fields[param.name].help_text
|
|
|
+ if help_text:
|
|
|
+ help_text += '<br />' # Line break
|
|
|
+ help_text += _('Current value: <strong>{value}</strong>').format(value=value or '—')
|
|
|
+ if value == param.default:
|
|
|
+ help_text += _(' (default)')
|
|
|
+ self.fields[param.name].help_text = help_text
|
|
|
+
|
|
|
+ def save(self, commit=True):
|
|
|
+ instance = super().save(commit=False)
|
|
|
+
|
|
|
+ # Populate JSON data on the instance
|
|
|
+ instance.data = self.render_json()
|
|
|
+
|
|
|
+ if commit:
|
|
|
+ instance.save()
|
|
|
+
|
|
|
+ return instance
|
|
|
+
|
|
|
+ def render_json(self):
|
|
|
+ json = {}
|
|
|
+
|
|
|
+ # Iterate through each field and populate non-empty values
|
|
|
+ for field_name in self.declared_fields:
|
|
|
+ if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
|
|
|
+ json[field_name] = self.cleaned_data[field_name]
|
|
|
+
|
|
|
+ return json
|