test_customfields.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705
  1. import datetime
  2. from decimal import Decimal
  3. from django.core.exceptions import ValidationError
  4. from django.urls import reverse
  5. from rest_framework import status
  6. from core.models import ObjectType
  7. from dcim.filtersets import SiteFilterSet
  8. from dcim.forms import SiteImportForm
  9. from dcim.models import Manufacturer, Rack, Site
  10. from extras.choices import *
  11. from extras.models import CustomField, CustomFieldChoiceSet
  12. from ipam.models import VLAN
  13. from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
  14. from utilities.testing import APITestCase, TestCase
  15. from virtualization.models import VirtualMachine
  16. class CustomFieldTest(TestCase):
  17. @classmethod
  18. def setUpTestData(cls):
  19. Site.objects.bulk_create([
  20. Site(name='Site A', slug='site-a'),
  21. Site(name='Site B', slug='site-b'),
  22. Site(name='Site C', slug='site-c'),
  23. ])
  24. cls.object_type = ObjectType.objects.get_for_model(Site)
  25. def test_invalid_name(self):
  26. """
  27. Try creating a CustomField with an invalid name.
  28. """
  29. with self.assertRaises(ValidationError):
  30. # Invalid character
  31. CustomField(name='?', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
  32. with self.assertRaises(ValidationError):
  33. # Double underscores not permitted
  34. CustomField(name='foo__bar', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
  35. def test_text_field(self):
  36. value = 'Foobar!'
  37. # Create a custom field & check that initial value is null
  38. cf = CustomField.objects.create(
  39. name='text_field',
  40. type=CustomFieldTypeChoices.TYPE_TEXT,
  41. required=False
  42. )
  43. cf.object_types.set([self.object_type])
  44. instance = Site.objects.first()
  45. self.assertIsNone(instance.custom_field_data[cf.name])
  46. # Assign a value and check that it is saved
  47. instance.custom_field_data[cf.name] = value
  48. instance.save()
  49. instance.refresh_from_db()
  50. self.assertEqual(instance.custom_field_data[cf.name], value)
  51. # Delete the stored value and check that it is now null
  52. instance.custom_field_data.pop(cf.name)
  53. instance.save()
  54. instance.refresh_from_db()
  55. self.assertIsNone(instance.custom_field_data.get(cf.name))
  56. def test_longtext_field(self):
  57. value = 'A' * 256
  58. # Create a custom field & check that initial value is null
  59. cf = CustomField.objects.create(
  60. name='longtext_field',
  61. type=CustomFieldTypeChoices.TYPE_LONGTEXT,
  62. required=False
  63. )
  64. cf.object_types.set([self.object_type])
  65. instance = Site.objects.first()
  66. self.assertIsNone(instance.custom_field_data[cf.name])
  67. # Assign a value and check that it is saved
  68. instance.custom_field_data[cf.name] = value
  69. instance.save()
  70. instance.refresh_from_db()
  71. self.assertEqual(instance.custom_field_data[cf.name], value)
  72. # Delete the stored value and check that it is now null
  73. instance.custom_field_data.pop(cf.name)
  74. instance.save()
  75. instance.refresh_from_db()
  76. self.assertIsNone(instance.custom_field_data.get(cf.name))
  77. def test_integer_field(self):
  78. # Create a custom field & check that initial value is null
  79. cf = CustomField.objects.create(
  80. name='integer_field',
  81. type=CustomFieldTypeChoices.TYPE_INTEGER,
  82. required=False
  83. )
  84. cf.object_types.set([self.object_type])
  85. instance = Site.objects.first()
  86. self.assertIsNone(instance.custom_field_data[cf.name])
  87. for value in (123456, 0, -123456):
  88. # Assign a value and check that it is saved
  89. instance.custom_field_data[cf.name] = value
  90. instance.save()
  91. instance.refresh_from_db()
  92. self.assertEqual(instance.custom_field_data[cf.name], value)
  93. # Delete the stored value and check that it is now null
  94. instance.custom_field_data.pop(cf.name)
  95. instance.save()
  96. instance.refresh_from_db()
  97. self.assertIsNone(instance.custom_field_data.get(cf.name))
  98. def test_decimal_field(self):
  99. # Create a custom field & check that initial value is null
  100. cf = CustomField.objects.create(
  101. name='decimal_field',
  102. type=CustomFieldTypeChoices.TYPE_DECIMAL,
  103. required=False
  104. )
  105. cf.object_types.set([self.object_type])
  106. instance = Site.objects.first()
  107. self.assertIsNone(instance.custom_field_data[cf.name])
  108. for value in (123456.54, 0, -123456.78):
  109. # Assign a value and check that it is saved
  110. instance.custom_field_data[cf.name] = value
  111. instance.save()
  112. instance.refresh_from_db()
  113. self.assertEqual(instance.custom_field_data[cf.name], value)
  114. # Delete the stored value and check that it is now null
  115. instance.custom_field_data.pop(cf.name)
  116. instance.save()
  117. instance.refresh_from_db()
  118. self.assertIsNone(instance.custom_field_data.get(cf.name))
  119. def test_boolean_field(self):
  120. # Create a custom field & check that initial value is null
  121. cf = CustomField.objects.create(
  122. name='boolean_field',
  123. type=CustomFieldTypeChoices.TYPE_INTEGER,
  124. required=False
  125. )
  126. cf.object_types.set([self.object_type])
  127. instance = Site.objects.first()
  128. self.assertIsNone(instance.custom_field_data[cf.name])
  129. for value in (True, False):
  130. # Assign a value and check that it is saved
  131. instance.custom_field_data[cf.name] = value
  132. instance.save()
  133. instance.refresh_from_db()
  134. self.assertEqual(instance.custom_field_data[cf.name], value)
  135. # Delete the stored value and check that it is now null
  136. instance.custom_field_data.pop(cf.name)
  137. instance.save()
  138. instance.refresh_from_db()
  139. self.assertIsNone(instance.custom_field_data.get(cf.name))
  140. def test_date_field(self):
  141. value = datetime.date(2016, 6, 23)
  142. # Create a custom field & check that initial value is null
  143. cf = CustomField.objects.create(
  144. name='date_field',
  145. type=CustomFieldTypeChoices.TYPE_DATE,
  146. required=False
  147. )
  148. cf.object_types.set([self.object_type])
  149. instance = Site.objects.first()
  150. self.assertIsNone(instance.custom_field_data[cf.name])
  151. # Assign a value and check that it is saved
  152. instance.custom_field_data[cf.name] = cf.serialize(value)
  153. instance.save()
  154. instance.refresh_from_db()
  155. self.assertEqual(instance.cf[cf.name], value)
  156. # Delete the stored value and check that it is now null
  157. instance.custom_field_data.pop(cf.name)
  158. instance.save()
  159. instance.refresh_from_db()
  160. self.assertIsNone(instance.custom_field_data.get(cf.name))
  161. def test_datetime_field(self):
  162. value = datetime.datetime(2016, 6, 23, 9, 45, 0)
  163. # Create a custom field & check that initial value is null
  164. cf = CustomField.objects.create(
  165. name='date_field',
  166. type=CustomFieldTypeChoices.TYPE_DATETIME,
  167. required=False
  168. )
  169. cf.object_types.set([self.object_type])
  170. instance = Site.objects.first()
  171. self.assertIsNone(instance.custom_field_data[cf.name])
  172. # Assign a value and check that it is saved
  173. instance.custom_field_data[cf.name] = cf.serialize(value)
  174. instance.save()
  175. instance.refresh_from_db()
  176. self.assertEqual(instance.cf[cf.name], value)
  177. # Delete the stored value and check that it is now null
  178. instance.custom_field_data.pop(cf.name)
  179. instance.save()
  180. instance.refresh_from_db()
  181. self.assertIsNone(instance.custom_field_data.get(cf.name))
  182. def test_url_field(self):
  183. value = 'http://example.com/'
  184. # Create a custom field & check that initial value is null
  185. cf = CustomField.objects.create(
  186. name='url_field',
  187. type=CustomFieldTypeChoices.TYPE_URL,
  188. required=False
  189. )
  190. cf.object_types.set([self.object_type])
  191. instance = Site.objects.first()
  192. self.assertIsNone(instance.custom_field_data[cf.name])
  193. # Assign a value and check that it is saved
  194. instance.custom_field_data[cf.name] = value
  195. instance.save()
  196. instance.refresh_from_db()
  197. self.assertEqual(instance.custom_field_data[cf.name], value)
  198. # Delete the stored value and check that it is now null
  199. instance.custom_field_data.pop(cf.name)
  200. instance.save()
  201. instance.refresh_from_db()
  202. self.assertIsNone(instance.custom_field_data.get(cf.name))
  203. def test_json_field(self):
  204. value = '{"foo": 1, "bar": 2}'
  205. # Create a custom field & check that initial value is null
  206. cf = CustomField.objects.create(
  207. name='json_field',
  208. type=CustomFieldTypeChoices.TYPE_JSON,
  209. required=False
  210. )
  211. cf.object_types.set([self.object_type])
  212. instance = Site.objects.first()
  213. self.assertIsNone(instance.custom_field_data[cf.name])
  214. # Assign a value and check that it is saved
  215. instance.custom_field_data[cf.name] = value
  216. instance.save()
  217. instance.refresh_from_db()
  218. self.assertEqual(instance.custom_field_data[cf.name], value)
  219. # Delete the stored value and check that it is now null
  220. instance.custom_field_data.pop(cf.name)
  221. instance.save()
  222. instance.refresh_from_db()
  223. self.assertIsNone(instance.custom_field_data.get(cf.name))
  224. def test_select_field(self):
  225. CHOICES = (
  226. ('a', 'Option A'),
  227. ('b', 'Option B'),
  228. ('c', 'Option C'),
  229. )
  230. value = 'a'
  231. # Create a set of custom field choices
  232. choice_set = CustomFieldChoiceSet.objects.create(
  233. name='Custom Field Choice Set 1',
  234. extra_choices=CHOICES
  235. )
  236. # Create a custom field & check that initial value is null
  237. cf = CustomField.objects.create(
  238. name='select_field',
  239. type=CustomFieldTypeChoices.TYPE_SELECT,
  240. required=False,
  241. choice_set=choice_set
  242. )
  243. cf.object_types.set([self.object_type])
  244. instance = Site.objects.first()
  245. self.assertIsNone(instance.custom_field_data[cf.name])
  246. # Assign a value and check that it is saved
  247. instance.custom_field_data[cf.name] = value
  248. instance.save()
  249. instance.refresh_from_db()
  250. self.assertEqual(instance.custom_field_data[cf.name], value)
  251. # Delete the stored value and check that it is now null
  252. instance.custom_field_data.pop(cf.name)
  253. instance.save()
  254. instance.refresh_from_db()
  255. self.assertIsNone(instance.custom_field_data.get(cf.name))
  256. def test_multiselect_field(self):
  257. CHOICES = (
  258. ('a', 'Option A'),
  259. ('b', 'Option B'),
  260. ('c', 'Option C'),
  261. )
  262. value = ['a', 'b']
  263. # Create a set of custom field choices
  264. choice_set = CustomFieldChoiceSet.objects.create(
  265. name='Custom Field Choice Set 1',
  266. extra_choices=CHOICES
  267. )
  268. # Create a custom field & check that initial value is null
  269. cf = CustomField.objects.create(
  270. name='multiselect_field',
  271. type=CustomFieldTypeChoices.TYPE_MULTISELECT,
  272. required=False,
  273. choice_set=choice_set
  274. )
  275. cf.object_types.set([self.object_type])
  276. instance = Site.objects.first()
  277. self.assertIsNone(instance.custom_field_data[cf.name])
  278. # Assign a value and check that it is saved
  279. instance.custom_field_data[cf.name] = value
  280. instance.save()
  281. instance.refresh_from_db()
  282. self.assertEqual(instance.custom_field_data[cf.name], value)
  283. # Delete the stored value and check that it is now null
  284. instance.custom_field_data.pop(cf.name)
  285. instance.save()
  286. instance.refresh_from_db()
  287. self.assertIsNone(instance.custom_field_data.get(cf.name))
  288. def test_remove_selected_choice(self):
  289. """
  290. Removing a ChoiceSet choice that is referenced by an object should raise
  291. a ValidationError exception.
  292. """
  293. CHOICES = (
  294. ('a', 'Option A'),
  295. ('b', 'Option B'),
  296. ('c', 'Option C'),
  297. ('d', 'Option D'),
  298. )
  299. # Create a set of custom field choices
  300. choice_set = CustomFieldChoiceSet.objects.create(
  301. name='Custom Field Choice Set 1',
  302. extra_choices=CHOICES
  303. )
  304. # Create a select custom field
  305. cf = CustomField.objects.create(
  306. name='select_field',
  307. type=CustomFieldTypeChoices.TYPE_SELECT,
  308. required=False,
  309. choice_set=choice_set
  310. )
  311. cf.object_types.set([self.object_type])
  312. # Create a multi-select custom field
  313. cf_multiselect = CustomField.objects.create(
  314. name='multiselect_field',
  315. type=CustomFieldTypeChoices.TYPE_MULTISELECT,
  316. required=False,
  317. choice_set=choice_set
  318. )
  319. cf_multiselect.object_types.set([self.object_type])
  320. # Assign a choice for both custom fields on an object
  321. instance = Site.objects.first()
  322. instance.custom_field_data[cf.name] = 'a'
  323. instance.custom_field_data[cf_multiselect.name] = ['b', 'c']
  324. instance.save()
  325. # Attempting to delete a selected choice should fail
  326. with self.assertRaises(ValidationError):
  327. choice_set.extra_choices = (
  328. ('b', 'Option B'),
  329. ('c', 'Option C'),
  330. ('d', 'Option D'),
  331. )
  332. choice_set.full_clean()
  333. # Attempting to delete either of the multi-select choices should fail
  334. with self.assertRaises(ValidationError):
  335. choice_set.extra_choices = (
  336. ('a', 'Option A'),
  337. ('b', 'Option B'),
  338. ('d', 'Option D'),
  339. )
  340. choice_set.full_clean()
  341. # Removing a non-selected choice should succeed
  342. choice_set.extra_choices = (
  343. ('a', 'Option A'),
  344. ('b', 'Option B'),
  345. ('c', 'Option C'),
  346. )
  347. choice_set.full_clean()
  348. def test_object_field(self):
  349. value = VLAN.objects.create(name='VLAN 1', vid=1).pk
  350. # Create a custom field & check that initial value is null
  351. cf = CustomField.objects.create(
  352. name='object_field',
  353. type=CustomFieldTypeChoices.TYPE_OBJECT,
  354. related_object_type=ObjectType.objects.get_for_model(VLAN),
  355. required=False
  356. )
  357. cf.object_types.set([self.object_type])
  358. instance = Site.objects.first()
  359. self.assertIsNone(instance.custom_field_data[cf.name])
  360. # Assign a value and check that it is saved
  361. instance.custom_field_data[cf.name] = value
  362. instance.save()
  363. instance.refresh_from_db()
  364. self.assertEqual(instance.custom_field_data[cf.name], value)
  365. # Delete the stored value and check that it is now null
  366. instance.custom_field_data.pop(cf.name)
  367. instance.save()
  368. instance.refresh_from_db()
  369. self.assertIsNone(instance.custom_field_data.get(cf.name))
  370. def test_multiobject_field(self):
  371. vlans = (
  372. VLAN(name='VLAN 1', vid=1),
  373. VLAN(name='VLAN 2', vid=2),
  374. VLAN(name='VLAN 3', vid=3),
  375. )
  376. VLAN.objects.bulk_create(vlans)
  377. value = [vlan.pk for vlan in vlans]
  378. # Create a custom field & check that initial value is null
  379. cf = CustomField.objects.create(
  380. name='object_field',
  381. type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
  382. related_object_type=ObjectType.objects.get_for_model(VLAN),
  383. required=False
  384. )
  385. cf.object_types.set([self.object_type])
  386. instance = Site.objects.first()
  387. self.assertIsNone(instance.custom_field_data[cf.name])
  388. # Assign a value and check that it is saved
  389. instance.custom_field_data[cf.name] = value
  390. instance.save()
  391. instance.refresh_from_db()
  392. self.assertEqual(instance.custom_field_data[cf.name], value)
  393. # Delete the stored value and check that it is now null
  394. instance.custom_field_data.pop(cf.name)
  395. instance.save()
  396. instance.refresh_from_db()
  397. self.assertIsNone(instance.custom_field_data.get(cf.name))
  398. def test_rename_customfield(self):
  399. obj_type = ObjectType.objects.get_for_model(Site)
  400. FIELD_DATA = 'abc'
  401. # Create a custom field
  402. cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
  403. cf.save()
  404. cf.object_types.set([obj_type])
  405. # Assign custom field data to an object
  406. site = Site.objects.create(
  407. name='Site 1',
  408. slug='site-1',
  409. custom_field_data={'field1': FIELD_DATA}
  410. )
  411. site.refresh_from_db()
  412. self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
  413. # Rename the custom field
  414. cf.name = 'field2'
  415. cf.save()
  416. # Check that custom field data on the object has been updated
  417. site.refresh_from_db()
  418. self.assertNotIn('field1', site.custom_field_data)
  419. self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
  420. def test_default_value_validation(self):
  421. choiceset = CustomFieldChoiceSet.objects.create(
  422. name="Test Choice Set",
  423. extra_choices=(
  424. ('choice1', 'Choice 1'),
  425. ('choice2', 'Choice 2'),
  426. )
  427. )
  428. site = Site.objects.create(name='Site 1', slug='site-1')
  429. object_type = ObjectType.objects.get_for_model(Site)
  430. # Text
  431. CustomField(name='test', type='text', required=True, default="Default text").full_clean()
  432. # Integer
  433. CustomField(name='test', type='integer', required=True, default=1).full_clean()
  434. with self.assertRaises(ValidationError):
  435. CustomField(name='test', type='integer', required=True, default='xxx').full_clean()
  436. # Boolean
  437. CustomField(name='test', type='boolean', required=True, default=True).full_clean()
  438. with self.assertRaises(ValidationError):
  439. CustomField(name='test', type='boolean', required=True, default='xxx').full_clean()
  440. # Date
  441. CustomField(name='test', type='date', required=True, default="2023-02-25").full_clean()
  442. with self.assertRaises(ValidationError):
  443. CustomField(name='test', type='date', required=True, default='xxx').full_clean()
  444. # Datetime
  445. CustomField(name='test', type='datetime', required=True, default="2023-02-25 02:02:02").full_clean()
  446. with self.assertRaises(ValidationError):
  447. CustomField(name='test', type='datetime', required=True, default='xxx').full_clean()
  448. # URL
  449. CustomField(name='test', type='url', required=True, default="https://www.netbox.dev").full_clean()
  450. # JSON
  451. CustomField(name='test', type='json', required=True, default='{"test": "object"}').full_clean()
  452. # Selection
  453. CustomField(name='test', type='select', required=True, choice_set=choiceset, default='choice1').full_clean()
  454. with self.assertRaises(ValidationError):
  455. CustomField(name='test', type='select', required=True, choice_set=choiceset, default='xxx').full_clean()
  456. # Multi-select
  457. CustomField(
  458. name='test',
  459. type='multiselect',
  460. required=True,
  461. choice_set=choiceset,
  462. default=['choice1'] # Single default choice
  463. ).full_clean()
  464. CustomField(
  465. name='test',
  466. type='multiselect',
  467. required=True,
  468. choice_set=choiceset,
  469. default=['choice1', 'choice2'] # Multiple default choices
  470. ).full_clean()
  471. with self.assertRaises(ValidationError):
  472. CustomField(
  473. name='test',
  474. type='multiselect',
  475. required=True,
  476. choice_set=choiceset,
  477. default=['xxx']
  478. ).full_clean()
  479. # Object
  480. CustomField(
  481. name='test',
  482. type='object',
  483. required=True,
  484. related_object_type=object_type,
  485. default=site.pk
  486. ).full_clean()
  487. with (self.assertRaises(ValidationError)):
  488. CustomField(
  489. name='test',
  490. type='object',
  491. required=True,
  492. related_object_type=object_type,
  493. default="xxx"
  494. ).full_clean()
  495. # Multi-object
  496. CustomField(
  497. name='test',
  498. type='multiobject',
  499. required=True,
  500. related_object_type=object_type,
  501. default=[site.pk]
  502. ).full_clean()
  503. with self.assertRaises(ValidationError):
  504. CustomField(
  505. name='test',
  506. type='multiobject',
  507. required=True,
  508. related_object_type=object_type,
  509. default=["xxx"]
  510. ).full_clean()
  511. class CustomFieldManagerTest(TestCase):
  512. @classmethod
  513. def setUpTestData(cls):
  514. object_type = ObjectType.objects.get_for_model(Site)
  515. custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
  516. custom_field.save()
  517. custom_field.object_types.set([object_type])
  518. def test_get_for_model(self):
  519. self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
  520. self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
  521. class CustomFieldAPITest(APITestCase):
  522. @classmethod
  523. def setUpTestData(cls):
  524. object_type = ObjectType.objects.get_for_model(Site)
  525. # Create some VLANs
  526. vlans = (
  527. VLAN(name='VLAN 1', vid=1),
  528. VLAN(name='VLAN 2', vid=2),
  529. VLAN(name='VLAN 3', vid=3),
  530. VLAN(name='VLAN 4', vid=4),
  531. VLAN(name='VLAN 5', vid=5),
  532. )
  533. VLAN.objects.bulk_create(vlans)
  534. # Create a set of custom field choices
  535. choice_set = CustomFieldChoiceSet.objects.create(
  536. name='Custom Field Choice Set 1',
  537. extra_choices=(('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz'))
  538. )
  539. custom_fields = (
  540. CustomField(
  541. type=CustomFieldTypeChoices.TYPE_TEXT,
  542. name='text_field',
  543. default='foo'
  544. ),
  545. CustomField(
  546. type=CustomFieldTypeChoices.TYPE_LONGTEXT,
  547. name='longtext_field',
  548. default='ABC'
  549. ),
  550. CustomField(
  551. type=CustomFieldTypeChoices.TYPE_INTEGER,
  552. name='integer_field',
  553. default=123
  554. ),
  555. CustomField(
  556. type=CustomFieldTypeChoices.TYPE_DECIMAL,
  557. name='decimal_field',
  558. default=123.45
  559. ),
  560. CustomField(
  561. type=CustomFieldTypeChoices.TYPE_BOOLEAN,
  562. name='boolean_field',
  563. default=False
  564. ),
  565. CustomField(
  566. type=CustomFieldTypeChoices.TYPE_DATE,
  567. name='date_field',
  568. default='2020-01-01'
  569. ),
  570. CustomField(
  571. type=CustomFieldTypeChoices.TYPE_DATETIME,
  572. name='datetime_field',
  573. default='2020-01-01T01:23:45'
  574. ),
  575. CustomField(
  576. type=CustomFieldTypeChoices.TYPE_URL,
  577. name='url_field',
  578. default='http://example.com/1'
  579. ),
  580. CustomField(
  581. type=CustomFieldTypeChoices.TYPE_JSON,
  582. name='json_field',
  583. default='{"x": "y"}'
  584. ),
  585. CustomField(
  586. type=CustomFieldTypeChoices.TYPE_SELECT,
  587. name='select_field',
  588. default='foo',
  589. choice_set=choice_set
  590. ),
  591. CustomField(
  592. type=CustomFieldTypeChoices.TYPE_MULTISELECT,
  593. name='multiselect_field',
  594. default=['foo'],
  595. choice_set=choice_set,
  596. ),
  597. CustomField(
  598. type=CustomFieldTypeChoices.TYPE_OBJECT,
  599. name='object_field',
  600. related_object_type=ObjectType.objects.get_for_model(VLAN),
  601. default=vlans[0].pk,
  602. ),
  603. CustomField(
  604. type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
  605. name='multiobject_field',
  606. related_object_type=ObjectType.objects.get_for_model(VLAN),
  607. default=[vlans[0].pk, vlans[1].pk],
  608. ),
  609. )
  610. for cf in custom_fields:
  611. cf.save()
  612. cf.object_types.set([object_type])
  613. # Create some sites *after* creating the custom fields. This ensures that
  614. # default values are not set for the assigned objects.
  615. sites = (
  616. Site(name='Site 1', slug='site-1'),
  617. Site(name='Site 2', slug='site-2'),
  618. )
  619. Site.objects.bulk_create(sites)
  620. # Assign custom field values for site 2
  621. sites[1].custom_field_data = {
  622. custom_fields[0].name: 'bar',
  623. custom_fields[1].name: 'DEF',
  624. custom_fields[2].name: 456,
  625. custom_fields[3].name: Decimal('456.78'),
  626. custom_fields[4].name: True,
  627. custom_fields[5].name: '2020-01-02',
  628. custom_fields[6].name: '2020-01-02 12:00:00',
  629. custom_fields[7].name: 'http://example.com/2',
  630. custom_fields[8].name: '{"foo": 1, "bar": 2}',
  631. custom_fields[9].name: 'bar',
  632. custom_fields[10].name: ['bar', 'baz'],
  633. custom_fields[11].name: vlans[1].pk,
  634. custom_fields[12].name: [vlans[2].pk, vlans[3].pk],
  635. }
  636. sites[1].save()
  637. def test_get_custom_fields(self):
  638. TYPES = {
  639. CustomFieldTypeChoices.TYPE_TEXT: 'string',
  640. CustomFieldTypeChoices.TYPE_LONGTEXT: 'string',
  641. CustomFieldTypeChoices.TYPE_INTEGER: 'integer',
  642. CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal',
  643. CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean',
  644. CustomFieldTypeChoices.TYPE_DATE: 'string',
  645. CustomFieldTypeChoices.TYPE_DATETIME: 'string',
  646. CustomFieldTypeChoices.TYPE_URL: 'string',
  647. CustomFieldTypeChoices.TYPE_JSON: 'object',
  648. CustomFieldTypeChoices.TYPE_SELECT: 'string',
  649. CustomFieldTypeChoices.TYPE_MULTISELECT: 'array',
  650. CustomFieldTypeChoices.TYPE_OBJECT: 'object',
  651. CustomFieldTypeChoices.TYPE_MULTIOBJECT: 'array',
  652. }
  653. self.add_permissions('extras.view_customfield')
  654. url = reverse('extras-api:customfield-list')
  655. response = self.client.get(url, **self.header)
  656. self.assertEqual(response.data['count'], len(TYPES))
  657. # Validate data types
  658. for customfield in response.data['results']:
  659. cf_type = customfield['type']['value']
  660. self.assertEqual(customfield['data_type'], TYPES[cf_type])
  661. def test_get_single_object_without_custom_field_data(self):
  662. """
  663. Validate that custom fields are present on an object even if it has no values defined.
  664. """
  665. site1 = Site.objects.get(name='Site 1')
  666. url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk})
  667. self.add_permissions('dcim.view_site')
  668. response = self.client.get(url, **self.header)
  669. self.assertEqual(response.data['name'], site1.name)
  670. self.assertEqual(response.data['custom_fields'], {
  671. 'text_field': None,
  672. 'longtext_field': None,
  673. 'integer_field': None,
  674. 'decimal_field': None,
  675. 'boolean_field': None,
  676. 'date_field': None,
  677. 'datetime_field': None,
  678. 'url_field': None,
  679. 'json_field': None,
  680. 'select_field': None,
  681. 'multiselect_field': None,
  682. 'object_field': None,
  683. 'multiobject_field': None,
  684. })
  685. def test_get_single_object_with_custom_field_data(self):
  686. """
  687. Validate that custom fields are present and correctly set for an object with values defined.
  688. """
  689. site2 = Site.objects.get(name='Site 2')
  690. site2_cfvs = site2.cf
  691. url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
  692. self.add_permissions('dcim.view_site')
  693. response = self.client.get(url, **self.header)
  694. self.assertEqual(response.data['name'], site2.name)
  695. self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
  696. self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
  697. self.assertEqual(response.data['custom_fields']['integer_field'], site2_cfvs['integer_field'])
  698. self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field'])
  699. self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
  700. self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
  701. self.assertEqual(response.data['custom_fields']['datetime_field'], site2_cfvs['datetime_field'])
  702. self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
  703. self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field'])
  704. self.assertEqual(response.data['custom_fields']['select_field'], site2_cfvs['select_field'])
  705. self.assertEqual(response.data['custom_fields']['multiselect_field'], site2_cfvs['multiselect_field'])
  706. self.assertEqual(response.data['custom_fields']['object_field']['id'], site2_cfvs['object_field'].pk)
  707. self.assertEqual(
  708. [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
  709. [obj.pk for obj in site2_cfvs['multiobject_field']]
  710. )
  711. def test_create_single_object_with_defaults(self):
  712. """
  713. Create a new site with no specified custom field values and check that it received the default values.
  714. """
  715. cf_defaults = {
  716. cf.name: cf.default for cf in CustomField.objects.all()
  717. }
  718. data = {
  719. 'name': 'Site 3',
  720. 'slug': 'site-3',
  721. }
  722. url = reverse('dcim-api:site-list')
  723. self.add_permissions('dcim.add_site')
  724. response = self.client.post(url, data, format='json', **self.header)
  725. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  726. # Validate response data
  727. response_cf = response.data['custom_fields']
  728. self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
  729. self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
  730. self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
  731. self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
  732. self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
  733. self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field'])
  734. self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field'])
  735. self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
  736. self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
  737. self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
  738. self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
  739. self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
  740. self.assertEqual(
  741. [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
  742. cf_defaults['multiobject_field']
  743. )
  744. # Validate database data
  745. site = Site.objects.get(pk=response.data['id'])
  746. self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
  747. self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
  748. self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
  749. self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
  750. self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
  751. self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field'])
  752. self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field'])
  753. self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
  754. self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
  755. self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
  756. self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
  757. self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
  758. self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
  759. def test_create_single_object_with_values(self):
  760. """
  761. Create a single new site with a value for each type of custom field.
  762. """
  763. data = {
  764. 'name': 'Site 3',
  765. 'slug': 'site-3',
  766. 'custom_fields': {
  767. 'text_field': 'bar',
  768. 'longtext_field': 'blah blah blah',
  769. 'integer_field': 456,
  770. 'decimal_field': 456.78,
  771. 'boolean_field': True,
  772. 'date_field': datetime.date(2020, 1, 2),
  773. 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
  774. 'url_field': 'http://example.com/2',
  775. 'json_field': '{"foo": 1, "bar": 2}',
  776. 'select_field': 'bar',
  777. 'multiselect_field': ['bar', 'baz'],
  778. 'object_field': VLAN.objects.get(vid=2).pk,
  779. 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
  780. },
  781. }
  782. url = reverse('dcim-api:site-list')
  783. self.add_permissions('dcim.add_site')
  784. response = self.client.post(url, data, format='json', **self.header)
  785. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  786. # Validate response data
  787. response_cf = response.data['custom_fields']
  788. data_cf = data['custom_fields']
  789. self.assertEqual(response_cf['text_field'], data_cf['text_field'])
  790. self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
  791. self.assertEqual(response_cf['integer_field'], data_cf['integer_field'])
  792. self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field'])
  793. self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
  794. self.assertEqual(response_cf['date_field'], data_cf['date_field'])
  795. self.assertEqual(response_cf['datetime_field'], data_cf['datetime_field'])
  796. self.assertEqual(response_cf['url_field'], data_cf['url_field'])
  797. self.assertEqual(response_cf['json_field'], data_cf['json_field'])
  798. self.assertEqual(response_cf['select_field'], data_cf['select_field'])
  799. self.assertEqual(response_cf['multiselect_field'], data_cf['multiselect_field'])
  800. self.assertEqual(response_cf['object_field']['id'], data_cf['object_field'])
  801. self.assertEqual(
  802. [obj['id'] for obj in response_cf['multiobject_field']],
  803. data_cf['multiobject_field']
  804. )
  805. # Validate database data
  806. site = Site.objects.get(pk=response.data['id'])
  807. self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
  808. self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
  809. self.assertEqual(site.custom_field_data['integer_field'], data_cf['integer_field'])
  810. self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field'])
  811. self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
  812. self.assertEqual(site.cf['date_field'], data_cf['date_field'])
  813. self.assertEqual(site.cf['datetime_field'], data_cf['datetime_field'])
  814. self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
  815. self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field'])
  816. self.assertEqual(site.custom_field_data['select_field'], data_cf['select_field'])
  817. self.assertEqual(site.custom_field_data['multiselect_field'], data_cf['multiselect_field'])
  818. self.assertEqual(site.custom_field_data['object_field'], data_cf['object_field'])
  819. self.assertEqual(site.custom_field_data['multiobject_field'], data_cf['multiobject_field'])
  820. def test_create_multiple_objects_with_defaults(self):
  821. """
  822. Create three new sites with no specified custom field values and check that each received
  823. the default custom field values.
  824. """
  825. cf_defaults = {
  826. cf.name: cf.default for cf in CustomField.objects.all()
  827. }
  828. data = (
  829. {
  830. 'name': 'Site 3',
  831. 'slug': 'site-3',
  832. },
  833. {
  834. 'name': 'Site 4',
  835. 'slug': 'site-4',
  836. },
  837. {
  838. 'name': 'Site 5',
  839. 'slug': 'site-5',
  840. },
  841. )
  842. url = reverse('dcim-api:site-list')
  843. self.add_permissions('dcim.add_site')
  844. response = self.client.post(url, data, format='json', **self.header)
  845. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  846. self.assertEqual(len(response.data), len(data))
  847. for i, obj in enumerate(data):
  848. # Validate response data
  849. response_cf = response.data[i]['custom_fields']
  850. self.assertEqual(response_cf['text_field'], cf_defaults['text_field'])
  851. self.assertEqual(response_cf['longtext_field'], cf_defaults['longtext_field'])
  852. self.assertEqual(response_cf['integer_field'], cf_defaults['integer_field'])
  853. self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field'])
  854. self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field'])
  855. self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field'])
  856. self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field'])
  857. self.assertEqual(response_cf['url_field'], cf_defaults['url_field'])
  858. self.assertEqual(response_cf['json_field'], cf_defaults['json_field'])
  859. self.assertEqual(response_cf['select_field'], cf_defaults['select_field'])
  860. self.assertEqual(response_cf['multiselect_field'], cf_defaults['multiselect_field'])
  861. self.assertEqual(response_cf['object_field']['id'], cf_defaults['object_field'])
  862. self.assertEqual(
  863. [obj['id'] for obj in response_cf['multiobject_field']],
  864. cf_defaults['multiobject_field']
  865. )
  866. # Validate database data
  867. site = Site.objects.get(pk=response.data[i]['id'])
  868. self.assertEqual(site.custom_field_data['text_field'], cf_defaults['text_field'])
  869. self.assertEqual(site.custom_field_data['longtext_field'], cf_defaults['longtext_field'])
  870. self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field'])
  871. self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field'])
  872. self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field'])
  873. self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field'])
  874. self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field'])
  875. self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field'])
  876. self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field'])
  877. self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field'])
  878. self.assertEqual(site.custom_field_data['multiselect_field'], cf_defaults['multiselect_field'])
  879. self.assertEqual(site.custom_field_data['object_field'], cf_defaults['object_field'])
  880. self.assertEqual(site.custom_field_data['multiobject_field'], cf_defaults['multiobject_field'])
  881. def test_create_multiple_objects_with_values(self):
  882. """
  883. Create a three new sites, each with custom fields defined.
  884. """
  885. custom_field_data = {
  886. 'text_field': 'bar',
  887. 'longtext_field': 'abcdefghij',
  888. 'integer_field': 456,
  889. 'decimal_field': 456.78,
  890. 'boolean_field': True,
  891. 'date_field': datetime.date(2020, 1, 2),
  892. 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
  893. 'url_field': 'http://example.com/2',
  894. 'json_field': '{"foo": 1, "bar": 2}',
  895. 'select_field': 'bar',
  896. 'multiselect_field': ['bar', 'baz'],
  897. 'object_field': VLAN.objects.get(vid=2).pk,
  898. 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
  899. }
  900. data = (
  901. {
  902. 'name': 'Site 3',
  903. 'slug': 'site-3',
  904. 'custom_fields': custom_field_data,
  905. },
  906. {
  907. 'name': 'Site 4',
  908. 'slug': 'site-4',
  909. 'custom_fields': custom_field_data,
  910. },
  911. {
  912. 'name': 'Site 5',
  913. 'slug': 'site-5',
  914. 'custom_fields': custom_field_data,
  915. },
  916. )
  917. url = reverse('dcim-api:site-list')
  918. self.add_permissions('dcim.add_site')
  919. response = self.client.post(url, data, format='json', **self.header)
  920. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  921. self.assertEqual(len(response.data), len(data))
  922. for i, obj in enumerate(data):
  923. # Validate response data
  924. response_cf = response.data[i]['custom_fields']
  925. self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
  926. self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
  927. self.assertEqual(response_cf['integer_field'], custom_field_data['integer_field'])
  928. self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field'])
  929. self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
  930. self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
  931. self.assertEqual(response_cf['datetime_field'], custom_field_data['datetime_field'])
  932. self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
  933. self.assertEqual(response_cf['json_field'], custom_field_data['json_field'])
  934. self.assertEqual(response_cf['select_field'], custom_field_data['select_field'])
  935. self.assertEqual(response_cf['multiselect_field'], custom_field_data['multiselect_field'])
  936. self.assertEqual(response_cf['object_field']['id'], custom_field_data['object_field'])
  937. self.assertEqual(
  938. [obj['id'] for obj in response_cf['multiobject_field']],
  939. custom_field_data['multiobject_field']
  940. )
  941. # Validate database data
  942. site = Site.objects.get(pk=response.data[i]['id'])
  943. self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
  944. self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
  945. self.assertEqual(site.custom_field_data['integer_field'], custom_field_data['integer_field'])
  946. self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field'])
  947. self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
  948. self.assertEqual(site.cf['date_field'], custom_field_data['date_field'])
  949. self.assertEqual(site.cf['datetime_field'], custom_field_data['datetime_field'])
  950. self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
  951. self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field'])
  952. self.assertEqual(site.custom_field_data['select_field'], custom_field_data['select_field'])
  953. self.assertEqual(site.custom_field_data['multiselect_field'], custom_field_data['multiselect_field'])
  954. self.assertEqual(site.custom_field_data['object_field'], custom_field_data['object_field'])
  955. self.assertEqual(site.custom_field_data['multiobject_field'], custom_field_data['multiobject_field'])
  956. def test_update_single_object_with_values(self):
  957. """
  958. Update an object with existing custom field values. Ensure that only the updated custom field values are
  959. modified.
  960. """
  961. site2 = Site.objects.get(name='Site 2')
  962. original_cfvs = {**site2.cf}
  963. data = {
  964. 'custom_fields': {
  965. 'text_field': 'ABCD',
  966. 'integer_field': 1234,
  967. },
  968. }
  969. url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
  970. self.add_permissions('dcim.change_site')
  971. response = self.client.patch(url, data, format='json', **self.header)
  972. self.assertHttpStatus(response, status.HTTP_200_OK)
  973. # Validate response data
  974. response_cf = response.data['custom_fields']
  975. self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
  976. self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
  977. self.assertEqual(response_cf['integer_field'], data['custom_fields']['integer_field'])
  978. self.assertEqual(response_cf['decimal_field'], original_cfvs['decimal_field'])
  979. self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
  980. self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
  981. self.assertEqual(response_cf['datetime_field'], original_cfvs['datetime_field'])
  982. self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
  983. self.assertEqual(response_cf['json_field'], original_cfvs['json_field'])
  984. self.assertEqual(response_cf['select_field'], original_cfvs['select_field'])
  985. self.assertEqual(response_cf['multiselect_field'], original_cfvs['multiselect_field'])
  986. self.assertEqual(response_cf['object_field']['id'], original_cfvs['object_field'].pk)
  987. self.assertListEqual(
  988. [obj['id'] for obj in response_cf['multiobject_field']],
  989. [obj.pk for obj in original_cfvs['multiobject_field']]
  990. )
  991. # Validate database data
  992. site2 = Site.objects.get(pk=site2.pk)
  993. self.assertEqual(site2.cf['text_field'], data['custom_fields']['text_field'])
  994. self.assertEqual(site2.cf['longtext_field'], original_cfvs['longtext_field'])
  995. self.assertEqual(site2.cf['integer_field'], data['custom_fields']['integer_field'])
  996. self.assertEqual(site2.cf['decimal_field'], original_cfvs['decimal_field'])
  997. self.assertEqual(site2.cf['boolean_field'], original_cfvs['boolean_field'])
  998. self.assertEqual(site2.cf['date_field'], original_cfvs['date_field'])
  999. self.assertEqual(site2.cf['datetime_field'], original_cfvs['datetime_field'])
  1000. self.assertEqual(site2.cf['url_field'], original_cfvs['url_field'])
  1001. self.assertEqual(site2.cf['json_field'], original_cfvs['json_field'])
  1002. self.assertEqual(site2.cf['select_field'], original_cfvs['select_field'])
  1003. self.assertEqual(site2.cf['multiselect_field'], original_cfvs['multiselect_field'])
  1004. self.assertEqual(site2.cf['object_field'], original_cfvs['object_field'])
  1005. self.assertListEqual(
  1006. list(site2.cf['multiobject_field']),
  1007. list(original_cfvs['multiobject_field'])
  1008. )
  1009. def test_specify_related_object_by_attr(self):
  1010. site1 = Site.objects.get(name='Site 1')
  1011. vlans = VLAN.objects.all()[:3]
  1012. url = reverse('dcim-api:site-detail', kwargs={'pk': site1.pk})
  1013. self.add_permissions('dcim.change_site')
  1014. # Set related objects by PK
  1015. data = {
  1016. 'custom_fields': {
  1017. 'object_field': vlans[0].pk,
  1018. 'multiobject_field': [vlans[1].pk, vlans[2].pk],
  1019. },
  1020. }
  1021. response = self.client.patch(url, data, format='json', **self.header)
  1022. self.assertHttpStatus(response, status.HTTP_200_OK)
  1023. self.assertEqual(
  1024. response.data['custom_fields']['object_field']['id'],
  1025. vlans[0].pk
  1026. )
  1027. self.assertListEqual(
  1028. [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
  1029. [vlans[1].pk, vlans[2].pk]
  1030. )
  1031. # Set related objects by name
  1032. data = {
  1033. 'custom_fields': {
  1034. 'object_field': {
  1035. 'name': vlans[0].name,
  1036. },
  1037. 'multiobject_field': [
  1038. {
  1039. 'name': vlans[1].name
  1040. },
  1041. {
  1042. 'name': vlans[2].name
  1043. },
  1044. ],
  1045. },
  1046. }
  1047. response = self.client.patch(url, data, format='json', **self.header)
  1048. self.assertHttpStatus(response, status.HTTP_200_OK)
  1049. self.assertEqual(
  1050. response.data['custom_fields']['object_field']['id'],
  1051. vlans[0].pk
  1052. )
  1053. self.assertListEqual(
  1054. [obj['id'] for obj in response.data['custom_fields']['multiobject_field']],
  1055. [vlans[1].pk, vlans[2].pk]
  1056. )
  1057. # Clear related objects
  1058. data = {
  1059. 'custom_fields': {
  1060. 'object_field': None,
  1061. 'multiobject_field': [],
  1062. },
  1063. }
  1064. response = self.client.patch(url, data, format='json', **self.header)
  1065. self.assertHttpStatus(response, status.HTTP_200_OK)
  1066. self.assertIsNone(response.data['custom_fields']['object_field'])
  1067. self.assertListEqual(response.data['custom_fields']['multiobject_field'], [])
  1068. def test_minimum_maximum_values_validation(self):
  1069. site2 = Site.objects.get(name='Site 2')
  1070. url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
  1071. self.add_permissions('dcim.change_site')
  1072. cf_integer = CustomField.objects.get(name='integer_field')
  1073. cf_integer.validation_minimum = 10
  1074. cf_integer.validation_maximum = 20
  1075. cf_integer.save()
  1076. data = {'custom_fields': {'integer_field': 9}}
  1077. response = self.client.patch(url, data, format='json', **self.header)
  1078. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1079. data = {'custom_fields': {'integer_field': 21}}
  1080. response = self.client.patch(url, data, format='json', **self.header)
  1081. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1082. data = {'custom_fields': {'integer_field': 15}}
  1083. response = self.client.patch(url, data, format='json', **self.header)
  1084. self.assertHttpStatus(response, status.HTTP_200_OK)
  1085. def test_regex_validation(self):
  1086. site2 = Site.objects.get(name='Site 2')
  1087. url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
  1088. self.add_permissions('dcim.change_site')
  1089. cf_text = CustomField.objects.get(name='text_field')
  1090. cf_text.validation_regex = r'^[A-Z]{3}$' # Three uppercase letters
  1091. cf_text.save()
  1092. data = {'custom_fields': {'text_field': 'ABC123'}}
  1093. response = self.client.patch(url, data, format='json', **self.header)
  1094. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1095. data = {'custom_fields': {'text_field': 'abc'}}
  1096. response = self.client.patch(url, data, format='json', **self.header)
  1097. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1098. data = {'custom_fields': {'text_field': 'ABC'}}
  1099. response = self.client.patch(url, data, format='json', **self.header)
  1100. self.assertHttpStatus(response, status.HTTP_200_OK)
  1101. def test_uniqueness_validation(self):
  1102. # Create a unique custom field
  1103. cf_text = CustomField.objects.get(name='text_field')
  1104. cf_text.unique = True
  1105. cf_text.save()
  1106. # Set a value on site 1
  1107. site1 = Site.objects.get(name='Site 1')
  1108. site1.custom_field_data['text_field'] = 'ABC123'
  1109. site1.save()
  1110. site2 = Site.objects.get(name='Site 2')
  1111. url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
  1112. self.add_permissions('dcim.change_site')
  1113. data = {'custom_fields': {'text_field': 'ABC123'}}
  1114. response = self.client.patch(url, data, format='json', **self.header)
  1115. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  1116. data = {'custom_fields': {'text_field': 'DEF456'}}
  1117. response = self.client.patch(url, data, format='json', **self.header)
  1118. self.assertHttpStatus(response, status.HTTP_200_OK)
  1119. class CustomFieldImportTest(TestCase):
  1120. user_permissions = (
  1121. 'dcim.view_site',
  1122. 'dcim.add_site',
  1123. )
  1124. @classmethod
  1125. def setUpTestData(cls):
  1126. # Create a set of custom field choices
  1127. choice_set = CustomFieldChoiceSet.objects.create(
  1128. name='Custom Field Choice Set 1',
  1129. extra_choices=(
  1130. ('a', 'Option A'),
  1131. ('b', 'Option B'),
  1132. ('c', 'Option C'),
  1133. )
  1134. )
  1135. custom_fields = (
  1136. CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
  1137. CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
  1138. CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
  1139. CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL),
  1140. CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
  1141. CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
  1142. CustomField(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME),
  1143. CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
  1144. CustomField(name='json', type=CustomFieldTypeChoices.TYPE_JSON),
  1145. CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choice_set=choice_set),
  1146. CustomField(name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choice_set=choice_set),
  1147. )
  1148. for cf in custom_fields:
  1149. cf.save()
  1150. cf.object_types.set([ObjectType.objects.get_for_model(Site)])
  1151. def test_import(self):
  1152. """
  1153. Import a Site in CSV format, including a value for each CustomField.
  1154. """
  1155. data = (
  1156. (
  1157. 'name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date',
  1158. 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect',
  1159. ),
  1160. (
  1161. 'Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01',
  1162. '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'a', '"a,b"',
  1163. ),
  1164. (
  1165. 'Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02',
  1166. '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'b', '"b,c"',
  1167. ),
  1168. ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
  1169. )
  1170. csv_data = '\n'.join(','.join(row) for row in data)
  1171. response = self.client.post(reverse('dcim:site_bulk_import'), {
  1172. 'data': csv_data,
  1173. 'format': ImportFormatChoices.CSV,
  1174. 'csv_delimiter': CSVDelimiterChoices.AUTO,
  1175. })
  1176. self.assertEqual(response.status_code, 302)
  1177. self.assertEqual(Site.objects.count(), 3)
  1178. # Validate data for site 1
  1179. site1 = Site.objects.get(name='Site 1')
  1180. self.assertEqual(len(site1.custom_field_data), 11)
  1181. self.assertEqual(site1.custom_field_data['text'], 'ABC')
  1182. self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
  1183. self.assertEqual(site1.custom_field_data['integer'], 123)
  1184. self.assertEqual(site1.custom_field_data['decimal'], 123.45)
  1185. self.assertEqual(site1.custom_field_data['boolean'], True)
  1186. self.assertEqual(site1.cf['date'].isoformat(), '2020-01-01')
  1187. self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00')
  1188. self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
  1189. self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
  1190. self.assertEqual(site1.custom_field_data['select'], 'a')
  1191. self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
  1192. # Validate data for site 2
  1193. site2 = Site.objects.get(name='Site 2')
  1194. self.assertEqual(len(site2.custom_field_data), 11)
  1195. self.assertEqual(site2.custom_field_data['text'], 'DEF')
  1196. self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
  1197. self.assertEqual(site2.custom_field_data['integer'], 456)
  1198. self.assertEqual(site2.custom_field_data['decimal'], 456.78)
  1199. self.assertEqual(site2.custom_field_data['boolean'], False)
  1200. self.assertEqual(site2.cf['date'].isoformat(), '2020-01-02')
  1201. self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00')
  1202. self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
  1203. self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
  1204. self.assertEqual(site2.custom_field_data['select'], 'b')
  1205. self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
  1206. # No custom field data should be set for site 3
  1207. site3 = Site.objects.get(name='Site 3')
  1208. self.assertFalse(any(site3.custom_field_data.values()))
  1209. def test_import_missing_required(self):
  1210. """
  1211. Attempt to import an object missing a required custom field.
  1212. """
  1213. # Set one of our CustomFields to required
  1214. CustomField.objects.filter(name='text').update(required=True)
  1215. form_data = {
  1216. 'name': 'Site 1',
  1217. 'slug': 'site-1',
  1218. }
  1219. form = SiteImportForm(data=form_data)
  1220. self.assertFalse(form.is_valid())
  1221. self.assertIn('cf_text', form.errors)
  1222. def test_import_invalid_choice(self):
  1223. """
  1224. Attempt to import an object with an invalid choice selection.
  1225. """
  1226. form_data = {
  1227. 'name': 'Site 1',
  1228. 'slug': 'site-1',
  1229. 'cf_select': 'Choice X'
  1230. }
  1231. form = SiteImportForm(data=form_data)
  1232. self.assertFalse(form.is_valid())
  1233. self.assertIn('cf_select', form.errors)
  1234. class CustomFieldModelTest(TestCase):
  1235. @classmethod
  1236. def setUpTestData(cls):
  1237. cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
  1238. cf1.save()
  1239. cf1.object_types.set([ObjectType.objects.get_for_model(Site)])
  1240. cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
  1241. cf2.save()
  1242. cf2.object_types.set([ObjectType.objects.get_for_model(Rack)])
  1243. def test_cf_data(self):
  1244. """
  1245. Check that custom field data is present on the instance immediately after being set and after being fetched
  1246. from the database.
  1247. """
  1248. site = Site(name='Test Site', slug='test-site')
  1249. # Check custom field data on new instance
  1250. site.custom_field_data['foo'] = 'abc'
  1251. self.assertEqual(site.cf['foo'], 'abc')
  1252. # Check custom field data from database
  1253. site.save()
  1254. site = Site.objects.get(name='Test Site')
  1255. self.assertEqual(site.cf['foo'], 'abc')
  1256. def test_invalid_data(self):
  1257. """
  1258. Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
  1259. """
  1260. site = Site(name='Test Site', slug='test-site')
  1261. # Set custom field data
  1262. site.custom_field_data['foo'] = 'abc'
  1263. site.custom_field_data['bar'] = 'def'
  1264. with self.assertRaises(ValidationError):
  1265. site.clean()
  1266. del site.custom_field_data['bar']
  1267. site.clean()
  1268. def test_missing_required_field(self):
  1269. """
  1270. Check that a ValidationError is raised if any required custom fields are not present.
  1271. """
  1272. cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
  1273. cf3.save()
  1274. cf3.object_types.set([ObjectType.objects.get_for_model(Site)])
  1275. site = Site(name='Test Site', slug='test-site')
  1276. # Set custom field data with a required field omitted
  1277. site.custom_field_data['foo'] = 'abc'
  1278. with self.assertRaises(ValidationError):
  1279. site.clean()
  1280. site.custom_field_data['baz'] = 'def'
  1281. site.clean()
  1282. class CustomFieldModelFilterTest(TestCase):
  1283. queryset = Site.objects.all()
  1284. filterset = SiteFilterSet
  1285. @classmethod
  1286. def setUpTestData(cls):
  1287. object_type = ObjectType.objects.get_for_model(Site)
  1288. manufacturers = Manufacturer.objects.bulk_create((
  1289. Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
  1290. Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
  1291. Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
  1292. Manufacturer(name='Manufacturer 4', slug='manufacturer-4'),
  1293. ))
  1294. choice_set = CustomFieldChoiceSet.objects.create(
  1295. name='Custom Field Choice Set 1',
  1296. extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
  1297. )
  1298. # Integer filtering
  1299. cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
  1300. cf.save()
  1301. cf.object_types.set([object_type])
  1302. # Decimal filtering
  1303. cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
  1304. cf.save()
  1305. cf.object_types.set([object_type])
  1306. # Boolean filtering
  1307. cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
  1308. cf.save()
  1309. cf.object_types.set([object_type])
  1310. # Exact text filtering
  1311. cf = CustomField(
  1312. name='cf4',
  1313. type=CustomFieldTypeChoices.TYPE_TEXT,
  1314. filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
  1315. )
  1316. cf.save()
  1317. cf.object_types.set([object_type])
  1318. # Loose text filtering
  1319. cf = CustomField(
  1320. name='cf5',
  1321. type=CustomFieldTypeChoices.TYPE_TEXT,
  1322. filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
  1323. )
  1324. cf.save()
  1325. cf.object_types.set([object_type])
  1326. # Date filtering
  1327. cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
  1328. cf.save()
  1329. cf.object_types.set([object_type])
  1330. # Exact URL filtering
  1331. cf = CustomField(
  1332. name='cf7',
  1333. type=CustomFieldTypeChoices.TYPE_URL,
  1334. filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
  1335. )
  1336. cf.save()
  1337. cf.object_types.set([object_type])
  1338. # Loose URL filtering
  1339. cf = CustomField(
  1340. name='cf8',
  1341. type=CustomFieldTypeChoices.TYPE_URL,
  1342. filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
  1343. )
  1344. cf.save()
  1345. cf.object_types.set([object_type])
  1346. # Selection filtering
  1347. cf = CustomField(
  1348. name='cf9',
  1349. type=CustomFieldTypeChoices.TYPE_SELECT,
  1350. choice_set=choice_set
  1351. )
  1352. cf.save()
  1353. cf.object_types.set([object_type])
  1354. # Multiselect filtering
  1355. cf = CustomField(
  1356. name='cf10',
  1357. type=CustomFieldTypeChoices.TYPE_MULTISELECT,
  1358. choice_set=choice_set
  1359. )
  1360. cf.save()
  1361. cf.object_types.set([object_type])
  1362. # Object filtering
  1363. cf = CustomField(
  1364. name='cf11',
  1365. type=CustomFieldTypeChoices.TYPE_OBJECT,
  1366. related_object_type=ObjectType.objects.get_for_model(Manufacturer)
  1367. )
  1368. cf.save()
  1369. cf.object_types.set([object_type])
  1370. # Multi-object filtering
  1371. cf = CustomField(
  1372. name='cf12',
  1373. type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
  1374. related_object_type=ObjectType.objects.get_for_model(Manufacturer)
  1375. )
  1376. cf.save()
  1377. cf.object_types.set([object_type])
  1378. Site.objects.bulk_create([
  1379. Site(name='Site 1', slug='site-1', custom_field_data={
  1380. 'cf1': 100,
  1381. 'cf2': 100.1,
  1382. 'cf3': True,
  1383. 'cf4': 'foo',
  1384. 'cf5': 'foo',
  1385. 'cf6': '2016-06-26',
  1386. 'cf7': 'http://a.example.com',
  1387. 'cf8': 'http://a.example.com',
  1388. 'cf9': 'A',
  1389. 'cf10': ['A', 'B'],
  1390. 'cf11': manufacturers[0].pk,
  1391. 'cf12': [manufacturers[0].pk, manufacturers[3].pk],
  1392. }),
  1393. Site(name='Site 2', slug='site-2', custom_field_data={
  1394. 'cf1': 200,
  1395. 'cf2': 200.2,
  1396. 'cf3': True,
  1397. 'cf4': 'foobar',
  1398. 'cf5': 'foobar',
  1399. 'cf6': '2016-06-27',
  1400. 'cf7': 'http://b.example.com',
  1401. 'cf8': 'http://b.example.com',
  1402. 'cf9': 'B',
  1403. 'cf10': ['B', 'C'],
  1404. 'cf11': manufacturers[1].pk,
  1405. 'cf12': [manufacturers[1].pk, manufacturers[3].pk],
  1406. }),
  1407. Site(name='Site 3', slug='site-3', custom_field_data={
  1408. 'cf1': 300,
  1409. 'cf2': 300.3,
  1410. 'cf3': False,
  1411. 'cf4': 'bar',
  1412. 'cf5': 'bar',
  1413. 'cf6': '2016-06-28',
  1414. 'cf7': 'http://c.example.com',
  1415. 'cf8': 'http://c.example.com',
  1416. 'cf9': 'C',
  1417. 'cf10': None,
  1418. 'cf11': manufacturers[2].pk,
  1419. 'cf12': [manufacturers[2].pk, manufacturers[3].pk],
  1420. }),
  1421. ])
  1422. def test_filter_integer(self):
  1423. self.assertEqual(self.filterset({'cf_cf1': [100, 200]}, self.queryset).qs.count(), 2)
  1424. self.assertEqual(self.filterset({'cf_cf1__n': [200]}, self.queryset).qs.count(), 2)
  1425. self.assertEqual(self.filterset({'cf_cf1__gt': [200]}, self.queryset).qs.count(), 1)
  1426. self.assertEqual(self.filterset({'cf_cf1__gte': [200]}, self.queryset).qs.count(), 2)
  1427. self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
  1428. self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
  1429. def test_filter_decimal(self):
  1430. self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2)
  1431. self.assertEqual(self.filterset({'cf_cf2__n': [200.2]}, self.queryset).qs.count(), 2)
  1432. self.assertEqual(self.filterset({'cf_cf2__gt': [200.2]}, self.queryset).qs.count(), 1)
  1433. self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2)
  1434. self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1)
  1435. self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2)
  1436. def test_filter_boolean(self):
  1437. self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2)
  1438. self.assertEqual(self.filterset({'cf_cf3': False}, self.queryset).qs.count(), 1)
  1439. def test_filter_text_strict(self):
  1440. self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 1)
  1441. self.assertEqual(self.filterset({'cf_cf4__n': ['foo']}, self.queryset).qs.count(), 2)
  1442. self.assertEqual(self.filterset({'cf_cf4__ic': ['foo']}, self.queryset).qs.count(), 2)
  1443. self.assertEqual(self.filterset({'cf_cf4__nic': ['foo']}, self.queryset).qs.count(), 1)
  1444. self.assertEqual(self.filterset({'cf_cf4__isw': ['foo']}, self.queryset).qs.count(), 2)
  1445. self.assertEqual(self.filterset({'cf_cf4__nisw': ['foo']}, self.queryset).qs.count(), 1)
  1446. self.assertEqual(self.filterset({'cf_cf4__iew': ['bar']}, self.queryset).qs.count(), 2)
  1447. self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1)
  1448. self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1)
  1449. self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2)
  1450. def test_filter_text_loose(self):
  1451. self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2)
  1452. def test_filter_date(self):
  1453. self.assertEqual(self.filterset({'cf_cf6': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
  1454. self.assertEqual(self.filterset({'cf_cf6__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
  1455. self.assertEqual(self.filterset({'cf_cf6__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
  1456. self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
  1457. self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
  1458. self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
  1459. def test_filter_url_strict(self):
  1460. self.assertEqual(
  1461. self.filterset({'cf_cf7': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(),
  1462. 2
  1463. )
  1464. self.assertEqual(self.filterset({'cf_cf7__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
  1465. self.assertEqual(self.filterset({'cf_cf7__ic': ['b']}, self.queryset).qs.count(), 1)
  1466. self.assertEqual(self.filterset({'cf_cf7__nic': ['b']}, self.queryset).qs.count(), 2)
  1467. self.assertEqual(self.filterset({'cf_cf7__isw': ['http://']}, self.queryset).qs.count(), 3)
  1468. self.assertEqual(self.filterset({'cf_cf7__nisw': ['http://']}, self.queryset).qs.count(), 0)
  1469. self.assertEqual(self.filterset({'cf_cf7__iew': ['.com']}, self.queryset).qs.count(), 3)
  1470. self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0)
  1471. self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
  1472. self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
  1473. def test_filter_url_loose(self):
  1474. self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3)
  1475. def test_filter_select(self):
  1476. self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
  1477. def test_filter_multiselect(self):
  1478. self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
  1479. self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
  1480. self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
  1481. def test_filter_object(self):
  1482. manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
  1483. self.assertEqual(
  1484. self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(),
  1485. 2
  1486. )
  1487. def test_filter_multiobject(self):
  1488. manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
  1489. self.assertEqual(
  1490. self.filterset({'cf_cf12': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(),
  1491. 2
  1492. )
  1493. self.assertEqual(
  1494. self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(),
  1495. 3
  1496. )