mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge remote-tracking branch 'netbox-community/develop' into 3589-interface-tagged-vlans
This commit is contained in:
commit
fa55571503
@ -80,6 +80,7 @@ AUTH_LDAP_USER_ATTR_MAP = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# User Groups for Permissions
|
# User Groups for Permissions
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
|
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` .
|
||||||
|
|
||||||
@ -117,6 +118,9 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
|||||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory.
|
||||||
|
|
||||||
# Troubleshooting LDAP
|
# Troubleshooting LDAP
|
||||||
|
|
||||||
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
|
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
|
||||||
|
@ -1,10 +1,25 @@
|
|||||||
# v2.6.10 (FUTURE)
|
# v2.6.11 (2020-01-03)
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression)
|
||||||
|
* [#3833](https://github.com/netbox-community/netbox/issues/3833) - Add missing region filters for multiple objects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v2.6.10 (2020-01-02)
|
||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2233](https://github.com/netbox-community/netbox/issues/2233) - Add ability to move inventory items between devices
|
||||||
|
* [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results
|
||||||
|
* [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses
|
||||||
|
* [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception
|
||||||
* [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts
|
* [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts
|
||||||
* [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets
|
* [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets
|
||||||
* [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items
|
* [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items
|
||||||
|
* [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field
|
||||||
|
* [#3827](https://github.com/netbox-community/netbox/issues/3827) - Allow filtering console/power/interface connections by device ID
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
@ -15,6 +30,7 @@
|
|||||||
* [#3780](https://github.com/netbox-community/netbox/issues/3780) - Fix AttributeError exception in API docs
|
* [#3780](https://github.com/netbox-community/netbox/issues/3780) - Fix AttributeError exception in API docs
|
||||||
* [#3809](https://github.com/netbox-community/netbox/issues/3809) - Filter platform by manufacturer when editing devices
|
* [#3809](https://github.com/netbox-community/netbox/issues/3809) - Filter platform by manufacturer when editing devices
|
||||||
* [#3811](https://github.com/netbox-community/netbox/issues/3811) - Fix filtering of racks by group on device list
|
* [#3811](https://github.com/netbox-community/netbox/issues/3811) - Fix filtering of racks by group on device list
|
||||||
|
* [#3822](https://github.com/netbox-community/netbox/issues/3822) - Fix exception when editing a device bay (regression from #3596)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -18,6 +18,17 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='circuits__terminations__site',
|
field_name='circuits__terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
@ -104,6 +104,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -302,6 +314,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/regions/",
|
api_url="/api/dcim/regions/",
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
|
filter_for={
|
||||||
|
'site': 'region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
|
@ -93,6 +93,17 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
|||||||
|
|
||||||
|
|
||||||
class RackGroupFilter(NameSlugSearchFilterSet):
|
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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -125,6 +136,17 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -831,6 +853,28 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
device_id = django_filters.ModelChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
@ -880,6 +924,17 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='master__site',
|
field_name='master__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -935,7 +990,7 @@ class CableFilter(django_filters.FilterSet):
|
|||||||
device_id = MultiValueNumberFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
method='filter_device'
|
method='filter_device'
|
||||||
)
|
)
|
||||||
device = MultiValueNumberFilter(
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
field_name='device__name'
|
field_name='device__name'
|
||||||
)
|
)
|
||||||
@ -978,9 +1033,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
|||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
device = django_filters.CharFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
|
method='filter_device'
|
||||||
|
)
|
||||||
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
label='Device',
|
field_name='device__name'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -993,11 +1051,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(connected_endpoint__device__site__slug=value)
|
return queryset.filter(connected_endpoint__device__site__slug=value)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(device__name__icontains=value) |
|
Q(**{'{}__in'.format(name): value}) |
|
||||||
Q(connected_endpoint__device__name__icontains=value)
|
Q(**{'connected_endpoint__{}__in'.format(name): value})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1006,9 +1064,12 @@ class PowerConnectionFilter(django_filters.FilterSet):
|
|||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
device = django_filters.CharFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
|
method='filter_device'
|
||||||
|
)
|
||||||
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
label='Device',
|
field_name='device__name'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1021,11 +1082,11 @@ class PowerConnectionFilter(django_filters.FilterSet):
|
|||||||
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
|
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(device__name__icontains=value) |
|
Q(**{'{}__in'.format(name): value}) |
|
||||||
Q(_connected_poweroutlet__device__name__icontains=value)
|
Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1034,9 +1095,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
|||||||
method='filter_site',
|
method='filter_site',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
device = django_filters.CharFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
|
method='filter_device'
|
||||||
|
)
|
||||||
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_device',
|
||||||
label='Device',
|
field_name='device__name'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1052,11 +1116,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(device__name__icontains=value) |
|
Q(**{'{}__in'.format(name): value}) |
|
||||||
Q(_connected_interface__device__name__icontains=value)
|
Q(**{'_connected_interface__{}__in'.format(name): value})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1069,6 +1133,17 @@ class PowerPanelFilter(django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -1107,6 +1182,17 @@ class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='power_panel__site',
|
field_name='power_panel__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
@ -375,6 +375,18 @@ class RackGroupCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class RackGroupFilterForm(BootstrapMixin, forms.Form):
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -646,11 +658,23 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
|
|
||||||
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||||
model = Rack
|
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(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -662,16 +686,15 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group_id = ChainedModelChoiceField(
|
group_id = FilterChoiceField(
|
||||||
label='Rack group',
|
queryset=RackGroup.objects.prefetch_related(
|
||||||
queryset=RackGroup.objects.prefetch_related('site'),
|
'site'
|
||||||
chains=(
|
|
||||||
('site', 'site'),
|
|
||||||
),
|
),
|
||||||
required=False,
|
label='Rack group',
|
||||||
|
null_label='-- None --',
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url="/api/dcim/rack-groups/",
|
api_url="/api/dcim/rack-groups/",
|
||||||
null_option=True,
|
null_option=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
@ -3122,9 +3145,13 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=ColorSelect()
|
widget=ColorSelect()
|
||||||
)
|
)
|
||||||
device = forms.CharField(
|
device_id = FilterChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
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):
|
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
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,
|
required=False,
|
||||||
label='Device name'
|
label='Device',
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url='/api/dcim/devices/',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
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,
|
required=False,
|
||||||
label='Device name'
|
label='Device',
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url='/api/dcim/devices/',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
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,
|
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:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
|
'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'device': APISelect(
|
||||||
|
api_url="/api/dcim/devices/"
|
||||||
|
),
|
||||||
'manufacturer': APISelect(
|
'manufacturer': APISelect(
|
||||||
api_url="/api/dcim/manufacturers/"
|
api_url="/api/dcim/manufacturers/"
|
||||||
)
|
)
|
||||||
@ -3274,9 +3325,19 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/"
|
||||||
|
)
|
||||||
|
)
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/manufacturers/"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
part_id = forms.CharField(
|
part_id = forms.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -3300,18 +3361,48 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
device = forms.CharField(
|
region = FilterChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
required=False,
|
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(
|
manufacturer = FilterChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --'
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/manufacturers/",
|
||||||
|
value_field="slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
discovered = forms.NullBooleanField(
|
discovered = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=StaticSelect2(
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -3458,6 +3549,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -3563,6 +3666,18 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -3783,6 +3898,18 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel):
|
|||||||
# Check that the installed device is not already installed elsewhere
|
# Check that the installed device is not already installed elsewhere
|
||||||
if self.installed_device:
|
if self.installed_device:
|
||||||
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
|
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
|
||||||
if current_bay:
|
if current_bay and current_bay != self:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
|
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
|
||||||
current_bay
|
current_bay
|
||||||
|
@ -3,7 +3,10 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from netbox.admin import admin_site
|
from netbox.admin import admin_site
|
||||||
from utilities.forms import LaxURLField
|
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):
|
def order_content_types(field):
|
||||||
@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
|||||||
form = ExportTemplateForm
|
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
|
# Topology maps
|
||||||
#
|
#
|
||||||
|
@ -52,7 +52,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
else:
|
else:
|
||||||
initial = None
|
initial = None
|
||||||
field = forms.NullBooleanField(
|
field = forms.NullBooleanField(
|
||||||
required=cf.required, initial=initial, widget=forms.Select(choices=choices)
|
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Date
|
# 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
|
default_choice = cf.choices.get(value=initial).pk
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
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
|
# URL
|
||||||
elif cf.type == CF_TYPE_URL:
|
elif cf.type == CF_TYPE_URL:
|
||||||
|
@ -915,6 +915,13 @@ class ReportResult(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['report']
|
ordering = ['report']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{} {} at {}".format(
|
||||||
|
self.report,
|
||||||
|
"passed" if not self.failed else "failed",
|
||||||
|
self.created
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Change logging
|
# Change logging
|
||||||
|
@ -46,12 +46,17 @@ def custom_links(obj):
|
|||||||
|
|
||||||
# Add non-grouped links
|
# Add non-grouped links
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
text_rendered = render_jinja2(cl.text, context)
|
text_rendered = render_jinja2(cl.text, context)
|
||||||
if text_rendered:
|
if text_rendered:
|
||||||
|
link_rendered = render_jinja2(cl.url, context)
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
template_code += LINK_BUTTON.format(
|
template_code += LINK_BUTTON.format(
|
||||||
cl.url, link_target, cl.button_class, text_rendered
|
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
|
# Add grouped links to template
|
||||||
for group, links in group_names.items():
|
for group, links in group_names.items():
|
||||||
@ -59,19 +64,22 @@ def custom_links(obj):
|
|||||||
links_rendered = []
|
links_rendered = []
|
||||||
|
|
||||||
for cl in links:
|
for cl in links:
|
||||||
|
try:
|
||||||
text_rendered = render_jinja2(cl.text, context)
|
text_rendered = render_jinja2(cl.text, context)
|
||||||
if text_rendered:
|
if text_rendered:
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
link_target = ' target="_blank"' if cl.new_window else ''
|
||||||
links_rendered.append(
|
links_rendered.append(
|
||||||
GROUP_LINK.format(cl.url, link_target, cl.text)
|
GROUP_LINK.format(cl.url, link_target, cl.text)
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
links_rendered.append(
|
||||||
|
'<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:
|
if links_rendered:
|
||||||
template_code += GROUP_BUTTON.format(
|
template_code += GROUP_BUTTON.format(
|
||||||
links[0].button_class, group, ''.join(links_rendered)
|
links[0].button_class, group, ''.join(links_rendered)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Render template
|
return mark_safe(template_code)
|
||||||
rendered = render_jinja2(template_code, context)
|
|
||||||
|
|
||||||
return mark_safe(rendered)
|
|
||||||
|
@ -4,10 +4,10 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr.core import AddrFormatError
|
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 extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
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 virtualization.models import VirtualMachine
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
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',
|
to_field_name='rd',
|
||||||
label='VRF (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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -309,6 +320,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
|||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
label='Interface (ID)',
|
label='Interface (ID)',
|
||||||
)
|
)
|
||||||
|
assigned_to_interface = django_filters.BooleanFilter(
|
||||||
|
method='_assigned_to_interface',
|
||||||
|
label='Is assigned to an interface',
|
||||||
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPADDRESS_STATUS_CHOICES,
|
choices=IPADDRESS_STATUS_CHOICES,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -366,8 +381,22 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
|
|||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def _assigned_to_interface(self, queryset, name, value):
|
||||||
|
return queryset.exclude(interface__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(NameSlugSearchFilterSet):
|
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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -393,6 +422,17 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
|
@ -3,7 +3,7 @@ from django.core.exceptions import MultipleObjectsReturned
|
|||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from taggit.forms import TagField
|
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 extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -492,8 +492,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
field_order = [
|
field_order = [
|
||||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant',
|
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group',
|
||||||
'is_pool', 'expand',
|
'tenant', 'is_pool', 'expand',
|
||||||
]
|
]
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -534,6 +534,18 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -938,7 +950,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
|||||||
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
field_order = [
|
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(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -984,6 +997,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect2Multiple()
|
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):
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -1207,11 +1239,24 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
|
|
||||||
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||||
model = VLAN
|
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(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
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(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.6.10-dev'
|
VERSION = '2.6.12-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -103,14 +103,16 @@ $(document).ready(function() {
|
|||||||
placeholder: "---------",
|
placeholder: "---------",
|
||||||
theme: "bootstrap",
|
theme: "bootstrap",
|
||||||
templateResult: colorPickerClassCopy,
|
templateResult: colorPickerClassCopy,
|
||||||
templateSelection: colorPickerClassCopy
|
templateSelection: colorPickerClassCopy,
|
||||||
|
width: "off"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Static choice selection
|
// Static choice selection
|
||||||
$('.netbox-select2-static').select2({
|
$('.netbox-select2-static').select2({
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
placeholder: "---------",
|
placeholder: "---------",
|
||||||
theme: "bootstrap"
|
theme: "bootstrap",
|
||||||
|
width: "off"
|
||||||
});
|
});
|
||||||
|
|
||||||
// API backed selection
|
// API backed selection
|
||||||
@ -120,6 +122,7 @@ $(document).ready(function() {
|
|||||||
allowClear: true,
|
allowClear: true,
|
||||||
placeholder: "---------",
|
placeholder: "---------",
|
||||||
theme: "bootstrap",
|
theme: "bootstrap",
|
||||||
|
width: "off",
|
||||||
ajax: {
|
ajax: {
|
||||||
delay: 500,
|
delay: 500,
|
||||||
|
|
||||||
@ -299,7 +302,8 @@ $(document).ready(function() {
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
placeholder: "Tags",
|
placeholder: "Tags",
|
||||||
|
theme: "bootstrap",
|
||||||
|
width: "off",
|
||||||
ajax: {
|
ajax: {
|
||||||
delay: 250,
|
delay: 250,
|
||||||
url: netbox_api_path + "extras/tags/",
|
url: netbox_api_path + "extras/tags/",
|
||||||
|
@ -285,6 +285,8 @@ class APISelect(SelectWithDisabled):
|
|||||||
name of the query param and the value if the query param's value.
|
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.
|
: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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
9
netbox/utilities/templates/widgets/select_api.html
Normal file
9
netbox/utilities/templates/widgets/select_api.html
Normal 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>
|
@ -36,6 +36,27 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='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(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
label='Parent group (ID)',
|
label='Parent group (ID)',
|
||||||
@ -56,16 +77,6 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Cluster type (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()
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -173,6 +173,29 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
|||||||
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
q = forms.CharField(required=False, label='Search')
|
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(
|
type = FilterChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -193,17 +216,6 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
null_option=True,
|
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):
|
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
@ -563,7 +575,9 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
|||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
api_url='/api/dcim/regions/',
|
api_url='/api/dcim/regions/',
|
||||||
value_field="slug",
|
value_field="slug",
|
||||||
null_option=True,
|
filter_for={
|
||||||
|
'site': 'region'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
|
Loading…
Reference in New Issue
Block a user