test_customfields.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. from django.contrib.contenttypes.models import ContentType
  2. from django.core.exceptions import ValidationError
  3. from django.urls import reverse
  4. from rest_framework import status
  5. from dcim.filtersets import SiteFilterSet
  6. from dcim.forms import SiteCSVForm
  7. from dcim.models import Site, Rack
  8. from extras.choices import *
  9. from extras.models import CustomField
  10. from utilities.testing import APITestCase, TestCase
  11. from virtualization.models import VirtualMachine
  12. class CustomFieldTest(TestCase):
  13. def setUp(self):
  14. Site.objects.bulk_create([
  15. Site(name='Site A', slug='site-a'),
  16. Site(name='Site B', slug='site-b'),
  17. Site(name='Site C', slug='site-c'),
  18. ])
  19. def test_simple_fields(self):
  20. DATA = (
  21. {
  22. 'field_type': CustomFieldTypeChoices.TYPE_TEXT,
  23. 'field_value': 'Foobar!',
  24. 'empty_value': '',
  25. },
  26. {
  27. 'field_type': CustomFieldTypeChoices.TYPE_LONGTEXT,
  28. 'field_value': 'Text with **Markdown**',
  29. 'empty_value': '',
  30. },
  31. {
  32. 'field_type': CustomFieldTypeChoices.TYPE_INTEGER,
  33. 'field_value': 0,
  34. 'empty_value': None,
  35. },
  36. {
  37. 'field_type': CustomFieldTypeChoices.TYPE_INTEGER,
  38. 'field_value': 42,
  39. 'empty_value': None,
  40. },
  41. {
  42. 'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN,
  43. 'field_value': True,
  44. 'empty_value': None,
  45. },
  46. {
  47. 'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN,
  48. 'field_value': False,
  49. 'empty_value': None,
  50. },
  51. {
  52. 'field_type': CustomFieldTypeChoices.TYPE_DATE,
  53. 'field_value': '2016-06-23',
  54. 'empty_value': None,
  55. },
  56. {
  57. 'field_type': CustomFieldTypeChoices.TYPE_URL,
  58. 'field_value': 'http://example.com/',
  59. 'empty_value': '',
  60. },
  61. {
  62. 'field_type': CustomFieldTypeChoices.TYPE_JSON,
  63. 'field_value': '{"foo": 1, "bar": 2}',
  64. 'empty_value': 'null',
  65. },
  66. )
  67. obj_type = ContentType.objects.get_for_model(Site)
  68. for data in DATA:
  69. # Create a custom field
  70. cf = CustomField(type=data['field_type'], name='my_field', required=False)
  71. cf.save()
  72. cf.content_types.set([obj_type])
  73. # Check that the field has a null initial value
  74. site = Site.objects.first()
  75. self.assertIsNone(site.custom_field_data[cf.name])
  76. # Assign a value to the first Site
  77. site.custom_field_data[cf.name] = data['field_value']
  78. site.save()
  79. # Retrieve the stored value
  80. site.refresh_from_db()
  81. self.assertEqual(site.custom_field_data[cf.name], data['field_value'])
  82. # Delete the stored value
  83. site.custom_field_data.pop(cf.name)
  84. site.save()
  85. site.refresh_from_db()
  86. self.assertIsNone(site.custom_field_data.get(cf.name))
  87. # Delete the custom field
  88. cf.delete()
  89. def test_select_field(self):
  90. obj_type = ContentType.objects.get_for_model(Site)
  91. # Create a custom field
  92. cf = CustomField(
  93. type=CustomFieldTypeChoices.TYPE_SELECT,
  94. name='my_field',
  95. required=False,
  96. choices=['Option A', 'Option B', 'Option C']
  97. )
  98. cf.save()
  99. cf.content_types.set([obj_type])
  100. # Check that the field has a null initial value
  101. site = Site.objects.first()
  102. self.assertIsNone(site.custom_field_data[cf.name])
  103. # Assign a value to the first Site
  104. site.custom_field_data[cf.name] = 'Option A'
  105. site.save()
  106. # Retrieve the stored value
  107. site.refresh_from_db()
  108. self.assertEqual(site.custom_field_data[cf.name], 'Option A')
  109. # Delete the stored value
  110. site.custom_field_data.pop(cf.name)
  111. site.save()
  112. site.refresh_from_db()
  113. self.assertIsNone(site.custom_field_data.get(cf.name))
  114. # Delete the custom field
  115. cf.delete()
  116. def test_rename_customfield(self):
  117. obj_type = ContentType.objects.get_for_model(Site)
  118. FIELD_DATA = 'abc'
  119. # Create a custom field
  120. cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
  121. cf.save()
  122. cf.content_types.set([obj_type])
  123. # Assign custom field data to an object
  124. site = Site.objects.create(
  125. name='Site 1',
  126. slug='site-1',
  127. custom_field_data={'field1': FIELD_DATA}
  128. )
  129. site.refresh_from_db()
  130. self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
  131. # Rename the custom field
  132. cf.name = 'field2'
  133. cf.save()
  134. # Check that custom field data on the object has been updated
  135. site.refresh_from_db()
  136. self.assertNotIn('field1', site.custom_field_data)
  137. self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
  138. class CustomFieldManagerTest(TestCase):
  139. def setUp(self):
  140. content_type = ContentType.objects.get_for_model(Site)
  141. custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
  142. custom_field.save()
  143. custom_field.content_types.set([content_type])
  144. def test_get_for_model(self):
  145. self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
  146. self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
  147. class CustomFieldAPITest(APITestCase):
  148. @classmethod
  149. def setUpTestData(cls):
  150. content_type = ContentType.objects.get_for_model(Site)
  151. # Text custom field
  152. cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
  153. cls.cf_text.save()
  154. cls.cf_text.content_types.set([content_type])
  155. # Long text custom field
  156. cls.cf_longtext = CustomField(type=CustomFieldTypeChoices.TYPE_LONGTEXT, name='longtext_field', default='ABC')
  157. cls.cf_longtext.save()
  158. cls.cf_longtext.content_types.set([content_type])
  159. # Integer custom field
  160. cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123)
  161. cls.cf_integer.save()
  162. cls.cf_integer.content_types.set([content_type])
  163. # Boolean custom field
  164. cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False)
  165. cls.cf_boolean.save()
  166. cls.cf_boolean.content_types.set([content_type])
  167. # Date custom field
  168. cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01')
  169. cls.cf_date.save()
  170. cls.cf_date.content_types.set([content_type])
  171. # URL custom field
  172. cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1')
  173. cls.cf_url.save()
  174. cls.cf_url.content_types.set([content_type])
  175. # JSON custom field
  176. cls.cf_json = CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}')
  177. cls.cf_json.save()
  178. cls.cf_json.content_types.set([content_type])
  179. # Select custom field
  180. cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field', choices=['Foo', 'Bar', 'Baz'])
  181. cls.cf_select.default = 'Foo'
  182. cls.cf_select.save()
  183. cls.cf_select.content_types.set([content_type])
  184. # Create some sites
  185. cls.sites = (
  186. Site(name='Site 1', slug='site-1'),
  187. Site(name='Site 2', slug='site-2'),
  188. )
  189. Site.objects.bulk_create(cls.sites)
  190. # Assign custom field values for site 2
  191. cls.sites[1].custom_field_data = {
  192. cls.cf_text.name: 'bar',
  193. cls.cf_longtext.name: 'DEF',
  194. cls.cf_integer.name: 456,
  195. cls.cf_boolean.name: True,
  196. cls.cf_date.name: '2020-01-02',
  197. cls.cf_url.name: 'http://example.com/2',
  198. cls.cf_json.name: '{"foo": 1, "bar": 2}',
  199. cls.cf_select.name: 'Bar',
  200. }
  201. cls.sites[1].save()
  202. def test_get_single_object_without_custom_field_data(self):
  203. """
  204. Validate that custom fields are present on an object even if it has no values defined.
  205. """
  206. url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[0].pk})
  207. self.add_permissions('dcim.view_site')
  208. response = self.client.get(url, **self.header)
  209. self.assertEqual(response.data['name'], self.sites[0].name)
  210. self.assertEqual(response.data['custom_fields'], {
  211. 'text_field': None,
  212. 'longtext_field': None,
  213. 'number_field': None,
  214. 'boolean_field': None,
  215. 'date_field': None,
  216. 'url_field': None,
  217. 'json_field': None,
  218. 'choice_field': None,
  219. })
  220. def test_get_single_object_with_custom_field_data(self):
  221. """
  222. Validate that custom fields are present and correctly set for an object with values defined.
  223. """
  224. site2_cfvs = self.sites[1].custom_field_data
  225. url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
  226. self.add_permissions('dcim.view_site')
  227. response = self.client.get(url, **self.header)
  228. self.assertEqual(response.data['name'], self.sites[1].name)
  229. self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
  230. self.assertEqual(response.data['custom_fields']['longtext_field'], site2_cfvs['longtext_field'])
  231. self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
  232. self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
  233. self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
  234. self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
  235. self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field'])
  236. self.assertEqual(response.data['custom_fields']['choice_field'], site2_cfvs['choice_field'])
  237. def test_create_single_object_with_defaults(self):
  238. """
  239. Create a new site with no specified custom field values and check that it received the default values.
  240. """
  241. data = {
  242. 'name': 'Site 3',
  243. 'slug': 'site-3',
  244. }
  245. url = reverse('dcim-api:site-list')
  246. self.add_permissions('dcim.add_site')
  247. response = self.client.post(url, data, format='json', **self.header)
  248. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  249. # Validate response data
  250. response_cf = response.data['custom_fields']
  251. self.assertEqual(response_cf['text_field'], self.cf_text.default)
  252. self.assertEqual(response_cf['longtext_field'], self.cf_longtext.default)
  253. self.assertEqual(response_cf['number_field'], self.cf_integer.default)
  254. self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
  255. self.assertEqual(response_cf['date_field'], self.cf_date.default)
  256. self.assertEqual(response_cf['url_field'], self.cf_url.default)
  257. self.assertEqual(response_cf['json_field'], self.cf_json.default)
  258. self.assertEqual(response_cf['choice_field'], self.cf_select.default)
  259. # Validate database data
  260. site = Site.objects.get(pk=response.data['id'])
  261. self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
  262. self.assertEqual(site.custom_field_data['longtext_field'], self.cf_longtext.default)
  263. self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
  264. self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
  265. self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
  266. self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
  267. self.assertEqual(site.custom_field_data['json_field'], self.cf_json.default)
  268. self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
  269. def test_create_single_object_with_values(self):
  270. """
  271. Create a single new site with a value for each type of custom field.
  272. """
  273. data = {
  274. 'name': 'Site 3',
  275. 'slug': 'site-3',
  276. 'custom_fields': {
  277. 'text_field': 'bar',
  278. 'longtext_field': 'blah blah blah',
  279. 'number_field': 456,
  280. 'boolean_field': True,
  281. 'date_field': '2020-01-02',
  282. 'url_field': 'http://example.com/2',
  283. 'json_field': '{"foo": 1, "bar": 2}',
  284. 'choice_field': 'Bar',
  285. },
  286. }
  287. url = reverse('dcim-api:site-list')
  288. self.add_permissions('dcim.add_site')
  289. response = self.client.post(url, data, format='json', **self.header)
  290. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  291. # Validate response data
  292. response_cf = response.data['custom_fields']
  293. data_cf = data['custom_fields']
  294. self.assertEqual(response_cf['text_field'], data_cf['text_field'])
  295. self.assertEqual(response_cf['longtext_field'], data_cf['longtext_field'])
  296. self.assertEqual(response_cf['number_field'], data_cf['number_field'])
  297. self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
  298. self.assertEqual(response_cf['date_field'], data_cf['date_field'])
  299. self.assertEqual(response_cf['url_field'], data_cf['url_field'])
  300. self.assertEqual(response_cf['json_field'], data_cf['json_field'])
  301. self.assertEqual(response_cf['choice_field'], data_cf['choice_field'])
  302. # Validate database data
  303. site = Site.objects.get(pk=response.data['id'])
  304. self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
  305. self.assertEqual(site.custom_field_data['longtext_field'], data_cf['longtext_field'])
  306. self.assertEqual(site.custom_field_data['number_field'], data_cf['number_field'])
  307. self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
  308. self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
  309. self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
  310. self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field'])
  311. self.assertEqual(site.custom_field_data['choice_field'], data_cf['choice_field'])
  312. def test_create_multiple_objects_with_defaults(self):
  313. """
  314. Create three news sites with no specified custom field values and check that each received
  315. the default custom field values.
  316. """
  317. data = (
  318. {
  319. 'name': 'Site 3',
  320. 'slug': 'site-3',
  321. },
  322. {
  323. 'name': 'Site 4',
  324. 'slug': 'site-4',
  325. },
  326. {
  327. 'name': 'Site 5',
  328. 'slug': 'site-5',
  329. },
  330. )
  331. url = reverse('dcim-api:site-list')
  332. self.add_permissions('dcim.add_site')
  333. response = self.client.post(url, data, format='json', **self.header)
  334. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  335. self.assertEqual(len(response.data), len(data))
  336. for i, obj in enumerate(data):
  337. # Validate response data
  338. response_cf = response.data[i]['custom_fields']
  339. self.assertEqual(response_cf['text_field'], self.cf_text.default)
  340. self.assertEqual(response_cf['longtext_field'], self.cf_longtext.default)
  341. self.assertEqual(response_cf['number_field'], self.cf_integer.default)
  342. self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
  343. self.assertEqual(response_cf['date_field'], self.cf_date.default)
  344. self.assertEqual(response_cf['url_field'], self.cf_url.default)
  345. self.assertEqual(response_cf['json_field'], self.cf_json.default)
  346. self.assertEqual(response_cf['choice_field'], self.cf_select.default)
  347. # Validate database data
  348. site = Site.objects.get(pk=response.data[i]['id'])
  349. self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
  350. self.assertEqual(site.custom_field_data['longtext_field'], self.cf_longtext.default)
  351. self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
  352. self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
  353. self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
  354. self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
  355. self.assertEqual(site.custom_field_data['json_field'], self.cf_json.default)
  356. self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
  357. def test_create_multiple_objects_with_values(self):
  358. """
  359. Create a three new sites, each with custom fields defined.
  360. """
  361. custom_field_data = {
  362. 'text_field': 'bar',
  363. 'longtext_field': 'abcdefghij',
  364. 'number_field': 456,
  365. 'boolean_field': True,
  366. 'date_field': '2020-01-02',
  367. 'url_field': 'http://example.com/2',
  368. 'json_field': '{"foo": 1, "bar": 2}',
  369. 'choice_field': 'Bar',
  370. }
  371. data = (
  372. {
  373. 'name': 'Site 3',
  374. 'slug': 'site-3',
  375. 'custom_fields': custom_field_data,
  376. },
  377. {
  378. 'name': 'Site 4',
  379. 'slug': 'site-4',
  380. 'custom_fields': custom_field_data,
  381. },
  382. {
  383. 'name': 'Site 5',
  384. 'slug': 'site-5',
  385. 'custom_fields': custom_field_data,
  386. },
  387. )
  388. url = reverse('dcim-api:site-list')
  389. self.add_permissions('dcim.add_site')
  390. response = self.client.post(url, data, format='json', **self.header)
  391. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  392. self.assertEqual(len(response.data), len(data))
  393. for i, obj in enumerate(data):
  394. # Validate response data
  395. response_cf = response.data[i]['custom_fields']
  396. self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
  397. self.assertEqual(response_cf['longtext_field'], custom_field_data['longtext_field'])
  398. self.assertEqual(response_cf['number_field'], custom_field_data['number_field'])
  399. self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
  400. self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
  401. self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
  402. self.assertEqual(response_cf['json_field'], custom_field_data['json_field'])
  403. self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field'])
  404. # Validate database data
  405. site = Site.objects.get(pk=response.data[i]['id'])
  406. self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
  407. self.assertEqual(site.custom_field_data['longtext_field'], custom_field_data['longtext_field'])
  408. self.assertEqual(site.custom_field_data['number_field'], custom_field_data['number_field'])
  409. self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
  410. self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
  411. self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
  412. self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field'])
  413. self.assertEqual(site.custom_field_data['choice_field'], custom_field_data['choice_field'])
  414. def test_update_single_object_with_values(self):
  415. """
  416. Update an object with existing custom field values. Ensure that only the updated custom field values are
  417. modified.
  418. """
  419. site = self.sites[1]
  420. original_cfvs = {**site.custom_field_data}
  421. data = {
  422. 'custom_fields': {
  423. 'text_field': 'ABCD',
  424. 'number_field': 1234,
  425. },
  426. }
  427. url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
  428. self.add_permissions('dcim.change_site')
  429. response = self.client.patch(url, data, format='json', **self.header)
  430. self.assertHttpStatus(response, status.HTTP_200_OK)
  431. # Validate response data
  432. response_cf = response.data['custom_fields']
  433. self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
  434. self.assertEqual(response_cf['number_field'], data['custom_fields']['number_field'])
  435. self.assertEqual(response_cf['longtext_field'], original_cfvs['longtext_field'])
  436. self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
  437. self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
  438. self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
  439. self.assertEqual(response_cf['json_field'], original_cfvs['json_field'])
  440. self.assertEqual(response_cf['choice_field'], original_cfvs['choice_field'])
  441. # Validate database data
  442. site.refresh_from_db()
  443. self.assertEqual(site.custom_field_data['text_field'], data['custom_fields']['text_field'])
  444. self.assertEqual(site.custom_field_data['number_field'], data['custom_fields']['number_field'])
  445. self.assertEqual(site.custom_field_data['longtext_field'], original_cfvs['longtext_field'])
  446. self.assertEqual(site.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
  447. self.assertEqual(site.custom_field_data['date_field'], original_cfvs['date_field'])
  448. self.assertEqual(site.custom_field_data['url_field'], original_cfvs['url_field'])
  449. self.assertEqual(site.custom_field_data['json_field'], original_cfvs['json_field'])
  450. self.assertEqual(site.custom_field_data['choice_field'], original_cfvs['choice_field'])
  451. def test_minimum_maximum_values_validation(self):
  452. url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
  453. self.add_permissions('dcim.change_site')
  454. self.cf_integer.validation_minimum = 10
  455. self.cf_integer.validation_maximum = 20
  456. self.cf_integer.save()
  457. data = {'custom_fields': {'number_field': 9}}
  458. response = self.client.patch(url, data, format='json', **self.header)
  459. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  460. data = {'custom_fields': {'number_field': 21}}
  461. response = self.client.patch(url, data, format='json', **self.header)
  462. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  463. data = {'custom_fields': {'number_field': 15}}
  464. response = self.client.patch(url, data, format='json', **self.header)
  465. self.assertHttpStatus(response, status.HTTP_200_OK)
  466. def test_regex_validation(self):
  467. url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
  468. self.add_permissions('dcim.change_site')
  469. self.cf_text.validation_regex = r'^[A-Z]{3}$' # Three uppercase letters
  470. self.cf_text.save()
  471. data = {'custom_fields': {'text_field': 'ABC123'}}
  472. response = self.client.patch(url, data, format='json', **self.header)
  473. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  474. data = {'custom_fields': {'text_field': 'abc'}}
  475. response = self.client.patch(url, data, format='json', **self.header)
  476. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  477. data = {'custom_fields': {'text_field': 'ABC'}}
  478. response = self.client.patch(url, data, format='json', **self.header)
  479. self.assertHttpStatus(response, status.HTTP_200_OK)
  480. class CustomFieldImportTest(TestCase):
  481. user_permissions = (
  482. 'dcim.view_site',
  483. 'dcim.add_site',
  484. )
  485. @classmethod
  486. def setUpTestData(cls):
  487. custom_fields = (
  488. CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
  489. CustomField(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT),
  490. CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
  491. CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
  492. CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
  493. CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
  494. CustomField(name='json', type=CustomFieldTypeChoices.TYPE_JSON),
  495. CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[
  496. 'Choice A', 'Choice B', 'Choice C',
  497. ]),
  498. )
  499. for cf in custom_fields:
  500. cf.save()
  501. cf.content_types.set([ContentType.objects.get_for_model(Site)])
  502. def test_import(self):
  503. """
  504. Import a Site in CSV format, including a value for each CustomField.
  505. """
  506. data = (
  507. ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select'),
  508. ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A'),
  509. ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B'),
  510. ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', ''),
  511. )
  512. csv_data = '\n'.join(','.join(row) for row in data)
  513. response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
  514. self.assertEqual(response.status_code, 200)
  515. # Validate data for site 1
  516. site1 = Site.objects.get(name='Site 1')
  517. self.assertEqual(len(site1.custom_field_data), 8)
  518. self.assertEqual(site1.custom_field_data['text'], 'ABC')
  519. self.assertEqual(site1.custom_field_data['longtext'], 'Foo')
  520. self.assertEqual(site1.custom_field_data['integer'], 123)
  521. self.assertEqual(site1.custom_field_data['boolean'], True)
  522. self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
  523. self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
  524. self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
  525. self.assertEqual(site1.custom_field_data['select'], 'Choice A')
  526. # Validate data for site 2
  527. site2 = Site.objects.get(name='Site 2')
  528. self.assertEqual(len(site2.custom_field_data), 8)
  529. self.assertEqual(site2.custom_field_data['text'], 'DEF')
  530. self.assertEqual(site2.custom_field_data['longtext'], 'Bar')
  531. self.assertEqual(site2.custom_field_data['integer'], 456)
  532. self.assertEqual(site2.custom_field_data['boolean'], False)
  533. self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
  534. self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
  535. self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
  536. self.assertEqual(site2.custom_field_data['select'], 'Choice B')
  537. # No custom field data should be set for site 3
  538. site3 = Site.objects.get(name='Site 3')
  539. self.assertFalse(any(site3.custom_field_data.values()))
  540. def test_import_missing_required(self):
  541. """
  542. Attempt to import an object missing a required custom field.
  543. """
  544. # Set one of our CustomFields to required
  545. CustomField.objects.filter(name='text').update(required=True)
  546. form_data = {
  547. 'name': 'Site 1',
  548. 'slug': 'site-1',
  549. }
  550. form = SiteCSVForm(data=form_data)
  551. self.assertFalse(form.is_valid())
  552. self.assertIn('cf_text', form.errors)
  553. def test_import_invalid_choice(self):
  554. """
  555. Attempt to import an object with an invalid choice selection.
  556. """
  557. form_data = {
  558. 'name': 'Site 1',
  559. 'slug': 'site-1',
  560. 'cf_select': 'Choice X'
  561. }
  562. form = SiteCSVForm(data=form_data)
  563. self.assertFalse(form.is_valid())
  564. self.assertIn('cf_select', form.errors)
  565. class CustomFieldModelTest(TestCase):
  566. @classmethod
  567. def setUpTestData(cls):
  568. cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
  569. cf1.save()
  570. cf1.content_types.set([ContentType.objects.get_for_model(Site)])
  571. cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
  572. cf2.save()
  573. cf2.content_types.set([ContentType.objects.get_for_model(Rack)])
  574. def test_cf_data(self):
  575. """
  576. Check that custom field data is present on the instance immediately after being set and after being fetched
  577. from the database.
  578. """
  579. site = Site(name='Test Site', slug='test-site')
  580. # Check custom field data on new instance
  581. site.cf['foo'] = 'abc'
  582. self.assertEqual(site.cf['foo'], 'abc')
  583. # Check custom field data from database
  584. site.save()
  585. site = Site.objects.get(name='Test Site')
  586. self.assertEqual(site.cf['foo'], 'abc')
  587. def test_invalid_data(self):
  588. """
  589. Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
  590. """
  591. site = Site(name='Test Site', slug='test-site')
  592. # Set custom field data
  593. site.cf['foo'] = 'abc'
  594. site.cf['bar'] = 'def'
  595. with self.assertRaises(ValidationError):
  596. site.clean()
  597. del(site.cf['bar'])
  598. site.clean()
  599. def test_missing_required_field(self):
  600. """
  601. Check that a ValidationError is raised if any required custom fields are not present.
  602. """
  603. cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
  604. cf3.save()
  605. cf3.content_types.set([ContentType.objects.get_for_model(Site)])
  606. site = Site(name='Test Site', slug='test-site')
  607. # Set custom field data with a required field omitted
  608. site.cf['foo'] = 'abc'
  609. with self.assertRaises(ValidationError):
  610. site.clean()
  611. site.cf['baz'] = 'def'
  612. site.clean()
  613. class CustomFieldModelFilterTest(TestCase):
  614. queryset = Site.objects.all()
  615. filterset = SiteFilterSet
  616. @classmethod
  617. def setUpTestData(cls):
  618. obj_type = ContentType.objects.get_for_model(Site)
  619. # Integer filtering
  620. cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
  621. cf.save()
  622. cf.content_types.set([obj_type])
  623. # Boolean filtering
  624. cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
  625. cf.save()
  626. cf.content_types.set([obj_type])
  627. # Exact text filtering
  628. cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT,
  629. filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
  630. cf.save()
  631. cf.content_types.set([obj_type])
  632. # Loose text filtering
  633. cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
  634. filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
  635. cf.save()
  636. cf.content_types.set([obj_type])
  637. # Date filtering
  638. cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE)
  639. cf.save()
  640. cf.content_types.set([obj_type])
  641. # Exact URL filtering
  642. cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL,
  643. filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
  644. cf.save()
  645. cf.content_types.set([obj_type])
  646. # Loose URL filtering
  647. cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
  648. filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
  649. cf.save()
  650. cf.content_types.set([obj_type])
  651. # Selection filtering
  652. cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
  653. cf.save()
  654. cf.content_types.set([obj_type])
  655. # Multiselect filtering
  656. cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'B', 'C', 'X'])
  657. cf.save()
  658. cf.content_types.set([obj_type])
  659. Site.objects.bulk_create([
  660. Site(name='Site 1', slug='site-1', custom_field_data={
  661. 'cf1': 100,
  662. 'cf2': True,
  663. 'cf3': 'foo',
  664. 'cf4': 'foo',
  665. 'cf5': '2016-06-26',
  666. 'cf6': 'http://a.example.com',
  667. 'cf7': 'http://a.example.com',
  668. 'cf8': 'Foo',
  669. 'cf9': ['A', 'X'],
  670. }),
  671. Site(name='Site 2', slug='site-2', custom_field_data={
  672. 'cf1': 200,
  673. 'cf2': True,
  674. 'cf3': 'foobar',
  675. 'cf4': 'foobar',
  676. 'cf5': '2016-06-27',
  677. 'cf6': 'http://b.example.com',
  678. 'cf7': 'http://b.example.com',
  679. 'cf8': 'Bar',
  680. 'cf9': ['B', 'X'],
  681. }),
  682. Site(name='Site 3', slug='site-3', custom_field_data={
  683. 'cf1': 300,
  684. 'cf2': False,
  685. 'cf3': 'bar',
  686. 'cf4': 'bar',
  687. 'cf5': '2016-06-28',
  688. 'cf6': 'http://c.example.com',
  689. 'cf7': 'http://c.example.com',
  690. 'cf8': 'Baz',
  691. 'cf9': ['C', 'X'],
  692. }),
  693. ])
  694. def test_filter_integer(self):
  695. self.assertEqual(self.filterset({'cf_cf1': [100, 200]}, self.queryset).qs.count(), 2)
  696. self.assertEqual(self.filterset({'cf_cf1__n': [200]}, self.queryset).qs.count(), 2)
  697. self.assertEqual(self.filterset({'cf_cf1__gt': [200]}, self.queryset).qs.count(), 1)
  698. self.assertEqual(self.filterset({'cf_cf1__gte': [200]}, self.queryset).qs.count(), 2)
  699. self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1)
  700. self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2)
  701. def test_filter_boolean(self):
  702. self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 2)
  703. self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1)
  704. def test_filter_text_strict(self):
  705. self.assertEqual(self.filterset({'cf_cf3': ['foo']}, self.queryset).qs.count(), 1)
  706. self.assertEqual(self.filterset({'cf_cf3__n': ['foo']}, self.queryset).qs.count(), 2)
  707. self.assertEqual(self.filterset({'cf_cf3__ic': ['foo']}, self.queryset).qs.count(), 2)
  708. self.assertEqual(self.filterset({'cf_cf3__nic': ['foo']}, self.queryset).qs.count(), 1)
  709. self.assertEqual(self.filterset({'cf_cf3__isw': ['foo']}, self.queryset).qs.count(), 2)
  710. self.assertEqual(self.filterset({'cf_cf3__nisw': ['foo']}, self.queryset).qs.count(), 1)
  711. self.assertEqual(self.filterset({'cf_cf3__iew': ['bar']}, self.queryset).qs.count(), 2)
  712. self.assertEqual(self.filterset({'cf_cf3__niew': ['bar']}, self.queryset).qs.count(), 1)
  713. self.assertEqual(self.filterset({'cf_cf3__ie': ['FOO']}, self.queryset).qs.count(), 1)
  714. self.assertEqual(self.filterset({'cf_cf3__nie': ['FOO']}, self.queryset).qs.count(), 2)
  715. def test_filter_text_loose(self):
  716. self.assertEqual(self.filterset({'cf_cf4': ['foo']}, self.queryset).qs.count(), 2)
  717. def test_filter_date(self):
  718. self.assertEqual(self.filterset({'cf_cf5': ['2016-06-26', '2016-06-27']}, self.queryset).qs.count(), 2)
  719. self.assertEqual(self.filterset({'cf_cf5__n': ['2016-06-27']}, self.queryset).qs.count(), 2)
  720. self.assertEqual(self.filterset({'cf_cf5__gt': ['2016-06-27']}, self.queryset).qs.count(), 1)
  721. self.assertEqual(self.filterset({'cf_cf5__gte': ['2016-06-27']}, self.queryset).qs.count(), 2)
  722. self.assertEqual(self.filterset({'cf_cf5__lt': ['2016-06-27']}, self.queryset).qs.count(), 1)
  723. self.assertEqual(self.filterset({'cf_cf5__lte': ['2016-06-27']}, self.queryset).qs.count(), 2)
  724. def test_filter_url_strict(self):
  725. self.assertEqual(self.filterset({'cf_cf6': ['http://a.example.com', 'http://b.example.com']}, self.queryset).qs.count(), 2)
  726. self.assertEqual(self.filterset({'cf_cf6__n': ['http://b.example.com']}, self.queryset).qs.count(), 2)
  727. self.assertEqual(self.filterset({'cf_cf6__ic': ['b']}, self.queryset).qs.count(), 1)
  728. self.assertEqual(self.filterset({'cf_cf6__nic': ['b']}, self.queryset).qs.count(), 2)
  729. self.assertEqual(self.filterset({'cf_cf6__isw': ['http://']}, self.queryset).qs.count(), 3)
  730. self.assertEqual(self.filterset({'cf_cf6__nisw': ['http://']}, self.queryset).qs.count(), 0)
  731. self.assertEqual(self.filterset({'cf_cf6__iew': ['.com']}, self.queryset).qs.count(), 3)
  732. self.assertEqual(self.filterset({'cf_cf6__niew': ['.com']}, self.queryset).qs.count(), 0)
  733. self.assertEqual(self.filterset({'cf_cf6__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1)
  734. self.assertEqual(self.filterset({'cf_cf6__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2)
  735. def test_filter_url_loose(self):
  736. self.assertEqual(self.filterset({'cf_cf7': ['example.com']}, self.queryset).qs.count(), 3)
  737. def test_filter_select(self):
  738. self.assertEqual(self.filterset({'cf_cf8': ['Foo', 'Bar']}, self.queryset).qs.count(), 2)
  739. def test_filter_multiselect(self):
  740. self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
  741. self.assertEqual(self.filterset({'cf_cf9': ['X']}, self.queryset).qs.count(), 3)