test_customfields.py 29 KB

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