test_customfields.py 31 KB

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