1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge remote-tracking branch 'netbox-community/develop' into 3589-interface-tagged-vlans

This commit is contained in:
Saria Hajjar
2020-01-03 19:19:12 +00:00
19 changed files with 533 additions and 99 deletions

View File

@ -18,6 +18,17 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site',
queryset=Site.objects.all(),

View File

@ -104,6 +104,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -302,6 +314,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(

View File

@ -93,6 +93,17 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
class RackGroupFilter(NameSlugSearchFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -125,6 +136,17 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -831,6 +853,28 @@ class InventoryItemFilter(DeviceComponentFilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='device__site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@ -880,6 +924,17 @@ class VirtualChassisFilter(django_filters.FilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='master__site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='master__site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='master__site',
queryset=Site.objects.all(),
@ -935,7 +990,7 @@ class CableFilter(django_filters.FilterSet):
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueNumberFilter(
device = MultiValueCharFilter(
method='filter_device',
field_name='device__name'
)
@ -978,9 +1033,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@ -993,11 +1051,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(connected_endpoint__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'connected_endpoint__{}__in'.format(name): value})
)
@ -1006,9 +1064,12 @@ class PowerConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@ -1021,11 +1082,11 @@ class PowerConnectionFilter(django_filters.FilterSet):
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(_connected_poweroutlet__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
)
@ -1034,9 +1095,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
method='filter_site',
label='Site (slug)',
)
device = django_filters.CharFilter(
device_id = MultiValueNumberFilter(
method='filter_device'
)
device = MultiValueCharFilter(
method='filter_device',
label='Device',
field_name='device__name'
)
class Meta:
@ -1052,11 +1116,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
)
def filter_device(self, queryset, name, value):
if not value.strip():
if not value:
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(_connected_interface__device__name__icontains=value)
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_interface__{}__in'.format(name): value})
)
@ -1069,6 +1133,17 @@ class PowerPanelFilter(django_filters.FilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -1107,6 +1182,17 @@ class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='power_panel__site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='power_panel__site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='power_panel__site',
queryset=Site.objects.all(),

View File

@ -375,6 +375,18 @@ class RackGroupCSVForm(forms.ModelForm):
class RackGroupFilterForm(BootstrapMixin, forms.Form):
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -646,11 +658,23 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Rack
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -662,16 +686,15 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
}
)
)
group_id = ChainedModelChoiceField(
label='Rack group',
queryset=RackGroup.objects.prefetch_related('site'),
chains=(
('site', 'site'),
group_id = FilterChoiceField(
queryset=RackGroup.objects.prefetch_related(
'site'
),
required=False,
label='Rack group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
null_option=True
)
)
status = forms.MultipleChoiceField(
@ -3122,9 +3145,13 @@ class CableFilterForm(BootstrapMixin, forms.Form):
required=False,
widget=ColorSelect()
)
device = forms.CharField(
device_id = FilterChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device name'
label='Device',
widget=APISelectMultiple(
api_url='/api/dcim/devices/',
)
)
@ -3189,38 +3216,59 @@ class DeviceBayBulkRenameForm(BulkRenameForm):
#
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(
site = FilterChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
device = forms.CharField(
device_id = FilterChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device name'
label='Device',
widget=APISelectMultiple(
api_url='/api/dcim/devices/',
)
)
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(
site = FilterChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
device = forms.CharField(
device_id = FilterChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device name'
label='Device',
widget=APISelectMultiple(
api_url='/api/dcim/devices/',
)
)
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(
site = FilterChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='slug'
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
)
device = forms.CharField(
device_id = FilterChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device name'
label='Device',
widget=APISelectMultiple(
api_url='/api/dcim/devices/',
)
)
@ -3236,9 +3284,12 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InventoryItem
fields = [
'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
]
widgets = {
'device': APISelect(
api_url="/api/dcim/devices/"
),
'manufacturer': APISelect(
api_url="/api/dcim/manufacturers/"
)
@ -3274,9 +3325,19 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
queryset=InventoryItem.objects.all(),
widget=forms.MultipleHiddenInput()
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/devices/"
)
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
required=False,
widget=APISelect(
api_url="/api/dcim/manufacturers/"
)
)
part_id = forms.CharField(
max_length=50,
@ -3300,18 +3361,48 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
device = forms.CharField(
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
label='Device name'
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
filter_for={
'device_id': 'site'
}
)
)
device_id = FilterChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/',
)
)
manufacturer = FilterChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='slug',
null_label='-- None --'
widget=APISelect(
api_url="/api/dcim/manufacturers/",
value_field="slug",
)
)
discovered = forms.NullBooleanField(
required=False,
widget=forms.Select(
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
@ -3458,6 +3549,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -3563,6 +3666,18 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -3783,6 +3898,18 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',

View File

@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel):
# Check that the installed device is not already installed elsewhere
if self.installed_device:
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
if current_bay:
if current_bay and current_bay != self:
raise ValidationError({
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
current_bay

View File

@ -3,7 +3,10 @@ from django.contrib import admin
from netbox.admin import admin_site
from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
from .models import (
CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook,
)
from .reports import get_report
def order_content_types(field):
@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin):
form = ExportTemplateForm
#
# Reports
#
@admin.register(ReportResult, site=admin_site)
class ReportResultAdmin(admin.ModelAdmin):
list_display = [
'report', 'active', 'created', 'user', 'passing',
]
fields = [
'report', 'user', 'passing', 'data',
]
list_filter = [
'failed',
]
readonly_fields = fields
def has_add_permission(self, request):
return False
def active(self, obj):
module, report_name = obj.report.split('.')
return True if get_report(module, report_name) else False
active.boolean = True
def passing(self, obj):
return not obj.failed
passing.boolean = True
#
# Topology maps
#

View File

@ -52,7 +52,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
else:
initial = None
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
)
# Date
@ -71,7 +71,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
field = forms.TypedChoiceField(
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
)
# URL
elif cf.type == CF_TYPE_URL:

View File

@ -915,6 +915,13 @@ class ReportResult(models.Model):
class Meta:
ordering = ['report']
def __str__(self):
return "{} {} at {}".format(
self.report,
"passed" if not self.failed else "failed",
self.created
)
#
# Change logging

View File

@ -46,12 +46,17 @@ def custom_links(obj):
# Add non-grouped links
else:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
cl.url, link_target, cl.button_class, text_rendered
)
try:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_rendered = render_jinja2(cl.url, context)
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
link_rendered, link_target, cl.button_class, text_rendered
)
except Exception as e:
template_code += '<a class="btn btn-sm btn-default" disabled="disabled" title="{}">' \
'<i class="fa fa-warning"></i> {}</a>\n'.format(e, cl.name)
# Add grouped links to template
for group, links in group_names.items():
@ -59,11 +64,17 @@ def custom_links(obj):
links_rendered = []
for cl in links:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
try:
text_rendered = render_jinja2(cl.text, context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
links_rendered.append(
GROUP_LINK.format(cl.url, link_target, cl.text)
)
except Exception as e:
links_rendered.append(
GROUP_LINK.format(cl.url, link_target, cl.text)
'<li><a disabled="disabled" title="{}"><span class="text-muted">'
'<i class="fa fa-warning"></i> {}</span></a></li>'.format(e, cl.name)
)
if links_rendered:
@ -71,7 +82,4 @@ def custom_links(obj):
links[0].button_class, group, ''.join(links_rendered)
)
# Render template
rendered = render_jinja2(template_code, context)
return mark_safe(rendered)
return mark_safe(template_code)

View File

@ -4,10 +4,10 @@ from django.core.exceptions import ValidationError
from django.db.models import Q
from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from dcim.models import Device, Interface, Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from virtualization.models import VirtualMachine
from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@ -149,6 +149,17 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
to_field_name='rd',
label='VRF (RD)',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -309,6 +320,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
queryset=Interface.objects.all(),
label='Interface (ID)',
)
assigned_to_interface = django_filters.BooleanFilter(
method='_assigned_to_interface',
label='Is assigned to an interface',
)
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES,
null_value=None
@ -366,8 +381,22 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
except Device.DoesNotExist:
return queryset.none()
def _assigned_to_interface(self, queryset, name, value):
return queryset.exclude(interface__isnull=value)
class VLANGroupFilter(NameSlugSearchFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@ -393,6 +422,17 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',

View File

@ -3,7 +3,7 @@ from django.core.exceptions import MultipleObjectsReturned
from django.core.validators import MaxValueValidator, MinValueValidator
from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface
from dcim.models import Device, Interface, Rack, Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
@ -492,8 +492,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Prefix
field_order = [
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant',
'is_pool', 'expand',
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
'tenant', 'is_pool', 'expand',
]
q = forms.CharField(
required=False,
@ -534,6 +534,18 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
required=False,
widget=StaticSelect2Multiple()
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -938,7 +950,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = IPAddress
field_order = [
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant',
'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group',
'tenant',
]
q = forms.CharField(
required=False,
@ -984,6 +997,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
required=False,
widget=StaticSelect2Multiple()
)
assigned_to_interface = forms.NullBooleanField(
required=False,
label='Assigned to an interface',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
#
@ -1026,6 +1046,18 @@ class VLANGroupCSVForm(forms.ModelForm):
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region',
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
@ -1207,11 +1239,24 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = VLAN
field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region',
'group_id': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',

View File

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
# Environment setup
#
VERSION = '2.6.10-dev'
VERSION = '2.6.12-dev'
# Hostname
HOSTNAME = platform.node()

View File

@ -103,14 +103,16 @@ $(document).ready(function() {
placeholder: "---------",
theme: "bootstrap",
templateResult: colorPickerClassCopy,
templateSelection: colorPickerClassCopy
templateSelection: colorPickerClassCopy,
width: "off"
});
// Static choice selection
$('.netbox-select2-static').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap"
theme: "bootstrap",
width: "off"
});
// API backed selection
@ -120,6 +122,7 @@ $(document).ready(function() {
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
width: "off",
ajax: {
delay: 500,
@ -299,7 +302,8 @@ $(document).ready(function() {
multiple: true,
allowClear: true,
placeholder: "Tags",
theme: "bootstrap",
width: "off",
ajax: {
delay: 250,
url: netbox_api_path + "extras/tags/",

View File

@ -285,6 +285,8 @@ class APISelect(SelectWithDisabled):
name of the query param and the value if the query param's value.
:param null_option: If true, include the static null option in the selection list.
"""
# Only preload the selected option(s); new options are dynamically displayed and added via the API
template_name = 'widgets/select_api.html'
def __init__(
self,

View File

@ -0,0 +1,9 @@
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
{% for group_name, group_choices, group_index in widget.optgroups %}
{% if group_name %}<optgroup label="{{ group_name }}">{% endif %}
{% for option in group_choices %}
{% if option.attrs.selected or option.value == "null" %}{% include option.template_name with widget=option %}{% endif %}
{% endfor %}
{% if group_name %}</optgroup>{% endif %}
{% endfor %}
</select>

View File

@ -36,6 +36,27 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
method='search',
label='Search',
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=ClusterGroup.objects.all(),
label='Parent group (ID)',
@ -56,16 +77,6 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
to_field_name='slug',
label='Cluster type (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
tag = TagFilter()
class Meta:

View File

@ -173,6 +173,29 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cluster
q = forms.CharField(required=False, label='Search')
region = FilterChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
null_label='-- None --',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field='slug',
null_option=True,
)
)
type = FilterChoiceField(
queryset=ClusterType.objects.all(),
to_field_name='slug',
@ -193,17 +216,6 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True,
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
null_label='-- None --',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field='slug',
null_option=True,
)
)
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
@ -563,7 +575,9 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
widget=APISelectMultiple(
api_url='/api/dcim/regions/',
value_field="slug",
null_option=True,
filter_for={
'site': 'region'
}
)
)
site = FilterChoiceField(