Merge branch 'feature' into 14736-htmx

This commit is contained in:
Jeremy Stretch 2024-03-25 10:31:05 -04:00
commit 3061e16304
243 changed files with 184773 additions and 4797 deletions

View File

@ -14,10 +14,6 @@ django-debug-toolbar
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
django-filter
# Django debug toolbar extension with support for GraphiQL
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
django-graphiql-debug-toolbar
# HTMX utilities for Django
# https://django-htmx.readthedocs.io/en/latest/changelog.html
django-htmx
@ -75,11 +71,6 @@ drf-spectacular-sidecar
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
feedparser
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django/releases
# Pinned to v3.0.0 for GraphiQL UI issue (see #12762)
graphene_django==3.0.0
# WSGI HTTP server
# https://docs.gunicorn.org/en/latest/news.html
gunicorn
@ -136,8 +127,16 @@ social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
social-auth-app-django
# Strawberry GraphQL
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
strawberry-graphql
# Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/blob/main/CHANGELOG.md
strawberry-graphql-django
# SVG image rendering (used for rack elevations)
# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
svgwrite
# Tabular dataset library (for table-based exports)

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 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
{
@ -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.
### Validation Types
The `CustomValidator` class supports several validation types:
* `min`: Minimum value
@ -34,16 +36,33 @@ The `min` and `max` types should be defined for numeric values, whereas `min_len
!!! 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.
### 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
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
from extras.validators import CustomValidator
class MyValidator(CustomValidator):
def validate(self, instance):
def validate(self, instance, request):
if instance.status == 'active' and not instance.description:
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 `_`.
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
from django.utils.translation import gettext_lazy as _
from utilities.forms.rendering import FieldSet
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
@ -74,7 +75,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
)
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
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.
| Attribute | Description |
|-------------|-------------------------------------------------------------|
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
| Attribute | Description |
|-------------|---------------------------------------------------------------------------------------|
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
**Example**
```python
from django.utils.translation import gettext_lazy as _
from dcim.models import Site
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from .models import MyModel
class MyModelForm(NetBoxModelForm):
@ -33,8 +35,8 @@ class MyModelForm(NetBoxModelForm):
)
comments = CommentField()
fieldsets = (
('Model Stuff', ('name', 'status', 'site', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
FieldSet('name', 'status', 'site', 'tags', name=_('Model Stuff')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -52,6 +54,7 @@ This form facilitates the bulk import of new objects from CSV, JSON, or YAML dat
**Example**
```python
from django.utils.translation import gettext_lazy as _
from dcim.models import Site
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelChoiceField
@ -62,7 +65,7 @@ class MyModelImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Assigned site'
help_text=_('Assigned site')
)
class Meta:
@ -77,16 +80,18 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
| Attribute | Description |
|-------------------|---------------------------------------------------------------------------------------------|
| `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) |
**Example**
```python
from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import Site
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from .models import MyModel, MyModelStatusChoices
@ -106,7 +111,7 @@ class MyModelEditForm(NetBoxModelImportForm):
model = MyModel
fieldsets = (
('Model Stuff', ('name', 'status', 'site')),
FieldSet('name', 'status', 'site', name=_('Model Stuff')),
)
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.
| Attribute | Description |
|-------------------|-------------------------------------------------------------|
| `model` | The model of object being edited |
| `fieldsets` | A tuple of two-tuples defining the form's layout (optional) |
| Attribute | Description |
|-------------|---------------------------------------------------------------------------------------|
| `model` | The model of object being edited |
| `fieldsets` | A tuple of `FieldSet` instances which control how form fields are rendered (optional) |
**Example**
@ -206,3 +211,13 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
::: utilities.forms.fields.CSVMultipleContentTypeField
options:
members: false
## Form Rendering
::: utilities.forms.rendering.FieldSet
::: utilities.forms.rendering.InlineFields
::: utilities.forms.rendering.TabbedGroups
::: utilities.forms.rendering.ObjectAttribute

View File

@ -8,23 +8,32 @@ A plugin can extend NetBox's GraphQL API by registering its own schema class. By
```python
# graphql.py
import graphene
from netbox.graphql.types import NetBoxObjectType
from netbox.graphql.fields import ObjectField, ObjectListField
from . import filtersets, models
from typing import List
import strawberry
import strawberry_django
class MyModelType(NetBoxObjectType):
from . import models
class Meta:
model = models.MyModel
fields = '__all__'
filterset_class = filtersets.MyModelFilterSet
class MyQuery(graphene.ObjectType):
mymodel = ObjectField(MyModelType)
mymodel_list = ObjectListField(MyModelType)
@strawberry_django.type(
models.MyModel,
fields='__all__',
)
class MyModelType:
pass
schema = MyQuery
@strawberry.type
class MyQuery:
@strawberry.field
def dummymodel(self, id: int) -> DummyModelType:
return None
dummymodel_list: List[DummyModelType] = strawberry_django.field()
schema = [
MyQuery,
]
```
## GraphQL Objects
@ -38,15 +47,3 @@ NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.NetBoxObjectType
options:
members: false
## GraphQL Fields
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
options:
members: false
::: netbox.graphql.fields.ObjectListField
options:
members: false

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.
```python title="navigation.py"
from netbox.choices import ButtonColorChoices
from netbox.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
item1 = PluginMenuItem(
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 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
@ -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.
#### 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
* [#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
* [#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
* [#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
* [#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
* [#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
* [#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
* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters)
### 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
* [#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
* [#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
* [#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 utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
@ -34,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider
fieldsets = (
(None, ('asns', 'description')),
FieldSet('asns', 'description'),
)
nullable_fields = (
'asns', 'description', 'comments',
@ -56,7 +57,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
model = ProviderAccount
fieldsets = (
(None, ('provider', 'description')),
FieldSet('provider', 'description'),
)
nullable_fields = (
'description', 'comments',
@ -83,7 +84,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
model = ProviderNetwork
fieldsets = (
(None, ('provider', 'service_id', 'description')),
FieldSet('provider', 'service_id', 'description'),
)
nullable_fields = (
'service_id', 'description', 'comments',
@ -103,7 +104,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
model = CircuitType
fieldsets = (
(None, ('color', 'description')),
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description')
@ -164,9 +165,9 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
model = Circuit
fieldsets = (
(_('Circuit'), ('provider', 'type', 'status', 'description')),
(_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
(_('Tenancy'), ('tenant',)),
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
'tenant', 'commit_rate', 'description', 'comments',

View File

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

View File

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

View File

@ -0,0 +1,50 @@
import strawberry
import strawberry_django
from circuits import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitTerminationFilter',
'CircuitFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
'ProviderNetworkFilter',
)
@strawberry_django.filter(models.CircuitTermination, lookups=True)
@autotype_decorator(filtersets.CircuitTerminationFilterSet)
class CircuitTerminationFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Circuit, lookups=True)
@autotype_decorator(filtersets.CircuitFilterSet)
class CircuitFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.CircuitType, lookups=True)
@autotype_decorator(filtersets.CircuitTypeFilterSet)
class CircuitTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Provider, lookups=True)
@autotype_decorator(filtersets.ProviderFilterSet)
class ProviderFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ProviderAccount, lookups=True)
@autotype_decorator(filtersets.ProviderAccountFilterSet)
class ProviderAccountFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
class ProviderNetworkFilter(BaseFilterMixin):
pass

View File

@ -1,41 +1,40 @@
import graphene
from typing import List
import strawberry
import strawberry_django
from circuits import models
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from utilities.graphql_optimizer import gql_query_optimizer
class CircuitsQuery(graphene.ObjectType):
circuit = ObjectField(CircuitType)
circuit_list = ObjectListField(CircuitType)
@strawberry.type
class CircuitsQuery:
@strawberry.field
def circuit(self, id: int) -> CircuitType:
return models.Circuit.objects.get(pk=id)
circuit_list: List[CircuitType] = strawberry_django.field()
def resolve_circuit_list(root, info, **kwargs):
return gql_query_optimizer(models.Circuit.objects.all(), info)
@strawberry.field
def circuit_termination(self, id: int) -> CircuitTerminationType:
return models.CircuitTermination.objects.get(pk=id)
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
circuit_termination = ObjectField(CircuitTerminationType)
circuit_termination_list = ObjectListField(CircuitTerminationType)
@strawberry.field
def circuit_type(self, id: int) -> CircuitTypeType:
return models.CircuitType.objects.get(pk=id)
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
def resolve_circuit_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.CircuitTermination.objects.all(), info)
@strawberry.field
def provider(self, id: int) -> ProviderType:
return models.Provider.objects.get(pk=id)
provider_list: List[ProviderType] = strawberry_django.field()
circuit_type = ObjectField(CircuitTypeType)
circuit_type_list = ObjectListField(CircuitTypeType)
@strawberry.field
def provider_account(self, id: int) -> ProviderAccountType:
return models.ProviderAccount.objects.get(pk=id)
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
def resolve_circuit_type_list(root, info, **kwargs):
return gql_query_optimizer(models.CircuitType.objects.all(), info)
provider = ObjectField(ProviderType)
provider_list = ObjectListField(ProviderType)
def resolve_provider_list(root, info, **kwargs):
return gql_query_optimizer(models.Provider.objects.all(), info)
provider_account = ObjectField(ProviderAccountType)
provider_account_list = ObjectListField(ProviderAccountType)
provider_network = ObjectField(ProviderNetworkType)
provider_network_list = ObjectListField(ProviderNetworkType)
def resolve_provider_network_list(root, info, **kwargs):
return gql_query_optimizer(models.ProviderNetwork.objects.all(), info)
@strawberry.field
def provider_network(self, id: int) -> ProviderNetworkType:
return models.ProviderNetwork.objects.get(pk=id)
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()

View File

@ -1,9 +1,14 @@
import graphene
from typing import Annotated, List
from circuits import filtersets, models
import strawberry
import strawberry_django
from circuits import models
from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
from tenancy.graphql.types import TenantType
from .filters import *
__all__ = (
'CircuitTerminationType',
@ -15,48 +20,93 @@ __all__ = (
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
class Meta:
model = models.CircuitTermination
fields = '__all__'
filterset_class = filtersets.CircuitTerminationFilterSet
class CircuitType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.Circuit
fields = '__all__'
filterset_class = filtersets.CircuitFilterSet
class CircuitTypeType(OrganizationalObjectType):
class Meta:
model = models.CircuitType
fields = '__all__'
filterset_class = filtersets.CircuitTypeFilterSet
@strawberry_django.type(
models.Provider,
fields='__all__',
filters=ProviderFilter
)
class ProviderType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.Provider
fields = '__all__'
filterset_class = filtersets.ProviderFilterSet
@strawberry_django.field
def networks(self) -> List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]:
return self.networks.all()
@strawberry_django.field
def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuits.all()
@strawberry_django.field
def asns(self) -> List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]:
return self.asns.all()
@strawberry_django.field
def accounts(self) -> List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]:
return self.accounts.all()
@strawberry_django.type(
models.ProviderAccount,
fields='__all__',
filters=ProviderAccountFilter
)
class ProviderAccountType(NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
class Meta:
model = models.ProviderAccount
fields = '__all__'
filterset_class = filtersets.ProviderAccountFilterSet
@strawberry_django.field
def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuits.all()
@strawberry_django.type(
models.ProviderNetwork,
fields='__all__',
filters=ProviderNetworkFilter
)
class ProviderNetworkType(NetBoxObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
class Meta:
model = models.ProviderNetwork
fields = '__all__'
filterset_class = filtersets.ProviderNetworkFilterSet
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuit_terminations.all()
@strawberry_django.type(
models.CircuitTermination,
fields='__all__',
filters=CircuitTerminationFilter
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
@strawberry_django.type(
models.CircuitType,
fields='__all__',
filters=CircuitTypeFilter
)
class CircuitTypeType(OrganizationalObjectType):
color: str
@strawberry_django.field
def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuits.all()
@strawberry_django.type(
models.Circuit,
fields='__all__',
filters=CircuitFilter
)
class CircuitType(NetBoxObjectType, ContactsMixin):
provider: ProviderType
provider_account: ProviderAccountType | None
termination_a: CircuitTerminationType | None
termination_z: CircuitTerminationType | None
type: CircuitTypeType
tenant: TenantType | None
@strawberry_django.field
def terminations(self) -> List[CircuitTerminationType]:
return self.terminations.all()

View File

@ -6,7 +6,7 @@ from dcim.views import PathTraceView
from netbox.views import generic
from tenancy.views import ObjectContactsView
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 . import filtersets, forms, tables
from .models import *
@ -412,7 +412,6 @@ class CircuitContactsView(ObjectContactsView):
class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all()
form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@register_model_view(CircuitTermination, 'delete')

View File

@ -5,6 +5,7 @@ from core.models import *
from netbox.forms import NetBoxModelBulkEditForm
from netbox.utils import get_data_backend_choices
from utilities.forms.fields import CommentField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
__all__ = (
@ -41,7 +42,7 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
model = DataSource
fieldsets = (
(None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')),
FieldSet('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules'),
)
nullable_fields = (
'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 utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
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__ = (
'ConfigRevisionFilterForm',
@ -22,8 +23,8 @@ __all__ = (
class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource
fieldsets = (
(None, ('q', 'filter_id')),
(_('Data Source'), ('type', 'status')),
FieldSet('q', 'filter_id'),
FieldSet('type', 'status', name=_('Data Source')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
@ -47,8 +48,8 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
class DataFileFilterForm(NetBoxModelFilterSetForm):
model = DataFile
fieldsets = (
(None, ('q', 'filter_id')),
(_('File'), ('source_id',)),
FieldSet('q', 'filter_id'),
FieldSet('source_id', name=_('File')),
)
source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -59,12 +60,12 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
class JobFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), ('object_type', 'status')),
(_('Creation'), (
FieldSet('q', 'filter_id'),
FieldSet('object_type', 'status', name=_('Attributes')),
FieldSet(
'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(
label=_('Object Type'),
@ -125,5 +126,5 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
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 utilities.forms import get_field_value
from utilities.forms.fields import CommentField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect
__all__ = (
@ -49,11 +50,11 @@ class DataSourceForm(NetBoxModelForm):
@property
def fieldsets(self):
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:
fieldsets.append(
(_('Backend Parameters'), self.backend_fields)
FieldSet(*self.backend_fields, name=_('Backend Parameters'))
)
return fieldsets
@ -91,8 +92,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
)
fieldsets = (
(_('File Upload'), ('upload_file',)),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
FieldSet('upload_file', name=_('File Upload')),
FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
)
class Meta:
@ -144,18 +145,24 @@ class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
"""
fieldsets = (
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
(_('Security'), ('ALLOWED_URL_SCHEMES',)),
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
(_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
(_('Miscellaneous'), (
FieldSet(
'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', name=_('Rack Elevations')
),
FieldSet(
'POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION',
name=_('Power')
),
FieldSet('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4', name=_('IPAM')),
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',
)),
(_('Config Revision'), ('comment',))
name=_('Miscellaneous')
),
FieldSet('comment', name=_('Config Revision'))
)
class Meta:

View File

@ -0,0 +1,21 @@
import strawberry_django
from core import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'DataFileFilter',
'DataSourceFilter',
)
@strawberry_django.filter(models.DataFile, lookups=True)
@autotype_decorator(filtersets.DataFileFilterSet)
class DataFileFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.DataSource, lookups=True)
@autotype_decorator(filtersets.DataSourceFilterSet)
class DataSourceFilter(BaseFilterMixin):
pass

View File

@ -1,20 +1,20 @@
import graphene
from typing import List
import strawberry
import strawberry_django
from core import models
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from utilities.graphql_optimizer import gql_query_optimizer
class CoreQuery(graphene.ObjectType):
data_file = ObjectField(DataFileType)
data_file_list = ObjectListField(DataFileType)
@strawberry.type
class CoreQuery:
@strawberry.field
def data_file(self, id: int) -> DataFileType:
return models.DataFile.objects.get(pk=id)
data_file_list: List[DataFileType] = strawberry_django.field()
def resolve_data_file_list(root, info, **kwargs):
return gql_query_optimizer(models.DataFile.objects.all(), info)
data_source = ObjectField(DataSourceType)
data_source_list = ObjectListField(DataSourceType)
def resolve_data_source_list(root, info, **kwargs):
return gql_query_optimizer(models.DataSource.objects.all(), info)
@strawberry.field
def data_source(self, id: int) -> DataSourceType:
return models.DataSource.objects.get(pk=id)
data_source_list: List[DataSourceType] = strawberry_django.field()

View File

@ -1,5 +1,11 @@
from core import filtersets, models
from typing import Annotated, List
import strawberry
import strawberry_django
from core import models
from netbox.graphql.types import BaseObjectType, NetBoxObjectType
from .filters import *
__all__ = (
'DataFileType',
@ -7,15 +13,22 @@ __all__ = (
)
@strawberry_django.type(
models.DataFile,
exclude=['data',],
filters=DataFileFilter
)
class DataFileType(BaseObjectType):
class Meta:
model = models.DataFile
exclude = ('data',)
filterset_class = filtersets.DataFileFilterSet
source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')]
@strawberry_django.type(
models.DataSource,
fields='__all__',
filters=DataSourceFilter
)
class DataSourceType(NetBoxObjectType):
class Meta:
model = models.DataSource
fields = '__all__'
filterset_class = filtersets.DataSourceFilterSet
@strawberry_django.field
def datafiles(self) -> List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]:
return self.datafiles.all()

View File

@ -1,3 +1,4 @@
import hashlib
import logging
import os
import yaml
@ -18,7 +19,6 @@ from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel
from netbox.models.features import JobsMixin
from netbox.registry import registry
from utilities.files import sha256_hash
from utilities.querysets import RestrictedQuerySet
from ..choices import *
from ..exceptions import SyncError
@ -357,7 +357,8 @@ class DataFile(models.Model):
has changed.
"""
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
if is_modified := file_hash != self.hash:

View File

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

View File

@ -92,7 +92,7 @@ class LocationSerializer(NestedGroupModelSerializer):
class Meta:
model = Location
fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_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 ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, IPAddress, VRF
from netbox.choices import ColorChoices
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
)
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import *
from utilities.choices import ColorChoices
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
@ -270,13 +270,14 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
class Meta:
model = Location
fields = ('id', 'name', 'slug', 'status', 'description')
fields = ('id', 'name', 'slug', 'status', 'facility', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(facility__icontains=value) |
Q(description__icontains=value)
)

View File

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

View File

@ -157,7 +157,7 @@ class LocationImportForm(NetBoxModelImportForm):
class Meta:
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):
super().__init__(data, *args, **kwargs)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,294 @@
import strawberry_django
from dcim import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CableFilter',
'CableTerminationFilter',
'ConsolePortFilter',
'ConsolePortTemplateFilter',
'ConsoleServerPortFilter',
'ConsoleServerPortTemplateFilter',
'DeviceFilter',
'DeviceBayFilter',
'DeviceBayTemplateFilter',
'InventoryItemTemplateFilter',
'DeviceRoleFilter',
'DeviceTypeFilter',
'FrontPortFilter',
'FrontPortTemplateFilter',
'InterfaceFilter',
'InterfaceTemplateFilter',
'InventoryItemFilter',
'InventoryItemRoleFilter',
'LocationFilter',
'ManufacturerFilter',
'ModuleFilter',
'ModuleBayFilter',
'ModuleBayTemplateFilter',
'ModuleTypeFilter',
'PlatformFilter',
'PowerFeedFilter',
'PowerOutletFilter',
'PowerOutletTemplateFilter',
'PowerPanelFilter',
'PowerPortFilter',
'PowerPortTemplateFilter',
'RackFilter',
'RackReservationFilter',
'RackRoleFilter',
'RearPortFilter',
'RearPortTemplateFilter',
'RegionFilter',
'SiteFilter',
'SiteGroupFilter',
'VirtualChassisFilter',
'VirtualDeviceContextFilter',
)
@strawberry_django.filter(models.Cable, lookups=True)
@autotype_decorator(filtersets.CableFilterSet)
class CableFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.CableTermination, lookups=True)
@autotype_decorator(filtersets.CableTerminationFilterSet)
class CableTerminationFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ConsolePort, lookups=True)
@autotype_decorator(filtersets.ConsolePortFilterSet)
class ConsolePortFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
@autotype_decorator(filtersets.ConsolePortTemplateFilterSet)
class ConsolePortTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
@autotype_decorator(filtersets.ConsoleServerPortFilterSet)
class ConsoleServerPortFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
@autotype_decorator(filtersets.ConsoleServerPortTemplateFilterSet)
class ConsoleServerPortTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Device, lookups=True)
@autotype_decorator(filtersets.DeviceFilterSet)
class DeviceFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.DeviceBay, lookups=True)
@autotype_decorator(filtersets.DeviceBayFilterSet)
class DeviceBayFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
@autotype_decorator(filtersets.DeviceBayTemplateFilterSet)
class DeviceBayTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
@autotype_decorator(filtersets.InventoryItemTemplateFilterSet)
class InventoryItemTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.DeviceRole, lookups=True)
@autotype_decorator(filtersets.DeviceRoleFilterSet)
class DeviceRoleFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.DeviceType, lookups=True)
@autotype_decorator(filtersets.DeviceTypeFilterSet)
class DeviceTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.FrontPort, lookups=True)
@autotype_decorator(filtersets.FrontPortFilterSet)
class FrontPortFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
@autotype_decorator(filtersets.FrontPortTemplateFilterSet)
class FrontPortTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Interface, lookups=True)
@autotype_decorator(filtersets.InterfaceFilterSet)
class InterfaceFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
@autotype_decorator(filtersets.InterfaceTemplateFilterSet)
class InterfaceTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.InventoryItem, lookups=True)
@autotype_decorator(filtersets.InventoryItemFilterSet)
class InventoryItemFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
@autotype_decorator(filtersets.InventoryItemRoleFilterSet)
class InventoryItemRoleFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Location, lookups=True)
@autotype_decorator(filtersets.LocationFilterSet)
class LocationFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Manufacturer, lookups=True)
@autotype_decorator(filtersets.ManufacturerFilterSet)
class ManufacturerFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Module, lookups=True)
@autotype_decorator(filtersets.ModuleFilterSet)
class ModuleFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ModuleBay, lookups=True)
@autotype_decorator(filtersets.ModuleBayFilterSet)
class ModuleBayFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
@autotype_decorator(filtersets.ModuleBayTemplateFilterSet)
class ModuleBayTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ModuleType, lookups=True)
@autotype_decorator(filtersets.ModuleTypeFilterSet)
class ModuleTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Platform, lookups=True)
@autotype_decorator(filtersets.PlatformFilterSet)
class PlatformFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerFeed, lookups=True)
@autotype_decorator(filtersets.PowerFeedFilterSet)
class PowerFeedFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerOutlet, lookups=True)
@autotype_decorator(filtersets.PowerOutletFilterSet)
class PowerOutletFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
@autotype_decorator(filtersets.PowerOutletTemplateFilterSet)
class PowerOutletTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerPanel, lookups=True)
@autotype_decorator(filtersets.PowerPanelFilterSet)
class PowerPanelFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerPort, lookups=True)
@autotype_decorator(filtersets.PowerPortFilterSet)
class PowerPortFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
@autotype_decorator(filtersets.PowerPortTemplateFilterSet)
class PowerPortTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Rack, lookups=True)
@autotype_decorator(filtersets.RackFilterSet)
class RackFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RackReservation, lookups=True)
@autotype_decorator(filtersets.RackReservationFilterSet)
class RackReservationFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RackRole, lookups=True)
@autotype_decorator(filtersets.RackRoleFilterSet)
class RackRoleFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RearPort, lookups=True)
@autotype_decorator(filtersets.RearPortFilterSet)
class RearPortFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
@autotype_decorator(filtersets.RearPortTemplateFilterSet)
class RearPortTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Region, lookups=True)
@autotype_decorator(filtersets.RegionFilterSet)
class RegionFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Site, lookups=True)
@autotype_decorator(filtersets.SiteFilterSet)
class SiteFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.SiteGroup, lookups=True)
@autotype_decorator(filtersets.SiteGroupFilterSet)
class SiteGroupFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualChassis, lookups=True)
@autotype_decorator(filtersets.VirtualChassisFilterSet)
class VirtualChassisFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
@autotype_decorator(filtersets.VirtualDeviceContextFilterSet)
class VirtualDeviceContextFilter(BaseFilterMixin):
pass

View File

@ -1,4 +1,3 @@
import graphene
from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType
from circuits.models import CircuitTermination, ProviderNetwork
from dcim.graphql.types import (
@ -37,79 +36,7 @@ from dcim.models import (
)
class LinkPeerType(graphene.Union):
class Meta:
types = (
CircuitTerminationType,
ConsolePortType,
ConsoleServerPortType,
FrontPortType,
InterfaceType,
PowerFeedType,
PowerOutletType,
PowerPortType,
RearPortType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) is FrontPort:
return FrontPortType
if type(instance) is Interface:
return InterfaceType
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) is PowerPort:
return PowerPortType
if type(instance) is RearPort:
return RearPortType
class CableTerminationTerminationType(graphene.Union):
class Meta:
types = (
CircuitTerminationType,
ConsolePortType,
ConsoleServerPortType,
FrontPortType,
InterfaceType,
PowerFeedType,
PowerOutletType,
PowerPortType,
RearPortType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) is FrontPort:
return FrontPortType
if type(instance) is Interface:
return InterfaceType
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) is PowerPort:
return PowerPortType
if type(instance) is RearPort:
return RearPortType
class InventoryItemTemplateComponentType(graphene.Union):
class InventoryItemTemplateComponentType:
class Meta:
types = (
ConsolePortTemplateType,
@ -139,7 +66,7 @@ class InventoryItemTemplateComponentType(graphene.Union):
return RearPortTemplateType
class InventoryItemComponentType(graphene.Union):
class InventoryItemComponentType:
class Meta:
types = (
ConsolePortType,
@ -169,7 +96,7 @@ class InventoryItemComponentType(graphene.Union):
return RearPortType
class ConnectedEndpointType(graphene.Union):
class ConnectedEndpointType:
class Meta:
types = (
CircuitTerminationType,

View File

@ -1,20 +1,47 @@
import graphene
from typing import Annotated, List, Union
import strawberry
import strawberry_django
__all__ = (
'CabledObjectMixin',
'PathEndpointMixin',
)
@strawberry.type
class CabledObjectMixin:
link_peers = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType')
cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
def resolve_cable_end(self, info):
# Handle empty values
return self.cable_end or None
def resolve_link_peers(self, info):
@strawberry_django.field
def link_peers(self) -> List[Annotated[Union[
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
], strawberry.union("LinkPeerType")]]:
return self.link_peers
@strawberry.type
class PathEndpointMixin:
connected_endpoints = graphene.List('dcim.graphql.gfk_mixins.ConnectedEndpointType')
def resolve_connected_endpoints(self, info):
# Handle empty values
@strawberry_django.field
def connected_endpoints(self) -> List[Annotated[Union[
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
], strawberry.union("ConnectedEndpointType")]]:
return self.connected_endpoints or None

View File

@ -1,249 +1,210 @@
import graphene
from typing import List
import strawberry
import strawberry_django
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from dcim import models
from .types import VirtualDeviceContextType
from utilities.graphql_optimizer import gql_query_optimizer
class DCIMQuery(graphene.ObjectType):
cable = ObjectField(CableType)
cable_list = ObjectListField(CableType)
def resolve_cable_list(root, info, **kwargs):
return gql_query_optimizer(models.Cable.objects.all(), info)
console_port = ObjectField(ConsolePortType)
console_port_list = ObjectListField(ConsolePortType)
def resolve_console_port_list(root, info, **kwargs):
return gql_query_optimizer(models.ConsolePort.objects.all(), info)
console_port_template = ObjectField(ConsolePortTemplateType)
console_port_template_list = ObjectListField(ConsolePortTemplateType)
def resolve_console_port_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ConsolePortTemplate.objects.all(), info)
console_server_port = ObjectField(ConsoleServerPortType)
console_server_port_list = ObjectListField(ConsoleServerPortType)
def resolve_console_server_port_list(root, info, **kwargs):
return gql_query_optimizer(models.ConsoleServerPort.objects.all(), info)
console_server_port_template = ObjectField(ConsoleServerPortTemplateType)
console_server_port_template_list = ObjectListField(ConsoleServerPortTemplateType)
def resolve_console_server_port_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ConsoleServerPortTemplate.objects.all(), info)
device = ObjectField(DeviceType)
device_list = ObjectListField(DeviceType)
def resolve_device_list(root, info, **kwargs):
return gql_query_optimizer(models.Device.objects.all(), info)
device_bay = ObjectField(DeviceBayType)
device_bay_list = ObjectListField(DeviceBayType)
def resolve_device_bay_list(root, info, **kwargs):
return gql_query_optimizer(models.DeviceBay.objects.all(), info)
device_bay_template = ObjectField(DeviceBayTemplateType)
device_bay_template_list = ObjectListField(DeviceBayTemplateType)
def resolve_device_bay_template_list(root, info, **kwargs):
return gql_query_optimizer(models.DeviceBayTemplate.objects.all(), info)
device_role = ObjectField(DeviceRoleType)
device_role_list = ObjectListField(DeviceRoleType)
def resolve_device_role_list(root, info, **kwargs):
return gql_query_optimizer(models.DeviceRole.objects.all(), info)
device_type = ObjectField(DeviceTypeType)
device_type_list = ObjectListField(DeviceTypeType)
def resolve_device_type_list(root, info, **kwargs):
return gql_query_optimizer(models.DeviceType.objects.all(), info)
front_port = ObjectField(FrontPortType)
front_port_list = ObjectListField(FrontPortType)
def resolve_front_port_list(root, info, **kwargs):
return gql_query_optimizer(models.FrontPort.objects.all(), info)
front_port_template = ObjectField(FrontPortTemplateType)
front_port_template_list = ObjectListField(FrontPortTemplateType)
def resolve_front_port_template_list(root, info, **kwargs):
return gql_query_optimizer(models.FrontPortTemplate.objects.all(), info)
interface = ObjectField(InterfaceType)
interface_list = ObjectListField(InterfaceType)
def resolve_interface_list(root, info, **kwargs):
return gql_query_optimizer(models.Interface.objects.all(), info)
interface_template = ObjectField(InterfaceTemplateType)
interface_template_list = ObjectListField(InterfaceTemplateType)
def resolve_interface_template_list(root, info, **kwargs):
return gql_query_optimizer(models.InterfaceTemplate.objects.all(), info)
inventory_item = ObjectField(InventoryItemType)
inventory_item_list = ObjectListField(InventoryItemType)
def resolve_inventory_item_list(root, info, **kwargs):
return gql_query_optimizer(models.InventoryItem.objects.all(), info)
inventory_item_role = ObjectField(InventoryItemRoleType)
inventory_item_role_list = ObjectListField(InventoryItemRoleType)
def resolve_inventory_item_role_list(root, info, **kwargs):
return gql_query_optimizer(models.InventoryItemRole.objects.all(), info)
inventory_item_template = ObjectField(InventoryItemTemplateType)
inventory_item_template_list = ObjectListField(InventoryItemTemplateType)
def resolve_inventory_item_template_list(root, info, **kwargs):
return gql_query_optimizer(models.InventoryItemTemplate.objects.all(), info)
location = ObjectField(LocationType)
location_list = ObjectListField(LocationType)
def resolve_location_list(root, info, **kwargs):
return gql_query_optimizer(models.Location.objects.all(), info)
manufacturer = ObjectField(ManufacturerType)
manufacturer_list = ObjectListField(ManufacturerType)
def resolve_manufacturer_list(root, info, **kwargs):
return gql_query_optimizer(models.Manufacturer.objects.all(), info)
module = ObjectField(ModuleType)
module_list = ObjectListField(ModuleType)
def resolve_module_list(root, info, **kwargs):
return gql_query_optimizer(models.Module.objects.all(), info)
module_bay = ObjectField(ModuleBayType)
module_bay_list = ObjectListField(ModuleBayType)
def resolve_module_bay_list(root, info, **kwargs):
return gql_query_optimizer(models.ModuleBay.objects.all(), info)
module_bay_template = ObjectField(ModuleBayTemplateType)
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
def resolve_module_bay_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ModuleBayTemplate.objects.all(), info)
module_type = ObjectField(ModuleTypeType)
module_type_list = ObjectListField(ModuleTypeType)
def resolve_module_type_list(root, info, **kwargs):
return gql_query_optimizer(models.ModuleType.objects.all(), info)
platform = ObjectField(PlatformType)
platform_list = ObjectListField(PlatformType)
def resolve_platform_list(root, info, **kwargs):
return gql_query_optimizer(models.Platform.objects.all(), info)
power_feed = ObjectField(PowerFeedType)
power_feed_list = ObjectListField(PowerFeedType)
def resolve_power_feed_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerFeed.objects.all(), info)
power_outlet = ObjectField(PowerOutletType)
power_outlet_list = ObjectListField(PowerOutletType)
def resolve_power_outlet_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerOutlet.objects.all(), info)
power_outlet_template = ObjectField(PowerOutletTemplateType)
power_outlet_template_list = ObjectListField(PowerOutletTemplateType)
def resolve_power_outlet_template_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerOutletTemplate.objects.all(), info)
power_panel = ObjectField(PowerPanelType)
power_panel_list = ObjectListField(PowerPanelType)
def resolve_power_panel_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerPanel.objects.all(), info)
power_port = ObjectField(PowerPortType)
power_port_list = ObjectListField(PowerPortType)
def resolve_power_port_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerPort.objects.all(), info)
power_port_template = ObjectField(PowerPortTemplateType)
power_port_template_list = ObjectListField(PowerPortTemplateType)
def resolve_power_port_template_list(root, info, **kwargs):
return gql_query_optimizer(models.PowerPortTemplate.objects.all(), info)
rack = ObjectField(RackType)
rack_list = ObjectListField(RackType)
def resolve_rack_list(root, info, **kwargs):
return gql_query_optimizer(models.Rack.objects.all(), info)
rack_reservation = ObjectField(RackReservationType)
rack_reservation_list = ObjectListField(RackReservationType)
def resolve_rack_reservation_list(root, info, **kwargs):
return gql_query_optimizer(models.RackReservation.objects.all(), info)
rack_role = ObjectField(RackRoleType)
rack_role_list = ObjectListField(RackRoleType)
def resolve_rack_role_list(root, info, **kwargs):
return gql_query_optimizer(models.RackRole.objects.all(), info)
rear_port = ObjectField(RearPortType)
rear_port_list = ObjectListField(RearPortType)
def resolve_rear_port_list(root, info, **kwargs):
return gql_query_optimizer(models.RearPort.objects.all(), info)
rear_port_template = ObjectField(RearPortTemplateType)
rear_port_template_list = ObjectListField(RearPortTemplateType)
def resolve_rear_port_template_list(root, info, **kwargs):
return gql_query_optimizer(models.RearPortTemplate.objects.all(), info)
region = ObjectField(RegionType)
region_list = ObjectListField(RegionType)
def resolve_region_list(root, info, **kwargs):
return gql_query_optimizer(models.Region.objects.all(), info)
site = ObjectField(SiteType)
site_list = ObjectListField(SiteType)
def resolve_site_list(root, info, **kwargs):
return gql_query_optimizer(models.Site.objects.all(), info)
site_group = ObjectField(SiteGroupType)
site_group_list = ObjectListField(SiteGroupType)
def resolve_site_group_list(root, info, **kwargs):
return gql_query_optimizer(models.SiteGroup.objects.all(), info)
virtual_chassis = ObjectField(VirtualChassisType)
virtual_chassis_list = ObjectListField(VirtualChassisType)
def resolve_virtual_chassis_list(root, info, **kwargs):
return gql_query_optimizer(models.VirtualChassis.objects.all(), info)
virtual_device_context = ObjectField(VirtualDeviceContextType)
virtual_device_context_list = ObjectListField(VirtualDeviceContextType)
def resolve_virtual_device_context_list(root, info, **kwargs):
return gql_query_optimizer(models.VirtualDeviceContext.objects.all(), info)
from .types import *
@strawberry.type
class DCIMQuery:
@strawberry.field
def cable(self, id: int) -> CableType:
return models.Cable.objects.get(pk=id)
cable_list: List[CableType] = strawberry_django.field()
@strawberry.field
def console_port(self, id: int) -> ConsolePortType:
return models.ConsolePort.objects.get(pk=id)
console_port_list: List[ConsolePortType] = strawberry_django.field()
@strawberry.field
def console_port_template(self, id: int) -> ConsolePortTemplateType:
return models.ConsolePortTemplate.objects.get(pk=id)
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
@strawberry.field
def console_server_port(self, id: int) -> ConsoleServerPortType:
return models.ConsoleServerPort.objects.get(pk=id)
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
@strawberry.field
def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
return models.ConsoleServerPortTemplate.objects.get(pk=id)
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
@strawberry.field
def device(self, id: int) -> DeviceType:
return models.Device.objects.get(pk=id)
device_list: List[DeviceType] = strawberry_django.field()
@strawberry.field
def device_bay(self, id: int) -> DeviceBayType:
return models.DeviceBay.objects.get(pk=id)
device_bay_list: List[DeviceBayType] = strawberry_django.field()
@strawberry.field
def device_bay_template(self, id: int) -> DeviceBayTemplateType:
return models.DeviceBayTemplate.objects.get(pk=id)
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
@strawberry.field
def device_role(self, id: int) -> DeviceRoleType:
return models.DeviceRole.objects.get(pk=id)
device_role_list: List[DeviceRoleType] = strawberry_django.field()
@strawberry.field
def device_type(self, id: int) -> DeviceTypeType:
return models.DeviceType.objects.get(pk=id)
device_type_list: List[DeviceTypeType] = strawberry_django.field()
@strawberry.field
def front_port(self, id: int) -> FrontPortType:
return models.FrontPort.objects.get(pk=id)
front_port_list: List[FrontPortType] = strawberry_django.field()
@strawberry.field
def front_port_template(self, id: int) -> FrontPortTemplateType:
return models.FrontPortTemplate.objects.get(pk=id)
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
@strawberry.field
def interface(self, id: int) -> InterfaceType:
return models.Interface.objects.get(pk=id)
interface_list: List[InterfaceType] = strawberry_django.field()
@strawberry.field
def interface_template(self, id: int) -> InterfaceTemplateType:
return models.InterfaceTemplate.objects.get(pk=id)
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
@strawberry.field
def inventory_item(self, id: int) -> InventoryItemType:
return models.InventoryItem.objects.get(pk=id)
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
@strawberry.field
def inventory_item_role(self, id: int) -> InventoryItemRoleType:
return models.InventoryItemRole.objects.get(pk=id)
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
@strawberry.field
def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
return models.InventoryItemTemplate.objects.get(pk=id)
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
@strawberry.field
def location(self, id: int) -> LocationType:
return models.Location.objects.get(pk=id)
location_list: List[LocationType] = strawberry_django.field()
@strawberry.field
def manufacturer(self, id: int) -> ManufacturerType:
return models.Manufacturer.objects.get(pk=id)
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
@strawberry.field
def module(self, id: int) -> ModuleType:
return models.Module.objects.get(pk=id)
module_list: List[ModuleType] = strawberry_django.field()
@strawberry.field
def module_bay(self, id: int) -> ModuleBayType:
return models.ModuleBay.objects.get(pk=id)
module_bay_list: List[ModuleBayType] = strawberry_django.field()
@strawberry.field
def module_bay_template(self, id: int) -> ModuleBayTemplateType:
return models.ModuleBayTemplate.objects.get(pk=id)
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
@strawberry.field
def module_type(self, id: int) -> ModuleTypeType:
return models.ModuleType.objects.get(pk=id)
module_type_list: List[ModuleTypeType] = strawberry_django.field()
@strawberry.field
def platform(self, id: int) -> PlatformType:
return models.Platform.objects.get(pk=id)
platform_list: List[PlatformType] = strawberry_django.field()
@strawberry.field
def power_feed(self, id: int) -> PowerFeedType:
return models.PowerFeed.objects.get(pk=id)
power_feed_list: List[PowerFeedType] = strawberry_django.field()
@strawberry.field
def power_outlet(self, id: int) -> PowerOutletType:
return models.PowerOutlet.objects.get(pk=id)
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
@strawberry.field
def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
return models.PowerOutletTemplate.objects.get(pk=id)
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
@strawberry.field
def power_panel(self, id: int) -> PowerPanelType:
return models.PowerPanel.objects.get(id=id)
power_panel_list: List[PowerPanelType] = strawberry_django.field()
@strawberry.field
def power_port(self, id: int) -> PowerPortType:
return models.PowerPort.objects.get(id=id)
power_port_list: List[PowerPortType] = strawberry_django.field()
@strawberry.field
def power_port_template(self, id: int) -> PowerPortTemplateType:
return models.PowerPortTemplate.objects.get(id=id)
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
@strawberry.field
def rack(self, id: int) -> RackType:
return models.Rack.objects.get(id=id)
rack_list: List[RackType] = strawberry_django.field()
@strawberry.field
def rack_reservation(self, id: int) -> RackReservationType:
return models.RackReservation.objects.get(id=id)
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
@strawberry.field
def rack_role(self, id: int) -> RackRoleType:
return models.RackRole.objects.get(id=id)
rack_role_list: List[RackRoleType] = strawberry_django.field()
@strawberry.field
def rear_port(self, id: int) -> RearPortType:
return models.RearPort.objects.get(id=id)
rear_port_list: List[RearPortType] = strawberry_django.field()
@strawberry.field
def rear_port_template(self, id: int) -> RearPortTemplateType:
return models.RearPortTemplate.objects.get(id=id)
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
@strawberry.field
def region(self, id: int) -> RegionType:
return models.Region.objects.get(id=id)
region_list: List[RegionType] = strawberry_django.field()
@strawberry.field
def site(self, id: int) -> SiteType:
return models.Site.objects.get(id=id)
site_list: List[SiteType] = strawberry_django.field()
@strawberry.field
def site_group(self, id: int) -> SiteGroupType:
return models.SiteGroup.objects.get(id=id)
site_group_list: List[SiteGroupType] = strawberry_django.field()
@strawberry.field
def virtual_chassis(self, id: int) -> VirtualChassisType:
return models.VirtualChassis.objects.get(id=id)
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
@strawberry.field
def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
return models.VirtualDeviceContext.objects.get(id=id)
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()

File diff suppressed because it is too large Load Diff

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.utils import decompile_path_node, object_to_path_node
from netbox.models import ChangeLoggedModel, PrimaryModel
from utilities.conversion import to_meters
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
from wireless.models import WirelessLink
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.constants import *
from dcim.fields import MACAddressField, WWNField
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface

View File

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

View File

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

View File

@ -14,11 +14,12 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
from dcim.svg import RackElevationSVG
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, PrimaryModel
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.utils import array_to_string, drange, to_grams
from .device_components import PowerPort
from .devices import Device, Module
from .mixins import WeightMixin

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ from django.urls import reverse
from django.utils.http import urlencode
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

View File

@ -152,7 +152,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'status', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description',
'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated',
'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
'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.models import *
from ipam.models import ASN, IPAddress, RIR, VRF
from netbox.choices import ColorChoices
from tenancy.models import Tenant, TenantGroup
from utilities.choices import ColorChoices
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
User = get_user_model()
@ -359,9 +358,9 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
location.save()
locations = (
Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, 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 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
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, facility='Facility 2', description='foobar2'),
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:
location.save()
@ -390,6 +389,10 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [LocationStatusChoices.STATUS_PLANNED, LocationStatusChoices.STATUS_STAGING]}
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):
params = {'description': ['foobar1', 'foobar2']}
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 extras.models import CustomField
from tenancy.models import Tenant
from utilities.utils import drange
from utilities.data import drange
class LocationTestCase(TestCase):

View File

@ -11,12 +11,11 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from tenancy.models import Tenant
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN
User = get_user_model()
@ -213,6 +212,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'slug': 'location-x',
'site': site.pk,
'status': LocationStatusChoices.STATUS_PLANNED,
'facility': 'Facility X',
'tenant': tenant.pk,
'description': 'A new location',
'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.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
from utilities.query import count_related
from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
from virtualization.models import VirtualMachine
from . import filtersets, forms, tables
@ -727,7 +727,6 @@ class RackNonRackedView(generic.ObjectChildrenView):
class RackEditView(generic.ObjectEditView):
queryset = Rack.objects.all()
form = forms.RackForm
template_name = 'dcim/rack_edit.html'
@register_model_view(Rack, 'delete')
@ -2925,14 +2924,12 @@ class InventoryItemView(generic.ObjectView):
class InventoryItemEditView(generic.ObjectEditView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_edit.html'
@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.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request
from utilities.request import copy_safe_request
from . import serializers
from .mixins import ConfigTemplateRenderMixin

View File

@ -2,7 +2,8 @@ import logging
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 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.querydict import dict_to_querydict
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
__all__ = (
@ -33,15 +35,15 @@ __all__ = (
def get_object_type_choices():
return [
(content_type_identifier(ct), content_type_name(ct))
for ct in ObjectType.objects.public().order_by('app_label', 'model')
(object_type_identifier(ot), object_type_name(ot))
for ot in ObjectType.objects.public().order_by('app_label', 'model')
]
def get_bookmarks_object_type_choices():
return [
(content_type_identifier(ct), content_type_name(ct))
for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
(object_type_identifier(ot), object_type_name(ot))
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.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
@ -15,9 +12,9 @@ from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.registry import registry
from utilities.api import get_serializer_for_model
from utilities.rqworker import get_rq_retry
from utilities.utils import serialize_object
from utilities.serialization import serialize_object
from .choices import *
from .models import EventRule, ScriptModule
from .models import EventRule
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 (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -36,11 +37,11 @@ __all__ = (
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), (
FieldSet('q', 'filter_id'),
FieldSet(
'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(
queryset=ObjectType.objects.with_feature('custom_fields'),
@ -93,8 +94,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Choices'), ('base_choices', 'choice')),
FieldSet('q', 'filter_id'),
FieldSet('base_choices', 'choice', name=_('Choices')),
)
base_choices = forms.MultipleChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
@ -107,8 +108,8 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
FieldSet('q', 'filter_id'),
FieldSet('object_type', 'enabled', 'new_window', 'weight', name=_('Attributes')),
)
object_type = ContentTypeMultipleChoiceField(
label=_('Object types'),
@ -137,9 +138,9 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Data'), ('data_source_id', 'data_file_id')),
(_('Attributes'), ('object_type_id', 'mime_type', 'file_extension', 'as_attachment')),
FieldSet('q', 'filter_id'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
FieldSet('object_type_id', 'mime_type', 'file_extension', 'as_attachment', name=_('Attributes')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -178,8 +179,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), ('object_type_id', 'name',)),
FieldSet('q', 'filter_id'),
FieldSet('object_type_id', 'name', name=_('Attributes')),
)
object_type_id = ContentTypeChoiceField(
label=_('Object type'),
@ -194,8 +195,8 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
FieldSet('q', 'filter_id'),
FieldSet('object_type', 'enabled', 'shared', 'weight', name=_('Attributes')),
)
object_type = ContentTypeMultipleChoiceField(
label=_('Object types'),
@ -225,8 +226,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
class WebhookFilterForm(NetBoxModelFilterSetForm):
model = Webhook
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('payload_url', 'http_method', 'http_content_type')),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
)
http_content_type = forms.CharField(
label=_('HTTP content type'),
@ -249,9 +250,9 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('object_type_id', 'action_type', 'enabled')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('object_type_id', 'action_type', 'enabled', name=_('Attributes')),
FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
)
object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('event_rules'),
@ -323,12 +324,12 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag_id')),
(_('Data'), ('data_source_id', 'data_file_id')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
(_('Device'), ('device_type_id', 'platform_id', 'role_id')),
(_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id'))
FieldSet('q', 'filter_id', 'tag_id'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
FieldSet('device_type_id', 'platform_id', 'role_id', name=_('Device')),
FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant'))
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -412,8 +413,8 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Data'), ('data_source_id', 'data_file_id')),
FieldSet('q', 'filter_id', 'tag'),
FieldSet('data_source_id', 'data_file_id', name=_('Data')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -444,9 +445,9 @@ class LocalConfigContextFilterForm(forms.Form):
class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Creation'), ('created_before', 'created_after', 'created_by_id')),
(_('Attributes'), ('assigned_object_type_id', 'kind'))
FieldSet('q', 'filter_id', 'tag'),
FieldSet('created_before', 'created_after', 'created_by_id', name=_('Creation')),
FieldSet('assigned_object_type_id', 'kind', name=_('Attributes')),
)
created_after = forms.DateTimeField(
required=False,
@ -482,9 +483,9 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange
fieldsets = (
(None, ('q', 'filter_id')),
(_('Time'), ('time_before', 'time_after')),
(_('Attributes'), ('action', 'user_id', 'changed_object_type_id')),
FieldSet('q', 'filter_id'),
FieldSet('time_before', 'time_after', name=_('Time')),
FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
)
time_after = forms.DateTimeField(
required=False,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,98 @@
import strawberry_django
from extras import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'ConfigContextFilter',
'ConfigTemplateFilter',
'CustomFieldFilter',
'CustomFieldChoiceSetFilter',
'CustomLinkFilter',
'EventRuleFilter',
'ExportTemplateFilter',
'ImageAttachmentFilter',
'JournalEntryFilter',
'ObjectChangeFilter',
'SavedFilterFilter',
'TagFilter',
'WebhookFilter',
)
@strawberry_django.filter(models.ConfigContext, lookups=True)
@autotype_decorator(filtersets.ConfigContextFilterSet)
class ConfigContextFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ConfigTemplate, lookups=True)
@autotype_decorator(filtersets.ConfigTemplateFilterSet)
class ConfigTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.CustomField, lookups=True)
@autotype_decorator(filtersets.CustomFieldFilterSet)
class CustomFieldFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True)
@autotype_decorator(filtersets.CustomFieldChoiceSetFilterSet)
class CustomFieldChoiceSetFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.CustomLink, lookups=True)
@autotype_decorator(filtersets.CustomLinkFilterSet)
class CustomLinkFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ExportTemplate, lookups=True)
@autotype_decorator(filtersets.ExportTemplateFilterSet)
class ExportTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ImageAttachment, lookups=True)
@autotype_decorator(filtersets.ImageAttachmentFilterSet)
class ImageAttachmentFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.JournalEntry, lookups=True)
@autotype_decorator(filtersets.JournalEntryFilterSet)
class JournalEntryFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ObjectChange, lookups=True)
@autotype_decorator(filtersets.ObjectChangeFilterSet)
class ObjectChangeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.SavedFilter, lookups=True)
@autotype_decorator(filtersets.SavedFilterFilterSet)
class SavedFilterFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Tag, lookups=True)
@autotype_decorator(filtersets.TagFilterSet)
class TagFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Webhook, lookups=True)
@autotype_decorator(filtersets.WebhookFilterSet)
class WebhookFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.EventRule, lookups=True)
@autotype_decorator(filtersets.EventRuleFilterSet)
class EventRuleFilter(BaseFilterMixin):
pass

View File

@ -1,6 +1,8 @@
import graphene
from typing import TYPE_CHECKING, Annotated, List
import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType
from graphene.types.generic import GenericScalar
from extras.models import ObjectChange
@ -14,56 +16,67 @@ __all__ = (
'TagsMixin',
)
if TYPE_CHECKING:
from .types import ImageAttachmentType, JournalEntryType, ObjectChangeType, TagType
from tenancy.graphql.types import ContactAssignmentType
@strawberry.type
class ChangelogMixin:
changelog = graphene.List('extras.graphql.types.ObjectChangeType')
def resolve_changelog(self, info):
@strawberry_django.field
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
content_type = ContentType.objects.get_for_model(self)
object_changes = ObjectChange.objects.filter(
changed_object_type=content_type,
changed_object_id=self.pk
)
return object_changes.restrict(info.context.user, 'view')
return object_changes.restrict(info.context.request.user, 'view')
@strawberry.type
class ConfigContextMixin:
config_context = GenericScalar()
def resolve_config_context(self, info):
@strawberry_django.field
def config_context(self) -> strawberry.scalars.JSON:
return self.get_config_context()
@strawberry.type
class CustomFieldsMixin:
custom_fields = GenericScalar()
def resolve_custom_fields(self, info):
@strawberry_django.field
def custom_fields(self) -> strawberry.scalars.JSON:
return self.custom_field_data
@strawberry.type
class ImageAttachmentsMixin:
image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType')
def resolve_image_attachments(self, info):
return self.images.restrict(info.context.user, 'view')
@strawberry_django.field
def image_attachments(self, info) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]:
return self.images.restrict(info.context.request.user, 'view')
@strawberry.type
class JournalEntriesMixin:
journal_entries = graphene.List('extras.graphql.types.JournalEntryType')
def resolve_journal_entries(self, info):
return self.journal_entries.restrict(info.context.user, 'view')
@strawberry_django.field
def journal_entries(self, info) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]:
return self.journal_entries.all()
@strawberry.type
class TagsMixin:
tags = graphene.List('extras.graphql.types.TagType')
def resolve_tags(self, info):
@strawberry_django.field
def tags(self) -> List[Annotated["TagType", strawberry.lazy('.types')]]:
return self.tags.all()
@strawberry.type
class ContactsMixin:
contacts = graphene.List('tenancy.graphql.types.ContactAssignmentType')
def resolve_contacts(self, info):
@strawberry_django.field
def contacts(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]:
return list(self.contacts.all())

View File

@ -1,80 +1,70 @@
import graphene
from typing import List
import strawberry
import strawberry_django
from extras import models
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from utilities.graphql_optimizer import gql_query_optimizer
class ExtrasQuery(graphene.ObjectType):
config_context = ObjectField(ConfigContextType)
config_context_list = ObjectListField(ConfigContextType)
@strawberry.type
class ExtrasQuery:
@strawberry.field
def config_context(self, id: int) -> ConfigContextType:
return models.ConfigContext.objects.get(pk=id)
config_context_list: List[ConfigContextType] = strawberry_django.field()
def resolve_config_context_list(root, info, **kwargs):
return gql_query_optimizer(models.ConfigContext.objects.all(), info)
@strawberry.field
def config_template(self, id: int) -> ConfigTemplateType:
return models.ConfigTemplate.objects.get(pk=id)
config_template_list: List[ConfigTemplateType] = strawberry_django.field()
config_template = ObjectField(ConfigTemplateType)
config_template_list = ObjectListField(ConfigTemplateType)
@strawberry.field
def custom_field(self, id: int) -> CustomFieldType:
return models.CustomField.objects.get(pk=id)
custom_field_list: List[CustomFieldType] = strawberry_django.field()
def resolve_config_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ConfigTemplate.objects.all(), info)
@strawberry.field
def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType:
return models.CustomFieldChoiceSet.objects.get(pk=id)
custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field()
custom_field = ObjectField(CustomFieldType)
custom_field_list = ObjectListField(CustomFieldType)
@strawberry.field
def custom_link(self, id: int) -> CustomLinkType:
return models.CustomLink.objects.get(pk=id)
custom_link_list: List[CustomLinkType] = strawberry_django.field()
def resolve_custom_field_list(root, info, **kwargs):
return gql_query_optimizer(models.CustomField.objects.all(), info)
@strawberry.field
def export_template(self, id: int) -> ExportTemplateType:
return models.ExportTemplate.objects.get(pk=id)
export_template_list: List[ExportTemplateType] = strawberry_django.field()
custom_field_choice_set = ObjectField(CustomFieldChoiceSetType)
custom_field_choice_set_list = ObjectListField(CustomFieldChoiceSetType)
@strawberry.field
def image_attachment(self, id: int) -> ImageAttachmentType:
return models.ImageAttachment.objects.get(pk=id)
image_attachment_list: List[ImageAttachmentType] = strawberry_django.field()
def resolve_custom_field_choices_list(root, info, **kwargs):
return gql_query_optimizer(models.CustomFieldChoiceSet.objects.all(), info)
@strawberry.field
def saved_filter(self, id: int) -> SavedFilterType:
return models.SavedFilter.objects.get(pk=id)
saved_filter_list: List[SavedFilterType] = strawberry_django.field()
custom_link = ObjectField(CustomLinkType)
custom_link_list = ObjectListField(CustomLinkType)
@strawberry.field
def journal_entry(self, id: int) -> JournalEntryType:
return models.JournalEntry.objects.get(pk=id)
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
def resolve_custom_link_list(root, info, **kwargs):
return gql_query_optimizer(models.CustomLink.objects.all(), info)
@strawberry.field
def tag(self, id: int) -> TagType:
return models.Tag.objects.get(pk=id)
tag_list: List[TagType] = strawberry_django.field()
export_template = ObjectField(ExportTemplateType)
export_template_list = ObjectListField(ExportTemplateType)
@strawberry.field
def webhook(self, id: int) -> WebhookType:
return models.Webhook.objects.get(pk=id)
webhook_list: List[WebhookType] = strawberry_django.field()
def resolve_export_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ExportTemplate.objects.all(), info)
image_attachment = ObjectField(ImageAttachmentType)
image_attachment_list = ObjectListField(ImageAttachmentType)
def resolve_image_attachment_list(root, info, **kwargs):
return gql_query_optimizer(models.ImageAttachment.objects.all(), info)
saved_filter = ObjectField(SavedFilterType)
saved_filter_list = ObjectListField(SavedFilterType)
def resolve_saved_filter_list(root, info, **kwargs):
return gql_query_optimizer(models.SavedFilter.objects.all(), info)
journal_entry = ObjectField(JournalEntryType)
journal_entry_list = ObjectListField(JournalEntryType)
def resolve_journal_entry_list(root, info, **kwargs):
return gql_query_optimizer(models.JournalEntry.objects.all(), info)
tag = ObjectField(TagType)
tag_list = ObjectListField(TagType)
def resolve_tag_list(root, info, **kwargs):
return gql_query_optimizer(models.Tag.objects.all(), info)
webhook = ObjectField(WebhookType)
webhook_list = ObjectListField(WebhookType)
def resolve_webhook_list(root, info, **kwargs):
return gql_query_optimizer(models.Webhook.objects.all(), info)
event_rule = ObjectField(EventRuleType)
event_rule_list = ObjectListField(EventRuleType)
def resolve_eventrule_list(root, info, **kwargs):
return gql_query_optimizer(models.EventRule.objects.all(), info)
@strawberry.field
def event_rule(self, id: int) -> EventRuleType:
return models.EventRule.objects.get(pk=id)
event_rule_list: List[EventRuleType] = strawberry_django.field()

View File

@ -1,6 +1,12 @@
from extras import filtersets, models
from typing import Annotated, List
import strawberry
import strawberry_django
from extras import models
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType
from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, OrganizationalObjectType
from .filters import *
__all__ = (
'ConfigContextType',
@ -19,104 +25,202 @@ __all__ = (
)
@strawberry_django.type(
models.ConfigContext,
fields='__all__',
filters=ConfigContextFilter
)
class ConfigContextType(ObjectType):
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
class Meta:
model = models.ConfigContext
fields = '__all__'
filterset_class = filtersets.ConfigContextFilterSet
@strawberry_django.field
def roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]:
return self.roles.all()
@strawberry_django.field
def device_types(self) -> List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]:
return self.device_types.all()
@strawberry_django.field
def tags(self) -> List[Annotated["TagType", strawberry.lazy('extras.graphql.types')]]:
return self.tags.all()
@strawberry_django.field
def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]:
return self.platforms.all()
@strawberry_django.field
def regions(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]:
return self.regions.all()
@strawberry_django.field
def cluster_groups(self) -> List[Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')]]:
return self.cluster_groups.all()
@strawberry_django.field
def tenant_groups(self) -> List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]:
return self.tenant_groups.all()
@strawberry_django.field
def cluster_types(self) -> List[Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')]]:
return self.cluster_types.all()
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self.clusters.all()
@strawberry_django.field
def locations(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]:
return self.locations.all()
@strawberry_django.field
def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]:
return self.sites.all()
@strawberry_django.field
def tenants(self) -> List[Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')]]:
return self.tenants.all()
@strawberry_django.field
def site_groups(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]:
return self.site_groups.all()
@strawberry_django.type(
models.ConfigTemplate,
fields='__all__',
filters=ConfigTemplateFilter
)
class ConfigTemplateType(TagsMixin, ObjectType):
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
class Meta:
model = models.ConfigTemplate
fields = '__all__'
filterset_class = filtersets.ConfigTemplateFilterSet
@strawberry_django.field
def virtualmachines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]:
return self.virtualmachines.all()
@strawberry_django.field
def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]:
return self.devices.all()
@strawberry_django.field
def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]:
return self.platforms.all()
@strawberry_django.field
def device_roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]:
return self.device_roles.all()
@strawberry_django.type(
models.CustomField,
fields='__all__',
filters=CustomFieldFilter
)
class CustomFieldType(ObjectType):
class Meta:
model = models.CustomField
fields = '__all__'
filterset_class = filtersets.CustomFieldFilterSet
related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None
@strawberry_django.type(
models.CustomFieldChoiceSet,
exclude=('extra_choices', ),
filters=CustomFieldChoiceSetFilter
)
class CustomFieldChoiceSetType(ObjectType):
class Meta:
model = models.CustomFieldChoiceSet
fields = '__all__'
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
@strawberry_django.field
def choices_for(self) -> List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]:
return self.choices_for.all()
@strawberry_django.field
def extra_choices(self) -> List[str] | None:
return list(self.extra_choices)
@strawberry_django.type(
models.CustomLink,
fields='__all__',
filters=CustomLinkFilter
)
class CustomLinkType(ObjectType):
class Meta:
model = models.CustomLink
fields = '__all__'
filterset_class = filtersets.CustomLinkFilterSet
class EventRuleType(OrganizationalObjectType):
class Meta:
model = models.EventRule
fields = '__all__'
filterset_class = filtersets.EventRuleFilterSet
pass
@strawberry_django.type(
models.ExportTemplate,
fields='__all__',
filters=ExportTemplateFilter
)
class ExportTemplateType(ObjectType):
class Meta:
model = models.ExportTemplate
fields = '__all__'
filterset_class = filtersets.ExportTemplateFilterSet
data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None
data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None
@strawberry_django.type(
models.ImageAttachment,
fields='__all__',
filters=ImageAttachmentFilter
)
class ImageAttachmentType(BaseObjectType):
class Meta:
model = models.ImageAttachment
fields = '__all__'
filterset_class = filtersets.ImageAttachmentFilterSet
object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
@strawberry_django.type(
models.JournalEntry,
fields='__all__',
filters=JournalEntryFilter
)
class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
class Meta:
model = models.JournalEntry
fields = '__all__'
filterset_class = filtersets.JournalEntryFilterSet
assigned_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
models.ObjectChange,
fields='__all__',
filters=ObjectChangeFilter
)
class ObjectChangeType(BaseObjectType):
class Meta:
model = models.ObjectChange
fields = '__all__'
filterset_class = filtersets.ObjectChangeFilterSet
pass
@strawberry_django.type(
models.SavedFilter,
exclude=['content_types',],
filters=SavedFilterFilter
)
class SavedFilterType(ObjectType):
class Meta:
model = models.SavedFilter
fields = '__all__'
filterset_class = filtersets.SavedFilterFilterSet
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
@strawberry_django.type(
models.Tag,
exclude=['extras_taggeditem_items', ],
filters=TagFilter
)
class TagType(ObjectType):
color: str
class Meta:
model = models.Tag
exclude = ('extras_taggeditem_items',)
filterset_class = filtersets.TagFilterSet
@strawberry_django.field
def object_types(self) -> List[ContentTypeType]:
return self.object_types.all()
@strawberry_django.type(
models.Webhook,
exclude=['content_types',],
filters=WebhookFilter
)
class WebhookType(OrganizationalObjectType):
pass
class Meta:
model = models.Webhook
filterset_class = filtersets.WebhookFilterSet
@strawberry_django.type(
models.EventRule,
exclude=['content_types',],
filters=EventRuleFilter
)
class EventRuleType(OrganizationalObjectType):
action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None

View File

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

View File

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

View File

@ -22,8 +22,10 @@ from netbox.models import ChangeLoggedModel
from netbox.models.features import (
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.utils import clean_html, dict_to_querydict, render_jinja2
from utilities.jinja2 import render_jinja2
__all__ = (
'Bookmark',

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import importlib
import logging
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.signals import m2m_changed, post_save, pre_delete
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.events import process_event_rules
from extras.models import EventRule
from extras.validators import run_validators
from netbox.config import get_config
from netbox.context import current_request, events_queue
from netbox.models.features import ChangeLoggingMixin
@ -22,6 +22,30 @@ from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices
from .events import enqueue_object, get_snapshots, serialize_for_event
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

View File

@ -5,7 +5,7 @@ from circuits.api.serializers import ProviderSerializer
from circuits.forms import ProviderForm
from circuits.models import Provider
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

View File

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

View File

@ -3,11 +3,13 @@ from django.core.exceptions import ValidationError
from django.db import transaction
from django.test import TestCase, override_settings
from ipam.models import ASN, RIR
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.validators import CustomValidator
from ipam.models import ASN, RIR
from users.models import User
from utilities.exceptions import AbortRequest
from utilities.request import NetBoxFakeRequest
class MyValidator(CustomValidator):
@ -79,6 +81,13 @@ prohibited_validator = CustomValidator({
}
})
request_validator = CustomValidator({
'request.user.username': {
'eq': 'Bob'
}
})
custom_validator = MyValidator()
@ -154,6 +163,28 @@ class CustomValidatorTest(TestCase):
def test_custom_valid(self):
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):

View File

@ -1,4 +1,5 @@
import importlib
import inspect
import operator
from django.core import validators
from django.core.exceptions import ValidationError
@ -74,6 +75,8 @@ class CustomValidator:
:param validation_rules: A dictionary mapping object attributes to validation rules
"""
REQUEST_TOKEN = 'request'
VALIDATORS = {
'eq': IsEqualValidator,
'neq': IsNotEqualValidator,
@ -88,25 +91,56 @@ class CustomValidator:
def __init__(self, validation_rules=None):
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):
# Validate instance attributes per validation rules
for attr_name, rules in self.validation_rules.items():
attr = self._getattr(instance, attr_name)
def __call__(self, instance, request=None):
"""
Validate the instance and (optional) request against the validation rule(s).
"""
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():
validator = self.get_validator(descriptor, value)
try:
validator(attr)
except ValidationError as exc:
# Re-package the raised ValidationError to associate it with the specific attr
raise ValidationError({attr_name: exc})
raise ValidationError(
_("Custom validation failed for {attribute}: {exception}").format(
attribute=attr_path, exception=exc
)
)
# 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
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
m2m_fields = [f.name for f in instance._meta.local_many_to_many]
if name in m2m_fields:
@ -137,7 +171,7 @@ class CustomValidator:
validator_cls = self.VALIDATORS.get(descriptor)
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
raise a ValidationError exception.
@ -151,21 +185,3 @@ class CustomValidator:
if field is not None:
raise ValidationError({field: 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,13 +18,16 @@ from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from netbox.views.generic.mixins import TableMixin
from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import render_partial
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.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, register_model_view
from utilities.views import ContentTypePermissionRequiredMixin, get_viewname, register_model_view
from . import filtersets, forms, tables
from .models import *
from .scripts import run_script
@ -760,7 +763,6 @@ class ImageAttachmentListView(generic.ObjectListView):
class ImageAttachmentEditView(generic.ObjectEditView):
queryset = ImageAttachment.objects.all()
form = forms.ImageAttachmentForm
template_name = 'extras/imageattachment_edit.html'
def alter_object(self, instance, request, args, kwargs):
if not instance.pk:

View File

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

View File

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

View File

@ -16,6 +16,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
from utilities.forms.widgets import DatePicker
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
@ -56,9 +57,9 @@ class VRFForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('VRF'), ('name', 'rd', 'enforce_unique', 'description', 'tags')),
(_('Route Targets'), ('import_targets', 'export_targets')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -74,8 +75,8 @@ class VRFForm(TenancyForm, NetBoxModelForm):
class RouteTargetForm(TenancyForm, NetBoxModelForm):
fieldsets = (
('Route Target', ('name', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
FieldSet('name', 'description', 'tags', name=_('Route Target')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
comments = CommentField()
@ -90,9 +91,7 @@ class RIRForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
(_('RIR'), (
'name', 'slug', 'is_private', 'description', 'tags',
)),
FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
)
class Meta:
@ -110,8 +109,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('Aggregate'), ('prefix', 'rir', 'date_added', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -131,8 +130,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
(_('ASN Range'), ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -155,8 +154,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('ASN'), ('asn', 'rir', 'sites', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -184,9 +183,7 @@ class RoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
(_('Role'), (
'name', 'slug', 'weight', 'description', 'tags',
)),
FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
)
class Meta:
@ -226,9 +223,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('Prefix'), ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
(_('Site/VLAN Assignment'), ('site', 'vlan')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet(
'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
),
FieldSet('site', 'vlan', name=_('Site/VLAN Assignment')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -253,8 +252,11 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('IP Range'), ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
FieldSet(
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags',
name=_('IP Range')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
@ -307,6 +309,20 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
)
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:
model = IPAddress
fields = [
@ -443,9 +459,9 @@ class FHRPGroupForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('FHRP Group'), ('protocol', 'group_id', 'name', 'description', 'tags')),
(_('Authentication'), ('auth_type', 'auth_key')),
(_('Virtual IP Address'), ('ip_vrf', 'ip_address', 'ip_status'))
FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
FieldSet('auth_type', 'auth_key', name=_('Authentication')),
FieldSet('ip_vrf', 'ip_address', 'ip_status', name=_('Virtual IP Address'))
)
class Meta:
@ -502,6 +518,10 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
queryset=FHRPGroup.objects.all()
)
fieldsets = (
FieldSet(ObjectAttribute('interface'), 'group', 'priority'),
)
class Meta:
model = FHRPGroupAssignment
fields = ('group', 'priority')
@ -587,9 +607,12 @@ class VLANGroupForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
(_('VLAN Group'), ('name', 'slug', 'description', 'tags')),
(_('Child VLANs'), ('min_vid', 'max_vid')),
(_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
name=_('Scope')
),
)
class Meta:
@ -662,9 +685,7 @@ class ServiceTemplateForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
(_('Service Template'), (
'name', 'protocol', 'ports', 'description', 'tags',
)),
FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Service Template')),
)
class Meta:
@ -704,6 +725,18 @@ class ServiceForm(NetBoxModelForm):
)
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:
model = Service
fields = [
@ -718,6 +751,20 @@ class ServiceCreateForm(ServiceForm):
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):
fields = [
'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',

View File

@ -0,0 +1,119 @@
import strawberry_django
from ipam import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'ASNFilter',
'ASNRangeFilter',
'AggregateFilter',
'FHRPGroupFilter',
'FHRPGroupAssignmentFilter',
'IPAddressFilter',
'IPRangeFilter',
'PrefixFilter',
'RIRFilter',
'RoleFilter',
'RouteTargetFilter',
'ServiceFilter',
'ServiceTemplateFilter',
'VLANFilter',
'VLANGroupFilter',
'VRFFilter',
)
@strawberry_django.filter(models.ASN, lookups=True)
@autotype_decorator(filtersets.ASNFilterSet)
class ASNFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ASNRange, lookups=True)
@autotype_decorator(filtersets.ASNRangeFilterSet)
class ASNRangeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Aggregate, lookups=True)
@autotype_decorator(filtersets.AggregateFilterSet)
class AggregateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.FHRPGroup, lookups=True)
@autotype_decorator(filtersets.FHRPGroupFilterSet)
class FHRPGroupFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True)
@autotype_decorator(filtersets.FHRPGroupAssignmentFilterSet)
class FHRPGroupAssignmentFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.IPAddress, lookups=True)
@autotype_decorator(filtersets.IPAddressFilterSet)
class IPAddressFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.IPRange, lookups=True)
@autotype_decorator(filtersets.IPRangeFilterSet)
class IPRangeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Prefix, lookups=True)
@autotype_decorator(filtersets.PrefixFilterSet)
class PrefixFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RIR, lookups=True)
@autotype_decorator(filtersets.RIRFilterSet)
class RIRFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Role, lookups=True)
@autotype_decorator(filtersets.RoleFilterSet)
class RoleFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.RouteTarget, lookups=True)
@autotype_decorator(filtersets.RouteTargetFilterSet)
class RouteTargetFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.Service, lookups=True)
@autotype_decorator(filtersets.ServiceFilterSet)
class ServiceFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.ServiceTemplate, lookups=True)
@autotype_decorator(filtersets.ServiceTemplateFilterSet)
class ServiceTemplateFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VLAN, lookups=True)
@autotype_decorator(filtersets.VLANFilterSet)
class VLANFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VLANGroup, lookups=True)
@autotype_decorator(filtersets.VLANGroupFilterSet)
class VLANGroupFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VRF, lookups=True)
@autotype_decorator(filtersets.VRFFilterSet)
class VRFFilter(BaseFilterMixin):
pass

View File

@ -1,95 +0,0 @@
import graphene
from dcim.graphql.types import (
InterfaceType,
LocationType,
RackType,
RegionType,
SiteGroupType,
SiteType,
)
from dcim.models import Interface, Location, Rack, Region, Site, SiteGroup
from ipam.graphql.types import FHRPGroupType, VLANType
from ipam.models import VLAN, FHRPGroup
from virtualization.graphql.types import ClusterGroupType, ClusterType, VMInterfaceType
from virtualization.models import Cluster, ClusterGroup, VMInterface
class IPAddressAssignmentType(graphene.Union):
class Meta:
types = (
InterfaceType,
FHRPGroupType,
VMInterfaceType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is Interface:
return InterfaceType
if type(instance) is FHRPGroup:
return FHRPGroupType
if type(instance) is VMInterface:
return VMInterfaceType
class L2VPNAssignmentType(graphene.Union):
class Meta:
types = (
InterfaceType,
VLANType,
VMInterfaceType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is Interface:
return InterfaceType
if type(instance) is VLAN:
return VLANType
if type(instance) is VMInterface:
return VMInterfaceType
class FHRPGroupInterfaceType(graphene.Union):
class Meta:
types = (
InterfaceType,
VMInterfaceType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is Interface:
return InterfaceType
if type(instance) is VMInterface:
return VMInterfaceType
class VLANGroupScopeType(graphene.Union):
class Meta:
types = (
ClusterType,
ClusterGroupType,
LocationType,
RackType,
RegionType,
SiteType,
SiteGroupType,
)
@classmethod
def resolve_type(cls, instance, info):
if type(instance) is Cluster:
return ClusterType
if type(instance) is ClusterGroup:
return ClusterGroupType
if type(instance) is Location:
return LocationType
if type(instance) is Rack:
return RackType
if type(instance) is Region:
return RegionType
if type(instance) is Site:
return SiteType
if type(instance) is SiteGroup:
return SiteGroupType

View File

@ -1,4 +1,7 @@
import graphene
from typing import Annotated, List
import strawberry
import strawberry_django
__all__ = (
'IPAddressesMixin',
@ -6,15 +9,15 @@ __all__ = (
)
@strawberry.type
class IPAddressesMixin:
ip_addresses = graphene.List('ipam.graphql.types.IPAddressType')
def resolve_ip_addresses(self, info):
return self.ip_addresses.restrict(info.context.user, 'view')
@strawberry_django.field
def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]:
return self.ip_addresses.all()
@strawberry.type
class VLANGroupsMixin:
vlan_groups = graphene.List('ipam.graphql.types.VLANGroupType')
def resolve_vlan_groups(self, info):
return self.vlan_groups.restrict(info.context.user, 'view')
@strawberry_django.field
def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]:
return self.vlan_groups.all()

View File

@ -1,104 +1,90 @@
import graphene
from typing import List
import strawberry
import strawberry_django
from ipam import models
from netbox.graphql.fields import ObjectField, ObjectListField
from utilities.graphql_optimizer import gql_query_optimizer
from .types import *
class IPAMQuery(graphene.ObjectType):
asn = ObjectField(ASNType)
asn_list = ObjectListField(ASNType)
@strawberry.type
class IPAMQuery:
@strawberry.field
def asn(self, id: int) -> ASNType:
return models.ASN.objects.get(pk=id)
asn_list: List[ASNType] = strawberry_django.field()
def resolve_asn_list(root, info, **kwargs):
return gql_query_optimizer(models.ASN.objects.all(), info)
@strawberry.field
def asn_range(self, id: int) -> ASNRangeType:
return models.ASNRange.objects.get(pk=id)
asn_range_list: List[ASNRangeType] = strawberry_django.field()
asn_range = ObjectField(ASNRangeType)
asn_range_list = ObjectListField(ASNRangeType)
@strawberry.field
def aggregate(self, id: int) -> AggregateType:
return models.Aggregate.objects.get(pk=id)
aggregate_list: List[AggregateType] = strawberry_django.field()
def resolve_asn_range_list(root, info, **kwargs):
return gql_query_optimizer(models.ASNRange.objects.all(), info)
@strawberry.field
def ip_address(self, id: int) -> IPAddressType:
return models.IPAddress.objects.get(pk=id)
ip_address_list: List[IPAddressType] = strawberry_django.field()
aggregate = ObjectField(AggregateType)
aggregate_list = ObjectListField(AggregateType)
@strawberry.field
def ip_range(self, id: int) -> IPRangeType:
return models.IPRange.objects.get(pk=id)
ip_range_list: List[IPRangeType] = strawberry_django.field()
def resolve_aggregate_list(root, info, **kwargs):
return gql_query_optimizer(models.Aggregate.objects.all(), info)
@strawberry.field
def prefix(self, id: int) -> PrefixType:
return models.Prefix.objects.get(pk=id)
prefix_list: List[PrefixType] = strawberry_django.field()
ip_address = ObjectField(IPAddressType)
ip_address_list = ObjectListField(IPAddressType)
@strawberry.field
def rir(self, id: int) -> RIRType:
return models.RIR.objects.get(pk=id)
rir_list: List[RIRType] = strawberry_django.field()
def resolve_ip_address_list(root, info, **kwargs):
return gql_query_optimizer(models.IPAddress.objects.all(), info)
@strawberry.field
def role(self, id: int) -> RoleType:
return models.Role.objects.get(pk=id)
role_list: List[RoleType] = strawberry_django.field()
ip_range = ObjectField(IPRangeType)
ip_range_list = ObjectListField(IPRangeType)
@strawberry.field
def route_target(self, id: int) -> RouteTargetType:
return models.RouteTarget.objects.get(pk=id)
route_target_list: List[RouteTargetType] = strawberry_django.field()
def resolve_ip_range_list(root, info, **kwargs):
return gql_query_optimizer(models.IPRange.objects.all(), info)
@strawberry.field
def service(self, id: int) -> ServiceType:
return models.Service.objects.get(pk=id)
service_list: List[ServiceType] = strawberry_django.field()
prefix = ObjectField(PrefixType)
prefix_list = ObjectListField(PrefixType)
@strawberry.field
def service_template(self, id: int) -> ServiceTemplateType:
return models.ServiceTemplate.objects.get(pk=id)
service_template_list: List[ServiceTemplateType] = strawberry_django.field()
def resolve_prefix_list(root, info, **kwargs):
return gql_query_optimizer(models.Prefix.objects.all(), info)
@strawberry.field
def fhrp_group(self, id: int) -> FHRPGroupType:
return models.FHRPGroup.objects.get(pk=id)
fhrp_group_list: List[FHRPGroupType] = strawberry_django.field()
rir = ObjectField(RIRType)
rir_list = ObjectListField(RIRType)
@strawberry.field
def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType:
return models.FHRPGroupAssignment.objects.get(pk=id)
fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field()
def resolve_rir_list(root, info, **kwargs):
return gql_query_optimizer(models.RIR.objects.all(), info)
@strawberry.field
def vlan(self, id: int) -> VLANType:
return models.VLAN.objects.get(pk=id)
vlan_list: List[VLANType] = strawberry_django.field()
role = ObjectField(RoleType)
role_list = ObjectListField(RoleType)
@strawberry.field
def vlan_group(self, id: int) -> VLANGroupType:
return models.VLANGroup.objects.get(pk=id)
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
def resolve_role_list(root, info, **kwargs):
return gql_query_optimizer(models.Role.objects.all(), info)
route_target = ObjectField(RouteTargetType)
route_target_list = ObjectListField(RouteTargetType)
def resolve_route_target_list(root, info, **kwargs):
return gql_query_optimizer(models.RouteTarget.objects.all(), info)
service = ObjectField(ServiceType)
service_list = ObjectListField(ServiceType)
def resolve_service_list(root, info, **kwargs):
return gql_query_optimizer(models.Service.objects.all(), info)
service_template = ObjectField(ServiceTemplateType)
service_template_list = ObjectListField(ServiceTemplateType)
def resolve_service_template_list(root, info, **kwargs):
return gql_query_optimizer(models.ServiceTemplate.objects.all(), info)
fhrp_group = ObjectField(FHRPGroupType)
fhrp_group_list = ObjectListField(FHRPGroupType)
def resolve_fhrp_group_list(root, info, **kwargs):
return gql_query_optimizer(models.FHRPGroup.objects.all(), info)
fhrp_group_assignment = ObjectField(FHRPGroupAssignmentType)
fhrp_group_assignment_list = ObjectListField(FHRPGroupAssignmentType)
def resolve_fhrp_group_assignment_list(root, info, **kwargs):
return gql_query_optimizer(models.FHRPGroupAssignment.objects.all(), info)
vlan = ObjectField(VLANType)
vlan_list = ObjectListField(VLANType)
def resolve_vlan_list(root, info, **kwargs):
return gql_query_optimizer(models.VLAN.objects.all(), info)
vlan_group = ObjectField(VLANGroupType)
vlan_group_list = ObjectListField(VLANGroupType)
def resolve_vlan_group_list(root, info, **kwargs):
return gql_query_optimizer(models.VLANGroup.objects.all(), info)
vrf = ObjectField(VRFType)
vrf_list = ObjectListField(VRFType)
def resolve_vrf_list(root, info, **kwargs):
return gql_query_optimizer(models.VRF.objects.all(), info)
@strawberry.field
def vrf(self, id: int) -> VRFType:
return models.VRF.objects.get(pk=id)
vrf_list: List[VRFType] = strawberry_django.field()

View File

@ -1,9 +1,15 @@
import graphene
from typing import Annotated, List, Union
from ipam import filtersets, models
from .mixins import IPAddressesMixin
import strawberry
import strawberry_django
from circuits.graphql.types import ProviderType
from dcim.graphql.types import SiteType
from ipam import models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
from .filters import *
from .mixins import IPAddressesMixin
__all__ = (
'ASNType',
@ -25,164 +31,335 @@ __all__ = (
)
class IPAddressFamilyType(graphene.ObjectType):
value = graphene.Int()
label = graphene.String()
def __init__(self, value):
self.value = value
self.label = f'IPv{value}'
@strawberry.type
class IPAddressFamilyType:
value: int
label: str
@strawberry.type
class BaseIPAddressFamilyType:
"""
Base type for models that need to expose their IPAddress family type.
"""
family = graphene.Field(IPAddressFamilyType)
def resolve_family(self, _):
@strawberry.field
def family(self) -> IPAddressFamilyType:
# Note that self, is an instance of models.IPAddress
# thus resolves to the address family value.
return IPAddressFamilyType(self.family)
return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}')
@strawberry_django.type(
models.ASN,
fields='__all__',
filters=ASNFilter
)
class ASNType(NetBoxObjectType):
asn = graphene.Field(BigInt)
asn: BigInt
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
class Meta:
model = models.ASN
fields = '__all__'
filterset_class = filtersets.ASNFilterSet
@strawberry_django.field
def sites(self) -> List[SiteType]:
return self.sites.all()
@strawberry_django.field
def providers(self) -> List[ProviderType]:
return self.providers.all()
@strawberry_django.type(
models.ASNRange,
fields='__all__',
filters=ASNRangeFilter
)
class ASNRangeType(NetBoxObjectType):
class Meta:
model = models.ASNRange
fields = '__all__'
filterset_class = filtersets.ASNRangeFilterSet
start: BigInt
end: BigInt
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@strawberry_django.type(
models.Aggregate,
fields='__all__',
filters=AggregateFilter
)
class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
class Meta:
model = models.Aggregate
fields = '__all__'
filterset_class = filtersets.AggregateFilterSet
prefix: str
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@strawberry_django.type(
models.FHRPGroup,
fields='__all__',
filters=FHRPGroupFilter
)
class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
class Meta:
model = models.FHRPGroup
fields = '__all__'
filterset_class = filtersets.FHRPGroupFilterSet
def resolve_auth_type(self, info):
return self.auth_type or None
@strawberry_django.field
def fhrpgroupassignment_set(self) -> List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]:
return self.fhrpgroupassignment_set.all()
@strawberry_django.type(
models.FHRPGroupAssignment,
exclude=('interface_type', 'interface_id'),
filters=FHRPGroupAssignmentFilter
)
class FHRPGroupAssignmentType(BaseObjectType):
interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType')
group: Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')]
class Meta:
model = models.FHRPGroupAssignment
exclude = ('interface_type', 'interface_id')
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
@strawberry_django.field
def interface(self) -> Annotated[Union[
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
], strawberry.union("FHRPGroupInterfaceType")]:
return self.interface
@strawberry_django.type(
models.IPAddress,
exclude=('assigned_object_type', 'assigned_object_id', 'address'),
filters=IPAddressFilter
)
class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType):
assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType')
address: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
nat_inside: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
class Meta:
model = models.IPAddress
exclude = ('assigned_object_type', 'assigned_object_id')
filterset_class = filtersets.IPAddressFilterSet
@strawberry_django.field
def nat_outside(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]:
return self.nat_outside.all()
def resolve_role(self, info):
return self.role or None
@strawberry_django.field
def tunnel_terminations(self) -> List[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]:
return self.tunnel_terminations.all()
@strawberry_django.field
def services(self) -> List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]:
return self.services.all()
@strawberry_django.field
def assigned_object(self) -> Annotated[Union[
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')],
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
], strawberry.union("IPAddressAssignmentType")]:
return self.assigned_object
@strawberry_django.type(
models.IPRange,
fields='__all__',
filters=IPRangeFilter
)
class IPRangeType(NetBoxObjectType):
class Meta:
model = models.IPRange
fields = '__all__'
filterset_class = filtersets.IPRangeFilterSet
def resolve_role(self, info):
return self.role or None
start_address: str
end_address: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
@strawberry_django.type(
models.Prefix,
fields='__all__',
filters=PrefixFilter
)
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
class Meta:
model = models.Prefix
fields = '__all__'
filterset_class = filtersets.PrefixFilterSet
prefix: str
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
@strawberry_django.type(
models.RIR,
fields='__all__',
filters=RIRFilter
)
class RIRType(OrganizationalObjectType):
class Meta:
model = models.RIR
fields = '__all__'
filterset_class = filtersets.RIRFilterSet
@strawberry_django.field
def asn_ranges(self) -> List[Annotated["ASNRangeType", strawberry.lazy('ipam.graphql.types')]]:
return self.asn_ranges.all()
@strawberry_django.field
def asns(self) -> List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]:
return self.asns.all()
@strawberry_django.field
def aggregates(self) -> List[Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')]]:
return self.aggregates.all()
@strawberry_django.type(
models.Role,
fields='__all__',
filters=RoleFilter
)
class RoleType(OrganizationalObjectType):
class Meta:
model = models.Role
fields = '__all__'
filterset_class = filtersets.RoleFilterSet
@strawberry_django.field
def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]:
return self.prefixes.all()
@strawberry_django.field
def ip_ranges(self) -> List[Annotated["IPRangeType", strawberry.lazy('ipam.graphql.types')]]:
return self.ip_ranges.all()
@strawberry_django.field
def vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]:
return self.vlans.all()
@strawberry_django.type(
models.RouteTarget,
fields='__all__',
filters=RouteTargetFilter
)
class RouteTargetType(NetBoxObjectType):
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
class Meta:
model = models.RouteTarget
fields = '__all__'
filterset_class = filtersets.RouteTargetFilterSet
@strawberry_django.field
def exporting_l2vpns(self) -> List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]:
return self.exporting_l2vpns.all()
@strawberry_django.field
def exporting_vrfs(self) -> List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]:
return self.exporting_vrfs.all()
@strawberry_django.field
def importing_vrfs(self) -> List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]:
return self.importing_vrfs.all()
@strawberry_django.field
def importing_l2vpns(self) -> List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]:
return self.importing_l2vpns.all()
@strawberry_django.type(
models.Service,
fields='__all__',
filters=ServiceFilter
)
class ServiceType(NetBoxObjectType):
ports: List[int]
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None
class Meta:
model = models.Service
fields = '__all__'
filterset_class = filtersets.ServiceFilterSet
@strawberry_django.field
def ipaddresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]:
return self.ipaddresses.all()
@strawberry_django.type(
models.ServiceTemplate,
fields='__all__',
filters=ServiceTemplateFilter
)
class ServiceTemplateType(NetBoxObjectType):
class Meta:
model = models.ServiceTemplate
fields = '__all__'
filterset_class = filtersets.ServiceTemplateFilterSet
ports: List[int]
@strawberry_django.type(
models.VLAN,
fields='__all__',
filters=VLANFilter
)
class VLANType(NetBoxObjectType):
site: Annotated["SiteType", strawberry.lazy('ipam.graphql.types')] | None
group: Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
class Meta:
model = models.VLAN
fields = '__all__'
filterset_class = filtersets.VLANFilterSet
@strawberry_django.field
def interfaces_as_untagged(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]:
return self.interfaces_as_untagged.all()
@strawberry_django.field
def vminterfaces_as_untagged(self) -> List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]:
return self.vminterfaces_as_untagged.all()
@strawberry_django.field
def wirelesslan_set(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]:
return self.wirelesslan_set.all()
@strawberry_django.field
def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]:
return self.prefixes.all()
@strawberry_django.field
def interfaces_as_tagged(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]:
return self.interfaces_as_tagged.all()
@strawberry_django.field
def vminterfaces_as_tagged(self) -> List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]:
return self.vminterfaces_as_tagged.all()
@strawberry_django.type(
models.VLANGroup,
exclude=('scope_type', 'scope_id'),
filters=VLANGroupFilter
)
class VLANGroupType(OrganizationalObjectType):
scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType')
class Meta:
model = models.VLANGroup
exclude = ('scope_type', 'scope_id')
filterset_class = filtersets.VLANGroupFilterSet
@strawberry_django.field
def vlans(self) -> List[VLANType]:
return self.vlans.all()
@strawberry_django.field
def scope(self) -> Annotated[Union[
Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')],
Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')],
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RackType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
], strawberry.union("VLANGroupScopeType")]:
return self.scope
@strawberry_django.type(
models.VRF,
fields='__all__',
filters=VRFFilter
)
class VRFType(NetBoxObjectType):
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
class Meta:
model = models.VRF
fields = '__all__'
filterset_class = filtersets.VRFFilterSet
@strawberry_django.field
def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]:
return self.interfaces.all()
@strawberry_django.field
def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]:
return self.ip_addresses.all()
@strawberry_django.field
def vminterfaces(self) -> List[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]]:
return self.vminterfaces.all()
@strawberry_django.field
def ip_ranges(self) -> List[Annotated["IPRangeType", strawberry.lazy('ipam.graphql.types')]]:
return self.ip_ranges.all()
@strawberry_django.field
def export_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]:
return self.export_targets.all()
@strawberry_django.field
def import_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]:
return self.import_targets.all()
@strawberry_django.field
def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]:
return self.prefixes.all()

View File

@ -8,8 +8,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.choices import *
from ipam.constants import *
from netbox.models import PrimaryModel
from utilities.utils import array_to_string
from utilities.data import array_to_string
__all__ = (
'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.functions import Round
from utilities.query import count_related
from utilities.querysets import RestrictedQuerySet
from utilities.utils import count_related
__all__ = (
'ASNRangeQuerySet',

View File

@ -9,8 +9,8 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface
@ -781,7 +781,6 @@ class IPAddressView(generic.ObjectView):
class IPAddressEditView(generic.ObjectEditView):
queryset = IPAddress.objects.all()
form = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
def alter_object(self, obj, request, url_args, url_kwargs):
@ -1059,7 +1058,6 @@ class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
class FHRPGroupAssignmentEditView(generic.ObjectEditView):
queryset = FHRPGroupAssignment.objects.all()
form = forms.FHRPGroupAssignmentForm
template_name = 'ipam/fhrpgroupassignment_edit.html'
def alter_object(self, instance, request, args, kwargs):
if not instance.pk:
@ -1236,14 +1234,12 @@ class ServiceView(generic.ObjectView):
class ServiceCreateView(generic.ObjectEditView):
queryset = Service.objects.all()
form = forms.ServiceCreateForm
template_name = 'ipam/service_create.html'
@register_model_view(Service, 'edit')
class ServiceEditView(generic.ObjectEditView):
queryset = Service.objects.all()
form = forms.ServiceForm
template_name = 'ipam/service_edit.html'
@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 rest_framework import serializers
from core.models import ObjectType
from netbox.api.fields import ContentTypeField
from utilities.api import get_serializer_for_model
from utilities.utils import content_type_identifier
from utilities.object_types import object_type_identifier
__all__ = (
'GenericObjectSerializer',
@ -27,9 +28,9 @@ class GenericObjectSerializer(serializers.Serializer):
return model.objects.get(pk=data['object_id'])
def to_representation(self, instance):
ct = ContentType.objects.get_for_model(instance)
object_type = ObjectType.objects.get_for_model(instance)
data = {
'object_type': content_type_identifier(ct),
'object_type': object_type_identifier(object_type),
'object_id': instance.pk,
}
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.models import Group, ObjectPermission
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()
@ -284,11 +284,9 @@ class RemoteUserBackend(_RemoteUserBackend):
permissions_list = []
for permission_name, constraints in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS.items():
try:
object_type, action = resolve_permission_ct(
permission_name)
# TODO: Merge multiple actions into a single ObjectPermission per content type
obj_perm = ObjectPermission(
actions=[action], constraints=constraints)
object_type, action = resolve_permission_type(permission_name)
# TODO: Merge multiple actions into a single ObjectPermission per object type
obj_perm = ObjectPermission(actions=[action], constraints=constraints)
obj_perm.save()
obj_perm.users.add(user)
obj_perm.object_types.add(object_type)
@ -303,7 +301,9 @@ class RemoteUserBackend(_RemoteUserBackend):
f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}")
else:
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

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.
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.
"""
fieldsets = ()

View File

@ -1,69 +0,0 @@
import graphene
from dcim.fields import MACAddressField, WWNField
from django.db import models
from graphene import Dynamic
from graphene_django.converter import convert_django_field, get_django_field_description
from graphene_django.fields import DjangoConnectionField
from ipam.fields import IPAddressField, IPNetworkField
from taggit.managers import TaggableManager
from .fields import ObjectListField
@convert_django_field.register(TaggableManager)
def convert_field_to_tags_list(field, registry=None):
"""
Register conversion handler for django-taggit's TaggableManager
"""
return graphene.List(graphene.String)
@convert_django_field.register(IPAddressField)
@convert_django_field.register(IPNetworkField)
@convert_django_field.register(MACAddressField)
@convert_django_field.register(WWNField)
def convert_field_to_string(field, registry=None):
# TODO: Update to use get_django_field_description under django_graphene v3.0
return graphene.String(description=field.help_text, required=not field.null)
@convert_django_field.register(models.ManyToManyField)
@convert_django_field.register(models.ManyToManyRel)
@convert_django_field.register(models.ManyToOneRel)
def convert_field_to_list_or_connection(field, registry=None):
"""
From graphene_django.converter.py we need to monkey-patch this to return
our ObjectListField with filtering support instead of DjangoListField
"""
model = field.related_model
def dynamic_type():
_type = registry.get_type_for_model(model)
if not _type:
return
if isinstance(field, models.ManyToManyField):
description = get_django_field_description(field)
else:
description = get_django_field_description(field.field)
# If there is a connection, we should transform the field
# into a DjangoConnectionField
if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are
# defined filter_fields or a filterset_class in the
# DjangoObjectType Meta
if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(_type, required=True, description=description)
return DjangoConnectionField(_type, required=True, description=description)
return ObjectListField(
_type,
required=True, # A Set is always returned, never None.
description=description,
)
return Dynamic(dynamic_type)

View File

@ -1,70 +0,0 @@
from functools import partial
import graphene
from graphene_django import DjangoListField
from .utils import get_graphene_type
__all__ = (
'ObjectField',
'ObjectListField',
)
class ObjectField(graphene.Field):
"""
Retrieve a single object, identified by its numeric ID.
"""
def __init__(self, *args, **kwargs):
if 'id' not in kwargs:
kwargs['id'] = graphene.Int(required=True)
super().__init__(*args, **kwargs)
@staticmethod
def object_resolver(django_object_type, root, info, **args):
"""
Return an object given its numeric ID.
"""
manager = django_object_type._meta.model._default_manager
queryset = django_object_type.get_queryset(manager, info)
return queryset.get(**args)
def get_resolver(self, parent_resolver):
return partial(self.object_resolver, self._type)
class ObjectListField(DjangoListField):
"""
Retrieve a list of objects, optionally filtered by one or more FilterSet filters.
"""
def __init__(self, _type, *args, **kwargs):
filter_kwargs = {}
# Get FilterSet kwargs
filterset_class = getattr(_type._meta, 'filterset_class', None)
if filterset_class:
for filter_name, filter_field in filterset_class.get_filters().items():
field_type = get_graphene_type(type(filter_field))
filter_kwargs[filter_name] = graphene.Argument(field_type)
super().__init__(_type, args=filter_kwargs, *args, **kwargs)
@staticmethod
def list_resolver(django_object_type, resolver, default_manager, root, info, **args):
queryset = super(ObjectListField, ObjectListField).list_resolver(django_object_type, resolver, default_manager, root, info, **args)
# if there are no filter params then don't need to filter
if not args:
return queryset
filterset_class = django_object_type._meta.filterset_class
if filterset_class:
filterset = filterset_class(data=args if args else None, queryset=queryset, request=info.context)
if not filterset.is_valid():
return queryset.none()
return filterset.qs
return queryset

View File

@ -0,0 +1,198 @@
from functools import partial, partialmethod, wraps
from typing import List
import django_filters
import strawberry
import strawberry_django
from strawberry import auto
from ipam.fields import ASNField
from netbox.graphql.scalars import BigInt
from utilities.fields import ColorField, CounterCacheField
from utilities.filters import *
def map_strawberry_type(field):
should_create_function = False
attr_type = None
# NetBox Filter types - put base classes after derived classes
if isinstance(field, ContentTypeFilter):
should_create_function = True
attr_type = str | None
elif isinstance(field, MultiValueArrayFilter):
pass
elif isinstance(field, MultiValueCharFilter):
should_create_function = True
attr_type = List[str] | None
elif isinstance(field, MultiValueDateFilter):
attr_type = auto
elif isinstance(field, MultiValueDateTimeFilter):
attr_type = auto
elif isinstance(field, MultiValueDecimalFilter):
pass
elif isinstance(field, MultiValueMACAddressFilter):
should_create_function = True
attr_type = List[str] | None
elif isinstance(field, MultiValueNumberFilter):
should_create_function = True
attr_type = List[str] | None
elif isinstance(field, MultiValueTimeFilter):
pass
elif isinstance(field, MultiValueWWNFilter):
should_create_function = True
attr_type = List[str] | None
elif isinstance(field, NullableCharFieldFilter):
pass
elif isinstance(field, NumericArrayFilter):
should_create_function = True
attr_type = int
elif isinstance(field, TreeNodeMultipleChoiceFilter):
should_create_function = True
attr_type = List[str] | None
# From django_filters - ordering of these matters as base classes must
# come after derived classes so the base class doesn't get matched first
# a pass for the check (no attr_type) means we don't currently handle
# or use that type
elif issubclass(type(field), django_filters.OrderingFilter):
pass
elif issubclass(type(field), django_filters.BaseRangeFilter):
pass
elif issubclass(type(field), django_filters.BaseInFilter):
pass
elif issubclass(type(field), django_filters.LookupChoiceFilter):
pass
elif issubclass(type(field), django_filters.AllValuesMultipleFilter):
pass
elif issubclass(type(field), django_filters.AllValuesFilter):
pass
elif issubclass(type(field), django_filters.TimeRangeFilter):
pass
elif issubclass(type(field), django_filters.IsoDateTimeFromToRangeFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.DateTimeFromToRangeFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.DateFromToRangeFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.DateRangeFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.RangeFilter):
pass
elif issubclass(type(field), django_filters.NumericRangeFilter):
pass
elif issubclass(type(field), django_filters.NumberFilter):
should_create_function = True
attr_type = int
elif issubclass(type(field), django_filters.ModelMultipleChoiceFilter):
should_create_function = True
attr_type = List[str] | None
elif issubclass(type(field), django_filters.ModelChoiceFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.DurationFilter):
pass
elif issubclass(type(field), django_filters.IsoDateTimeFilter):
pass
elif issubclass(type(field), django_filters.DateTimeFilter):
attr_type = auto
elif issubclass(type(field), django_filters.TimeFilter):
attr_type = auto
elif issubclass(type(field), django_filters.DateFilter):
attr_type = auto
elif issubclass(type(field), django_filters.TypedMultipleChoiceFilter):
pass
elif issubclass(type(field), django_filters.MultipleChoiceFilter):
should_create_function = True
attr_type = List[str] | None
elif issubclass(type(field), django_filters.TypedChoiceFilter):
pass
elif issubclass(type(field), django_filters.ChoiceFilter):
pass
elif issubclass(type(field), django_filters.BooleanFilter):
should_create_function = True
attr_type = bool | None
elif issubclass(type(field), django_filters.UUIDFilter):
should_create_function = True
attr_type = str | None
elif issubclass(type(field), django_filters.CharFilter):
# looks like only used by 'q'
should_create_function = True
attr_type = str | None
return should_create_function, attr_type
def autotype_decorator(filterset):
"""
Decorator used to auto creates a dataclass used by Strawberry based on a filterset.
Must go after the Strawberry decorator as follows:
@strawberry_django.filter(models.Example, lookups=True)
@autotype_decorator(filtersets.ExampleFilterSet)
class ExampleFilter(BaseFilterMixin):
pass
The Filter itself must be derived from BaseFilterMixin. For items listed in meta.fields
of the filterset, usually just a type specifier is generated, so for
`fields = [created, ]` the dataclass would be:
class ExampleFilter(BaseFilterMixin):
created: auto
For other filter fields a function needs to be created for Strawberry with the
naming convention `filter_{fieldname}` which is auto detected and called by
Strawberry, this function uses the filterset to handle the query.
"""
def create_attribute_and_function(cls, fieldname, attr_type, should_create_function):
if fieldname not in cls.__annotations__ and attr_type:
cls.__annotations__[fieldname] = attr_type
filter_name = f"filter_{fieldname}"
if should_create_function and not hasattr(cls, filter_name):
filter_by_filterset = getattr(cls, 'filter_by_filterset')
setattr(cls, filter_name, partialmethod(filter_by_filterset, key=fieldname))
def wrapper(cls):
cls.filterset = filterset
fields = filterset.get_fields()
model = filterset._meta.model
for fieldname in fields.keys():
should_create_function = False
attr_type = auto
if fieldname not in cls.__annotations__:
field = model._meta.get_field(fieldname)
if isinstance(field, CounterCacheField):
should_create_function = True
attr_type = BigInt | None
elif isinstance(field, ASNField):
should_create_function = True
attr_type = List[str] | None
elif isinstance(field, ColorField):
should_create_function = True
attr_type = List[str] | None
create_attribute_and_function(cls, fieldname, attr_type, should_create_function)
declared_filters = filterset.declared_filters
for fieldname, field in declared_filters.items():
should_create_function, attr_type = map_strawberry_type(field)
if attr_type is None:
raise NotImplementedError(f"GraphQL Filter field unknown: {fieldname}: {field}")
create_attribute_and_function(cls, fieldname, attr_type, should_create_function)
return cls
return wrapper
@strawberry.input
class BaseFilterMixin:
def filter_by_filterset(self, queryset, key):
return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs

View File

@ -1,23 +1,10 @@
from graphene import Scalar
from graphql.language import ast
from graphene.types.scalars import MAX_INT, MIN_INT
from typing import Union
import strawberry
class BigInt(Scalar):
"""
Handle any BigInts
"""
@staticmethod
def to_float(value):
num = int(value)
if num > MAX_INT or num < MIN_INT:
return float(num)
return num
serialize = to_float
parse_value = to_float
@staticmethod
def parse_literal(node):
if isinstance(node, ast.IntValue):
return BigInt.to_float(node.value)
BigInt = strawberry.scalar(
Union[int, str], # type: ignore
serialize=lambda v: int(v),
parse_value=lambda v: str(v),
description="BigInt field",
)

View File

@ -1,4 +1,6 @@
import graphene
import strawberry
from strawberry_django.optimizer import DjangoOptimizerExtension
from strawberry.schema.config import StrawberryConfig
from circuits.graphql.schema import CircuitsQuery
from core.graphql.schema import CoreQuery
@ -13,6 +15,7 @@ from vpn.graphql.schema import VPNQuery
from wireless.graphql.schema import WirelessQuery
@strawberry.type
class Query(
UsersQuery,
CircuitsQuery,
@ -25,9 +28,14 @@ class Query(
VPNQuery,
WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
graphene.ObjectType
):
pass
schema = graphene.Schema(query=Query, auto_camelcase=False)
schema = strawberry.Schema(
query=Query,
config=StrawberryConfig(auto_camel_case=False),
extensions=[
DjangoOptimizerExtension,
]
)

View File

@ -1,4 +1,8 @@
import graphene
from typing import Annotated, List
import strawberry
from strawberry import auto
import strawberry_django
from core.models import ObjectType as ObjectType_
from django.contrib.contenttypes.models import ContentType
@ -8,13 +12,10 @@ from extras.graphql.mixins import (
JournalEntriesMixin,
TagsMixin,
)
from graphene_django import DjangoObjectType
__all__ = (
'BaseObjectType',
'ContentTypeType',
'ObjectType',
'ObjectTypeType',
'OrganizationalObjectType',
'NetBoxObjectType',
)
@ -24,26 +25,27 @@ __all__ = (
# Base types
#
class BaseObjectType(DjangoObjectType):
@strawberry.type
class BaseObjectType:
"""
Base GraphQL object type for all NetBox objects. Restricts the model queryset to enforce object permissions.
"""
display = graphene.String()
class_type = graphene.String()
class Meta:
abstract = True
@classmethod
def get_queryset(cls, queryset, info):
def get_queryset(cls, queryset, info, **kwargs):
# Enforce object permissions on the queryset
return queryset.restrict(info.context.user, 'view')
if hasattr(queryset, 'restrict'):
return queryset.restrict(info.context.request.user, 'view')
else:
return queryset
def resolve_display(parent, info, **kwargs):
return str(parent)
@strawberry_django.field
def display(self) -> str:
return str(self)
def resolve_class_type(parent, info, **kwargs):
return parent.__class__.__name__
@strawberry_django.field
def class_type(self) -> str:
return self.__class__.__name__
class ObjectType(
@ -53,8 +55,7 @@ class ObjectType(
"""
Base GraphQL object type for unclassified models which support change logging
"""
class Meta:
abstract = True
pass
class OrganizationalObjectType(
@ -66,8 +67,7 @@ class OrganizationalObjectType(
"""
Base type for organizational models
"""
class Meta:
abstract = True
pass
class NetBoxObjectType(
@ -80,23 +80,24 @@ class NetBoxObjectType(
"""
GraphQL type for most NetBox models. Includes support for custom fields, change logging, journaling, and tags.
"""
class Meta:
abstract = True
pass
#
# Miscellaneous types
#
class ContentTypeType(DjangoObjectType):
class Meta:
model = ContentType
fields = ('id', 'app_label', 'model')
@strawberry_django.type(
ContentType,
fields=['id', 'app_label', 'model'],
)
class ContentTypeType:
pass
class ObjectTypeType(DjangoObjectType):
class Meta:
model = ObjectType_
fields = ('id', 'app_label', 'model')
@strawberry_django.type(
ObjectType_,
fields=['id', 'app_label', 'model'],
)
class ObjectTypeType:
pass

View File

@ -1,25 +0,0 @@
import graphene
from django_filters import filters
def get_graphene_type(filter_cls):
"""
Return the appropriate Graphene scalar type for a django_filters Filter
"""
if issubclass(filter_cls, filters.BooleanFilter):
field_type = graphene.Boolean
elif issubclass(filter_cls, filters.NumberFilter):
# TODO: Floats? BigInts?
field_type = graphene.Int
elif issubclass(filter_cls, filters.DateFilter):
field_type = graphene.Date
elif issubclass(filter_cls, filters.DateTimeFilter):
field_type = graphene.DateTime
else:
field_type = graphene.String
# Multi-value filters should be handled as lists
if issubclass(filter_cls, filters.MultipleChoiceFilter):
return graphene.List(field_type)
return field_type

View File

@ -1,20 +1,26 @@
import json
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseNotFound, HttpResponseForbidden
from django.http import HttpResponse
from django.template import loader
from django.urls import reverse
from graphene_django.views import GraphQLView as GraphQLView_
from django.views.decorators.csrf import csrf_exempt
from rest_framework.exceptions import AuthenticationFailed
from strawberry.django.views import GraphQLView
from netbox.api.authentication import TokenAuthentication
from netbox.config import get_config
class GraphQLView(GraphQLView_):
class NetBoxGraphQLView(GraphQLView):
"""
Extends graphene_django's GraphQLView to support DRF's token-based authentication.
Extends strawberry's GraphQLView to support DRF's token-based authentication.
"""
graphiql_template = 'graphiql.html'
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
config = get_config()
@ -34,11 +40,15 @@ class GraphQLView(GraphQLView_):
# Enforce LOGIN_REQUIRED
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
# If this is a human user, send a redirect to the login page
if self.request_wants_html(request):
if request.accepts("text/html"):
return redirect_to_login(reverse('graphql'))
return HttpResponseForbidden("No credentials provided.")
else:
return HttpResponseForbidden("No credentials provided.")
return super().dispatch(request, *args, **kwargs)
def render_graphql_ide(self, request):
template = loader.get_template("graphiql.html")
context = {"SUBSCRIPTION_ENABLED": json.dumps(self.subscriptions_enabled)}
return HttpResponse(template.render(context, request))

View File

@ -13,7 +13,8 @@ from django.http import Http404, HttpResponseRedirect
from extras.context_managers import event_tracking
from netbox.config import clear_config, get_config
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__ = (
'CoreMiddleware',
@ -71,7 +72,7 @@ class CoreMiddleware:
# Cleanly handle exceptions that occur from REST API requests
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)
if isinstance(exception, Http404):
@ -211,7 +212,7 @@ class MaintenanceModeMiddleware:
'operations. Please try again later.'
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)
return HttpResponseRedirect(request.path_info)

View File

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

View File

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

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