1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

CustomField.type to slug

This commit is contained in:
Jeremy Stretch
2019-12-04 20:58:26 -05:00
parent ca11b9a2f5
commit 3ff22bea56
11 changed files with 123 additions and 64 deletions

View File

@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from extras.constants import * from extras.choices import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer from utilities.api import ValidatedModelSerializer
@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if value not in [None, '']: if value not in [None, '']:
# Validate integer # Validate integer
if cf.type == CF_TYPE_INTEGER: if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
try: try:
int(value) int(value)
except ValueError: except ValueError:
@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
) )
# Validate boolean # Validate boolean
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError( raise ValidationError(
"Invalid value for boolean field {}: {}".format(field_name, value) "Invalid value for boolean field {}: {}".format(field_name, value)
) )
# Validate date # Validate date
if cf.type == CF_TYPE_DATE: if cf.type == CustomFieldTypeChoices.TYPE_DATE:
try: try:
datetime.strptime(value, '%Y-%m-%d') datetime.strptime(value, '%Y-%m-%d')
except ValueError: except ValueError:
@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
) )
# Validate selected choice # Validate selected choice
if cf.type == CF_TYPE_SELECT: if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
try: try:
value = int(value) value = int(value)
except ValueError: except ValueError:
@ -100,7 +100,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
instance.custom_fields = {} instance.custom_fields = {}
for field in fields: for field in fields:
value = instance.cf.get(field.name) value = instance.cf.get(field.name)
if field.type == CF_TYPE_SELECT and value is not None: if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
else: else:
instance.custom_fields[field.name] = value instance.custom_fields[field.name] = value

33
netbox/extras/choices.py Normal file
View File

@ -0,0 +1,33 @@
from utilities.choices import ChoiceSet
#
# CustomFields
#
class CustomFieldTypeChoices(ChoiceSet):
TYPE_TEXT = 'text'
TYPE_INTEGER = 'integer'
TYPE_BOOLEAN = 'boolean'
TYPE_DATE = 'date'
TYPE_URL = 'url'
TYPE_SELECT = 'select'
CHOICES = (
(TYPE_TEXT, 'Text'),
(TYPE_INTEGER, 'Integer'),
(TYPE_BOOLEAN, 'Boolean (true/false)'),
(TYPE_DATE, 'Date'),
(TYPE_URL, 'URL'),
(TYPE_SELECT, 'Selection'),
)
LEGACY_MAP = {
TYPE_TEXT: 100,
TYPE_INTEGER: 200,
TYPE_BOOLEAN: 300,
TYPE_DATE: 400,
TYPE_URL: 500,
TYPE_SELECT: 600,
}

View File

@ -19,22 +19,6 @@ CUSTOMFIELD_MODELS = [
'virtualization.virtualmachine', 'virtualization.virtualmachine',
] ]
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Custom field filter logic choices # Custom field filter logic choices
CF_FILTER_DISABLED = 0 CF_FILTER_DISABLED = 0
CF_FILTER_LOOSE = 1 CF_FILTER_LOOSE = 1

View File

@ -4,6 +4,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from .choices import *
from .constants import * from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
@ -25,7 +26,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset return queryset
# Selection fields get special treatment (values must be integers) # Selection fields get special treatment (values must be integers)
if self.cf_type == CF_TYPE_SELECT: if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
try: try:
# Treat 0 as None # Treat 0 as None
if int(value) == 0: if int(value) == 0:
@ -42,7 +43,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset.none() return queryset.none()
# Apply the assigned filter logic (exact or loose) # Apply the assigned filter logic (exact or loose)
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT: if self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
queryset = queryset.filter( queryset = queryset.filter(
custom_field_values__field__name=self.field_name, custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value custom_field_values__serialized_value=value

View File

@ -13,6 +13,7 @@ from utilities.forms import (
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2, CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES, BOOLEAN_WITH_BLANK_CHOICES,
) )
from .choices import *
from .constants import * from .constants import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@ -35,11 +36,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
initial = cf.default if not bulk_edit else None initial = cf.default if not bulk_edit else None
# Integer # Integer
if cf.type == CF_TYPE_INTEGER: if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial) field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean # Boolean
elif cf.type == CF_TYPE_BOOLEAN: elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = ( choices = (
(None, '---------'), (None, '---------'),
(1, 'True'), (1, 'True'),
@ -56,11 +57,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
) )
# Date # Date
elif cf.type == CF_TYPE_DATE: elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD") field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
# Select # Select
elif cf.type == CF_TYPE_SELECT: elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only: if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices choices = [(None, '---------')] + choices
@ -74,7 +75,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice) field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
# URL # URL
elif cf.type == CF_TYPE_URL: elif cf.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial) field = LaxURLField(required=cf.required, initial=initial)
# Text # Text

View File

@ -8,8 +8,6 @@ import django.db.models.deletion
import extras.models import extras.models
from django.db.utils import OperationalError from django.db.utils import OperationalError
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def verify_postgresql_version(apps, schema_editor): def verify_postgresql_version(apps, schema_editor):
""" """

View File

@ -2,7 +2,7 @@
# Generated by Django 1.11.9 on 2018-02-21 19:48 # Generated by Django 1.11.9 on 2018-02-21 19:48
from django.db import migrations, models from django.db import migrations, models
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE
def is_filterable_to_filter_logic(apps, schema_editor): def is_filterable_to_filter_logic(apps, schema_editor):
@ -10,7 +10,7 @@ def is_filterable_to_filter_logic(apps, schema_editor):
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED) CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE) CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
# Select fields match on primary key only # Select fields match on primary key only
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT) CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=CF_FILTER_EXACT)
def filter_logic_to_is_filterable(apps, schema_editor): def filter_logic_to_is_filterable(apps, schema_editor):

View File

@ -0,0 +1,36 @@
from django.db import migrations, models
CUSTOMFIELD_TYPE_CHOICES = (
(100, 'text'),
(200, 'integer'),
(300, 'boolean'),
(400, 'date'),
(500, 'url'),
(600, 'select')
)
def customfield_type_to_slug(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
for id, slug in CUSTOMFIELD_TYPE_CHOICES:
CustomField.objects.filter(type=str(id)).update(type=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0028_remove_topology_maps'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='type',
field=models.CharField(default='text', max_length=50),
),
migrations.RunPython(
code=customfield_type_to_slug
),
]

View File

@ -15,6 +15,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.utils import deepmerge, model_names_to_filter_dict from utilities.utils import deepmerge, model_names_to_filter_dict
from .choices import *
from .constants import * from .constants import *
from .querysets import ConfigContextQuerySet from .querysets import ConfigContextQuerySet
@ -182,9 +183,10 @@ class CustomField(models.Model):
limit_choices_to=get_custom_field_models, limit_choices_to=get_custom_field_models,
help_text='The object(s) to which this field applies.' help_text='The object(s) to which this field applies.'
) )
type = models.PositiveSmallIntegerField( type = models.CharField(
choices=CUSTOMFIELD_TYPE_CHOICES, max_length=50,
default=CF_TYPE_TEXT choices=CustomFieldTypeChoices,
default=CustomFieldTypeChoices.TYPE_TEXT
) )
name = models.CharField( name = models.CharField(
max_length=50, max_length=50,
@ -233,15 +235,15 @@ class CustomField(models.Model):
""" """
if value is None: if value is None:
return '' return ''
if self.type == CF_TYPE_BOOLEAN: if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
return str(int(bool(value))) return str(int(bool(value)))
if self.type == CF_TYPE_DATE: if self.type == CustomFieldTypeChoices.TYPE_DATE:
# Could be date/datetime object or string # Could be date/datetime object or string
try: try:
return value.strftime('%Y-%m-%d') return value.strftime('%Y-%m-%d')
except AttributeError: except AttributeError:
return value return value
if self.type == CF_TYPE_SELECT: if self.type == CustomFieldTypeChoices.TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField # Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value) return str(value.id) if hasattr(value, 'id') else str(value)
return value return value
@ -252,14 +254,14 @@ class CustomField(models.Model):
""" """
if serialized_value == '': if serialized_value == '':
return None return None
if self.type == CF_TYPE_INTEGER: if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
return int(serialized_value) return int(serialized_value)
if self.type == CF_TYPE_BOOLEAN: if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
return bool(int(serialized_value)) return bool(int(serialized_value))
if self.type == CF_TYPE_DATE: if self.type == CustomFieldTypeChoices.TYPE_DATE:
# Read date as YYYY-MM-DD # Read date as YYYY-MM-DD
return date(*[int(n) for n in serialized_value.split('-')]) return date(*[int(n) for n in serialized_value.split('-')])
if self.type == CF_TYPE_SELECT: if self.type == CustomFieldTypeChoices.TYPE_SELECT:
return self.choices.get(pk=int(serialized_value)) return self.choices.get(pk=int(serialized_value))
return serialized_value return serialized_value
@ -312,7 +314,7 @@ class CustomFieldChoice(models.Model):
to='extras.CustomField', to='extras.CustomField',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='choices', related_name='choices',
limit_choices_to={'type': CF_TYPE_SELECT} limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
) )
value = models.CharField( value = models.CharField(
max_length=100 max_length=100
@ -330,14 +332,17 @@ class CustomFieldChoice(models.Model):
return self.value return self.value
def clean(self): def clean(self):
if self.field.type != CF_TYPE_SELECT: if self.field.type != CustomFieldTypeChoices.TYPE_SELECT:
raise ValidationError("Custom field choices can only be assigned to selection fields.") raise ValidationError("Custom field choices can only be assigned to selection fields.")
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
pk = self.pk pk = self.pk
super().delete(using, keep_parents) super().delete(using, keep_parents)
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() CustomFieldValue.objects.filter(
field__type=CustomFieldTypeChoices.TYPE_SELECT,
serialized_value=str(pk)
).delete()
# #

View File

@ -3,6 +3,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.models import Site from dcim.models import Site
from extras.choices import *
from extras.constants import * from extras.constants import *
from extras.models import CustomField, CustomFieldValue, ObjectChange from extras.models import CustomField, CustomFieldValue, ObjectChange
from utilities.testing import APITestCase from utilities.testing import APITestCase
@ -17,7 +18,7 @@ class ChangeLogTest(APITestCase):
# Create a custom field on the Site model # Create a custom field on the Site model
ct = ContentType.objects.get_for_model(Site) ct = ContentType.objects.get_for_model(Site)
cf = CustomField( cf = CustomField(
type=CF_TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
name='my_field', name='my_field',
required=False required=False
) )

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.models import Site from dcim.models import Site
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT from extras.choices import *
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
from utilities.testing import APITestCase from utilities.testing import APITestCase
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -25,13 +25,13 @@ class CustomFieldTest(TestCase):
def test_simple_fields(self): def test_simple_fields(self):
DATA = ( DATA = (
{'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''}, {'field_type': CustomFieldTypeChoices.TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
{'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None}, {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
{'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None}, {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
{'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None}, {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
{'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None}, {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
{'field_type': CF_TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None}, {'field_type': CustomFieldTypeChoices.TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
{'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''}, {'field_type': CustomFieldTypeChoices.TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
) )
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
@ -67,7 +67,7 @@ class CustomFieldTest(TestCase):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
# Create a custom field # Create a custom field
cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False) cf = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='my_field', required=False)
cf.save() cf.save()
cf.obj_type.set([obj_type]) cf.obj_type.set([obj_type])
cf.save() cf.save()
@ -107,37 +107,37 @@ class CustomFieldAPITest(APITestCase):
content_type = ContentType.objects.get_for_model(Site) content_type = ContentType.objects.get_for_model(Site)
# Text custom field # Text custom field
self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word') self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word')
self.cf_text.save() self.cf_text.save()
self.cf_text.obj_type.set([content_type]) self.cf_text.obj_type.set([content_type])
self.cf_text.save() self.cf_text.save()
# Integer custom field # Integer custom field
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number') self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number')
self.cf_integer.save() self.cf_integer.save()
self.cf_integer.obj_type.set([content_type]) self.cf_integer.obj_type.set([content_type])
self.cf_integer.save() self.cf_integer.save()
# Boolean custom field # Boolean custom field
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic') self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic')
self.cf_boolean.save() self.cf_boolean.save()
self.cf_boolean.obj_type.set([content_type]) self.cf_boolean.obj_type.set([content_type])
self.cf_boolean.save() self.cf_boolean.save()
# Date custom field # Date custom field
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date') self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date')
self.cf_date.save() self.cf_date.save()
self.cf_date.obj_type.set([content_type]) self.cf_date.obj_type.set([content_type])
self.cf_date.save() self.cf_date.save()
# URL custom field # URL custom field
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url') self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url')
self.cf_url.save() self.cf_url.save()
self.cf_url.obj_type.set([content_type]) self.cf_url.obj_type.set([content_type])
self.cf_url.save() self.cf_url.save()
# Select custom field # Select custom field
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice') self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice')
self.cf_select.save() self.cf_select.save()
self.cf_select.obj_type.set([content_type]) self.cf_select.obj_type.set([content_type])
self.cf_select.save() self.cf_select.save()
@ -308,8 +308,8 @@ class CustomFieldChoiceAPITest(APITestCase):
vm_content_type = ContentType.objects.get_for_model(VirtualMachine) vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT) self.cf_1 = CustomField.objects.create(name="cf_1", type=CustomFieldTypeChoices.TYPE_SELECT)
self.cf_2 = CustomField.objects.create(name="cf_2", type=CF_TYPE_SELECT) self.cf_2 = CustomField.objects.create(name="cf_2", type=CustomFieldTypeChoices.TYPE_SELECT)
self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100) self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100)
self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50) self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)