test_forms.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. from django.test import TestCase
  2. from dcim.choices import (
  3. DeviceFaceChoices,
  4. DeviceStatusChoices,
  5. InterfaceModeChoices,
  6. InterfaceTypeChoices,
  7. PortTypeChoices,
  8. PowerOutletStatusChoices,
  9. )
  10. from dcim.forms import *
  11. from dcim.models import *
  12. from ipam.models import ASN, RIR, VLAN
  13. from utilities.exceptions import AbortRequest
  14. from utilities.forms.rendering import M2MAddRemoveFields
  15. from utilities.testing import create_test_device
  16. from virtualization.models import Cluster, ClusterGroup, ClusterType
  17. def get_id(model, slug):
  18. return model.objects.get(slug=slug).id
  19. class PowerOutletFormTestCase(TestCase):
  20. @classmethod
  21. def setUpTestData(cls):
  22. cls.site = site = Site.objects.create(name='Site 1', slug='site-1')
  23. cls.manufacturer = manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  24. cls.role = role = DeviceRole.objects.create(
  25. name='Device Role 1', slug='device-role-1', color='ff0000'
  26. )
  27. cls.device_type = device_type = DeviceType.objects.create(
  28. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
  29. )
  30. cls.rack = rack = Rack.objects.create(name='Rack 1', site=site)
  31. cls.device = Device.objects.create(
  32. name='Device 1', device_type=device_type, role=role, site=site, rack=rack, position=1
  33. )
  34. def test_status_is_required(self):
  35. form = PowerOutletForm(data={
  36. 'device': self.device,
  37. 'module': None,
  38. 'name': 'New Enabled Outlet',
  39. })
  40. self.assertFalse(form.is_valid())
  41. self.assertIn('status', form.errors)
  42. def test_status_must_be_defined_choice(self):
  43. form = PowerOutletForm(data={
  44. 'device': self.device,
  45. 'module': None,
  46. 'name': 'New Enabled Outlet',
  47. 'status': 'this isn\'t a defined choice',
  48. })
  49. self.assertFalse(form.is_valid())
  50. self.assertIn('status', form.errors)
  51. self.assertTrue(form.errors['status'][-1].startswith('Select a valid choice.'))
  52. def test_status_recognizes_choices(self):
  53. for index, choice in enumerate(PowerOutletStatusChoices.CHOICES):
  54. form = PowerOutletForm(data={
  55. 'device': self.device,
  56. 'module': None,
  57. 'name': f'New Enabled Outlet {index + 1}',
  58. 'status': choice[0],
  59. })
  60. self.assertEqual({}, form.errors)
  61. self.assertTrue(form.is_valid())
  62. instance = form.save()
  63. self.assertEqual(instance.status, choice[0])
  64. class DeviceTestCase(TestCase):
  65. @classmethod
  66. def setUpTestData(cls):
  67. site = Site.objects.create(name='Site 1', slug='site-1')
  68. rack = Rack.objects.create(name='Rack 1', site=site)
  69. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  70. device_type = DeviceType.objects.create(
  71. manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
  72. )
  73. role = DeviceRole.objects.create(
  74. name='Device Role 1', slug='device-role-1', color='ff0000'
  75. )
  76. Platform.objects.create(name='Platform 1', slug='platform-1')
  77. Device.objects.create(
  78. name='Device 1', device_type=device_type, role=role, site=site, rack=rack, position=1
  79. )
  80. cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
  81. cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
  82. Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group)
  83. def test_racked_device(self):
  84. form = DeviceForm(data={
  85. 'name': 'New Device',
  86. 'role': DeviceRole.objects.first().pk,
  87. 'tenant': None,
  88. 'manufacturer': Manufacturer.objects.first().pk,
  89. 'device_type': DeviceType.objects.first().pk,
  90. 'site': Site.objects.first().pk,
  91. 'rack': Rack.objects.first().pk,
  92. 'face': DeviceFaceChoices.FACE_FRONT,
  93. 'position': 2,
  94. 'platform': Platform.objects.first().pk,
  95. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  96. })
  97. self.assertTrue(form.is_valid())
  98. self.assertTrue(form.save())
  99. def test_racked_device_occupied(self):
  100. form = DeviceForm(data={
  101. 'name': 'test',
  102. 'role': DeviceRole.objects.first().pk,
  103. 'tenant': None,
  104. 'manufacturer': Manufacturer.objects.first().pk,
  105. 'device_type': DeviceType.objects.first().pk,
  106. 'site': Site.objects.first().pk,
  107. 'rack': Rack.objects.first().pk,
  108. 'face': DeviceFaceChoices.FACE_FRONT,
  109. 'position': 1,
  110. 'platform': Platform.objects.first().pk,
  111. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  112. })
  113. self.assertFalse(form.is_valid())
  114. self.assertIn('position', form.errors)
  115. def test_non_racked_device(self):
  116. form = DeviceForm(data={
  117. 'name': 'New Device',
  118. 'role': DeviceRole.objects.first().pk,
  119. 'tenant': None,
  120. 'manufacturer': Manufacturer.objects.first().pk,
  121. 'device_type': DeviceType.objects.first().pk,
  122. 'site': Site.objects.first().pk,
  123. 'rack': None,
  124. 'face': None,
  125. 'position': None,
  126. 'platform': Platform.objects.first().pk,
  127. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  128. })
  129. self.assertTrue(form.is_valid())
  130. self.assertTrue(form.save())
  131. def test_non_racked_device_with_face(self):
  132. form = DeviceForm(data={
  133. 'name': 'New Device',
  134. 'role': DeviceRole.objects.first().pk,
  135. 'tenant': None,
  136. 'manufacturer': Manufacturer.objects.first().pk,
  137. 'device_type': DeviceType.objects.first().pk,
  138. 'site': Site.objects.first().pk,
  139. 'rack': None,
  140. 'face': DeviceFaceChoices.FACE_REAR,
  141. 'platform': None,
  142. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  143. })
  144. self.assertFalse(form.is_valid())
  145. self.assertIn('face', form.errors)
  146. def test_non_racked_device_with_position(self):
  147. form = DeviceForm(data={
  148. 'name': 'New Device',
  149. 'role': DeviceRole.objects.first().pk,
  150. 'tenant': None,
  151. 'manufacturer': Manufacturer.objects.first().pk,
  152. 'device_type': DeviceType.objects.first().pk,
  153. 'site': Site.objects.first().pk,
  154. 'rack': None,
  155. 'position': 10,
  156. 'platform': None,
  157. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  158. })
  159. self.assertFalse(form.is_valid())
  160. self.assertIn('position', form.errors)
  161. class VCPositionTokenFormTestCase(TestCase):
  162. @classmethod
  163. def setUpTestData(cls):
  164. Site.objects.create(name='Site VC 1', slug='site-vc-1')
  165. manufacturer = Manufacturer.objects.create(name='Manufacturer VC 1', slug='manufacturer-vc-1')
  166. device_type = DeviceType.objects.create(
  167. manufacturer=manufacturer, model='Device Type VC 1', slug='device-type-vc-1'
  168. )
  169. DeviceRole.objects.create(name='Device Role VC 1', slug='device-role-vc-1', color='ff0000')
  170. InterfaceTemplate.objects.create(
  171. device_type=device_type,
  172. name='ge-{vc_position:0}/0/0',
  173. type='1000base-t',
  174. )
  175. VirtualChassis.objects.create(name='VC 1')
  176. def test_device_creation_in_vc_resolves_vc_position(self):
  177. form = DeviceForm(data={
  178. 'name': 'Device VC Form 1',
  179. 'role': DeviceRole.objects.first().pk,
  180. 'tenant': None,
  181. 'manufacturer': Manufacturer.objects.first().pk,
  182. 'device_type': DeviceType.objects.first().pk,
  183. 'site': Site.objects.first().pk,
  184. 'rack': None,
  185. 'face': None,
  186. 'position': None,
  187. 'platform': None,
  188. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  189. 'virtual_chassis': VirtualChassis.objects.first().pk,
  190. 'vc_position': 2,
  191. })
  192. self.assertTrue(form.is_valid())
  193. device = form.save()
  194. self.assertTrue(device.interfaces.filter(name='ge-2/0/0').exists())
  195. def test_device_creation_not_in_vc_uses_fallback(self):
  196. form = DeviceForm(data={
  197. 'name': 'Device VC Form 2',
  198. 'role': DeviceRole.objects.first().pk,
  199. 'tenant': None,
  200. 'manufacturer': Manufacturer.objects.first().pk,
  201. 'device_type': DeviceType.objects.first().pk,
  202. 'site': Site.objects.first().pk,
  203. 'rack': None,
  204. 'face': None,
  205. 'position': None,
  206. 'platform': None,
  207. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  208. })
  209. self.assertTrue(form.is_valid())
  210. device = form.save()
  211. self.assertTrue(device.interfaces.filter(name='ge-0/0/0').exists())
  212. def test_device_creation_duplicate_name_conflict(self):
  213. # With conflict
  214. device_type = DeviceType.objects.first()
  215. # to generate conflicts create an interface that will exist
  216. InterfaceTemplate.objects.create(
  217. device_type=device_type,
  218. name='ge-0/0/0',
  219. type='1000base-t',
  220. )
  221. form = DeviceForm(data={
  222. 'name': 'Device VC Form 3',
  223. 'role': DeviceRole.objects.first().pk,
  224. 'tenant': None,
  225. 'manufacturer': Manufacturer.objects.first().pk,
  226. 'device_type': device_type.pk,
  227. 'site': Site.objects.first().pk,
  228. 'rack': None,
  229. 'face': None,
  230. 'position': None,
  231. 'platform': None,
  232. 'status': DeviceStatusChoices.STATUS_ACTIVE,
  233. })
  234. self.assertTrue(form.is_valid())
  235. with self.assertRaises(AbortRequest):
  236. form.save()
  237. class FrontPortTestCase(TestCase):
  238. @classmethod
  239. def setUpTestData(cls):
  240. cls.device = create_test_device('Panel Device 1')
  241. cls.rear_ports = (
  242. RearPort(name='RearPort1', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
  243. RearPort(name='RearPort2', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
  244. RearPort(name='RearPort3', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
  245. RearPort(name='RearPort4', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
  246. )
  247. RearPort.objects.bulk_create(cls.rear_ports)
  248. def test_front_port_label_count_valid(self):
  249. """
  250. Test that generating an equal number of names and labels passes form validation.
  251. """
  252. front_port_data = {
  253. 'device': self.device.pk,
  254. 'name': 'FrontPort[1-4]',
  255. 'label': 'Port[1-4]',
  256. 'type': PortTypeChoices.TYPE_8P8C,
  257. 'positions': 1,
  258. 'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
  259. }
  260. form = FrontPortCreateForm(front_port_data)
  261. self.assertTrue(form.is_valid())
  262. def test_front_port_label_count_mismatch(self):
  263. """
  264. Check that attempting to generate a differing number of names and labels results in a validation error.
  265. """
  266. bad_front_port_data = {
  267. 'device': self.device.pk,
  268. 'name': 'FrontPort[1-4]',
  269. 'label': 'Port[1-2]',
  270. 'type': PortTypeChoices.TYPE_8P8C,
  271. 'positions': 1,
  272. 'rear_ports': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
  273. }
  274. form = FrontPortCreateForm(bad_front_port_data)
  275. self.assertFalse(form.is_valid())
  276. self.assertIn('label', form.errors)
  277. class InterfaceTestCase(TestCase):
  278. @classmethod
  279. def setUpTestData(cls):
  280. cls.device = create_test_device('Device 1')
  281. cls.vlans = (
  282. VLAN(name='VLAN 1', vid=1),
  283. VLAN(name='VLAN 2', vid=2),
  284. VLAN(name='VLAN 3', vid=3),
  285. )
  286. VLAN.objects.bulk_create(cls.vlans)
  287. cls.interface = Interface.objects.create(
  288. device=cls.device,
  289. name='Interface 1',
  290. type=InterfaceTypeChoices.TYPE_1GE_GBIC,
  291. mode=InterfaceModeChoices.MODE_TAGGED,
  292. )
  293. def test_interface_label_count_valid(self):
  294. """
  295. Test that generating an equal number of names and labels passes form validation.
  296. """
  297. interface_data = {
  298. 'device': self.device.pk,
  299. 'name': 'eth[0-9]',
  300. 'label': 'Interface[0-9]',
  301. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  302. }
  303. form = InterfaceCreateForm(interface_data)
  304. self.assertTrue(form.is_valid())
  305. def test_interface_label_count_mismatch(self):
  306. """
  307. Check that attempting to generate a differing number of names and labels results in a validation error.
  308. """
  309. bad_interface_data = {
  310. 'device': self.device.pk,
  311. 'name': 'eth[0-9]',
  312. 'label': 'Interface[0-1]',
  313. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  314. }
  315. form = InterfaceCreateForm(bad_interface_data)
  316. self.assertFalse(form.is_valid())
  317. self.assertIn('label', form.errors)
  318. def test_create_interface_mode_valid_data(self):
  319. """
  320. Test that saving valid interface mode and tagged/untagged vlans works properly
  321. """
  322. # Validate access mode
  323. data = {
  324. 'device': self.device.pk,
  325. 'name': 'ethernet1/1',
  326. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  327. 'mode': InterfaceModeChoices.MODE_ACCESS,
  328. 'untagged_vlan': self.vlans[0].pk
  329. }
  330. form = InterfaceCreateForm(data)
  331. self.assertTrue(form.is_valid())
  332. # Validate tagged vlans
  333. data = {
  334. 'device': self.device.pk,
  335. 'name': 'ethernet1/2',
  336. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  337. 'mode': InterfaceModeChoices.MODE_TAGGED,
  338. 'untagged_vlan': self.vlans[0].pk,
  339. 'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
  340. }
  341. form = InterfaceCreateForm(data)
  342. self.assertTrue(form.is_valid())
  343. # Validate tagged vlans
  344. data = {
  345. 'device': self.device.pk,
  346. 'name': 'ethernet1/3',
  347. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  348. 'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
  349. 'untagged_vlan': self.vlans[0].pk,
  350. }
  351. form = InterfaceCreateForm(data)
  352. self.assertTrue(form.is_valid())
  353. def test_create_interface_mode_access_invalid_data(self):
  354. """
  355. Test that saving invalid interface mode and tagged/untagged vlans works properly
  356. """
  357. data = {
  358. 'device': self.device.pk,
  359. 'name': 'ethernet1/4',
  360. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  361. 'mode': InterfaceModeChoices.MODE_ACCESS,
  362. 'untagged_vlan': self.vlans[0].pk,
  363. 'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
  364. }
  365. form = InterfaceCreateForm(data)
  366. self.assertTrue(form.is_valid())
  367. self.assertIn('untagged_vlan', form.cleaned_data.keys())
  368. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  369. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  370. def test_edit_interface_mode_access_invalid_data(self):
  371. """
  372. Test that saving invalid interface mode and tagged/untagged vlans works properly
  373. """
  374. data = {
  375. 'device': self.device.pk,
  376. 'name': 'Ethernet 1/5',
  377. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  378. 'mode': InterfaceModeChoices.MODE_ACCESS,
  379. 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
  380. }
  381. form = InterfaceForm(data, instance=self.interface)
  382. self.assertTrue(form.is_valid())
  383. self.assertIn('untagged_vlan', form.cleaned_data.keys())
  384. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  385. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  386. def test_create_interface_mode_tagged_all_invalid_data(self):
  387. """
  388. Test that saving invalid interface mode and tagged/untagged vlans works properly
  389. """
  390. data = {
  391. 'device': self.device.pk,
  392. 'name': 'ethernet1/6',
  393. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  394. 'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
  395. 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
  396. }
  397. form = InterfaceCreateForm(data)
  398. self.assertTrue(form.is_valid())
  399. self.assertIn('untagged_vlan', form.cleaned_data.keys())
  400. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  401. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  402. def test_edit_interface_mode_tagged_all_invalid_data(self):
  403. """
  404. Test that saving invalid interface mode and tagged/untagged vlans works properly
  405. """
  406. data = {
  407. 'device': self.device.pk,
  408. 'name': 'Ethernet 1/7',
  409. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  410. 'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
  411. 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
  412. }
  413. form = InterfaceForm(data)
  414. self.assertTrue(form.is_valid())
  415. self.assertIn('untagged_vlan', form.cleaned_data.keys())
  416. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  417. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  418. def test_create_interface_mode_routed_invalid_data(self):
  419. """
  420. Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly
  421. """
  422. data = {
  423. 'device': self.device.pk,
  424. 'name': 'ethernet1/6',
  425. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  426. 'mode': None,
  427. 'untagged_vlan': self.vlans[0].pk,
  428. 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
  429. }
  430. form = InterfaceCreateForm(data)
  431. self.assertTrue(form.is_valid())
  432. self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
  433. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  434. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  435. def test_edit_interface_mode_routed_invalid_data(self):
  436. """
  437. Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly
  438. """
  439. data = {
  440. 'device': self.device.pk,
  441. 'name': 'Ethernet 1/7',
  442. 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
  443. 'mode': None,
  444. 'untagged_vlan': self.vlans[0].pk,
  445. 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
  446. }
  447. form = InterfaceForm(data)
  448. self.assertTrue(form.is_valid())
  449. self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
  450. self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
  451. self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
  452. class CableTestCase(TestCase):
  453. def test_invalid_side_designation_raises_value_error(self):
  454. """_clean_side rejects a side other than 'a' or 'b' with ValueError."""
  455. form = CableImportForm.__new__(CableImportForm)
  456. with self.assertRaisesMessage(ValueError, "Invalid side designation: c"):
  457. form._clean_side('c')
  458. class SiteFormTestCase(TestCase):
  459. """
  460. Tests for M2MAddRemoveFields using Site ASN assignments as the test case.
  461. Covers both simple mode (single multi-select field) and add/remove mode (dual fields).
  462. """
  463. @classmethod
  464. def setUpTestData(cls):
  465. cls.rir = RIR.objects.create(name='RIR 1', slug='rir-1')
  466. # Create 110 ASNs: 100 to pre-assign (triggering add/remove mode) plus 10 extras
  467. ASN.objects.bulk_create([ASN(asn=i, rir=cls.rir) for i in range(1, 111)])
  468. cls.asns = list(ASN.objects.order_by('asn'))
  469. def _site_data(self, **kwargs):
  470. data = {'name': 'Test Site', 'slug': 'test-site', 'status': 'active'}
  471. data.update(kwargs)
  472. return data
  473. def test_new_site_uses_simple_mode(self):
  474. """A form for a new site uses the single 'asns' field (simple mode)."""
  475. form = SiteForm(data=self._site_data())
  476. self.assertIn('asns', form.fields)
  477. self.assertNotIn('add_asns', form.fields)
  478. self.assertNotIn('remove_asns', form.fields)
  479. def test_existing_site_below_threshold_uses_simple_mode(self):
  480. """A form for an existing site with fewer than THRESHOLD ASNs uses simple mode."""
  481. site = Site.objects.create(name='Site 1', slug='site-1')
  482. site.asns.set(self.asns[:5])
  483. form = SiteForm(instance=site)
  484. self.assertIn('asns', form.fields)
  485. self.assertNotIn('add_asns', form.fields)
  486. self.assertNotIn('remove_asns', form.fields)
  487. def test_existing_site_at_threshold_uses_add_remove_mode(self):
  488. """A form for an existing site with THRESHOLD or more ASNs uses add/remove mode."""
  489. site = Site.objects.create(name='Site 2', slug='site-2')
  490. site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
  491. form = SiteForm(instance=site)
  492. self.assertNotIn('asns', form.fields)
  493. self.assertIn('add_asns', form.fields)
  494. self.assertIn('remove_asns', form.fields)
  495. def test_simple_mode_assigns_asns_on_create(self):
  496. """Saving a new site via simple mode assigns the selected ASNs."""
  497. asn_pks = [asn.pk for asn in self.asns[:3]]
  498. form = SiteForm(data=self._site_data(asns=asn_pks))
  499. self.assertTrue(form.is_valid(), form.errors)
  500. site = form.save()
  501. self.assertEqual(set(site.asns.values_list('pk', flat=True)), set(asn_pks))
  502. def test_simple_mode_replaces_asns_on_edit(self):
  503. """Saving an existing site via simple mode replaces the current ASN assignments."""
  504. site = Site.objects.create(name='Site 3', slug='site-3')
  505. site.asns.set(self.asns[:3])
  506. new_asn_pks = [asn.pk for asn in self.asns[3:6]]
  507. form = SiteForm(
  508. data=self._site_data(name='Site 3', slug='site-3', asns=new_asn_pks),
  509. instance=site
  510. )
  511. self.assertTrue(form.is_valid(), form.errors)
  512. site = form.save()
  513. self.assertEqual(set(site.asns.values_list('pk', flat=True)), set(new_asn_pks))
  514. def test_add_remove_mode_adds_asns(self):
  515. """In add/remove mode, specifying 'add_asns' appends to current assignments."""
  516. site = Site.objects.create(name='Site 4', slug='site-4')
  517. site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
  518. new_asn_pks = [asn.pk for asn in self.asns[M2MAddRemoveFields.THRESHOLD:]]
  519. form = SiteForm(
  520. data=self._site_data(name='Site 4', slug='site-4', add_asns=new_asn_pks),
  521. instance=site
  522. )
  523. self.assertTrue(form.is_valid(), form.errors)
  524. site = form.save()
  525. self.assertEqual(site.asns.count(), len(self.asns))
  526. def test_add_remove_mode_removes_asns(self):
  527. """In add/remove mode, specifying 'remove_asns' drops those assignments."""
  528. site = Site.objects.create(name='Site 5', slug='site-5')
  529. site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
  530. remove_pks = [asn.pk for asn in self.asns[:5]]
  531. form = SiteForm(
  532. data=self._site_data(name='Site 5', slug='site-5', remove_asns=remove_pks),
  533. instance=site
  534. )
  535. self.assertTrue(form.is_valid(), form.errors)
  536. site = form.save()
  537. self.assertEqual(site.asns.count(), M2MAddRemoveFields.THRESHOLD - 5)
  538. self.assertFalse(site.asns.filter(pk__in=remove_pks).exists())
  539. def test_add_remove_mode_simultaneous_add_and_remove(self):
  540. """In add/remove mode, add and remove operations are applied together."""
  541. site = Site.objects.create(name='Site 6', slug='site-6')
  542. site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
  543. add_pks = [asn.pk for asn in self.asns[M2MAddRemoveFields.THRESHOLD:M2MAddRemoveFields.THRESHOLD + 3]]
  544. remove_pks = [asn.pk for asn in self.asns[:3]]
  545. form = SiteForm(
  546. data=self._site_data(name='Site 6', slug='site-6', add_asns=add_pks, remove_asns=remove_pks),
  547. instance=site
  548. )
  549. self.assertTrue(form.is_valid(), form.errors)
  550. site = form.save()
  551. self.assertEqual(site.asns.count(), M2MAddRemoveFields.THRESHOLD)
  552. self.assertTrue(site.asns.filter(pk__in=add_pks).count() == 3)
  553. self.assertFalse(site.asns.filter(pk__in=remove_pks).exists())