forms.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. from django import forms
  2. from django.db.models import Count
  3. from dcim.models import Site, Device, Interface
  4. from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  5. from tenancy.models import Tenant
  6. from utilities.forms import (
  7. APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, add_blank_choice,
  8. )
  9. from .models import (
  10. Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
  11. VLAN_STATUS_CHOICES, VRF,
  12. )
  13. IP_FAMILY_CHOICES = [
  14. ('', 'All'),
  15. (4, 'IPv4'),
  16. (6, 'IPv6'),
  17. ]
  18. #
  19. # VRFs
  20. #
  21. class VRFForm(BootstrapMixin, CustomFieldForm):
  22. class Meta:
  23. model = VRF
  24. fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
  25. labels = {
  26. 'rd': "RD",
  27. }
  28. help_texts = {
  29. 'rd': "Route distinguisher in any format",
  30. }
  31. class VRFFromCSVForm(forms.ModelForm):
  32. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  33. error_messages={'invalid_choice': 'Tenant not found.'})
  34. class Meta:
  35. model = VRF
  36. fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
  37. class VRFImportForm(BulkImportForm, BootstrapMixin):
  38. csv = CSVDataField(csv_form=VRFFromCSVForm)
  39. class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  40. pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
  41. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  42. description = forms.CharField(max_length=100, required=False)
  43. class Meta:
  44. nullable_fields = ['tenant', 'description']
  45. class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
  46. model = VRF
  47. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
  48. null_option=(0, None))
  49. #
  50. # RIRs
  51. #
  52. class RIRForm(forms.ModelForm, BootstrapMixin):
  53. slug = SlugField()
  54. class Meta:
  55. model = RIR
  56. fields = ['name', 'slug']
  57. #
  58. # Aggregates
  59. #
  60. class AggregateForm(BootstrapMixin, CustomFieldForm):
  61. class Meta:
  62. model = Aggregate
  63. fields = ['prefix', 'rir', 'date_added', 'description']
  64. help_texts = {
  65. 'prefix': "IPv4 or IPv6 network",
  66. 'rir': "Regional Internet Registry responsible for this prefix",
  67. 'date_added': "Format: YYYY-MM-DD",
  68. }
  69. class AggregateFromCSVForm(forms.ModelForm):
  70. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
  71. error_messages={'invalid_choice': 'RIR not found.'})
  72. class Meta:
  73. model = Aggregate
  74. fields = ['prefix', 'rir', 'date_added', 'description']
  75. class AggregateImportForm(BulkImportForm, BootstrapMixin):
  76. csv = CSVDataField(csv_form=AggregateFromCSVForm)
  77. class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  78. pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
  79. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
  80. date_added = forms.DateField(required=False)
  81. description = forms.CharField(max_length=100, required=False)
  82. class Meta:
  83. nullable_fields = ['date_added', 'description']
  84. class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
  85. model = Aggregate
  86. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
  87. rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
  88. label='RIR')
  89. #
  90. # Roles
  91. #
  92. class RoleForm(forms.ModelForm, BootstrapMixin):
  93. slug = SlugField()
  94. class Meta:
  95. model = Role
  96. fields = ['name', 'slug']
  97. #
  98. # Prefixes
  99. #
  100. class PrefixForm(BootstrapMixin, CustomFieldForm):
  101. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
  102. widget=forms.Select(attrs={'filter-for': 'vlan'}))
  103. vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
  104. widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
  105. display_field='display_name'))
  106. class Meta:
  107. model = Prefix
  108. fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
  109. help_texts = {
  110. 'prefix': "IPv4 or IPv6 network",
  111. 'vrf': "VRF (if applicable)",
  112. 'site': "The site to which this prefix is assigned (if applicable)",
  113. 'vlan': "The VLAN to which this prefix is assigned (if applicable)",
  114. 'status': "Operational status of this prefix",
  115. 'role': "The primary function of this prefix",
  116. }
  117. def __init__(self, *args, **kwargs):
  118. super(PrefixForm, self).__init__(*args, **kwargs)
  119. self.fields['vrf'].empty_label = 'Global'
  120. # Initialize field without choices to avoid pulling all VLANs from the database
  121. if self.is_bound and self.data.get('site'):
  122. self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
  123. elif self.initial.get('site'):
  124. self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
  125. else:
  126. self.fields['vlan'].choices = []
  127. def clean_prefix(self):
  128. prefix = self.cleaned_data['prefix']
  129. if prefix.version == 4 and prefix.prefixlen == 32:
  130. raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
  131. "addresses instead.")
  132. elif prefix.version == 6 and prefix.prefixlen == 128:
  133. raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
  134. "addresses instead.")
  135. return prefix
  136. class PrefixFromCSVForm(forms.ModelForm):
  137. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  138. error_messages={'invalid_choice': 'VRF not found.'})
  139. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  140. error_messages={'invalid_choice': 'Tenant not found.'})
  141. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
  142. error_messages={'invalid_choice': 'Site not found.'})
  143. vlan_group_name = forms.CharField(required=False)
  144. vlan_vid = forms.IntegerField(required=False)
  145. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
  146. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
  147. error_messages={'invalid_choice': 'Invalid role.'})
  148. class Meta:
  149. model = Prefix
  150. fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
  151. 'description']
  152. def clean(self):
  153. super(PrefixFromCSVForm, self).clean()
  154. site = self.cleaned_data.get('site')
  155. vlan_group_name = self.cleaned_data.get('vlan_group_name')
  156. vlan_vid = self.cleaned_data.get('vlan_vid')
  157. # Validate VLAN
  158. vlan_group = None
  159. if vlan_group_name:
  160. try:
  161. vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
  162. except VLANGroup.DoesNotExist:
  163. self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
  164. if vlan_vid and vlan_group:
  165. try:
  166. self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
  167. except VLAN.DoesNotExist:
  168. self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
  169. elif vlan_vid and site:
  170. try:
  171. self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
  172. except VLAN.MultipleObjectsReturned:
  173. self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
  174. elif vlan_vid:
  175. self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
  176. def save(self, *args, **kwargs):
  177. m = super(PrefixFromCSVForm, self).save(commit=False)
  178. # Assign Prefix status by name
  179. m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  180. if kwargs.get('commit'):
  181. m.save()
  182. return m
  183. class PrefixImportForm(BulkImportForm, BootstrapMixin):
  184. csv = CSVDataField(csv_form=PrefixFromCSVForm)
  185. class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  186. pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
  187. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  188. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  189. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  190. status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
  191. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  192. description = forms.CharField(max_length=100, required=False)
  193. class Meta:
  194. nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
  195. def prefix_status_choices():
  196. status_counts = {}
  197. for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
  198. status_counts[status['status']] = status['count']
  199. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
  200. class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
  201. model = Prefix
  202. parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
  203. 'placeholder': 'Network',
  204. }))
  205. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
  206. vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
  207. label='VRF', null_option=(0, 'Global'))
  208. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
  209. null_option=(0, 'None'))
  210. status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
  211. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
  212. null_option=(0, 'None'))
  213. role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
  214. null_option=(0, 'None'))
  215. expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
  216. #
  217. # IP addresses
  218. #
  219. class IPAddressForm(BootstrapMixin, CustomFieldForm):
  220. nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
  221. widget=forms.Select(attrs={'filter-for': 'nat_device'}))
  222. nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
  223. widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
  224. display_field='display_name',
  225. attrs={'filter-for': 'nat_inside'}))
  226. livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
  227. query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
  228. )
  229. nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)',
  230. widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
  231. display_field='address'))
  232. class Meta:
  233. model = IPAddress
  234. fields = ['address', 'vrf', 'tenant', 'status', 'nat_device', 'nat_inside', 'description']
  235. help_texts = {
  236. 'address': "IPv4 or IPv6 address and mask",
  237. 'vrf': "VRF (if applicable)",
  238. }
  239. def __init__(self, *args, **kwargs):
  240. super(IPAddressForm, self).__init__(*args, **kwargs)
  241. self.fields['vrf'].empty_label = 'Global'
  242. if self.instance.nat_inside:
  243. nat_inside = self.instance.nat_inside
  244. # If the IP is assigned to an interface, populate site/device fields accordingly
  245. if self.instance.nat_inside.interface:
  246. self.initial['nat_site'] = self.instance.nat_inside.interface.device.rack.site.pk
  247. self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
  248. self.fields['nat_device'].queryset = Device.objects.filter(
  249. rack__site=nat_inside.interface.device.rack.site)
  250. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  251. interface__device=nat_inside.interface.device)
  252. else:
  253. self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
  254. else:
  255. # Initialize nat_device choices if nat_site is set
  256. if self.is_bound and self.data.get('nat_site'):
  257. self.fields['nat_device'].queryset = Device.objects.filter(rack__site__pk=self.data['nat_site'])
  258. elif self.initial.get('nat_site'):
  259. self.fields['nat_device'].queryset = Device.objects.filter(rack__site=self.initial['nat_site'])
  260. else:
  261. self.fields['nat_device'].choices = []
  262. # Initialize nat_inside choices if nat_device is set
  263. if self.is_bound and self.data.get('nat_device'):
  264. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  265. interface__device__pk=self.data['nat_device'])
  266. elif self.initial.get('nat_device'):
  267. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  268. interface__device__pk=self.initial['nat_device'])
  269. else:
  270. self.fields['nat_inside'].choices = []
  271. class IPAddressFromCSVForm(forms.ModelForm):
  272. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  273. error_messages={'invalid_choice': 'VRF not found.'})
  274. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  275. error_messages={'invalid_choice': 'Tenant not found.'})
  276. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
  277. device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
  278. error_messages={'invalid_choice': 'Device not found.'})
  279. interface_name = forms.CharField(required=False)
  280. is_primary = forms.BooleanField(required=False)
  281. class Meta:
  282. model = IPAddress
  283. fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
  284. def clean(self):
  285. device = self.cleaned_data.get('device')
  286. interface_name = self.cleaned_data.get('interface_name')
  287. is_primary = self.cleaned_data.get('is_primary')
  288. # Validate interface
  289. if device and interface_name:
  290. try:
  291. Interface.objects.get(device=device, name=interface_name)
  292. except Interface.DoesNotExist:
  293. self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
  294. elif device and not interface_name:
  295. self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
  296. elif interface_name and not device:
  297. self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
  298. # Validate is_primary
  299. if is_primary and not device:
  300. self.add_error('is_primary', "No device specified; cannot set as primary IP")
  301. def save(self, commit=True):
  302. # Set interface
  303. if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
  304. self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
  305. name=self.cleaned_data['interface_name'])
  306. # Set as primary for device
  307. if self.cleaned_data['is_primary']:
  308. if self.instance.address.version == 4:
  309. self.instance.primary_ip4_for = self.cleaned_data['device']
  310. elif self.instance.address.version == 6:
  311. self.instance.primary_ip6_for = self.cleaned_data['device']
  312. return super(IPAddressFromCSVForm, self).save(commit=commit)
  313. class IPAddressImportForm(BulkImportForm, BootstrapMixin):
  314. csv = CSVDataField(csv_form=IPAddressFromCSVForm)
  315. class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  316. pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
  317. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  318. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  319. status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
  320. description = forms.CharField(max_length=100, required=False)
  321. class Meta:
  322. nullable_fields = ['vrf', 'tenant', 'description']
  323. def ipaddress_status_choices():
  324. status_counts = {}
  325. for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
  326. status_counts[status['status']] = status['count']
  327. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
  328. class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
  329. model = IPAddress
  330. parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
  331. 'placeholder': 'Prefix',
  332. }))
  333. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
  334. vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
  335. label='VRF', null_option=(0, 'Global'))
  336. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
  337. to_field_name='slug', null_option=(0, 'None'))
  338. status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
  339. #
  340. # VLAN groups
  341. #
  342. class VLANGroupForm(forms.ModelForm, BootstrapMixin):
  343. slug = SlugField()
  344. class Meta:
  345. model = VLANGroup
  346. fields = ['site', 'name', 'slug']
  347. class VLANGroupFilterForm(forms.Form, BootstrapMixin):
  348. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
  349. #
  350. # VLANs
  351. #
  352. class VLANForm(BootstrapMixin, CustomFieldForm):
  353. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
  354. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  355. ))
  356. class Meta:
  357. model = VLAN
  358. fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
  359. help_texts = {
  360. 'site': "The site at which this VLAN exists",
  361. 'group': "VLAN group (optional)",
  362. 'vid': "Configured VLAN ID",
  363. 'name': "Configured VLAN name",
  364. 'status': "Operational status of this VLAN",
  365. 'role': "The primary function of this VLAN",
  366. }
  367. widgets = {
  368. 'site': forms.Select(attrs={'filter-for': 'group'}),
  369. }
  370. def __init__(self, *args, **kwargs):
  371. super(VLANForm, self).__init__(*args, **kwargs)
  372. # Limit VLAN group choices
  373. if self.is_bound and self.data.get('site'):
  374. self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
  375. elif self.initial.get('site'):
  376. self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
  377. else:
  378. self.fields['group'].choices = []
  379. class VLANFromCSVForm(forms.ModelForm):
  380. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
  381. error_messages={'invalid_choice': 'Device not found.'})
  382. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
  383. error_messages={'invalid_choice': 'VLAN group not found.'})
  384. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  385. error_messages={'invalid_choice': 'Tenant not found.'})
  386. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
  387. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
  388. error_messages={'invalid_choice': 'Invalid role.'})
  389. class Meta:
  390. model = VLAN
  391. fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
  392. def save(self, *args, **kwargs):
  393. m = super(VLANFromCSVForm, self).save(commit=False)
  394. # Assign VLAN status by name
  395. m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  396. if kwargs.get('commit'):
  397. m.save()
  398. return m
  399. class VLANImportForm(BulkImportForm, BootstrapMixin):
  400. csv = CSVDataField(csv_form=VLANFromCSVForm)
  401. class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  402. pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
  403. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  404. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
  405. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  406. status = forms.ChoiceField(choices=add_blank_choice(VLAN_STATUS_CHOICES), required=False)
  407. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  408. description = forms.CharField(max_length=100, required=False)
  409. class Meta:
  410. nullable_fields = ['group', 'tenant', 'role', 'description']
  411. def vlan_status_choices():
  412. status_counts = {}
  413. for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
  414. status_counts[status['status']] = status['count']
  415. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
  416. class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
  417. model = VLAN
  418. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
  419. group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
  420. null_option=(0, 'None'))
  421. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
  422. null_option=(0, 'None'))
  423. status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
  424. role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
  425. null_option=(0, 'None'))