Closes #5400: Store custom field defaults as JSON values

This commit is contained in:
Jeremy Stretch 2020-12-01 16:05:23 -05:00
parent 0b57389af6
commit cc271aefe1
8 changed files with 58 additions and 17 deletions

View File

@ -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,

View File

@ -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

View File

@ -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):

View File

@ -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',

View File

@ -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
), ),

View File

@ -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',
), ),

View File

@ -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)
) )

View File

@ -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'),
] ]