mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* Initial work on saved filters * Return only enabled/shared filters * Add tests * Clean up filtering of usable SavedFilters
This commit is contained in:
@ -20,7 +20,7 @@ __all__ = (
|
|||||||
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('ASN', ('asn',)),
|
('ASN', ('asn',)),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
@ -59,7 +59,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = ProviderNetwork
|
model = ProviderNetwork
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('provider_id', 'service_id')),
|
('Attributes', ('provider_id', 'service_id')),
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
@ -82,7 +82,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Provider', ('provider_id', 'provider_network_id')),
|
('Provider', ('provider_id', 'provider_network_id')),
|
||||||
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
|
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
|
@ -116,7 +116,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Region
|
model = Region
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'parent_id')),
|
(None, ('q', 'filter', 'tag', 'parent_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@ -130,7 +130,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'parent_id')),
|
(None, ('q', 'filter', 'tag', 'parent_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
parent_id = DynamicModelMultipleChoiceField(
|
parent_id = DynamicModelMultipleChoiceField(
|
||||||
@ -144,7 +144,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Site
|
model = Site
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
@ -174,7 +174,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Location
|
model = Location
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
|
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
@ -222,7 +222,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Function', ('status', 'role_id')),
|
('Function', ('status', 'role_id')),
|
||||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
@ -306,7 +306,7 @@ class RackElevationFilterForm(RackFilterForm):
|
|||||||
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('User', ('user_id',)),
|
('User', ('user_id',)),
|
||||||
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -362,7 +362,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
@ -371,7 +371,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
|
||||||
('Images', ('has_front_image', 'has_rear_image')),
|
('Images', ('has_front_image', 'has_rear_image')),
|
||||||
('Components', (
|
('Components', (
|
||||||
@ -486,7 +486,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Hardware', ('manufacturer_id', 'part_number')),
|
('Hardware', ('manufacturer_id', 'part_number')),
|
||||||
('Components', (
|
('Components', (
|
||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||||
@ -578,7 +578,7 @@ class DeviceFilterForm(
|
|||||||
):
|
):
|
||||||
model = Device
|
model = Device
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
|
||||||
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
|
||||||
@ -731,7 +731,7 @@ class DeviceFilterForm(
|
|||||||
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Module
|
model = Module
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
|
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
|
||||||
)
|
)
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
@ -761,7 +761,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
|
|||||||
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -790,7 +790,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Cable
|
model = Cable
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
|
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
|
||||||
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
|
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -862,7 +862,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = PowerPanel
|
model = PowerPanel
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
@ -900,7 +900,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
|
||||||
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
|
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
|
||||||
)
|
)
|
||||||
@ -1002,7 +1002,7 @@ class PathEndpointFilterForm(CabledFilterForm):
|
|||||||
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
@ -1021,7 +1021,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
@ -1040,7 +1040,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
@ -1055,7 +1055,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Connection', ('cabled', 'connected', 'occupied')),
|
('Connection', ('cabled', 'connected', 'occupied')),
|
||||||
@ -1070,7 +1070,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
|
||||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
@ -1159,7 +1159,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
|
|
||||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Cable', ('cabled', 'occupied')),
|
('Cable', ('cabled', 'occupied')),
|
||||||
@ -1178,7 +1178,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
('Cable', ('cabled', 'occupied')),
|
('Cable', ('cabled', 'occupied')),
|
||||||
@ -1196,7 +1196,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'position')),
|
('Attributes', ('name', 'label', 'position')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
@ -1209,7 +1209,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label')),
|
('Attributes', ('name', 'label')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
@ -1219,7 +1219,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
|
@ -13,6 +13,7 @@ __all__ = [
|
|||||||
'NestedImageAttachmentSerializer',
|
'NestedImageAttachmentSerializer',
|
||||||
'NestedJobResultSerializer',
|
'NestedJobResultSerializer',
|
||||||
'NestedJournalEntrySerializer',
|
'NestedJournalEntrySerializer',
|
||||||
|
'NestedSavedFilterSerializer',
|
||||||
'NestedTagSerializer', # Defined in netbox.api.serializers
|
'NestedTagSerializer', # Defined in netbox.api.serializers
|
||||||
'NestedWebhookSerializer',
|
'NestedWebhookSerializer',
|
||||||
]
|
]
|
||||||
@ -58,6 +59,14 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSavedFilterSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.SavedFilter
|
||||||
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ __all__ = (
|
|||||||
'ReportDetailSerializer',
|
'ReportDetailSerializer',
|
||||||
'ReportSerializer',
|
'ReportSerializer',
|
||||||
'ReportInputSerializer',
|
'ReportInputSerializer',
|
||||||
|
'SavedFilterSerializer',
|
||||||
'ScriptDetailSerializer',
|
'ScriptDetailSerializer',
|
||||||
'ScriptInputSerializer',
|
'ScriptInputSerializer',
|
||||||
'ScriptLogMessageSerializer',
|
'ScriptLogMessageSerializer',
|
||||||
@ -149,6 +150,25 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||||
|
content_types = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'content_types', 'name', 'description', 'user', 'weight',
|
||||||
|
'enabled', 'shared', 'parameters', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -5,43 +5,19 @@ from . import views
|
|||||||
router = NetBoxRouter()
|
router = NetBoxRouter()
|
||||||
router.APIRootView = views.ExtrasRootView
|
router.APIRootView = views.ExtrasRootView
|
||||||
|
|
||||||
# Webhooks
|
|
||||||
router.register('webhooks', views.WebhookViewSet)
|
router.register('webhooks', views.WebhookViewSet)
|
||||||
|
|
||||||
# Custom fields
|
|
||||||
router.register('custom-fields', views.CustomFieldViewSet)
|
router.register('custom-fields', views.CustomFieldViewSet)
|
||||||
|
|
||||||
# Custom links
|
|
||||||
router.register('custom-links', views.CustomLinkViewSet)
|
router.register('custom-links', views.CustomLinkViewSet)
|
||||||
|
|
||||||
# Export templates
|
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
|
router.register('saved-filters', views.SavedFilterViewSet)
|
||||||
# Tags
|
|
||||||
router.register('tags', views.TagViewSet)
|
router.register('tags', views.TagViewSet)
|
||||||
|
|
||||||
# Image attachments
|
|
||||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||||
|
|
||||||
# Journal entries
|
|
||||||
router.register('journal-entries', views.JournalEntryViewSet)
|
router.register('journal-entries', views.JournalEntryViewSet)
|
||||||
|
|
||||||
# Config contexts
|
|
||||||
router.register('config-contexts', views.ConfigContextViewSet)
|
router.register('config-contexts', views.ConfigContextViewSet)
|
||||||
|
|
||||||
# Reports
|
|
||||||
router.register('reports', views.ReportViewSet, basename='report')
|
router.register('reports', views.ReportViewSet, basename='report')
|
||||||
|
|
||||||
# Scripts
|
|
||||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||||
|
|
||||||
# Change logging
|
|
||||||
router.register('object-changes', views.ObjectChangeViewSet)
|
router.register('object-changes', views.ObjectChangeViewSet)
|
||||||
|
|
||||||
# Job Results
|
|
||||||
router.register('job-results', views.JobResultViewSet)
|
router.register('job-results', views.JobResultViewSet)
|
||||||
|
|
||||||
# ContentTypes
|
|
||||||
router.register('content-types', views.ContentTypeViewSet)
|
router.register('content-types', views.ContentTypeViewSet)
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -98,6 +99,17 @@ class ExportTemplateViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.ExportTemplateFilterSet
|
filterset_class = filtersets.ExportTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterViewSet(NetBoxModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
serializer_class = serializers.SavedFilterSerializer
|
||||||
|
filterset_class = filtersets.SavedFilterFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -23,6 +23,7 @@ __all__ = (
|
|||||||
'JournalEntryFilterSet',
|
'JournalEntryFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
|
'SavedFilterFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
'WebhookFilterSet',
|
'WebhookFilterSet',
|
||||||
)
|
)
|
||||||
@ -138,6 +139,55 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
|
content_types = ContentTypeFilter()
|
||||||
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
label='User (ID)',
|
||||||
|
)
|
||||||
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='user__username',
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
to_field_name='username',
|
||||||
|
label='User (name)',
|
||||||
|
)
|
||||||
|
usable = django_filters.BooleanFilter(
|
||||||
|
method='_usable'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = ['id', 'content_types', 'name', 'description', 'enabled', 'shared', 'weight']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _usable(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Return only SavedFilters that are both enabled and are shared (or belong to the current user).
|
||||||
|
"""
|
||||||
|
user = self.request.user if self.request else None
|
||||||
|
if not user or user.is_anonymous:
|
||||||
|
if value:
|
||||||
|
return queryset.filter(enabled=True, shared=True)
|
||||||
|
return queryset.filter(Q(enabled=False) | Q(shared=False))
|
||||||
|
if value:
|
||||||
|
return queryset.filter(enabled=True).filter(Q(shared=True) | Q(user=user))
|
||||||
|
return queryset.filter(Q(enabled=False) | Q(Q(shared=False) & ~Q(user=user)))
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -2,6 +2,6 @@ from .model_forms import *
|
|||||||
from .filtersets import *
|
from .filtersets import *
|
||||||
from .bulk_edit import *
|
from .bulk_edit import *
|
||||||
from .bulk_import import *
|
from .bulk_import import *
|
||||||
from .customfields import *
|
from .mixins import *
|
||||||
from .config import *
|
from .config import *
|
||||||
from .scripts import *
|
from .scripts import *
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
|
add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, StaticSelect,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -14,6 +12,7 @@ __all__ = (
|
|||||||
'CustomLinkBulkEditForm',
|
'CustomLinkBulkEditForm',
|
||||||
'ExportTemplateBulkEditForm',
|
'ExportTemplateBulkEditForm',
|
||||||
'JournalEntryBulkEditForm',
|
'JournalEntryBulkEditForm',
|
||||||
|
'SavedFilterBulkEditForm',
|
||||||
'TagBulkEditForm',
|
'TagBulkEditForm',
|
||||||
'WebhookBulkEditForm',
|
'WebhookBulkEditForm',
|
||||||
)
|
)
|
||||||
@ -96,6 +95,30 @@ class ExportTemplateBulkEditForm(BulkEditForm):
|
|||||||
nullable_fields = ('description', 'mime_type', 'file_extension')
|
nullable_fields = ('description', 'mime_type', 'file_extension')
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkEditForm(BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=SavedFilter.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=200,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
shared = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
)
|
||||||
|
|
||||||
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class WebhookBulkEditForm(BulkEditForm):
|
class WebhookBulkEditForm(BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Webhook.objects.all(),
|
queryset=Webhook.objects.all(),
|
||||||
|
@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'CustomFieldCSVForm',
|
'CustomFieldCSVForm',
|
||||||
'CustomLinkCSVForm',
|
'CustomLinkCSVForm',
|
||||||
'ExportTemplateCSVForm',
|
'ExportTemplateCSVForm',
|
||||||
|
'SavedFilterCSVForm',
|
||||||
'TagCSVForm',
|
'TagCSVForm',
|
||||||
'WebhookCSVForm',
|
'WebhookCSVForm',
|
||||||
)
|
)
|
||||||
@ -81,6 +82,19 @@ class ExportTemplateCSVForm(CSVModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterCSVForm(CSVModelForm):
|
||||||
|
content_types = CSVMultipleContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
help_text="One or more assigned object types"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
fields = (
|
||||||
|
'name', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookCSVForm(CSVModelForm):
|
class WebhookCSVForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -15,6 +15,7 @@ from utilities.forms import (
|
|||||||
StaticSelect, TagFilterField,
|
StaticSelect, TagFilterField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
from .mixins import SavedFiltersMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextFilterForm',
|
'ConfigContextFilterForm',
|
||||||
@ -25,14 +26,15 @@ __all__ = (
|
|||||||
'JournalEntryFilterForm',
|
'JournalEntryFilterForm',
|
||||||
'LocalConfigContextFilterForm',
|
'LocalConfigContextFilterForm',
|
||||||
'ObjectChangeFilterForm',
|
'ObjectChangeFilterForm',
|
||||||
|
'SavedFilterFilterForm',
|
||||||
'TagFilterForm',
|
'TagFilterForm',
|
||||||
'WebhookFilterForm',
|
'WebhookFilterForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterForm(FilterForm):
|
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
|
('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
@ -66,9 +68,9 @@ class CustomFieldFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class JobResultFilterForm(FilterForm):
|
class JobResultFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
('Attributes', ('obj_type', 'status')),
|
('Attributes', ('obj_type', 'status')),
|
||||||
('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after',
|
('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after',
|
||||||
'scheduled_time__before', 'scheduled_time__after', 'user')),
|
'scheduled_time__before', 'scheduled_time__after', 'user')),
|
||||||
@ -118,9 +120,9 @@ class JobResultFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
|
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
@ -145,9 +147,9 @@ class CustomLinkFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateFilterForm(FilterForm):
|
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
|
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
@ -170,9 +172,36 @@ class ExportTemplateFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookFilterForm(FilterForm):
|
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
|
('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
|
||||||
|
)
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=FeatureQuery('export_templates'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
enabled = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
shared = forms.NullBooleanField(
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect(
|
||||||
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
weight = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter')),
|
||||||
('Attributes', ('content_type_id', 'http_method', 'enabled')),
|
('Attributes', ('content_type_id', 'http_method', 'enabled')),
|
||||||
('Events', ('type_create', 'type_update', 'type_delete')),
|
('Events', ('type_create', 'type_update', 'type_delete')),
|
||||||
)
|
)
|
||||||
@ -213,7 +242,7 @@ class WebhookFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TagFilterForm(FilterForm):
|
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
model = Tag
|
model = Tag
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||||
@ -222,9 +251,9 @@ class TagFilterForm(FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextFilterForm(FilterForm):
|
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag_id')),
|
(None, ('q', 'filter', 'tag_id')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
('Device', ('device_type_id', 'platform_id', 'role_id')),
|
('Device', ('device_type_id', 'platform_id', 'role_id')),
|
||||||
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
|
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
|
||||||
@ -311,7 +340,7 @@ class LocalConfigContextFilterForm(forms.Form):
|
|||||||
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = JournalEntry
|
model = JournalEntry
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Creation', ('created_before', 'created_after', 'created_by_id')),
|
('Creation', ('created_before', 'created_after', 'created_by_id')),
|
||||||
('Attributes', ('assigned_object_type_id', 'kind'))
|
('Attributes', ('assigned_object_type_id', 'kind'))
|
||||||
)
|
)
|
||||||
@ -349,10 +378,10 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ObjectChangeFilterForm(FilterForm):
|
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q', 'filter')),
|
||||||
('Time', ('time_before', 'time_after')),
|
('Time', ('time_before', 'time_after')),
|
||||||
('Attributes', ('action', 'user_id', 'changed_object_type_id')),
|
('Attributes', ('action', 'user_id', 'changed_object_type_id')),
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django import forms
|
||||||
|
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.choices import CustomFieldVisibilityChoices
|
from extras.choices import CustomFieldVisibilityChoices
|
||||||
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
|
'SavedFiltersMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -57,3 +60,14 @@ class CustomFieldsMixin:
|
|||||||
if customfield.group_name not in self.custom_field_groups:
|
if customfield.group_name not in self.custom_field_groups:
|
||||||
self.custom_field_groups[customfield.group_name] = []
|
self.custom_field_groups[customfield.group_name] = []
|
||||||
self.custom_field_groups[customfield.group_name].append(field_name)
|
self.custom_field_groups[customfield.group_name].append(field_name)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFiltersMixin(forms.Form):
|
||||||
|
filter = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=SavedFilter.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Saved Filter',
|
||||||
|
query_params={
|
||||||
|
'usable': True,
|
||||||
|
}
|
||||||
|
)
|
@ -1,5 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http import QueryDict
|
||||||
|
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
@ -20,6 +21,7 @@ __all__ = (
|
|||||||
'ExportTemplateForm',
|
'ExportTemplateForm',
|
||||||
'ImageAttachmentForm',
|
'ImageAttachmentForm',
|
||||||
'JournalEntryForm',
|
'JournalEntryForm',
|
||||||
|
'SavedFilterForm',
|
||||||
'TagForm',
|
'TagForm',
|
||||||
'WebhookForm',
|
'WebhookForm',
|
||||||
)
|
)
|
||||||
@ -108,6 +110,34 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
|
queryset=ContentType.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Saved Filter', ('name', 'content_types', 'description', 'weight', 'enabled', 'shared')),
|
||||||
|
('Parameters', ('parameters',)),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SavedFilter
|
||||||
|
exclude = ('user',)
|
||||||
|
widgets = {
|
||||||
|
'parameters': forms.Textarea(attrs={'class': 'font-monospace'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
|
||||||
|
# Convert any parameters delivered via initial data to a dictionary
|
||||||
|
if initial and 'parameters' in initial:
|
||||||
|
if type(initial['parameters']) is str:
|
||||||
|
# TODO: Make a utility function for this
|
||||||
|
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
|
||||||
|
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
class WebhookForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -20,6 +20,9 @@ class ExtrasQuery(graphene.ObjectType):
|
|||||||
image_attachment = ObjectField(ImageAttachmentType)
|
image_attachment = ObjectField(ImageAttachmentType)
|
||||||
image_attachment_list = ObjectListField(ImageAttachmentType)
|
image_attachment_list = ObjectListField(ImageAttachmentType)
|
||||||
|
|
||||||
|
saved_filter = ObjectField(SavedFilterType)
|
||||||
|
saved_filter_list = ObjectListField(SavedFilterType)
|
||||||
|
|
||||||
journal_entry = ObjectField(JournalEntryType)
|
journal_entry = ObjectField(JournalEntryType)
|
||||||
journal_entry_list = ObjectListField(JournalEntryType)
|
journal_entry_list = ObjectListField(JournalEntryType)
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ __all__ = (
|
|||||||
'ImageAttachmentType',
|
'ImageAttachmentType',
|
||||||
'JournalEntryType',
|
'JournalEntryType',
|
||||||
'ObjectChangeType',
|
'ObjectChangeType',
|
||||||
|
'SavedFilterType',
|
||||||
'TagType',
|
'TagType',
|
||||||
'WebhookType',
|
'WebhookType',
|
||||||
)
|
)
|
||||||
@ -71,6 +72,14 @@ class ObjectChangeType(BaseObjectType):
|
|||||||
filterset_class = filtersets.ObjectChangeFilterSet
|
filterset_class = filtersets.ObjectChangeFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterType(ObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.SavedFilter
|
||||||
|
exclude = ('content_types', )
|
||||||
|
filterset_class = filtersets.SavedFilterFilterSet
|
||||||
|
|
||||||
|
|
||||||
class TagType(ObjectType):
|
class TagType(ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
36
netbox/extras/migrations/0083_savedfilter.py
Normal file
36
netbox/extras/migrations/0083_savedfilter.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.1.1 on 2022-10-27 18:18
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0082_exporttemplate_content_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SavedFilter',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('weight', models.PositiveSmallIntegerField(default=100)),
|
||||||
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
('shared', models.BooleanField(default=True)),
|
||||||
|
('parameters', models.JSONField()),
|
||||||
|
('content_types', models.ManyToManyField(related_name='saved_filters', to='contenttypes.contenttype')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('weight', 'name'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -18,6 +18,7 @@ __all__ = (
|
|||||||
'JournalEntry',
|
'JournalEntry',
|
||||||
'ObjectChange',
|
'ObjectChange',
|
||||||
'Report',
|
'Report',
|
||||||
|
'SavedFilter',
|
||||||
'Script',
|
'Script',
|
||||||
'Tag',
|
'Tag',
|
||||||
'TaggedItem',
|
'TaggedItem',
|
||||||
|
@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, QueryDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'JobResult',
|
'JobResult',
|
||||||
'JournalEntry',
|
'JournalEntry',
|
||||||
'Report',
|
'Report',
|
||||||
|
'SavedFilter',
|
||||||
'Script',
|
'Script',
|
||||||
'Webhook',
|
'Webhook',
|
||||||
)
|
)
|
||||||
@ -350,6 +351,69 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
||||||
|
"""
|
||||||
|
A set of predefined keyword parameters that can be reused to filter for specific objects.
|
||||||
|
"""
|
||||||
|
content_types = models.ManyToManyField(
|
||||||
|
to=ContentType,
|
||||||
|
related_name='saved_filters',
|
||||||
|
help_text='The object type(s) to which this filter applies.'
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
to=User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100
|
||||||
|
)
|
||||||
|
enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
shared = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
parameters = models.JSONField()
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'enabled', 'weight',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('weight', 'name')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:savedfilter', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Verify that `parameters` is a JSON object
|
||||||
|
if type(self.parameters) is not dict:
|
||||||
|
raise ValidationError(
|
||||||
|
{'parameters': 'Filter parameters must be stored as a dictionary of keyword arguments.'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_params(self):
|
||||||
|
qd = QueryDict(mutable=True)
|
||||||
|
qd.update(self.parameters)
|
||||||
|
return qd.urlencode()
|
||||||
|
|
||||||
|
|
||||||
class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
An uploaded image which is associated with an object.
|
An uploaded image which is associated with an object.
|
||||||
|
@ -13,16 +13,13 @@ __all__ = (
|
|||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
'JournalEntryTable',
|
'JournalEntryTable',
|
||||||
'ObjectChangeTable',
|
'ObjectChangeTable',
|
||||||
|
'SavedFilterTable',
|
||||||
'TaggedItemTable',
|
'TaggedItemTable',
|
||||||
'TagTable',
|
'TagTable',
|
||||||
'WebhookTable',
|
'WebhookTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomFieldTable(NetBoxTable):
|
class CustomFieldTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -40,10 +37,6 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom fields
|
|
||||||
#
|
|
||||||
|
|
||||||
class JobResultTable(NetBoxTable):
|
class JobResultTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -61,10 +54,6 @@ class JobResultTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
|
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Custom links
|
|
||||||
#
|
|
||||||
|
|
||||||
class CustomLinkTable(NetBoxTable):
|
class CustomLinkTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -82,10 +71,6 @@ class CustomLinkTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
|
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Export templates
|
|
||||||
#
|
|
||||||
|
|
||||||
class ExportTemplateTable(NetBoxTable):
|
class ExportTemplateTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
@ -104,9 +89,24 @@ class ExportTemplateTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
class SavedFilterTable(NetBoxTable):
|
||||||
# Webhooks
|
name = tables.Column(
|
||||||
#
|
linkify=True
|
||||||
|
)
|
||||||
|
content_types = columns.ContentTypesColumn()
|
||||||
|
enabled = columns.BooleanColumn()
|
||||||
|
shared = columns.BooleanColumn()
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = SavedFilter
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared',
|
||||||
|
'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebhookTable(NetBoxTable):
|
class WebhookTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
@ -139,10 +139,6 @@ class WebhookTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Tags
|
|
||||||
#
|
|
||||||
|
|
||||||
class TagTable(NetBoxTable):
|
class TagTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -3,7 +3,6 @@ from unittest import skipIf
|
|||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
@ -17,7 +16,6 @@ from extras.reports import Report
|
|||||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
|
|
||||||
rq_worker_running = Worker.count(get_connection('default'))
|
rq_worker_running = Worker.count(get_connection('default'))
|
||||||
|
|
||||||
|
|
||||||
@ -192,6 +190,73 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
custom_link.content_types.set([site_ct])
|
custom_link.content_types.set([site_ct])
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = SavedFilter
|
||||||
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
|
create_data = [
|
||||||
|
{
|
||||||
|
'content_types': ['dcim.site'],
|
||||||
|
'name': 'Saved Filter 4',
|
||||||
|
'weight': 100,
|
||||||
|
'enabled': True,
|
||||||
|
'shared': True,
|
||||||
|
'parameters': {'status': ['active']},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'content_types': ['dcim.site'],
|
||||||
|
'name': 'Saved Filter 5',
|
||||||
|
'weight': 200,
|
||||||
|
'enabled': True,
|
||||||
|
'shared': True,
|
||||||
|
'parameters': {'status': ['planned']},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'content_types': ['dcim.site'],
|
||||||
|
'name': 'Saved Filter 6',
|
||||||
|
'weight': 300,
|
||||||
|
'enabled': True,
|
||||||
|
'shared': True,
|
||||||
|
'parameters': {'status': ['retired']},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
bulk_update_data = {
|
||||||
|
'weight': 1000,
|
||||||
|
'enabled': False,
|
||||||
|
'shared': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
|
||||||
|
saved_filters = (
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 1',
|
||||||
|
weight=100,
|
||||||
|
enabled=True,
|
||||||
|
shared=True,
|
||||||
|
parameters={'status': ['active']}
|
||||||
|
),
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 2',
|
||||||
|
weight=200,
|
||||||
|
enabled=True,
|
||||||
|
shared=True,
|
||||||
|
parameters={'status': ['planned']}
|
||||||
|
),
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 3',
|
||||||
|
weight=300,
|
||||||
|
enabled=True,
|
||||||
|
shared=True,
|
||||||
|
parameters={'status': ['retired']}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
|
for i, savedfilter in enumerate(saved_filters):
|
||||||
|
savedfilter.content_types.set([site_ct])
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
|
@ -222,6 +222,92 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = SavedFilterFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||||
|
|
||||||
|
users = (
|
||||||
|
User(username='User 1'),
|
||||||
|
User(username='User 2'),
|
||||||
|
User(username='User 3'),
|
||||||
|
)
|
||||||
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
|
saved_filters = (
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 1',
|
||||||
|
user=users[0],
|
||||||
|
weight=100,
|
||||||
|
enabled=True,
|
||||||
|
shared=True,
|
||||||
|
parameters={'status': ['active']}
|
||||||
|
),
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 2',
|
||||||
|
user=users[1],
|
||||||
|
weight=200,
|
||||||
|
enabled=True,
|
||||||
|
shared=True,
|
||||||
|
parameters={'status': ['planned']}
|
||||||
|
),
|
||||||
|
SavedFilter(
|
||||||
|
name='Saved Filter 3',
|
||||||
|
user=users[2],
|
||||||
|
weight=300,
|
||||||
|
enabled=False,
|
||||||
|
shared=False,
|
||||||
|
parameters={'status': ['retired']}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
|
for i, savedfilter in enumerate(saved_filters):
|
||||||
|
savedfilter.content_types.set([content_types[i]])
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Saved Filter 1', 'Saved Filter 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_types(self):
|
||||||
|
params = {'content_types': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_user(self):
|
||||||
|
users = User.objects.filter(username__startswith='User')
|
||||||
|
params = {'user': [users[0].username, users[1].username]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'user_id': [users[0].pk, users[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [100, 200]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_enabled(self):
|
||||||
|
params = {'enabled': True}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'enabled': False}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_shared(self):
|
||||||
|
params = {'enabled': True}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'enabled': False}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_usable(self):
|
||||||
|
# Filtering for an anonymous user
|
||||||
|
params = {'usable': True}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'usable': False}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
filterset = ExportTemplateFilterSet
|
filterset = ExportTemplateFilterSet
|
||||||
|
@ -107,6 +107,58 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
model = SavedFilter
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
|
||||||
|
users = (
|
||||||
|
User(username='User 1'),
|
||||||
|
User(username='User 2'),
|
||||||
|
User(username='User 3'),
|
||||||
|
)
|
||||||
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
|
saved_filters = (
|
||||||
|
SavedFilter(name='Saved Filter 1', user=users[0], weight=100, parameters={'status': ['active']}),
|
||||||
|
SavedFilter(name='Saved Filter 2', user=users[1], weight=200, parameters={'status': ['planned']}),
|
||||||
|
SavedFilter(name='Saved Filter 3', user=users[2], weight=300, parameters={'status': ['retired']}),
|
||||||
|
)
|
||||||
|
SavedFilter.objects.bulk_create(saved_filters)
|
||||||
|
for i, savedfilter in enumerate(saved_filters):
|
||||||
|
savedfilter.content_types.set([site_ct])
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'name': 'Saved Filter X',
|
||||||
|
'content_types': [site_ct.pk],
|
||||||
|
'description': 'Foo',
|
||||||
|
'weight': 1000,
|
||||||
|
'enabled': True,
|
||||||
|
'shared': True,
|
||||||
|
'parameters': '{"foo": 123}',
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
'name,content_types,weight,enabled,shared,parameters',
|
||||||
|
'Saved Filter 4,dcim.device,400,True,True,{"foo": "a"}',
|
||||||
|
'Saved Filter 5,dcim.device,500,True,True,{"foo": "b"}',
|
||||||
|
'Saved Filter 6,dcim.device,600,True,True,{"foo": "c"}',
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,name",
|
||||||
|
f"{saved_filters[0].pk},Saved Filter 7",
|
||||||
|
f"{saved_filters[1].pk},Saved Filter 8",
|
||||||
|
f"{saved_filters[2].pk},Saved Filter 9",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'weight': 999,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = ExportTemplate
|
model = ExportTemplate
|
||||||
|
|
||||||
|
@ -31,6 +31,14 @@ urlpatterns = [
|
|||||||
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
|
||||||
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
path('export-templates/<int:pk>/', include(get_model_urls('extras', 'exporttemplate'))),
|
||||||
|
|
||||||
|
# Saved filters
|
||||||
|
path('saved-filters/', views.SavedFilterListView.as_view(), name='savedfilter_list'),
|
||||||
|
path('saved-filters/add/', views.SavedFilterEditView.as_view(), name='savedfilter_add'),
|
||||||
|
path('saved-filters/import/', views.SavedFilterBulkImportView.as_view(), name='savedfilter_import'),
|
||||||
|
path('saved-filters/edit/', views.SavedFilterBulkEditView.as_view(), name='savedfilter_bulk_edit'),
|
||||||
|
path('saved-filters/delete/', views.SavedFilterBulkDeleteView.as_view(), name='savedfilter_bulk_delete'),
|
||||||
|
path('saved-filters/<int:pk>/', include(get_model_urls('extras', 'savedfilter'))),
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
|
||||||
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
|
||||||
|
@ -9,7 +9,6 @@ from django_rq.queues import get_connection
|
|||||||
from rq import Worker
|
from rq import Worker
|
||||||
|
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||||
@ -159,6 +158,74 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.ExportTemplateTable
|
table = tables.ExportTemplateTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Saved filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class SavedFilterMixin:
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""
|
||||||
|
Return only shared SavedFilters, or those owned by the current user, unless
|
||||||
|
this is a superuser.
|
||||||
|
"""
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
user = request.user
|
||||||
|
if user.is_superuser:
|
||||||
|
return queryset
|
||||||
|
if user.is_anonymous:
|
||||||
|
return queryset.filter(shared=True)
|
||||||
|
return queryset.filter(
|
||||||
|
Q(shared=True) | Q(user=user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterListView(SavedFilterMixin, generic.ObjectListView):
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
filterset_form = forms.SavedFilterFilterForm
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter)
|
||||||
|
class SavedFilterView(SavedFilterMixin, generic.ObjectView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter, 'edit')
|
||||||
|
class SavedFilterEditView(SavedFilterMixin, generic.ObjectEditView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
form = forms.SavedFilterForm
|
||||||
|
|
||||||
|
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||||
|
if not obj.pk:
|
||||||
|
obj.user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(SavedFilter, 'delete')
|
||||||
|
class SavedFilterDeleteView(SavedFilterMixin, generic.ObjectDeleteView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkImportView(SavedFilterMixin, generic.BulkImportView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
model_form = forms.SavedFilterCSVForm
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkEditView(SavedFilterMixin, generic.BulkEditView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
form = forms.SavedFilterBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class SavedFilterBulkDeleteView(SavedFilterMixin, generic.BulkDeleteView):
|
||||||
|
queryset = SavedFilter.objects.all()
|
||||||
|
filterset = filtersets.SavedFilterFilterSet
|
||||||
|
table = tables.SavedFilterTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
|
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
|
||||||
@ -11,7 +10,7 @@ from netbox.forms import NetBoxModelFilterSetForm
|
|||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple,
|
MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
|||||||
class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Route Targets', ('import_target_id', 'export_target_id')),
|
('Route Targets', ('import_target_id', 'export_target_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -66,7 +65,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = RouteTarget
|
model = RouteTarget
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('VRF', ('importing_vrf_id', 'exporting_vrf_id')),
|
('VRF', ('importing_vrf_id', 'exporting_vrf_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -98,7 +97,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('family', 'rir_id')),
|
('Attributes', ('family', 'rir_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -119,7 +118,7 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = ASN
|
model = ASN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Assignment', ('rir_id', 'site_id')),
|
('Assignment', ('rir_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -144,7 +143,7 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
|
('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
|
||||||
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
@ -233,7 +232,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')),
|
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -265,7 +264,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
|
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
|
||||||
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
('VRF', ('vrf_id', 'present_in_vrf_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -334,7 +333,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
|
class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = FHRPGroup
|
model = FHRPGroup
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('name', 'protocol', 'group_id')),
|
('Attributes', ('name', 'protocol', 'group_id')),
|
||||||
('Authentication', ('auth_type', 'auth_key')),
|
('Authentication', ('auth_type', 'auth_key')),
|
||||||
)
|
)
|
||||||
@ -364,7 +363,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
|
|||||||
|
|
||||||
class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region', 'sitegroup', 'site', 'location', 'rack')),
|
('Location', ('region', 'sitegroup', 'site', 'location', 'rack')),
|
||||||
('VLAN ID', ('min_vid', 'max_vid')),
|
('VLAN ID', ('min_vid', 'max_vid')),
|
||||||
)
|
)
|
||||||
@ -412,7 +411,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attributes', ('group_id', 'status', 'role_id', 'vid')),
|
('Attributes', ('group_id', 'status', 'role_id', 'vid')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -465,7 +464,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = ServiceTemplate
|
model = ServiceTemplate
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('protocol', 'port')),
|
('Attributes', ('protocol', 'port')),
|
||||||
)
|
)
|
||||||
protocol = forms.ChoiceField(
|
protocol = forms.ChoiceField(
|
||||||
@ -486,7 +485,7 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
|
|||||||
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = L2VPN
|
model = L2VPN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('type', 'import_target_id', 'export_target_id')),
|
('Attributes', ('type', 'import_target_id', 'export_target_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -511,8 +510,10 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = L2VPNTermination
|
model = L2VPNTermination
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('l2vpn_id', )),
|
(None, ('filter', 'l2vpn_id',)),
|
||||||
('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')),
|
('Assigned Object', (
|
||||||
|
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
l2vpn_id = DynamicModelChoiceField(
|
l2vpn_id = DynamicModelChoiceField(
|
||||||
queryset=L2VPN.objects.all(),
|
queryset=L2VPN.objects.all(),
|
||||||
|
@ -4,10 +4,11 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django_filters.exceptions import FieldLookupError
|
from django_filters.exceptions import FieldLookupError
|
||||||
from django_filters.utils import get_model_field, resolve_field
|
from django_filters.utils import get_model_field, resolve_field
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from extras.choices import CustomFieldFilterLogicChoices
|
from extras.choices import CustomFieldFilterLogicChoices
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField, SavedFilter
|
||||||
from utilities.constants import (
|
from utilities.constants import (
|
||||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||||
@ -80,12 +81,28 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
# bit of a hack for #9231 - extras.lookup.Empty is registered in apps.ready
|
||||||
# however FilterSet Factory is setup before this which creates the
|
# however FilterSet Factory is setup before this which creates the
|
||||||
# initial filters. This recreates the filters so Empty is picked up correctly.
|
# initial filters. This recreates the filters so Empty is picked up correctly.
|
||||||
self.base_filters = self.__class__.get_filters()
|
self.base_filters = self.__class__.get_filters()
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
# Apply any referenced SavedFilters
|
||||||
|
if data and 'filter' in data:
|
||||||
|
data = data.copy() # Get a mutable copy
|
||||||
|
saved_filters = SavedFilter.objects.filter(pk__in=data.pop('filter'))
|
||||||
|
for sf in saved_filters:
|
||||||
|
for key, value in sf.parameters.items():
|
||||||
|
# QueryDicts are... fun
|
||||||
|
if type(value) not in (list, tuple):
|
||||||
|
value = [value]
|
||||||
|
if key in data:
|
||||||
|
for v in value:
|
||||||
|
data.appendlist(key, v)
|
||||||
|
else:
|
||||||
|
data.setlist(key, value)
|
||||||
|
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_filter_lookup_dict(existing_filter):
|
def _get_filter_lookup_dict(existing_filter):
|
||||||
|
@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
|
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
|
||||||
from extras.forms.customfields import CustomFieldsMixin
|
from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
|
||||||
from extras.models import CustomField, Tag
|
from extras.models import CustomField, Tag
|
||||||
from utilities.forms import BootstrapMixin, CSVModelForm
|
from utilities.forms import BootstrapMixin, CSVModelForm
|
||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
@ -114,7 +114,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
|||||||
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
|
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
|
||||||
corresponding FilterSet *must* provide a `q` filter.
|
corresponding FilterSet *must* provide a `q` filter.
|
||||||
@ -129,6 +129,15 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
|
|||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Limit saved filters to those applicable to the form's model
|
||||||
|
content_type = ContentType.objects.get_for_model(self.model)
|
||||||
|
self.fields['filter'].widget.add_query_params({
|
||||||
|
'content_type_id': content_type.pk,
|
||||||
|
})
|
||||||
|
|
||||||
def _get_custom_fields(self, content_type):
|
def _get_custom_fields(self, content_type):
|
||||||
return super()._get_custom_fields(content_type).exclude(
|
return super()._get_custom_fields(content_type).exclude(
|
||||||
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
|
||||||
|
@ -278,6 +278,7 @@ OTHER_MENU = Menu(
|
|||||||
get_model_item('extras', 'customfield', 'Custom Fields'),
|
get_model_item('extras', 'customfield', 'Custom Fields'),
|
||||||
get_model_item('extras', 'customlink', 'Custom Links'),
|
get_model_item('extras', 'customlink', 'Custom Links'),
|
||||||
get_model_item('extras', 'exporttemplate', 'Export Templates'),
|
get_model_item('extras', 'exporttemplate', 'Export Templates'),
|
||||||
|
get_model_item('extras', 'savedfilter', 'Saved Filters'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
@ -4,17 +4,17 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldDoesNotExist, ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ManyToManyField, ProtectedError
|
from django.db.models import ManyToManyField, ProtectedError
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, model_to_dict
|
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django_tables2.export import TableExport
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django_tables2.export import TableExport
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate, SavedFilter
|
||||||
from extras.signals import clear_webhooks
|
from extras.signals import clear_webhooks
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
@ -330,7 +330,6 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||||||
return headers, records
|
return headers, records
|
||||||
|
|
||||||
def _update_objects(self, form, request, headers, records):
|
def _update_objects(self, form, request, headers, records):
|
||||||
from utilities.forms import CSVModelChoiceField
|
|
||||||
updated_objs = []
|
updated_objs = []
|
||||||
|
|
||||||
ids = [int(record["id"]) for record in records]
|
ids = [int(record["id"]) for record in records]
|
||||||
|
70
netbox/templates/extras/savedfilter.html
Normal file
70
netbox/templates/extras/savedfilter.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Saved Filter</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">User</th>
|
||||||
|
<td>{{ object.user|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enabled</th>
|
||||||
|
<td>{% checkmark object.enabled %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Shared</th>
|
||||||
|
<td>{% checkmark object.shared %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Weight</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Assigned Models</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-7">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">
|
||||||
|
Parameters
|
||||||
|
</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre>{{ object.parameters }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -64,7 +64,7 @@ Context:
|
|||||||
|
|
||||||
{# Applied filters #}
|
{# Applied filters #}
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
{% applied_filters filter_form request.GET %}
|
{% applied_filters model filter_form request.GET %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# "Select all" form #}
|
{# "Select all" form #}
|
||||||
|
@ -31,7 +31,7 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag', 'group_id')),
|
(None, ('q', 'filter', 'tag', 'group_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
('Contacts', ('contact', 'contact_role', 'contact_group'))
|
||||||
)
|
)
|
||||||
group_id = DynamicModelMultipleChoiceField(
|
group_id = DynamicModelMultipleChoiceField(
|
||||||
|
@ -10,5 +10,10 @@
|
|||||||
<i class="mdi mdi-tag-off"></i> Clear all
|
<i class="mdi mdi-tag-off"></i> Clear all
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if save_link %}
|
||||||
|
<a href="{{ save_link }}" class="badge rounded-pill bg-success text-decoration-none me-1">
|
||||||
|
<i class="mdi mdi-content-save"></i> Save
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
from urllib.parse import quote
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.defaultfilters import date
|
from django.template.defaultfilters import date
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -278,12 +280,13 @@ def table_config_form(table, table_name=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('helpers/applied_filters.html')
|
@register.inclusion_tag('helpers/applied_filters.html', takes_context=True)
|
||||||
def applied_filters(form, query_params):
|
def applied_filters(context, model, form, query_params):
|
||||||
"""
|
"""
|
||||||
Display the active filters for a given filter form.
|
Display the active filters for a given filter form.
|
||||||
"""
|
"""
|
||||||
form.is_valid()
|
user = context['request'].user
|
||||||
|
form.is_valid() # Ensure cleaned_data has been set
|
||||||
|
|
||||||
applied_filters = []
|
applied_filters = []
|
||||||
for filter_name in form.changed_data:
|
for filter_name in form.changed_data:
|
||||||
@ -305,6 +308,14 @@ def applied_filters(form, query_params):
|
|||||||
'link_text': f'{bound_field.label}: {display_value}',
|
'link_text': f'{bound_field.label}: {display_value}',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
save_link = None
|
||||||
|
if user.has_perm('extras.add_savedfilter') and 'filter' not in context['request'].GET:
|
||||||
|
content_type = ContentType.objects.get_for_model(model).pk
|
||||||
|
parameters = context['request'].GET.urlencode()
|
||||||
|
url = reverse('extras:savedfilter_add')
|
||||||
|
save_link = f"{url}?content_types={content_type}¶meters={quote(parameters)}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'applied_filters': applied_filters,
|
'applied_filters': applied_filters,
|
||||||
|
'save_link': save_link,
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.models import ManyToManyField
|
from django.db.models import ManyToManyField, JSONField
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from django.test import Client, TestCase as _TestCase
|
from django.test import Client, TestCase as _TestCase
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
@ -132,6 +134,10 @@ class ModelTestCase(TestCase):
|
|||||||
if type(instance._meta.get_field(key)) is ArrayField:
|
if type(instance._meta.get_field(key)) is ArrayField:
|
||||||
model_dict[key] = ','.join([str(v) for v in value])
|
model_dict[key] = ','.join([str(v) for v in value])
|
||||||
|
|
||||||
|
# JSON
|
||||||
|
if type(instance._meta.get_field(key)) is JSONField and value is not None:
|
||||||
|
model_dict[key] = json.dumps(value)
|
||||||
|
|
||||||
return model_dict
|
return model_dict
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('group_id', 'type_id', 'status')),
|
('Attributes', ('group_id', 'type_id', 'status')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -90,7 +90,7 @@ class VirtualMachineFilterForm(
|
|||||||
):
|
):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
|
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
||||||
@ -175,7 +175,7 @@ class VirtualMachineFilterForm(
|
|||||||
class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
|
('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
|
||||||
('Attributes', ('enabled', 'mac_address', 'vrf_id')),
|
('Attributes', ('enabled', 'mac_address', 'vrf_id')),
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,7 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm):
|
|||||||
class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('ssid', 'group_id',)),
|
('Attributes', ('ssid', 'group_id',)),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
||||||
@ -62,7 +62,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = WirelessLink
|
model = WirelessLink
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'filter', 'tag')),
|
||||||
('Attributes', ('ssid', 'status',)),
|
('Attributes', ('ssid', 'status',)),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
|
||||||
|
Reference in New Issue
Block a user