mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'writable-custom-fields' into v2-develop
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
||||
|
||||
@@ -14,12 +16,40 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
def to_representation(self, obj):
|
||||
return obj
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
||||
custom_fields = {field.name: field for field in CustomField.objects.filter(obj_type=content_type)}
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
|
||||
# Validate selected choice
|
||||
cf = custom_fields[field_name]
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
for field_name, field in custom_fields.items():
|
||||
if field.required and field_name not in data:
|
||||
missing_fields.append(field_name)
|
||||
if missing_fields:
|
||||
raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields)))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
custom_fields = CustomFieldsSerializer()
|
||||
custom_fields = CustomFieldsSerializer(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -34,16 +64,59 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if self.instance is not None:
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
for obj in self.instance:
|
||||
_populate_custom_fields(obj, fields)
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
# Retrieve the set of CustomFields which apply to this type of object
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
fields = CustomField.objects.filter(obj_type=content_type)
|
||||
|
||||
# Populate CustomFieldValues for each instance from database
|
||||
try:
|
||||
for obj in self.instance:
|
||||
_populate_custom_fields(obj, fields)
|
||||
except TypeError:
|
||||
_populate_custom_fields(self.instance, fields)
|
||||
|
||||
def _save_custom_fields(self, instance, custom_fields):
|
||||
content_type = ContentType.objects.get_for_model(self.Meta.model)
|
||||
for field_name, value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=field_name)
|
||||
CustomFieldValue.objects.update_or_create(
|
||||
field=custom_field,
|
||||
obj_type=content_type,
|
||||
obj_id=instance.pk,
|
||||
defaults={'serialized_value': value},
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).create(validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
self._save_custom_fields(instance, custom_fields)
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
self._save_custom_fields(instance, custom_fields)
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class CustomFieldChoiceSerializer(serializers.ModelSerializer):
|
||||
|
@@ -156,10 +156,7 @@ class CustomField(models.Model):
|
||||
# Read date as YYYY-MM-DD
|
||||
return date(*[int(n) for n in serialized_value.split('-')])
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
except CustomFieldChoice.DoesNotExist:
|
||||
return None
|
||||
return self.choices.get(pk=int(serialized_value))
|
||||
return serialized_value
|
||||
|
||||
|
||||
|
@@ -1,7 +1,12 @@
|
||||
from datetime import date
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Site
|
||||
|
||||
@@ -9,9 +14,11 @@ from extras.models import (
|
||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
||||
CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
)
|
||||
from users.models import Token
|
||||
from utilities.tests import HttpStatusMixin
|
||||
|
||||
|
||||
class CustomFieldTestCase(TestCase):
|
||||
class CustomFieldTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -95,3 +102,209 @@ class CustomFieldTestCase(TestCase):
|
||||
|
||||
# Delete the custom field
|
||||
cf.delete()
|
||||
|
||||
|
||||
class CustomFieldAPITest(HttpStatusMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = User.objects.create(username='testuser', is_superuser=True)
|
||||
token = Token.objects.create(user=user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(token.key)}
|
||||
|
||||
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.save()
|
||||
self.cf_text.obj_type = [content_type]
|
||||
self.cf_text.save()
|
||||
|
||||
# Integer custom field
|
||||
self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
|
||||
self.cf_integer.save()
|
||||
self.cf_integer.obj_type = [content_type]
|
||||
self.cf_integer.save()
|
||||
|
||||
# Boolean custom field
|
||||
self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
|
||||
self.cf_boolean.save()
|
||||
self.cf_boolean.obj_type = [content_type]
|
||||
self.cf_boolean.save()
|
||||
|
||||
# Date custom field
|
||||
self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
|
||||
self.cf_date.save()
|
||||
self.cf_date.obj_type = [content_type]
|
||||
self.cf_date.save()
|
||||
|
||||
# URL custom field
|
||||
self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
|
||||
self.cf_url.save()
|
||||
self.cf_url.obj_type = [content_type]
|
||||
self.cf_url.save()
|
||||
|
||||
# Select custom field
|
||||
self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
|
||||
self.cf_select.save()
|
||||
self.cf_select.obj_type = [content_type]
|
||||
self.cf_select.save()
|
||||
self.cf_select_choice1 = CustomFieldChoice(field=self.cf_select, value='Foo')
|
||||
self.cf_select_choice1.save()
|
||||
self.cf_select_choice2 = CustomFieldChoice(field=self.cf_select, value='Bar')
|
||||
self.cf_select_choice2.save()
|
||||
self.cf_select_choice3 = CustomFieldChoice(field=self.cf_select, value='Baz')
|
||||
self.cf_select_choice3.save()
|
||||
|
||||
self.site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
|
||||
def test_get_obj_without_custom_fields(self):
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'], {
|
||||
'magic_word': None,
|
||||
'magic_number': None,
|
||||
'is_magic': None,
|
||||
'magic_date': None,
|
||||
'magic_url': None,
|
||||
'magic_choice': None,
|
||||
})
|
||||
|
||||
def test_get_obj_with_custom_fields(self):
|
||||
|
||||
CUSTOM_FIELD_VALUES = [
|
||||
(self.cf_text, 'Test string'),
|
||||
(self.cf_integer, 1234),
|
||||
(self.cf_boolean, True),
|
||||
(self.cf_date, date(2016, 6, 23)),
|
||||
(self.cf_url, 'http://example.com/'),
|
||||
(self.cf_select, self.cf_select_choice1.pk),
|
||||
]
|
||||
for field, value in CUSTOM_FIELD_VALUES:
|
||||
cfv = CustomFieldValue(field=field, obj=self.site)
|
||||
cfv.value = value
|
||||
cfv.save()
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.site.name)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), CUSTOM_FIELD_VALUES[0][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), CUSTOM_FIELD_VALUES[1][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), CUSTOM_FIELD_VALUES[2][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), CUSTOM_FIELD_VALUES[3][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), CUSTOM_FIELD_VALUES[4][1])
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), {
|
||||
'value': self.cf_select_choice1.pk, 'label': 'Foo'
|
||||
})
|
||||
|
||||
def test_set_custom_field_text(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_word': 'Foo bar baz',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_word'), data['custom_fields']['magic_word'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_text)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_word'])
|
||||
|
||||
def test_set_custom_field_integer(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_number': 42,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_number'), data['custom_fields']['magic_number'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_integer)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_number'])
|
||||
|
||||
def test_set_custom_field_boolean(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'is_magic': 0,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('is_magic'), data['custom_fields']['is_magic'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_boolean)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['is_magic'])
|
||||
|
||||
def test_set_custom_field_date(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_date': '2017-04-25',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_date'), data['custom_fields']['magic_date'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_date)
|
||||
self.assertEqual(cfv.value.isoformat(), data['custom_fields']['magic_date'])
|
||||
|
||||
def test_set_custom_field_url(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_url': 'http://example.com/2/',
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_url'), data['custom_fields']['magic_url'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_url)
|
||||
self.assertEqual(cfv.value, data['custom_fields']['magic_url'])
|
||||
|
||||
def test_set_custom_field_select(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Site 1',
|
||||
'slug': 'test-site-1',
|
||||
'custom_fields': {
|
||||
'magic_choice': self.cf_select_choice2.pk,
|
||||
}
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': self.site.pk})
|
||||
response = self.client.put(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice'])
|
||||
cfv = self.site.custom_field_values.get(field=self.cf_select)
|
||||
self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice'])
|
||||
|
Reference in New Issue
Block a user