Merge branch 'feature' into 7854-vdc

This commit is contained in:
Jeremy Stretch 2022-11-04 08:32:24 -04:00 committed by GitHub
commit 2832dc7c9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
189 changed files with 2261 additions and 1230 deletions

View File

@ -1,5 +1,13 @@
# Security & Authentication Parameters
## ALLOW_TOKEN_RETRIEVAL
Default: True
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token immediately upon its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
---
## ALLOWED_URL_SCHEMES
!!! tip "Dynamic Configuration Parameter"

View File

@ -1,5 +1,5 @@
# Journaling
All primary objects in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
All primary and organizational models in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created.

View File

@ -579,6 +579,9 @@ By default, a token can be used to perform all actions via the API that a user w
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
!!! warning "Restricting Token Retrieval"
The ability to retrieve the key value of a previously-created API token can be restricted by disabling the [`ALLOW_TOKEN_RETRIEVAL`](../configuration/security.md#allow_token_retrieval) configuration parameter.
#### Client IP Restriction
!!! note

View File

@ -33,7 +33,7 @@ Each site can have multiple [AS numbers](../ipam/asn.md) assigned to it.
### Time Zone
The site's local time zone. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.)
The site's local time zone. (Time zones are provided by the [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) library.)
### Physical Address

View File

@ -117,11 +117,11 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
!!! tip "Accessing Config Parameters"
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
```python
from django.conf import settings
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
from extras.plugins import get_plugin_config
get_plugin_config('my_plugin', 'verbose_name')
```
#### Important Notes About `django_apps`

View File

@ -28,6 +28,8 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
* [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types
* [#8485](https://github.com/netbox-community/netbox/issues/8485) - Enable journaling for all organizational models
* [#8853](https://github.com/netbox-community/netbox/issues/8853) - Introduce the `ALLOW_TOKEN_RETRIEVAL` config parameter to restrict the display of API tokens
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
@ -37,6 +39,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#10348](https://github.com/netbox-community/netbox/issues/10348) - Add decimal custom field type
* [#10556](https://github.com/netbox-community/netbox/issues/10556) - Include a `display` field in all GraphQL object types
* [#10595](https://github.com/netbox-community/netbox/issues/10595) - Add GraphQL relationships for additional generic foreign key fields
* [#10698](https://github.com/netbox-community/netbox/issues/10698) - Omit app label from content type in table columns
* [#10761](https://github.com/netbox-community/netbox/issues/10761) - Enable associating an export template with multiple object types
* [#10781](https://github.com/netbox-community/netbox/issues/10781) - Add support for Python v3.11
@ -48,6 +51,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#9880](https://github.com/netbox-community/netbox/issues/9880) - Introduce `django_apps` plugin configuration parameter
* [#9887](https://github.com/netbox-community/netbox/issues/9887) - Inspect `docs_url` property to determine link to model documentation
* [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin
* [#10543](https://github.com/netbox-community/netbox/issues/10543) - Introduce `get_plugin_config()` utility function
* [#10739](https://github.com/netbox-community/netbox/issues/10739) - Introduce `get_queryset()` method on generic views
### Other Changes
@ -55,24 +59,79 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
* [#9045](https://github.com/netbox-community/netbox/issues/9045) - Remove legacy ASN field from provider model
* [#9046](https://github.com/netbox-community/netbox/issues/9046) - Remove legacy contact fields from provider model
* [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11
* [#10697](https://github.com/netbox-community/netbox/issues/10697) - Move application registry into core app
* [#10699](https://github.com/netbox-community/netbox/issues/10699) - Remove custom `import_object()` function
* [#10816](https://github.com/netbox-community/netbox/issues/10816) - Pass the current request when instantiating a FilterSet within UI views
* [#10820](https://github.com/netbox-community/netbox/issues/10820) - Switch timezone library from pytz to zoneinfo
* [#10821](https://github.com/netbox-community/netbox/issues/10821) - Enable data localization
### REST API Changes
* circuits.provider
* Removed the `asn`, `noc_contact`, `admin_contact`, and `portal_url` fields
* Added a `description` field
* dcim.Cable
* Added `description` and `comments` fields
* dcim.Device
* Added a `description` field
* dcim.DeviceType
* Added a `description` field
* Added optional `weight` and `weight_unit` fields
* dcim.Module
* Added a `description` field
* dcim.ModuleType
* Added a `description` field
* Added optional `weight` and `weight_unit` fields
* dcim.PowerFeed
* Added a `description` field
* dcim.PowerPanel
* Added `description` and `comments` fields
* dcim.Rack
* Added a `description` field
* Added optional `weight` and `weight_unit` fields
* dcim.RackReservation
* Added a `comments` field
* dcim.VirtualChassis
* Added `description` and `comments` fields
* extras.CustomLink
* Renamed `content_type` field to `content_types`
* extras.ExportTemplate
* Renamed `content_type` field to `content_types`
* ipam.Aggregate
* Added a `comments` field
* ipam.ASN
* Added a `comments` field
* ipam.FHRPGroup
* Added a `comments` field
* Added optional `name` field
* ipam.IPAddress
* Added a `comments` field
* ipam.IPRange
* Added a `comments` field
* ipam.L2VPN
* Added a `comments` field
* ipam.Prefix
* Added a `comments` field
* ipam.RouteTarget
* Added a `comments` field
* ipam.Service
* Added a `comments` field
* ipam.ServiceTemplate
* Added a `comments` field
* ipam.VLAN
* Added a `comments` field
* ipam.VRF
* Added a `comments` field
* tenancy.Contact
* Added a `description` field
* virtualization.Cluster
* Added a `description` field
* virtualization.VirtualMachine
* Added a `description` field
* wireless.WirelessLAN
* Added a `comments` field
* wireless.WirelessLink
* Added a `comments` field
### GraphQL API Changes

View File

@ -31,8 +31,8 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'url', 'display', 'name', 'slug', 'account',
'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]

View File

@ -30,6 +30,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Account number'
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
@ -40,7 +44,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
(None, ('asns', 'account', )),
)
nullable_fields = (
'asns', 'account', 'comments',
'asns', 'account', 'description', 'comments',
)

View File

@ -18,7 +18,7 @@ class ProviderCSVForm(NetBoxModelCSVForm):
class Meta:
model = Provider
fields = (
'name', 'slug', 'account', 'comments',
'name', 'slug', 'account', 'description', 'comments',
)

View File

@ -20,7 +20,7 @@ __all__ = (
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role', 'contact_group')),
@ -59,7 +59,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('provider_id', 'service_id')),
)
provider_id = DynamicModelMultipleChoiceField(
@ -82,7 +82,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Provider', ('provider_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),

View File

@ -1,4 +1,3 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.models import *
@ -7,8 +6,8 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import (
BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
SelectSpeedWidget, SmallTextarea, SlugField, StaticSelect,
CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField,
StaticSelect,
)
__all__ = (
@ -30,14 +29,14 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Provider', ('name', 'slug', 'asns', 'tags')),
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
('Support Info', ('account',)),
)
class Meta:
model = Provider
fields = [
'name', 'slug', 'account', 'asns', 'comments', 'tags',
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
]
help_texts = {
'name': "Full name of the provider",

View File

@ -65,7 +65,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
'ordering': ['name'],
'ordering': ('name',),
},
),
migrations.CreateModel(

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-11-03 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0040_provider_remove_deprecated_fields'),
]
operations = [
migrations.AddField(
model_name='provider',
name='description',
field=models.CharField(blank=True, max_length=200),
),
]

View File

@ -7,7 +7,7 @@ from django.urls import reverse
from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import (
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin,
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin,
)
from netbox.models.features import WebhooksMixin
@ -23,30 +23,11 @@ class CircuitType(OrganizationalModel):
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
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('circuits:circuittype', args=[self.pk])
class Circuit(NetBoxModel):
class Circuit(PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@ -92,13 +73,6 @@ class Circuit(NetBoxModel):
blank=True,
null=True,
verbose_name='Commit rate (Kbps)')
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
# Generic relations
contacts = GenericRelation(

View File

@ -2,8 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from dcim.fields import ASNField
from netbox.models import NetBoxModel
from netbox.models import PrimaryModel
__all__ = (
'ProviderNetwork',
@ -11,7 +10,7 @@ __all__ = (
)
class Provider(NetBoxModel):
class Provider(PrimaryModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
@ -34,9 +33,6 @@ class Provider(NetBoxModel):
blank=True,
verbose_name='Account number'
)
comments = models.TextField(
blank=True
)
# Generic relations
contacts = GenericRelation(
@ -57,7 +53,7 @@ class Provider(NetBoxModel):
return reverse('circuits:provider', args=[self.pk])
class ProviderNetwork(NetBoxModel):
class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user.
@ -75,13 +71,6 @@ class ProviderNetwork(NetBoxModel):
blank=True,
verbose_name='Service ID'
)
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
class Meta:
ordering = ('provider', 'name')

View File

@ -39,8 +39,8 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Provider
fields = (
'pk', 'id', 'name', 'asns', 'account', 'asn_count',
'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'account', 'circuit_count')

View File

@ -210,8 +210,8 @@ class RackSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'device_count', 'powerfeed_count',
'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'powerfeed_count',
]
@ -243,8 +243,8 @@ class RackReservationSerializer(NetBoxModelSerializer):
class Meta:
model = RackReservation
fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'tags',
'custom_fields',
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
'comments', 'tags', 'custom_fields',
]
@ -324,8 +324,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
model = DeviceType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
]
@ -333,13 +333,12 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = ModuleType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@ -656,8 +655,8 @@ class DeviceSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments',
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
@ -697,8 +696,8 @@ class ModuleSerializer(NetBoxModelSerializer):
class Meta:
model = Module
fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@ -1041,7 +1040,7 @@ class CableSerializer(NetBoxModelSerializer):
model = Cable
fields = [
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
'length', 'length_unit', 'tags', 'custom_fields', 'created', 'last_updated',
'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
@ -1107,8 +1106,8 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
class Meta:
model = VirtualChassis
fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count',
'created', 'last_updated',
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'member_count', 'created', 'last_updated',
]
@ -1129,8 +1128,8 @@ class PowerPanelSerializer(NetBoxModelSerializer):
class Meta:
model = PowerPanel
fields = [
'id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count',
'created', 'last_updated',
'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
'powerfeed_count', 'created', 'last_updated',
]
@ -1163,7 +1162,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
model = PowerFeed
fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]

View File

@ -128,22 +128,26 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Contact E-mail'
)
description = forms.CharField(
max_length=100,
required=False
)
time_zone = TimeZoneFormField(
choices=add_blank_choice(TimeZoneFormField().choices),
required=False,
widget=StaticSelect()
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Site
fieldsets = (
(None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')),
)
nullable_fields = (
'region', 'group', 'tenant', 'asns', 'description', 'time_zone',
'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
)
@ -286,10 +290,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False,
min_value=1
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
weight = forms.DecimalField(
min_value=0,
required=False
@ -300,10 +300,18 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
initial='',
widget=StaticSelect()
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Rack
fieldsets = (
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')),
('Location', ('region', 'site_group', 'site', 'location')),
('Hardware', (
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
@ -311,8 +319,8 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
('Weight', ('weight', 'weight_unit')),
)
nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
'weight', 'weight_unit'
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
'weight_unit', 'description', 'comments',
)
@ -329,14 +337,19 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = RackReservation
fieldsets = (
(None, ('user', 'tenant', 'description')),
)
nullable_fields = ('comments',)
class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
@ -384,13 +397,21 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
initial='',
widget=StaticSelect()
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = DeviceType
fieldsets = (
('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
('Weight', ('weight', 'weight_unit')),
)
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit')
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
@ -411,13 +432,21 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
initial='',
widget=StaticSelect()
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ModuleType
fieldsets = (
('Module Type', ('manufacturer', 'part_number')),
('Module Type', ('manufacturer', 'part_number', 'description')),
('Weight', ('weight', 'weight_unit')),
)
nullable_fields = ('part_number', 'weight', 'weight_unit')
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
@ -513,15 +542,23 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Serial Number'
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Device
fieldsets = (
('Device', ('device_role', 'status', 'tenant', 'platform')),
('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
('Location', ('site', 'location')),
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
)
nullable_fields = (
'location', 'tenant', 'platform', 'serial', 'airflow',
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
)
@ -542,12 +579,20 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False,
label='Serial Number'
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Module
fieldsets = (
(None, ('manufacturer', 'module_type', 'serial')),
(None, ('manufacturer', 'module_type', 'serial', 'description')),
)
nullable_fields = ('serial',)
nullable_fields = ('serial', 'description', 'comments')
class CableBulkEditForm(NetBoxModelBulkEditForm):
@ -584,14 +629,22 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
initial='',
widget=StaticSelect()
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Cable
fieldsets = (
(None, ('type', 'status', 'tenant', 'label')),
(None, ('type', 'status', 'tenant', 'label', 'description')),
('Attributes', ('color', 'length', 'length_unit')),
)
nullable_fields = (
'type', 'status', 'tenant', 'label', 'color', 'length',
'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
)
@ -600,12 +653,20 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
max_length=30,
required=False
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VirtualChassis
fieldsets = (
(None, ('domain',)),
(None, ('domain', 'description')),
)
nullable_fields = ('domain',)
nullable_fields = ('domain', 'description', 'comments')
class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
@ -638,12 +699,20 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
'site_id': '$site'
}
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = PowerPanel
fieldsets = (
(None, ('region', 'site_group', 'site', 'location')),
(None, ('region', 'site_group', 'site', 'location', 'description')),
)
nullable_fields = ('location',)
nullable_fields = ('location', 'description', 'comments')
class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
@ -692,6 +761,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
@ -699,10 +772,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
model = PowerFeed
fieldsets = (
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected')),
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')),
('Power', ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
)
nullable_fields = ('location', 'comments')
nullable_fields = ('location', 'description', 'comments')
#

View File

@ -197,7 +197,8 @@ class RackCSVForm(NetBoxModelCSVForm):
model = Rack
fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'comments',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
'description', 'comments',
)
def __init__(self, data=None, *args, **kwargs):
@ -241,7 +242,7 @@ class RackReservationCSVForm(NetBoxModelCSVForm):
class Meta:
model = RackReservation
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description')
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
@ -388,7 +389,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
'cluster', 'comments',
'cluster', 'description', 'comments',
]
def __init__(self, data=None, *args, **kwargs):
@ -425,7 +426,7 @@ class ModuleCSVForm(NetBoxModelCSVForm):
class Meta:
model = Module
fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments',
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments',
)
def __init__(self, data=None, *args, **kwargs):
@ -928,7 +929,7 @@ class CableCSVForm(NetBoxModelCSVForm):
model = Cable
fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments',
]
help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
@ -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):

View File

@ -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')),
)

View File

@ -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(),

View File

@ -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']
#

View File

@ -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(

View File

@ -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(

View File

@ -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),
),
]

View File

@ -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.
"""

View File

@ -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])

View File

@ -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).
"""

View File

@ -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',

View File

@ -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.
"""

View File

@ -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(

View File

@ -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',

View File

@ -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',

View File

@ -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',
)

View File

@ -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',

View File

@ -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',

View File

@ -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')

View File

@ -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',
}

View File

@ -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')

View File

@ -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
#

View File

@ -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'

View File

@ -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
#

View File

@ -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',

View File

@ -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 *

View File

@ -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(),

View File

@ -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(),

View File

@ -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')),
)

View File

@ -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,
}
)

View File

@ -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(),

View File

@ -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)

View File

@ -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:

View File

@ -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

View 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'),
},
),
]

View File

@ -18,6 +18,7 @@ __all__ = (
'JournalEntry',
'ObjectChange',
'Report',
'SavedFilter',
'Script',
'Tag',
'TaggedItem',

View File

@ -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.

View File

@ -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 '<app_label>.<model_name>'. render() provides the following context data:
* object - The object being viewed
* request - The current request
* settings - Global NetBox settings
* config - Plugin-specific configuration parameters
Args:
plugin_name: The name of the plugin
parameter: The name of the configuration parameter
default: The value to return if the parameter is not defined (default: None)
"""
model = None
def __init__(self, context):
self.context = context
def render(self, template_name, extra_context=None):
"""
Convenience method for rendering the specified Django template using the default context data. An additional
context dictionary may be passed as `extra_context`.
"""
if extra_context is None:
extra_context = {}
elif not isinstance(extra_context, dict):
raise TypeError("extra_context must be a dictionary")
return get_template(template_name).render({**self.context, **extra_context})
def left_page(self):
"""
Content that will be rendered on the left of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def right_page(self):
"""
Content that will be rendered on the right of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def full_width_page(self):
"""
Content that will be rendered within the full width of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def buttons(self):
"""
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
automatically handled.
"""
raise NotImplementedError
def register_template_extensions(class_list):
"""
Register a list of PluginTemplateExtension classes
"""
# Validation
for template_extension in class_list:
if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None:
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
#
# Navigation menu links
#
class PluginMenu:
icon_class = 'mdi mdi-puzzle'
def __init__(self, label, groups, icon_class=None):
self.label = label
self.groups = [
MenuGroup(label, items) for label, items in groups
]
if icon_class is not None:
self.icon_class = icon_class
class PluginMenuItem:
"""
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
specifying additional link buttons that appear to the right of the item in the van menu.
Links are specified as Django reverse URL strings.
Buttons are each specified as a list of PluginMenuButton instances.
"""
permissions = []
buttons = []
def __init__(self, link, link_text, permissions=None, buttons=None):
self.link = link
self.link_text = link_text
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if buttons is not None:
if type(buttons) not in (list, tuple):
raise TypeError("Buttons must be passed as a tuple or list.")
self.buttons = buttons
class PluginMenuButton:
"""
This class represents a button within a PluginMenuItem. Note that button colors should come from
ButtonColorChoices.
"""
color = ButtonColorChoices.DEFAULT
permissions = []
def __init__(self, link, title, icon_class, color=None, permissions=None):
self.link = link
self.title = title
self.icon_class = icon_class
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if color is not None:
if color not in ButtonColorChoices.values():
raise ValueError("Button color must be a choice within ButtonColorChoices.")
self.color = color
def register_menu(menu):
if not isinstance(menu, PluginMenu):
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
registry['plugins']['menus'].append(menu)
def register_menu_items(section_name, class_list):
"""
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
"""
# Validation
for menu_link in class_list:
if not isinstance(menu_link, PluginMenuItem):
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
for button in menu_link.buttons:
if not isinstance(button, PluginMenuButton):
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
registry['plugins']['menu_items'][section_name] = class_list
#
# GraphQL schemas
#
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
#
# User preferences
#
def register_user_preferences(plugin_name, preferences):
"""
Register a list of user preferences defined by a plugin.
"""
registry['plugins']['preferences'][plugin_name] = preferences
try:
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
return plugin_config.get(parameter, default)
except KeyError:
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")

View File

@ -0,0 +1,66 @@
from netbox.navigation import MenuGroup
from utilities.choices import ButtonColorChoices
__all__ = (
'PluginMenu',
'PluginMenuButton',
'PluginMenuItem',
)
class PluginMenu:
icon_class = 'mdi mdi-puzzle'
def __init__(self, label, groups, icon_class=None):
self.label = label
self.groups = [
MenuGroup(label, items) for label, items in groups
]
if icon_class is not None:
self.icon_class = icon_class
class PluginMenuItem:
"""
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
specifying additional link buttons that appear to the right of the item in the van menu.
Links are specified as Django reverse URL strings.
Buttons are each specified as a list of PluginMenuButton instances.
"""
permissions = []
buttons = []
def __init__(self, link, link_text, permissions=None, buttons=None):
self.link = link
self.link_text = link_text
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if buttons is not None:
if type(buttons) not in (list, tuple):
raise TypeError("Buttons must be passed as a tuple or list.")
self.buttons = buttons
class PluginMenuButton:
"""
This class represents a button within a PluginMenuItem. Note that button colors should come from
ButtonColorChoices.
"""
color = ButtonColorChoices.DEFAULT
permissions = []
def __init__(self, link, title, icon_class, color=None, permissions=None):
self.link = link
self.title = title
self.icon_class = icon_class
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError("Permissions must be passed as a tuple or list.")
self.permissions = permissions
if color is not None:
if color not in ButtonColorChoices.values():
raise ValueError("Button color must be a choice within ButtonColorChoices.")
self.color = color

View File

@ -0,0 +1,64 @@
import inspect
from netbox.registry import registry
from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
from .templates import PluginTemplateExtension
__all__ = (
'register_graphql_schema',
'register_menu',
'register_menu_items',
'register_template_extensions',
'register_user_preferences',
)
def register_template_extensions(class_list):
"""
Register a list of PluginTemplateExtension classes
"""
# Validation
for template_extension in class_list:
if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None:
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
def register_menu(menu):
if not isinstance(menu, PluginMenu):
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
registry['plugins']['menus'].append(menu)
def register_menu_items(section_name, class_list):
"""
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
"""
# Validation
for menu_link in class_list:
if not isinstance(menu_link, PluginMenuItem):
raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
for button in menu_link.buttons:
if not isinstance(button, PluginMenuButton):
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
registry['plugins']['menu_items'][section_name] = class_list
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
def register_user_preferences(plugin_name, preferences):
"""
Register a list of user preferences defined by a plugin.
"""
registry['plugins']['preferences'][plugin_name] = preferences

View File

@ -0,0 +1,65 @@
from django.template.loader import get_template
__all__ = (
'PluginTemplateExtension',
)
class PluginTemplateExtension:
"""
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.
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 '<app_label>.<model_name>'. render() provides the following context data:
* object - The object being viewed
* request - The current request
* settings - Global NetBox settings
* config - Plugin-specific configuration parameters
"""
model = None
def __init__(self, context):
self.context = context
def render(self, template_name, extra_context=None):
"""
Convenience method for rendering the specified Django template using the default context data. An additional
context dictionary may be passed as `extra_context`.
"""
if extra_context is None:
extra_context = {}
elif not isinstance(extra_context, dict):
raise TypeError("extra_context must be a dictionary")
return get_template(template_name).render({**self.context, **extra_context})
def left_page(self):
"""
Content that will be rendered on the left of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def right_page(self):
"""
Content that will be rendered on the right of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def full_width_page(self):
"""
Content that will be rendered within the full width of the detail page view. Content should be returned as an
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
def buttons(self):
"""
Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
should be returned as an HTML string. Note that content does not need to be marked as safe because this is
automatically handled.
"""
raise NotImplementedError

View File

@ -13,16 +13,13 @@ __all__ = (
'ExportTemplateTable',
'JournalEntryTable',
'ObjectChangeTable',
'SavedFilterTable',
'TaggedItemTable',
'TagTable',
'WebhookTable',
)
#
# Custom fields
#
class CustomFieldTable(NetBoxTable):
name = tables.Column(
linkify=True
@ -40,10 +37,6 @@ class CustomFieldTable(NetBoxTable):
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
#
# Custom fields
#
class JobResultTable(NetBoxTable):
name = tables.Column(
linkify=True
@ -61,10 +54,6 @@ class JobResultTable(NetBoxTable):
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'completed', 'user',)
#
# Custom links
#
class CustomLinkTable(NetBoxTable):
name = tables.Column(
linkify=True
@ -82,10 +71,6 @@ class CustomLinkTable(NetBoxTable):
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window')
#
# Export templates
#
class ExportTemplateTable(NetBoxTable):
name = tables.Column(
linkify=True
@ -104,9 +89,24 @@ class ExportTemplateTable(NetBoxTable):
)
#
# Webhooks
#
class SavedFilterTable(NetBoxTable):
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):
name = tables.Column(
@ -139,10 +139,6 @@ class WebhookTable(NetBoxTable):
)
#
# Tags
#
class TagTable(NetBoxTable):
name = tables.Column(
linkify=True

View File

@ -3,7 +3,7 @@ from django.conf import settings
from django.utils.safestring import mark_safe
from extras.plugins import PluginTemplateExtension
from extras.registry import registry
from netbox.registry import registry
register = template_.Library()

View File

@ -3,7 +3,6 @@ from unittest import skipIf
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from django.utils.timezone import make_aware
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 utilities.testing import APITestCase, APIViewTestCases
rq_worker_running = Worker.count(get_connection('default'))
@ -192,6 +190,73 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
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):
model = ExportTemplate
brief_fields = ['display', 'id', 'name', 'url']

View File

@ -222,6 +222,92 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
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):
queryset = ExportTemplate.objects.all()
filterset = ExportTemplateFilterSet

View File

@ -5,10 +5,10 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from extras.plugins import PluginMenu
from extras.registry import registry
from extras.plugins import PluginMenu, get_plugin_config
from extras.tests.dummy_plugin import config as dummy_config
from netbox.graphql.schema import Query
from netbox.registry import registry
@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@ -173,3 +173,13 @@ class PluginTest(TestCase):
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery))
@override_settings(PLUGINS_CONFIG={'extras.tests.dummy_plugin': {'foo': 123}})
def test_get_plugin_config(self):
"""
Validate that get_plugin_config() returns config parameters correctly.
"""
plugin = 'extras.tests.dummy_plugin'
self.assertEqual(get_plugin_config(plugin, 'foo'), 123)
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)

View File

@ -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):
model = ExportTemplate

View File

@ -31,6 +31,14 @@ urlpatterns = [
path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'),
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
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),

View File

@ -3,7 +3,7 @@ from django.utils.deconstruct import deconstructible
from taggit.managers import _TaggableManager
from extras.constants import EXTRAS_FEATURES
from extras.registry import registry
from netbox.registry import registry
def is_taggable(obj):

View File

@ -9,7 +9,6 @@ from django_rq.queues import get_connection
from rq import Worker
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.htmx import is_htmx
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
@ -159,6 +158,74 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
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
#

View File

@ -5,11 +5,11 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django_rq import get_queue
from netbox.registry import registry
from utilities.api import get_serializer_for_model
from utilities.utils import serialize_object
from .choices import *
from .models import Webhook
from .registry import registry
def serialize_for_webhook(instance):

View File

@ -31,8 +31,8 @@ class ASNSerializer(NetBoxModelSerializer):
class Meta:
model = ASN
fields = [
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'site_count', 'provider_count', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'site_count', 'provider_count',
]
@ -61,8 +61,9 @@ class VRFSerializer(NetBoxModelSerializer):
class Meta:
model = VRF
fields = [
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets',
'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count',
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments',
'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
'prefix_count',
]
@ -77,7 +78,8 @@ class RouteTargetSerializer(NetBoxModelSerializer):
class Meta:
model = RouteTarget
fields = [
'id', 'url', 'display', 'name', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
]
@ -106,8 +108,8 @@ class AggregateSerializer(NetBoxModelSerializer):
class Meta:
model = Aggregate
fields = [
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
read_only_fields = ['family']
@ -123,8 +125,8 @@ class FHRPGroupSerializer(NetBoxModelSerializer):
class Meta:
model = FHRPGroup
fields = [
'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses',
'tags', 'custom_fields', 'created', 'last_updated',
'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses',
]
@ -215,7 +217,7 @@ class VLANSerializer(NetBoxModelSerializer):
model = VLAN
fields = [
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description',
'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count',
'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count',
]
@ -273,7 +275,8 @@ class PrefixSerializer(NetBoxModelSerializer):
model = Prefix
fields = [
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'children', '_depth',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
'_depth',
]
read_only_fields = ['family']
@ -342,7 +345,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
model = IPRange
fields = [
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
]
read_only_fields = ['family']
@ -371,8 +374,8 @@ class IPAddressSerializer(NetBoxModelSerializer):
model = IPAddress
fields = [
'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type',
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
@ -415,8 +418,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer):
class Meta:
model = ServiceTemplate
fields = [
'id', 'url', 'display', 'name', 'ports', 'protocol', 'description', 'tags', 'custom_fields', 'created',
'last_updated',
'id', 'url', 'display', 'name', 'ports', 'protocol', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
@ -436,7 +439,7 @@ class ServiceSerializer(NetBoxModelSerializer):
model = Service
fields = [
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
'description', 'tags', 'custom_fields', 'created', 'last_updated',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
#
@ -465,7 +468,7 @@ class L2VPNSerializer(NetBoxModelSerializer):
model = L2VPN
fields = [
'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
'description', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
]

View File

@ -8,8 +8,8 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BulkEditNullBooleanSelect, DynamicModelChoiceField, NumericArrayField, StaticSelect,
DynamicModelMultipleChoiceField,
add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, NumericArrayField,
SmallTextarea, StaticSelect, DynamicModelMultipleChoiceField,
)
__all__ = (
@ -43,15 +43,19 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
label='Enforce unique space'
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VRF
fieldsets = (
(None, ('tenant', 'enforce_unique', 'description')),
)
nullable_fields = ('tenant', 'description')
nullable_fields = ('tenant', 'description', 'comments')
class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
@ -63,12 +67,16 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = RouteTarget
fieldsets = (
(None, ('tenant', 'description')),
)
nullable_fields = ('tenant', 'description')
nullable_fields = ('tenant', 'description', 'comments')
class RIRBulkEditForm(NetBoxModelBulkEditForm):
@ -103,15 +111,19 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ASN
fieldsets = (
(None, ('sites', 'rir', 'tenant', 'description')),
)
nullable_fields = ('date_added', 'description')
nullable_fields = ('date_added', 'description', 'comments')
class AggregateBulkEditForm(NetBoxModelBulkEditForm):
@ -128,15 +140,19 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Aggregate
fieldsets = (
(None, ('rir', 'tenant', 'date_added', 'description')),
)
nullable_fields = ('date_added', 'description')
nullable_fields = ('date_added', 'description', 'comments')
class RoleBulkEditForm(NetBoxModelBulkEditForm):
@ -206,9 +222,13 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
label='Treat as 100% utilized'
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = Prefix
fieldsets = (
@ -217,7 +237,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
('Addressing', ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
)
nullable_fields = (
'site', 'vrf', 'tenant', 'role', 'description',
'site', 'vrf', 'tenant', 'role', 'description', 'comments',
)
@ -241,16 +261,20 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = IPRange
fieldsets = (
(None, ('status', 'role', 'vrf', 'tenant', 'description')),
)
nullable_fields = (
'vrf', 'tenant', 'role', 'description',
'vrf', 'tenant', 'role', 'description', 'comments',
)
@ -285,9 +309,13 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
label='DNS name'
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = IPAddress
fieldsets = (
@ -295,7 +323,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
('Addressing', ('vrf', 'mask_length', 'dns_name')),
)
nullable_fields = (
'vrf', 'role', 'tenant', 'dns_name', 'description',
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
)
@ -329,13 +357,17 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = FHRPGroup
fieldsets = (
(None, ('protocol', 'group_id', 'name', 'description')),
('Authentication', ('auth_type', 'auth_key')),
)
nullable_fields = ('auth_type', 'auth_key', 'name', 'description')
nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
@ -405,9 +437,13 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = VLAN
fieldsets = (
@ -415,7 +451,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
('Site & Group', ('region', 'site_group', 'site', 'group')),
)
nullable_fields = (
'site', 'group', 'tenant', 'role', 'description',
'site', 'group', 'tenant', 'role', 'description', 'comments',
)
@ -433,15 +469,19 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = ServiceTemplate
fieldsets = (
(None, ('protocol', 'ports', 'description')),
)
nullable_fields = ('description',)
nullable_fields = ('description', 'comments')
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
@ -459,15 +499,19 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
max_length=100,
max_length=200,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
model = L2VPN
fieldsets = (
(None, ('type', 'description', 'tenant')),
(None, ('type', 'tenant', 'description')),
)
nullable_fields = ('tenant', 'description',)
nullable_fields = ('tenant', 'description', 'comments')
class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -41,7 +41,7 @@ class VRFCSVForm(NetBoxModelCSVForm):
class Meta:
model = VRF
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description')
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments')
class RouteTargetCSVForm(NetBoxModelCSVForm):
@ -54,7 +54,7 @@ class RouteTargetCSVForm(NetBoxModelCSVForm):
class Meta:
model = RouteTarget
fields = ('name', 'description', 'tenant')
fields = ('name', 'tenant', 'description', 'comments')
class RIRCSVForm(NetBoxModelCSVForm):
@ -83,7 +83,7 @@ class AggregateCSVForm(NetBoxModelCSVForm):
class Meta:
model = Aggregate
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description')
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments')
class ASNCSVForm(NetBoxModelCSVForm):
@ -101,7 +101,7 @@ class ASNCSVForm(NetBoxModelCSVForm):
class Meta:
model = ASN
fields = ('asn', 'rir', 'tenant', 'description')
fields = ('asn', 'rir', 'tenant', 'description', 'comments')
help_texts = {}
@ -159,7 +159,7 @@ class PrefixCSVForm(NetBoxModelCSVForm):
model = Prefix
fields = (
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
'description',
'description', 'comments',
)
def __init__(self, data=None, *args, **kwargs):
@ -204,7 +204,7 @@ class IPRangeCSVForm(NetBoxModelCSVForm):
class Meta:
model = IPRange
fields = (
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description',
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', 'comments',
)
@ -257,7 +257,7 @@ class IPAddressCSVForm(NetBoxModelCSVForm):
model = IPAddress
fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'dns_name', 'description',
'dns_name', 'description', 'comments',
]
def __init__(self, data=None, *args, **kwargs):
@ -326,7 +326,7 @@ class FHRPGroupCSVForm(NetBoxModelCSVForm):
class Meta:
model = FHRPGroup
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description')
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'comments')
class VLANGroupCSVForm(NetBoxModelCSVForm):
@ -389,7 +389,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
class Meta:
model = VLAN
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description')
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments')
help_texts = {
'vid': 'Numeric VLAN ID (1-4094)',
'name': 'VLAN name',
@ -404,7 +404,7 @@ class ServiceTemplateCSVForm(NetBoxModelCSVForm):
class Meta:
model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description')
fields = ('name', 'protocol', 'ports', 'description', 'comments')
class ServiceCSVForm(NetBoxModelCSVForm):
@ -427,7 +427,7 @@ class ServiceCSVForm(NetBoxModelCSVForm):
class Meta:
model = Service
fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description')
fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments')
class L2VPNCSVForm(NetBoxModelCSVForm):
@ -443,7 +443,7 @@ class L2VPNCSVForm(NetBoxModelCSVForm):
class Meta:
model = L2VPN
fields = ('identifier', 'name', 'slug', 'type', 'description')
fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments')
class L2VPNTerminationCSVForm(NetBoxModelCSVForm):

View File

@ -1,6 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
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 utilities.forms import (
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
@ -46,7 +45,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VRF
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Route Targets', ('import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -66,7 +65,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RouteTarget
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('VRF', ('importing_vrf_id', 'exporting_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -98,7 +97,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Aggregate
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('family', 'rir_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -119,7 +118,7 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Assignment', ('rir_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -144,7 +143,7 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Prefix
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
('VRF', ('vrf_id', 'present_in_vrf_id')),
('Location', ('region_id', 'site_group_id', 'site_id')),
@ -233,7 +232,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -265,7 +264,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@ -334,7 +333,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
model = FHRPGroup
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('name', 'protocol', 'group_id')),
('Authentication', ('auth_type', 'auth_key')),
)
@ -364,7 +363,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
class VLANGroupFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Location', ('region', 'sitegroup', 'site', 'location', 'rack')),
('VLAN ID', ('min_vid', 'max_vid')),
)
@ -412,7 +411,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VLAN
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Attributes', ('group_id', 'status', 'role_id', 'vid')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@ -465,7 +464,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
model = ServiceTemplate
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('protocol', 'port')),
)
protocol = forms.ChoiceField(
@ -486,7 +485,7 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = L2VPN
fieldsets = (
(None, ('q', 'tag')),
(None, ('q', 'filter', 'tag')),
('Attributes', ('type', 'import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
@ -511,8 +510,10 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
model = L2VPNTermination
fieldsets = (
(None, ('l2vpn_id', )),
('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')),
(None, ('filter', 'l2vpn_id',)),
('Assigned Object', (
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
)),
)
l2vpn_id = DynamicModelChoiceField(
queryset=L2VPN.objects.all(),

View File

@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation
from utilities.forms import (
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@ -49,6 +49,7 @@ class VRFForm(TenancyForm, NetBoxModelForm):
queryset=RouteTarget.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
@ -59,8 +60,8 @@ class VRFForm(TenancyForm, NetBoxModelForm):
class Meta:
model = VRF
fields = [
'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant',
'tags',
'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
'comments', 'tags',
]
labels = {
'rd': "RD",
@ -75,11 +76,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
('Route Target', ('name', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
comments = CommentField()
class Meta:
model = RouteTarget
fields = [
'name', 'description', 'tenant_group', 'tenant', 'tags',
'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
]
@ -104,6 +106,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
queryset=RIR.objects.all(),
label='RIR'
)
comments = CommentField()
fieldsets = (
('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
@ -113,7 +116,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
class Meta:
model = Aggregate
fields = [
'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags',
'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
]
help_texts = {
'prefix': "IPv4 or IPv6 network",
@ -134,6 +137,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
label='Sites',
required=False
)
comments = CommentField()
fieldsets = (
('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
@ -143,7 +147,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
class Meta:
model = ASN
fields = [
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'tags'
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
]
help_texts = {
'asn': "AS number",
@ -235,6 +239,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
@ -245,8 +250,8 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
class Meta:
model = Prefix
fields = [
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
'tenant_group', 'tenant', 'tags',
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant',
'description', 'comments', 'tags',
]
widgets = {
'status': StaticSelect(),
@ -263,6 +268,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')),
@ -272,7 +278,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
class Meta:
model = IPRange
fields = [
'vrf', 'start_address', 'end_address', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'description',
'comments', 'tags',
]
widgets = {
'status': StaticSelect(),
@ -394,13 +401,14 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
required=False,
label='Make this the primary IP for the device/VM'
)
comments = CommentField()
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'primary_for_parent', 'nat_site', 'nat_rack',
'nat_device', 'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant',
'tags',
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_site', 'nat_rack', 'nat_device',
'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description',
'comments', 'tags',
]
widgets = {
'status': StaticSelect(),
@ -535,6 +543,7 @@ class FHRPGroupForm(NetBoxModelForm):
required=False,
label='Status'
)
comments = CommentField()
fieldsets = (
('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
@ -545,7 +554,8 @@ class FHRPGroupForm(NetBoxModelForm):
class Meta:
model = FHRPGroup
fields = (
'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags',
'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
'comments', 'tags',
)
def save(self, *args, **kwargs):
@ -767,11 +777,13 @@ class VLANForm(TenancyForm, NetBoxModelForm):
queryset=Role.objects.all(),
required=False
)
comments = CommentField()
class Meta:
model = VLAN
fields = [
'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'description', 'comments',
'tags',
]
help_texts = {
'site': "Leave blank if this VLAN spans multiple sites",
@ -794,6 +806,7 @@ class ServiceTemplateForm(NetBoxModelForm):
),
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
)
comments = CommentField()
fieldsets = (
('Service Template', (
@ -803,7 +816,7 @@ class ServiceTemplateForm(NetBoxModelForm):
class Meta:
model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'tags')
fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
widgets = {
'protocol': StaticSelect(),
}
@ -834,11 +847,12 @@ class ServiceForm(NetBoxModelForm):
'virtual_machine_id': '$virtual_machine',
}
)
comments = CommentField()
class Meta:
model = Service
fields = [
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
]
help_texts = {
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
@ -899,6 +913,7 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
queryset=RouteTarget.objects.all(),
required=False
)
comments = CommentField()
fieldsets = (
('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
@ -909,7 +924,8 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
class Meta:
model = L2VPN
fields = (
'name', 'slug', 'type', 'identifier', 'description', 'import_targets', 'export_targets', 'tenant', 'tags'
'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
'comments', 'tags'
)
widgets = {
'type': StaticSelect(),

View File

@ -91,7 +91,7 @@ class Migration(migrations.Migration):
options={
'verbose_name': 'RIR',
'verbose_name_plural': 'RIRs',
'ordering': ['name'],
'ordering': ('name',),
},
),
migrations.CreateModel(
@ -107,7 +107,7 @@ class Migration(migrations.Migration):
('description', models.CharField(blank=True, max_length=200)),
],
options={
'ordering': ['weight', 'name'],
'ordering': ('weight', 'name'),
},
),
migrations.CreateModel(

View File

@ -0,0 +1,73 @@
# Generated by Django 4.1.2 on 2022-11-03 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0062_unique_constraints'),
]
operations = [
migrations.AddField(
model_name='aggregate',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='asn',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='fhrpgroup',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='ipaddress',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='iprange',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='l2vpn',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='prefix',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='routetarget',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='service',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='servicetemplate',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='vlan',
name='comments',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='vrf',
name='comments',
field=models.TextField(blank=True),
),
]

View File

@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
from netbox.models import ChangeLoggedModel, NetBoxModel
from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import WebhooksMixin
from ipam.choices import *
from ipam.constants import *
@ -15,7 +15,7 @@ __all__ = (
)
class FHRPGroup(NetBoxModel):
class FHRPGroup(PrimaryModel):
"""
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
"""
@ -41,10 +41,6 @@ class FHRPGroup(NetBoxModel):
blank=True,
verbose_name='Authentication key'
)
description = models.CharField(
max_length=200,
blank=True
)
ip_addresses = GenericRelation(
to='ipam.IPAddress',
content_type_field='assigned_object_type',

View File

@ -9,7 +9,7 @@ from django.utils.functional import cached_property
from dcim.fields import ASNField
from dcim.models import Device
from netbox.models import OrganizationalModel, NetBoxModel
from netbox.models import OrganizationalModel, PrimaryModel
from ipam.choices import *
from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField
@ -61,37 +61,22 @@ class RIR(OrganizationalModel):
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918.
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
is_private = models.BooleanField(
default=False,
verbose_name='Private',
help_text='IP space managed by this RIR is considered private'
)
description = models.CharField(
max_length=200,
blank=True
)
class Meta:
ordering = ['name']
ordering = ('name',)
verbose_name = 'RIR'
verbose_name_plural = 'RIRs'
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:rir', args=[self.pk])
class ASN(NetBoxModel):
class ASN(PrimaryModel):
"""
An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have
one or more ASNs assigned to it.
@ -101,10 +86,6 @@ class ASN(NetBoxModel):
verbose_name='ASN',
help_text='32-bit autonomous system number'
)
description = models.CharField(
max_length=200,
blank=True
)
rir = models.ForeignKey(
to='ipam.RIR',
on_delete=models.PROTECT,
@ -154,7 +135,7 @@ class ASN(NetBoxModel):
return self.asn
class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
"""
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR.
@ -177,10 +158,6 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
clone_fields = (
'rir', 'tenant', 'date_added', 'description',
@ -265,24 +242,12 @@ class Role(OrganizationalModel):
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
"Management."
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
weight = models.PositiveSmallIntegerField(
default=1000
)
description = models.CharField(
max_length=200,
blank=True,
)
class Meta:
ordering = ['weight', 'name']
ordering = ('weight', 'name')
def __str__(self):
return self.name
@ -291,7 +256,7 @@ class Role(OrganizationalModel):
return reverse('ipam:role', args=[self.pk])
class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
@ -354,10 +319,6 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
default=False,
help_text="Treat as 100% utilized"
)
description = models.CharField(
max_length=200,
blank=True
)
# Cached depth & child counts
_depth = models.PositiveSmallIntegerField(
@ -572,7 +533,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
return min(utilization, 100)
class IPRange(NetBoxModel):
class IPRange(PrimaryModel):
"""
A range of IP addresses, defined by start and end addresses.
"""
@ -614,10 +575,6 @@ class IPRange(NetBoxModel):
null=True,
help_text='The primary function of this range'
)
description = models.CharField(
max_length=200,
blank=True
)
clone_fields = (
'vrf', 'tenant', 'status', 'role', 'description',
@ -767,7 +724,7 @@ class IPRange(NetBoxModel):
return int(float(child_count) / self.size * 100)
class IPAddress(NetBoxModel):
class IPAddress(PrimaryModel):
"""
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
@ -840,10 +797,6 @@ class IPAddress(NetBoxModel):
verbose_name='DNS Name',
help_text='Hostname or FQDN (not case-sensitive)'
)
description = models.CharField(
max_length=200,
blank=True
)
objects = IPAddressManager()

View File

@ -8,7 +8,7 @@ from django.utils.functional import cached_property
from ipam.choices import L2VPNTypeChoices
from ipam.constants import L2VPN_ASSIGNMENT_MODELS
from netbox.models import NetBoxModel
from netbox.models import NetBoxModel, PrimaryModel
__all__ = (
'L2VPN',
@ -16,7 +16,7 @@ __all__ = (
)
class L2VPN(NetBoxModel):
class L2VPN(PrimaryModel):
name = models.CharField(
max_length=100,
unique=True
@ -43,10 +43,6 @@ class L2VPN(NetBoxModel):
related_name='exporting_l2vpns',
blank=True
)
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from ipam.choices import *
from ipam.constants import *
from netbox.models import NetBoxModel
from netbox.models import PrimaryModel
from utilities.utils import array_to_string
@ -30,10 +30,6 @@ class ServiceBase(models.Model):
),
verbose_name='Port numbers'
)
description = models.CharField(
max_length=200,
blank=True
)
class Meta:
abstract = True
@ -46,7 +42,7 @@ class ServiceBase(models.Model):
return array_to_string(self.ports)
class ServiceTemplate(ServiceBase, NetBoxModel):
class ServiceTemplate(ServiceBase, PrimaryModel):
"""
A template for a Service to be applied to a device or virtual machine.
"""
@ -62,7 +58,7 @@ class ServiceTemplate(ServiceBase, NetBoxModel):
return reverse('ipam:servicetemplate', args=[self.pk])
class Service(ServiceBase, NetBoxModel):
class Service(ServiceBase, PrimaryModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent.

View File

@ -8,12 +8,10 @@ from django.urls import reverse
from dcim.models import Interface
from ipam.choices import *
from ipam.constants import *
from ipam.models import L2VPNTermination
from ipam.querysets import VLANQuerySet
from netbox.models import OrganizationalModel, NetBoxModel
from netbox.models import OrganizationalModel, PrimaryModel
from virtualization.models import VMInterface
__all__ = (
'VLAN',
'VLANGroup',
@ -63,10 +61,6 @@ class VLANGroup(OrganizationalModel):
),
help_text='Highest permissible ID of a child VLAN'
)
description = models.CharField(
max_length=200,
blank=True
)
class Meta:
ordering = ('name', 'pk') # Name may be non-unique
@ -83,9 +77,6 @@ class VLANGroup(OrganizationalModel):
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('ipam:vlangroup', args=[self.pk])
@ -123,7 +114,7 @@ class VLANGroup(OrganizationalModel):
return None
class VLAN(NetBoxModel):
class VLAN(PrimaryModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
@ -175,10 +166,6 @@ class VLAN(NetBoxModel):
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True
)
l2vpn_terminations = GenericRelation(
to='ipam.L2VPNTermination',

View File

@ -2,7 +2,7 @@ from django.db import models
from django.urls import reverse
from ipam.constants import *
from netbox.models import NetBoxModel
from netbox.models import PrimaryModel
__all__ = (
@ -11,7 +11,7 @@ __all__ = (
)
class VRF(NetBoxModel):
class VRF(PrimaryModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
table). Prefixes and IPAddresses can optionally be assigned to VRFs. (Prefixes and IPAddresses not assigned to a VRF
@ -40,10 +40,6 @@ class VRF(NetBoxModel):
verbose_name='Enforce unique space',
help_text='Prevent duplicate prefixes/IP addresses within this VRF'
)
description = models.CharField(
max_length=200,
blank=True
)
import_targets = models.ManyToManyField(
to='ipam.RouteTarget',
related_name='importing_vrfs',
@ -73,7 +69,7 @@ class VRF(NetBoxModel):
return reverse('ipam:vrf', args=[self.pk])
class RouteTarget(NetBoxModel):
class RouteTarget(PrimaryModel):
"""
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
"""
@ -82,10 +78,6 @@ class RouteTarget(NetBoxModel):
unique=True,
help_text='Route target value (formatted in accordance with RFC 4360)'
)
description = models.CharField(
max_length=200,
blank=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,

View File

@ -20,7 +20,6 @@ class FHRPGroupTable(NetBoxTable):
group_id = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn()
ip_addresses = tables.TemplateColumn(
template_code=IPADDRESSES,
orderable=False,
@ -29,6 +28,7 @@ class FHRPGroupTable(NetBoxTable):
member_count = tables.Column(
verbose_name='Members'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:fhrpgroup_list'
)
@ -36,7 +36,7 @@ class FHRPGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = FHRPGroup
fields = (
'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'ip_addresses',
'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'comments', 'ip_addresses',
'member_count', 'tags', 'created', 'last_updated',
)
default_columns = (

View File

@ -120,6 +120,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
linkify_item=True,
verbose_name='Sites'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:asn_list'
)
@ -127,8 +128,8 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ASN
fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags',
'created', 'last_updated', 'actions',
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@ -153,6 +154,7 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
accessor='get_utilization',
orderable=False
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:aggregate_list'
)
@ -160,8 +162,8 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Aggregate
fields = (
'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags',
'created', 'last_updated',
'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added',
'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@ -278,6 +280,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
accessor='get_utilization',
orderable=False
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:prefix_list'
)
@ -285,8 +288,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Prefix
fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site',
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
'site', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
'created', 'last_updated',
)
default_columns = (
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
@ -317,6 +321,7 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
accessor='utilization',
orderable=False
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:iprange_list'
)
@ -324,8 +329,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPRange
fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description',
'utilization', 'tags', 'created', 'last_updated',
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
'utilization', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@ -378,6 +383,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
linkify=lambda record: record.assigned_object.get_absolute_url(),
verbose_name='Assigned'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:ipaddress_list'
)
@ -385,8 +391,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPAddress
fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description',
'tags', 'created', 'last_updated',
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
'assigned', 'dns_name', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',

View File

@ -29,12 +29,16 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
template_code=L2VPN_TARGETS,
orderable=False
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:prefix_list'
)
class Meta(NetBoxTable.Meta):
model = L2VPN
fields = (
'pk', 'name', 'slug', 'identifier', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
'actions',
'pk', 'name', 'slug', 'identifier', 'type', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
'description', 'comments', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'identifier', 'type', 'description', 'actions')

View File

@ -17,13 +17,16 @@ class ServiceTemplateTable(NetBoxTable):
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:servicetemplate_list'
)
class Meta(NetBoxTable.Meta):
model = ServiceTemplate
fields = ('pk', 'id', 'name', 'protocol', 'ports', 'description', 'tags')
fields = (
'pk', 'id', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'protocol', 'ports', 'description')
@ -39,6 +42,7 @@ class ServiceTable(NetBoxTable):
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:service_list'
)
@ -46,7 +50,7 @@ class ServiceTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Service
fields = (
'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags', 'created',
'last_updated',
'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description')

View File

@ -121,6 +121,7 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Prefixes'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:vlan_list'
)
@ -129,7 +130,7 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
model = VLAN
fields = (
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
'description', 'tags', 'l2vpn', 'created', 'last_updated',
'description', 'comments', 'tags', 'l2vpn', 'created', 'last_updated',
)
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
row_attrs = {

View File

@ -38,6 +38,7 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
template_code=VRF_TARGETS,
orderable=False
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:vrf_list'
)
@ -45,8 +46,8 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = VRF
fields = (
'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'import_targets', 'export_targets',
'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@ -59,11 +60,14 @@ class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:vrf_list'
)
class Meta(NetBoxTable.Meta):
model = RouteTarget
fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',)
fields = (
'pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'tenant', 'description')

View File

@ -72,6 +72,9 @@ ADMINS = [
# ('John Doe', 'jdoe@example.com'),
]
# Permit the retrieval of API tokens after their creation.
ALLOW_TOKEN_RETRIEVAL = False
# Enable any desired validators for local account passwords below. For a list of included validators, please see the
# Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation.
AUTH_PASSWORD_VALIDATORS = [

View File

@ -1,7 +1,7 @@
from django.conf import settings as django_settings
from extras.registry import registry
from netbox.config import get_config
from netbox.registry import registry
def settings_and_registry(request):

View File

@ -3,7 +3,7 @@ import logging
from django.db.models.signals import post_save
from django.dispatch import receiver
from extras.registry import registry
from netbox.registry import registry
logger = logging.getLogger('netbox.denormalized')

View File

@ -4,10 +4,11 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django_filters.exceptions import FieldLookupError
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.filters import TagFilter
from extras.models import CustomField
from extras.models import CustomField, SavedFilter
from utilities.constants import (
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_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
# however FilterSet Factory is setup before this which creates the
# initial filters. This recreates the filters so Empty is picked up correctly.
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
def _get_filter_lookup_dict(existing_filter):

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
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 utilities.forms import BootstrapMixin, CSVModelForm
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)
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
corresponding FilterSet *must* provide a `q` filter.
@ -129,6 +129,15 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
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):
return super()._get_custom_fields(content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |

View File

@ -3,8 +3,8 @@ import graphene
from circuits.graphql.schema import CircuitsQuery
from dcim.graphql.schema import DCIMQuery
from extras.graphql.schema import ExtrasQuery
from extras.registry import registry
from ipam.graphql.schema import IPAMQuery
from netbox.registry import registry
from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery
from virtualization.graphql.schema import VirtualizationQuery

View File

@ -10,8 +10,9 @@ from netbox.models.features import *
__all__ = (
'ChangeLoggedModel',
'NestedGroupModel',
'OrganizationalModel',
'NetBoxModel',
'OrganizationalModel',
'PrimaryModel',
)
@ -21,6 +22,7 @@ class NetBoxFeatureSet(
CustomLinksMixin,
CustomValidationMixin,
ExportTemplatesMixin,
JournalingMixin,
TagsMixin,
WebhooksMixin
):
@ -55,11 +57,27 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
abstract = True
class NetBoxModel(CloningMixin, JournalingMixin, NetBoxFeatureSet, models.Model):
class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
"""
Base model for most object types. Suitable for use by plugins.
"""
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
class PrimaryModel(NetBoxModel):
"""
Primary models represent real objects within the infrastructure being modeled.
"""
objects = RestrictedQuerySet.as_manager()
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
class Meta:
abstract = True
@ -81,6 +99,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
name = models.CharField(
max_length=100
)
slug = models.SlugField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
@ -134,3 +155,6 @@ class OrganizationalModel(NetBoxFeatureSet, models.Model):
class Meta:
abstract = True
ordering = ('name',)
def __str__(self):
return self.name

View File

@ -1,4 +1,4 @@
from extras.registry import registry
from netbox.registry import registry
from . import *
@ -279,6 +279,7 @@ OTHER_MENU = Menu(
get_model_item('extras', 'customfield', 'Custom Fields'),
get_model_item('extras', 'customlink', 'Custom Links'),
get_model_item('extras', 'exporttemplate', 'Export Templates'),
get_model_item('extras', 'savedfilter', 'Saved Filters'),
),
),
MenuGroup(

View File

@ -1,4 +1,4 @@
from extras.registry import registry
from netbox.registry import registry
from users.preferences import UserPreference
from utilities.paginator import EnhancedPaginator

View File

@ -2,7 +2,7 @@ from collections import namedtuple
from django.db import models
from extras.registry import registry
from netbox.registry import registry
ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))

View File

@ -9,9 +9,9 @@ from django.db.models.signals import post_delete, post_save
from django.utils.module_loading import import_string
from extras.models import CachedValue, CustomField
from extras.registry import registry
from netbox.registry import registry
from utilities.querysets import RestrictedPrefetch
from utilities.templatetags.builtins.filters import bettertitle
from utilities.utils import title
from . import FieldTypes, LookupTypes, get_indexer
DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL
@ -34,8 +34,7 @@ class SearchBackend:
# Organize choices by category
categories = defaultdict(dict)
for label, idx in registry['search'].items():
title = bettertitle(idx.model._meta.verbose_name)
categories[idx.get_category()][label] = title
categories[idx.get_category()][label] = title(idx.model._meta.verbose_name)
# Compile a nested tuple of choices for form rendering
results = (

Some files were not shown because too many files have changed in this diff Show More