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:
@ -5,7 +5,7 @@ from django.db import transaction
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.constants import *
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
if value not in [None, '']:
|
||||
|
||||
# Validate integer
|
||||
if cf.type == CF_TYPE_INTEGER:
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
)
|
||||
|
||||
# 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(
|
||||
"Invalid value for boolean field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
)
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
@ -100,7 +100,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
instance.custom_fields = {}
|
||||
for field in fields:
|
||||
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
|
||||
else:
|
||||
instance.custom_fields[field.name] = value
|
||||
|
33
netbox/extras/choices.py
Normal file
33
netbox/extras/choices.py
Normal 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,
|
||||
}
|
@ -19,22 +19,6 @@ CUSTOMFIELD_MODELS = [
|
||||
'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
|
||||
CF_FILTER_DISABLED = 0
|
||||
CF_FILTER_LOOSE = 1
|
||||
|
@ -4,6 +4,7 @@ from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
||||
|
||||
@ -25,7 +26,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
return queryset
|
||||
|
||||
# Selection fields get special treatment (values must be integers)
|
||||
if self.cf_type == CF_TYPE_SELECT:
|
||||
if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
try:
|
||||
# Treat 0 as None
|
||||
if int(value) == 0:
|
||||
@ -42,7 +43,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
return queryset.none()
|
||||
|
||||
# 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(
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value
|
||||
|
@ -13,6 +13,7 @@ from utilities.forms import (
|
||||
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
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
|
||||
|
||||
# Integer
|
||||
if cf.type == CF_TYPE_INTEGER:
|
||||
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
field = forms.IntegerField(required=cf.required, initial=initial)
|
||||
|
||||
# Boolean
|
||||
elif cf.type == CF_TYPE_BOOLEAN:
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
choices = (
|
||||
(None, '---------'),
|
||||
(1, 'True'),
|
||||
@ -56,11 +57,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
)
|
||||
|
||||
# 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")
|
||||
|
||||
# Select
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required or bulk_edit or filterable_only:
|
||||
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)
|
||||
|
||||
# URL
|
||||
elif cf.type == CF_TYPE_URL:
|
||||
elif cf.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
field = LaxURLField(required=cf.required, initial=initial)
|
||||
|
||||
# Text
|
||||
|
@ -8,8 +8,6 @@ import django.db.models.deletion
|
||||
import extras.models
|
||||
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):
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||
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):
|
||||
@ -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=True).update(filter_logic=CF_FILTER_LOOSE)
|
||||
# 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):
|
||||
|
36
netbox/extras/migrations/0029_3569_customfield_fields.py
Normal file
36
netbox/extras/migrations/0029_3569_customfield_fields.py
Normal 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
|
||||
),
|
||||
]
|
@ -15,6 +15,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from utilities.fields import ColorField
|
||||
from utilities.utils import deepmerge, model_names_to_filter_dict
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
@ -182,9 +183,10 @@ class CustomField(models.Model):
|
||||
limit_choices_to=get_custom_field_models,
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||
default=CF_TYPE_TEXT
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldTypeChoices,
|
||||
default=CustomFieldTypeChoices.TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
@ -233,15 +235,15 @@ class CustomField(models.Model):
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CF_TYPE_DATE:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
return value
|
||||
@ -252,14 +254,14 @@ class CustomField(models.Model):
|
||||
"""
|
||||
if serialized_value == '':
|
||||
return None
|
||||
if self.type == CF_TYPE_INTEGER:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||
return int(serialized_value)
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||
return bool(int(serialized_value))
|
||||
if self.type == CF_TYPE_DATE:
|
||||
if self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||
# Read date as YYYY-MM-DD
|
||||
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 serialized_value
|
||||
|
||||
@ -312,7 +314,7 @@ class CustomFieldChoice(models.Model):
|
||||
to='extras.CustomField',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='choices',
|
||||
limit_choices_to={'type': CF_TYPE_SELECT}
|
||||
limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT}
|
||||
)
|
||||
value = models.CharField(
|
||||
max_length=100
|
||||
@ -330,14 +332,17 @@ class CustomFieldChoice(models.Model):
|
||||
return self.value
|
||||
|
||||
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.")
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
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()
|
||||
|
||||
|
||||
#
|
||||
|
@ -3,6 +3,7 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.models import Site
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
from extras.models import CustomField, CustomFieldValue, ObjectChange
|
||||
from utilities.testing import APITestCase
|
||||
@ -17,7 +18,7 @@ class ChangeLogTest(APITestCase):
|
||||
# Create a custom field on the Site model
|
||||
ct = ContentType.objects.get_for_model(Site)
|
||||
cf = CustomField(
|
||||
type=CF_TYPE_TEXT,
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
name='my_field',
|
||||
required=False
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
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 utilities.testing import APITestCase
|
||||
from virtualization.models import VirtualMachine
|
||||
@ -25,13 +25,13 @@ class CustomFieldTest(TestCase):
|
||||
def test_simple_fields(self):
|
||||
|
||||
DATA = (
|
||||
{'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
|
||||
{'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
|
||||
{'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
|
||||
{'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
|
||||
{'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
|
||||
{'field_type': CF_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_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
|
||||
{'field_type': CustomFieldTypeChoices.TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
|
||||
)
|
||||
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
@ -67,7 +67,7 @@ class CustomFieldTest(TestCase):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
# 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.obj_type.set([obj_type])
|
||||
cf.save()
|
||||
@ -107,37 +107,37 @@ class CustomFieldAPITest(APITestCase):
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_text.save()
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_integer.save()
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_boolean.save()
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_date.save()
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_url.save()
|
||||
|
||||
# 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.obj_type.set([content_type])
|
||||
self.cf_select.save()
|
||||
@ -308,8 +308,8 @@ class CustomFieldChoiceAPITest(APITestCase):
|
||||
|
||||
vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
|
||||
|
||||
self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT)
|
||||
self.cf_2 = CustomField.objects.create(name="cf_2", 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=CustomFieldTypeChoices.TYPE_SELECT)
|
||||
|
||||
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)
|
||||
|
Reference in New Issue
Block a user