forms.py 23 KB

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