00ff00
)'),
@@ -985,7 +986,7 @@ class VirtualChassisCSVForm(NetBoxModelCSVForm):
class Meta:
model = VirtualChassis
- fields = ('name', 'domain', 'master')
+ fields = ('name', 'domain', 'master', 'description')
#
@@ -1006,7 +1007,7 @@ class PowerPanelCSVForm(NetBoxModelCSVForm):
class Meta:
model = PowerPanel
- fields = ('site', 'location', 'name')
+ fields = ('site', 'location', 'name', 'description', 'comments')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
@@ -1062,7 +1063,7 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
model = PowerFeed
fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
- 'voltage', 'amperage', 'max_utilization', 'comments',
+ 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
)
def __init__(self, data=None, *args, **kwargs):
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 031488f43..8fa78cfbf 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -117,7 +117,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region
fieldsets = (
- (None, ('q', 'tag', 'parent_id')),
+ (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
@@ -131,7 +131,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup
fieldsets = (
- (None, ('q', 'tag', 'parent_id')),
+ (None, ('q', 'filter', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
@@ -145,7 +145,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@@ -175,7 +175,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@@ -223,7 +223,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
@@ -307,7 +307,7 @@ class RackElevationFilterForm(RackFilterForm):
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('User', ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@@ -363,7 +363,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
tag = TagFilterField(model)
@@ -372,7 +372,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')),
('Components', (
@@ -487,7 +487,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
@@ -579,7 +579,7 @@ class DeviceFilterForm(
):
model = Device
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
@@ -763,7 +763,7 @@ class VirtualDeviceContextFilterForm(
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
@@ -793,7 +793,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@@ -822,7 +822,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@@ -894,7 +894,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
@@ -932,7 +932,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
model = PowerFeed
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
)
@@ -1034,7 +1034,7 @@ class PathEndpointFilterForm(CabledFilterForm):
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1053,7 +1053,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1072,7 +1072,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1087,7 +1087,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Connection', ('cabled', 'connected', 'occupied')),
@@ -1102,7 +1102,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')),
@@ -1200,7 +1200,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
@@ -1219,7 +1219,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
('Cable', ('cabled', 'occupied')),
@@ -1237,7 +1237,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label', 'position')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
@@ -1250,7 +1250,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'label')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
)
@@ -1260,7 +1260,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('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')),
)
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index d450a25d5..60cd88ffa 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -279,7 +279,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
fields = [
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
- 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'comments', 'tags',
+ 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'description', 'comments', 'tags',
]
help_texts = {
'site': "The site at which the rack exists",
@@ -343,6 +343,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
),
widget=StaticSelect()
)
+ comments = CommentField()
fieldsets = (
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
@@ -353,7 +354,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
model = RackReservation
fields = [
'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
- 'description', 'tags',
+ 'description', 'comments', 'tags',
]
@@ -384,10 +385,10 @@ class DeviceTypeForm(NetBoxModelForm):
fieldsets = (
('Device Type', (
- 'manufacturer', 'model', 'slug', 'part_number', 'tags',
+ 'manufacturer', 'model', 'slug', 'description', 'tags',
)),
('Chassis', (
- 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
+ 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
)),
('Attributes', ('weight', 'weight_unit')),
('Images', ('front_image', 'rear_image')),
@@ -397,7 +398,7 @@ class DeviceTypeForm(NetBoxModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
- 'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
+ 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags',
]
widgets = {
'airflow': StaticSelect(),
@@ -419,15 +420,14 @@ class ModuleTypeForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
- ('Module Type', (
- 'manufacturer', 'model', 'part_number', 'tags', 'weight', 'weight_unit'
- )),
+ ('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
+ ('Weight', ('weight', 'weight_unit'))
)
class Meta:
model = ModuleType
fields = [
- 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
+ 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
]
widgets = {
@@ -592,7 +592,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
- 'comments', 'tags', 'local_context_data'
+ 'description', 'comments', 'tags', 'local_context_data'
]
help_texts = {
'device_role': "The function this device serves",
@@ -706,7 +706,7 @@ class ModuleForm(NetBoxModelForm):
fieldsets = (
('Module', (
- 'device', 'module_bay', 'manufacturer', 'module_type', 'tags',
+ 'device', 'module_bay', 'manufacturer', 'module_type', 'description', 'tags',
)),
('Hardware', (
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
@@ -717,7 +717,7 @@ class ModuleForm(NetBoxModelForm):
model = Module
fields = [
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
- 'replicate_components', 'adopt_components', 'comments',
+ 'replicate_components', 'adopt_components', 'description', 'comments',
]
def __init__(self, *args, **kwargs):
@@ -794,11 +794,13 @@ class ModuleForm(NetBoxModelForm):
class CableForm(TenancyForm, NetBoxModelForm):
+ comments = CommentField()
class Meta:
model = Cable
fields = [
- 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
+ 'comments', 'tags',
]
widgets = {
'status': StaticSelect,
@@ -841,15 +843,16 @@ class PowerPanelForm(NetBoxModelForm):
'site_id': '$site'
}
)
+ comments = CommentField()
fieldsets = (
- ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')),
+ ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'description', 'tags')),
)
class Meta:
model = PowerPanel
fields = [
- 'region', 'site_group', 'site', 'location', 'name', 'tags',
+ 'region', 'site_group', 'site', 'location', 'name', 'description', 'comments', 'tags',
]
@@ -895,7 +898,7 @@ class PowerFeedForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
- ('Power Panel', ('region', 'site', 'power_panel')),
+ ('Power Panel', ('region', 'site', 'power_panel', 'description')),
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
)
@@ -904,7 +907,7 @@ class PowerFeedForm(NetBoxModelForm):
model = PowerFeed
fields = [
'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply',
- 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags',
+ 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
]
widgets = {
'status': StaticSelect(),
@@ -923,11 +926,12 @@ class VirtualChassisForm(NetBoxModelForm):
queryset=Device.objects.all(),
required=False,
)
+ comments = CommentField()
class Meta:
model = VirtualChassis
fields = [
- 'name', 'domain', 'master', 'tags',
+ 'name', 'domain', 'master', 'description', 'comments', 'tags',
]
widgets = {
'master': SelectWithPK(),
diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py
index 023aba8f1..82ee093dd 100644
--- a/netbox/dcim/forms/object_import.py
+++ b/netbox/dcim/forms/object_import.py
@@ -30,7 +30,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
- 'comments',
+ 'description', 'comments',
]
@@ -42,7 +42,7 @@ class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ModuleType
- fields = ['manufacturer', 'model', 'part_number', 'comments']
+ fields = ['manufacturer', 'model', 'part_number', 'description', 'comments']
#
diff --git a/netbox/dcim/migrations/0001_squashed.py b/netbox/dcim/migrations/0001_squashed.py
index fca7d8eb9..3d7156e17 100644
--- a/netbox/dcim/migrations/0001_squashed.py
+++ b/netbox/dcim/migrations/0001_squashed.py
@@ -195,7 +195,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -352,7 +352,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -369,7 +369,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
@@ -538,7 +538,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.CreateModel(
diff --git a/netbox/dcim/migrations/0147_inventoryitemrole.py b/netbox/dcim/migrations/0147_inventoryitemrole.py
index cbdd36c08..4b6c27450 100644
--- a/netbox/dcim/migrations/0147_inventoryitemrole.py
+++ b/netbox/dcim/migrations/0147_inventoryitemrole.py
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
- 'ordering': ['name'],
+ 'ordering': ('name',),
},
),
migrations.AddField(
diff --git a/netbox/dcim/migrations/0165_standardize_description_comments.py b/netbox/dcim/migrations/0165_standardize_description_comments.py
new file mode 100644
index 000000000..f17f1d321
--- /dev/null
+++ b/netbox/dcim/migrations/0165_standardize_description_comments.py
@@ -0,0 +1,78 @@
+# Generated by Django 4.1.2 on 2022-11-03 18:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0164_rack_mounting_depth'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cable',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='cable',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='devicetype',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='module',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='moduletype',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='powerfeed',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='powerpanel',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='powerpanel',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ migrations.AddField(
+ model_name='rackreservation',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='virtualchassis',
+ name='comments',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='virtualchassis',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0165_virtualdevicecontexts.py b/netbox/dcim/migrations/0166_virtualdevicecontexts.py
similarity index 88%
rename from netbox/dcim/migrations/0165_virtualdevicecontexts.py
rename to netbox/dcim/migrations/0166_virtualdevicecontexts.py
index f72061005..1db331bb3 100644
--- a/netbox/dcim/migrations/0165_virtualdevicecontexts.py
+++ b/netbox/dcim/migrations/0166_virtualdevicecontexts.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.1.2 on 2022-11-02 13:24
+# Generated by Django 4.1.2 on 2022-11-04 12:46
from django.db import migrations, models
import django.db.models.deletion
@@ -9,10 +9,10 @@ import utilities.json
class Migration(migrations.Migration):
dependencies = [
- ('ipam', '0062_unique_constraints'),
- ('extras', '0082_exporttemplate_content_types'),
- ('tenancy', '0008_unique_constraints'),
- ('dcim', '0164_rack_mounting_depth'),
+ ('extras', '0083_savedfilter'),
+ ('ipam', '0063_standardize_description_comments'),
+ ('tenancy', '0009_standardize_description_comments'),
+ ('dcim', '0165_standardize_description_comments'),
]
operations = [
@@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
+ ('description', models.CharField(blank=True, max_length=200)),
('name', models.CharField(max_length=64)),
('status', models.CharField(blank=True, max_length=50)),
('identifier', models.PositiveSmallIntegerField(blank=True, null=True)),
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index fad3e8bd6..c51b59f94 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -12,8 +12,8 @@ from django.urls import reverse
from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
-from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
-from netbox.models import NetBoxModel
+from dcim.utils import decompile_path_node, object_to_path_node
+from netbox.models import PrimaryModel
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
@@ -34,7 +34,7 @@ trace_paths = Signal()
# Cables
#
-class Cable(NetBoxModel):
+class Cable(PrimaryModel):
"""
A physical connection between two endpoints.
"""
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 45ce4a23f..23c820571 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1029,27 +1029,9 @@ class InventoryItemRole(OrganizationalModel):
"""
Inventory items may optionally be assigned a functional role.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:inventoryitemrole', args=[self.pk])
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 244aab77a..5e34cf25b 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -18,7 +18,7 @@ from dcim.constants import *
from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet
from netbox.config import ConfigItem
-from netbox.models import OrganizationalModel, NetBoxModel
+from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from .device_components import *
@@ -46,35 +46,16 @@ class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
contacts = GenericRelation(
to='tenancy.ContactAssignment'
)
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
-
def get_absolute_url(self):
return reverse('dcim:manufacturer', args=[self.pk])
-class DeviceType(NetBoxModel, WeightMixin):
+class DeviceType(PrimaryModel, WeightMixin):
"""
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
well as high-level functional role(s).
@@ -137,9 +118,6 @@ class DeviceType(NetBoxModel, WeightMixin):
upload_to='devicetype-images',
blank=True
)
- comments = models.TextField(
- blank=True
- )
clone_fields = (
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
@@ -318,7 +296,7 @@ class DeviceType(NetBoxModel, WeightMixin):
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
-class ModuleType(NetBoxModel, WeightMixin):
+class ModuleType(PrimaryModel, WeightMixin):
"""
A ModuleType represents a hardware element that can be installed within a device and which houses additional
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
@@ -338,9 +316,6 @@ class ModuleType(NetBoxModel, WeightMixin):
blank=True,
help_text='Discrete part number (optional)'
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
images = GenericRelation(
@@ -419,14 +394,6 @@ class DeviceRole(OrganizationalModel):
color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
virtual machines as well.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
@@ -435,16 +402,6 @@ class DeviceRole(OrganizationalModel):
verbose_name='VM Role',
help_text='Virtual machines may be assigned to this role'
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:devicerole', args=[self.pk])
@@ -456,14 +413,6 @@ class Platform(OrganizationalModel):
NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
specifying a NAPALM driver.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
@@ -484,22 +433,12 @@ class Platform(OrganizationalModel):
verbose_name='NAPALM arguments',
help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:platform', args=[self.pk])
-class Device(NetBoxModel, ConfigContextModel):
+class Device(PrimaryModel, ConfigContextModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@@ -643,9 +582,6 @@ class Device(NetBoxModel, ConfigContextModel):
null=True,
validators=[MaxValueValidator(255)]
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
contacts = GenericRelation(
@@ -962,7 +898,7 @@ class Device(NetBoxModel, ConfigContextModel):
return round(total_weight / 1000, 2)
-class Module(NetBoxModel, ConfigContextModel):
+class Module(PrimaryModel, ConfigContextModel):
"""
A Module represents a field-installable component within a Device which may itself hold multiple device components
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
@@ -995,9 +931,6 @@ class Module(NetBoxModel, ConfigContextModel):
verbose_name='Asset tag',
help_text='A unique tag used to identify this device'
)
- comments = models.TextField(
- blank=True
- )
clone_fields = ('device', 'module_type')
@@ -1075,7 +1008,7 @@ class Module(NetBoxModel, ConfigContextModel):
# Virtual chassis
#
-class VirtualChassis(NetBoxModel):
+class VirtualChassis(PrimaryModel):
"""
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
"""
@@ -1132,7 +1065,7 @@ class VirtualChassis(NetBoxModel):
return super().delete(*args, **kwargs)
-class VirtualDeviceContext(NetBoxModel):
+class VirtualDeviceContext(PrimaryModel):
device = models.ForeignKey(
to='Device',
on_delete=models.PROTECT,
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 39f0f37ef..e79cf4c44 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -6,9 +6,8 @@ from django.db import models
from django.urls import reverse
from dcim.choices import *
-from dcim.constants import *
from netbox.config import ConfigItem
-from netbox.models import NetBoxModel
+from netbox.models import PrimaryModel
from utilities.validators import ExclusionValidator
from .device_components import CabledObjectModel, PathEndpoint
@@ -22,7 +21,7 @@ __all__ = (
# Power
#
-class PowerPanel(NetBoxModel):
+class PowerPanel(PrimaryModel):
"""
A distribution point for electrical power; e.g. a data center RPP.
"""
@@ -77,7 +76,7 @@ class PowerPanel(NetBoxModel):
)
-class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
+class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
"""
An electrical circuit delivered from a PowerPanel.
"""
@@ -132,9 +131,6 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
default=0,
editable=False
)
- comments = models.TextField(
- blank=True
- )
clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 6fcd65a19..e37fc8dc3 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -14,7 +14,7 @@ from django.urls import reverse
from dcim.choices import *
from dcim.constants import *
from dcim.svg import RackElevationSVG
-from netbox.models import OrganizationalModel, NetBoxModel
+from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange
@@ -38,33 +38,15 @@ class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.
"""
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- max_length=100,
- unique=True
- )
color = ColorField(
default=ColorChoices.COLOR_GREY
)
- description = models.CharField(
- max_length=200,
- blank=True,
- )
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
def get_absolute_url(self):
return reverse('dcim:rackrole', args=[self.pk])
-class Rack(NetBoxModel, WeightMixin):
+class Rack(PrimaryModel, WeightMixin):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
Each Rack is assigned to a Site and (optionally) a Location.
@@ -175,9 +157,6 @@ class Rack(NetBoxModel, WeightMixin):
'distance between the front and rear rails.'
)
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
vlan_groups = GenericRelation(
@@ -481,7 +460,7 @@ class Rack(NetBoxModel, WeightMixin):
return round(total_weight / 1000, 2)
-class RackReservation(NetBoxModel):
+class RackReservation(PrimaryModel):
"""
One or more reserved units within a Rack.
"""
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index 9ddadace2..c760119fb 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -2,12 +2,11 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
-from mptt.models import TreeForeignKey
from timezone_field import TimeZoneField
from dcim.choices import *
from dcim.constants import *
-from netbox.models import NestedGroupModel, NetBoxModel
+from netbox.models import NestedGroupModel, PrimaryModel
from utilities.fields import NaturalOrderingField
__all__ = (
@@ -28,25 +27,6 @@ class Region(NestedGroupModel):
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
also considered to be members of its parent and ancestor region(s).
"""
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
@@ -102,25 +82,6 @@ class SiteGroup(NestedGroupModel):
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
nested recursively to form a hierarchy.
"""
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
-
# Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
@@ -170,7 +131,7 @@ class SiteGroup(NestedGroupModel):
# Sites
#
-class Site(NetBoxModel):
+class Site(PrimaryModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@@ -227,10 +188,6 @@ class Site(NetBoxModel):
time_zone = TimeZoneField(
blank=True
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
physical_address = models.CharField(
max_length=200,
blank=True
@@ -253,9 +210,6 @@ class Site(NetBoxModel):
null=True,
help_text='GPS coordinate (longitude)'
)
- comments = models.TextField(
- blank=True
- )
# Generic relations
vlan_groups = GenericRelation(
@@ -298,25 +252,11 @@ class Location(NestedGroupModel):
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
site, or a room within a building, for example.
"""
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- max_length=100
- )
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='locations'
)
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
status = models.CharField(
max_length=50,
choices=LocationStatusChoices,
@@ -329,10 +269,6 @@ class Location(NestedGroupModel):
blank=True,
null=True
)
- description = models.CharField(
- max_length=200,
- blank=True
- )
# Generic relations
vlan_groups = GenericRelation(
diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py
index e5410e42a..6e9d49719 100644
--- a/netbox/dcim/tables/cables.py
+++ b/netbox/dcim/tables/cables.py
@@ -111,6 +111,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
order_by=('_abs_length', 'length_unit')
)
color = columns.ColorColumn()
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:cable_list'
)
@@ -120,7 +121,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
fields = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'tenant_group', 'color',
- 'length', 'tags', 'created', 'last_updated',
+ 'length', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index efa8ee111..abaf0536d 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -1,22 +1,5 @@
import django_tables2 as tables
-from dcim.models import (
- ConsolePort,
- ConsoleServerPort,
- Device,
- DeviceBay,
- DeviceRole,
- FrontPort,
- Interface,
- InventoryItem,
- InventoryItemRole,
- ModuleBay,
- Platform,
- PowerOutlet,
- PowerPort,
- RearPort,
- VirtualChassis,
- VirtualDeviceContext,
-)
+from dcim import models
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@@ -108,7 +91,7 @@ class DeviceRoleTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = DeviceRole
+ model = models.DeviceRole
fields = (
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions', 'created', 'last_updated',
@@ -139,7 +122,7 @@ class PlatformTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Platform
+ model = models.Platform
fields = (
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args',
'description', 'tags', 'actions', 'created', 'last_updated',
@@ -222,12 +205,12 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Device
+ model = models.Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
- 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
+ 'vc_priority', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@@ -254,7 +237,7 @@ class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Device
+ model = models.Device
fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False
@@ -328,7 +311,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsolePort
+ model = models.ConsolePort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@@ -347,7 +330,7 @@ class DeviceConsolePortTable(ConsolePortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsolePort
+ model = models.ConsolePort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
@@ -370,7 +353,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsoleServerPort
+ model = models.ConsoleServerPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created', 'last_updated',
@@ -390,7 +373,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ConsoleServerPort
+ model = models.ConsoleServerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -413,7 +396,7 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerPort
+ model = models.PowerPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@@ -434,7 +417,7 @@ class DevicePowerPortTable(PowerPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerPort
+ model = models.PowerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -462,7 +445,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerOutlet
+ model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'created',
@@ -482,7 +465,7 @@ class DevicePowerOutletTable(PowerOutletTable):
)
class Meta(DeviceComponentTable.Meta):
- model = PowerOutlet
+ model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
@@ -546,7 +529,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
)
class Meta(DeviceComponentTable.Meta):
- model = Interface
+ model = models.Interface
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
@@ -580,7 +563,7 @@ class DeviceInterfaceTable(InterfaceTable):
)
class Meta(DeviceComponentTable.Meta):
- model = Interface
+ model = models.Interface
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
@@ -619,7 +602,7 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
)
class Meta(DeviceComponentTable.Meta):
- model = FrontPort
+ model = models.FrontPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
@@ -642,7 +625,7 @@ class DeviceFrontPortTable(FrontPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = FrontPort
+ model = models.FrontPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@@ -668,7 +651,7 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
)
class Meta(DeviceComponentTable.Meta):
- model = RearPort
+ model = models.RearPort
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'created', 'last_updated',
@@ -688,7 +671,7 @@ class DeviceRearPortTable(RearPortTable):
)
class Meta(DeviceComponentTable.Meta):
- model = RearPort
+ model = models.RearPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
@@ -729,7 +712,7 @@ class DeviceBayTable(DeviceComponentTable):
)
class Meta(DeviceComponentTable.Meta):
- model = DeviceBay
+ model = models.DeviceBay
fields = (
'pk', 'id', 'name', 'device', 'label', 'status', 'device_role', 'device_type', 'installed_device', 'description', 'tags',
'created', 'last_updated',
@@ -750,7 +733,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
)
class Meta(DeviceComponentTable.Meta):
- model = DeviceBay
+ model = models.DeviceBay
fields = (
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
)
@@ -779,7 +762,7 @@ class ModuleBayTable(DeviceComponentTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ModuleBay
+ model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags',
@@ -793,7 +776,7 @@ class DeviceModuleBayTable(ModuleBayTable):
)
class Meta(DeviceComponentTable.Meta):
- model = ModuleBay
+ model = models.ModuleBay
fields = (
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_serial', 'module_asset_tag',
'description', 'tags', 'actions',
@@ -823,7 +806,7 @@ class InventoryItemTable(DeviceComponentTable):
cable = None # Override DeviceComponentTable
class Meta(NetBoxTable.Meta):
- model = InventoryItem
+ model = models.InventoryItem
fields = (
'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
@@ -842,7 +825,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
)
class Meta(NetBoxTable.Meta):
- model = InventoryItem
+ model = models.InventoryItem
fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions',
@@ -867,7 +850,7 @@ class InventoryItemRoleTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = InventoryItemRole
+ model = models.InventoryItemRole
fields = (
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
)
@@ -890,13 +873,17 @@ class VirtualChassisTable(NetBoxTable):
url_params={'virtual_chassis_id': 'pk'},
verbose_name='Members'
)
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:virtualchassis_list'
)
class Meta(NetBoxTable.Meta):
- model = VirtualChassis
- fields = ('pk', 'id', 'name', 'domain', 'master', 'member_count', 'tags', 'created', 'last_updated',)
+ model = models.VirtualChassis
+ fields = (
+ 'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created',
+ 'last_updated',
+ )
default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
@@ -931,7 +918,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = VirtualDeviceContext
+ model = models.VirtualDeviceContext
fields = (
'pk', 'id', 'name', 'identifier', 'tenant', 'tenant_group',
'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags', 'created', 'last_updated',
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 19b04c70d..a52d41b70 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -1,19 +1,6 @@
import django_tables2 as tables
-from dcim.models import (
- ConsolePortTemplate,
- ConsoleServerPortTemplate,
- DeviceBayTemplate,
- DeviceType,
- FrontPortTemplate,
- InterfaceTemplate,
- InventoryItemTemplate,
- Manufacturer,
- ModuleBayTemplate,
- PowerOutletTemplate,
- PowerPortTemplate,
- RearPortTemplate,
-)
+from dcim import models
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
@@ -59,7 +46,7 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
)
class Meta(NetBoxTable.Meta):
- model = Manufacturer
+ model = models.Manufacturer
fields = (
'pk', 'id', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
'contacts', 'actions', 'created', 'last_updated',
@@ -100,15 +87,12 @@ class DeviceTypeTable(NetBoxTable):
template_code=DEVICE_WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
- u_height = columns.TemplateColumn(
- template_code='{{ value|floatformat }}'
- )
class Meta(NetBoxTable.Meta):
- model = DeviceType
+ model = models.DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
- 'airflow', 'weight', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
+ 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@@ -138,7 +122,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ConsolePortTemplate
+ model = models.ConsolePortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None"
@@ -150,7 +134,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ConsoleServerPortTemplate
+ model = models.ConsoleServerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'description', 'actions')
empty_text = "None"
@@ -162,7 +146,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = PowerPortTemplate
+ model = models.PowerPortTemplate
fields = ('pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'actions')
empty_text = "None"
@@ -174,7 +158,7 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = PowerOutletTemplate
+ model = models.PowerOutletTemplate
fields = ('pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'actions')
empty_text = "None"
@@ -189,7 +173,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = InterfaceTemplate
+ model = models.InterfaceTemplate
fields = ('pk', 'name', 'label', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions')
empty_text = "None"
@@ -205,7 +189,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = FrontPortTemplate
+ model = models.FrontPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
empty_text = "None"
@@ -218,7 +202,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = RearPortTemplate
+ model = models.RearPortTemplate
fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
empty_text = "None"
@@ -229,7 +213,7 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = ModuleBayTemplate
+ model = models.ModuleBayTemplate
fields = ('pk', 'name', 'label', 'position', 'description', 'actions')
empty_text = "None"
@@ -240,7 +224,7 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = DeviceBayTemplate
+ model = models.DeviceBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None"
@@ -260,7 +244,7 @@ class InventoryItemTemplateTable(ComponentTemplateTable):
)
class Meta(ComponentTemplateTable.Meta):
- model = InventoryItemTemplate
+ model = models.InventoryItemTemplate
fields = (
'pk', 'name', 'label', 'parent', 'role', 'manufacturer', 'part_id', 'component', 'description', 'actions',
)
diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py
index b644e6ba6..9df26eb73 100644
--- a/netbox/dcim/tables/modules.py
+++ b/netbox/dcim/tables/modules.py
@@ -35,7 +35,7 @@ class ModuleTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ModuleType
fields = (
- 'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'comments', 'tags',
+ 'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'description', 'comments', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number',
@@ -64,8 +64,8 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Module
fields = (
- 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
- 'tags',
+ 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'description',
+ 'comments', 'tags',
)
default_columns = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py
index 04012ea4a..feff29e12 100644
--- a/netbox/dcim/tables/power.py
+++ b/netbox/dcim/tables/power.py
@@ -31,6 +31,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:powerpanel_list'
)
@@ -38,7 +39,8 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = PowerPanel
fields = (
- 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags',
+ 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
@@ -77,7 +79,7 @@ class PowerFeedTable(CableTerminationTable):
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'available_power',
- 'comments', 'tags', 'created', 'last_updated',
+ 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index 1a355cc2a..b360002d2 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -90,8 +90,8 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
'asset_tag', 'type', 'u_height', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'weight',
- 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'contacts', 'tags', 'created',
- 'last_updated',
+ 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags',
+ 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
@@ -123,6 +123,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Units'
)
+ comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:rackreservation_list'
)
@@ -130,7 +131,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
- 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
- 'actions', 'created', 'last_updated',
+ 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant',
+ 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 184b95a5a..18f1c16e1 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -1,6 +1,10 @@
from decimal import Decimal
+try:
+ from zoneinfo import ZoneInfo
+except ImportError:
+ # Python 3.8
+ from backports.zoneinfo import ZoneInfo
-import pytz
import yaml
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
@@ -12,7 +16,6 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN
@@ -153,7 +156,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tenant': None,
'facility': 'Facility X',
'asns': [asns[6].pk, asns[7].pk],
- 'time_zone': pytz.UTC,
+ 'time_zone': ZoneInfo('UTC'),
'description': 'Site description',
'physical_address': '742 Evergreen Terrace, Springfield, USA',
'shipping_address': '742 Evergreen Terrace, Springfield, USA',
@@ -182,7 +185,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'region': regions[1].pk,
'group': groups[1].pk,
'tenant': None,
- 'time_zone': pytz.timezone('US/Eastern'),
+ 'time_zone': ZoneInfo('US/Eastern'),
'description': 'New description',
}
diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py
index 44dfe7cbc..dce062b84 100644
--- a/netbox/extras/api/nested_serializers.py
+++ b/netbox/extras/api/nested_serializers.py
@@ -13,6 +13,7 @@ __all__ = [
'NestedImageAttachmentSerializer',
'NestedJobResultSerializer',
'NestedJournalEntrySerializer',
+ 'NestedSavedFilterSerializer',
'NestedTagSerializer', # Defined in netbox.api.serializers
'NestedWebhookSerializer',
]
@@ -58,6 +59,14 @@ class NestedExportTemplateSerializer(WritableNestedSerializer):
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):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index ac025ff16..1afb8fa8f 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -39,6 +39,7 @@ __all__ = (
'ReportDetailSerializer',
'ReportSerializer',
'ReportInputSerializer',
+ 'SavedFilterSerializer',
'ScriptDetailSerializer',
'ScriptInputSerializer',
'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
#
diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py
index bcad6b77c..91067d40d 100644
--- a/netbox/extras/api/urls.py
+++ b/netbox/extras/api/urls.py
@@ -5,43 +5,19 @@ from . import views
router = NetBoxRouter()
router.APIRootView = views.ExtrasRootView
-# Webhooks
router.register('webhooks', views.WebhookViewSet)
-
-# Custom fields
router.register('custom-fields', views.CustomFieldViewSet)
-
-# Custom links
router.register('custom-links', views.CustomLinkViewSet)
-
-# Export templates
router.register('export-templates', views.ExportTemplateViewSet)
-
-# Tags
+router.register('saved-filters', views.SavedFilterViewSet)
router.register('tags', views.TagViewSet)
-
-# Image attachments
router.register('image-attachments', views.ImageAttachmentViewSet)
-
-# Journal entries
router.register('journal-entries', views.JournalEntryViewSet)
-
-# Config contexts
router.register('config-contexts', views.ConfigContextViewSet)
-
-# Reports
router.register('reports', views.ReportViewSet, basename='report')
-
-# Scripts
router.register('scripts', views.ScriptViewSet, basename='script')
-
-# Change logging
router.register('object-changes', views.ObjectChangeViewSet)
-
-# Job Results
router.register('job-results', views.JobResultViewSet)
-
-# ContentTypes
router.register('content-types', views.ContentTypeViewSet)
app_name = 'extras-api'
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index 62a011530..ab111b0ec 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
from django.http import Http404
from django_rq.queues import get_connection
from rest_framework import status
@@ -98,6 +99,17 @@ class ExportTemplateViewSet(NetBoxModelViewSet):
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
#
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index 22fe6537e..6010c733a 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -23,6 +23,7 @@ __all__ = (
'JournalEntryFilterSet',
'LocalConfigContextFilterSet',
'ObjectChangeFilterSet',
+ 'SavedFilterFilterSet',
'TagFilterSet',
'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):
q = django_filters.CharFilter(
method='search',
diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py
index d2f2fb015..af0f7cf43 100644
--- a/netbox/extras/forms/__init__.py
+++ b/netbox/extras/forms/__init__.py
@@ -2,6 +2,6 @@ from .model_forms import *
from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
-from .customfields import *
+from .mixins import *
from .config import *
from .scripts import *
diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py
index df17324ec..a061d9784 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -1,11 +1,9 @@
from django import forms
-from django.contrib.contenttypes.models import ContentType
from extras.choices import *
from extras.models import *
-from extras.utils import FeatureQuery
from utilities.forms import (
- add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect,
+ add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, StaticSelect,
)
__all__ = (
@@ -14,6 +12,7 @@ __all__ = (
'CustomLinkBulkEditForm',
'ExportTemplateBulkEditForm',
'JournalEntryBulkEditForm',
+ 'SavedFilterBulkEditForm',
'TagBulkEditForm',
'WebhookBulkEditForm',
)
@@ -96,6 +95,30 @@ class ExportTemplateBulkEditForm(BulkEditForm):
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):
pk = forms.ModelMultipleChoiceField(
queryset=Webhook.objects.all(),
diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index ee638015b..0f5974698 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -12,6 +12,7 @@ __all__ = (
'CustomFieldCSVForm',
'CustomLinkCSVForm',
'ExportTemplateCSVForm',
+ 'SavedFilterCSVForm',
'TagCSVForm',
'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):
content_types = CSVMultipleContentTypeField(
queryset=ContentType.objects.all(),
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index a164a3d95..479367ff0 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -15,6 +15,7 @@ from utilities.forms import (
StaticSelect, TagFilterField,
)
from virtualization.models import Cluster, ClusterGroup, ClusterType
+from .mixins import SavedFiltersMixin
__all__ = (
'ConfigContextFilterForm',
@@ -25,14 +26,15 @@ __all__ = (
'JournalEntryFilterForm',
'LocalConfigContextFilterForm',
'ObjectChangeFilterForm',
+ 'SavedFilterFilterForm',
'TagFilterForm',
'WebhookFilterForm',
)
-class CustomFieldFilterForm(FilterForm):
+class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
- (None, ('q',)),
+ (None, ('q', 'filter')),
('Attributes', ('type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility')),
)
content_type_id = ContentTypeMultipleChoiceField(
@@ -66,9 +68,9 @@ class CustomFieldFilterForm(FilterForm):
)
-class JobResultFilterForm(FilterForm):
+class JobResultFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
- (None, ('q',)),
+ (None, ('q', 'filter')),
('Attributes', ('obj_type', 'status')),
('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after',
'scheduled_time__before', 'scheduled_time__after', 'user')),
@@ -118,9 +120,9 @@ class JobResultFilterForm(FilterForm):
)
-class CustomLinkFilterForm(FilterForm):
+class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
- (None, ('q',)),
+ (None, ('q', 'filter')),
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
@@ -145,9 +147,9 @@ class CustomLinkFilterForm(FilterForm):
)
-class ExportTemplateFilterForm(FilterForm):
+class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
- (None, ('q',)),
+ (None, ('q', 'filter')),
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
)
content_types = ContentTypeMultipleChoiceField(
@@ -170,9 +172,36 @@ class ExportTemplateFilterForm(FilterForm):
)
-class WebhookFilterForm(FilterForm):
+class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
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')),
('Events', ('type_create', 'type_update', 'type_delete')),
)
@@ -213,7 +242,7 @@ class WebhookFilterForm(FilterForm):
)
-class TagFilterForm(FilterForm):
+class TagFilterForm(SavedFiltersMixin, FilterForm):
model = Tag
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
@@ -222,9 +251,9 @@ class TagFilterForm(FilterForm):
)
-class ConfigContextFilterForm(FilterForm):
+class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
- (None, ('q', 'tag_id')),
+ (None, ('q', 'filter', 'tag_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Device', ('device_type_id', 'platform_id', 'role_id')),
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
@@ -311,7 +340,7 @@ class LocalConfigContextFilterForm(forms.Form):
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry
fieldsets = (
- (None, ('q', 'tag')),
+ (None, ('q', 'filter', 'tag')),
('Creation', ('created_before', 'created_after', 'created_by_id')),
('Attributes', ('assigned_object_type_id', 'kind'))
)
@@ -349,10 +378,10 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
-class ObjectChangeFilterForm(FilterForm):
+class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange
fieldsets = (
- (None, ('q',)),
+ (None, ('q', 'filter')),
('Time', ('time_before', 'time_after')),
('Attributes', ('action', 'user_id', 'changed_object_type_id')),
)
diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/mixins.py
similarity index 84%
rename from netbox/extras/forms/customfields.py
rename to netbox/extras/forms/mixins.py
index 40d068450..2b64d1a74 100644
--- a/netbox/extras/forms/customfields.py
+++ b/netbox/extras/forms/mixins.py
@@ -1,10 +1,13 @@
from django.contrib.contenttypes.models import ContentType
+from django import forms
from extras.models import *
from extras.choices import CustomFieldVisibilityChoices
+from utilities.forms.fields import DynamicModelMultipleChoiceField
__all__ = (
'CustomFieldsMixin',
+ 'SavedFiltersMixin',
)
@@ -57,3 +60,14 @@ class CustomFieldsMixin:
if customfield.group_name not in self.custom_field_groups:
self.custom_field_groups[customfield.group_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,
+ }
+ )
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 7ff4f3e27..97e80100a 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -1,5 +1,6 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
+from django.http import QueryDict
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
@@ -20,6 +21,7 @@ __all__ = (
'ExportTemplateForm',
'ImageAttachmentForm',
'JournalEntryForm',
+ 'SavedFilterForm',
'TagForm',
'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):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py
index 3073976e8..0c3113879 100644
--- a/netbox/extras/graphql/schema.py
+++ b/netbox/extras/graphql/schema.py
@@ -20,6 +20,9 @@ class ExtrasQuery(graphene.ObjectType):
image_attachment = ObjectField(ImageAttachmentType)
image_attachment_list = ObjectListField(ImageAttachmentType)
+ saved_filter = ObjectField(SavedFilterType)
+ saved_filter_list = ObjectListField(SavedFilterType)
+
journal_entry = ObjectField(JournalEntryType)
journal_entry_list = ObjectListField(JournalEntryType)
diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py
index 3be7b371e..b5d4dffce 100644
--- a/netbox/extras/graphql/types.py
+++ b/netbox/extras/graphql/types.py
@@ -10,6 +10,7 @@ __all__ = (
'ImageAttachmentType',
'JournalEntryType',
'ObjectChangeType',
+ 'SavedFilterType',
'TagType',
'WebhookType',
)
@@ -71,6 +72,14 @@ class ObjectChangeType(BaseObjectType):
filterset_class = filtersets.ObjectChangeFilterSet
+class SavedFilterType(ObjectType):
+
+ class Meta:
+ model = models.SavedFilter
+ exclude = ('content_types', )
+ filterset_class = filtersets.SavedFilterFilterSet
+
+
class TagType(ObjectType):
class Meta:
diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py
index 6dc9bbb2d..f519688f8 100644
--- a/netbox/extras/management/commands/reindex.py
+++ b/netbox/extras/management/commands/reindex.py
@@ -1,7 +1,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
-from extras.registry import registry
+from netbox.registry import registry
from netbox.search.backends import search_backend
diff --git a/netbox/extras/migrations/0083_savedfilter.py b/netbox/extras/migrations/0083_savedfilter.py
new file mode 100644
index 000000000..6bae7ccde
--- /dev/null
+++ b/netbox/extras/migrations/0083_savedfilter.py
@@ -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'),
+ },
+ ),
+ ]
diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py
index e3a4be3fe..6d2bf288c 100644
--- a/netbox/extras/models/__init__.py
+++ b/netbox/extras/models/__init__.py
@@ -18,6 +18,7 @@ __all__ = (
'JournalEntry',
'ObjectChange',
'Report',
+ 'SavedFilter',
'Script',
'Tag',
'TaggedItem',
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index a8b2f2647..4b4e7c0cf 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.validators import ValidationError
from django.db import models
-from django.http import HttpResponse
+from django.http import HttpResponse, QueryDict
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format
@@ -34,6 +34,7 @@ __all__ = (
'JobResult',
'JournalEntry',
'Report',
+ 'SavedFilter',
'Script',
'Webhook',
)
@@ -350,6 +351,69 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
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):
"""
An uploaded image which is associated with an object.
diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py
index f855027e2..7694a1fbe 100644
--- a/netbox/extras/plugins/__init__.py
+++ b/netbox/extras/plugins/__init__.py
@@ -1,17 +1,16 @@
import collections
-import inspect
-from packaging import version
from django.apps import AppConfig
+from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-from django.template.loader import get_template
from django.utils.module_loading import import_string
+from packaging import version
-from extras.registry import registry
-from netbox.navigation import MenuGroup
+from netbox.registry import registry
from netbox.search import register_search
-from utilities.choices import ButtonColorChoices
-
+from .navigation import *
+from .registration import *
+from .templates import *
# Initialize plugin registry
registry['plugins'] = {
@@ -145,185 +144,20 @@ class PluginConfig(AppConfig):
#
-# Template content injection
+# Utilities
#
-class PluginTemplateExtension:
+def get_plugin_config(plugin_name, parameter, default=None):
"""
- This class is used to register plugin content to be injected into core NetBox templates. It contains methods
- that are overridden by plugin authors to return template content.
+ Return the value of the specified plugin configuration parameter.
- The `model` attribute on the class defines the which model detail page this class renders content for. It
- should be set as a string in the form 'Site | -{{ object.site|linkify }} | -
---|---|
Location | -{{ object.location|linkify|placeholder }} | -
Site | +{{ object.site|linkify }} | +
---|---|
Location | +{{ object.location|linkify|placeholder }} | +
Description | +{{ object.description|placeholder }} | +
Name | +{{ object.name }} | +
---|---|
Description | +{{ object.description|placeholder }} | +
User | +{{ object.user|placeholder }} | +
Enabled | +{% checkmark object.enabled %} | +
Shared | +{% checkmark object.shared %} | +
Weight | +{{ object.weight }} | +
{{ ct }} | +
{{ object.parameters }}+
Key | +
+
+
+
+
+
+ {{ key }}
+ |
+
---|---|
Description | +{{ object.description|placeholder }} | +
User | +{{ object.user }} | +
Created | +{{ object.created|annotated_date }} | +
Expires | ++ {% if object.expires %} + {{ object.expires|annotated_date }} + {% else %} + Never + {% endif %} + | +