Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables

This commit is contained in:
Jeremy Stretch 2024-03-20 15:02:12 -04:00
commit 25a4e9448c
145 changed files with 2925 additions and 2005 deletions

View File

@ -17,15 +17,16 @@ body:
How are you running NetBox? (For issues with the Docker image, please go to the How are you running NetBox? (For issues with the Docker image, please go to the
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.) [netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
options: options:
- Self-hosted
- NetBox Cloud - NetBox Cloud
- NetBox Enterprise
- Self-hosted
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.7.3 placeholder: v3.7.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.7.3 placeholder: v3.7.4
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -84,4 +84,4 @@ jobs:
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
- name: Show coverage report - name: Show coverage report
run: coverage report --skip-covered --omit *migrations* run: coverage report --skip-covered --omit '*/migrations/*,*/tests/*'

View File

@ -101,7 +101,7 @@ markdown-include
mkdocs-material mkdocs-material
# Introspection for embedded code # Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md # https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
mkdocstrings[python-legacy] mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses

View File

@ -384,7 +384,10 @@
"8gfc-sfpp", "8gfc-sfpp",
"16gfc-sfpp", "16gfc-sfpp",
"32gfc-sfp28", "32gfc-sfp28",
"32gfc-sfpp",
"64gfc-qsfpp", "64gfc-qsfpp",
"64gfc-sfpdd",
"64gfc-sfpp",
"128gfc-qsfp28", "128gfc-qsfp28",
"infiniband-sdr", "infiniband-sdr",
"infiniband-ddr", "infiniband-ddr",

View File

@ -62,10 +62,11 @@ class Circuit(PrimaryModel):
1. Import `gettext_lazy` as `_`. 1. Import `gettext_lazy` as `_`.
2. All form fields must specify a `label` wrapped with `gettext_lazy()`. 2. All form fields must specify a `label` wrapped with `gettext_lazy()`.
3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`. 3. The name of each FieldSet on a form must be wrapped with `gettext_lazy()`.
```python ```python
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utilities.forms.rendering import FieldSet
class CircuitBulkEditForm(NetBoxModelBulkEditForm): class CircuitBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField( description = forms.CharField(
@ -74,7 +75,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
) )
fieldsets = ( fieldsets = (
(_('Circuit'), ('provider', 'type', 'status', 'description')), FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
) )
``` ```

View File

@ -31,8 +31,7 @@ This section entails the installation and configuration of a local PostgreSQL da
Once PostgreSQL has been installed, start the service and enable it to run at boot: Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight ```no-highlight
sudo systemctl start postgresql sudo systemctl enable --now postgresql
sudo systemctl enable postgresql
``` ```
Before continuing, verify that you have installed PostgreSQL 12 or later: Before continuing, verify that you have installed PostgreSQL 12 or later:

View File

@ -14,8 +14,7 @@
```no-highlight ```no-highlight
sudo yum install -y redis sudo yum install -y redis
sudo systemctl start redis sudo systemctl enable --now redis
sudo systemctl enable redis
``` ```
Before continuing, verify that your installed version of Redis is at least v4.0: Before continuing, verify that your installed version of Redis is at least v4.0:

View File

@ -27,8 +27,7 @@ sudo systemctl daemon-reload
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight ```no-highlight
sudo systemctl start netbox netbox-rq sudo systemctl enable --now netbox netbox-rq
sudo systemctl enable netbox netbox-rq
``` ```
You can use the command `systemctl status netbox` to verify that the WSGI service is running: You can use the command `systemctl status netbox` to verify that the WSGI service is running:

View File

@ -26,3 +26,7 @@ The location's operational status.
!!! tip !!! tip
Additional statuses may be defined by setting `Location.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. Additional statuses may be defined by setting `Location.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Facility
Data center or facility designation for identifying the location.

View File

@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following:
| Object | A single NetBox object of the type defined by `object_type` | | Object | A single NetBox object of the type defined by `object_type` |
| Multiple object | One or more NetBox objects of the type defined by `object_type` | | Multiple object | One or more NetBox objects of the type defined by `object_type` |
### Object Type ### Related Object Type
For object and multiple-object fields only. Designates the type of NetBox object being referenced. For object and multiple-object fields only. Designates the type of NetBox object being referenced.

View File

@ -16,15 +16,17 @@ NetBox provides several base form classes for use by plugins.
This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields. This is the base form for creating and editing NetBox models. It extends Django's ModelForm to add support for tags and custom fields.
| Attribute | Description | | Attribute | Description |
|-------------|-------------------------------------------------------------| |-------------|---------------------------------------------------------------------------------------|
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | | `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
**Example** **Example**
```python ```python
from django.utils.translation import gettext_lazy as _
from dcim.models import Site from dcim.models import Site
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField from utilities.forms.fields import CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from .models import MyModel from .models import MyModel
class MyModelForm(NetBoxModelForm): class MyModelForm(NetBoxModelForm):
@ -33,8 +35,8 @@ class MyModelForm(NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Model Stuff', ('name', 'status', 'site', 'tags')), FieldSet('name', 'status', 'site', 'tags', name=_('Model Stuff')),
('Tenancy', ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -52,6 +54,7 @@ This form facilitates the bulk import of new objects from CSV, JSON, or YAML dat
**Example** **Example**
```python ```python
from django.utils.translation import gettext_lazy as _
from dcim.models import Site from dcim.models import Site
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelChoiceField from utilities.forms import CSVModelChoiceField
@ -62,7 +65,7 @@ class MyModelImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text='Assigned site' help_text=_('Assigned site')
) )
class Meta: class Meta:
@ -77,16 +80,18 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
| Attribute | Description | | Attribute | Description |
|-------------------|---------------------------------------------------------------------------------------------| |-------------------|---------------------------------------------------------------------------------------------|
| `model` | The model of object being edited | | `model` | The model of object being edited |
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | | `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
| `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) | | `nullable_fields` | A tuple of fields which can be nullified (set to empty) using the bulk edit form (optional) |
**Example** **Example**
```python ```python
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import Site from dcim.models import Site
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from utilities.forms import CommentField, DynamicModelChoiceField from utilities.forms import CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from .models import MyModel, MyModelStatusChoices from .models import MyModel, MyModelStatusChoices
@ -106,7 +111,7 @@ class MyModelEditForm(NetBoxModelImportForm):
model = MyModel model = MyModel
fieldsets = ( fieldsets = (
('Model Stuff', ('name', 'status', 'site')), FieldSet('name', 'status', 'site', name=_('Model Stuff')),
) )
nullable_fields = ('site', 'comments') nullable_fields = ('site', 'comments')
``` ```
@ -116,9 +121,9 @@ class MyModelEditForm(NetBoxModelImportForm):
This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set. This form class is used to render a form expressly for filtering a list of objects. Its fields should correspond to filters defined on the model's filter set.
| Attribute | Description | | Attribute | Description |
|-------------------|-------------------------------------------------------------| |-------------|---------------------------------------------------------------------------------------|
| `model` | The model of object being edited | | `model` | The model of object being edited |
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) | | `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
**Example** **Example**
@ -206,3 +211,13 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
::: utilities.forms.fields.CSVMultipleContentTypeField ::: utilities.forms.fields.CSVMultipleContentTypeField
options: options:
members: false members: false
## Form Rendering
::: utilities.forms.rendering.FieldSet
::: utilities.forms.rendering.InlineFields
::: utilities.forms.rendering.TabbedGroups
::: utilities.forms.rendering.ObjectAttribute

View File

@ -1,6 +1,31 @@
# NetBox v3.7 # NetBox v3.7
## v3.7.4 (FUTURE) ## v3.7.5 (FUTURE)
---
## v3.7.4 (2024-03-13)
### Enhancements
* [#14206](https://github.com/netbox-community/netbox/issues/14206) - Add additional FibreChannel SFP+ interface types
* [#14366](https://github.com/netbox-community/netbox/issues/14366) - Enable custom links for config contexts & templates
* [#15291](https://github.com/netbox-community/netbox/issues/15291) - Add tunnel termination buttons to VM interfaces table
* [#15297](https://github.com/netbox-community/netbox/issues/15297) - Linkify platform column in device & virtual machine tables
### Bug Fixes
* [#13722](https://github.com/netbox-community/netbox/issues/13722) - Fix range expansion for comma-separated numerical values
* [#14832](https://github.com/netbox-community/netbox/issues/14832) - Enable querying IP addresses for an FHRP group via GraphQL
* [#15220](https://github.com/netbox-community/netbox/issues/15220) - Fix validation check when bulk editing the mask length of IP addresses
* [#15232](https://github.com/netbox-community/netbox/issues/15232) - Permit user with sufficient permissions to assign an inventory item to a device type
* [#15241](https://github.com/netbox-community/netbox/issues/15241) - Restore missing `display` field on VirtualDisk serialization in REST API
* [#15243](https://github.com/netbox-community/netbox/issues/15243) - Correct representation of installed module when listing module bays using REST API brief mode
* [#15316](https://github.com/netbox-community/netbox/issues/15316) - Fix selection of 3DES encryption for IKE & IPSec proposals
* [#15322](https://github.com/netbox-community/netbox/issues/15322) - Add description field to YAML export for device & module types
* [#15336](https://github.com/netbox-community/netbox/issues/15336) - Correct label for recurring scheduled jobs
* [#15347](https://github.com/netbox-community/netbox/issues/15347) - Fix querying virtual machine contacts via GraphQL
* [#15356](https://github.com/netbox-community/netbox/issues/15356) - Fix assignment of front & rear images to device types via REST API
--- ---

View File

@ -6,6 +6,7 @@
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.) * The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade. * The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
* The `parent` and `parent_id` filters for locations now return only immediate children of the specified location. (Use `ancestor` and `ancestor_id` to return _all_ descendants.)
### New Features ### New Features
@ -17,18 +18,26 @@ The NetBox user interface has been completely refreshed and updated.
The REST API now supports specifying which fields to include in the response data. The REST API now supports specifying which fields to include in the response data.
#### Advanced FieldSet Functionality ([#14739](https://github.com/netbox-community/netbox/issues/14739))
New resources have been introduced to enable advanced form rendering without a need for custom HTML templates.
### Enhancements ### Enhancements
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields * [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields
* [#13918](https://github.com/netbox-community/netbox/issues/13918) - Add `facility` field to Location model
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection * [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection
* [#14454](https://github.com/netbox-community/netbox/issues/14454) - Include member devices for virtual chassis in REST API
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0 * [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects * [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets * [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
* [#15237](https://github.com/netbox-community/netbox/issues/15237) - Ensure consistent filtering ability for all model fields
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations * [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters)
### Other Changes ### Other Changes
@ -44,6 +53,7 @@ The REST API now supports specifying which fields to include in the response dat
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
* [#15193](https://github.com/netbox-community/netbox/issues/15193) - Switch to compiled distribution of the `psycopg` library
* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names * [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names
* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6) * [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6)

View File

@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -95,7 +95,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ProviderAccount model = ProviderAccount
fields = ['id', 'name', 'account', 'description'] fields = ('id', 'name', 'account', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -122,7 +122,7 @@ class ProviderNetworkFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = ['id', 'name', 'service_id', 'description'] fields = ('id', 'name', 'service_id', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -139,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -158,6 +158,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'), label=_('Provider account (ID)'),
) )
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter( provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network', field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(), queryset=ProviderNetwork.objects.all(),
@ -214,10 +220,18 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
termination_a_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
label=_('Termination A (ID)'),
)
termination_z_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
label=_('Termination A (ID)'),
)
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate'] fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -258,7 +272,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end'] fields = (
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
'pp_info', 'cable_end',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -8,6 +8,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import add_blank_choice from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
@ -34,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('asns', 'description')), FieldSet('asns', 'description'),
) )
nullable_fields = ( nullable_fields = (
'asns', 'description', 'comments', 'asns', 'description', 'comments',
@ -56,7 +57,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
model = ProviderAccount model = ProviderAccount
fieldsets = ( fieldsets = (
(None, ('provider', 'description')), FieldSet('provider', 'description'),
) )
nullable_fields = ( nullable_fields = (
'description', 'comments', 'description', 'comments',
@ -83,7 +84,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (
(None, ('provider', 'service_id', 'description')), FieldSet('provider', 'service_id', 'description'),
) )
nullable_fields = ( nullable_fields = (
'service_id', 'description', 'comments', 'service_id', 'description', 'comments',
@ -103,7 +104,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
model = CircuitType model = CircuitType
fieldsets = ( fieldsets = (
(None, ('color', 'description')), FieldSet('color', 'description'),
) )
nullable_fields = ('color', 'description') nullable_fields = ('color', 'description')
@ -164,9 +165,9 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
(_('Circuit'), ('provider', 'type', 'status', 'description')), FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
(_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')), FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
(_('Tenancy'), ('tenant',)), FieldSet('tenant', name=_('Tenancy')),
) )
nullable_fields = ( nullable_fields = (
'tenant', 'commit_rate', 'description', 'comments', 'tenant', 'commit_rate', 'description', 'comments',

View File

@ -8,6 +8,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
@ -22,10 +23,10 @@ __all__ = (
class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
(_('ASN'), ('asn',)), FieldSet('asn', name=_('ASN')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -61,8 +62,8 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class ProviderAccountFilterForm(NetBoxModelFilterSetForm): class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
model = ProviderAccount model = ProviderAccount
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('provider_id', 'account')), FieldSet('provider_id', 'account', name=_('Attributes')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
@ -79,8 +80,8 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('provider_id', 'service_id')), FieldSet('provider_id', 'service_id', name=_('Attributes')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
@ -98,8 +99,8 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
class CircuitTypeFilterForm(NetBoxModelFilterSetForm): class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = CircuitType model = CircuitType
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('color',)), FieldSet('color', name=_('Attributes')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -112,12 +113,12 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
(_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')), FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
) )
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id') selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(

View File

@ -7,6 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import DatePicker, NumberWithOptions from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
@ -29,7 +30,7 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')), FieldSet('name', 'slug', 'asns', 'description', 'tags'),
) )
class Meta: class Meta:
@ -61,7 +62,7 @@ class ProviderNetworkForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')), FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
) )
class Meta: class Meta:
@ -75,9 +76,7 @@ class CircuitTypeForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Circuit Type'), ( FieldSet('name', 'slug', 'color', 'description', 'tags'),
'name', 'slug', 'color', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -107,9 +106,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')), FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')), FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -146,6 +145,18 @@ class CircuitTerminationForm(NetBoxModelForm):
selector=True selector=True
) )
fieldsets = (
FieldSet(
'circuit', 'term_side', 'description', 'tags',
TabbedGroups(
FieldSet('site', name=_('Site')),
FieldSet('provider_network', name=_('Provider Network')),
),
'mark_connected', name=_('Circuit Termination')
),
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [

View File

@ -330,6 +330,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet filterset = CircuitTerminationFilterSet
ignore_fields = ('cable',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -412,7 +412,6 @@ class CircuitContactsView(ObjectContactsView):
class CircuitTerminationEditView(generic.ObjectEditView): class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
form = forms.CircuitTerminationForm form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@register_model_view(CircuitTermination, 'delete') @register_model_view(CircuitTermination, 'delete')

View File

@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DataSource model = DataSource
fields = ('id', 'name', 'enabled', 'description') fields = ('id', 'name', 'enabled', 'description', 'source_url', 'last_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -115,7 +115,7 @@ class JobFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Job model = Job
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user') fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -134,9 +134,7 @@ class ConfigRevisionFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConfigRevision model = ConfigRevision
fields = [ fields = ('id', 'created', 'comment')
'id',
]
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -5,6 +5,7 @@ from core.models import *
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from netbox.utils import get_data_backend_choices from netbox.utils import get_data_backend_choices
from utilities.forms.fields import CommentField from utilities.forms.fields import CommentField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect from utilities.forms.widgets import BulkEditNullBooleanSelect
__all__ = ( __all__ = (
@ -41,7 +42,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
model = DataSource model = DataSource
fieldsets = ( fieldsets = (
(None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')), FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'),
) )
nullable_fields = ( nullable_fields = (
'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules', 'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules',

View File

@ -9,7 +9,8 @@ from netbox.forms.mixins import SavedFiltersMixin
from netbox.utils import get_data_backend_choices from netbox.utils import get_data_backend_choices
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import APISelectMultiple, DateTimePicker from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker
__all__ = ( __all__ = (
'ConfigRevisionFilterForm', 'ConfigRevisionFilterForm',
@ -22,8 +23,8 @@ __all__ = (
class DataSourceFilterForm(NetBoxModelFilterSetForm): class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource model = DataSource
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Data Source'), ('type', 'status')), FieldSet('type', 'status', name=_('Data Source')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -47,8 +48,8 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
class DataFileFilterForm(NetBoxModelFilterSetForm): class DataFileFilterForm(NetBoxModelFilterSetForm):
model = DataFile model = DataFile
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('File'), ('source_id',)), FieldSet('source_id', name=_('File')),
) )
source_id = DynamicModelMultipleChoiceField( source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -59,12 +60,12 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
class JobFilterForm(SavedFiltersMixin, FilterForm): class JobFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ('object_type', 'status')), FieldSet('object_type', 'status', name=_('Attributes')),
(_('Creation'), ( FieldSet(
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before', 'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user', 'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
)), ),
) )
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object Type'), label=_('Object Type'),
@ -125,5 +126,5 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm): class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
) )

View File

@ -13,6 +13,7 @@ from netbox.registry import registry
from netbox.utils import get_data_backend_choices from netbox.utils import get_data_backend_choices
from utilities.forms import get_field_value from utilities.forms import get_field_value
from utilities.forms.fields import CommentField from utilities.forms.fields import CommentField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect from utilities.forms.widgets import HTMXSelect
__all__ = ( __all__ = (
@ -49,11 +50,11 @@ class DataSourceForm(NetBoxModelForm):
@property @property
def fieldsets(self): def fieldsets(self):
fieldsets = [ fieldsets = [
(_('Source'), ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')), FieldSet('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules', name=_('Source')),
] ]
if self.backend_fields: if self.backend_fields:
fieldsets.append( fieldsets.append(
(_('Backend Parameters'), self.backend_fields) FieldSet(*self.backend_fields, name=_('Backend Parameters'))
) )
return fieldsets return fieldsets
@ -91,8 +92,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('File Upload'), ('upload_file',)), FieldSet('upload_file', name=_('File Upload')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
) )
class Meta: class Meta:
@ -144,18 +145,24 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
""" """
fieldsets = ( fieldsets = (
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')), FieldSet(
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')), 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', name=_('Rack Elevations')
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')), ),
(_('Security'), ('ALLOWED_URL_SCHEMES',)), FieldSet(
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')), 'POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION',
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')), name=_('Power')
(_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')), ),
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)), FieldSet('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4', name=_('IPAM')),
(_('Miscellaneous'), ( FieldSet('ALLOWED_URL_SCHEMES', name=_('Security')),
FieldSet('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM', name=_('Banners')),
FieldSet('PAGINATE_COUNT', 'MAX_PAGE_SIZE', name=_('Pagination')),
FieldSet('CUSTOM_VALIDATORS', 'PROTECTION_RULES', name=_('Validation')),
FieldSet('DEFAULT_USER_PREFERENCES', name=_('User Preferences')),
FieldSet(
'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL', 'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
)), name=_('Miscellaneous')
(_('Config Revision'), ('comment',)) ),
FieldSet('comment', name=_('Config Revision'))
) )
class Meta: class Meta:

View File

@ -10,6 +10,7 @@ from ..models import *
class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests): class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DataSource.objects.all() queryset = DataSource.objects.all()
filterset = DataSourceFilterSet filterset = DataSourceFilterSet
ignore_fields = ('ignore_rules', 'parameters')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -70,6 +71,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests): class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DataFile.objects.all() queryset = DataFile.objects.all()
filterset = DataFileFilterSet filterset = DataFileFilterSet
ignore_fields = ('data',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -309,6 +309,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'serial']
class NestedModuleSerializer(WritableNestedSerializer): class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
@ -392,11 +400,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class NestedModuleBaySerializer(WritableNestedSerializer): class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True) installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = models.ModuleBay model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name'] fields = ['id', 'url', 'display', 'installed_module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):

View File

@ -28,8 +28,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
front_image = serializers.URLField(allow_null=True, required=False) front_image = serializers.ImageField(required=False, allow_null=True)
rear_image = serializers.URLField(allow_null=True, required=False) rear_image = serializers.ImageField(required=False, allow_null=True)
# Counter fields # Counter fields
console_port_template_count = serializers.IntegerField(read_only=True) console_port_template_count = serializers.IntegerField(read_only=True)

View File

@ -92,7 +92,7 @@ class LocationSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = Location model = Location
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description',
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

View File

@ -12,6 +12,7 @@ __all__ = (
class VirtualChassisSerializer(NetBoxModelSerializer): class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False, allow_null=True, default=None) master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
members = NestedDeviceSerializer(many=True, read_only=True)
# Counter fields # Counter fields
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
@ -20,6 +21,6 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
model = VirtualChassis model = VirtualChassis
fields = [ fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'member_count', 'created', 'last_updated', 'member_count', 'members',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')

View File

@ -511,7 +511,10 @@ class CableTerminationViewSet(NetBoxModelViewSet):
# #
class VirtualChassisViewSet(NetBoxModelViewSet): class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.all() queryset = VirtualChassis.objects.prefetch_related(
# Prefetch related object for the display of unnamed devices
'master__virtual_chassis',
)
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet filterset_class = filtersets.VirtualChassisFilterSet

View File

@ -889,7 +889,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28' TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_32GFC_SFP_PLUS = '32gfc-sfpp'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp' TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_64GFC_SFP_DD = '64gfc-sfpdd'
TYPE_64GFC_SFP_PLUS = '64gfc-sfpp'
TYPE_128GFC_QSFP28 = '128gfc-qsfp28' TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
# InfiniBand # InfiniBand
@ -1058,7 +1061,10 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_32GFC_SFP_PLUS, 'SFP+ (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'), (TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_64GFC_SFP_DD, 'SFP-DD (64GFC)'),
(TYPE_64GFC_SFP_PLUS, 'SFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
) )
), ),

View File

@ -18,11 +18,12 @@ from tenancy.models import *
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
TreeNodeMultipleChoiceFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from vpn.models import L2VPN from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import * from .models import *
@ -89,10 +90,23 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Parent region (slug)'), label=_('Parent region (slug)'),
) )
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='parent',
lookup_expr='in',
label=_('Region (ID)'),
)
ancestor = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='parent',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
@ -106,10 +120,23 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Parent site group (slug)'), label=_('Parent site group (slug)'),
) )
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='parent',
lookup_expr='in',
label=_('Site group (ID)'),
)
ancestor = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='parent',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -152,12 +179,11 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
label=_('AS (ID)'), label=_('AS (ID)'),
) )
time_zone = MultiValueCharFilter()
class Meta: class Meta:
model = Site model = Site
fields = ( fields = ('id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description')
'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description'
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -214,13 +240,23 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
parent_id = TreeNodeMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Location.objects.all(),
label=_('Parent location (ID)'),
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label=_('Parent location (slug)'),
)
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(), queryset=Location.objects.all(),
field_name='parent', field_name='parent',
lookup_expr='in', lookup_expr='in',
label=_('Location (ID)'), label=_('Location (ID)'),
) )
parent = TreeNodeMultipleChoiceFilter( ancestor = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(), queryset=Location.objects.all(),
field_name='parent', field_name='parent',
lookup_expr='in', lookup_expr='in',
@ -234,13 +270,14 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
class Meta: class Meta:
model = Location model = Location
fields = ['id', 'name', 'slug', 'status', 'description'] fields = ('id', 'name', 'slug', 'status', 'facility', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(facility__icontains=value) |
Q(description__icontains=value) Q(description__icontains=value)
) )
@ -249,7 +286,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -328,10 +365,10 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -411,10 +448,14 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='username', to_field_name='username',
label=_('User (name)'), label=_('User (name)'),
) )
unit = NumericArrayFilter(
field_name='units',
lookup_expr='contains'
)
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'created', 'description'] fields = ('id', 'created', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -431,7 +472,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class DeviceTypeFilterSet(NetBoxModelFilterSet): class DeviceTypeFilterSet(NetBoxModelFilterSet):
@ -502,10 +543,22 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = (
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description',
]
# Counters
'console_port_template_count',
'console_server_port_template_count',
'power_port_template_count',
'power_outlet_template_count',
'interface_template_count',
'front_port_template_count',
'rear_port_template_count',
'device_bay_template_count',
'module_bay_template_count',
'inventory_item_template_count',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -599,7 +652,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description'] fields = ('id', 'model', 'part_number', 'weight', 'weight_unit', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -639,12 +692,15 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
devicetype_id = django_filters.ModelMultipleChoiceFilter( device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
field_name='device_type_id', field_name='device_type_id',
label=_('Device type (ID)'), label=_('Device type (ID)'),
) )
# TODO: Remove in v4.1
devicetype_id = device_type_id
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
@ -655,32 +711,35 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet): class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
moduletype_id = django_filters.ModelMultipleChoiceFilter( module_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
field_name='module_type_id', field_name='module_type_id',
label=_('Module type (ID)'), label=_('Module type (ID)'),
) )
# TODO: Remove in v4.1
moduletype_id = module_type_id
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type', 'description'] fields = ('id', 'name', 'label', 'type', 'description')
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type', 'description'] fields = ('id', 'name', 'label', 'type', 'description')
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -688,10 +747,14 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
) )
power_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=PowerPortTemplate.objects.all(),
label=_('Power port (ID)'),
)
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'type', 'feed_leg', 'description'] fields = ('id', 'name', 'label', 'type', 'feed_leg', 'description')
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -715,7 +778,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description'] fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -723,10 +786,13 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
) )
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
)
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'name', 'type', 'color', 'description'] fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -737,21 +803,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'name', 'type', 'color', 'positions', 'description'] fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ModuleBayTemplate model = ModuleBayTemplate
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'label', 'position', 'description')
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'label', 'description')
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
@ -784,7 +850,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
class Meta: class Meta:
model = InventoryItemTemplate model = InventoryItemTemplate
fields = ['id', 'name', 'label', 'part_id', 'description'] fields = ('id', 'name', 'label', 'part_id', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -805,7 +871,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description'] fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
class PlatformFilterSet(OrganizationalModelFilterSet): class PlatformFilterSet(OrganizationalModelFilterSet):
@ -831,7 +897,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
@extend_schema_field(OpenApiTypes.STR) @extend_schema_field(OpenApiTypes.STR)
def get_for_device_type(self, queryset, name, value): def get_for_device_type(self, queryset, name, value):
@ -943,6 +1009,11 @@ class DeviceFilterSet(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label=_('Rack (ID)'), label=_('Rack (ID)'),
) )
parent_bay_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent_bay',
queryset=DeviceBay.objects.all(),
label=_('Parent bay (ID)'),
)
cluster_id = django_filters.ModelMultipleChoiceFilter( cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label=_('VM cluster (ID)'), label=_('VM cluster (ID)'),
@ -1032,10 +1103,22 @@ class DeviceFilterSet(
class Meta: class Meta:
model = Device model = Device
fields = [ fields = (
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority', 'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
'description', 'description',
]
# Counters
'console_port_count',
'console_server_port_count',
'power_port_count',
'power_outlet_count',
'interface_count',
'front_port_count',
'rear_port_count',
'device_bay_count',
'module_bay_count',
'inventory_item_count',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1098,24 +1181,29 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
field_name='device', field_name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='VDC (ID)', label=_('VDC (ID)')
) )
device = django_filters.ModelMultipleChoiceFilter( device = django_filters.ModelMultipleChoiceFilter(
field_name='device', field_name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device model', label=_('Device model')
)
interface_id = django_filters.ModelMultipleChoiceFilter(
field_name='interfaces',
queryset=Interface.objects.all(),
label=_('Interface (ID)')
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VirtualDeviceContextStatusChoices choices=VirtualDeviceContextStatusChoices
) )
has_primary_ip = django_filters.BooleanFilter( has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip', method='_has_primary_ip',
label='Has a primary IP', label=_('Has a primary IP')
) )
class Meta: class Meta:
model = VirtualDeviceContext model = VirtualDeviceContext
fields = ['id', 'device', 'name', 'description'] fields = ('id', 'device', 'name', 'identifier', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1181,7 +1269,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = Module model = Module
fields = ['id', 'status', 'asset_tag', 'description'] fields = ('id', 'status', 'asset_tag', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1325,6 +1413,10 @@ class ModularDeviceComponentFilterSet(DeviceComponentFilterSet):
class CabledObjectFilterSet(django_filters.FilterSet): class CabledObjectFilterSet(django_filters.FilterSet):
cable_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cable.objects.all(),
label=_('Cable (ID)'),
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -1366,7 +1458,7 @@ class ConsolePortFilterSet(
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['id', 'name', 'label', 'description', 'cable_end'] fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
class ConsoleServerPortFilterSet( class ConsoleServerPortFilterSet(
@ -1382,7 +1474,7 @@ class ConsoleServerPortFilterSet(
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ['id', 'name', 'label', 'description', 'cable_end'] fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
class PowerPortFilterSet( class PowerPortFilterSet(
@ -1398,7 +1490,9 @@ class PowerPortFilterSet(
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
)
class PowerOutletFilterSet( class PowerOutletFilterSet(
@ -1415,10 +1509,16 @@ class PowerOutletFilterSet(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
) )
power_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=PowerPort.objects.all(),
label=_('Power port (ID)'),
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end',
)
class CommonInterfaceFilterSet(django_filters.FilterSet): class CommonInterfaceFilterSet(django_filters.FilterSet):
@ -1533,27 +1633,37 @@ class InterfaceFilterSet(
vdc_id = django_filters.ModelMultipleChoiceFilter( vdc_id = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs', field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
label='Virtual Device Context', label=_('Virtual Device Context')
) )
vdc_identifier = django_filters.ModelMultipleChoiceFilter( vdc_identifier = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__identifier', field_name='vdcs__identifier',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
to_field_name='identifier', to_field_name='identifier',
label='Virtual Device Context (Identifier)', label=_('Virtual Device Context (Identifier)')
) )
vdc = django_filters.ModelMultipleChoiceFilter( vdc = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__name', field_name='vdcs__name',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
to_field_name='name', to_field_name='name',
label='Virtual Device Context', label=_('Virtual Device Context')
)
wireless_lan_id = django_filters.ModelMultipleChoiceFilter(
field_name='wireless_lans',
queryset=WirelessLAN.objects.all(),
label=_('Wireless LAN')
)
wireless_link_id = django_filters.ModelMultipleChoiceFilter(
queryset=WirelessLink.objects.all(),
label=_('Wireless link')
) )
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = (
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role', 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
] 'cable_id', 'cable_end',
)
def filter_virtual_chassis_member(self, queryset, name, value): def filter_virtual_chassis_member(self, queryset, name, value):
try: try:
@ -1582,10 +1692,15 @@ class FrontPortFilterSet(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
) )
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
)
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
)
class RearPortFilterSet( class RearPortFilterSet(
@ -1600,21 +1715,38 @@ class RearPortFilterSet(
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
)
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
installed_module_id = django_filters.ModelMultipleChoiceFilter(
field_name='installed_module',
queryset=ModuleBay.objects.all(),
label=_('Installed module (ID)'),
)
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = ['id', 'name', 'label', 'description'] fields = ('id', 'name', 'label', 'position', 'description')
class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
installed_device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label=_('Installed device (ID)'),
)
installed_device = django_filters.ModelMultipleChoiceFilter(
field_name='installed_device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Installed device (name)'),
)
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'label', 'description'] fields = ('id', 'name', 'label', 'description')
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
@ -1650,7 +1782,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered'] fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1669,7 +1801,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = InventoryItemRole model = InventoryItemRole
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class VirtualChassisFilterSet(NetBoxModelFilterSet): class VirtualChassisFilterSet(NetBoxModelFilterSet):
@ -1734,7 +1866,7 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'domain', 'name', 'description'] fields = ('id', 'domain', 'name', 'description', 'member_count')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1839,7 +1971,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = Cable model = Cable
fields = ['id', 'label', 'length', 'length_unit', 'description'] fields = ('id', 'label', 'length', 'length_unit', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1917,12 +2049,12 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return self.filter_by_termination_object(queryset, CircuitTermination, value) return self.filter_by_termination_object(queryset, CircuitTermination, value)
class CableTerminationFilterSet(BaseFilterSet): class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
termination_type = ContentTypeFilter() termination_type = ContentTypeFilter()
class Meta: class Meta:
model = CableTermination model = CableTermination
fields = ['id', 'cable', 'cable_end', 'termination_type', 'termination_id'] fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
@ -1971,7 +2103,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -2037,10 +2169,10 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = (
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end', 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
'description', 'available_power', 'mark_connected', 'cable_end', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -2099,18 +2231,18 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['name'] fields = ('name',)
class PowerConnectionFilterSet(ConnectionFilterSet): class PowerConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['name'] fields = ('name',)
class InterfaceConnectionFilterSet(ConnectionFilterSet): class InterfaceConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = [] fields = tuple()

View File

@ -13,6 +13,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from wireless.choices import WirelessRoleChoices from wireless.choices import WirelessRoleChoices
@ -75,7 +76,7 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
model = Region model = Region
fieldsets = ( fieldsets = (
(None, ('parent', 'description')), FieldSet('parent', 'description'),
) )
nullable_fields = ('parent', 'description') nullable_fields = ('parent', 'description')
@ -94,7 +95,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
model = SiteGroup model = SiteGroup
fieldsets = ( fieldsets = (
(None, ('parent', 'description')), FieldSet('parent', 'description'),
) )
nullable_fields = ('parent', 'description') nullable_fields = ('parent', 'description')
@ -154,7 +155,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description')), FieldSet('status', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description'),
) )
nullable_fields = ( nullable_fields = (
'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments', 'region', 'group', 'tenant', 'asns', 'time_zone', 'description', 'comments',
@ -194,7 +195,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
model = Location model = Location
fieldsets = ( fieldsets = (
(None, ('site', 'parent', 'status', 'tenant', 'description')), FieldSet('site', 'parent', 'status', 'tenant', 'description'),
) )
nullable_fields = ('parent', 'tenant', 'description') nullable_fields = ('parent', 'tenant', 'description')
@ -212,7 +213,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
model = RackRole model = RackRole
fieldsets = ( fieldsets = (
(None, ('color', 'description')), FieldSet('color', 'description'),
) )
nullable_fields = ('color', 'description') nullable_fields = ('color', 'description')
@ -341,12 +342,13 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
model = Rack model = Rack
fieldsets = ( fieldsets = (
(_('Rack'), ('status', 'role', 'tenant', 'serial', 'asset_tag', 'description')), FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
(_('Location'), ('region', 'site_group', 'site', 'location')), FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
(_('Hardware'), ( FieldSet(
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
)), name=_('Hardware')
(_('Weight'), ('weight', 'max_weight', 'weight_unit')), ),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight', 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
@ -376,7 +378,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
model = RackReservation model = RackReservation
fieldsets = ( fieldsets = (
(None, ('user', 'tenant', 'description')), FieldSet('user', 'tenant', 'description'),
) )
nullable_fields = ('comments',) nullable_fields = ('comments',)
@ -390,7 +392,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
model = Manufacturer model = Manufacturer
fieldsets = ( fieldsets = (
(None, ('description',)), FieldSet('description'),
) )
nullable_fields = ('description',) nullable_fields = ('description',)
@ -450,11 +452,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
(_('Device Type'), ( FieldSet(
'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'airflow', 'description', 'airflow', 'description', name=_('Device Type')
)), ),
(_('Weight'), ('weight', 'weight_unit')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments') nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
@ -489,8 +491,8 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
(_('Module Type'), ('manufacturer', 'part_number', 'description')), FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')),
(_('Weight'), ('weight', 'weight_unit')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments') nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
@ -518,7 +520,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
model = DeviceRole model = DeviceRole
fieldsets = ( fieldsets = (
(None, ('color', 'vm_role', 'config_template', 'description')), FieldSet('color', 'vm_role', 'config_template', 'description'),
) )
nullable_fields = ('color', 'config_template', 'description') nullable_fields = ('color', 'config_template', 'description')
@ -542,7 +544,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
model = Platform model = Platform
fieldsets = ( fieldsets = (
(None, ('manufacturer', 'config_template', 'description')), FieldSet('manufacturer', 'config_template', 'description'),
) )
nullable_fields = ('manufacturer', 'config_template', 'description') nullable_fields = ('manufacturer', 'config_template', 'description')
@ -621,10 +623,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
model = Device model = Device
fieldsets = ( fieldsets = (
(_('Device'), ('role', 'status', 'tenant', 'platform', 'description')), FieldSet('role', 'status', 'tenant', 'platform', 'description', name=_('Device')),
(_('Location'), ('site', 'location')), FieldSet('site', 'location', name=_('Location')),
(_('Hardware'), ('manufacturer', 'device_type', 'airflow', 'serial')), FieldSet('manufacturer', 'device_type', 'airflow', 'serial', name=_('Hardware')),
(_('Configuration'), ('config_template',)), FieldSet('config_template', name=_('Configuration')),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments', 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',
@ -668,7 +670,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('manufacturer', 'module_type', 'status', 'serial', 'description')), FieldSet('manufacturer', 'module_type', 'status', 'serial', 'description'),
) )
nullable_fields = ('serial', 'description', 'comments') nullable_fields = ('serial', 'description', 'comments')
@ -720,8 +722,8 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
model = Cable model = Cable
fieldsets = ( fieldsets = (
(None, ('type', 'status', 'tenant', 'label', 'description')), FieldSet('type', 'status', 'tenant', 'label', 'description'),
(_('Attributes'), ('color', 'length', 'length_unit')), FieldSet('color', 'length', 'length_unit', name=_('Attributes')),
) )
nullable_fields = ( nullable_fields = (
'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments', 'type', 'status', 'tenant', 'label', 'color', 'length', 'description', 'comments',
@ -743,7 +745,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
model = VirtualChassis model = VirtualChassis
fieldsets = ( fieldsets = (
(None, ('domain', 'description')), FieldSet('domain', 'description'),
) )
nullable_fields = ('domain', 'description', 'comments') nullable_fields = ('domain', 'description', 'comments')
@ -791,7 +793,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('region', 'site_group', 'site', 'location', 'description')), FieldSet('region', 'site_group', 'site', 'location', 'description'),
) )
nullable_fields = ('location', 'description', 'comments') nullable_fields = ('location', 'description', 'comments')
@ -861,8 +863,8 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant')), FieldSet('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant'),
(_('Power'), ('supply', 'phase', 'voltage', 'amperage', 'max_utilization')) FieldSet('supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Power'))
) )
nullable_fields = ('location', 'tenant', 'description', 'comments') nullable_fields = ('location', 'tenant', 'description', 'comments')
@ -1210,7 +1212,7 @@ class ConsolePortBulkEditForm(
model = ConsolePort model = ConsolePort
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'),
) )
nullable_fields = ('module', 'label', 'description') nullable_fields = ('module', 'label', 'description')
@ -1227,7 +1229,7 @@ class ConsoleServerPortBulkEditForm(
model = ConsoleServerPort model = ConsoleServerPort
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'speed', 'description', 'mark_connected'),
) )
nullable_fields = ('module', 'label', 'description') nullable_fields = ('module', 'label', 'description')
@ -1244,8 +1246,8 @@ class PowerPortBulkEditForm(
model = PowerPort model = PowerPort
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
(_('Power'), ('maximum_draw', 'allocated_draw')), FieldSet('maximum_draw', 'allocated_draw', name=_('Power')),
) )
nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw') nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw')
@ -1262,8 +1264,8 @@ class PowerOutletBulkEditForm(
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
(_('Power'), ('feed_leg', 'power_port')), FieldSet('feed_leg', 'power_port', name=_('Power')),
) )
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description') nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
@ -1395,20 +1397,21 @@ class InterfaceBulkEditForm(
model = Interface model = Interface
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')), FieldSet('module', 'type', 'label', 'speed', 'duplex', 'description'),
(_('Addressing'), ('vrf', 'mac_address', 'wwn')), FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
(_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
(_('PoE'), ('poe_mode', 'poe_type')), FieldSet('poe_mode', 'poe_type', name=_('PoE')),
(_('Related Interfaces'), ('parent', 'bridge', 'lag')), FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
(_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
(_('Wireless'), ( FieldSet(
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
)), name=_('Wireless')
),
) )
nullable_fields = ( nullable_fields = (
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description', 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu',
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'description', 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tagged_vlans', 'vrf', 'wireless_lans' 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans'
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -1488,7 +1491,7 @@ class FrontPortBulkEditForm(
model = FrontPort model = FrontPort
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'),
) )
nullable_fields = ('module', 'label', 'description', 'color') nullable_fields = ('module', 'label', 'description', 'color')
@ -1505,7 +1508,7 @@ class RearPortBulkEditForm(
model = RearPort model = RearPort
fieldsets = ( fieldsets = (
(None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), FieldSet('module', 'type', 'label', 'color', 'description', 'mark_connected'),
) )
nullable_fields = ('module', 'label', 'description', 'color') nullable_fields = ('module', 'label', 'description', 'color')
@ -1516,7 +1519,7 @@ class ModuleBayBulkEditForm(
): ):
model = ModuleBay model = ModuleBay
fieldsets = ( fieldsets = (
(None, ('label', 'position', 'description')), FieldSet('label', 'position', 'description'),
) )
nullable_fields = ('label', 'position', 'description') nullable_fields = ('label', 'position', 'description')
@ -1527,7 +1530,7 @@ class DeviceBayBulkEditForm(
): ):
model = DeviceBay model = DeviceBay
fieldsets = ( fieldsets = (
(None, ('label', 'description')), FieldSet('label', 'description'),
) )
nullable_fields = ('label', 'description') nullable_fields = ('label', 'description')
@ -1554,7 +1557,7 @@ class InventoryItemBulkEditForm(
model = InventoryItem model = InventoryItem
fieldsets = ( fieldsets = (
(None, ('device', 'label', 'role', 'manufacturer', 'part_id', 'description')), FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'),
) )
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
@ -1576,7 +1579,7 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
model = InventoryItemRole model = InventoryItemRole
fieldsets = ( fieldsets = (
(None, ('color', 'description')), FieldSet('color', 'description'),
) )
nullable_fields = ('color', 'description') nullable_fields = ('color', 'description')
@ -1599,6 +1602,6 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
) )
model = VirtualDeviceContext model = VirtualDeviceContext
fieldsets = ( fieldsets = (
(None, ('device', 'status', 'tenant')), FieldSet('device', 'status', 'tenant'),
) )
nullable_fields = ('device', 'tenant', ) nullable_fields = ('device', 'tenant', )

View File

@ -157,7 +157,7 @@ class LocationImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Location model = Location
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags') fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)

View File

@ -12,7 +12,8 @@ from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import NumberWithOptions
from vpn.models import L2VPN from vpn.models import L2VPN
from wireless.choices import * from wireless.choices import *
@ -132,8 +133,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region model = Region
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')), FieldSet('q', 'filter_id', 'tag', 'parent_id'),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')) FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -146,8 +147,8 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup model = SiteGroup
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')), FieldSet('q', 'filter_id', 'tag', 'parent_id'),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')) FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
@ -160,10 +161,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('status', 'region_id', 'group_id', 'asn_id')), FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
) )
selector_fields = ('filter_id', 'q', 'region_id', 'group_id') selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
@ -192,10 +193,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Location model = Location
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')), FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -241,13 +242,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Rack model = Rack
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
(_('Function'), ('status', 'role_id')), FieldSet('status', 'role_id', name=_('Function')),
(_('Hardware'), ('type', 'width', 'serial', 'asset_tag')), FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
(_('Weight'), ('weight', 'max_weight', 'weight_unit')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
) )
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id') selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
@ -326,13 +327,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class RackElevationFilterForm(RackFilterForm): class RackElevationFilterForm(RackFilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')),
(_('Function'), ('status', 'role_id')), FieldSet('status', 'role_id', name=_('Function')),
(_('Hardware'), ('type', 'width', 'serial', 'asset_tag')), FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
(_('Weight'), ('weight', 'max_weight', 'weight_unit')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
) )
id = DynamicModelMultipleChoiceField( id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
@ -348,10 +349,10 @@ class RackElevationFilterForm(RackFilterForm):
class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation model = RackReservation
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('User'), ('user_id',)), FieldSet('user_id', name=_('User')),
(_('Rack'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -401,8 +402,8 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer model = Manufacturer
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')) FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -410,14 +411,16 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class DeviceTypeFilterForm(NetBoxModelFilterSetForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Hardware'), ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')), FieldSet(
(_('Images'), ('has_front_image', 'has_rear_image')), 'manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow', name=_('Hardware')
(_('Components'), ( ),
FieldSet('has_front_image', 'has_rear_image', name=_('Images')),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', 'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
)), ),
(_('Weight'), ('weight', 'weight_unit')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
selector_fields = ('filter_id', 'q', 'manufacturer_id') selector_fields = ('filter_id', 'q', 'manufacturer_id')
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
@ -536,13 +539,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Hardware'), ('manufacturer_id', 'part_number')), FieldSet('manufacturer_id', 'part_number', name=_('Hardware')),
(_('Components'), ( FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'pass_through_ports', name=_('Components')
)), ),
(_('Weight'), ('weight', 'weight_unit')), FieldSet('weight', 'weight_unit', name=_('Weight')),
) )
selector_fields = ('filter_id', 'q', 'manufacturer_id') selector_fields = ('filter_id', 'q', 'manufacturer_id')
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
@ -642,18 +645,20 @@ class DeviceFilterForm(
): ):
model = Device model = Device
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
(_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')), FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
(_('Components'), ( FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)), name=_('Components')
(_('Miscellaneous'), ( ),
FieldSet(
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', 'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
)) name=_('Miscellaneous')
)
) )
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id') selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
@ -754,7 +759,7 @@ class DeviceFilterForm(
) )
has_oob_ip = forms.NullBooleanField( has_oob_ip = forms.NullBooleanField(
required=False, required=False,
label='Has an OOB IP', label=_('Has an OOB IP'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
@ -817,9 +822,9 @@ class VirtualDeviceContextFilterForm(
): ):
model = VirtualDeviceContext model = VirtualDeviceContext
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('device', 'status', 'has_primary_ip')), FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
device = DynamicModelMultipleChoiceField( device = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -844,8 +849,8 @@ class VirtualDeviceContextFilterForm(
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Hardware'), ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')), FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -879,9 +884,9 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis model = VirtualChassis
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -908,10 +913,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable model = Cable
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')), FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
(_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit', 'unterminated')), FieldSet('type', 'status', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -992,9 +997,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
) )
selector_fields = ('filter_id', 'q', 'site_id', 'location_id') selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
@ -1031,10 +1036,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
(_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')), FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -1141,11 +1146,11 @@ class PathEndpointFilterForm(CabledFilterForm):
class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort model = ConsolePort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type', 'speed')), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Connection'), ('cabled', 'connected', 'occupied')), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -1163,11 +1168,11 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort model = ConsoleServerPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type', 'speed')), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Connection'), ('cabled', 'connected', 'occupied')), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -1185,11 +1190,11 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort model = PowerPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type')), FieldSet('name', 'label', 'type', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Connection'), ('cabled', 'connected', 'occupied')), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -1202,11 +1207,11 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type')), FieldSet('name', 'label', 'type', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Connection'), ('cabled', 'connected', 'occupied')), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -1219,14 +1224,14 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface model = Interface
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')), FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
(_('Addressing'), ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')), FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
(_('PoE'), ('poe_mode', 'poe_type')), FieldSet('poe_mode', 'poe_type', name=_('PoE')),
(_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')), FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
(_('Connection'), ('cabled', 'connected', 'occupied')), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
) )
selector_fields = ('filter_id', 'q', 'device_id') selector_fields = ('filter_id', 'q', 'device_id')
vdc_id = DynamicModelMultipleChoiceField( vdc_id = DynamicModelMultipleChoiceField(
@ -1330,11 +1335,11 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type', 'color')), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Cable'), ('cabled', 'occupied')), FieldSet('cabled', 'occupied', name=_('Cable')),
) )
model = FrontPort model = FrontPort
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
@ -1352,11 +1357,11 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort model = RearPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'type', 'color')), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
(_('Cable'), ('cabled', 'occupied')), FieldSet('cabled', 'occupied', name=_('Cable')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'), label=_('Type'),
@ -1373,10 +1378,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
class ModuleBayFilterForm(DeviceComponentFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay model = ModuleBay
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'position')), FieldSet('name', 'label', 'position', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
position = forms.CharField( position = forms.CharField(
@ -1388,10 +1393,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
class DeviceBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay model = DeviceBay
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label')), FieldSet('name', 'label', name=_('Attributes')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1399,10 +1404,13 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
class InventoryItemFilterForm(DeviceComponentFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem model = InventoryItem
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')), FieldSet(
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), 'name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
) )
role_id = DynamicModelMultipleChoiceField( role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),

View File

@ -16,6 +16,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
NumericArrayField, SlugField, NumericArrayField, SlugField,
) )
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from virtualization.models import Cluster from virtualization.models import Cluster
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
@ -77,9 +78,7 @@ class RegionForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Region'), ( FieldSet('parent', 'name', 'slug', 'description', 'tags'),
'parent', 'name', 'slug', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -98,9 +97,7 @@ class SiteGroupForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Site Group'), ( FieldSet('parent', 'name', 'slug', 'description', 'tags'),
'parent', 'name', 'slug', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -135,11 +132,12 @@ class SiteForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Site'), ( FieldSet(
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags', 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
)), name=_('Site')
(_('Tenancy'), ('tenant_group', 'tenant')), ),
(_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet('physical_address', 'shipping_address', 'latitude', 'longitude', name=_('Contact Info')),
) )
class Meta: class Meta:
@ -179,14 +177,14 @@ class LocationForm(TenancyForm, NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')), FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
model = Location model = Location
fields = ( fields = (
'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags',
) )
@ -194,9 +192,7 @@ class RackRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Rack Role'), ( FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
'name', 'slug', 'color', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -227,6 +223,18 @@ class RackForm(TenancyForm, NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = (
FieldSet('site', 'location', 'name', 'status', 'role', 'description', 'tags', name=_('Rack')),
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet(
'type', 'width', 'starting_unit', 'u_height',
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', 'desc_units', name=_('Dimensions')
),
)
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
@ -256,8 +264,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')), FieldSet('rack', 'units', 'user', 'description', 'tags', name=_('Reservation')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -271,9 +279,7 @@ class ManufacturerForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Manufacturer'), ( FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
'name', 'slug', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -304,12 +310,12 @@ class DeviceTypeForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')), FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
(_('Chassis'), ( FieldSet(
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
'weight', 'weight_unit', 'weight', 'weight_unit', name=_('Chassis')
)), ),
(_('Images'), ('front_image', 'rear_image')), FieldSet('front_image', 'rear_image', name=_('Images')),
) )
class Meta: class Meta:
@ -337,8 +343,8 @@ class ModuleTypeForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')), FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
(_('Weight'), ('weight', 'weight_unit')) FieldSet('weight', 'weight_unit', name=_('Weight'))
) )
class Meta: class Meta:
@ -357,9 +363,9 @@ class DeviceRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Device Role'), ( FieldSet(
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', name=_('Device Role')
)), ),
) )
class Meta: class Meta:
@ -386,7 +392,7 @@ class PlatformForm(NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')), FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')),
) )
class Meta: class Meta:
@ -601,10 +607,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')), FieldSet('device', 'module_bay', 'module_type', 'status', 'description', 'tags', name=_('Module')),
(_('Hardware'), ( FieldSet('serial', 'asset_tag', 'replicate_components', 'adopt_components', name=_('Hardware')),
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
)),
) )
class Meta: class Meta:
@ -658,7 +662,7 @@ class PowerPanelForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Power Panel', ('site', 'location', 'name', 'description', 'tags')), FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
) )
class Meta: class Meta:
@ -683,9 +687,12 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')), FieldSet(
(_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), 'power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags',
(_('Tenancy'), ('tenant_group', 'tenant')), name=_('Power Feed')
),
FieldSet('supply', 'voltage', 'amperage', 'phase', 'max_utilization', name=_('Characteristics')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -832,7 +839,7 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
class ConsolePortTemplateForm(ModularComponentTemplateForm): class ConsolePortTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
) )
class Meta: class Meta:
@ -844,7 +851,7 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'description'),
) )
class Meta: class Meta:
@ -856,9 +863,9 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
class PowerPortTemplateForm(ModularComponentTemplateForm): class PowerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
)), ),
) )
class Meta: class Meta:
@ -879,7 +886,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
) )
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')), FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'),
) )
class Meta: class Meta:
@ -901,9 +908,11 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
) )
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')), FieldSet(
(_('PoE'), ('poe_mode', 'poe_type')), 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge',
(_('Wireless'), ('rf_role',)), ),
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('rf_role', name=_('Wireless')),
) )
class Meta: class Meta:
@ -925,10 +934,10 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
) )
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description', 'description',
)), ),
) )
class Meta: class Meta:
@ -941,7 +950,7 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
class RearPortTemplateForm(ModularComponentTemplateForm): class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')), FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description'),
) )
class Meta: class Meta:
@ -953,7 +962,7 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
class ModuleBayTemplateForm(ComponentTemplateForm): class ModuleBayTemplateForm(ComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'name', 'label', 'position', 'description')), FieldSet('device_type', 'name', 'label', 'position', 'description'),
) )
class Meta: class Meta:
@ -965,7 +974,7 @@ class ModuleBayTemplateForm(ComponentTemplateForm):
class DeviceBayTemplateForm(ComponentTemplateForm): class DeviceBayTemplateForm(ComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'name', 'label', 'description')), FieldSet('device_type', 'name', 'label', 'description'),
) )
class Meta: class Meta:
@ -1006,10 +1015,10 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
) )
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id', 'component_type', 'component_id',
)), ),
) )
class Meta: class Meta:
@ -1052,9 +1061,9 @@ class ModularDeviceComponentForm(DeviceComponentForm):
class ConsolePortForm(ModularDeviceComponentForm): class ConsolePortForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1065,11 +1074,10 @@ class ConsolePortForm(ModularDeviceComponentForm):
class ConsoleServerPortForm(ModularDeviceComponentForm): class ConsoleServerPortForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1080,12 +1088,11 @@ class ConsoleServerPortForm(ModularDeviceComponentForm):
class PowerPortForm(ModularDeviceComponentForm): class PowerPortForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'tags', 'description', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1107,10 +1114,10 @@ class PowerOutletForm(ModularDeviceComponentForm):
) )
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1206,15 +1213,18 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
) )
fieldsets = ( fieldsets = (
(_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')), FieldSet(
(_('Addressing'), ('vrf', 'mac_address', 'wwn')), 'device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags', name=_('Interface')
(_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), ),
(_('Related Interfaces'), ('parent', 'bridge', 'lag')), FieldSet('vrf', 'mac_address', 'wwn', name=_('Addressing')),
(_('PoE'), ('poe_mode', 'poe_type')), FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
(_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
(_('Wireless'), ( FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
FieldSet(
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
)), name=_('Wireless')
),
) )
class Meta: class Meta:
@ -1245,10 +1255,10 @@ class FrontPortForm(ModularDeviceComponentForm):
) )
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
'description', 'tags', 'description', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1261,9 +1271,9 @@ class FrontPortForm(ModularDeviceComponentForm):
class RearPortForm(ModularDeviceComponentForm): class RearPortForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
)), ),
) )
class Meta: class Meta:
@ -1275,7 +1285,7 @@ class RearPortForm(ModularDeviceComponentForm):
class ModuleBayForm(DeviceComponentForm): class ModuleBayForm(DeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ('device', 'name', 'label', 'position', 'description', 'tags',)), FieldSet('device', 'name', 'label', 'position', 'description', 'tags',),
) )
class Meta: class Meta:
@ -1287,7 +1297,7 @@ class ModuleBayForm(DeviceComponentForm):
class DeviceBayForm(DeviceComponentForm): class DeviceBayForm(DeviceComponentForm):
fieldsets = ( fieldsets = (
(None, ('device', 'name', 'label', 'description', 'tags',)), FieldSet('device', 'name', 'label', 'description', 'tags',),
) )
class Meta: class Meta:
@ -1395,8 +1405,20 @@ class InventoryItemForm(DeviceComponentForm):
) )
fieldsets = ( fieldsets = (
(_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')),
(_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet(
TabbedGroups(
FieldSet('interface', name=_('Interface')),
FieldSet('consoleport', name=_('Console Port')),
FieldSet('consoleserverport', name=_('Console Server Port')),
FieldSet('frontport', name=_('Front Port')),
FieldSet('rearport', name=_('Rear Port')),
FieldSet('powerport', name=_('Power Port')),
FieldSet('poweroutlet', name=_('Power Outlet')),
),
name=_('Component Assignment')
)
) )
class Meta: class Meta:
@ -1412,22 +1434,17 @@ class InventoryItemForm(DeviceComponentForm):
component_type = initial.get('component_type') component_type = initial.get('component_type')
component_id = initial.get('component_id') component_id = initial.get('component_id')
# Used for picking the default active tab for component selection
self.no_component = True
if instance: if instance:
# When editing set the initial value for component selectin # When editing set the initial value for component selection
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS): for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
if type(instance.component) is component_model.model_class(): if type(instance.component) is component_model.model_class():
initial[component_model.model] = instance.component initial[component_model.model] = instance.component
self.no_component = False
break break
elif component_type and component_id: elif component_type and component_id:
# When adding the InventoryItem from a component page # When adding the InventoryItem from a component page
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first(): if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
if component := content_type.model_class().objects.filter(pk=component_id).first(): if component := content_type.model_class().objects.filter(pk=component_id).first():
initial[content_type.model] = component initial[content_type.model] = component
self.no_component = False
kwargs['initial'] = initial kwargs['initial'] = initial
@ -1461,9 +1478,7 @@ class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Inventory Item Role'), ( FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
'name', 'slug', 'color', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -1499,8 +1514,11 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')), FieldSet(
(_('Tenancy'), ('tenant_group', 'tenant')) 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags',
name=_('Virtual Device Context')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy'))
) )
class Meta: class Meta:

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import * from dcim.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelect from utilities.forms.widgets import APISelect
from . import model_forms from . import model_forms
@ -113,7 +114,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')), FieldSet('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description'),
) )
class Meta(model_forms.FrontPortTemplateForm.Meta): class Meta(model_forms.FrontPortTemplateForm.Meta):
@ -274,9 +275,9 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
# Override fieldsets from FrontPortForm to omit rear_port_position # Override fieldsets from FrontPortForm to omit rear_port_position
fieldsets = ( fieldsets = (
(None, ( FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
)), ),
) )
class Meta(model_forms.FrontPortForm.Meta): class Meta(model_forms.FrontPortForm.Meta):

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.4 on 2024-03-17 02:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0185_gfk_indexes'),
]
operations = [
migrations.AddField(
model_name='location',
name='facility',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@ -229,15 +229,16 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name, 'manufacturer': self.manufacturer.name,
'model': self.model, 'model': self.model,
'slug': self.slug, 'slug': self.slug,
'description': self.description,
'default_platform': self.default_platform.name if self.default_platform else None, 'default_platform': self.default_platform.name if self.default_platform else None,
'part_number': self.part_number, 'part_number': self.part_number,
'u_height': float(self.u_height), 'u_height': float(self.u_height),
'is_full_depth': self.is_full_depth, 'is_full_depth': self.is_full_depth,
'subdevice_role': self.subdevice_role, 'subdevice_role': self.subdevice_role,
'airflow': self.airflow, 'airflow': self.airflow,
'comments': self.comments,
'weight': float(self.weight) if self.weight is not None else None, 'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit, 'weight_unit': self.weight_unit,
'comments': self.comments,
} }
# Component templates # Component templates
@ -415,9 +416,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name, 'manufacturer': self.manufacturer.name,
'model': self.model, 'model': self.model,
'part_number': self.part_number, 'part_number': self.part_number,
'comments': self.comments, 'description': self.description,
'weight': float(self.weight) if self.weight is not None else None, 'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit, 'weight_unit': self.weight_unit,
'comments': self.comments,
} }
# Component templates # Component templates

View File

@ -275,6 +275,12 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
blank=True, blank=True,
null=True null=True
) )
facility = models.CharField(
verbose_name=_('facility'),
max_length=50,
blank=True,
help_text=_('Local facility ID or description')
)
# Generic relations # Generic relations
vlan_groups = GenericRelation( vlan_groups = GenericRelation(
@ -284,7 +290,7 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
related_query_name='location' related_query_name='location'
) )
clone_fields = ('site', 'parent', 'status', 'tenant', 'description') clone_fields = ('site', 'parent', 'status', 'tenant', 'facility', 'description')
prerequisite_models = ( prerequisite_models = (
'dcim.Site', 'dcim.Site',
) )

View File

@ -132,10 +132,11 @@ class LocationIndex(SearchIndex):
model = models.Location model = models.Location
fields = ( fields = (
('name', 100), ('name', 100),
('facility', 100),
('slug', 110), ('slug', 110),
('description', 500), ('description', 500),
) )
display_attrs = ('site', 'status', 'tenant', 'description') display_attrs = ('site', 'status', 'tenant', 'facility', 'description')
@register_search @register_search

View File

@ -210,6 +210,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True, linkify=True,
verbose_name=_('Type') verbose_name=_('Type')
) )
platform = tables.Column(
linkify=True,
verbose_name=_('Platform')
)
primary_ip = tables.Column( primary_ip = tables.Column(
linkify=True, linkify=True,
order_by=('primary_ip4', 'primary_ip6'), order_by=('primary_ip4', 'primary_ip6'),
@ -294,7 +298,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
model = models.Device model = models.Device
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type', 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4', 'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',

View File

@ -152,7 +152,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Location model = Location
fields = ( fields = (
'pk', 'id', 'name', 'site', 'status', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', 'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated', 'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'
) )
default_columns = ('pk', 'name', 'site', 'status', 'tenant', 'rack_count', 'device_count', 'description')

View File

@ -64,21 +64,32 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
regions = ( parent_regions = (
Region(name='Region 1', slug='region-1', description='foobar1'), Region(name='Region 1', slug='region-1', description='foobar1'),
Region(name='Region 2', slug='region-2', description='foobar2'), Region(name='Region 2', slug='region-2', description='foobar2'),
Region(name='Region 3', slug='region-3', description='foobar3'), Region(name='Region 3', slug='region-3', description='foobar3'),
) )
for region in parent_regions:
region.save()
regions = (
Region(name='Region 1A', slug='region-1a', parent=parent_regions[0]),
Region(name='Region 1B', slug='region-1b', parent=parent_regions[0]),
Region(name='Region 2A', slug='region-2a', parent=parent_regions[1]),
Region(name='Region 2B', slug='region-2b', parent=parent_regions[1]),
Region(name='Region 3A', slug='region-3a', parent=parent_regions[2]),
Region(name='Region 3B', slug='region-3b', parent=parent_regions[2]),
)
for region in regions: for region in regions:
region.save() region.save()
child_regions = ( child_regions = (
Region(name='Region 1A', slug='region-1a', parent=regions[0]), Region(name='Region 1A1', slug='region-1a1', parent=regions[0]),
Region(name='Region 1B', slug='region-1b', parent=regions[0]), Region(name='Region 1B1', slug='region-1b1', parent=regions[1]),
Region(name='Region 2A', slug='region-2a', parent=regions[1]), Region(name='Region 2A1', slug='region-2a1', parent=regions[2]),
Region(name='Region 2B', slug='region-2b', parent=regions[1]), Region(name='Region 2B1', slug='region-2b1', parent=regions[3]),
Region(name='Region 3A', slug='region-3a', parent=regions[2]), Region(name='Region 3A1', slug='region-3a1', parent=regions[4]),
Region(name='Region 3B', slug='region-3b', parent=regions[2]), Region(name='Region 3B1', slug='region-3b1', parent=regions[5]),
) )
for region in child_regions: for region in child_regions:
region.save() region.save()
@ -100,12 +111,19 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self): def test_parent(self):
parent_regions = Region.objects.filter(parent__isnull=True)[:2] regions = Region.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} params = {'parent_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} params = {'parent': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_ancestor(self):
regions = Region.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'ancestor': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
@ -114,24 +132,35 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
sitegroups = ( parent_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'), SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'),
SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'), SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'),
SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'), SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'),
) )
for sitegroup in sitegroups: for site_group in parent_groups:
sitegroup.save() site_group.save()
child_sitegroups = ( groups = (
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]), SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=parent_groups[0]),
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]), SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=parent_groups[0]),
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]), SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]), SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]), SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]), SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
) )
for sitegroup in child_sitegroups: for site_group in groups:
sitegroup.save() site_group.save()
child_groups = (
SiteGroup(name='Site Group 1A1', slug='site-group-1a1', parent=groups[0]),
SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]),
SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]),
SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]),
SiteGroup(name='Site Group 3A1', slug='site-group-3a1', parent=groups[4]),
SiteGroup(name='Site Group 3B1', slug='site-group-3b1', parent=groups[5]),
)
for site_group in child_groups:
site_group.save()
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -150,16 +179,24 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self): def test_parent(self):
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2] site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]} params = {'parent_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]} params = {'parent': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_ancestor(self):
site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'ancestor': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Site.objects.all() queryset = Site.objects.all()
filterset = SiteFilterSet filterset = SiteFilterSet
ignore_fields = ('physical_address', 'shipping_address')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -314,21 +351,29 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
parent_locations = ( parent_locations = (
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]), Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]), Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]), Location(name='Location 3', slug='location-3', site=sites[2]),
) )
for location in parent_locations: for location in parent_locations:
location.save() location.save()
locations = ( locations = (
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'), Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, facility='Facility 1', description='foobar1'),
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'), Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, facility='Facility 2', description='foobar2'),
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'), Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, facility='Facility 3', description='foobar3'),
) )
for location in locations: for location in locations:
location.save() location.save()
child_locations = (
Location(name='Location 1A1', slug='location-1a1', site=sites[0], parent=locations[0]),
Location(name='Location 2A1', slug='location-2a1', site=sites[1], parent=locations[1]),
Location(name='Location 3A1', slug='location-3a1', site=sites[2], parent=locations[2]),
)
for location in child_locations:
location.save()
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -345,6 +390,10 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [LocationStatusChoices.STATUS_PLANNED, LocationStatusChoices.STATUS_STAGING]} params = {'status': [LocationStatusChoices.STATUS_PLANNED, LocationStatusChoices.STATUS_STAGING]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_facility(self):
params = {'facility': ['Facility 1', 'Facility 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self): def test_description(self):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -352,31 +401,38 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site_group(self): def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2] site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]} params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]} params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_parent(self): def test_parent(self):
parent_groups = Location.objects.filter(name__startswith='Parent')[:2] locations = Location.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} params = {'parent_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} params = {'parent': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ancestor(self):
locations = Location.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'ancestor': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests): class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
@ -416,6 +472,7 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
class RackTestCase(TestCase, ChangeLoggedFilterSetTests): class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all() queryset = Rack.objects.all()
filterset = RackFilterSet filterset = RackFilterSet
ignore_fields = ('units',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -675,6 +732,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests): class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackReservation.objects.all() queryset = RackReservation.objects.all()
filterset = RackReservationFilterSet filterset = RackReservationFilterSet
ignore_fields = ('units',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -838,6 +896,7 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
filterset = DeviceTypeFilterSet filterset = DeviceTypeFilterSet
ignore_fields = ('front_image', 'rear_image')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1829,6 +1888,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Device.objects.all() queryset = Device.objects.all()
filterset = DeviceFilterSet filterset = DeviceFilterSet
ignore_fields = ('local_context_data', 'oob_ip', 'primary_ip4', 'primary_ip6', 'vc_master_for')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2281,6 +2341,7 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Module.objects.all() queryset = Module.objects.all()
filterset = ModuleFilterSet filterset = ModuleFilterSet
ignore_fields = ('local_context_data',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -3178,6 +3239,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = Interface.objects.all() queryset = Interface.objects.all()
filterset = InterfaceFilterSet filterset = InterfaceFilterSet
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'vdcs')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -5281,6 +5343,7 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
filterset = VirtualDeviceContextFilterSet filterset = VirtualDeviceContextFilterSet
ignore_fields = ('primary_ip4', 'primary_ip6')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -5350,15 +5413,22 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
VirtualDeviceContext.objects.bulk_create(vdcs) VirtualDeviceContext.objects.bulk_create(vdcs)
interfaces = ( interfaces = (
Interface(device=devices[0], name='Interface 1', type='virtual'), Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[0], name='Interface 2', type='virtual'), Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
) )
Interface.objects.bulk_create(interfaces) Interface.objects.bulk_create(interfaces)
interfaces[0].vdcs.set([vdcs[0]]) interfaces[0].vdcs.set([vdcs[0]])
interfaces[1].vdcs.set([vdcs[1]]) interfaces[1].vdcs.set([vdcs[1]])
interfaces[2].vdcs.set([vdcs[2]])
interfaces[3].vdcs.set([vdcs[3]])
interfaces[4].vdcs.set([vdcs[4]])
interfaces[5].vdcs.set([vdcs[5]])
addresses = ( ip_addresses = (
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'), IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'), IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
IPAddress(assigned_object=None, address='10.1.1.3/24'), IPAddress(assigned_object=None, address='10.1.1.3/24'),
@ -5366,13 +5436,12 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'), IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
IPAddress(assigned_object=None, address='2001:db8::3/64'), IPAddress(assigned_object=None, address='2001:db8::3/64'),
) )
IPAddress.objects.bulk_create(addresses) IPAddress.objects.bulk_create(ip_addresses)
vdcs[0].primary_ip4 = ip_addresses[0]
vdcs[0].primary_ip4 = addresses[0] vdcs[0].primary_ip6 = ip_addresses[3]
vdcs[0].primary_ip6 = addresses[3]
vdcs[0].save() vdcs[0].save()
vdcs[1].primary_ip4 = addresses[1] vdcs[1].primary_ip4 = ip_addresses[1]
vdcs[1].primary_ip6 = addresses[4] vdcs[1].primary_ip6 = ip_addresses[4]
vdcs[1].save() vdcs[1].save()
def test_q(self): def test_q(self):
@ -5380,8 +5449,11 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_device(self): def test_device(self):
params = {'device': ['Device 1', 'Device 2']} devices = Device.objects.filter(name__in=['Device 1', 'Device 2'])
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_status(self): def test_status(self):
params = {'status': ['active']} params = {'status': ['active']}
@ -5391,10 +5463,10 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device_id(self): def test_interface(self):
devices = Device.objects.filter(name__in=['Device 1', 'Device 2']) interfaces = Interface.objects.filter(name__in=['Interface 1', 'Interface 3'])
params = {'device_id': [devices[0].pk, devices[1].pk]} params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_has_primary_ip(self): def test_has_primary_ip(self):
params = {'has_primary_ip': True} params = {'has_primary_ip': True}

View File

@ -213,6 +213,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'slug': 'location-x', 'slug': 'location-x',
'site': site.pk, 'site': site.pk,
'status': LocationStatusChoices.STATUS_PLANNED, 'status': LocationStatusChoices.STATUS_PLANNED,
'facility': 'Facility X',
'tenant': tenant.pk, 'tenant': tenant.pk,
'description': 'A new location', 'description': 'A new location',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],

View File

@ -727,7 +727,6 @@ class RackNonRackedView(generic.ObjectChildrenView):
class RackEditView(generic.ObjectEditView): class RackEditView(generic.ObjectEditView):
queryset = Rack.objects.all() queryset = Rack.objects.all()
form = forms.RackForm form = forms.RackForm
template_name = 'dcim/rack_edit.html'
@register_model_view(Rack, 'delete') @register_model_view(Rack, 'delete')
@ -1079,7 +1078,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
tab = ViewTab( tab = ViewTab(
label=_('Inventory Items'), label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_template_count, badge=lambda obj: obj.inventory_item_template_count,
permission='dcim.view_invenotryitemtemplate', permission='dcim.view_inventoryitemtemplate',
weight=590, weight=590,
hide_if_empty=True hide_if_empty=True
) )
@ -2925,14 +2924,12 @@ class InventoryItemView(generic.ObjectView):
class InventoryItemEditView(generic.ObjectEditView): class InventoryItemEditView(generic.ObjectEditView):
queryset = InventoryItem.objects.all() queryset = InventoryItem.objects.all()
form = forms.InventoryItemForm form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
class InventoryItemCreateView(generic.ComponentCreateView): class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all() queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
@register_model_view(InventoryItem, 'delete') @register_model_view(InventoryItem, 'delete')

View File

@ -57,10 +57,10 @@ class CustomFieldsDataField(Field):
for cf in self._get_custom_fields(): for cf in self._get_custom_fields():
value = cf.deserialize(obj.get(cf.name)) value = cf.deserialize(obj.get(cf.name))
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
serializer = get_serializer_for_model(cf.object_type.model_class()) serializer = get_serializer_for_model(cf.related_object_type.model_class())
value = serializer(value, nested=True, context=self.parent.context).data value = serializer(value, nested=True, context=self.parent.context).data
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
serializer = get_serializer_for_model(cf.object_type.model_class()) serializer = get_serializer_for_model(cf.related_object_type.model_class())
value = serializer(value, nested=True, many=True, context=self.parent.context).data value = serializer(value, nested=True, many=True, context=self.parent.context).data
data[cf.name] = value data[cf.name] = value
@ -79,7 +79,7 @@ class CustomFieldsDataField(Field):
CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT CustomFieldTypeChoices.TYPE_MULTIOBJECT
): ):
serializer_class = get_serializer_for_model(cf.object_type.model_class()) serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
if serializer.is_valid(): if serializer.is_valid():

View File

@ -44,7 +44,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
many=True many=True
) )
type = ChoiceField(choices=CustomFieldTypeChoices) type = ChoiceField(choices=CustomFieldTypeChoices)
object_type = ContentTypeField( related_object_type = ContentTypeField(
queryset=ObjectType.objects.all(), queryset=ObjectType.objects.all(),
required=False, required=False,
allow_null=True allow_null=True
@ -62,10 +62,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = [
'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', 'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'created', 'last_updated', 'choice_set', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -40,12 +40,14 @@ class ScriptFilterSet(BaseFilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
module_id = django_filters.ModelMultipleChoiceFilter(
queryset=ScriptModule.objects.all(),
label=_('Script module (ID)'),
)
class Meta: class Meta:
model = Script model = Script
fields = [ fields = ('id', 'name', 'is_executable')
'id', 'name',
]
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -69,10 +71,10 @@ class WebhookFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = Webhook model = Webhook
fields = [ fields = (
'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification',
'ca_file_path', 'description', 'ca_file_path', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -89,8 +91,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
object_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='object_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
@ -103,10 +106,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = EventRule model = EventRule
fields = [ fields = (
'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
'action_type', 'description', 'action_type', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -118,7 +121,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
) )
class CustomFieldFilterSet(BaseFilterSet): class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -126,12 +129,18 @@ class CustomFieldFilterSet(BaseFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices choices=CustomFieldTypeChoices
) )
object_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='object_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
) )
related_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.all(),
field_name='related_object_type'
)
related_object_type = ContentTypeFilter()
choice_set_id = django_filters.ModelMultipleChoiceFilter( choice_set_id = django_filters.ModelMultipleChoiceFilter(
queryset=CustomFieldChoiceSet.objects.all() queryset=CustomFieldChoiceSet.objects.all()
) )
@ -143,10 +152,11 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = (
'id', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
'weight', 'is_cloneable', 'description', 'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
] 'validation_regex',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -159,7 +169,7 @@ class CustomFieldFilterSet(BaseFilterSet):
) )
class CustomFieldChoiceSetFilterSet(BaseFilterSet): class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -170,9 +180,9 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = [ fields = (
'id', 'name', 'description', 'base_choices', 'order_alphabetically', 'id', 'name', 'description', 'base_choices', 'order_alphabetically',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -187,13 +197,14 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
return queryset.filter(extra_choices__overlap=value) return queryset.filter(extra_choices__overlap=value)
class CustomLinkFilterSet(BaseFilterSet): class CustomLinkFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
object_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='object_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
@ -201,9 +212,9 @@ class CustomLinkFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomLink model = CustomLink
fields = [ fields = (
'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'button_class',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -216,13 +227,14 @@ class CustomLinkFilterSet(BaseFilterSet):
) )
class ExportTemplateFilterSet(BaseFilterSet): class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
object_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='object_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
@ -238,7 +250,10 @@ class ExportTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['id', 'name', 'description', 'data_synced'] fields = (
'id', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment', 'auto_sync_enabled',
'data_synced',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -249,13 +264,14 @@ class ExportTemplateFilterSet(BaseFilterSet):
) )
class SavedFilterFilterSet(BaseFilterSet): class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
object_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='object_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
@ -276,7 +292,7 @@ class SavedFilterFilterSet(BaseFilterSet):
class Meta: class Meta:
model = SavedFilter model = SavedFilter
fields = ['id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] fields = ('id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -317,20 +333,19 @@ class BookmarkFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Bookmark model = Bookmark
fields = ['id', 'object_id'] fields = ('id', 'object_id')
class ImageAttachmentFilterSet(BaseFilterSet): class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
created = django_filters.DateTimeFilter()
object_type = ContentTypeFilter() object_type = ContentTypeFilter()
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = ['id', 'object_type_id', 'object_id', 'name'] fields = ('id', 'object_type_id', 'object_id', 'name', 'image_width', 'image_height')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -360,7 +375,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = JournalEntry model = JournalEntry
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind'] fields = ('id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -385,7 +400,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'slug', 'color', 'description', 'object_types'] fields = ('id', 'name', 'slug', 'color', 'description', 'object_types')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -482,12 +497,12 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label=_('Device type'), label=_('Device type'),
) )
role_id = django_filters.ModelMultipleChoiceFilter( device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='roles', field_name='roles',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
label=_('Role'), label=_('Role'),
) )
role = django_filters.ModelMultipleChoiceFilter( device_role = django_filters.ModelMultipleChoiceFilter(
field_name='roles__slug', field_name='roles__slug',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -573,9 +588,13 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
label=_('Data file (ID)'), label=_('Data file (ID)'),
) )
# TODO: Remove in v4.1
role = device_role
role_id = device_role_id
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = ['id', 'name', 'is_active', 'data_synced', 'description'] fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -587,7 +606,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
) )
class ConfigTemplateFilterSet(BaseFilterSet): class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -604,7 +623,7 @@ class ConfigTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConfigTemplate model = ConfigTemplate
fields = ['id', 'name', 'description', 'data_synced'] fields = ('id', 'name', 'description', 'auto_sync_enabled', 'data_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -652,10 +671,10 @@ class ObjectChangeFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ObjectChange model = ObjectChange
fields = [ fields = (
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id', 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
'object_repr', 'related_object_type', 'related_object_id', 'object_repr',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -678,7 +697,7 @@ class ObjectTypeFilterSet(django_filters.FilterSet):
class Meta: class Meta:
model = ObjectType model = ObjectType
fields = ['id', 'app_label', 'model'] fields = ('id', 'app_label', 'model')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -40,7 +40,7 @@ class CustomFieldImportForm(CSVModelForm):
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
help_text=_('Field data type (e.g. text, integer, etc.)') help_text=_('Field data type (e.g. text, integer, etc.)')
) )
object_type = CSVContentTypeField( related_object_type = CSVContentTypeField(
label=_('Object type'), label=_('Object type'),
queryset=ObjectType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
class Meta: class Meta:
model = CustomField model = CustomField
fields = ( fields = (
'name', 'label', 'group_name', 'type', 'object_types', 'object_type', 'required', 'description', 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
) )

View File

@ -13,6 +13,7 @@ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_ch
from utilities.forms.fields import ( from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
) )
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelectMultiple, DateTimePicker from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -36,16 +37,16 @@ __all__ = (
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ( FieldSet(
'type', 'object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'is_cloneable', 'ui_editable', 'is_cloneable', name=_('Attributes')
)), ),
) )
object_type_id = ContentTypeMultipleChoiceField( related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
required=False, required=False,
label=_('Object type') label=_('Related object type')
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
@ -93,8 +94,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Choices'), ('base_choices', 'choice')), FieldSet('base_choices', 'choice', name=_('Choices')),
) )
base_choices = forms.MultipleChoiceField( base_choices = forms.MultipleChoiceField(
choices=CustomFieldChoiceSetBaseChoices, choices=CustomFieldChoiceSetBaseChoices,
@ -107,8 +108,8 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')), FieldSet('object_type', 'enabled', 'new_window', 'weight', name=_('Attributes')),
) )
object_type = ContentTypeMultipleChoiceField( object_type = ContentTypeMultipleChoiceField(
label=_('Object types'), label=_('Object types'),
@ -137,9 +138,9 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Data'), ('data_source_id', 'data_file_id')), FieldSet('data_source_id', 'data_file_id', name=_('Data')),
(_('Attributes'), ('object_type_id', 'mime_type', 'file_extension', 'as_attachment')), FieldSet('object_type_id', 'mime_type', 'file_extension', 'as_attachment', name=_('Attributes')),
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -178,8 +179,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ('object_type_id', 'name',)), FieldSet('object_type_id', 'name', name=_('Attributes')),
) )
object_type_id = ContentTypeChoiceField( object_type_id = ContentTypeChoiceField(
label=_('Object type'), label=_('Object type'),
@ -194,8 +195,8 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')), FieldSet('object_type', 'enabled', 'shared', 'weight', name=_('Attributes')),
) )
object_type = ContentTypeMultipleChoiceField( object_type = ContentTypeMultipleChoiceField(
label=_('Object types'), label=_('Object types'),
@ -225,8 +226,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
class WebhookFilterForm(NetBoxModelFilterSetForm): class WebhookFilterForm(NetBoxModelFilterSetForm):
model = Webhook model = Webhook
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('payload_url', 'http_method', 'http_content_type')), FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
) )
http_content_type = forms.CharField( http_content_type = forms.CharField(
label=_('HTTP content type'), label=_('HTTP content type'),
@ -249,9 +250,9 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('object_type_id', 'action_type', 'enabled')), FieldSet('object_type_id', 'action_type', 'enabled', name=_('Attributes')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
) )
object_type_id = ContentTypeMultipleChoiceField( object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
@ -323,12 +324,12 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag_id')), FieldSet('q', 'filter_id', 'tag_id'),
(_('Data'), ('data_source_id', 'data_file_id')), FieldSet('data_source_id', 'data_file_id', name=_('Data')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
(_('Device'), ('device_type_id', 'platform_id', 'role_id')), FieldSet('device_type_id', 'platform_id', 'role_id', name=_('Device')),
(_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')), FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')) FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant'))
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -412,8 +413,8 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Data'), ('data_source_id', 'data_file_id')), FieldSet('data_source_id', 'data_file_id', name=_('Data')),
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -444,9 +445,9 @@ class LocalConfigContextFilterForm(forms.Form):
class JournalEntryFilterForm(NetBoxModelFilterSetForm): class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry model = JournalEntry
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Creation'), ('created_before', 'created_after', 'created_by_id')), FieldSet('created_before', 'created_after', 'created_by_id', name=_('Creation')),
(_('Attributes'), ('assigned_object_type_id', 'kind')) FieldSet('assigned_object_type_id', 'kind', name=_('Attributes')),
) )
created_after = forms.DateTimeField( created_after = forms.DateTimeField(
required=False, required=False,
@ -482,9 +483,9 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange model = ObjectChange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Time'), ('time_before', 'time_after')), FieldSet('time_before', 'time_after', name=_('Time')),
(_('Attributes'), ('action', 'user_id', 'changed_object_type_id')), FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
) )
time_after = forms.DateTimeField( time_after = forms.DateTimeField(
required=False, required=False,

View File

@ -17,6 +17,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, DynamicModelMultipleChoiceField, JSONField, SlugField,
) )
from utilities.forms.rendering import FieldSet, ObjectAttribute
from utilities.forms.widgets import ChoicesWidget, HTMXSelect from utilities.forms.widgets import ChoicesWidget, HTMXSelect
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -42,8 +43,8 @@ class CustomFieldForm(forms.ModelForm):
label=_('Object types'), label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_fields') queryset=ObjectType.objects.with_feature('custom_fields')
) )
object_type = ContentTypeChoiceField( related_object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Related object type'),
queryset=ObjectType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
help_text=_("Type of the related object (for object/multi-object fields only)") help_text=_("Type of the related object (for object/multi-object fields only)")
@ -54,12 +55,15 @@ class CustomFieldForm(forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Custom Field'), ( FieldSet(
'object_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', 'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description',
)), name=_('Custom Field')
(_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), ),
(_('Values'), ('default', 'choice_set')), FieldSet(
(_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')), 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
),
FieldSet('default', 'choice_set', name=_('Values')),
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
) )
class Meta: class Meta:
@ -128,8 +132,11 @@ class CustomLinkForm(forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Custom Link'), ('name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), FieldSet(
(_('Templates'), ('link_text', 'link_url')), 'name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window',
name=_('Custom Link')
),
FieldSet('link_text', 'link_url', name=_('Templates')),
) )
class Meta: class Meta:
@ -162,9 +169,9 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Export Template'), ('name', 'object_types', 'description', 'template_code')), FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')), FieldSet('mime_type', 'file_extension', 'as_attachment', name=_('Rendering')),
) )
class Meta: class Meta:
@ -199,8 +206,8 @@ class SavedFilterForm(forms.ModelForm):
parameters = JSONField() parameters = JSONField()
fieldsets = ( fieldsets = (
(_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')), FieldSet('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', name=_('Saved Filter')),
(_('Parameters'), ('parameters',)), FieldSet('parameters', name=_('Parameters')),
) )
class Meta: class Meta:
@ -231,11 +238,12 @@ class BookmarkForm(forms.ModelForm):
class WebhookForm(NetBoxModelForm): class WebhookForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
(_('Webhook'), ('name', 'description', 'tags',)), FieldSet('name', 'description', 'tags', name=_('Webhook')),
(_('HTTP Request'), ( FieldSet(
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
)), name=_('HTTP Request')
(_('SSL'), ('ssl_verification', 'ca_file_path')), ),
FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')),
) )
class Meta: class Meta:
@ -266,12 +274,13 @@ class EventRuleForm(NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('Event Rule'), ('name', 'description', 'object_types', 'enabled', 'tags')), FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
(_('Conditions'), ('conditions',)), FieldSet('conditions', name=_('Conditions')),
(_('Action'), ( FieldSet(
'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data', 'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data',
)), name=_('Action')
),
) )
class Meta: class Meta:
@ -360,7 +369,7 @@ class TagForm(forms.ModelForm):
) )
fieldsets = ( fieldsets = (
('Tag', ('name', 'slug', 'color', 'description', 'object_types')), FieldSet('name', 'slug', 'color', 'description', 'object_types', name=_('Tag')),
) )
class Meta: class Meta:
@ -442,12 +451,13 @@ class ConfigContextForm(SyncedDataMixin, forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Config Context'), ('name', 'weight', 'description', 'data', 'is_active')), FieldSet('name', 'weight', 'description', 'data', 'is_active', name=_('Config Context')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
(_('Assignment'), ( FieldSet(
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
)), name=_('Assignment')
),
) )
class Meta: class Meta:
@ -494,9 +504,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Config Template'), ('name', 'description', 'environment_params', 'tags')), FieldSet('name', 'description', 'environment_params', 'tags', name=_('Config Template')),
(_('Content'), ('template_code',)), FieldSet('template_code', name=_('Content')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
) )
class Meta: class Meta:
@ -526,6 +536,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
class ImageAttachmentForm(forms.ModelForm): class ImageAttachmentForm(forms.ModelForm):
fieldsets = (
FieldSet(ObjectAttribute('parent'), 'name', 'image'),
)
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment

View File

@ -7,6 +7,7 @@ from extras.models import ObjectChange
__all__ = ( __all__ = (
'ChangelogMixin', 'ChangelogMixin',
'ConfigContextMixin', 'ConfigContextMixin',
'ContactsMixin',
'CustomFieldsMixin', 'CustomFieldsMixin',
'ImageAttachmentsMixin', 'ImageAttachmentsMixin',
'JournalEntriesMixin', 'JournalEntriesMixin',

View File

@ -25,7 +25,4 @@ class Migration(migrations.Migration):
migrations.DeleteModel( migrations.DeleteModel(
name='Report', name='Report',
), ),
migrations.DeleteModel(
name='ReportModule',
),
] ]

View File

@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Script = apps.get_model('extras', 'Script') Script = apps.get_model('extras', 'Script')
ScriptModule = apps.get_model('extras', 'ScriptModule') ScriptModule = apps.get_model('extras', 'ScriptModule')
ReportModule = apps.get_model('extras', 'ReportModule')
Job = apps.get_model('core', 'Job') Job = apps.get_model('core', 'Job')
script_ct = ContentType.objects.get_for_model(Script) script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule) scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False)
reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False)
for module in ScriptModule.objects.all(): for module in ScriptModule.objects.all():
for script_name in get_module_scripts(module): for script_name in get_module_scripts(module):
@ -96,10 +98,16 @@ def update_scripts(apps, schema_editor):
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter( Job.objects.filter(
object_type=scriptmodule_ct, object_type_id=scriptmodule_ct.id,
object_id=module.pk, object_id=module.pk,
name=script_name name=script_name
).update(object_type=script_ct, object_id=script.pk) ).update(object_type_id=script_ct.id, object_id=script.pk)
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter(
object_type_id=reportmodule_ct.id,
object_id=module.pk,
name=script_name
).update(object_type_id=script_ct.id, object_id=script.pk)
def update_event_rules(apps, schema_editor): def update_event_rules(apps, schema_editor):

View File

@ -12,4 +12,7 @@ class Migration(migrations.Migration):
model_name='eventrule', model_name='eventrule',
name='action_parameters', name='action_parameters',
), ),
migrations.DeleteModel(
name='ReportModule',
),
] ]

View File

@ -0,0 +1,16 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('extras', '0112_tag_update_object_types'),
]
operations = [
migrations.RenameField(
model_name='customfield',
old_name='object_type',
new_name='related_object_type',
),
]

View File

@ -11,7 +11,7 @@ from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry from netbox.registry import registry
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from utilities.jinja2 import ConfigTemplateLoader from utilities.jinja2 import ConfigTemplateLoader
from utilities.utils import deepmerge from utilities.utils import deepmerge
@ -26,7 +26,7 @@ __all__ = (
# Config contexts # Config contexts
# #
class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel): class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel):
""" """
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@ -210,7 +210,7 @@ class ConfigContextModel(models.Model):
# Config templates # Config templates
# #
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100 max_length=100

View File

@ -78,7 +78,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
default=CustomFieldTypeChoices.TYPE_TEXT, default=CustomFieldTypeChoices.TYPE_TEXT,
help_text=_('The type of data this custom field holds') help_text=_('The type of data this custom field holds')
) )
object_type = models.ForeignKey( related_object_type = models.ForeignKey(
to='core.ObjectType', to='core.ObjectType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
objects = CustomFieldManager() objects = CustomFieldManager()
clone_fields = ( clone_fields = (
'object_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
) )
@ -344,11 +344,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object fields must define an object_type; other fields must not # Object fields must define an object_type; other fields must not
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.object_type: if not self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _("Object fields must define an object type.") 'object_type': _("Object fields must define an object type.")
}) })
elif self.object_type: elif self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _( 'object_type': _(
"{type} fields may not define an object type.") "{type} fields may not define an object type.")
@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
except ValueError: except ValueError:
return value return value
if self.type == CustomFieldTypeChoices.TYPE_OBJECT: if self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
return model.objects.filter(pk=value).first() return model.objects.filter(pk=value).first()
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
return model.objects.filter(pk__in=value) return model.objects.filter(pk__in=value)
return value return value
@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),
@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Multiple objects # Multiple objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),

View File

@ -5,7 +5,7 @@ from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.models import * from extras.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import BaseTable, NetBoxTable, columns
from .template_code import * from .template_code import *
__all__ = ( __all__ = (
@ -21,6 +21,8 @@ __all__ = (
'JournalEntryTable', 'JournalEntryTable',
'ObjectChangeTable', 'ObjectChangeTable',
'SavedFilterTable', 'SavedFilterTable',
'ReportResultsTable',
'ScriptResultsTable',
'TaggedItemTable', 'TaggedItemTable',
'TagTable', 'TagTable',
'WebhookTable', 'WebhookTable',
@ -55,6 +57,9 @@ class CustomFieldTable(NetBoxTable):
description = columns.MarkdownColumn( description = columns.MarkdownColumn(
verbose_name=_('Description') verbose_name=_('Description')
) )
related_object_type = columns.ContentTypeColumn(
verbose_name=_('Related Object Type')
)
choice_set = tables.Column( choice_set = tables.Column(
linkify=True, linkify=True,
verbose_name=_('Choice Set') verbose_name=_('Choice Set')
@ -71,9 +76,9 @@ class CustomFieldTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'choices', 'created', 'last_updated', 'weight', 'choice_set', 'choices', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
@ -507,3 +512,61 @@ class JournalEntryTable(NetBoxTable):
default_columns = ( default_columns = (
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments' 'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
) )
class ScriptResultsTable(BaseTable):
index = tables.Column(
verbose_name=_('Line')
)
time = tables.Column(
verbose_name=_('Time')
)
status = tables.TemplateColumn(
template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level')
)
message = tables.Column(
verbose_name=_('Message')
)
class Meta(BaseTable.Meta):
empty_text = _('No results found')
fields = (
'index', 'time', 'status', 'message',
)
class ReportResultsTable(BaseTable):
index = tables.Column(
verbose_name=_('Line')
)
method = tables.Column(
verbose_name=_('Method')
)
time = tables.Column(
verbose_name=_('Time')
)
status = tables.Column(
empty_values=(),
verbose_name=_('Level')
)
status = tables.TemplateColumn(
template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level')
)
object = tables.Column(
verbose_name=_('Object')
)
url = tables.Column(
verbose_name=_('URL')
)
message = tables.Column(
verbose_name=_('Message')
)
class Meta(BaseTable.Meta):
empty_text = _('No results found')
fields = (
'index', 'method', 'time', 'status', 'object', 'url', 'message',
)

View File

@ -350,7 +350,7 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.object_types.set([self.object_type]) cf.object_types.set([self.object_type])
@ -382,7 +382,7 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.object_types.set([self.object_type]) cf.object_types.set([self.object_type])
@ -498,16 +498,28 @@ class CustomFieldTest(TestCase):
).full_clean() ).full_clean()
# Object # Object
CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() CustomField(
with self.assertRaises(ValidationError): name='test',
CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() type='object',
required=True,
related_object_type=object_type,
default=site.pk
).full_clean()
with (self.assertRaises(ValidationError)):
CustomField(
name='test',
type='object',
required=True,
related_object_type=object_type,
default="xxx"
).full_clean()
# Multi-object # Multi-object
CustomField( CustomField(
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=[site.pk] default=[site.pk]
).full_clean() ).full_clean()
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -515,7 +527,7 @@ class CustomFieldTest(TestCase):
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=["xxx"] default=["xxx"]
).full_clean() ).full_clean()
@ -581,13 +593,13 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
name='object_field', name='object_field',
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
default=vlans[0].pk, default=vlans[0].pk,
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
name='multiobject_field', name='multiobject_field',
object_type=ObjectType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
default=[vlans[0].pk, vlans[1].pk], default=[vlans[0].pk, vlans[1].pk],
), ),
) )
@ -1410,7 +1422,7 @@ class CustomFieldModelFilterTest(TestCase):
cf = CustomField( cf = CustomField(
name='cf11', name='cf11',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ObjectType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.object_types.set([object_type]) cf.object_types.set([object_type])
@ -1419,7 +1431,7 @@ class CustomFieldModelFilterTest(TestCase):
cf = CustomField( cf = CustomField(
name='cf12', name='cf12',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ObjectType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.object_types.set([object_type]) cf.object_types.set([object_type])

View File

@ -23,9 +23,10 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
User = get_user_model() User = get_user_model()
class CustomFieldTestCase(TestCase, BaseFilterSetTests): class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomField.objects.all() queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet filterset = CustomFieldFilterSet
ignore_fields = ('default',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -86,6 +87,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
ui_editable=CustomFieldUIEditableChoices.HIDDEN, ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1] choice_set=choice_sets[1]
), ),
CustomField(
name='Custom Field 6',
type=CustomFieldTypeChoices.TYPE_OBJECT,
related_object_type=ObjectType.objects.get_by_natural_key('dcim', 'site'),
required=False,
weight=600,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
ui_editable=CustomFieldUIEditableChoices.HIDDEN
),
) )
CustomField.objects.bulk_create(custom_fields) CustomField.objects.bulk_create(custom_fields)
custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
@ -108,6 +119,12 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_related_object_type(self):
params = {'related_object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_required(self): def test_required(self):
params = {'required': True} params = {'required': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -139,9 +156,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests): class CustomFieldChoiceSetTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomFieldChoiceSet.objects.all() queryset = CustomFieldChoiceSet.objects.all()
filterset = CustomFieldChoiceSetFilterSet filterset = CustomFieldChoiceSetFilterSet
ignore_fields = ('extra_choices',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -172,6 +190,7 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
class WebhookTestCase(TestCase, BaseFilterSetTests): class WebhookTestCase(TestCase, BaseFilterSetTests):
queryset = Webhook.objects.all() queryset = Webhook.objects.all()
filterset = WebhookFilterSet filterset = WebhookFilterSet
ignore_fields = ('additional_headers', 'body_template')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -236,6 +255,7 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
class EventRuleTestCase(TestCase, BaseFilterSetTests): class EventRuleTestCase(TestCase, BaseFilterSetTests):
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
filterset = EventRuleFilterSet filterset = EventRuleFilterSet
ignore_fields = ('action_data', 'conditions')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -389,7 +409,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CustomLinkTestCase(TestCase, BaseFilterSetTests): class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomLink.objects.all() queryset = CustomLink.objects.all()
filterset = CustomLinkFilterSet filterset = CustomLinkFilterSet
@ -458,9 +478,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class SavedFilterTestCase(TestCase, BaseFilterSetTests): class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = SavedFilter.objects.all() queryset = SavedFilter.objects.all()
filterset = SavedFilterFilterSet filterset = SavedFilterFilterSet
ignore_fields = ('parameters',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -631,9 +652,10 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class ExportTemplateTestCase(TestCase, BaseFilterSetTests): class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
filterset = ExportTemplateFilterSet filterset = ExportTemplateFilterSet
ignore_fields = ('template_code', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -667,9 +689,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
filterset = ImageAttachmentFilterSet filterset = ImageAttachmentFilterSet
ignore_fields = ('image',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -744,12 +767,6 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
} }
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_created(self):
pk_list = self.queryset.values_list('pk', flat=True)[:2]
self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
params = {'created': '2021-01-01T00:00:00'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = JournalEntry.objects.all() queryset = JournalEntry.objects.all()
@ -857,6 +874,7 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
filterset = ConfigContextFilterSet filterset = ConfigContextFilterSet
ignore_fields = ('data', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1025,11 +1043,11 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_role(self): def test_device_role(self):
device_roles = DeviceRole.objects.all()[:2] device_roles = DeviceRole.objects.all()[:2]
params = {'role_id': [device_roles[0].pk, device_roles[1].pk]} params = {'device_role_id': [device_roles[0].pk, device_roles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'role': [device_roles[0].slug, device_roles[1].slug]} params = {'device_role': [device_roles[0].slug, device_roles[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_platform(self): def test_platform(self):
@ -1080,9 +1098,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ConfigTemplateTestCase(TestCase, BaseFilterSetTests): class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConfigTemplate.objects.all() queryset = ConfigTemplate.objects.all()
filterset = ConfigTemplateFilterSet filterset = ConfigTemplateFilterSet
ignore_fields = ('template_code', 'environment_params', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1109,6 +1128,93 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
class TagTestCase(TestCase, ChangeLoggedFilterSetTests): class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Tag.objects.all() queryset = Tag.objects.all()
filterset = TagFilterSet filterset = TagFilterSet
ignore_fields = (
'object_types',
# Reverse relationships (to tagged models) we can ignore
'aggregate',
'asn',
'asnrange',
'cable',
'circuit',
'circuittermination',
'circuittype',
'cluster',
'clustergroup',
'clustertype',
'configtemplate',
'consoleport',
'consoleserverport',
'contact',
'contactassignment',
'contactgroup',
'contactrole',
'datasource',
'device',
'devicebay',
'devicerole',
'devicetype',
'dummymodel', # From dummy_plugin
'eventrule',
'fhrpgroup',
'frontport',
'ikepolicy',
'ikeproposal',
'interface',
'inventoryitem',
'inventoryitemrole',
'ipaddress',
'iprange',
'ipsecpolicy',
'ipsecprofile',
'ipsecproposal',
'journalentry',
'l2vpn',
'l2vpntermination',
'location',
'manufacturer',
'module',
'modulebay',
'moduletype',
'platform',
'powerfeed',
'poweroutlet',
'powerpanel',
'powerport',
'prefix',
'provider',
'provideraccount',
'providernetwork',
'rack',
'rackreservation',
'rackrole',
'rearport',
'region',
'rir',
'role',
'routetarget',
'service',
'servicetemplate',
'site',
'sitegroup',
'tenant',
'tenantgroup',
'tunnel',
'tunnelgroup',
'tunneltermination',
'virtualchassis',
'virtualdevicecontext',
'virtualdisk',
'virtualmachine',
'vlan',
'vlangroup',
'vminterface',
'vrf',
'webhook',
'wirelesslan',
'wirelesslangroup',
'wirelesslink',
)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1177,6 +1283,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
class ObjectChangeTestCase(TestCase, BaseFilterSetTests): class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
queryset = ObjectChange.objects.all() queryset = ObjectChange.objects.all()
filterset = ObjectChangeFilterSet filterset = ObjectChangeFilterSet
ignore_fields = ('prechange_data', 'postchange_data')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -62,14 +62,14 @@ class CustomFieldModelFormTest(TestCase):
cf_object = CustomField.objects.create( cf_object = CustomField.objects.create(
name='object', name='object',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ObjectType.objects.get_for_model(Site) related_object_type=ObjectType.objects.get_for_model(Site)
) )
cf_object.object_types.set([object_type]) cf_object.object_types.set([object_type])
cf_multiobject = CustomField.objects.create( cf_multiobject = CustomField.objects.create(
name='multiobject', name='multiobject',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ObjectType.objects.get_for_model(Site) related_object_type=ObjectType.objects.get_for_model(Site)
) )
cf_multiobject.object_types.set([object_type]) cf_multiobject.object_types.set([object_type])

View File

@ -54,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
'name,label,type,object_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'name,label,type,object_types,related_object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable',
'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes',
'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes',
'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',

View File

@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.rqworker import get_workers_for_queue from utilities.rqworker import get_workers_for_queue
@ -26,6 +27,7 @@ from utilities.views import ContentTypePermissionRequiredMixin, register_model_v
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
from .scripts import run_script from .scripts import run_script
from .tables import ReportResultsTable, ScriptResultsTable
# #
@ -757,7 +759,6 @@ class ImageAttachmentListView(generic.ObjectListView):
class ImageAttachmentEditView(generic.ObjectEditView): class ImageAttachmentEditView(generic.ObjectEditView):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
form = forms.ImageAttachmentForm form = forms.ImageAttachmentForm
template_name = 'extras/imageattachment_edit.html'
def alter_object(self, instance, request, args, kwargs): def alter_object(self, instance, request, args, kwargs):
if not instance.pk: if not instance.pk:
@ -1143,19 +1144,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
return redirect(f'{url}{path}') return redirect(f'{url}{path}')
class ScriptResultView(generic.ObjectView): class ScriptResultView(TableMixin, generic.ObjectView):
queryset = Job.objects.all() queryset = Job.objects.all()
def get_required_permission(self): def get_required_permission(self):
return 'extras.view_script' return 'extras.view_script'
def get_table(self, job, request, bulk_actions=True):
data = []
tests = None
table = None
index = 0
if job.data:
if 'log' in job.data:
if 'tests' in job.data:
tests = job.data['tests']
for log in job.data['log']:
index += 1
result = {
'index': index,
'time': log.get('time'),
'status': log.get('status'),
'message': log.get('message'),
}
data.append(result)
table = ScriptResultsTable(data, user=request.user)
table.configure(request)
else:
# for legacy reports
tests = job.data
if tests:
for method, test_data in tests.items():
if 'log' in test_data:
for time, status, obj, url, message in test_data['log']:
index += 1
result = {
'index': index,
'method': method,
'time': time,
'status': status,
'object': obj,
'url': url,
'message': message,
}
data.append(result)
table = ReportResultsTable(data, user=request.user)
table.configure(request)
return table
def get(self, request, **kwargs): def get(self, request, **kwargs):
table = None
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk')) job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
if job.completed:
table = self.get_table(job, request, bulk_actions=False)
context = { context = {
'script': job.object, 'script': job.object,
'job': job, 'job': job,
'table': table,
} }
if job.data and 'log' in job.data: if job.data and 'log' in job.data:
# Script # Script
context['tests'] = job.data.get('tests', {}) context['tests'] = job.data.get('tests', {})

View File

@ -8,6 +8,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from circuits.models import Provider
from dcim.models import Device, Interface, Region, Site, SiteGroup from dcim.models import Device, Interface, Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
@ -75,7 +76,7 @@ class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = VRF model = VRF
fields = ['id', 'name', 'rd', 'enforce_unique', 'description'] fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
@ -101,6 +102,28 @@ class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='rd', to_field_name='rd',
label=_('Export VRF (RD)'), label=_('Export VRF (RD)'),
) )
importing_l2vpn_id = django_filters.ModelMultipleChoiceFilter(
field_name='importing_l2vpns',
queryset=L2VPN.objects.all(),
label=_('Importing L2VPN'),
)
importing_l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='importing_l2vpns__identifier',
queryset=L2VPN.objects.all(),
to_field_name='identifier',
label=_('Importing L2VPN (identifier)'),
)
exporting_l2vpn_id = django_filters.ModelMultipleChoiceFilter(
field_name='exporting_l2vpns',
queryset=L2VPN.objects.all(),
label=_('Exporting L2VPN'),
)
exporting_l2vpn = django_filters.ModelMultipleChoiceFilter(
field_name='exporting_l2vpns__identifier',
queryset=L2VPN.objects.all(),
to_field_name='identifier',
label=_('Exporting L2VPN (identifier)'),
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -112,14 +135,14 @@ class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'description')
class RIRFilterSet(OrganizationalModelFilterSet): class RIRFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = RIR model = RIR
fields = ['id', 'name', 'slug', 'is_private', 'description'] fields = ('id', 'name', 'slug', 'is_private', 'description')
class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
@ -144,7 +167,7 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = ['id', 'date_added', 'description'] fields = ('id', 'date_added', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -183,7 +206,7 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = ASNRange model = ASNRange
fields = ['id', 'name', 'start', 'end', 'description'] fields = ('id', 'name', 'slug', 'start', 'end', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -214,10 +237,21 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='providers',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='providers__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
class Meta: class Meta:
model = ASN model = ASN
fields = ['id', 'asn', 'description'] fields = ('id', 'asn', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -234,7 +268,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = Role model = Role
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description', 'weight')
class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
@ -359,7 +393,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = Prefix model = Prefix
fields = ['id', 'is_pool', 'mark_utilized', 'description'] fields = ('id', 'is_pool', 'mark_utilized', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -475,7 +509,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = IPRange model = IPRange
fields = ['id', 'mark_utilized', 'description'] fields = ('id', 'mark_utilized', 'size', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -628,10 +662,20 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
role = django_filters.MultipleChoiceFilter( role = django_filters.MultipleChoiceFilter(
choices=IPAddressRoleChoices choices=IPAddressRoleChoices
) )
service_id = django_filters.ModelMultipleChoiceFilter(
field_name='services',
queryset=Service.objects.all(),
label=_('Service (ID)'),
)
nat_inside_id = django_filters.ModelMultipleChoiceFilter(
field_name='nat_inside',
queryset=IPAddress.objects.all(),
label=_('NAT inside IP address (ID)'),
)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['id', 'dns_name', 'description'] fields = ('id', 'dns_name', 'description', 'assigned_object_type', 'assigned_object_id')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -758,7 +802,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ['id', 'group_id', 'name', 'auth_key', 'description'] fields = ('id', 'group_id', 'name', 'auth_key', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -819,7 +863,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = FHRPGroupAssignment model = FHRPGroupAssignment
fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority'] fields = ('id', 'group_id', 'interface_type', 'interface_id', 'priority')
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
devices = Device.objects.filter(**{f'{name}__in': value}) devices = Device.objects.filter(**{f'{name}__in': value})
@ -849,7 +893,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
region = django_filters.NumberFilter( region = django_filters.NumberFilter(
method='filter_scope' method='filter_scope'
) )
sitegroup = django_filters.NumberFilter( site_group = django_filters.NumberFilter(
method='filter_scope' method='filter_scope'
) )
site = django_filters.NumberFilter( site = django_filters.NumberFilter(
@ -861,16 +905,20 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
rack = django_filters.NumberFilter( rack = django_filters.NumberFilter(
method='filter_scope' method='filter_scope'
) )
clustergroup = django_filters.NumberFilter( cluster_group = django_filters.NumberFilter(
method='filter_scope' method='filter_scope'
) )
cluster = django_filters.NumberFilter( cluster = django_filters.NumberFilter(
method='filter_scope' method='filter_scope'
) )
# TODO: Remove in v4.1
sitegroup = site_group
clustergroup = cluster_group
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ['id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id'] fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -882,8 +930,9 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
def filter_scope(self, queryset, name, value): def filter_scope(self, queryset, name, value):
model_name = name.replace('_', '')
return queryset.filter( return queryset.filter(
scope_type=ContentType.objects.get(model=name), scope_type=ContentType.objects.get(model=model_name),
scope_id=value scope_id=value
) )
@ -975,7 +1024,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['id', 'vid', 'name', 'description'] fields = ('id', 'vid', 'name', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1008,7 +1057,7 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = ['id', 'name', 'protocol', 'description'] fields = ('id', 'name', 'protocol', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1041,26 +1090,29 @@ class ServiceFilterSet(NetBoxModelFilterSet):
to_field_name='name', to_field_name='name',
label=_('Virtual machine (name)'), label=_('Virtual machine (name)'),
) )
ipaddress_id = django_filters.ModelMultipleChoiceFilter( ip_address_id = django_filters.ModelMultipleChoiceFilter(
field_name='ipaddresses', field_name='ipaddresses',
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
label=_('IP address (ID)'), label=_('IP address (ID)'),
) )
ipaddress = django_filters.ModelMultipleChoiceFilter( ip_address = django_filters.ModelMultipleChoiceFilter(
field_name='ipaddresses__address', field_name='ipaddresses__address',
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
to_field_name='address', to_field_name='address',
label=_('IP address'), label=_('IP address'),
) )
port = NumericArrayFilter( port = NumericArrayFilter(
field_name='ports', field_name='ports',
lookup_expr='contains' lookup_expr='contains'
) )
# TODO: Remove in v4.1
ipaddress = ip_address
ipaddress_id = ip_address_id
class Meta: class Meta:
model = Service model = Service
fields = ['id', 'name', 'protocol', 'description'] fields = ('id', 'name', 'protocol', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -13,6 +13,7 @@ from utilities.forms import add_blank_choice
from utilities.forms.fields import ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
) )
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect from utilities.forms.widgets import BulkEditNullBooleanSelect
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
@ -55,7 +56,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
model = VRF model = VRF
fieldsets = ( fieldsets = (
(None, ('tenant', 'enforce_unique', 'description')), FieldSet('tenant', 'enforce_unique', 'description'),
) )
nullable_fields = ('tenant', 'description', 'comments') nullable_fields = ('tenant', 'description', 'comments')
@ -75,7 +76,7 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
(None, ('tenant', 'description')), FieldSet('tenant', 'description'),
) )
nullable_fields = ('tenant', 'description', 'comments') nullable_fields = ('tenant', 'description', 'comments')
@ -94,7 +95,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm):
model = RIR model = RIR
fieldsets = ( fieldsets = (
(None, ('is_private', 'description')), FieldSet('is_private', 'description'),
) )
nullable_fields = ('is_private', 'description') nullable_fields = ('is_private', 'description')
@ -118,7 +119,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
model = ASNRange model = ASNRange
fieldsets = ( fieldsets = (
(None, ('rir', 'tenant', 'description')), FieldSet('rir', 'tenant', 'description'),
) )
nullable_fields = ('description',) nullable_fields = ('description',)
@ -148,7 +149,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
model = ASN model = ASN
fieldsets = ( fieldsets = (
(None, ('sites', 'rir', 'tenant', 'description')), FieldSet('sites', 'rir', 'tenant', 'description'),
) )
nullable_fields = ('tenant', 'description', 'comments') nullable_fields = ('tenant', 'description', 'comments')
@ -177,7 +178,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
(None, ('rir', 'tenant', 'date_added', 'description')), FieldSet('rir', 'tenant', 'date_added', 'description'),
) )
nullable_fields = ('date_added', 'description', 'comments') nullable_fields = ('date_added', 'description', 'comments')
@ -195,7 +196,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
model = Role model = Role
fieldsets = ( fieldsets = (
(None, ('weight', 'description')), FieldSet('weight', 'description'),
) )
nullable_fields = ('description',) nullable_fields = ('description',)
@ -265,9 +266,9 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
(None, ('tenant', 'status', 'role', 'description')), FieldSet('tenant', 'status', 'role', 'description'),
(_('Site'), ('region', 'site_group', 'site')), FieldSet('region', 'site_group', 'site', name=_('Site')),
(_('Addressing'), ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')), FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
) )
nullable_fields = ( nullable_fields = (
'site', 'vrf', 'tenant', 'role', 'description', 'comments', 'site', 'vrf', 'tenant', 'role', 'description', 'comments',
@ -309,7 +310,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description')), FieldSet('status', 'role', 'vrf', 'tenant', 'mark_utilized', 'description'),
) )
nullable_fields = ( nullable_fields = (
'vrf', 'tenant', 'role', 'description', 'comments', 'vrf', 'tenant', 'role', 'description', 'comments',
@ -357,8 +358,8 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'tenant', 'description')), FieldSet('status', 'role', 'tenant', 'description'),
(_('Addressing'), ('vrf', 'mask_length', 'dns_name')), FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
) )
nullable_fields = ( nullable_fields = (
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
@ -400,8 +401,8 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('protocol', 'group_id', 'name', 'description')), FieldSet('protocol', 'group_id', 'name', 'description'),
(_('Authentication'), ('auth_type', 'auth_key')), FieldSet('auth_type', 'auth_key', name=_('Authentication')),
) )
nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments') nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
@ -485,8 +486,10 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
model = VLANGroup model = VLANGroup
fieldsets = ( fieldsets = (
(None, ('site', 'min_vid', 'max_vid', 'description')), FieldSet('site', 'min_vid', 'max_vid', 'description'),
(_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope')
),
) )
nullable_fields = ('description',) nullable_fields = ('description',)
@ -556,8 +559,8 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'tenant', 'description')), FieldSet('status', 'role', 'tenant', 'description'),
(_('Site & Group'), ('region', 'site_group', 'site', 'group')), FieldSet('region', 'site_group', 'site', 'group', name=_('Site & Group')),
) )
nullable_fields = ( nullable_fields = (
'site', 'group', 'tenant', 'role', 'description', 'comments', 'site', 'group', 'tenant', 'role', 'description', 'comments',
@ -587,7 +590,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
(None, ('protocol', 'ports', 'description')), FieldSet('protocol', 'ports', 'description'),
) )
nullable_fields = ('description', 'comments') nullable_fields = ('description', 'comments')

View File

@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from vpn.models import L2VPN from vpn.models import L2VPN
@ -42,9 +43,9 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VRF model = VRF
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Route Targets'), ('import_target_id', 'export_target_id')), FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
import_target_id = DynamicModelMultipleChoiceField( import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
@ -62,9 +63,9 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('VRF'), ('importing_vrf_id', 'exporting_vrf_id')), FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
importing_vrf_id = DynamicModelMultipleChoiceField( importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
@ -94,9 +95,9 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('family', 'rir_id')), FieldSet('family', 'rir_id', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
@ -114,9 +115,9 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASNRange model = ASNRange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Range'), ('rir_id', 'start', 'end')), FieldSet('rir_id', 'start', 'end', name=_('Range')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
rir_id = DynamicModelMultipleChoiceField( rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
@ -137,9 +138,9 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN model = ASN
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Assignment'), ('rir_id', 'site_id')), FieldSet('rir_id', 'site_id', name=_('Assignment')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
rir_id = DynamicModelMultipleChoiceField( rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
@ -162,11 +163,14 @@ class RoleFilterForm(NetBoxModelFilterSetForm):
class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Addressing'), ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')), FieldSet(
(_('VRF'), ('vrf_id', 'present_in_vrf_id')), 'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
(_('Location'), ('region_id', 'site_group_id', 'site_id')), name=_('Addressing')
(_('Tenant'), ('tenant_group_id', 'tenant_id')), ),
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
mask_length__lte = forms.IntegerField( mask_length__lte = forms.IntegerField(
widget=forms.HiddenInput() widget=forms.HiddenInput()
@ -251,9 +255,9 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')), FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_utilized', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
@ -290,11 +294,14 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name')), FieldSet(
(_('VRF'), ('vrf_id', 'present_in_vrf_id')), 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
(_('Tenant'), ('tenant_group_id', 'tenant_id')), name=_('Attributes')
(_('Device/VM'), ('device_id', 'virtual_machine_id')), ),
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
) )
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role') selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
parent = forms.CharField( parent = forms.CharField(
@ -304,7 +311,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
'placeholder': 'Prefix', 'placeholder': 'Prefix',
} }
), ),
label='Parent Prefix' label=_('Parent Prefix')
) )
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
@ -364,9 +371,9 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('name', 'protocol', 'group_id')), FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
(_('Authentication'), ('auth_type', 'auth_key')), FieldSet('auth_type', 'auth_key', name=_('Authentication')),
) )
name = forms.CharField( name = forms.CharField(
label=_('Name'), label=_('Name'),
@ -396,9 +403,9 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
class VLANGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region', 'sitegroup', 'site', 'location', 'rack')), FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
(_('VLAN ID'), ('min_vid', 'max_vid')), FieldSet('min_vid', 'max_vid', name=_('VLAN ID')),
) )
model = VLANGroup model = VLANGroup
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
@ -444,10 +451,10 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Location'), ('region_id', 'site_group_id', 'site_id')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
(_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')), FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
) )
selector_fields = ('filter_id', 'q', 'site_id') selector_fields = ('filter_id', 'q', 'site_id')
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
@ -504,8 +511,8 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('protocol', 'port')), FieldSet('protocol', 'port', name=_('Attributes')),
) )
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
label=_('Protocol'), label=_('Protocol'),
@ -522,9 +529,9 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
class ServiceFilterForm(ServiceTemplateFilterForm): class ServiceFilterForm(ServiceTemplateFilterForm):
model = Service model = Service
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), FieldSet('q', 'filter_id', 'tag'),
(_('Attributes'), ('protocol', 'port')), FieldSet('protocol', 'port', name=_('Attributes')),
(_('Assignment'), ('device_id', 'virtual_machine_id')), FieldSet('device_id', 'virtual_machine_id', name=_('Assignment')),
) )
device_id = DynamicModelMultipleChoiceField( device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),

View File

@ -16,6 +16,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
SlugField, SlugField,
) )
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
from utilities.forms.widgets import DatePicker from utilities.forms.widgets import DatePicker
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@ -56,9 +57,9 @@ class VRFForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('VRF'), ('name', 'rd', 'enforce_unique', 'description', 'tags')), FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
(_('Route Targets'), ('import_targets', 'export_targets')), FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -74,8 +75,8 @@ class VRFForm(TenancyForm, NetBoxModelForm):
class RouteTargetForm(TenancyForm, NetBoxModelForm): class RouteTargetForm(TenancyForm, NetBoxModelForm):
fieldsets = ( fieldsets = (
('Route Target', ('name', 'description', 'tags')), FieldSet('name', 'description', 'tags', name=_('Route Target')),
('Tenancy', ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
comments = CommentField() comments = CommentField()
@ -90,9 +91,7 @@ class RIRForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('RIR'), ( FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
'name', 'slug', 'is_private', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -110,8 +109,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Aggregate'), ('prefix', 'rir', 'date_added', 'description', 'tags')), FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -131,8 +130,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('ASN Range'), ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')), FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -155,8 +154,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('ASN'), ('asn', 'rir', 'sites', 'description', 'tags')), FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
(_('Tenancy'), ('tenant_group', 'tenant')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -184,9 +183,7 @@ class RoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('Role'), ( FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
'name', 'slug', 'weight', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -226,9 +223,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Prefix'), ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), FieldSet(
(_('Site/VLAN Assignment'), ('site', 'vlan')), 'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
(_('Tenancy'), ('tenant_group', 'tenant')), ),
FieldSet('site', 'vlan', name=_('Site/VLAN Assignment')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -253,8 +252,11 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('IP Range'), ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')), FieldSet(
(_('Tenancy'), ('tenant_group', 'tenant')), 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags',
name=_('IP Range')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
class Meta: class Meta:
@ -307,6 +309,20 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = (
FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet(
TabbedGroups(
FieldSet('interface', name=_('Device')),
FieldSet('vminterface', name=_('Virtual Machine')),
FieldSet('fhrpgroup', name=_('FHRP Group')),
),
'primary_for_parent', name=_('Assignment')
),
FieldSet('nat_inside', name=_('NAT IP (Inside)')),
)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
@ -373,20 +389,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.") 'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
) )
# Do not allow assigning a network ID or broadcast address to an interface.
if interface and (address := self.cleaned_data.get('address')):
if address.ip == address.network:
msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(ip=address.ip)
if address.version == 4 and address.prefixlen not in (31, 32):
raise ValidationError(msg)
if address.version == 6 and address.prefixlen not in (127, 128):
raise ValidationError(msg)
if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format(
ip=address.ip
)
raise ValidationError(msg)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
ipaddress = super().save(*args, **kwargs) ipaddress = super().save(*args, **kwargs)
@ -457,9 +459,9 @@ class FHRPGroupForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('FHRP Group'), ('protocol', 'group_id', 'name', 'description', 'tags')), FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
(_('Authentication'), ('auth_type', 'auth_key')), FieldSet('auth_type', 'auth_key', name=_('Authentication')),
(_('Virtual IP Address'), ('ip_vrf', 'ip_address', 'ip_status')) FieldSet('ip_vrf', 'ip_address', 'ip_status', name=_('Virtual IP Address'))
) )
class Meta: class Meta:
@ -516,6 +518,10 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
queryset=FHRPGroup.objects.all() queryset=FHRPGroup.objects.all()
) )
fieldsets = (
FieldSet(ObjectAttribute('interface'), 'group', 'priority'),
)
class Meta: class Meta:
model = FHRPGroupAssignment model = FHRPGroupAssignment
fields = ('group', 'priority') fields = ('group', 'priority')
@ -601,9 +607,12 @@ class VLANGroupForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
(_('VLAN Group'), ('name', 'slug', 'description', 'tags')), FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
(_('Child VLANs'), ('min_vid', 'max_vid')), FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
(_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
name=_('Scope')
),
) )
class Meta: class Meta:
@ -676,9 +685,7 @@ class ServiceTemplateForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
(_('Service Template'), ( FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Service Template')),
'name', 'protocol', 'ports', 'description', 'tags',
)),
) )
class Meta: class Meta:
@ -718,6 +725,18 @@ class ServiceForm(NetBoxModelForm):
) )
comments = CommentField() comments = CommentField()
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device', name=_('Device')),
FieldSet('virtual_machine', name=_('Virtual Machine')),
),
'name',
InlineFields('protocol', 'ports', label=_('Port(s)')),
'ipaddresses', 'description', 'tags', name=_('Service')
),
)
class Meta: class Meta:
model = Service model = Service
fields = [ fields = [
@ -732,6 +751,20 @@ class ServiceCreateForm(ServiceForm):
required=False required=False
) )
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device', name=_('Device')),
FieldSet('virtual_machine', name=_('Virtual Machine')),
),
TabbedGroups(
FieldSet('service_template', name=_('From Template')),
FieldSet('name', 'protocol', 'ports', name=_('Custom')),
),
'ipaddresses', 'description', 'tags', name=_('Service')
),
)
class Meta(ServiceForm.Meta): class Meta(ServiceForm.Meta):
fields = [ fields = [
'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',

View File

@ -1,6 +1,7 @@
import graphene import graphene
from ipam import filtersets, models from ipam import filtersets, models
from .mixins import IPAddressesMixin
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@ -71,7 +72,7 @@ class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
class FHRPGroupType(NetBoxObjectType): class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
class Meta: class Meta:
model = models.FHRPGroup model = models.FHRPGroup

View File

@ -844,6 +844,25 @@ class IPAddress(PrimaryModel):
'address': _("Cannot create IP address with /0 mask.") 'address': _("Cannot create IP address with /0 mask.")
}) })
# Do not allow assigning a network ID or broadcast address to an interface.
if self.assigned_object:
if self.address.ip == self.address.network:
msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(
ip=self.address.ip
)
if self.address.version == 4 and self.address.prefixlen not in (31, 32):
raise ValidationError(msg)
if self.address.version == 6 and self.address.prefixlen not in (127, 128):
raise ValidationError(msg)
if (
self.address.version == 4 and self.address.ip == self.address.broadcast and
self.address.prefixlen not in (31, 32)
):
msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format(
ip=self.address.ip
)
raise ValidationError(msg)
# Enforce unique IP space (if applicable) # Enforce unique IP space (if applicable)
if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates() duplicate_ips = self.get_duplicates()

View File

@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from netaddr import IPNetwork from netaddr import IPNetwork
from circuits.models import Provider
from dcim.choices import InterfaceTypeChoices from dcim.choices import InterfaceTypeChoices
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
from ipam.choices import * from ipam.choices import *
@ -10,6 +11,8 @@ from ipam.models import *
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from vpn.choices import L2VPNTypeChoices
from vpn.models import L2VPN
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests): class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
@ -110,13 +113,6 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
] ]
RIR.objects.bulk_create(rirs) RIR.objects.bulk_create(rirs)
sites = [
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3')
]
Site.objects.bulk_create(sites)
tenants = [ tenants = [
Tenant(name='Tenant 1', slug='tenant-1'), Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'), Tenant(name='Tenant 2', slug='tenant-2'),
@ -136,6 +132,12 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
ASN.objects.bulk_create(asns) ASN.objects.bulk_create(asns)
sites = [
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3')
]
Site.objects.bulk_create(sites)
asns[0].sites.set([sites[0]]) asns[0].sites.set([sites[0]])
asns[1].sites.set([sites[1]]) asns[1].sites.set([sites[1]])
asns[2].sites.set([sites[2]]) asns[2].sites.set([sites[2]])
@ -143,6 +145,16 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
asns[4].sites.set([sites[1]]) asns[4].sites.set([sites[1]])
asns[5].sites.set([sites[2]]) asns[5].sites.set([sites[2]])
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
providers[0].asns.add(asns[0])
providers[1].asns.add(asns[1])
providers[2].asns.add(asns[2])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -176,11 +188,24 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests): class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VRF.objects.all() queryset = VRF.objects.all()
filterset = VRFFilterSet filterset = VRFFilterSet
def get_m2m_filter_name(self, field):
# Override filter names for import & export RouteTargets
if field.name == 'import_targets':
return 'import_target'
if field.name == 'export_targets':
return 'export_target'
return ChangeLoggedFilterSetTests.get_m2m_filter_name(field)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -277,6 +302,18 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RouteTarget.objects.all() queryset = RouteTarget.objects.all()
filterset = RouteTargetFilterSet filterset = RouteTargetFilterSet
def get_m2m_filter_name(self, field):
# Override filter names for import & export VRFs and L2VPNs
if field.name == 'importing_vrfs':
return 'importing_vrf'
if field.name == 'exporting_vrfs':
return 'exporting_vrf'
if field.name == 'importing_l2vpns':
return 'importing_l2vpn'
if field.name == 'exporting_l2vpns':
return 'exporting_l2vpn'
return ChangeLoggedFilterSetTests.get_m2m_filter_name(field)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -322,6 +359,17 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
vrfs[1].import_targets.add(route_targets[4], route_targets[5]) vrfs[1].import_targets.add(route_targets[4], route_targets[5])
vrfs[1].export_targets.add(route_targets[6], route_targets[7]) vrfs[1].export_targets.add(route_targets[6], route_targets[7])
l2vpns = (
L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=100),
L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=200),
L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=300),
)
L2VPN.objects.bulk_create(l2vpns)
l2vpns[0].import_targets.add(route_targets[0], route_targets[1])
l2vpns[0].export_targets.add(route_targets[2], route_targets[3])
l2vpns[1].import_targets.add(route_targets[4], route_targets[5])
l2vpns[1].export_targets.add(route_targets[6], route_targets[7])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -344,6 +392,20 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'exporting_vrf': [vrfs[0].rd, vrfs[1].rd]} params = {'exporting_vrf': [vrfs[0].rd, vrfs[1].rd]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_importing_l2vpn(self):
l2vpns = L2VPN.objects.all()[:2]
params = {'importing_l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'importing_l2vpn': [l2vpns[0].identifier, l2vpns[1].identifier]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_exporting_l2vpn(self):
l2vpns = L2VPN.objects.all()[:2]
params = {'exporting_l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'exporting_l2vpn': [l2vpns[0].identifier, l2vpns[1].identifier]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_tenant(self): def test_tenant(self):
tenants = Tenant.objects.all()[:2] tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
@ -922,6 +984,7 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.all()
filterset = IPAddressFilterSet filterset = IPAddressFilterSet
ignore_fields = ('fhrpgroup',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1092,6 +1155,16 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
IPAddress.objects.bulk_create(ipaddresses) IPAddress.objects.bulk_create(ipaddresses)
services = (
Service(name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]),
Service(name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]),
Service(name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1]),
)
Service.objects.bulk_create(services)
services[0].ipaddresses.add(ipaddresses[0])
services[1].ipaddresses.add(ipaddresses[1])
services[2].ipaddresses.add(ipaddresses[2])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -1231,6 +1304,11 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]} params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_service(self):
services = Service.objects.all()[:2]
params = {'service_id': [services[0].pk, services[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests): class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = FHRPGroup.objects.all() queryset = FHRPGroup.objects.all()
@ -1475,6 +1553,7 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLAN.objects.all() queryset = VLAN.objects.all()
filterset = VLANFilterSet filterset = VLANFilterSet
ignore_fields = ('interfaces_as_tagged', 'vminterfaces_as_tagged')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1733,6 +1812,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ServiceTemplate.objects.all() queryset = ServiceTemplate.objects.all()
filterset = ServiceTemplateFilterSet filterset = ServiceTemplateFilterSet
ignore_fields = ('ports',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1797,6 +1877,7 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Service.objects.all() queryset = Service.objects.all()
filterset = ServiceFilterSet filterset = ServiceFilterSet
ignore_fields = ('ports',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1883,9 +1964,9 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'virtual_machine': [vms[0].name, vms[1].name]} params = {'virtual_machine': [vms[0].name, vms[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ipaddress(self): def test_ip_address(self):
ips = IPAddress.objects.all()[:2] ips = IPAddress.objects.all()[:2]
params = {'ipaddress_id': [ips[0].pk, ips[1].pk]} params = {'ip_address_id': [ips[0].pk, ips[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]} params = {'ip_address': [str(ips[0].address), str(ips[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -781,7 +781,6 @@ class IPAddressView(generic.ObjectView):
class IPAddressEditView(generic.ObjectEditView): class IPAddressEditView(generic.ObjectEditView):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.all()
form = forms.IPAddressForm form = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
def alter_object(self, obj, request, url_args, url_kwargs): def alter_object(self, obj, request, url_args, url_kwargs):
@ -1059,7 +1058,6 @@ class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
class FHRPGroupAssignmentEditView(generic.ObjectEditView): class FHRPGroupAssignmentEditView(generic.ObjectEditView):
queryset = FHRPGroupAssignment.objects.all() queryset = FHRPGroupAssignment.objects.all()
form = forms.FHRPGroupAssignmentForm form = forms.FHRPGroupAssignmentForm
template_name = 'ipam/fhrpgroupassignment_edit.html'
def alter_object(self, instance, request, args, kwargs): def alter_object(self, instance, request, args, kwargs):
if not instance.pk: if not instance.pk:
@ -1236,14 +1234,12 @@ class ServiceView(generic.ObjectView):
class ServiceCreateView(generic.ObjectEditView): class ServiceCreateView(generic.ObjectEditView):
queryset = Service.objects.all() queryset = Service.objects.all()
form = forms.ServiceCreateForm form = forms.ServiceCreateForm
template_name = 'ipam/service_create.html'
@register_model_view(Service, 'edit') @register_model_view(Service, 'edit')
class ServiceEditView(generic.ObjectEditView): class ServiceEditView(generic.ObjectEditView):
queryset = Service.objects.all() queryset = Service.objects.all()
form = forms.ServiceForm form = forms.ServiceForm
template_name = 'ipam/service_edit.html'
@register_model_view(Service, 'delete') @register_model_view(Service, 'delete')

View File

@ -24,7 +24,7 @@ class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
Attributes: Attributes:
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of fieldsets: An iterable of FieldSets which define a name and set of fields to display per section of
the rendered form (optional). If not defined, the all fields will be rendered as a single section. the rendered form (optional). If not defined, the all fields will be rendered as a single section.
""" """
fieldsets = () fieldsets = ()

View File

@ -1,6 +1,9 @@
from collections import namedtuple from collections import namedtuple
from decimal import Decimal
from django.core.exceptions import FieldDoesNotExist
from django.db import models from django.db import models
from netaddr import IPAddress, IPNetwork
from ipam.fields import IPAddressField, IPNetworkField from ipam.fields import IPAddressField, IPNetworkField
from netbox.registry import registry from netbox.registry import registry
@ -56,6 +59,24 @@ class SearchIndex:
return FieldTypes.INTEGER return FieldTypes.INTEGER
return FieldTypes.STRING return FieldTypes.STRING
@staticmethod
def get_attr_type(instance, field_name):
"""
Return the data type of the specified object attribute.
"""
value = getattr(instance, field_name)
if type(value) is str:
return FieldTypes.STRING
if type(value) is int:
return FieldTypes.INTEGER
if type(value) in (float, Decimal):
return FieldTypes.FLOAT
if type(value) is IPNetwork:
return FieldTypes.CIDR
if type(value) is IPAddress:
return FieldTypes.INET
return FieldTypes.STRING
@staticmethod @staticmethod
def get_field_value(instance, field_name): def get_field_value(instance, field_name):
""" """
@ -82,7 +103,11 @@ class SearchIndex:
# Capture built-in fields # Capture built-in fields
for name, weight in cls.fields: for name, weight in cls.fields:
try:
type_ = cls.get_field_type(instance, name) type_ = cls.get_field_type(instance, name)
except FieldDoesNotExist:
# Not a concrete field; handle as an object attribute
type_ = cls.get_attr_type(instance, name)
value = cls.get_field_value(instance, name) value = cls.get_field_value(instance, name)
if type_ and value: if type_ and value:
values.append( values.append(

View File

@ -264,8 +264,10 @@ class SearchTable(tables.Table):
super().__init__(data, **kwargs) super().__init__(data, **kwargs)
def render_field(self, value, record): def render_field(self, value, record):
if hasattr(record.object, value): try:
return title(record.object._meta.get_field(value).verbose_name) model_field = record.object._meta.get_field(value)
return title(model_field.verbose_name)
except FieldDoesNotExist:
return value return value
def render_value(self, value): def render_value(self, value):

View File

@ -5,10 +5,11 @@
* @param inferred {boolean} Value is inferred from browser/system preference. * @param inferred {boolean} Value is inferred from browser/system preference.
*/ */
function setMode(mode, inferred) { function setMode(mode, inferred) {
document.documentElement.setAttribute("data-netbox-color-mode", mode); document.documentElement.setAttribute("data-bs-theme", mode);
localStorage.setItem("netbox-color-mode", mode); localStorage.setItem("netbox-color-mode", mode);
localStorage.setItem("netbox-color-mode-inferred", inferred); localStorage.setItem("netbox-color-mode-inferred", inferred);
} }
/** /**
* Determine the best initial color mode to use prior to rendering. * Determine the best initial color mode to use prior to rendering.
*/ */
@ -69,4 +70,4 @@ function initMode() {
console.error(error); console.error(error);
} }
return setMode("light", true); return setMode("light", true);
}; }

View File

@ -10,15 +10,8 @@
{% csrf_token %} {% csrf_token %}
{# Built-in preferences #} {# Built-in preferences #}
{% for group, fields in form.fieldsets %} {% for fieldset in form.fieldsets %}
<div class="field-group my-5"> {% render_fieldset form fieldset %}
<div class="row">
<h5 class="col-9 offset-3">{{ group }}</h5>
</div>
{% for name in fields %}
{% render_field form|getfield:name %}
{% endfor %}
</div>
{% endfor %} {% endfor %}
{# Plugin preferences #} {# Plugin preferences #}

View File

@ -9,13 +9,7 @@
data-netbox-url-name="{{ request.resolver_match.url_name }}" data-netbox-url-name="{{ request.resolver_match.url_name }}"
data-netbox-base-path="{{ settings.BASE_PATH }}" data-netbox-base-path="{{ settings.BASE_PATH }}"
{% with preferences|get_key:'ui.colormode' as color_mode %} {% with preferences|get_key:'ui.colormode' as color_mode %}
{% if color_mode == 'dark'%} data-netbox-color-mode="{{ color_mode|default:"unset" }}"
data-netbox-color-mode="dark"
{% elif color_mode == 'light' %}
data-netbox-color-mode="light"
{% else %}
data-netbox-color-mode="unset"
{% endif %}
{% endwith %} {% endwith %}
> >
<head> <head>
@ -25,7 +19,16 @@
{# Page title #} {# Page title #}
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title> <title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
{# Initialize color mode #}
<script
type="text/javascript"
src="{% static 'setmode.js' %}"
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
</script>
<script type="text/javascript"> <script type="text/javascript">
(function () {
initMode()
})();
window.CSRF_TOKEN = "{{ csrf_token }}"; window.CSRF_TOKEN = "{{ csrf_token }}";
</script> </script>
@ -53,13 +56,9 @@
{# Additional <head> content #} {# Additional <head> content #}
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head>
<body </head>
{% if preferences|get_key:'ui.colormode' == 'dark' %} <body>
data-bs-theme="dark"
{% endif %}
>
{# Page layout #} {# Page layout #}
{% block layout %}{% endblock %} {% block layout %}{% endblock %}

View File

@ -41,7 +41,7 @@ Blocks:
{# Top menu #} {# Top menu #}
<header class="navbar navbar-expand-md d-none d-lg-flex d-print-none"> <header class="navbar navbar-expand-md d-none d-lg-flex d-print-none">
<div class="container-xl"> <div class="container-fluid">
{# Nav menu toggle #} {# Nav menu toggle #}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
@ -105,7 +105,7 @@ Blocks:
{# Page body #} {# Page body #}
{% block page %} {% block page %}
<div class="page-body my-1"> <div class="page-body my-1">
<div class="container-xl tab-content py-3"> <div class="container-fluid tab-content py-3">
{# Page content #} {# Page content #}
{% block content %}{% endblock %} {% block content %}{% endblock %}
@ -124,7 +124,7 @@ Blocks:
{# Page footer #} {# Page footer #}
<footer class="footer footer-transparent d-print-none py-2"> <footer class="footer footer-transparent d-print-none py-2">
<div class="container-xl d-flex justify-content-between align-items-center"> <div class="container-fluid d-flex justify-content-between align-items-center">
{% block footer %} {% block footer %}
{# Footer links #} {# Footer links #}

View File

@ -1,58 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load static %}
{% load form_helpers %}
{% load i18n %}
{% block form %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Circuit Termination" %}</h5>
</div>
{% render_field form.circuit %}
{% render_field form.term_side %}
{% render_field form.tags %}
{% render_field form.mark_connected %}
{% with providernetwork_tab_active=form.initial.provider_network %}
<div class="row">
<div class="col-9 offset-3">
<ul class="nav nav-pills mb-1" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link{% if not providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-target="#site" data-bs-toggle="tab">{% trans "Site" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link{% if providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#providernetwork">{% trans "Provider Network" %}</button>
</li>
</ul>
</div>
</div>
<div class="tab-content p-0 border-0">
<div class="tab-pane{% if not providernetwork_tab_active %} active{% endif %}" id="site">
{% render_field form.site %}
</div>
<div class="tab-pane{% if providernetwork_tab_active %} active{% endif %}" id="providernetwork">
{% render_field form.provider_network %}
</div>
</div>
{% endwith %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Termination Details" %}</h5>
</div>
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
{% render_field form.description %}
</div>
{% if form.custom_fields %}
<div class="field-group mb-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Custom Fields" %}</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %}

View File

@ -56,7 +56,7 @@
<td> <td>
{{ object.scheduled|annotated_date|placeholder }} {{ object.scheduled|annotated_date|placeholder }}
{% if object.interval %} {% if object.interval %}
({% blocktrans with interval=object.interval %}every {{ interval }} seconds{% endblocktrans %}) ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -5,7 +5,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block page-header %} {% block page-header %}
<div class="container-xl"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mt-2"> <div class="d-flex justify-content-between align-items-center mt-2">
{# Breadcrumbs #} {# Breadcrumbs #}
<nav class="breadcrumb-container" aria-label="breadcrumb"> <nav class="breadcrumb-container" aria-label="breadcrumb">

View File

@ -4,7 +4,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block page-header %} {% block page-header %}
<div class="container-xl"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mt-2"> <div class="d-flex justify-content-between align-items-center mt-2">
{# Breadcrumbs #} {# Breadcrumbs #}
<nav class="breadcrumb-container" aria-label="breadcrumb"> <nav class="breadcrumb-container" aria-label="breadcrumb">

View File

@ -1,107 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load static %}
{% load form_helpers %}
{% load helpers %}
{% load i18n %}
{% block form %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Inventory Item" %}</h5>
</div>
{% render_field form.device %}
{% render_field form.parent %}
{% render_field form.name %}
{% render_field form.label %}
{% render_field form.role %}
{% render_field form.description %}
{% render_field form.tags %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Hardware" %}</h5>
</div>
{% render_field form.manufacturer %}
{% render_field form.part_id %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Component Assignment" %}</h5>
</div>
<div class="row offset-sm-3">
<ul class="nav nav-pills mb-1" role="tablist">
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="consoleport_tab" data-bs-toggle="tab" aria-controls="consoleport" data-bs-target="#consoleport" class="nav-link {% if form.initial.consoleport or form.no_component %}active{% endif %}">
{% trans "Console Port" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="consoleserverport_tab" data-bs-toggle="tab" aria-controls="consoleserverport" data-bs-target="#consoleserverport" class="nav-link {% if form.initial.consoleserverport %}active{% endif %}">
{% trans "Console Server Port" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="frontport_tab" data-bs-toggle="tab" aria-controls="frontport" data-bs-target="#frontport" class="nav-link {% if form.initial.frontport %}active{% endif %}">
{% trans "Front Port" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
{% trans "Interface" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="poweroutlet_tab" data-bs-toggle="tab" aria-controls="poweroutlet" data-bs-target="#poweroutlet" class="nav-link {% if form.initial.poweroutlet %}active{% endif %}">
{% trans "Power Outlet" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="powerport_tab" data-bs-toggle="tab" aria-controls="powerport" data-bs-target="#powerport" class="nav-link {% if form.initial.powerport %}active{% endif %}">
{% trans "Power Port" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="rearport_tab" data-bs-toggle="tab" aria-controls="rearport" data-bs-target="#rearport" class="nav-link {% if form.initial.rearport %}active{% endif %}">
{% trans "Rear Port" %}
</button>
</li>
</ul>
</div>
<div class="tab-content p-0 border-0">
<div class="tab-pane {% if form.initial.consoleport or form.no_component %}active{% endif %}" id="consoleport" role="tabpanel" aria-labeled-by="consoleport_tab">
{% render_field form.consoleport %}
</div>
<div class="tab-pane {% if form.initial.consoleserverport %}active{% endif %}" id="consoleserverport" role="tabpanel" aria-labeled-by="consoleserverport_tab">
{% render_field form.consoleserverport %}
</div>
<div class="tab-pane {% if form.initial.frontport %}active{% endif %}" id="frontport" role="tabpanel" aria-labeled-by="frontport_tab">
{% render_field form.frontport %}
</div>
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
{% render_field form.interface %}
</div>
<div class="tab-pane {% if form.initial.poweroutlet %}active{% endif %}" id="poweroutlet" role="tabpanel" aria-labeled-by="poweroutlet_tab">
{% render_field form.poweroutlet %}
</div>
<div class="tab-pane {% if form.initial.powerport %}active{% endif %}" id="powerport" role="tabpanel" aria-labeled-by="powerport_tab">
{% render_field form.powerport %}
</div>
<div class="tab-pane {% if form.initial.rearport %}active{% endif %}" id="rearport" role="tabpanel" aria-labeled-by="rearport_tab">
{% render_field form.rearport %}
</div>
</div>
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Custom Fields" %}</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %}

View File

@ -54,6 +54,10 @@
{{ object.tenant|linkify|placeholder }} {{ object.tenant|linkify|placeholder }}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Facility" %}</th>
<td>{{ object.facility|placeholder }}</td>
</tr>
</table> </table>
</div> </div>
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}

View File

@ -1,90 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load form_helpers %}
{% load i18n %}
{% block form %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Rack" %}</h5>
</div>
{% render_field form.site %}
{% render_field form.location %}
{% render_field form.name %}
{% render_field form.status %}
{% render_field form.role %}
{% render_field form.description %}
{% render_field form.tags %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Inventory Control" %}</h5>
</div>
{% render_field form.facility_id %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Tenancy" %}</h5>
</div>
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Dimensions" %}</h5>
</div>
{% render_field form.type %}
{% render_field form.width %}
{% render_field form.starting_unit %}
{% render_field form.u_height %}
<div class="row mb-3">
<label class="col col-md-3 col-form-label text-lg-end">{% trans "Outer Dimensions" %}</label>
<div class="col col-md-3 mb-1">
{{ form.outer_width }}
<div class="form-text">{% trans "Width" %}</div>
</div>
<div class="col col-md-3 mb-1">
{{ form.outer_depth }}
<div class="form-text">{% trans "Depth" %}</div>
</div>
<div class="col col-md-3 mb-1">
{{ form.outer_unit }}
<div class="form-text">{% trans "Unit" %}</div>
</div>
</div>
<div class="row mb-3">
<label class="col col-md-3 col-form-label text-lg-end">{% trans "Weight" %}</label>
<div class="col col-md-3 mb-1">
{{ form.weight }}
<div class="form-text">{% trans "Weight" %}</div>
</div>
<div class="col col-md-3 mb-1">
{{ form.max_weight }}
<div class="form-text">{% trans "Maximum Weight" %}</div>
</div>
<div class="col col-md-3 mb-1">
{{ form.weight_unit }}
<div class="form-text">{% trans "Unit" %}</div>
</div>
</div>
{% render_field form.mounting_depth %}
{% render_field form.desc_units %}
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Custom Fields" %}</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
<div class="field-group my-5">
{% render_field form.comments %}
</div>
{% endblock %}

View File

@ -17,7 +17,9 @@
<th scope="row">Type</th> <th scope="row">Type</th>
<td> <td>
{{ object.get_type_display }} {{ object.get_type_display }}
{% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %} {% if object.related_object_type %}
({{ object.related_object_type.model|bettertitle }})
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -3,7 +3,8 @@
{% load log_levels %} {% load log_levels %}
{% load i18n %} {% load i18n %}
<p> <div class="htmx-container">
<p>
{% if job.started %} {% if job.started %}
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong> {% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
{% elif job.scheduled %} {% elif job.scheduled %}
@ -15,51 +16,9 @@
{% trans "Duration" %}: <strong>{{ job.duration }}</strong> {% trans "Duration" %}: <strong>{{ job.duration }}</strong>
{% endif %} {% endif %}
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span> <span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p> </p>
{% if job.completed %} {% if job.completed %}
{# Script log. Legacy reports will not have this. #}
{% if 'log' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Log" %}</h5>
{% if job.data.log %}
<table class="table table-hover panel-body">
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Message" %}</th>
</tr>
{% for log in job.data.log %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ log.time|placeholder }}</td>
<td>{% log_level log.status %}</td>
<td>{{ log.message|markdown }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
{# Script output. Legacy reports will not have this. #}
{% if 'output' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Output" %}</h5>
{% if job.data.output %}
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
{# Test method logs (for legacy Reports) #}
{% if tests %} {% if tests %}
{# Summary of test methods #} {# Summary of test methods #}
<div class="card"> <div class="card">
<h5 class="card-header">{% trans "Test Summary" %}</h5> <h5 class="card-header">{% trans "Test Summary" %}</h5>
@ -77,50 +36,30 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% endif %}
{# Detailed results for individual tests #} {% if table %}
<div class="card"> <div class="card">
<h5 class="card-header">{% trans "Test Details" %}</h5> <div class="table-responsive" id="object_list">
<table class="table table-hover report"> <h5 class="card-header">{% trans "Log" %}</h5>
<thead> {% include 'htmx/table.html' %}
<tr class="table-headings"> </div>
<th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Object" %}</th>
<th>{% trans "Message" %}</th>
</tr>
</thead>
<tbody>
{% for test, data in tests.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ test }}"></a>{{ test }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="badge text-bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="rendered-markdown">{{ message|markdown }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div> </div>
{% endif %} {% endif %}
{% elif job.started %}
{# Script output. Legacy reports will not have this. #}
{% if 'output' in job.data %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Output" %}</h5>
{% if job.data.output %}
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% endif %}
{% elif job.started %}
{% include 'extras/inc/result_pending.html' %} {% include 'extras/inc/result_pending.html' %}
{% endif %} {% endif %}
</div>

View File

@ -1,19 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load helpers %}
{% load form_helpers %}
{% block form %}
<div class="field-group mb-5">
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end required">
{{ object.parent|meta:"verbose_name"|bettertitle }}
</label>
<div class="col-sm-9">
<div class="form-control-plaintext">
{{ object.parent|linkify }}
</div>
</div>
</div>
{% render_form form %}
</div>
{% endblock form %}

View File

@ -11,7 +11,7 @@
{% endblock %} {% endblock %}
{% block page-header %} {% block page-header %}
<div class="container-xl mt-2"> <div class="container-fluid mt-2">
<nav class="breadcrumb-container" aria-label="breadcrumb"> <nav class="breadcrumb-container" aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">{% trans "Scripts" %}</a></li> <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">{% trans "Scripts" %}</a></li>
@ -32,28 +32,74 @@
{% block tabs %} {% block tabs %}
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Log" %}</a> <a href="#results" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Results" %}</a>
</li>
<li class="nav-item" role="presentation">
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">{% trans "Source" %}</a>
</li> </li>
</ul> </ul>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div role="tabpanel" class="tab-pane active" id="log"> {# Object list tab #}
<div class="row"> <div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
{# Object table controls #}
<div class="row mb-3">
<div class="col-auto ms-auto d-print-none">
{% if request.user.is_authenticated %}
<div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"
class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button>
</div>
{% endif %}
</div>
</div>
<form method="post" class="form form-horizontal">
{% csrf_token %}
{# "Select all" form #}
{% if table.paginator.num_pages > 1 %}
<div id="select-all-box" class="d-none card d-print-none">
<div class="form col-md-12">
<div class="card-body">
<div class="form-check">
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
<label for="select-all" class="form-check-label">
{% blocktrans trimmed with count=table.rows|length object_type_plural=table.data.verbose_name_plural %}
Select <strong>all {{ count }} {{ object_type_plural }}</strong> matching query
{% endblocktrans %}
</label>
</div>
</div>
</div>
</div>
{% endif %}
<div class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{# Objects table #}
<div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}> <div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
{% include 'extras/htmx/script_result.html' %} {% include 'extras/htmx/script_result.html' %}
</div> </div>
{# /Objects table #}
</div> </div>
</form>
</div> </div>
<div role="tabpanel" class="tab-pane" id="source"> {# /Object list tab #}
<p><code>{{ script.filename }}</code></p>
<pre class="block">{{ script.source }}</pre> {# Filters tab #}
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div> </div>
{% endif %}
{# /Filters tab #}
{% endblock content %} {% endblock content %}
{% block modals %} {% block modals %}
{% include 'inc/htmx_modal.html' %} {% table_config_form table table_name="ObjectTable" %}
{% endblock modals %} {% endblock modals %}

View File

@ -5,7 +5,7 @@
{{ block.super }} {{ block.super }}
{% block page-header %} {% block page-header %}
<div class="container-xl mt-2 d-print-none"> <div class="container-fluid mt-2 d-print-none">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
{# Title #} {# Title #}
@ -29,7 +29,7 @@
{# Tabs #} {# Tabs #}
<div class="page-tabs mt-3"> <div class="page-tabs mt-3">
<div class="container-xl"> <div class="container-fluid">
{% block tabs %}{% endblock %} {% block tabs %}{% endblock %}
</div> </div>
</div> </div>

View File

@ -71,7 +71,7 @@ Context:
{# Selected objects list #} {# Selected objects list #}
<div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab"> <div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
<div class="card"> <div class="card">
<div class="card-body table-responsive"> <div class="table-responsive">
{% render_table table 'inc/table.html' %} {% render_table table 'inc/table.html' %}
</div> </div>
</div> </div>

View File

@ -49,23 +49,8 @@ Context:
{% if form.fieldsets %} {% if form.fieldsets %}
{# Render grouped fields according to declared fieldsets #} {# Render grouped fields according to declared fieldsets #}
{% for group, fields in form.fieldsets %} {% for fieldset in form.fieldsets %}
<div class="field-group mb-5"> {% render_fieldset form fieldset %}
<div class="row">
<h5 class="col-9 offset-3">
{% if group %}{{ group }}{% else %}{{ model|meta:"verbose_name"|bettertitle }}{% endif %}
</h5>
</div>
{% for name in fields %}
{% with field=form|getfield:name %}
{% if field.name in form.nullable_fields %}
{% render_field field bulk_nullable=True %}
{% else %}
{% render_field field %}
{% endif %}
{% endwith %}
{% endfor %}
</div>
{% endfor %} {% endfor %}
{# Render tag add/remove fields #} {# Render tag add/remove fields #}

View File

@ -33,10 +33,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container-xl px-0"> <div class="container-fluid px-0">
<div class="card">
<div class="table-responsive"> <div class="table-responsive">
{% render_table table 'inc/table.html' %} {% render_table table 'inc/table.html' %}
</div> </div>
</div>
<form action="." method="post" class="form"> <form action="." method="post" class="form">
{% csrf_token %} {% csrf_token %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}

View File

@ -19,7 +19,7 @@ Context:
{% endcomment %} {% endcomment %}
{% block page-header %} {% block page-header %}
<div class="container-xl"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mt-2"> <div class="d-flex justify-content-between align-items-center mt-2">
{# Breadcrumbs #} {# Breadcrumbs #}

View File

@ -9,21 +9,8 @@
{% endfor %} {% endfor %}
{# Render grouped fields according to Form #} {# Render grouped fields according to Form #}
{% for group, fields in form.fieldsets %} {% for fieldset in form.fieldsets %}
<div class="field-group mb-5"> {% render_fieldset form fieldset %}
{% if group %}
<div class="row">
<h5 class="col-9 offset-3">{{ group }}</h5>
</div>
{% endif %}
{% for name in fields %}
{% with field=form|getfield:name %}
{% if field and not field.field.widget.is_hidden %}
{% render_field field %}
{% endif %}
{% endwith %}
{% endfor %}
</div>
{% endfor %} {% endfor %}
{% if form.custom_fields %} {% if form.custom_fields %}

View File

@ -9,18 +9,9 @@
{{ field }} {{ field }}
{% endfor %} {% endfor %}
{# List filters by group #} {# List filters by group #}
{% for heading, fields in filter_form.fieldsets %} {% for fieldset in filter_form.fieldsets %}
<div class="col col-12"> <div class="col col-12">
{% if heading %} {% render_fieldset filter_form fieldset %}
<div class="hr-text">
<span>{{ heading }}</span>
</div>
{% endif %}
{% for name in fields %}
{% with field=filter_form|get_item:name %}
{% render_field field %}
{% endwith %}
{% endfor %}
</div> </div>
{% empty %} {% empty %}
{# List all non-customfield filters as declared in the form class #} {# List all non-customfield filters as declared in the form class #}

View File

@ -1,19 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load form_helpers %}
{% load i18n %}
{% block form %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "FHRP Group Assignment" %}</h5>
</div>
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">{% trans "Interface" %}</label>
<div class="col">
<input class="form-control" value="{{ form.instance.interface }}" disabled />
</div>
</div>
{% render_field form.group %}
{% render_field form.priority %}
</div>
{% endblock %}

View File

@ -1,93 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load static %}
{% load form_helpers %}
{% load helpers %}
{% load i18n %}
{% block tabs %}
{% include 'ipam/inc/ipaddress_edit_header.html' with active_tab='add' %}
{% endblock tabs %}
{% block form %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "IP Address" %}</h5>
</div>
{% render_field form.address %}
{% render_field form.status %}
{% render_field form.role %}
{% render_field form.vrf %}
{% render_field form.dns_name %}
{% render_field form.description %}
{% render_field form.tags %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Tenancy" %}</h5>
</div>
{% render_field form.tenant_group %}
{% render_field form.tenant %}
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Interface Assignment" %}</h5>
</div>
<div class="row">
<div class="col-9 offset-3">
<ul class="nav nav-pills mb-1" role="tablist">
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="device_tab" data-bs-toggle="tab" aria-controls="device" data-bs-target="#device" class="nav-link {% if not form.initial.vminterface and not form.initial.fhrpgroup %}active{% endif %}">
{% trans "Device" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="vm_tab" data-bs-toggle="tab" aria-controls="vm" data-bs-target="#vm" class="nav-link {% if form.initial.vminterface %}active{% endif %}">
{% trans "Virtual Machine" %}
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="fhrpgroup_tab" data-bs-toggle="tab" aria-controls="fhrpgroup" data-bs-target="#fhrpgroup" class="nav-link {% if form.initial.fhrpgroup %}active{% endif %}">
{% trans "FHRP Group" %}
</button>
</li>
</ul>
</div>
</div>
<div class="tab-content p-0 border-0">
<div class="tab-pane {% if not form.initial.vminterface and not form.initial.fhrpgroup %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
{% render_field form.interface %}
</div>
<div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vm" role="tabpanel" aria-labeled-by="vm_tab">
{% render_field form.vminterface %}
</div>
<div class="tab-pane {% if form.initial.fhrpgroup %}active{% endif %}" id="fhrpgroup" role="tabpanel" aria-labeled-by="fhrpgroup_tab">
{% render_field form.fhrpgroup %}
</div>
{% render_field form.primary_for_parent %}
</div>
</div>
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "NAT IP (Inside" %})</h5>
</div>
<div class="row">
{% render_field form.nat_inside %}
</div>
</div>
<div class="field-group my-5">
{% render_field form.comments %}
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row">
<h5 class="col-9 offset-3">{% trans "Custom Fields" %}</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %}

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