Merge branch 'feature' into 9856-strawberry-2

This commit is contained in:
Arthur 2024-03-22 07:29:45 -07:00
commit be522467ab
167 changed files with 2282 additions and 2399 deletions

View File

@ -4,7 +4,7 @@ NetBox validates every object prior to it being written to the database to ensur
## Custom Validation Rules ## Custom Validation Rules
Custom validation rules are expressed as a mapping of model attributes to a set of rules to which that attribute must conform. For example: Custom validation rules are expressed as a mapping of object attributes to a set of rules to which that attribute must conform. For example:
```json ```json
{ {
@ -17,6 +17,8 @@ Custom validation rules are expressed as a mapping of model attributes to a set
This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation. This defines a custom validator which checks that the length of the `name` attribute for an object is at least five characters long, and no longer than 30 characters. This validation is executed _after_ NetBox has performed its own internal validation.
### Validation Types
The `CustomValidator` class supports several validation types: The `CustomValidator` class supports several validation types:
* `min`: Minimum value * `min`: Minimum value
@ -34,16 +36,33 @@ The `min` and `max` types should be defined for numeric values, whereas `min_len
!!! warning !!! warning
Bear in mind that these validators merely supplement NetBox's own validation: They will not override it. For example, if a certain model field is required by NetBox, setting a validator for it with `{'prohibited': True}` will not work. Bear in mind that these validators merely supplement NetBox's own validation: They will not override it. For example, if a certain model field is required by NetBox, setting a validator for it with `{'prohibited': True}` will not work.
### Validating Request Parameters
!!! info "This feature was introduced in NetBox v4.0."
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
```json
{
"request.user.username": {
"eq": "admin"
}
}
```
!!! tip
Custom validation should generally not be used to enforce permissions. NetBox provides a robust [object-based permissions](../administration/permissions.md) mechanism which should be used for this purpose.
### Custom Validation Logic ### Custom Validation Logic
There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected. There may be instances where the provided validation types are insufficient. NetBox provides a `CustomValidator` class which can be extended to enforce arbitrary validation logic by overriding its `validate()` method, and calling `fail()` when an unsatisfactory condition is detected. The `validate()` method should accept an instance (the object being saved) as well as the current request effecting the change.
```python ```python
from extras.validators import CustomValidator from extras.validators import CustomValidator
class MyValidator(CustomValidator): class MyValidator(CustomValidator):
def validate(self, instance): def validate(self, instance, request):
if instance.status == 'active' and not instance.description: if instance.status == 'active' and not instance.description:
self.fail("Active sites must have a description set!", field='status') self.fail("Active sites must have a description set!", field='status')
``` ```

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

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

@ -15,16 +15,18 @@ 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')
``` ```
@ -115,10 +120,10 @@ 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

@ -49,8 +49,8 @@ menu_items = (item1, item2, item3)
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below. Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
```python title="navigation.py" ```python title="navigation.py"
from netbox.choices import ButtonColorChoices
from netbox.plugins import PluginMenuButton, PluginMenuItem from netbox.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
item1 = PluginMenuItem( item1 = PluginMenuItem(
link='plugins:myplugin:myview', link='plugins:myplugin:myview',

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

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

@ -6,7 +6,7 @@ from dcim.views import PathTraceView
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.utils import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -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

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

@ -1,3 +1,4 @@
import hashlib
import logging import logging
import os import os
import yaml import yaml
@ -18,7 +19,6 @@ from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel from netbox.models import PrimaryModel
from netbox.models.features import JobsMixin from netbox.models.features import JobsMixin
from netbox.registry import registry from netbox.registry import registry
from utilities.files import sha256_hash
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from ..choices import * from ..choices import *
from ..exceptions import SyncError from ..exceptions import SyncError
@ -357,7 +357,8 @@ class DataFile(models.Model):
has changed. has changed.
""" """
file_path = os.path.join(source_root, self.path) file_path = os.path.join(source_root, self.path)
file_hash = sha256_hash(file_path).hexdigest() with open(file_path, 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
# Update instance file attributes & data # Update instance file attributes & data
if is_modified := file_hash != self.hash: if is_modified := file_hash != self.hash:

View File

@ -25,7 +25,7 @@ from netbox.views import generic
from netbox.views.generic.base import BaseObjectView from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.utils import count_related from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *

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

@ -10,12 +10,12 @@ from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate from extras.models import ConfigTemplate
from ipam.filtersets import PrimaryIPFilterSet from ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, IPAddress, VRF from ipam.models import ASN, IPAddress, VRF
from netbox.choices import ColorChoices
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
) )
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import * from tenancy.models import *
from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
@ -270,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)
) )

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

@ -15,9 +15,9 @@ from dcim.constants import *
from dcim.fields import PathField from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node from dcim.utils import decompile_path_node, object_to_path_node
from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models import ChangeLoggedModel, PrimaryModel
from utilities.conversion import to_meters
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
from wireless.models import WirelessLink from wireless.models import WirelessLink
from .device_components import FrontPort, RearPort, PathEndpoint from .device_components import FrontPort, RearPort, PathEndpoint

View File

@ -12,8 +12,8 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import MACAddressField, WWNField from dcim.fields import MACAddressField, WWNField
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, NetBoxModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface

View File

@ -18,10 +18,10 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from extras.models import ConfigContextModel, CustomField from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet from extras.querysets import ConfigContextModelQuerySet
from netbox.choices import ColorChoices
from netbox.config import ConfigItem from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, PrimaryModel from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.choices import ColorChoices
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
from utilities.tracking import TrackingModelMixin from utilities.tracking import TrackingModelMixin
from .device_components import * from .device_components import *

View File

@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from utilities.utils import to_grams from utilities.conversion import to_grams
__all__ = ( __all__ = (
'RenderConfigMixin', 'RenderConfigMixin',

View File

@ -14,11 +14,12 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.svg import RackElevationSVG from dcim.svg import RackElevationSVG
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, PrimaryModel from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.choices import ColorChoices from utilities.conversion import to_grams
from utilities.data import array_to_string, drange
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange, to_grams
from .device_components import PowerPort from .device_components import PowerPort
from .devices import Device, Module from .devices import Device, Module
from .mixins import WeightMixin from .mixins import WeightMixin

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

@ -6,7 +6,7 @@ from svgwrite.text import Text
from django.conf import settings from django.conf import settings
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from utilities.utils import foreground_color from utilities.html import foreground_color
__all__ = ( __all__ = (

View File

@ -14,7 +14,8 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from netbox.config import get_config from netbox.config import get_config
from utilities.utils import foreground_color, array_to_ranges from utilities.data import array_to_ranges
from utilities.html import foreground_color
from dcim.constants import RACK_ELEVATION_BORDER_WIDTH from dcim.constants import RACK_ELEVATION_BORDER_WIDTH

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

@ -6,13 +6,12 @@ from dcim.choices import *
from dcim.filtersets import * from dcim.filtersets import *
from dcim.models import * from dcim.models import *
from ipam.models import ASN, IPAddress, RIR, VRF from ipam.models import ASN, IPAddress, RIR, VRF
from netbox.choices import ColorChoices
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.choices import ColorChoices
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
from virtualization.models import Cluster, ClusterType from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
User = get_user_model() User = get_user_model()
@ -359,9 +358,9 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
location.save() location.save()
locations = ( locations = (
Location(name='Location 1A', slug='location-1a', 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 2A', slug='location-2a', 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 3A', slug='location-3a', 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()
@ -390,6 +389,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)

View File

@ -7,7 +7,7 @@ from dcim.choices import *
from dcim.models import * from dcim.models import *
from extras.models import CustomField from extras.models import CustomField
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import drange from utilities.data import drange
class LocationTestCase(TestCase): class LocationTestCase(TestCase):

View File

@ -11,12 +11,11 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN from wireless.models import WirelessLAN
User = get_user_model() User = get_user_model()
@ -213,6 +212,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

@ -25,8 +25,8 @@ from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -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')
@ -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

@ -20,7 +20,7 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request from utilities.request import copy_safe_request
from . import serializers from . import serializers
from .mixins import ConfigTemplateRenderMixin from .mixins import ConfigTemplateRenderMixin

View File

@ -2,7 +2,8 @@ import logging
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utilities.choices import ButtonColorChoices, ChoiceSet from netbox.choices import ButtonColorChoices
from utilities.choices import ChoiceSet
# #

View File

@ -14,10 +14,12 @@ from django.utils.translation import gettext as _
from core.models import ObjectType from core.models import ObjectType
from extras.choices import BookmarkOrderingChoices from extras.choices import BookmarkOrderingChoices
from utilities.choices import ButtonColorChoices from netbox.choices import ButtonColorChoices
from utilities.object_types import object_type_identifier, object_type_name
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.querydict import dict_to_querydict
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname from utilities.views import get_viewname
from .utils import register_widget from .utils import register_widget
__all__ = ( __all__ = (
@ -33,15 +35,15 @@ __all__ = (
def get_object_type_choices(): def get_object_type_choices():
return [ return [
(content_type_identifier(ct), content_type_name(ct)) (object_type_identifier(ot), object_type_name(ot))
for ct in ObjectType.objects.public().order_by('app_label', 'model') for ot in ObjectType.objects.public().order_by('app_label', 'model')
] ]
def get_bookmarks_object_type_choices(): def get_bookmarks_object_type_choices():
return [ return [
(content_type_identifier(ct), content_type_name(ct)) (object_type_identifier(ot), object_type_name(ot))
for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model') for ot in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
] ]

View File

@ -1,9 +1,6 @@
import logging
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone from django.utils import timezone
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -15,9 +12,9 @@ from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.registry import registry from netbox.registry import registry
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.rqworker import get_rq_retry from utilities.rqworker import get_rq_retry
from utilities.utils import serialize_object from utilities.serialization import serialize_object
from .choices import * from .choices import *
from .models import EventRule, ScriptModule from .models import EventRule
logger = logging.getLogger('netbox.events_processor') logger = logging.getLogger('netbox.events_processor')

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,11 +37,11 @@ __all__ = (
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), FieldSet('q', 'filter_id'),
(_('Attributes'), ( FieldSet(
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'ui_editable', 'is_cloneable', 'ui_editable', 'is_cloneable', name=_('Attributes')
)), ),
) )
related_object_type_id = ContentTypeMultipleChoiceField( related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
@ -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
@ -54,12 +55,15 @@ class CustomFieldForm(forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Custom Field'), ( FieldSet(
'object_types', 'name', 'label', 'group_name', 'type', 'related_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

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now from utilities.datetime import local_now
__all__ = ( __all__ = (
'ReportForm', 'ReportForm',

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now from utilities.datetime import local_now
__all__ = ( __all__ = (
'ScriptForm', 'ScriptForm',

View File

@ -14,7 +14,7 @@ from extras.context_managers import event_tracking
from extras.scripts import get_module_and_script from extras.scripts import get_module_and_script
from extras.signals import clear_events from extras.signals import clear_events
from utilities.exceptions import AbortTransaction from utilities.exceptions import AbortTransaction
from utilities.utils import NetBoxFakeRequest from utilities.request import NetBoxFakeRequest
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -9,11 +9,11 @@ from jinja2.sandbox import SandboxedEnvironment
from extras.querysets import ConfigContextQuerySet from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from utilities.jinja2 import ConfigTemplateLoader from netbox.registry import registry
from utilities.utils import deepmerge from utilities.data import deepmerge
from utilities.jinja2 import DataFileLoader
__all__ = ( __all__ = (
'ConfigContext', 'ConfigContext',
@ -290,7 +290,7 @@ class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, Ta
""" """
# Initialize the template loader & cache the base template code (if applicable) # Initialize the template loader & cache the base template code (if applicable)
if self.data_file: if self.data_file:
loader = ConfigTemplateLoader(data_source=self.data_source) loader = DataFileLoader(data_source=self.data_source)
loader.cache_templates({ loader.cache_templates({
self.data_file.path: self.template_code self.data_file.path: self.template_code
}) })

View File

@ -22,8 +22,10 @@ from netbox.models import ChangeLoggedModel
from netbox.models.features import ( from netbox.models.features import (
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
) )
from utilities.html import clean_html
from utilities.querydict import dict_to_querydict
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import clean_html, dict_to_querydict, render_jinja2 from utilities.jinja2 import render_jinja2
__all__ = ( __all__ = (
'Bookmark', 'Bookmark',

View File

@ -4,9 +4,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.search.utils import get_indexer from netbox.search.utils import get_indexer
from netbox.registry import registry
from utilities.fields import RestrictedGenericForeignKey from utilities.fields import RestrictedGenericForeignKey
from utilities.utils import content_type_identifier
from ..fields import CachedValueField from ..fields import CachedValueField
__all__ = ( __all__ = (

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from extras.choices import ChangeActionChoices from extras.choices import ChangeActionChoices
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import * from netbox.models.features import *
from utilities.utils import deserialize_object from utilities.serialization import deserialize_object
__all__ = ( __all__ = (
'Branch', 'Branch',

View File

@ -5,9 +5,9 @@ from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
from netbox.choices import ColorChoices
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.models.features import CloningMixin, ExportTemplatesMixin
from utilities.choices import ColorChoices
from utilities.fields import ColorField from utilities.fields import ColorField
__all__ = ( __all__ = (

View File

@ -1,7 +1,8 @@
import importlib
import logging import logging
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db.models.fields.reverse_related import ManyToManyRel from django.db.models.fields.reverse_related import ManyToManyRel
from django.db.models.signals import m2m_changed, post_save, pre_delete from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
@ -13,7 +14,6 @@ from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from extras.events import process_event_rules from extras.events import process_event_rules
from extras.models import EventRule from extras.models import EventRule
from extras.validators import run_validators
from netbox.config import get_config from netbox.config import get_config
from netbox.context import current_request, events_queue from netbox.context import current_request, events_queue
from netbox.models.features import ChangeLoggingMixin from netbox.models.features import ChangeLoggingMixin
@ -22,6 +22,30 @@ from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .events import enqueue_object, get_snapshots, serialize_for_event from .events import enqueue_object, get_snapshots, serialize_for_event
from .models import CustomField, ObjectChange, TaggedItem from .models import CustomField, ObjectChange, TaggedItem
from .validators import CustomValidator
def run_validators(instance, validators):
"""
Run the provided iterable of validators for the instance.
"""
request = current_request.get()
for validator in validators:
# Loading a validator class by dotted path
if type(validator) is str:
module, cls = validator.rsplit('.', 1)
validator = getattr(importlib.import_module(module), cls)()
# Constructing a new instance on the fly from a ruleset
elif type(validator) is dict:
validator = CustomValidator(validator)
elif not issubclass(validator.__class__, CustomValidator):
raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
validator(instance, request)
# #
# Change logging/webhooks # Change logging/webhooks

View File

@ -5,7 +5,7 @@ from circuits.api.serializers import ProviderSerializer
from circuits.forms import ProviderForm from circuits.forms import ProviderForm
from circuits.models import Provider from circuits.models import Provider
from ipam.models import ASN, RIR from ipam.models import ASN, RIR
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data

View File

@ -12,7 +12,7 @@ from dcim.models import Manufacturer, Rack, Site
from extras.choices import * from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet from extras.models import CustomField, CustomFieldChoiceSet
from ipam.models import VLAN from ipam.models import VLAN
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import APITestCase, TestCase from utilities.testing import APITestCase, TestCase
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine

View File

@ -3,11 +3,13 @@ from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from ipam.models import ASN, RIR
from dcim.choices import SiteStatusChoices from dcim.choices import SiteStatusChoices
from dcim.models import Site from dcim.models import Site
from extras.validators import CustomValidator from extras.validators import CustomValidator
from ipam.models import ASN, RIR
from users.models import User
from utilities.exceptions import AbortRequest from utilities.exceptions import AbortRequest
from utilities.request import NetBoxFakeRequest
class MyValidator(CustomValidator): class MyValidator(CustomValidator):
@ -79,6 +81,13 @@ prohibited_validator = CustomValidator({
} }
}) })
request_validator = CustomValidator({
'request.user.username': {
'eq': 'Bob'
}
})
custom_validator = MyValidator() custom_validator = MyValidator()
@ -154,6 +163,28 @@ class CustomValidatorTest(TestCase):
def test_custom_valid(self): def test_custom_valid(self):
Site(name='foo', slug='foo').clean() Site(name='foo', slug='foo').clean()
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [request_validator]})
def test_request_validation(self):
alice = User.objects.create(username='Alice')
bob = User.objects.create(username='Bob')
request = NetBoxFakeRequest({
'META': {},
'POST': {},
'GET': {},
'FILES': {},
'user': alice,
'path': '',
})
site = Site(name='abc', slug='abc')
# Attempt to create the Site as Alice
with self.assertRaises(ValidationError):
request_validator(site, request)
# Creating the Site as Bob should succeed
request.user = bob
request_validator(site, request)
class CustomValidatorConfigTest(TestCase): class CustomValidatorConfigTest(TestCase):

View File

@ -1,4 +1,5 @@
import importlib import inspect
import operator
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -74,6 +75,8 @@ class CustomValidator:
:param validation_rules: A dictionary mapping object attributes to validation rules :param validation_rules: A dictionary mapping object attributes to validation rules
""" """
REQUEST_TOKEN = 'request'
VALIDATORS = { VALIDATORS = {
'eq': IsEqualValidator, 'eq': IsEqualValidator,
'neq': IsNotEqualValidator, 'neq': IsNotEqualValidator,
@ -88,25 +91,56 @@ class CustomValidator:
def __init__(self, validation_rules=None): def __init__(self, validation_rules=None):
self.validation_rules = validation_rules or {} self.validation_rules = validation_rules or {}
assert type(self.validation_rules) is dict, "Validation rules must be passed as a dictionary" if type(self.validation_rules) is not dict:
raise ValueError(_("Validation rules must be passed as a dictionary"))
def __call__(self, instance): def __call__(self, instance, request=None):
# Validate instance attributes per validation rules """
for attr_name, rules in self.validation_rules.items(): Validate the instance and (optional) request against the validation rule(s).
attr = self._getattr(instance, attr_name) """
for attr_path, rules in self.validation_rules.items():
# The rule applies to the current request
if attr_path.split('.')[0] == self.REQUEST_TOKEN:
# Skip if no request has been provided (we can't validate)
if request is None:
continue
attr = self._get_request_attr(request, attr_path)
# The rule applies to the instance
else:
attr = self._get_instance_attr(instance, attr_path)
# Validate the attribute's value against each of the rules defined for it
for descriptor, value in rules.items(): for descriptor, value in rules.items():
validator = self.get_validator(descriptor, value) validator = self.get_validator(descriptor, value)
try: try:
validator(attr) validator(attr)
except ValidationError as exc: except ValidationError as exc:
# Re-package the raised ValidationError to associate it with the specific attr raise ValidationError(
raise ValidationError({attr_name: exc}) _("Custom validation failed for {attribute}: {exception}").format(
attribute=attr_path, exception=exc
)
)
# Execute custom validation logic (if any) # Execute custom validation logic (if any)
self.validate(instance) # TODO: Remove in v4.1
# Inspect the validate() method, which may have been overridden, to determine
# whether we should pass the request (maintains backward compatibility for pre-v4.0)
if 'request' in inspect.signature(self.validate).parameters:
self.validate(instance, request)
else:
self.validate(instance)
@staticmethod @staticmethod
def _getattr(instance, name): def _get_request_attr(request, name):
name = name.split('.', maxsplit=1)[1] # Remove token
try:
return operator.attrgetter(name)(request)
except AttributeError:
raise ValidationError(_('Invalid attribute "{name}" for request').format(name=name))
@staticmethod
def _get_instance_attr(instance, name):
# Attempt to resolve many-to-many fields to their stored values # Attempt to resolve many-to-many fields to their stored values
m2m_fields = [f.name for f in instance._meta.local_many_to_many] m2m_fields = [f.name for f in instance._meta.local_many_to_many]
if name in m2m_fields: if name in m2m_fields:
@ -137,7 +171,7 @@ class CustomValidator:
validator_cls = self.VALIDATORS.get(descriptor) validator_cls = self.VALIDATORS.get(descriptor)
return validator_cls(value) return validator_cls(value)
def validate(self, instance): def validate(self, instance, request):
""" """
Custom validation method, to be overridden by the user. Validation failures should Custom validation method, to be overridden by the user. Validation failures should
raise a ValidationError exception. raise a ValidationError exception.
@ -151,21 +185,3 @@ class CustomValidator:
if field is not None: if field is not None:
raise ValidationError({field: message}) raise ValidationError({field: message})
raise ValidationError(message) raise ValidationError(message)
def run_validators(instance, validators):
"""
Run the provided iterable of validators for the instance.
"""
for validator in validators:
# Loading a validator class by dotted path
if type(validator) is str:
module, cls = validator.rsplit('.', 1)
validator = getattr(importlib.import_module(module), cls)()
# Constructing a new instance on the fly from a ruleset
elif type(validator) is dict:
validator = CustomValidator(validator)
validator(instance)

View File

@ -18,12 +18,15 @@ 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 netbox.views.generic.mixins import TableMixin
from utilities.data import shallow_compare_dict
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.query import count_related
from utilities.querydict import normalize_querydict
from utilities.request import copy_safe_request
from utilities.rqworker import get_workers_for_queue from utilities.rqworker import get_workers_for_queue
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
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
@ -759,7 +762,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:

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(
@ -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 = [
@ -443,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:
@ -502,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')
@ -587,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:
@ -662,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:
@ -704,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 = [
@ -718,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

@ -8,8 +8,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
from netbox.models import PrimaryModel from netbox.models import PrimaryModel
from utilities.utils import array_to_string from utilities.data import array_to_string
__all__ = ( __all__ = (
'Service', 'Service',

View File

@ -3,8 +3,8 @@ from django.db.models import Count, F, OuterRef, Q, Subquery, Value
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.functions import Round from django.db.models.functions import Round
from utilities.query import count_related
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import count_related
__all__ = ( __all__ = (
'ASNRangeQuerySet', 'ASNRangeQuerySet',

View File

@ -9,8 +9,8 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site from dcim.models import Interface, Site
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering from utilities.tables import get_table_ordering
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface from virtualization.models import VMInterface
@ -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

@ -2,9 +2,10 @@ from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from core.models import ObjectType
from netbox.api.fields import ContentTypeField from netbox.api.fields import ContentTypeField
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import content_type_identifier from utilities.object_types import object_type_identifier
__all__ = ( __all__ = (
'GenericObjectSerializer', 'GenericObjectSerializer',
@ -27,9 +28,9 @@ class GenericObjectSerializer(serializers.Serializer):
return model.objects.get(pk=data['object_id']) return model.objects.get(pk=data['object_id'])
def to_representation(self, instance): def to_representation(self, instance):
ct = ContentType.objects.get_for_model(instance) object_type = ObjectType.objects.get_for_model(instance)
data = { data = {
'object_type': content_type_identifier(ct), 'object_type': object_type_identifier(object_type),
'object_id': instance.pk, 'object_id': instance.pk,
} }
if 'request' in self.context: if 'request' in self.context:

View File

@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
from users.constants import CONSTRAINT_TOKEN_USER from users.constants import CONSTRAINT_TOKEN_USER
from users.models import Group, ObjectPermission from users.models import Group, ObjectPermission
from utilities.permissions import ( from utilities.permissions import (
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct, permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_type,
) )
UserModel = get_user_model() UserModel = get_user_model()
@ -284,11 +284,9 @@ class RemoteUserBackend(_RemoteUserBackend):
permissions_list = [] permissions_list = []
for permission_name, constraints in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS.items(): for permission_name, constraints in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS.items():
try: try:
object_type, action = resolve_permission_ct( object_type, action = resolve_permission_type(permission_name)
permission_name) # TODO: Merge multiple actions into a single ObjectPermission per object type
# TODO: Merge multiple actions into a single ObjectPermission per content type obj_perm = ObjectPermission(actions=[action], constraints=constraints)
obj_perm = ObjectPermission(
actions=[action], constraints=constraints)
obj_perm.save() obj_perm.save()
obj_perm.users.add(user) obj_perm.users.add(user)
obj_perm.object_types.add(object_type) obj_perm.object_types.add(object_type)
@ -303,7 +301,9 @@ class RemoteUserBackend(_RemoteUserBackend):
f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}") f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}")
else: else:
logger.debug( logger.debug(
f"Skipped initial assignment of permissions and groups to remotely-authenticated user {user} as Group sync is enabled") f"Skipped initial assignment of permissions and groups to remotely-authenticated user {user} as "
f"Group sync is enabled"
)
return user return user

162
netbox/netbox/choices.py Normal file
View File

@ -0,0 +1,162 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.constants import CSV_DELIMITERS
__all__ = (
'ButtonColorChoices',
'ColorChoices',
'CSVDelimiterChoices',
'ImportFormatChoices',
'ImportMethodChoices',
)
#
# Generic color choices
#
class ColorChoices(ChoiceSet):
COLOR_DARK_RED = 'aa1409'
COLOR_RED = 'f44336'
COLOR_PINK = 'e91e63'
COLOR_ROSE = 'ffe4e1'
COLOR_FUCHSIA = 'ff66ff'
COLOR_PURPLE = '9c27b0'
COLOR_DARK_PURPLE = '673ab7'
COLOR_INDIGO = '3f51b5'
COLOR_BLUE = '2196f3'
COLOR_LIGHT_BLUE = '03a9f4'
COLOR_CYAN = '00bcd4'
COLOR_TEAL = '009688'
COLOR_AQUA = '00ffff'
COLOR_DARK_GREEN = '2f6a31'
COLOR_GREEN = '4caf50'
COLOR_LIGHT_GREEN = '8bc34a'
COLOR_LIME = 'cddc39'
COLOR_YELLOW = 'ffeb3b'
COLOR_AMBER = 'ffc107'
COLOR_ORANGE = 'ff9800'
COLOR_DARK_ORANGE = 'ff5722'
COLOR_BROWN = '795548'
COLOR_LIGHT_GREY = 'c0c0c0'
COLOR_GREY = '9e9e9e'
COLOR_DARK_GREY = '607d8b'
COLOR_BLACK = '111111'
COLOR_WHITE = 'ffffff'
CHOICES = (
(COLOR_DARK_RED, _('Dark Red')),
(COLOR_RED, _('Red')),
(COLOR_PINK, _('Pink')),
(COLOR_ROSE, _('Rose')),
(COLOR_FUCHSIA, _('Fuchsia')),
(COLOR_PURPLE, _('Purple')),
(COLOR_DARK_PURPLE, _('Dark Purple')),
(COLOR_INDIGO, _('Indigo')),
(COLOR_BLUE, _('Blue')),
(COLOR_LIGHT_BLUE, _('Light Blue')),
(COLOR_CYAN, _('Cyan')),
(COLOR_TEAL, _('Teal')),
(COLOR_AQUA, _('Aqua')),
(COLOR_DARK_GREEN, _('Dark Green')),
(COLOR_GREEN, _('Green')),
(COLOR_LIGHT_GREEN, _('Light Green')),
(COLOR_LIME, _('Lime')),
(COLOR_YELLOW, _('Yellow')),
(COLOR_AMBER, _('Amber')),
(COLOR_ORANGE, _('Orange')),
(COLOR_DARK_ORANGE, _('Dark Orange')),
(COLOR_BROWN, _('Brown')),
(COLOR_LIGHT_GREY, _('Light Grey')),
(COLOR_GREY, _('Grey')),
(COLOR_DARK_GREY, _('Dark Grey')),
(COLOR_BLACK, _('Black')),
(COLOR_WHITE, _('White')),
)
#
# Button color choices
#
class ButtonColorChoices(ChoiceSet):
"""
Map standard button color choices to Bootstrap 3 button classes
"""
DEFAULT = 'outline-dark'
BLUE = 'blue'
INDIGO = 'indigo'
PURPLE = 'purple'
PINK = 'pink'
RED = 'red'
ORANGE = 'orange'
YELLOW = 'yellow'
GREEN = 'green'
TEAL = 'teal'
CYAN = 'cyan'
GRAY = 'gray'
GREY = 'gray' # Backward compatability for <3.2
BLACK = 'black'
WHITE = 'white'
CHOICES = (
(DEFAULT, _('Default')),
(BLUE, _('Blue')),
(INDIGO, _('Indigo')),
(PURPLE, _('Purple')),
(PINK, _('Pink')),
(RED, _('Red')),
(ORANGE, _('Orange')),
(YELLOW, _('Yellow')),
(GREEN, _('Green')),
(TEAL, _('Teal')),
(CYAN, _('Cyan')),
(GRAY, _('Gray')),
(BLACK, _('Black')),
(WHITE, _('White')),
)
#
# Import Choices
#
class ImportMethodChoices(ChoiceSet):
DIRECT = 'direct'
UPLOAD = 'upload'
DATA_FILE = 'datafile'
CHOICES = [
(DIRECT, _('Direct')),
(UPLOAD, _('Upload')),
(DATA_FILE, _('Data file')),
]
class ImportFormatChoices(ChoiceSet):
AUTO = 'auto'
CSV = 'csv'
JSON = 'json'
YAML = 'yaml'
CHOICES = [
(AUTO, _('Auto-detect')),
(CSV, 'CSV'),
(JSON, 'JSON'),
(YAML, 'YAML'),
]
class CSVDelimiterChoices(ChoiceSet):
AUTO = 'auto'
COMMA = CSV_DELIMITERS['comma']
SEMICOLON = CSV_DELIMITERS['semicolon']
TAB = CSV_DELIMITERS['tab']
CHOICES = [
(AUTO, _('Auto-detect')),
(COMMA, _('Comma')),
(SEMICOLON, _('Semicolon')),
(TAB, _('Tab')),
]

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

@ -13,7 +13,8 @@ from django.http import Http404, HttpResponseRedirect
from extras.context_managers import event_tracking from extras.context_managers import event_tracking
from netbox.config import clear_config, get_config from netbox.config import clear_config, get_config
from netbox.views import handler_500 from netbox.views import handler_500
from utilities.api import is_api_request, rest_api_server_error from utilities.api import is_api_request
from utilities.error_handlers import handle_rest_api_exception
__all__ = ( __all__ = (
'CoreMiddleware', 'CoreMiddleware',
@ -71,7 +72,7 @@ class CoreMiddleware:
# Cleanly handle exceptions that occur from REST API requests # Cleanly handle exceptions that occur from REST API requests
if is_api_request(request): if is_api_request(request):
return rest_api_server_error(request) return handle_rest_api_exception(request)
# Ignore Http404s (defer to Django's built-in 404 handling) # Ignore Http404s (defer to Django's built-in 404 handling)
if isinstance(exception, Http404): if isinstance(exception, Http404):
@ -211,7 +212,7 @@ class MaintenanceModeMiddleware:
'operations. Please try again later.' 'operations. Please try again later.'
if is_api_request(request): if is_api_request(request):
return rest_api_server_error(request, error=error_message) return handle_rest_api_exception(request, error=error_message)
messages.error(request, error_message) messages.error(request, error_message)
return HttpResponseRedirect(request.path_info) return HttpResponseRedirect(request.path_info)

View File

@ -17,7 +17,7 @@ from netbox.config import get_config
from netbox.registry import registry from netbox.registry import registry
from netbox.signals import post_clean from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder from utilities.json import CustomFieldJSONEncoder
from utilities.utils import serialize_object from utilities.serialization import serialize_object
from utilities.views import register_model_view from utilities.views import register_model_view
__all__ = ( __all__ = (

View File

@ -1,8 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Sequence, Optional from typing import Sequence, Optional
from utilities.choices import ButtonColorChoices
__all__ = ( __all__ = (
'get_model_item', 'get_model_item',

View File

@ -1,7 +1,6 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.registry import registry from netbox.registry import registry
from utilities.choices import ButtonColorChoices
from . import * from . import *
# #

View File

@ -1,8 +1,9 @@
from netbox.navigation import MenuGroup
from utilities.choices import ButtonColorChoices
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from netbox.choices import ButtonColorChoices
from netbox.navigation import MenuGroup
__all__ = ( __all__ = (
'PluginMenu', 'PluginMenu',
'PluginMenuButton', 'PluginMenuButton',

View File

@ -14,8 +14,9 @@ from netaddr.core import AddrFormatError
from core.models import ObjectType from core.models import ObjectType
from extras.models import CachedValue, CustomField from extras.models import CachedValue, CustomField
from netbox.registry import registry from netbox.registry import registry
from utilities.object_types import object_type_identifier
from utilities.querysets import RestrictedPrefetch from utilities.querysets import RestrictedPrefetch
from utilities.utils import content_type_identifier, title from utilities.string import title
from . import FieldTypes, LookupTypes, get_indexer from . import FieldTypes, LookupTypes, get_indexer
DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL
@ -156,7 +157,7 @@ class CachedValueSearchBackend(SearchBackend):
# related objects necessary to render the prescribed display attributes (display_attrs). # related objects necessary to render the prescribed display attributes (display_attrs).
for object_type in object_types: for object_type in object_types:
model = object_type.model_class() model = object_type.model_class()
indexer = registry['search'].get(content_type_identifier(object_type)) indexer = registry['search'].get(object_type_identifier(object_type))
if not (display_attrs := getattr(indexer, 'display_attrs', None)): if not (display_attrs := getattr(indexer, 'display_attrs', None)):
continue continue

View File

@ -1,14 +1,14 @@
from netbox.registry import registry from netbox.registry import registry
from utilities.utils import content_type_identifier from utilities.object_types import object_type_identifier
__all__ = ( __all__ = (
'get_indexer', 'get_indexer',
) )
def get_indexer(content_type): def get_indexer(object_type):
""" """
Return the registered search indexer for the given ContentType. Return the registered search indexer for the given ContentType.
""" """
ct_identifier = content_type_identifier(content_type) identifier = object_type_identifier(object_type)
return registry['search'].get(ct_identifier) return registry['search'].get(identifier)

View File

@ -6,7 +6,7 @@ from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.choices import ChangeActionChoices from extras.choices import ChangeActionChoices
from extras.models import StagedChange from extras.models import StagedChange
from utilities.utils import serialize_object from utilities.serialization import serialize_object
logger = logging.getLogger('netbox.staging') logger = logging.getLogger('netbox.staging')

View File

@ -18,9 +18,10 @@ from django_tables2.columns import library
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from utilities.object_types import object_type_identifier, object_type_name
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, get_viewname from utilities.views import get_viewname
__all__ = ( __all__ = (
'ActionsColumn', 'ActionsColumn',
@ -338,12 +339,12 @@ class ContentTypeColumn(tables.Column):
def render(self, value): def render(self, value):
if value is None: if value is None:
return None return None
return content_type_name(value, include_app=False) return object_type_name(value, include_app=False)
def value(self, value): def value(self, value):
if value is None: if value is None:
return None return None
return content_type_identifier(value) return object_type_identifier(value)
class ContentTypesColumn(tables.ManyToManyColumn): class ContentTypesColumn(tables.ManyToManyColumn):
@ -357,11 +358,11 @@ class ContentTypesColumn(tables.ManyToManyColumn):
super().__init__(separator=separator, *args, **kwargs) super().__init__(separator=separator, *args, **kwargs)
def transform(self, obj): def transform(self, obj):
return content_type_name(obj, include_app=False) return object_type_name(obj, include_app=False)
def value(self, value): def value(self, value):
return ','.join([ return ','.join([
content_type_identifier(ct) for ct in self.filter(value) object_type_identifier(ot) for ot in self.filter(value)
]) ])

View File

@ -17,7 +17,9 @@ from extras.models import CustomField, CustomLink
from netbox.registry import registry from netbox.registry import registry
from netbox.tables import columns from netbox.tables import columns
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.utils import get_viewname, highlight_string, title from utilities.html import highlight
from utilities.string import title
from utilities.views import get_viewname
from .template_code import * from .template_code import *
__all__ = ( __all__ = (
@ -273,6 +275,6 @@ class SearchTable(tables.Table):
if not self.highlight: if not self.highlight:
return value return value
value = highlight_string(value, self.highlight, trim_pre=self.trim_length, trim_post=self.trim_length) value = highlight(value, self.highlight, trim_pre=self.trim_length, trim_post=self.trim_length)
return mark_safe(value) return mark_safe(value)

View File

@ -2,8 +2,8 @@ from django.test import override_settings
from core.models import ObjectType from core.models import ObjectType
from dcim.models import * from dcim.models import *
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from users.models import ObjectPermission from users.models import ObjectPermission
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import ModelViewTestCase, create_tags from utilities.testing import ModelViewTestCase, create_tags

View File

@ -24,8 +24,7 @@ from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViol
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
from utilities.forms.bulk_import import BulkImportForm from utilities.forms.bulk_import import BulkImportForm
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.utils import get_viewname from utilities.views import GetReturnURLMixin, get_viewname
from utilities.views import GetReturnURLMixin
from .base import BaseMultiObjectView from .base import BaseMultiObjectView
from .mixins import ActionsMixin, TableMixin from .mixins import ActionsMixin, TableMixin
from .utils import get_prerequisite_model from .utils import get_prerequisite_model

View File

@ -18,8 +18,8 @@ from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields from utilities.forms import ConfirmationForm, restrict_form_fields
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.utils import get_viewname, normalize_querydict, prepare_cloned_fields from utilities.querydict import normalize_querydict, prepare_cloned_fields
from utilities.views import GetReturnURLMixin from utilities.views import GetReturnURLMixin, get_viewname
from .base import BaseObjectView from .base import BaseObjectView
from .mixins import ActionsMixin, TableMixin from .mixins import ActionsMixin, TableMixin
from .utils import get_prerequisite_model from .utils import get_prerequisite_model

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

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

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

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,9 +33,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container-xl px-0"> <div class="container-fluid px-0">
<div class="table-responsive"> <div class="card">
{% render_table table 'inc/table.html' %} <div class="table-responsive">
{% 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 %}

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 %}

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