mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Closes #5400: Store custom field defaults as JSON values
This commit is contained in:
parent
0b57389af6
commit
cc271aefe1
@ -2,7 +2,6 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import LaxURLField
|
||||||
from .choices import CustomFieldTypeChoices
|
|
||||||
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
from .models import CustomField, CustomLink, ExportTemplate, JobResult, Webhook
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +75,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
model = CustomField
|
model = CustomField
|
||||||
exclude = []
|
exclude = []
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'default': forms.TextInput(),
|
||||||
'validation_regex': forms.Textarea(
|
'validation_regex': forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
'cols': 80,
|
'cols': 80,
|
||||||
|
@ -26,15 +26,8 @@ class CustomFieldDefaultValues:
|
|||||||
# Populate the default value for each CustomField
|
# Populate the default value for each CustomField
|
||||||
value = {}
|
value = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field.default:
|
if field.default is not None:
|
||||||
if field.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
value[field.name] = field.default
|
||||||
field_value = int(field.default)
|
|
||||||
elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
|
||||||
# TODO: Fix default value assignment for boolean custom fields
|
|
||||||
field_value = False if field.default.lower() == 'false' else bool(field.default)
|
|
||||||
else:
|
|
||||||
field_value = field.default
|
|
||||||
value[field.name] = field_value
|
|
||||||
else:
|
else:
|
||||||
value[field.name] = None
|
value[field.name] = None
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'default', 'weight']
|
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateFilterSet(BaseFilterSet):
|
class ExportTemplateFilterSet(BaseFilterSet):
|
||||||
|
@ -34,6 +34,12 @@ class Migration(migrations.Migration):
|
|||||||
size=None
|
size=None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# Introduce new default field (to be renamed later)
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='default2',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
# Rename obj_type to content_types
|
# Rename obj_type to content_types
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='customfield',
|
model_name='customfield',
|
||||||
|
@ -16,6 +16,28 @@ def deserialize_value(field, value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_customfield_defaults(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy old serialized defaults to native JSON types.
|
||||||
|
"""
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
|
||||||
|
for customfield in CustomField.objects.exclude(default=''):
|
||||||
|
try:
|
||||||
|
if customfield.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
|
value = int(customfield.default)
|
||||||
|
elif customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
|
value = customfield.default in ['true', 'yes', '1']
|
||||||
|
else:
|
||||||
|
value = customfield.default
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f'Invalid default value "{customfield.default}" found for {customfield.type} '
|
||||||
|
f'custom field {customfield.name}'
|
||||||
|
)
|
||||||
|
CustomField.objects.filter(pk=customfield.pk).update(default2=value)
|
||||||
|
|
||||||
|
|
||||||
def migrate_customfieldchoices(apps, schema_editor):
|
def migrate_customfieldchoices(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Collect all CustomFieldChoices for each applicable CustomField, and save them locally as an array on
|
Collect all CustomFieldChoices for each applicable CustomField, and save them locally as an array on
|
||||||
@ -73,6 +95,9 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=migrate_customfield_defaults
|
||||||
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
code=migrate_customfieldchoices
|
code=migrate_customfieldchoices
|
||||||
),
|
),
|
||||||
|
@ -8,6 +8,15 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='CustomField',
|
||||||
|
name='default',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='CustomField',
|
||||||
|
old_name='default2',
|
||||||
|
new_name='default'
|
||||||
|
),
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name='CustomFieldChoice',
|
name='CustomFieldChoice',
|
||||||
),
|
),
|
@ -115,10 +115,11 @@ class CustomField(models.Model):
|
|||||||
help_text='Loose matches any instance of a given string; exact '
|
help_text='Loose matches any instance of a given string; exact '
|
||||||
'matches the entire field.'
|
'matches the entire field.'
|
||||||
)
|
)
|
||||||
default = models.CharField(
|
default = models.JSONField(
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Default value for the field. Use "true" or "false" for booleans.'
|
null=True,
|
||||||
|
help_text='Default value for the field (must be a JSON value). Encapsulate '
|
||||||
|
'strings with double quotes (e.g. "Foo").'
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
@ -171,6 +172,15 @@ class CustomField(models.Model):
|
|||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
# Validate the field's default value (if any)
|
||||||
|
if self.default is not None:
|
||||||
|
try:
|
||||||
|
self.validate(self.default)
|
||||||
|
except ValidationError as err:
|
||||||
|
raise ValidationError({
|
||||||
|
'default': f'Invalid default value "{self.default}": {err.message}'
|
||||||
|
})
|
||||||
|
|
||||||
# Minimum/maximum values can be set only for numeric fields
|
# Minimum/maximum values can be set only for numeric fields
|
||||||
if self.validation_minimum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
if self.validation_minimum is not None and self.type != CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -232,8 +242,6 @@ class CustomField(models.Model):
|
|||||||
(True, 'True'),
|
(True, 'True'),
|
||||||
(False, 'False'),
|
(False, 'False'),
|
||||||
)
|
)
|
||||||
if initial is not None:
|
|
||||||
initial = bool(initial)
|
|
||||||
field = forms.NullBooleanField(
|
field = forms.NullBooleanField(
|
||||||
required=required, initial=initial, widget=StaticSelect2(choices=choices)
|
required=required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('tenancy', '0010_custom_field_data'),
|
('tenancy', '0010_custom_field_data'),
|
||||||
('extras', '0052_delete_customfieldchoice_customfieldvalue'),
|
('extras', '0052_customfield_cleanup'),
|
||||||
('ipam', '0040_service_drop_port'),
|
('ipam', '0040_service_drop_port'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user