test_customfields.py 30 KB

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