| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705 |
- import datetime
- from decimal import Decimal
- from django.core.exceptions import ValidationError
- from django.urls import reverse
- from rest_framework import status
- from core.models import ObjectType
- from dcim.filtersets import SiteFilterSet
- from dcim.forms import SiteImportForm
- from dcim.models import Manufacturer, Rack, Site
- from extras.choices import *
- from extras.models import CustomField, CustomFieldChoiceSet
- from ipam.models import VLAN
- from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
- from utilities.testing import APITestCase, TestCase
- from virtualization.models import VirtualMachine
- class CustomFieldTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- Site.objects.bulk_create([
- Site(name='Site A', slug='site-a'),
- Site(name='Site B', slug='site-b'),
- Site(name='Site C', slug='site-c'),
- ])
- cls.object_type = ObjectType.objects.get_for_model(Site)
- def test_invalid_name(self):
- """
- Try creating a CustomField with an invalid name.
- """
- with self.assertRaises(ValidationError):
- # Invalid character
- CustomField(name='?', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
- with self.assertRaises(ValidationError):
- # Double underscores not permitted
- CustomField(name='foo__bar', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
- def test_text_field(self):
- value = 'Foobar!'
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='text_field',
- type=CustomFieldTypeChoices.TYPE_TEXT,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_longtext_field(self):
- value = 'A' * 256
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='longtext_field',
- type=CustomFieldTypeChoices.TYPE_LONGTEXT,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_integer_field(self):
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='integer_field',
- type=CustomFieldTypeChoices.TYPE_INTEGER,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- for value in (123456, 0, -123456):
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_decimal_field(self):
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='decimal_field',
- type=CustomFieldTypeChoices.TYPE_DECIMAL,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- for value in (123456.54, 0, -123456.78):
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_boolean_field(self):
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='boolean_field',
- type=CustomFieldTypeChoices.TYPE_INTEGER,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- for value in (True, False):
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_date_field(self):
- value = datetime.date(2016, 6, 23)
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='date_field',
- type=CustomFieldTypeChoices.TYPE_DATE,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = cf.serialize(value)
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.cf[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_datetime_field(self):
- value = datetime.datetime(2016, 6, 23, 9, 45, 0)
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='date_field',
- type=CustomFieldTypeChoices.TYPE_DATETIME,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = cf.serialize(value)
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.cf[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_url_field(self):
- value = 'http://example.com/'
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='url_field',
- type=CustomFieldTypeChoices.TYPE_URL,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_json_field(self):
- value = '{"foo": 1, "bar": 2}'
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='json_field',
- type=CustomFieldTypeChoices.TYPE_JSON,
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_select_field(self):
- CHOICES = (
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('c', 'Option C'),
- )
- value = 'a'
- # Create a set of custom field choices
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=CHOICES
- )
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='select_field',
- type=CustomFieldTypeChoices.TYPE_SELECT,
- required=False,
- choice_set=choice_set
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_multiselect_field(self):
- CHOICES = (
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('c', 'Option C'),
- )
- value = ['a', 'b']
- # Create a set of custom field choices
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=CHOICES
- )
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='multiselect_field',
- type=CustomFieldTypeChoices.TYPE_MULTISELECT,
- required=False,
- choice_set=choice_set
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_remove_selected_choice(self):
- """
- Removing a ChoiceSet choice that is referenced by an object should raise
- a ValidationError exception.
- """
- CHOICES = (
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('c', 'Option C'),
- ('d', 'Option D'),
- )
- # Create a set of custom field choices
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=CHOICES
- )
- # Create a select custom field
- cf = CustomField.objects.create(
- name='select_field',
- type=CustomFieldTypeChoices.TYPE_SELECT,
- required=False,
- choice_set=choice_set
- )
- cf.object_types.set([self.object_type])
- # Create a multi-select custom field
- cf_multiselect = CustomField.objects.create(
- name='multiselect_field',
- type=CustomFieldTypeChoices.TYPE_MULTISELECT,
- required=False,
- choice_set=choice_set
- )
- cf_multiselect.object_types.set([self.object_type])
- # Assign a choice for both custom fields on an object
- instance = Site.objects.first()
- instance.custom_field_data[cf.name] = 'a'
- instance.custom_field_data[cf_multiselect.name] = ['b', 'c']
- instance.save()
- # Attempting to delete a selected choice should fail
- with self.assertRaises(ValidationError):
- choice_set.extra_choices = (
- ('b', 'Option B'),
- ('c', 'Option C'),
- ('d', 'Option D'),
- )
- choice_set.full_clean()
- # Attempting to delete either of the multi-select choices should fail
- with self.assertRaises(ValidationError):
- choice_set.extra_choices = (
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('d', 'Option D'),
- )
- choice_set.full_clean()
- # Removing a non-selected choice should succeed
- choice_set.extra_choices = (
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('c', 'Option C'),
- )
- choice_set.full_clean()
- def test_object_field(self):
- value = VLAN.objects.create(name='VLAN 1', vid=1).pk
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='object_field',
- type=CustomFieldTypeChoices.TYPE_OBJECT,
- related_object_type=ObjectType.objects.get_for_model(VLAN),
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_multiobject_field(self):
- vlans = (
- VLAN(name='VLAN 1', vid=1),
- VLAN(name='VLAN 2', vid=2),
- VLAN(name='VLAN 3', vid=3),
- )
- VLAN.objects.bulk_create(vlans)
- value = [vlan.pk for vlan in vlans]
- # Create a custom field & check that initial value is null
- cf = CustomField.objects.create(
- name='object_field',
- type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
- related_object_type=ObjectType.objects.get_for_model(VLAN),
- required=False
- )
- cf.object_types.set([self.object_type])
- instance = Site.objects.first()
- self.assertIsNone(instance.custom_field_data[cf.name])
- # Assign a value and check that it is saved
- instance.custom_field_data[cf.name] = value
- instance.save()
- instance.refresh_from_db()
- self.assertEqual(instance.custom_field_data[cf.name], value)
- # Delete the stored value and check that it is now null
- instance.custom_field_data.pop(cf.name)
- instance.save()
- instance.refresh_from_db()
- self.assertIsNone(instance.custom_field_data.get(cf.name))
- def test_rename_customfield(self):
- obj_type = ObjectType.objects.get_for_model(Site)
- FIELD_DATA = 'abc'
- # Create a custom field
- cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
- cf.save()
- cf.object_types.set([obj_type])
- # Assign custom field data to an object
- site = Site.objects.create(
- name='Site 1',
- slug='site-1',
- custom_field_data={'field1': FIELD_DATA}
- )
- site.refresh_from_db()
- self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
- # Rename the custom field
- cf.name = 'field2'
- cf.save()
- # Check that custom field data on the object has been updated
- site.refresh_from_db()
- self.assertNotIn('field1', site.custom_field_data)
- self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
- def test_default_value_validation(self):
- choiceset = CustomFieldChoiceSet.objects.create(
- name="Test Choice Set",
- extra_choices=(
- ('choice1', 'Choice 1'),
- ('choice2', 'Choice 2'),
- )
- )
- site = Site.objects.create(name='Site 1', slug='site-1')
- object_type = ObjectType.objects.get_for_model(Site)
- # Text
- CustomField(name='test', type='text', required=True, default="Default text").full_clean()
- # Integer
- CustomField(name='test', type='integer', required=True, default=1).full_clean()
- with self.assertRaises(ValidationError):
- CustomField(name='test', type='integer', required=True, default='xxx').full_clean()
- # Boolean
- CustomField(name='test', type='boolean', required=True, default=True).full_clean()
- with self.assertRaises(ValidationError):
- CustomField(name='test', type='boolean', required=True, default='xxx').full_clean()
- # Date
- CustomField(name='test', type='date', required=True, default="2023-02-25").full_clean()
- with self.assertRaises(ValidationError):
- CustomField(name='test', type='date', required=True, default='xxx').full_clean()
- # Datetime
- CustomField(name='test', type='datetime', required=True, default="2023-02-25 02:02:02").full_clean()
- with self.assertRaises(ValidationError):
- CustomField(name='test', type='datetime', required=True, default='xxx').full_clean()
- # URL
- CustomField(name='test', type='url', required=True, default="https://www.netbox.dev").full_clean()
- # JSON
- CustomField(name='test', type='json', required=True, default='{"test": "object"}').full_clean()
- # Selection
- CustomField(name='test', type='select', required=True, choice_set=choiceset, default='choice1').full_clean()
- with self.assertRaises(ValidationError):
- CustomField(name='test', type='select', required=True, choice_set=choiceset, default='xxx').full_clean()
- # Multi-select
- CustomField(
- name='test',
- type='multiselect',
- required=True,
- choice_set=choiceset,
- default=['choice1'] # Single default choice
- ).full_clean()
- CustomField(
- name='test',
- type='multiselect',
- required=True,
- choice_set=choiceset,
- default=['choice1', 'choice2'] # Multiple default choices
- ).full_clean()
- with self.assertRaises(ValidationError):
- CustomField(
- name='test',
- type='multiselect',
- required=True,
- choice_set=choiceset,
- default=['xxx']
- ).full_clean()
- # Object
- CustomField(
- name='test',
- type='object',
- required=True,
- related_object_type=object_type,
- default=site.pk
- ).full_clean()
- with (self.assertRaises(ValidationError)):
- CustomField(
- name='test',
- type='object',
- required=True,
- related_object_type=object_type,
- default="xxx"
- ).full_clean()
- # Multi-object
- CustomField(
- name='test',
- type='multiobject',
- required=True,
- related_object_type=object_type,
- default=[site.pk]
- ).full_clean()
- with self.assertRaises(ValidationError):
- CustomField(
- name='test',
- type='multiobject',
- required=True,
- related_object_type=object_type,
- default=["xxx"]
- ).full_clean()
- class CustomFieldManagerTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- object_type = ObjectType.objects.get_for_model(Site)
- custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
- custom_field.save()
- custom_field.object_types.set([object_type])
- def test_get_for_model(self):
- self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
- self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
- class CustomFieldAPITest(APITestCase):
- @classmethod
- def setUpTestData(cls):
- object_type = ObjectType.objects.get_for_model(Site)
- # Create some VLANs
- vlans = (
- VLAN(name='VLAN 1', vid=1),
- VLAN(name='VLAN 2', vid=2),
- VLAN(name='VLAN 3', vid=3),
- VLAN(name='VLAN 4', vid=4),
- VLAN(name='VLAN 5', vid=5),
- )
- VLAN.objects.bulk_create(vlans)
- # Create a set of custom field choices
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=(('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz'))
- )
- custom_fields = (
- CustomField(
- type=CustomFieldTypeChoices.TYPE_TEXT,
- name='text_field',
- default='foo'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_LONGTEXT,
- name='longtext_field',
- default='ABC'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_INTEGER,
- name='integer_field',
- default=123
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_DECIMAL,
- name='decimal_field',
- default=123.45
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_BOOLEAN,
- name='boolean_field',
- default=False
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_DATE,
- name='date_field',
- default='2020-01-01'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_DATETIME,
- name='datetime_field',
- default='2020-01-01T01:23:45'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_URL,
- name='url_field',
- default='http://example.com/1'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_JSON,
- name='json_field',
- default='{"x": "y"}'
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_SELECT,
- name='select_field',
- default='foo',
- choice_set=choice_set
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_MULTISELECT,
- name='multiselect_field',
- default=['foo'],
- choice_set=choice_set,
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_OBJECT,
- name='object_field',
- related_object_type=ObjectType.objects.get_for_model(VLAN),
- default=vlans[0].pk,
- ),
- CustomField(
- type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
- name='multiobject_field',
- related_object_type=ObjectType.objects.get_for_model(VLAN),
- default=[vlans[0].pk, vlans[1].pk],
- ),
- )
- for cf in custom_fields:
- cf.save()
- cf.object_types.set([object_type])
- # Create some sites *after* creating the custom fields. This ensures that
- # default values are not set for the assigned objects.
- sites = (
- Site(name='Site 1', slug='site-1'),
- Site(name='Site 2', slug='site-2'),
- )
- Site.objects.bulk_create(sites)
- # Assign custom field values for site 2
- sites[1].custom_field_data = {
- custom_fields[0].name: 'bar',
- custom_fields[1].name: 'DEF',
- custom_fields[2].name: 456,
- custom_fields[3].name: Decimal('456.78'),
- custom_fields[4].name: True,
- custom_fields[5].name: '2020-01-02',
- custom_fields[6].name: '2020-01-02 12:00:00',
- custom_fields[7].name: 'http://example.com/2',
- custom_fields[8].name: '{"foo": 1, "bar": 2}',
- custom_fields[9].name: 'bar',
- custom_fields[10].name: ['bar', 'baz'],
- custom_fields[11].name: vlans[1].pk,
- custom_fields[12].name: [vlans[2].pk, vlans[3].pk],
- }
- sites[1].save()
- def test_get_custom_fields(self):
- TYPES = {
- CustomFieldTypeChoices.TYPE_TEXT: 'string',
- CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
- CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
- CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal',
- CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
- CustomFieldTypeChoices.TYPE_DATE: 'string',
- CustomFieldTypeChoices.TYPE_DATETIME: 'string',
- CustomFieldTypeChoices.TYPE_URL: 'string',
- CustomFieldTypeChoices.TYPE_JSON: 'object',
- CustomFieldTypeChoices.TYPE_SELECT: 'string',
- CustomFieldTypeChoices.TYPE_MULTISELECT: 'array',
- CustomFieldTypeChoices.TYPE_OBJECT: 'object',
- CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array',
- }
- self.add_permissions('extras.view_customfield')
- url = reverse('extras-api:customfield-list')
- response = self.client.get(url, **self.header)
- self.assertEqual(response.data['count'], len(TYPES))
- # Validate data types
- for customfield in response.data['results']:
- cf_type = customfield['type']['value']
- self.assertEqual(customfield['data_type'], TYPES[cf_type])
- def test_get_single_object_without_custom_field_data(self):
- """
- Validate that custom fields are present on an object even if it has no values defined.
- """
- site1 = Site.objects.get(name='Site 1')
- url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk})
- self.add_permissions('dcim.view_site')
- response = self.client.get(url, **self.header)
- self.assertEqual(response.data['name'], site1.name)
- self.assertEqual(response.data['custom_fields'], {
- 'text_field': None,
- 'longtext_field': None,
- 'integer_field': None,
- 'decimal_field': None,
- 'boolean_field': None,
- 'date_field': None,
- 'datetime_field': None,
- 'url_field': None,
- 'json_field': None,
- 'select_field': None,
- 'multiselect_field': None,
- 'object_field': None,
- 'multiobject_field': None,
- })
- def test_get_single_object_with_custom_field_data(self):
- """
- Validate that custom fields are present and correctly set for an object with values defined.
- """
- site2 = Site.objects.get(name='Site 2')
- site2_cfvs = site2.cf
- url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
- self.add_permissions('dcim.view_site')
- response = self.client.get(url, **self.header)
- self.assertEqual(response.data['name'], site2.name)
- self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
- self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
- self.assertEqual(response.data['custom_fields']['integer_field'], site2_cfvs['integer_field'])
- self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field'])
- self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
- self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
- self.assertEqual(response.data['custom_fields']['datetime_field'], site2_cfvs['datetime_field'])
- self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
- self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field'])
- self.assertEqual(response.data['custom_fields']['select_field'], site2_cfvs['select_field'])
- self.assertEqual(response.data['custom_fields']['multiselect_field'], site2_cfvs['multiselect_field'])
- self.assertEqual(response.data['custom_fields']['object_field']['id'], site2_cfvs['object_field'].pk)
- self.assertEqual(
- [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
- [obj.pk for obj in site2_cfvs['multiobject_field']]
- )
- def test_create_single_object_with_defaults(self):
- """
- Create a new site with no specified custom field values and check that it received the default values.
- """
- cf_defaults = {
- cf.name: cf.default for cf in CustomField.objects.all()
- }
- data = {
- 'name': 'Site 3',
- 'slug': 'site-3',
- }
- url = reverse('dcim-api:site-list')
- self.add_permissions('dcim.add_site')
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- # Validate response data
- response_cf = response.data['custom_fields']
- self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
- self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
- self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
- self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
- self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
- self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field'])
- self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field'])
- self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
- self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
- self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
- self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
- self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
- self.assertEqual(
- [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
- cf_defaults['multiobject_field']
- )
- # Validate database data
- site = Site.objects.get(pk=response.data['id'])
- self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
- self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
- self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
- self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
- self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
- self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field'])
- self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field'])
- self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
- self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
- self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
- self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
- self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
- self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
- def test_create_single_object_with_values(self):
- """
- Create a single new site with a value for each type of custom field.
- """
- data = {
- 'name': 'Site 3',
- 'slug': 'site-3',
- 'custom_fields': {
- 'text_field': 'bar',
- 'longtext_field': 'blah blah blah',
- 'integer_field': 456,
- 'decimal_field': 456.78,
- 'boolean_field': True,
- 'date_field': datetime.date(2020, 1, 2),
- 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
- 'url_field': 'http://example.com/2',
- 'json_field': '{"foo": 1, "bar": 2}',
- 'select_field': 'bar',
- 'multiselect_field': ['bar', 'baz'],
- 'object_field': VLAN.objects.get(vid=2).pk,
- 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
- },
- }
- url = reverse('dcim-api:site-list')
- self.add_permissions('dcim.add_site')
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- # Validate response data
- response_cf = response.data['custom_fields']
- data_cf = data['custom_fields']
- self.assertEqual(response_cf['text_field'], data_cf['text_field'])
- self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
- self.assertEqual(response_cf['integer_field'], data_cf['integer_field'])
- self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field'])
- self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
- self.assertEqual(response_cf['date_field'], data_cf['date_field'])
- self.assertEqual(response_cf['datetime_field'], data_cf['datetime_field'])
- self.assertEqual(response_cf['url_field'], data_cf['url_field'])
- self.assertEqual(response_cf['json_field'], data_cf['json_field'])
- self.assertEqual(response_cf['select_field'], data_cf['select_field'])
- self.assertEqual(response_cf['multiselect_field'], data_cf['multiselect_field'])
- self.assertEqual(response_cf['object_field']['id'], data_cf['object_field'])
- self.assertEqual(
- [obj['id'] for obj in response_cf['multiobject_field']],
- data_cf['multiobject_field']
- )
- # Validate database data
- site = Site.objects.get(pk=response.data['id'])
- self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
- self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
- self.assertEqual(site.custom_field_data['integer_field'], data_cf['integer_field'])
- self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field'])
- self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
- self.assertEqual(site.cf['date_field'], data_cf['date_field'])
- self.assertEqual(site.cf['datetime_field'], data_cf['datetime_field'])
- self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
- self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field'])
- self.assertEqual(site.custom_field_data['select_field'], data_cf['select_field'])
- self.assertEqual(site.custom_field_data['multiselect_field'], data_cf['multiselect_field'])
- self.assertEqual(site.custom_field_data['object_field'], data_cf['object_field'])
- self.assertEqual(site.custom_field_data['multiobject_field'], data_cf['multiobject_field'])
- def test_create_multiple_objects_with_defaults(self):
- """
- Create three new sites with no specified custom field values and check that each received
- the default custom field values.
- """
- cf_defaults = {
- cf.name: cf.default for cf in CustomField.objects.all()
- }
- data = (
- {
- 'name': 'Site 3',
- 'slug': 'site-3',
- },
- {
- 'name': 'Site 4',
- 'slug': 'site-4',
- },
- {
- 'name': 'Site 5',
- 'slug': 'site-5',
- },
- )
- url = reverse('dcim-api:site-list')
- self.add_permissions('dcim.add_site')
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(len(response.data), len(data))
- for i, obj in enumerate(data):
- # Validate response data
- response_cf = response.data[i]['custom_fields']
- self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
- self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
- self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
- self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
- self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
- self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field'])
- self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field'])
- self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
- self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
- self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
- self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
- self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
- self.assertEqual(
- [obj['id'] for obj in response_cf['multiobject_field']],
- cf_defaults['multiobject_field']
- )
- # Validate database data
- site = Site.objects.get(pk=response.data[i]['id'])
- self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
- self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
- self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
- self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
- self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
- self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field'])
- self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field'])
- self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
- self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
- self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
- self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
- self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
- self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
- def test_create_multiple_objects_with_values(self):
- """
- Create a three new sites, each with custom fields defined.
- """
- custom_field_data = {
- 'text_field': 'bar',
- 'longtext_field': 'abcdefghij',
- 'integer_field': 456,
- 'decimal_field': 456.78,
- 'boolean_field': True,
- 'date_field': datetime.date(2020, 1, 2),
- 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
- 'url_field': 'http://example.com/2',
- 'json_field': '{"foo": 1, "bar": 2}',
- 'select_field': 'bar',
- 'multiselect_field': ['bar', 'baz'],
- 'object_field': VLAN.objects.get(vid=2).pk,
- 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
- }
- data = (
- {
- 'name': 'Site 3',
- 'slug': 'site-3',
- 'custom_fields': custom_field_data,
- },
- {
- 'name': 'Site 4',
- 'slug': 'site-4',
- 'custom_fields': custom_field_data,
- },
- {
- 'name': 'Site 5',
- 'slug': 'site-5',
- 'custom_fields': custom_field_data,
- },
- )
- url = reverse('dcim-api:site-list')
- self.add_permissions('dcim.add_site')
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(len(response.data), len(data))
- for i, obj in enumerate(data):
- # Validate response data
- response_cf = response.data[i]['custom_fields']
- self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
- self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
- self.assertEqual(response_cf['integer_field'], custom_field_data['integer_field'])
- self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field'])
- self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
- self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
- self.assertEqual(response_cf['datetime_field'], custom_field_data['datetime_field'])
- self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
- self.assertEqual(response_cf['json_field'], custom_field_data['json_field'])
- self.assertEqual(response_cf['select_field'], custom_field_data['select_field'])
- self.assertEqual(response_cf['multiselect_field'], custom_field_data['multiselect_field'])
- self.assertEqual(response_cf['object_field']['id'], custom_field_data['object_field'])
- self.assertEqual(
- [obj['id'] for obj in response_cf['multiobject_field']],
- custom_field_data['multiobject_field']
- )
- # Validate database data
- site = Site.objects.get(pk=response.data[i]['id'])
- self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
- self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
- self.assertEqual(site.custom_field_data['integer_field'], custom_field_data['integer_field'])
- self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field'])
- self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
- self.assertEqual(site.cf['date_field'], custom_field_data['date_field'])
- self.assertEqual(site.cf['datetime_field'], custom_field_data['datetime_field'])
- self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
- self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field'])
- self.assertEqual(site.custom_field_data['select_field'], custom_field_data['select_field'])
- self.assertEqual(site.custom_field_data['multiselect_field'], custom_field_data['multiselect_field'])
- self.assertEqual(site.custom_field_data['object_field'], custom_field_data['object_field'])
- self.assertEqual(site.custom_field_data['multiobject_field'], custom_field_data['multiobject_field'])
- def test_update_single_object_with_values(self):
- """
- Update an object with existing custom field values. Ensure that only the updated custom field values are
- modified.
- """
- site2 = Site.objects.get(name='Site 2')
- original_cfvs = {**site2.cf}
- data = {
- 'custom_fields': {
- 'text_field': 'ABCD',
- 'integer_field': 1234,
- },
- }
- url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
- self.add_permissions('dcim.change_site')
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- # Validate response data
- response_cf = response.data['custom_fields']
- self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
- self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
- self.assertEqual(response_cf['integer_field'], data['custom_fields']['integer_field'])
- self.assertEqual(response_cf['decimal_field'], original_cfvs['decimal_field'])
- self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
- self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
- self.assertEqual(response_cf['datetime_field'], original_cfvs['datetime_field'])
- self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
- self.assertEqual(response_cf['json_field'], original_cfvs['json_field'])
- self.assertEqual(response_cf['select_field'], original_cfvs['select_field'])
- self.assertEqual(response_cf['multiselect_field'], original_cfvs['multiselect_field'])
- self.assertEqual(response_cf['object_field']['id'], original_cfvs['object_field'].pk)
- self.assertListEqual(
- [obj['id'] for obj in response_cf['multiobject_field']],
- [obj.pk for obj in original_cfvs['multiobject_field']]
- )
- # Validate database data
- site2 = Site.objects.get(pk=site2.pk)
- self.assertEqual(site2.cf['text_field'], data['custom_fields']['text_field'])
- self.assertEqual(site2.cf['longtext_field'], original_cfvs['longtext_field'])
- self.assertEqual(site2.cf['integer_field'], data['custom_fields']['integer_field'])
- self.assertEqual(site2.cf['decimal_field'], original_cfvs['decimal_field'])
- self.assertEqual(site2.cf['boolean_field'], original_cfvs['boolean_field'])
- self.assertEqual(site2.cf['date_field'], original_cfvs['date_field'])
- self.assertEqual(site2.cf['datetime_field'], original_cfvs['datetime_field'])
- self.assertEqual(site2.cf['url_field'], original_cfvs['url_field'])
- self.assertEqual(site2.cf['json_field'], original_cfvs['json_field'])
- self.assertEqual(site2.cf['select_field'], original_cfvs['select_field'])
- self.assertEqual(site2.cf['multiselect_field'], original_cfvs['multiselect_field'])
- self.assertEqual(site2.cf['object_field'], original_cfvs['object_field'])
- self.assertListEqual(
- list(site2.cf['multiobject_field']),
- list(original_cfvs['multiobject_field'])
- )
- def test_specify_related_object_by_attr(self):
- site1 = Site.objects.get(name='Site 1')
- vlans = VLAN.objects.all()[:3]
- url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk})
- self.add_permissions('dcim.change_site')
- # Set related objects by PK
- data = {
- 'custom_fields': {
- 'object_field': vlans[0].pk,
- 'multiobject_field': [vlans[1].pk, vlans[2].pk],
- },
- }
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(
- response.data['custom_fields']['object_field']['id'],
- vlans[0].pk
- )
- self.assertListEqual(
- [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
- [vlans[1].pk, vlans[2].pk]
- )
- # Set related objects by name
- data = {
- 'custom_fields': {
- 'object_field': {
- 'name': vlans[0].name,
- },
- 'multiobject_field': [
- {
- 'name': vlans[1].name
- },
- {
- 'name': vlans[2].name
- },
- ],
- },
- }
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(
- response.data['custom_fields']['object_field']['id'],
- vlans[0].pk
- )
- self.assertListEqual(
- [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
- [vlans[1].pk, vlans[2].pk]
- )
- # Clear related objects
- data = {
- 'custom_fields': {
- 'object_field': None,
- 'multiobject_field': [],
- },
- }
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertIsNone(response.data['custom_fields']['object_field'])
- self.assertListEqual(response.data['custom_fields']['multiobject_field'], [])
- def test_minimum_maximum_values_validation(self):
- site2 = Site.objects.get(name='Site 2')
- url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
- self.add_permissions('dcim.change_site')
- cf_integer = CustomField.objects.get(name='integer_field')
- cf_integer.validation_minimum = 10
- cf_integer.validation_maximum = 20
- cf_integer.save()
- data = {'custom_fields': {'integer_field': 9}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- data = {'custom_fields': {'integer_field': 21}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- data = {'custom_fields': {'integer_field': 15}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- def test_regex_validation(self):
- site2 = Site.objects.get(name='Site 2')
- url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
- self.add_permissions('dcim.change_site')
- cf_text = CustomField.objects.get(name='text_field')
- cf_text.validation_regex = r'^[A-Z]{3}$' # Three uppercase letters
- cf_text.save()
- data = {'custom_fields': {'text_field': 'ABC123'}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- data = {'custom_fields': {'text_field': 'abc'}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- data = {'custom_fields': {'text_field': 'ABC'}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- def test_uniqueness_validation(self):
- # Create a unique custom field
- cf_text = CustomField.objects.get(name='text_field')
- cf_text.unique = True
- cf_text.save()
- # Set a value on site 1
- site1 = Site.objects.get(name='Site 1')
- site1.custom_field_data['text_field'] = 'ABC123'
- site1.save()
- site2 = Site.objects.get(name='Site 2')
- url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
- self.add_permissions('dcim.change_site')
- data = {'custom_fields': {'text_field': 'ABC123'}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
- data = {'custom_fields': {'text_field': 'DEF456'}}
- response = self.client.patch(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
- class CustomFieldImportTest(TestCase):
- user_permissions = (
- 'dcim.view_site',
- 'dcim.add_site',
- )
- @classmethod
- def setUpTestData(cls):
- # Create a set of custom field choices
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=(
- ('a', 'Option A'),
- ('b', 'Option B'),
- ('c', 'Option C'),
- )
- )
- custom_fields = (
- CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
- CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
- CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
- CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL),
- CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
- CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
- CustomField(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME),
- CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
- CustomField(name='json', type=CustomFieldTypeChoices.TYPE_JSON),
- CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choice_set=choice_set),
- CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choice_set=choice_set),
- )
- for cf in custom_fields:
- cf.save()
- cf.object_types.set([ObjectType.objects.get_for_model(Site)])
- def test_import(self):
- """
- Import a Site in CSV format, including a value for each CustomField.
- """
- data = (
- (
- 'name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date',
- 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect',
- ),
- (
- 'Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01',
- '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'a', '"a,b"',
- ),
- (
- 'Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02',
- '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'b', '"b,c"',
- ),
- ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
- )
- csv_data = '\n'.join(','.join(row) for row in data)
- response = self.client.post(reverse('dcim:site_bulk_import'), {
- 'data': csv_data,
- 'format': ImportFormatChoices.CSV,
- 'csv_delimiter': CSVDelimiterChoices.AUTO,
- })
- self.assertEqual(response.status_code, 302)
- self.assertEqual(Site.objects.count(), 3)
- # Validate data for site 1
- site1 = Site.objects.get(name='Site 1')
- self.assertEqual(len(site1.custom_field_data), 11)
- self.assertEqual(site1.custom_field_data['text'], 'ABC')
- self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
- self.assertEqual(site1.custom_field_data['integer'], 123)
- self.assertEqual(site1.custom_field_data['decimal'], 123.45)
- self.assertEqual(site1.custom_field_data['boolean'], True)
- self.assertEqual(site1.cf['date'].isoformat(), '2020-01-01')
- self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00')
- self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
- self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
- self.assertEqual(site1.custom_field_data['select'], 'a')
- self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
- # Validate data for site 2
- site2 = Site.objects.get(name='Site 2')
- self.assertEqual(len(site2.custom_field_data), 11)
- self.assertEqual(site2.custom_field_data['text'], 'DEF')
- self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
- self.assertEqual(site2.custom_field_data['integer'], 456)
- self.assertEqual(site2.custom_field_data['decimal'], 456.78)
- self.assertEqual(site2.custom_field_data['boolean'], False)
- self.assertEqual(site2.cf['date'].isoformat(), '2020-01-02')
- self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00')
- self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
- self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
- self.assertEqual(site2.custom_field_data['select'], 'b')
- self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
- # No custom field data should be set for site 3
- site3 = Site.objects.get(name='Site 3')
- self.assertFalse(any(site3.custom_field_data.values()))
- def test_import_missing_required(self):
- """
- Attempt to import an object missing a required custom field.
- """
- # Set one of our CustomFields to required
- CustomField.objects.filter(name='text').update(required=True)
- form_data = {
- 'name': 'Site 1',
- 'slug': 'site-1',
- }
- form = SiteImportForm(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertIn('cf_text', form.errors)
- def test_import_invalid_choice(self):
- """
- Attempt to import an object with an invalid choice selection.
- """
- form_data = {
- 'name': 'Site 1',
- 'slug': 'site-1',
- 'cf_select': 'Choice X'
- }
- form = SiteImportForm(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertIn('cf_select', form.errors)
- class CustomFieldModelTest(TestCase):
- @classmethod
- def setUpTestData(cls):
- cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
- cf1.save()
- cf1.object_types.set([ObjectType.objects.get_for_model(Site)])
- cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
- cf2.save()
- cf2.object_types.set([ObjectType.objects.get_for_model(Rack)])
- def test_cf_data(self):
- """
- Check that custom field data is present on the instance immediately after being set and after being fetched
- from the database.
- """
- site = Site(name='Test Site', slug='test-site')
- # Check custom field data on new instance
- site.custom_field_data['foo'] = 'abc'
- self.assertEqual(site.cf['foo'], 'abc')
- # Check custom field data from database
- site.save()
- site = Site.objects.get(name='Test Site')
- self.assertEqual(site.cf['foo'], 'abc')
- def test_invalid_data(self):
- """
- Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
- """
- site = Site(name='Test Site', slug='test-site')
- # Set custom field data
- site.custom_field_data['foo'] = 'abc'
- site.custom_field_data['bar'] = 'def'
- with self.assertRaises(ValidationError):
- site.clean()
- del site.custom_field_data['bar']
- site.clean()
- def test_missing_required_field(self):
- """
- Check that a ValidationError is raised if any required custom fields are not present.
- """
- cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
- cf3.save()
- cf3.object_types.set([ObjectType.objects.get_for_model(Site)])
- site = Site(name='Test Site', slug='test-site')
- # Set custom field data with a required field omitted
- site.custom_field_data['foo'] = 'abc'
- with self.assertRaises(ValidationError):
- site.clean()
- site.custom_field_data['baz'] = 'def'
- site.clean()
- class CustomFieldModelFilterTest(TestCase):
- queryset = Site.objects.all()
- filterset = SiteFilterSet
- @classmethod
- def setUpTestData(cls):
- object_type = ObjectType.objects.get_for_model(Site)
- manufacturers = Manufacturer.objects.bulk_create((
- Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
- Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
- Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
- Manufacturer(name='Manufacturer 4', slug='manufacturer-4'),
- ))
- choice_set = CustomFieldChoiceSet.objects.create(
- name='Custom Field Choice Set 1',
- extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
- )
- # Integer filtering
- cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
- cf.save()
- cf.object_types.set([object_type])
- # Decimal filtering
- cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
- cf.save()
- cf.object_types.set([object_type])
- # Boolean filtering
- cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
- cf.save()
- cf.object_types.set([object_type])
- # Exact text filtering
- cf = CustomField(
- name='cf4',
- type=CustomFieldTypeChoices.TYPE_TEXT,
- filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
- )
- cf.save()
- cf.object_types.set([object_type])
- # Loose text filtering
- cf = CustomField(
- name='cf5',
- type=CustomFieldTypeChoices.TYPE_TEXT,
- filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
- )
- cf.save()
- cf.object_types.set([object_type])
- # Date filtering
- cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
- cf.save()
- cf.object_types.set([object_type])
- # Exact URL filtering
- cf = CustomField(
- name='cf7',
- type=CustomFieldTypeChoices.TYPE_URL,
- filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
- )
- cf.save()
- cf.object_types.set([object_type])
- # Loose URL filtering
- cf = CustomField(
- name='cf8',
- type=CustomFieldTypeChoices.TYPE_URL,
- filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
- )
- cf.save()
- cf.object_types.set([object_type])
- # Selection filtering
- cf = CustomField(
- name='cf9',
- type=CustomFieldTypeChoices.TYPE_SELECT,
- choice_set=choice_set
- )
- cf.save()
- cf.object_types.set([object_type])
- # Multiselect filtering
- cf = CustomField(
- name='cf10',
- type=CustomFieldTypeChoices.TYPE_MULTISELECT,
- choice_set=choice_set
- )
- cf.save()
- cf.object_types.set([object_type])
- # Object filtering
- cf = CustomField(
- name='cf11',
- type=CustomFieldTypeChoices.TYPE_OBJECT,
- related_object_type=ObjectType.objects.get_for_model(Manufacturer)
- )
- cf.save()
- cf.object_types.set([object_type])
- # Multi-object filtering
- cf = CustomField(
- name='cf12',
- type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
- related_object_type=ObjectType.objects.get_for_model(Manufacturer)
- )
- cf.save()
- cf.object_types.set([object_type])
- Site.objects.bulk_create([
- Site(name='Site 1', slug='site-1', custom_field_data={
- 'cf1': 100,
- 'cf2': 100.1,
- 'cf3': True,
- 'cf4': 'foo',
- 'cf5': 'foo',
- 'cf6': '2016-06-26',
- 'cf7': 'http://a.example.com',
- 'cf8': 'http://a.example.com',
- 'cf9': 'A',
- 'cf10': ['A', 'B'],
- 'cf11': manufacturers[0].pk,
- 'cf12': [manufacturers[0].pk, manufacturers[3].pk],
- }),
- Site(name='Site 2', slug='site-2', custom_field_data={
- 'cf1': 200,
- 'cf2': 200.2,
- 'cf3': True,
- 'cf4': 'foobar',
- 'cf5': 'foobar',
- 'cf6': '2016-06-27',
- 'cf7': 'http://b.example.com',
- 'cf8': 'http://b.example.com',
- 'cf9': 'B',
- 'cf10': ['B', 'C'],
- 'cf11': manufacturers[1].pk,
- 'cf12': [manufacturers[1].pk, manufacturers[3].pk],
- }),
- Site(name='Site 3', slug='site-3', custom_field_data={
- 'cf1': 300,
- 'cf2': 300.3,
- 'cf3': False,
- 'cf4': 'bar',
- 'cf5': 'bar',
- 'cf6': '2016-06-28',
- 'cf7': 'http://c.example.com',
- 'cf8': 'http://c.example.com',
- 'cf9': 'C',
- 'cf10': None,
- 'cf11': manufacturers[2].pk,
- 'cf12': [manufacturers[2].pk, manufacturers[3].pk],
- }),
- ])
- def test_filter_integer(self):
- self.assertEqual(self.filterset({'cf_cf1': [100, 200]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf1__n': [200]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf1__gt': [200]}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf1__gte': [200]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
- def test_filter_decimal(self):
- self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf2__n': [200.2]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf2__gt': [200.2]}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2)
- def test_filter_boolean(self):
- self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf3': False}, self.queryset).qs.count(), 1)
- def test_filter_text_strict(self):
- self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf4__n': ['foo']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf4__ic': ['foo']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf4__nic': ['foo']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf4__isw': ['foo']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf4__nisw': ['foo']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf4__iew': ['bar']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2)
- def test_filter_text_loose(self):
- self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2)
- def test_filter_date(self):
- self.assertEqual(self.filterset({'cf_cf6': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf6__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf6__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
- def test_filter_url_strict(self):
- self.assertEqual(
- self.filterset({'cf_cf7': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(),
- 2
- )
- self.assertEqual(self.filterset({'cf_cf7__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf7__ic': ['b']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf7__nic': ['b']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf7__isw': ['http://']}, self.queryset).qs.count(), 3)
- self.assertEqual(self.filterset({'cf_cf7__nisw': ['http://']}, self.queryset).qs.count(), 0)
- self.assertEqual(self.filterset({'cf_cf7__iew': ['.com']}, self.queryset).qs.count(), 3)
- self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0)
- self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
- def test_filter_url_loose(self):
- self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3)
- def test_filter_select(self):
- self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
- def test_filter_multiselect(self):
- self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
- self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
- def test_filter_object(self):
- manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
- self.assertEqual(
- self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(),
- 2
- )
- def test_filter_multiobject(self):
- manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
- self.assertEqual(
- self.filterset({'cf_cf12': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(),
- 2
- )
- self.assertEqual(
- self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(),
- 3
- )
|