Jeremy Stretch пре 6 година
родитељ
комит
3ff22bea56

+ 6 - 6
netbox/extras/api/customfields.py

@@ -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 - 0
netbox/extras/choices.py

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

+ 0 - 16
netbox/extras/constants.py

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

+ 3 - 2
netbox/extras/filters.py

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

+ 6 - 5
netbox/extras/forms.py

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

+ 0 - 2
netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py

@@ -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 - 2
netbox/extras/migrations/0010_customfield_filter_logic.py

@@ -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 - 0
netbox/extras/migrations/0029_3569_customfield_fields.py

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

+ 18 - 13
netbox/extras/models.py

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

+ 2 - 1
netbox/extras/tests/test_changelog.py

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

+ 17 - 17
netbox/extras/tests/test_customfields.py

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