From 3f2c21f0059cdfe279cb294d662cc5e757e88f71 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 22 Aug 2023 16:39:04 -0700 Subject: [PATCH 001/156] 9856 base strawberry integration --- base_requirements.txt | 6 ++++- netbox/netbox/graphql/schema.py | 41 +++++++++++---------------------- netbox/netbox/settings.py | 4 ++-- netbox/netbox/urls.py | 4 ++-- requirements.txt | 1 + 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/base_requirements.txt b/base_requirements.txt index 4b75b1313..f66a4c4c2 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -132,8 +132,12 @@ social-auth-core # https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md social-auth-app-django +# Enhanced Strawberry GraphQL integration with Django +# https://github.com/blb-ventures/strawberry-django-plus/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) diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 7224f3c38..8f81efd0d 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,31 +1,18 @@ -import graphene - -from circuits.graphql.schema import CircuitsQuery -from core.graphql.schema import CoreQuery -from dcim.graphql.schema import DCIMQuery -from extras.graphql.schema import ExtrasQuery -from ipam.graphql.schema import IPAMQuery -from netbox.registry import registry -from tenancy.graphql.schema import TenancyQuery -from users.graphql.schema import UsersQuery -from virtualization.graphql.schema import VirtualizationQuery -from wireless.graphql.schema import WirelessQuery +import strawberry +from strawberry_django.optimizer import DjangoOptimizerExtension -class Query( - UsersQuery, - CircuitsQuery, - CoreQuery, - DCIMQuery, - ExtrasQuery, - IPAMQuery, - TenancyQuery, - VirtualizationQuery, - WirelessQuery, - *registry['plugins']['graphql_schemas'], # Append plugin schemas - graphene.ObjectType -): - pass +@strawberry.type +class User: + name: str + age: int -schema = graphene.Schema(query=Query, auto_camelcase=False) +@strawberry.type +class Query: + @strawberry.field + def user(self) -> User: + return User(name="Patrick", age=100) + + +schema = strawberry.Schema(query=Query) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 69ef8f52d..f28449b12 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -361,7 +361,7 @@ INSTALLED_APPS = [ 'django_filters', 'django_tables2', 'django_prometheus', - 'graphene_django', + 'strawberry_django', 'mptt', 'rest_framework', 'social_django', @@ -385,7 +385,7 @@ INSTALLED_APPS = [ # Middleware MIDDLEWARE = [ - 'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware', + "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 595a9001f..4ed180535 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -9,8 +9,8 @@ from account.views import LoginView, LogoutView from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns from netbox.api.views import APIRootView, StatusView from netbox.graphql.schema import schema -from netbox.graphql.views import GraphQLView from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx +from strawberry.django.views import GraphQLView from .admin import admin_site _patterns = [ @@ -59,7 +59,7 @@ _patterns = [ path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'), # GraphQL - path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'), + path('graphql/', GraphQLView.as_view(schema=schema), name='graphql'), # Serving static media in Django to pipe it through LoginRequiredMiddleware path('media/', serve, {'document_root': settings.MEDIA_ROOT}), diff --git a/requirements.txt b/requirements.txt index 7a2d0f9c8..7580af9cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ PyYAML==6.0.1 sentry-sdk==1.29.2 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 +strawberry-graphql-django==0.16.0 svgwrite==1.4.3 tablib==3.5.0 tzdata==2023.3 From eceac90b1c1acdf82263321ef736197ee4d02d88 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 24 Aug 2023 09:11:23 -0700 Subject: [PATCH 002/156] 9856 user and group --- netbox/netbox/graphql/schema.py | 22 ++++++++-------- netbox/users/filtersets.py | 1 + netbox/users/graphql/schema.py | 21 ++++++--------- netbox/users/graphql/types.py | 45 ++++++++++++++++----------------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 8f81efd0d..8323e440c 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,18 +1,18 @@ import strawberry from strawberry_django.optimizer import DjangoOptimizerExtension +from strawberry.schema.config import StrawberryConfig +from users.graphql.schema import UsersQuery @strawberry.type -class User: - name: str - age: int +class Query(UsersQuery): + pass -@strawberry.type -class Query: - @strawberry.field - def user(self) -> User: - return User(name="Patrick", age=100) - - -schema = strawberry.Schema(query=Query) +schema = strawberry.Schema( + query=Query, + config=StrawberryConfig(auto_camel_case=False), + extensions=[ + DjangoOptimizerExtension, + ] +) diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 0f590e012..9cde161bc 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -1,4 +1,5 @@ import django_filters + from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.db.models import Q diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index f033a535a..b41df6779 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -1,21 +1,16 @@ -import graphene +from typing import List +import strawberry from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from netbox.graphql.fields import ObjectField, ObjectListField from .types import * -from utilities.graphql_optimizer import gql_query_optimizer -class UsersQuery(graphene.ObjectType): - group = ObjectField(GroupType) - group_list = ObjectListField(GroupType) +@strawberry.type +class UsersQuery: + group: GroupType = strawberry.django.field() + group_list: List[GroupType] = strawberry.django.field() - def resolve_group_list(root, info, **kwargs): - return gql_query_optimizer(Group.objects.all(), info) - - user = ObjectField(UserType) - user_list = ObjectListField(UserType) - - def resolve_user_list(root, info, **kwargs): - return gql_query_optimizer(get_user_model().objects.all(), info) + user: UserType = strawberry.django.field() + user_list: List[UserType] = strawberry.django.field() diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 4254f1791..60a97a7cc 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,9 +1,10 @@ +import strawberry from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from graphene_django import DjangoObjectType - +from strawberry import auto from users import filtersets from utilities.querysets import RestrictedQuerySet +from .filters import * __all__ = ( 'GroupType', @@ -11,28 +12,26 @@ __all__ = ( ) -class GroupType(DjangoObjectType): - - class Meta: - model = Group - fields = ('id', 'name') - filterset_class = filtersets.GroupFilterSet - +@strawberry.django.type( + Group, + fields=['id', 'name'], + filters=GroupFilter +) +class GroupType: @classmethod - def get_queryset(cls, queryset, info): - return RestrictedQuerySet(model=Group).restrict(info.context.user, 'view') + def get_queryset(cls, queryset, info, **kwargs): + return RestrictedQuerySet(model=Group).restrict(info.context.request.user, 'view') -class UserType(DjangoObjectType): - - class Meta: - model = get_user_model() - fields = ( - 'id', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', - 'groups', - ) - filterset_class = filtersets.UserFilterSet - +@strawberry.django.type( + get_user_model(), + fields=[ + 'id', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', + 'is_active', 'date_joined', 'groups', + ], + filters=UserFilter +) +class UserType: @classmethod - def get_queryset(cls, queryset, info): - return RestrictedQuerySet(model=get_user_model()).restrict(info.context.user, 'view') + def get_queryset(cls, queryset, info, **kwargs): + return RestrictedQuerySet(model=get_user_model()).restrict(info.context.request.user, 'view') From 700f01594219f82c0baf589b708002b4c46f3a2c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 24 Aug 2023 14:54:24 -0700 Subject: [PATCH 003/156] 9856 user and circuits base --- netbox/circuits/graphql/filters.py | 93 ++++++++++++++++++++++++++++++ netbox/circuits/graphql/schema.py | 47 +++++---------- netbox/circuits/graphql/types.py | 84 ++++++++++++++++----------- netbox/extras/graphql/mixins.py | 1 + netbox/netbox/graphql/schema.py | 3 +- netbox/netbox/graphql/types.py | 26 ++++----- netbox/users/graphql/filters.py | 28 +++++++++ netbox/users/graphql/schema.py | 1 - 8 files changed, 199 insertions(+), 84 deletions(-) create mode 100644 netbox/circuits/graphql/filters.py create mode 100644 netbox/users/graphql/filters.py diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py new file mode 100644 index 000000000..d47d36502 --- /dev/null +++ b/netbox/circuits/graphql/filters.py @@ -0,0 +1,93 @@ +import strawberry +from strawberry import auto +from circuits import models, filtersets + +__all__ = ( + 'CircuitTerminationFilter', + 'CircuitFilter', + 'CircuitTypeFilter', + 'ProviderFilter', + 'ProviderAccountFilter', + 'ProviderNetworkFilter', +) + + +@strawberry.django.filter(models.CircuitTermination, lookups=True) +class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): + id: auto + term_side: auto + port_speed: auto + upstream_speed: auto + xconnect_id: auto + description: auto + cable_end: auto + # q: auto + circuit_id: auto + site_id: auto + site: auto + # provider_network_id: auto + + +@strawberry.django.filter(models.Circuit, lookups=True) +class CircuitFilter(filtersets.CircuitFilterSet): + id: auto + cid: auto + description: auto + install_date: auto + termination_date: auto + commit_rate: auto + provider_id: auto + provider: auto + provider_account_id: auto + # provider_network_id: auto + type_id: auto + type: auto + status: auto + # region_id: auto + # region: auto + # site_group_id: auto + # site_group: auto + # site_id: auto + # site: auto + + +@strawberry.django.filter(models.CircuitType, lookups=True) +class CircuitTypeFilter(filtersets.CircuitTypeFilterSet): + id: auto + name: auto + slug: auto + description: auto + + +@strawberry.django.filter(models.Provider, lookups=True) +class ProviderFilter(filtersets.ProviderFilterSet): + id: auto + name: auto + slug: auto + # region_id: auto + # region: auto + # site_group_id: auto + # site_group: auto + # site_id: auto + # site: auto + # asn_id: auto + + +@strawberry.django.filter(models.ProviderAccount, lookups=True) +class ProviderAccountFilter(filtersets.ProviderAccountFilterSet): + id: auto + name: auto + account: auto + description: auto + # provider_id: auto + # provider: auto + + +@strawberry.django.filter(models.ProviderNetwork, lookups=True) +class ProviderNetworkFilter(filtersets.ProviderNetworkFilterSet): + id: auto + name: auto + service_id: auto + description: auto + # provider_id: auto + # provider: auto diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 3d85f2512..1eb665d72 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -1,41 +1,26 @@ -import graphene +from typing import List +import strawberry 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: + circuit: CircuitType = strawberry.django.field() + circuit_list: List[CircuitType] = strawberry.django.field() - def resolve_circuit_list(root, info, **kwargs): - return gql_query_optimizer(models.Circuit.objects.all(), info) + circuit_termination: CircuitTerminationType = strawberry.django.field() + circuit_termination_list: List[CircuitTerminationType] = strawberry.django.field() - circuit_termination = ObjectField(CircuitTerminationType) - circuit_termination_list = ObjectListField(CircuitTerminationType) + circuit_type: CircuitTypeType = strawberry.django.field() + 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) + provider: ProviderType = strawberry.django.field() + provider_list: List[ProviderType] = strawberry.django.field() - circuit_type = ObjectField(CircuitTypeType) - circuit_type_list = ObjectListField(CircuitTypeType) + provider_account: ProviderAccountType = strawberry.django.field() + 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) + provider_network: ProviderNetworkType = strawberry.django.field() + provider_network_list: List[ProviderNetworkType] = strawberry.django.field() diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index baa135e00..65a733a01 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,9 +1,10 @@ -import graphene +import strawberry from circuits import filtersets, models from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType +from .filters import * __all__ = ( 'CircuitTerminationType', @@ -15,48 +16,61 @@ __all__ = ( ) -class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): - - class Meta: - model = models.CircuitTermination - fields = '__all__' - filterset_class = filtersets.CircuitTerminationFilterSet +@strawberry.django.type( + models.CircuitTermination, + fields='__all__', + filters=CircuitTerminationFilter +) +class CircuitTerminationType: + # class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): + pass +@strawberry.django.type( + models.Circuit, + fields='__all__', + filters=CircuitFilter +) class CircuitType(NetBoxObjectType, ContactsMixin): - class Meta: - model = models.Circuit - fields = '__all__' - filterset_class = filtersets.CircuitFilterSet + # class CircuitType(NetBoxObjectType, ContactsMixin): + pass -class CircuitTypeType(OrganizationalObjectType): - - class Meta: - model = models.CircuitType - fields = '__all__' - filterset_class = filtersets.CircuitTypeFilterSet +@strawberry.django.type( + models.CircuitType, + fields='__all__', + filters=CircuitTypeFilter +) +class CircuitTypeType: + # class CircuitTypeType(OrganizationalObjectType): + pass -class ProviderType(NetBoxObjectType, ContactsMixin): - - class Meta: - model = models.Provider - fields = '__all__' - filterset_class = filtersets.ProviderFilterSet +@strawberry.django.type( + models.Provider, + fields='__all__', + filters=ProviderFilter +) +class ProviderType: + # class ProviderType(NetBoxObjectType, ContactsMixin): + pass -class ProviderAccountType(NetBoxObjectType): - - class Meta: - model = models.ProviderAccount - fields = '__all__' - filterset_class = filtersets.ProviderAccountFilterSet +@strawberry.django.type( + models.ProviderAccount, + fields='__all__', + filters=ProviderAccountFilter +) +class ProviderAccountType: + # class ProviderAccountType(NetBoxObjectType): + pass -class ProviderNetworkType(NetBoxObjectType): - - class Meta: - model = models.ProviderNetwork - fields = '__all__' - filterset_class = filtersets.ProviderNetworkFilterSet +@strawberry.django.type( + models.ProviderNetwork, + fields='__all__', + filters=ProviderNetworkFilter +) +class ProviderNetworkType: + # class ProviderNetworkType(NetBoxObjectType): + pass diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 7045575fb..ac535c99d 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -1,3 +1,4 @@ +import strawberry import graphene from django.contrib.contenttypes.models import ContentType from graphene.types.generic import GenericScalar diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 8323e440c..02737b819 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,11 +1,12 @@ import strawberry from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry.schema.config import StrawberryConfig +from circuits.graphql.schema import CircuitsQuery from users.graphql.schema import UsersQuery @strawberry.type -class Query(UsersQuery): +class Query(CircuitsQuery, UsersQuery): pass diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 10847742b..ed466b21a 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,4 +1,5 @@ -import graphene +import strawberry +from strawberry import auto from django.contrib.contenttypes.models import ContentType from extras.graphql.mixins import ( @@ -7,7 +8,6 @@ from extras.graphql.mixins import ( JournalEntriesMixin, TagsMixin, ) -from graphene_django import DjangoObjectType __all__ = ( 'BaseObjectType', @@ -21,20 +21,17 @@ __all__ = ( # Base types # -class BaseObjectType(DjangoObjectType): +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 + display: auto + class_type: auto @classmethod def get_queryset(cls, queryset, info): # Enforce object permissions on the queryset - return queryset.restrict(info.context.user, 'view') + return queryset.restrict(info.context.request.user, 'view') def resolve_display(parent, info, **kwargs): return str(parent) @@ -50,8 +47,7 @@ class ObjectType( """ Base GraphQL object type for unclassified models which support change logging """ - class Meta: - abstract = True + pass class OrganizationalObjectType( @@ -63,8 +59,7 @@ class OrganizationalObjectType( """ Base type for organizational models """ - class Meta: - abstract = True + pass class NetBoxObjectType( @@ -77,15 +72,14 @@ 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 ContentTypeType: class Meta: model = ContentType diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py new file mode 100644 index 000000000..4630cd70e --- /dev/null +++ b/netbox/users/graphql/filters.py @@ -0,0 +1,28 @@ +import strawberry +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from strawberry import auto +from users import filtersets + +__all__ = ( + 'GroupFilter', + 'UserFilter', +) + + +@strawberry.django.filter(Group, lookups=True) +class GroupFilter(filtersets.GroupFilterSet): + id: auto + name: auto + + +@strawberry.django.filter(get_user_model(), lookups=True) +class UserFilter(filtersets.UserFilterSet): + id: auto + username: auto + first_name: auto + last_name: auto + email: auto + is_staff: auto + is_active: auto + is_superuser: auto diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index b41df6779..808764363 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -3,7 +3,6 @@ import strawberry from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from netbox.graphql.fields import ObjectField, ObjectListField from .types import * From 46b0df43f9f893e7a6f2c3260c3c902be08a1c10 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 25 Aug 2023 13:31:10 -0700 Subject: [PATCH 004/156] 9856 extras and mixins --- netbox/extras/graphql/filters.py | 149 +++++++++++++++++++++++++++++++ netbox/extras/graphql/mixins.py | 43 +++++---- netbox/extras/graphql/types.py | 136 +++++++++++++++------------- netbox/netbox/graphql/types.py | 22 ++--- 4 files changed, 264 insertions(+), 86 deletions(-) create mode 100644 netbox/extras/graphql/filters.py diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py new file mode 100644 index 000000000..f5ce2b23a --- /dev/null +++ b/netbox/extras/graphql/filters.py @@ -0,0 +1,149 @@ +import strawberry +from strawberry import auto +from extras import models, filtersets + +__all__ = ( + 'ConfigContextFilter', + 'ConfigTemplateFilter', + 'CustomFieldFilter', + 'CustomFieldChoiceSetFilter', + 'CustomLinkFilter', + 'ExportTemplateFilter', + 'ImageAttachmentFilter', + 'JournalEntryFilter', + 'ObjectChangeFilter', + 'SavedFilterFilter', + 'TagFilter', + 'WebhookFilter', +) + + +@strawberry.django.filter(models.ConfigContext, lookups=True) +class ConfigContextFilter(filtersets.ConfigContextFilterSet): + id: auto + name: auto + is_active: auto + data_synced: auto + + +@strawberry.django.filter(models.ConfigTemplate, lookups=True) +class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet): + id: auto + name: auto + description: auto + data_synced: auto + + +@strawberry.django.filter(models.CustomField, lookups=True) +class CustomFieldFilter(filtersets.CustomFieldFilterSet): + id: auto + content_types: auto + name: auto + group_name: auto + required: auto + search_weight: auto + filter_logic: auto + ui_visibility: auto + weight: auto + is_cloneable: auto + description: auto + + +@strawberry.django.filter(models.CustomFieldChoiceSet, lookups=True) +class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet): + id: auto + name: auto + description: auto + base_choices: auto + order_alphabetically: auto + + +@strawberry.django.filter(models.CustomLink, lookups=True) +class CustomLinkFilter(filtersets.CustomLinkFilterSet): + id: auto + content_types: auto + name: auto + enabled: auto + link_text: auto + link_url: auto + weight: auto + group_name: auto + new_window: auto + + +@strawberry.django.filter(models.ExportTemplate, lookups=True) +class ExportTemplateFilter(filtersets.ExportTemplateFilterSet): + id: auto + content_types: auto + name: auto + description: auto + data_synced: auto + + +@strawberry.django.filter(models.ImageAttachment, lookups=True) +class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet): + id: auto + content_type_id: auto + object_id: auto + name: auto + + +@strawberry.django.filter(models.JournalEntry, lookups=True) +class JournalEntryFilter(filtersets.JournalEntryFilterSet): + id: auto + assigned_object_type_id: auto + assigned_object_id: auto + created: auto + kind: auto + + +@strawberry.django.filter(models.ObjectChange, lookups=True) +class ObjectChangeFilter(filtersets.ObjectChangeFilterSet): + id: auto + user: auto + user_name: auto + request_id: auto + action: auto + changed_object_type_id: auto + changed_object_id: auto + object_repr: auto + + +@strawberry.django.filter(models.SavedFilter, lookups=True) +class SavedFilterFilter(filtersets.SavedFilterFilterSet): + id: auto + content_types: auto + name: auto + slug: auto + description: auto + enabled: auto + shared: auto + weight: auto + + +@strawberry.django.filter(models.Tag, lookups=True) +class TagFilter(filtersets.TagFilterSet): + id: auto + name: auto + slug: auto + # color: auto + description: auto + object_types: auto + + +@strawberry.django.filter(models.Webhook, lookups=True) +class WebhookFilter(filtersets.WebhookFilterSet): + id: auto + name: auto + type_create: auto + type_update: auto + type_delete: auto + type_job_start: auto + type_job_end: auto + payload_url: auto + enabled: auto + http_method: auto + http_content_type: auto + secret: auto + ssl_verification: auto + ca_file_path: auto diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index ac535c99d..78b8b0ff8 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -1,7 +1,7 @@ import strawberry -import graphene +from typing import TYPE_CHECKING, Annotated, List + from django.contrib.contenttypes.models import ContentType -from graphene.types.generic import GenericScalar from extras.models import ObjectChange @@ -14,11 +14,16 @@ __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) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: content_type = ContentType.objects.get_for_model(self) object_changes = ObjectChange.objects.filter( changed_object_type=content_type, @@ -27,43 +32,49 @@ class ChangelogMixin: return object_changes.restrict(info.context.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): + @strawberry.django.field + def image_attachments(self) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]: return self.images.restrict(info.context.user, 'view') +@strawberry.type class JournalEntriesMixin: - journal_entries = graphene.List('extras.graphql.types.JournalEntryType') - def resolve_journal_entries(self, info): + @strawberry.django.field + def journal_entries(self) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]: return self.journal_entries.restrict(info.context.user, 'view') +@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()) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 068da02f2..e962859db 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,6 +1,10 @@ +import strawberry +from strawberry import auto + from extras import filtersets, models from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType +from .filters import * __all__ = ( 'ConfigContextType', @@ -18,97 +22,109 @@ __all__ = ( ) +@strawberry.django.type( + models.ConfigContext, + fields='__all__', + filters=ConfigContextFilter +) class ConfigContextType(ObjectType): - - class Meta: - model = models.ConfigContext - fields = '__all__' - filterset_class = filtersets.ConfigContextFilterSet + pass +@strawberry.django.type( + models.ConfigTemplate, + fields='__all__', + filters=ConfigTemplateFilter +) class ConfigTemplateType(TagsMixin, ObjectType): - - class Meta: - model = models.ConfigTemplate - fields = '__all__' - filterset_class = filtersets.ConfigTemplateFilterSet + pass +@strawberry.django.type( + models.CustomField, + fields='__all__', + filters=CustomFieldFilter +) class CustomFieldType(ObjectType): - - class Meta: - model = models.CustomField - exclude = ('content_types', ) - filterset_class = filtersets.CustomFieldFilterSet + pass +@strawberry.django.type( + models.CustomFieldChoiceSet, + fields='__all__', + filters=CustomFieldChoiceSetFilter +) class CustomFieldChoiceSetType(ObjectType): - - class Meta: - model = models.CustomFieldChoiceSet - fields = '__all__' - filterset_class = filtersets.CustomFieldChoiceSetFilterSet + pass +@strawberry.django.type( + models.CustomLink, + fields='__all__', + filters=CustomLinkFilter +) class CustomLinkType(ObjectType): - - class Meta: - model = models.CustomLink - exclude = ('content_types', ) - filterset_class = filtersets.CustomLinkFilterSet + pass +@strawberry.django.type( + models.ExportTemplate, + fields='__all__', + filters=ExportTemplateFilter +) class ExportTemplateType(ObjectType): - - class Meta: - model = models.ExportTemplate - exclude = ('content_types', ) - filterset_class = filtersets.ExportTemplateFilterSet + pass +@strawberry.django.type( + models.ImageAttachment, + fields='__all__', + filters=ImageAttachmentFilter +) class ImageAttachmentType(BaseObjectType): - - class Meta: - model = models.ImageAttachment - fields = '__all__' - filterset_class = filtersets.ImageAttachmentFilterSet + pass +@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 + pass +@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 - exclude = ('content_types', ) - filterset_class = filtersets.SavedFilterFilterSet + pass +@strawberry.django.type( + models.Tag, + exclude=['extras_taggeditem_items', 'color'], # bug - remove color from exclude + filters=TagFilter +) class TagType(ObjectType): - - class Meta: - model = models.Tag - exclude = ('extras_taggeditem_items',) - filterset_class = filtersets.TagFilterSet + pass +@strawberry.django.type( + models.Webhook, + exclude=['content_types',], + filters=WebhookFilter +) class WebhookType(OrganizationalObjectType): - - class Meta: - model = models.Webhook - exclude = ('content_types', ) - filterset_class = filtersets.WebhookFilterSet + pass diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index ed466b21a..56785c829 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -21,23 +21,24 @@ __all__ = ( # Base types # +@strawberry.type class BaseObjectType: """ Base GraphQL object type for all NetBox objects. Restricts the model queryset to enforce object permissions. """ - display: auto - class_type: auto @classmethod def get_queryset(cls, queryset, info): # Enforce object permissions on the queryset return queryset.restrict(info.context.request.user, 'view') - 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( @@ -79,8 +80,9 @@ class NetBoxObjectType( # Miscellaneous types # +@strawberry.django.type( + ContentType, + fields=['id', 'app_label', 'model'], +) class ContentTypeType: - - class Meta: - model = ContentType - fields = ('id', 'app_label', 'model') + pass From 36f57f8f08c5546f1d1d905065fc03b91edcef6e Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 25 Aug 2023 13:52:24 -0700 Subject: [PATCH 005/156] 9856 fk --- netbox/circuits/graphql/types.py | 67 +++++++++++++++----------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 65a733a01..33cb5749d 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -5,6 +5,7 @@ from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType from .filters import * +from typing import List __all__ = ( 'CircuitTerminationType', @@ -16,13 +17,39 @@ __all__ = ( ) +@strawberry.django.type( + models.Provider, + fields='__all__', + filters=ProviderFilter +) +class ProviderType(NetBoxObjectType, ContactsMixin): + pass + + +@strawberry.django.type( + models.ProviderAccount, + fields='__all__', + filters=ProviderAccountFilter +) +class ProviderAccountType(NetBoxObjectType): + pass + + +@strawberry.django.type( + models.ProviderNetwork, + fields='__all__', + filters=ProviderNetworkFilter +) +class ProviderNetworkType(NetBoxObjectType): + pass + + @strawberry.django.type( models.CircuitTermination, fields='__all__', filters=CircuitTerminationFilter ) -class CircuitTerminationType: - # class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): +class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): pass @@ -32,8 +59,7 @@ class CircuitTerminationType: filters=CircuitFilter ) class CircuitType(NetBoxObjectType, ContactsMixin): - # class CircuitType(NetBoxObjectType, ContactsMixin): - pass + provider: ProviderType @strawberry.django.type( @@ -41,36 +67,5 @@ class CircuitType(NetBoxObjectType, ContactsMixin): fields='__all__', filters=CircuitTypeFilter ) -class CircuitTypeType: - # class CircuitTypeType(OrganizationalObjectType): - pass - - -@strawberry.django.type( - models.Provider, - fields='__all__', - filters=ProviderFilter -) -class ProviderType: - # class ProviderType(NetBoxObjectType, ContactsMixin): - pass - - -@strawberry.django.type( - models.ProviderAccount, - fields='__all__', - filters=ProviderAccountFilter -) -class ProviderAccountType: - # class ProviderAccountType(NetBoxObjectType): - pass - - -@strawberry.django.type( - models.ProviderNetwork, - fields='__all__', - filters=ProviderNetworkFilter -) -class ProviderNetworkType: - # class ProviderNetworkType(NetBoxObjectType): +class CircuitTypeType(OrganizationalObjectType): pass From de2d9edbd4ab70ec2850c6627fe4adfc25267bd8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 3 Jan 2024 15:35:59 -0800 Subject: [PATCH 006/156] 9856 update strawberry version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1ab2caaea..f3412f00d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 social-auth-core[openidconnect]==4.5.1 -strawberry-graphql-django==0.16.0 +strawberry-graphql-django==0.28.3 svgwrite==1.4.3 tablib==3.5.0 tzdata==2023.3 From 7a222a650110139e12b2f41024417b003cae1da5 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 3 Jan 2024 17:20:13 -0800 Subject: [PATCH 007/156] 9856 update imports --- netbox/circuits/graphql/types.py | 13 +++++++------ netbox/extras/graphql/mixins.py | 15 ++++++++------- netbox/extras/graphql/types.py | 27 ++++++++++++++------------- netbox/netbox/graphql/types.py | 7 ++++--- netbox/users/graphql/types.py | 5 +++-- 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index fc33a81a7..e6729cc18 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from circuits import filtersets, models from dcim.graphql.mixins import CabledObjectMixin @@ -17,7 +18,7 @@ __all__ = ( ) -@strawberry.django.type( +@strawberry_django.type( models.Provider, fields='__all__', filters=ProviderFilter @@ -26,7 +27,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin): pass -@strawberry.django.type( +@strawberry_django.type( models.ProviderAccount, fields='__all__', filters=ProviderAccountFilter @@ -35,7 +36,7 @@ class ProviderAccountType(NetBoxObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.ProviderNetwork, fields='__all__', filters=ProviderNetworkFilter @@ -44,7 +45,7 @@ class ProviderNetworkType(NetBoxObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.CircuitTermination, fields='__all__', filters=CircuitTerminationFilter @@ -53,7 +54,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob pass -@strawberry.django.type( +@strawberry_django.type( models.Circuit, fields='__all__', filters=CircuitFilter @@ -62,7 +63,7 @@ class CircuitType(NetBoxObjectType, ContactsMixin): provider: ProviderType -@strawberry.django.type( +@strawberry_django.type( models.CircuitType, # fields='__all__', exclude=['color',], # bug - remove color from exclude diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 78b8b0ff8..04c06c9c3 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from typing import TYPE_CHECKING, Annotated, List from django.contrib.contenttypes.models import ContentType @@ -22,7 +23,7 @@ if TYPE_CHECKING: @strawberry.type class ChangelogMixin: - @strawberry.django.field + @strawberry_django.field def changelog(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: content_type = ContentType.objects.get_for_model(self) object_changes = ObjectChange.objects.filter( @@ -35,7 +36,7 @@ class ChangelogMixin: @strawberry.type class ConfigContextMixin: - @strawberry.django.field + @strawberry_django.field def config_context(self) -> strawberry.scalars.JSON: return self.get_config_context() @@ -43,7 +44,7 @@ class ConfigContextMixin: @strawberry.type class CustomFieldsMixin: - @strawberry.django.field + @strawberry_django.field def custom_fields(self) -> strawberry.scalars.JSON: return self.custom_field_data @@ -51,7 +52,7 @@ class CustomFieldsMixin: @strawberry.type class ImageAttachmentsMixin: - @strawberry.django.field + @strawberry_django.field def image_attachments(self) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]: return self.images.restrict(info.context.user, 'view') @@ -59,7 +60,7 @@ class ImageAttachmentsMixin: @strawberry.type class JournalEntriesMixin: - @strawberry.django.field + @strawberry_django.field def journal_entries(self) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]: return self.journal_entries.restrict(info.context.user, 'view') @@ -67,7 +68,7 @@ class JournalEntriesMixin: @strawberry.type class TagsMixin: - @strawberry.django.field + @strawberry_django.field def tags(self) -> List[Annotated["TagType", strawberry.lazy('.types')]]: return self.tags.all() @@ -75,6 +76,6 @@ class TagsMixin: @strawberry.type class ContactsMixin: - @strawberry.django.field + @strawberry_django.field def contacts(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: return list(self.contacts.all()) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index e11f495a1..86f3179b7 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,5 +1,6 @@ import strawberry from strawberry import auto +import strawberry_django from extras import filtersets, models from extras.graphql.mixins import CustomFieldsMixin, TagsMixin @@ -23,7 +24,7 @@ __all__ = ( ) -@strawberry.django.type( +@strawberry_django.type( models.ConfigContext, fields='__all__', filters=ConfigContextFilter @@ -32,7 +33,7 @@ class ConfigContextType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.ConfigTemplate, fields='__all__', filters=ConfigTemplateFilter @@ -41,7 +42,7 @@ class ConfigTemplateType(TagsMixin, ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.CustomField, fields='__all__', filters=CustomFieldFilter @@ -50,7 +51,7 @@ class CustomFieldType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.CustomFieldChoiceSet, fields='__all__', filters=CustomFieldChoiceSetFilter @@ -59,7 +60,7 @@ class CustomFieldChoiceSetType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.CustomLink, fields='__all__', filters=CustomLinkFilter @@ -68,7 +69,7 @@ class CustomLinkType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.ExportTemplate, fields='__all__', filters=ExportTemplateFilter @@ -77,7 +78,7 @@ class ExportTemplateType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.ImageAttachment, fields='__all__', filters=ImageAttachmentFilter @@ -86,7 +87,7 @@ class ImageAttachmentType(BaseObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.JournalEntry, fields='__all__', filters=JournalEntryFilter @@ -95,7 +96,7 @@ class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.ObjectChange, fields='__all__', filters=ObjectChangeFilter @@ -104,7 +105,7 @@ class ObjectChangeType(BaseObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.SavedFilter, exclude=['content_types',], filters=SavedFilterFilter @@ -113,7 +114,7 @@ class SavedFilterType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.Tag, exclude=['extras_taggeditem_items', 'color'], # bug - remove color from exclude filters=TagFilter @@ -122,7 +123,7 @@ class TagType(ObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.Webhook, exclude=['content_types',], filters=WebhookFilter @@ -131,7 +132,7 @@ class WebhookType(OrganizationalObjectType): pass -@strawberry.django.type( +@strawberry_django.type( models.EventRule, exclude=['content_types',], filters=EventRuleFilter diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 56785c829..818d263de 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,5 +1,6 @@ import strawberry from strawberry import auto +import strawberry_django from django.contrib.contenttypes.models import ContentType from extras.graphql.mixins import ( @@ -32,11 +33,11 @@ class BaseObjectType: # Enforce object permissions on the queryset return queryset.restrict(info.context.request.user, 'view') - @strawberry.django.field + @strawberry_django.field def display(self) -> str: return str(self) - @strawberry.django.field + @strawberry_django.field def class_type(self) -> str: return self.__class__.__name__ @@ -80,7 +81,7 @@ class NetBoxObjectType( # Miscellaneous types # -@strawberry.django.type( +@strawberry_django.type( ContentType, fields=['id', 'app_label', 'model'], ) diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 60a97a7cc..d6f67badd 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from strawberry import auto @@ -12,7 +13,7 @@ __all__ = ( ) -@strawberry.django.type( +@strawberry_django.type( Group, fields=['id', 'name'], filters=GroupFilter @@ -23,7 +24,7 @@ class GroupType: return RestrictedQuerySet(model=Group).restrict(info.context.request.user, 'view') -@strawberry.django.type( +@strawberry_django.type( get_user_model(), fields=[ 'id', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', From 28c48b3d6c00912d8bfc39443cba277147a1aa98 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 9 Jan 2024 14:23:10 -0800 Subject: [PATCH 008/156] 9856 compatability fixes --- netbox/circuits/graphql/filters.py | 44 ++++++++++++++++++++---------- netbox/circuits/graphql/schema.py | 25 +++++++++-------- netbox/netbox/graphql/types.py | 2 +- netbox/users/graphql/schema.py | 9 +++--- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index d47d36502..6253b4fa9 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from strawberry import auto from circuits import models, filtersets @@ -12,7 +13,7 @@ __all__ = ( ) -@strawberry.django.filter(models.CircuitTermination, lookups=True) +@strawberry_django.filter(models.CircuitTermination, lookups=True) class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): id: auto term_side: auto @@ -28,7 +29,7 @@ class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): # provider_network_id: auto -@strawberry.django.filter(models.Circuit, lookups=True) +@strawberry_django.filter(models.Circuit, lookups=True) class CircuitFilter(filtersets.CircuitFilterSet): id: auto cid: auto @@ -39,19 +40,34 @@ class CircuitFilter(filtersets.CircuitFilterSet): provider_id: auto provider: auto provider_account_id: auto - # provider_network_id: auto type_id: auto - type: auto status: auto - # region_id: auto - # region: auto - # site_group_id: auto - # site_group: auto - # site_id: auto - # site: auto -@strawberry.django.filter(models.CircuitType, lookups=True) +# @strawberry_django.filter(models.Circuit, lookups=True) +# class CircuitFilter(filtersets.CircuitFilterSet): +# id: auto +# cid: auto +# description: auto +# install_date: auto +# termination_date: auto +# commit_rate: auto +# provider_id: auto +# provider: auto +# provider_account_id: auto +# # provider_network_id: auto +# type_id: auto +# type: auto +# status: auto +# # region_id: auto +# # region: auto +# # site_group_id: auto +# # site_group: auto +# # site_id: auto +# # site: auto + + +@strawberry_django.filter(models.CircuitType, lookups=True) class CircuitTypeFilter(filtersets.CircuitTypeFilterSet): id: auto name: auto @@ -59,7 +75,7 @@ class CircuitTypeFilter(filtersets.CircuitTypeFilterSet): description: auto -@strawberry.django.filter(models.Provider, lookups=True) +@strawberry_django.filter(models.Provider, lookups=True) class ProviderFilter(filtersets.ProviderFilterSet): id: auto name: auto @@ -73,7 +89,7 @@ class ProviderFilter(filtersets.ProviderFilterSet): # asn_id: auto -@strawberry.django.filter(models.ProviderAccount, lookups=True) +@strawberry_django.filter(models.ProviderAccount, lookups=True) class ProviderAccountFilter(filtersets.ProviderAccountFilterSet): id: auto name: auto @@ -83,7 +99,7 @@ class ProviderAccountFilter(filtersets.ProviderAccountFilterSet): # provider: auto -@strawberry.django.filter(models.ProviderNetwork, lookups=True) +@strawberry_django.filter(models.ProviderNetwork, lookups=True) class ProviderNetworkFilter(filtersets.ProviderNetworkFilterSet): id: auto name: auto diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 1eb665d72..7b0356920 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -1,5 +1,6 @@ from typing import List import strawberry +import strawberry_django from circuits import models from .types import * @@ -7,20 +8,20 @@ from .types import * @strawberry.type class CircuitsQuery: - circuit: CircuitType = strawberry.django.field() - circuit_list: List[CircuitType] = strawberry.django.field() + circuit: CircuitType = strawberry_django.field() + circuit_list: List[CircuitType] = strawberry_django.field() - circuit_termination: CircuitTerminationType = strawberry.django.field() - circuit_termination_list: List[CircuitTerminationType] = strawberry.django.field() + circuit_termination: CircuitTerminationType = strawberry_django.field() + circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field() - circuit_type: CircuitTypeType = strawberry.django.field() - circuit_type_list: List[CircuitTypeType] = strawberry.django.field() + circuit_type: CircuitTypeType = strawberry_django.field() + circuit_type_list: List[CircuitTypeType] = strawberry_django.field() - provider: ProviderType = strawberry.django.field() - provider_list: List[ProviderType] = strawberry.django.field() + provider: ProviderType = strawberry_django.field() + provider_list: List[ProviderType] = strawberry_django.field() - provider_account: ProviderAccountType = strawberry.django.field() - provider_account_list: List[ProviderAccountType] = strawberry.django.field() + provider_account: ProviderAccountType = strawberry_django.field() + provider_account_list: List[ProviderAccountType] = strawberry_django.field() - provider_network: ProviderNetworkType = strawberry.django.field() - provider_network_list: List[ProviderNetworkType] = strawberry.django.field() + provider_network: ProviderNetworkType = strawberry_django.field() + provider_network_list: List[ProviderNetworkType] = strawberry_django.field() diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 818d263de..25dedd696 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -29,7 +29,7 @@ class BaseObjectType: """ @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.request.user, 'view') diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index 808764363..575bafecd 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -1,5 +1,6 @@ from typing import List import strawberry +import strawberry_django from django.contrib.auth import get_user_model from django.contrib.auth.models import Group @@ -8,8 +9,8 @@ from .types import * @strawberry.type class UsersQuery: - group: GroupType = strawberry.django.field() - group_list: List[GroupType] = strawberry.django.field() + group: GroupType = strawberry_django.field() + group_list: List[GroupType] = strawberry_django.field() - user: UserType = strawberry.django.field() - user_list: List[UserType] = strawberry.django.field() + user: UserType = strawberry_django.field() + user_list: List[UserType] = strawberry_django.field() From 2d4fbea8ff7a7dcbea9f1d2fae566c53322774fe Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 10 Jan 2024 12:46:55 -0800 Subject: [PATCH 009/156] 9856 compatability fixes --- netbox/circuits/graphql/filters.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index 6253b4fa9..c9c88ea01 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -2,6 +2,8 @@ import strawberry import strawberry_django from strawberry import auto from circuits import models, filtersets +from netbox.graphql import filters + __all__ = ( 'CircuitTerminationFilter', @@ -30,7 +32,17 @@ class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): @strawberry_django.filter(models.Circuit, lookups=True) -class CircuitFilter(filtersets.CircuitFilterSet): +class CircuitFilter(filtersets.CircuitFilterSet, filters.NetBoxModelFilter): + # NetBoxModelFilterSet + q: str | None + # tag: + # ChangeLoggedModelFilterSet + created: auto + last_updated: auto + created_by_request: str | None + updated_by_request: str | None + modified_by_request: str | None + id: auto cid: auto description: auto @@ -41,7 +53,16 @@ class CircuitFilter(filtersets.CircuitFilterSet): provider: auto provider_account_id: auto type_id: auto + # provider_network_id: auto + type_id: auto + type: auto status: auto + # region_id: auto + # region: auto + # site_group_id: auto + # site_group: auto + # site_id: auto + # site: auto # @strawberry_django.filter(models.Circuit, lookups=True) From 663af64ec15cf0a4f56d765fcdc02115d98ed3ec Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 6 Feb 2024 13:52:41 -0800 Subject: [PATCH 010/156] 9856 update strawberry types --- netbox/circuits/graphql/types.py | 1 - netbox/core/graphql/filters.py | 0 netbox/core/graphql/types.py | 26 +- netbox/dcim/graphql/filters.py | 0 netbox/dcim/graphql/types.py | 433 ++++++++++++----------- netbox/extras/graphql/types.py | 5 +- netbox/ipam/graphql/filters.py | 0 netbox/ipam/graphql/types.py | 176 ++++----- netbox/netbox/graphql/filters.py | 21 ++ netbox/netbox/graphql/schema.py | 31 +- netbox/tenancy/graphql/filters.py | 0 netbox/tenancy/graphql/types.py | 72 ++-- netbox/virtualization/graphql/filters.py | 0 netbox/virtualization/graphql/types.py | 70 ++-- netbox/vpn/graphql/filters.py | 0 netbox/vpn/graphql/types.py | 114 +++--- netbox/wireless/graphql/filters.py | 0 netbox/wireless/graphql/types.py | 37 +- 18 files changed, 544 insertions(+), 442 deletions(-) create mode 100644 netbox/core/graphql/filters.py create mode 100644 netbox/dcim/graphql/filters.py create mode 100644 netbox/ipam/graphql/filters.py create mode 100644 netbox/netbox/graphql/filters.py create mode 100644 netbox/tenancy/graphql/filters.py create mode 100644 netbox/virtualization/graphql/filters.py create mode 100644 netbox/vpn/graphql/filters.py create mode 100644 netbox/wireless/graphql/filters.py diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index e6729cc18..fa7e5e3b4 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -6,7 +6,6 @@ from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType from .filters import * -from typing import List __all__ = ( 'CircuitTerminationType', diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index 402e36345..02d8ab483 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -1,5 +1,9 @@ -from core import filtersets, models +import strawberry +import strawberry_django + +from core import models from netbox.graphql.types import BaseObjectType, NetBoxObjectType +from .filters import * __all__ = ( 'DataFileType', @@ -7,15 +11,19 @@ __all__ = ( ) +@strawberry_django.type( + models.DataFile, + fields='__all__', + filters=DataFileFilter +) class DataFileType(BaseObjectType): - class Meta: - model = models.DataFile - exclude = ('data',) - filterset_class = filtersets.DataFileFilterSet + pass +@strawberry_django.type( + models.DataSource, + fields='__all__', + filters=DataSourceFilter +) class DataSourceType(NetBoxObjectType): - class Meta: - model = models.DataSource - fields = '__all__' - filterset_class = filtersets.DataSourceFilterSet + pass diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 7d7434587..145624145 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,12 +1,14 @@ -import graphene +import strawberry +import strawberry_django -from dcim import filtersets, models +from dcim import models from extras.graphql.mixins import ( ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin, ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType +from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin __all__ = ( @@ -86,6 +88,11 @@ class ComponentTemplateObjectType( # Model types # +@strawberry_django.type( + models.Cable, + fields='__all__', + filters=CableFilter +) class CableType(NetBoxObjectType): a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') @@ -108,66 +115,66 @@ class CableType(NetBoxObjectType): return self.b_terminations +@strawberry_django.type( + models.CableTermination, + exclude=('termination_type', 'termination_id'), + filters=CableTerminationFilter +) class CableTerminationType(NetBoxObjectType): termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - class Meta: - model = models.CableTermination - exclude = ('termination_type', 'termination_id') - filterset_class = filtersets.CableTerminationFilterSet - +@strawberry_django.type( + models.ConsolePort, + exclude=('_path',), + filters=ConsolePortFilter +) class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - class Meta: - model = models.ConsolePort - exclude = ('_path',) - filterset_class = filtersets.ConsolePortFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.ConsolePortTemplate, + fields='__all__', + filters=ConsolePortTemplateFilter +) class ConsolePortTemplateType(ComponentTemplateObjectType): - class Meta: - model = models.ConsolePortTemplate - fields = '__all__' - filterset_class = filtersets.ConsolePortTemplateFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.ConsoleServerPort, + exclude=('_path',), + filters=ConsoleServerPortFilter +) class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - class Meta: - model = models.ConsoleServerPort - exclude = ('_path',) - filterset_class = filtersets.ConsoleServerPortFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.ConsoleServerPortTemplate, + fields='__all__', + filters=ConsoleServerPortTemplateFilter +) class ConsoleServerPortTemplateType(ComponentTemplateObjectType): - class Meta: - model = models.ConsoleServerPortTemplate - fields = '__all__' - filterset_class = filtersets.ConsoleServerPortTemplateFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.Device, + fields='__all__', + filters=DeviceFilter +) class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): - class Meta: - model = models.Device - fields = '__all__' - filterset_class = filtersets.DeviceFilterSet - def resolve_face(self, info): return self.face or None @@ -175,46 +182,49 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo return self.airflow or None +@strawberry_django.type( + models.DeviceBay, + fields='__all__', + filters=DeviceBayFilter +) class DeviceBayType(ComponentObjectType): - - class Meta: - model = models.DeviceBay - fields = '__all__' - filterset_class = filtersets.DeviceBayFilterSet + pass +@strawberry_django.type( + models.DeviceBayTemplate, + fields='__all__', + filters=DeviceBayTemplateFilter +) class DeviceBayTemplateType(ComponentTemplateObjectType): - - class Meta: - model = models.DeviceBayTemplate - fields = '__all__' - filterset_class = filtersets.DeviceBayTemplateFilterSet + pass +@strawberry_django.type( + models.InventoryItemTemplate, + exclude=('component_type', 'component_id'), + filters=InventoryItemTemplateFilter +) class InventoryItemTemplateType(ComponentTemplateObjectType): component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType') - class Meta: - model = models.InventoryItemTemplate - exclude = ('component_type', 'component_id') - filterset_class = filtersets.InventoryItemTemplateFilterSet - +@strawberry_django.type( + models.DeviceRole, + fields='__all__', + filters=DeviceRoleFilter +) class DeviceRoleType(OrganizationalObjectType): - - class Meta: - model = models.DeviceRole - fields = '__all__' - filterset_class = filtersets.DeviceRoleFilterSet + pass +@strawberry_django.type( + models.DeviceType, + fields='__all__', + filters=DeviceTypeFilter +) class DeviceTypeType(NetBoxObjectType): - class Meta: - model = models.DeviceType - fields = '__all__' - filterset_class = filtersets.DeviceTypeFilterSet - def resolve_subdevice_role(self, info): return self.subdevice_role or None @@ -225,29 +235,31 @@ class DeviceTypeType(NetBoxObjectType): return self.weight_unit or None +@strawberry_django.type( + models.FrontPort, + fields='__all__', + filters=FrontPortFilter +) class FrontPortType(ComponentObjectType, CabledObjectMixin): - - class Meta: - model = models.FrontPort - fields = '__all__' - filterset_class = filtersets.FrontPortFilterSet + pass +@strawberry_django.type( + models.FrontPortTemplate, + fields='__all__', + filters=FrontPortTemplateFilter +) class FrontPortTemplateType(ComponentTemplateObjectType): - - class Meta: - model = models.FrontPortTemplate - fields = '__all__' - filterset_class = filtersets.FrontPortTemplateFilterSet + pass +@strawberry_django.type( + models.Interface, + exclude=('_path',), + filters=InterfaceFilter +) class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - class Meta: - model = models.Interface - exclude = ('_path',) - filterset_class = filtersets.InterfaceFilterSet - def resolve_poe_mode(self, info): return self.poe_mode or None @@ -264,13 +276,13 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa return self.rf_channel or None +@strawberry_django.type( + models.InterfaceTemplate, + fields='__all__', + filters=InterfaceTemplateFilter +) class InterfaceTemplateType(ComponentTemplateObjectType): - class Meta: - model = models.InterfaceTemplate - fields = '__all__' - filterset_class = filtersets.InterfaceTemplateFilterSet - def resolve_poe_mode(self, info): return self.poe_mode or None @@ -281,97 +293,105 @@ class InterfaceTemplateType(ComponentTemplateObjectType): return self.rf_role or None +@strawberry_django.type( + models.InventoryItem, + exclude=('component_type', 'component_id'), + filters=InventoryItemFilter +) class InventoryItemType(ComponentObjectType): component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') - class Meta: - model = models.InventoryItem - exclude = ('component_type', 'component_id') - filterset_class = filtersets.InventoryItemFilterSet - +@strawberry_django.type( + models.InventoryItemRole, + fields='__all__', + filters=InventoryItemRoleFilter +) class InventoryItemRoleType(OrganizationalObjectType): - - class Meta: - model = models.InventoryItemRole - fields = '__all__' - filterset_class = filtersets.InventoryItemRoleFilterSet + pass +@strawberry_django.type( + models.Location, + fields='__all__', + filters=LocationFilter +) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): - - class Meta: - model = models.Location - fields = '__all__' - filterset_class = filtersets.LocationFilterSet + pass +@strawberry_django.type( + models.Manufacturer, + fields='__all__', + filters=ManufacturerFilter +) class ManufacturerType(OrganizationalObjectType, ContactsMixin): - - class Meta: - model = models.Manufacturer - fields = '__all__' - filterset_class = filtersets.ManufacturerFilterSet + pass +@strawberry_django.type( + models.Module, + fields='__all__', + filters=ModuleFilter +) class ModuleType(ComponentObjectType): - - class Meta: - model = models.Module - fields = '__all__' - filterset_class = filtersets.ModuleFilterSet + pass +@strawberry_django.type( + models.ModuleBay, + fields='__all__', + filters=ModuleBayFilter +) class ModuleBayType(ComponentObjectType): - - class Meta: - model = models.ModuleBay - fields = '__all__' - filterset_class = filtersets.ModuleBayFilterSet + pass +@strawberry_django.type( + models.ModuleBayTemplate, + fields='__all__', + filters=ModuleBayTemplateFilter +) class ModuleBayTemplateType(ComponentTemplateObjectType): - - class Meta: - model = models.ModuleBayTemplate - fields = '__all__' - filterset_class = filtersets.ModuleBayTemplateFilterSet + pass +@strawberry_django.type( + models.ModuleType, + fields='__all__', + filters=ModuleTypeFilter +) class ModuleTypeType(NetBoxObjectType): - class Meta: - model = models.ModuleType - fields = '__all__' - filterset_class = filtersets.ModuleTypeFilterSet - def resolve_weight_unit(self, info): return self.weight_unit or None +@strawberry_django.type( + models.Platform, + fields='__all__', + filters=PlatformFilter +) class PlatformType(OrganizationalObjectType): - - class Meta: - model = models.Platform - fields = '__all__' - filterset_class = filtersets.PlatformFilterSet + pass +@strawberry_django.type( + models.PowerFeed, + exclude=('_path',), + filters=PowerFeedFilter +) class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): - - class Meta: - model = models.PowerFeed - exclude = ('_path',) - filterset_class = filtersets.PowerFeedFilterSet + pass +@strawberry_django.type( + models.PowerOutlet, + exclude=('_path',), + filters=PowerOutletFilter +) class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - class Meta: - model = models.PowerOutlet - exclude = ('_path',) - filterset_class = filtersets.PowerOutletFilterSet - def resolve_feed_leg(self, info): return self.feed_leg or None @@ -379,13 +399,13 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) return self.type or None +@strawberry_django.type( + models.PowerOutletTemplate, + fields='__all__', + filters=PowerOutletTemplateFilter +) class PowerOutletTemplateType(ComponentTemplateObjectType): - class Meta: - model = models.PowerOutletTemplate - fields = '__all__' - filterset_class = filtersets.PowerOutletTemplateFilterSet - def resolve_feed_leg(self, info): return self.feed_leg or None @@ -393,43 +413,44 @@ class PowerOutletTemplateType(ComponentTemplateObjectType): return self.type or None +@strawberry_django.type( + models.PowerPanel, + fields='__all__', + filters=PowerPanelFilter +) class PowerPanelType(NetBoxObjectType, ContactsMixin): - - class Meta: - model = models.PowerPanel - fields = '__all__' - filterset_class = filtersets.PowerPanelFilterSet + pass +@strawberry_django.type( + models.PowerPort, + exclude=('_path',), + filters=PowerPortFilter +) class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - class Meta: - model = models.PowerPort - exclude = ('_path',) - filterset_class = filtersets.PowerPortFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.PowerPortTemplate, + fields='__all__', + filters=PowerPortTemplateFilter +) class PowerPortTemplateType(ComponentTemplateObjectType): - class Meta: - model = models.PowerPortTemplate - fields = '__all__' - filterset_class = filtersets.PowerPortTemplateFilterSet - def resolve_type(self, info): return self.type or None +@strawberry_django.type( + models.Rack, + fields='__all__', + filters=RackFilter +) class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): - class Meta: - model = models.Rack - fields = '__all__' - filterset_class = filtersets.RackFilterSet - def resolve_type(self, info): return self.type or None @@ -440,74 +461,82 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje return self.weight_unit or None +@strawberry_django.type( + models.RackReservation, + fields='__all__', + filters=RackReservationFilter +) class RackReservationType(NetBoxObjectType): - - class Meta: - model = models.RackReservation - fields = '__all__' - filterset_class = filtersets.RackReservationFilterSet + pass +@strawberry_django.type( + models.RackRole, + fields='__all__', + filters=RackRoleFilter +) class RackRoleType(OrganizationalObjectType): - - class Meta: - model = models.RackRole - fields = '__all__' - filterset_class = filtersets.RackRoleFilterSet + pass +@strawberry_django.type( + models.RearPort, + fields='__all__', + filters=RearPortFilter +) class RearPortType(ComponentObjectType, CabledObjectMixin): - - class Meta: - model = models.RearPort - fields = '__all__' - filterset_class = filtersets.RearPortFilterSet + pass +@strawberry_django.type( + models.RearPortTemplate, + fields='__all__', + filters=RearPortTemplateFilter +) class RearPortTemplateType(ComponentTemplateObjectType): - - class Meta: - model = models.RearPortTemplate - fields = '__all__' - filterset_class = filtersets.RearPortTemplateFilterSet + pass +@strawberry_django.type( + models.Region, + fields='__all__', + filters=RegionFilter +) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - - class Meta: - model = models.Region - fields = '__all__' - filterset_class = filtersets.RegionFilterSet + pass +@strawberry_django.type( + models.Site, + fields='__all__', + filters=SiteFilter +) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): asn = graphene.Field(BigInt) - class Meta: - model = models.Site - fields = '__all__' - filterset_class = filtersets.SiteFilterSet - +@strawberry_django.type( + models.SiteGroup, + fields='__all__', + filters=SiteGroupFilter +) class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - - class Meta: - model = models.SiteGroup - fields = '__all__' - filterset_class = filtersets.SiteGroupFilterSet + pass +@strawberry_django.type( + models.VirtualChassis, + fields='__all__', + filters=VirtualChassisFilter +) class VirtualChassisType(NetBoxObjectType): - - class Meta: - model = models.VirtualChassis - fields = '__all__' - filterset_class = filtersets.VirtualChassisFilterSet + pass +@strawberry_django.type( + models.VirtualDeviceContext, + fields='__all__', + filters=VirtualDeviceContextFilter +) class VirtualDeviceContextType(NetBoxObjectType): - - class Meta: - model = models.VirtualDeviceContext - fields = '__all__' - filterset_class = filtersets.VirtualDeviceContextFilterSet + pass diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 86f3179b7..5e69afa9d 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,8 +1,11 @@ +import strawberry +import strawberry_django + import strawberry from strawberry import auto import strawberry_django -from extras import filtersets, models +from extras import models from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType from .filters import * diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index b4350f9f2..1c827b0c0 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,8 +1,10 @@ -import graphene +import strawberry +import strawberry_django -from ipam import filtersets, models +from ipam import models from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType +from .filters import * __all__ = ( 'ASNType', @@ -46,142 +48,152 @@ class BaseIPAddressFamilyType: return IPAddressFamilyType(self.family) +@strawberry_django.type( + models.ASN, + fields='__all__', + filters=ProviderFilter +) class ASNType(NetBoxObjectType): asn = graphene.Field(BigInt) - class Meta: - model = models.ASN - fields = '__all__' - filterset_class = filtersets.ASNFilterSet - +@strawberry_django.type( + models.ASNRange, + fields='__all__', + filters=ASNRangeFilter +) class ASNRangeType(NetBoxObjectType): - - class Meta: - model = models.ASNRange - fields = '__all__' - filterset_class = filtersets.ASNRangeFilterSet + pass +@strawberry_django.type( + models.Aggregate, + fields='__all__', + filters=AggregateFilter +) class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): - - class Meta: - model = models.Aggregate - fields = '__all__' - filterset_class = filtersets.AggregateFilterSet + pass +@strawberry_django.type( + models.FHRPGroup, + fields='__all__', + filters=FHRPGroupFilter +) class FHRPGroupType(NetBoxObjectType): - 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.type( + models.FHRPGroupAssignment, + exclude=('interface_type', 'interface_id'), + filters=FHRPGroupAssignmentFilter +) class FHRPGroupAssignmentType(BaseObjectType): interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType') - class Meta: - model = models.FHRPGroupAssignment - exclude = ('interface_type', 'interface_id') - filterset_class = filtersets.FHRPGroupAssignmentFilterSet - +@strawberry_django.type( + models.IPAddress, + exclude=('assigned_object_type', 'assigned_object_id'), + filters=IPAddressFilter +) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType') - class Meta: - model = models.IPAddress - exclude = ('assigned_object_type', 'assigned_object_id') - filterset_class = filtersets.IPAddressFilterSet - def resolve_role(self, info): return self.role or None +@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 +@strawberry_django.type( + models.Prefix, + fields='__all__', + filters=PrefixFilter +) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): - - class Meta: - model = models.Prefix - fields = '__all__' - filterset_class = filtersets.PrefixFilterSet + pass +@strawberry_django.type( + models.RIR, + fields='__all__', + filters=RIRFilter +) class RIRType(OrganizationalObjectType): - - class Meta: - model = models.RIR - fields = '__all__' - filterset_class = filtersets.RIRFilterSet + pass +@strawberry_django.type( + models.Role, + fields='__all__', + filters=RoleFilter +) class RoleType(OrganizationalObjectType): - - class Meta: - model = models.Role - fields = '__all__' - filterset_class = filtersets.RoleFilterSet + pass +@strawberry_django.type( + models.RouteTarget, + fields='__all__', + filters=RouteTargetFilter +) class RouteTargetType(NetBoxObjectType): - - class Meta: - model = models.RouteTarget - fields = '__all__' - filterset_class = filtersets.RouteTargetFilterSet + pass +@strawberry_django.type( + models.Service, + fields='__all__', + filters=ServiceFilter +) class ServiceType(NetBoxObjectType): - - class Meta: - model = models.Service - fields = '__all__' - filterset_class = filtersets.ServiceFilterSet + pass +@strawberry_django.type( + models.ServiceTemplate, + fields='__all__', + filters=ServiceTemplateFilter +) class ServiceTemplateType(NetBoxObjectType): - - class Meta: - model = models.ServiceTemplate - fields = '__all__' - filterset_class = filtersets.ServiceTemplateFilterSet + pass +@strawberry_django.type( + models.VLAN, + fields='__all__', + filters=VLANFilter +) class VLANType(NetBoxObjectType): - - class Meta: - model = models.VLAN - fields = '__all__' - filterset_class = filtersets.VLANFilterSet + pass +@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.type( + models.VRF, + fields='__all__', + filters=VRFFilter +) class VRFType(NetBoxObjectType): - - class Meta: - model = models.VRF - fields = '__all__' - filterset_class = filtersets.VRFFilterSet + pass diff --git a/netbox/netbox/graphql/filters.py b/netbox/netbox/graphql/filters.py new file mode 100644 index 000000000..c9e3c5776 --- /dev/null +++ b/netbox/netbox/graphql/filters.py @@ -0,0 +1,21 @@ +import strawberry +import strawberry_django +from strawberry import auto + + +class ChangeLoggedModelFilter: + + def created_by_request(self, queryset): + return self.filter_by_request(queryset, "created_by_request", self.created_by_request) + + def updated_by_request(self, queryset): + return self.filter_by_request(queryset, "updated_by_request", self.updated_by_request) + + def modified_by_request(self, queryset): + return self.filter_by_request(queryset, "modified_by_request", self.modified_by_request) + + +class NetBoxModelFilter(ChangeLoggedModelFilter): + + def filter_q(self, queryset): + return self.search(queryset, None, self.q) diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 6a9e13d1c..5b33de143 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -9,25 +9,22 @@ from users.graphql.schema import UsersQuery @strawberry.type -class Query(CircuitsQuery, UsersQuery): +class Query( + UsersQuery, + # CircuitsQuery, + # CoreQuery, + # DCIMQuery, + # ExtrasQuery, + # IPAMQuery, + # TenancyQuery, + # VirtualizationQuery, + # VPNQuery, + # WirelessQuery, + # *registry['plugins']['graphql_schemas'], # Append plugin schemas + # graphene.ObjectType +): pass -# class Query( -# UsersQuery, -# CircuitsQuery, -# CoreQuery, -# DCIMQuery, -# ExtrasQuery, -# IPAMQuery, -# TenancyQuery, -# VirtualizationQuery, -# VPNQuery, -# WirelessQuery, -# *registry['plugins']['graphql_schemas'], # Append plugin schemas -# graphene.ObjectType -# ): -# pass - schema = strawberry.Schema( query=Query, diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index aab02b121..a1bdc4e47 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -1,8 +1,10 @@ -import graphene +import strawberry +import strawberry_django from extras.graphql.mixins import CustomFieldsMixin, TagsMixin -from tenancy import filtersets, models +from tenancy import models from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType +from .filters import * __all__ = ( 'ContactAssignmentType', @@ -25,53 +27,59 @@ class ContactAssignmentsMixin: # Tenants # +@strawberry_django.type( + models.Tenant, + fields='__all__', + filters=TenantFilter +) class TenantType(NetBoxObjectType): - - class Meta: - model = models.Tenant - fields = '__all__' - filterset_class = filtersets.TenantFilterSet + pass +@strawberry_django.type( + models.TenantGroup, + fields='__all__', + filters=TenantGroupFilter +) class TenantGroupType(OrganizationalObjectType): - - class Meta: - model = models.TenantGroup - fields = '__all__' - filterset_class = filtersets.TenantGroupFilterSet + pass # # Contacts # +@strawberry_django.type( + models.Contact, + fields='__all__', + filters=ContactFilter +) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): - - class Meta: - model = models.Contact - fields = '__all__' - filterset_class = filtersets.ContactFilterSet + pass +@strawberry_django.type( + models.ContactRole, + fields='__all__', + filters=ContactRoleFilter +) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): - - class Meta: - model = models.ContactRole - fields = '__all__' - filterset_class = filtersets.ContactRoleFilterSet + pass +@strawberry_django.type( + models.ContactGroup, + fields='__all__', + filters=ContactGroupFilter +) class ContactGroupType(OrganizationalObjectType): - - class Meta: - model = models.ContactGroup - fields = '__all__' - filterset_class = filtersets.ContactGroupFilterSet + pass +@strawberry_django.type( + models.ContactAssignment, + fields='__all__', + filters=ContactAssignmentFilter +) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): - - class Meta: - model = models.ContactAssignment - fields = '__all__' - filterset_class = filtersets.ContactAssignmentFilterSet + pass diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 9b97e1dc9..03d7ba37b 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,8 +1,12 @@ +import strawberry +import strawberry_django + from dcim.graphql.types import ComponentObjectType from extras.graphql.mixins import ConfigContextMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType -from virtualization import filtersets, models +from virtualization import models +from .filters import * __all__ = ( 'ClusterType', @@ -14,55 +18,59 @@ __all__ = ( ) +@strawberry_django.type( + models.Cluster, + fields='__all__', + filters=ClusterFilter +) class ClusterType(VLANGroupsMixin, NetBoxObjectType): - - class Meta: - model = models.Cluster - fields = '__all__' - filterset_class = filtersets.ClusterFilterSet + pass +@strawberry_django.type( + models.ClusterGroup, + fields='__all__', + filters=ClusterGroupFilter +) class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): - - class Meta: - model = models.ClusterGroup - fields = '__all__' - filterset_class = filtersets.ClusterGroupFilterSet + pass +@strawberry_django.type( + models.ClusterType, + fields='__all__', + filters=ClusterTypeFilter +) class ClusterTypeType(OrganizationalObjectType): - - class Meta: - model = models.ClusterType - fields = '__all__' - filterset_class = filtersets.ClusterTypeFilterSet + pass +@strawberry_django.type( + models.VirtualMachine, + fields='__all__', + filters=VirtualMachineFilter +) class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): - - class Meta: - model = models.VirtualMachine - fields = '__all__' - filterset_class = filtersets.VirtualMachineFilterSet + pass +@strawberry_django.type( + models.VMInterface, + fields='__all__', + filters=VMInterfaceFilter +) class VMInterfaceType(IPAddressesMixin, ComponentObjectType): - class Meta: - model = models.VMInterface - fields = '__all__' - filterset_class = filtersets.VMInterfaceFilterSet - def resolve_mode(self, info): return self.mode or None +@strawberry_django.type( + models.VirtualDisk, + fields='__all__', + filters=VirtualDiskFilter +) class VirtualDiskType(ComponentObjectType): - class Meta: - model = models.VirtualDisk - fields = '__all__' - filterset_class = filtersets.VirtualDiskFilterSet - def resolve_mode(self, info): return self.mode or None diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 0bfebb441..72919edfd 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -1,8 +1,10 @@ -import graphene +import strawberry +import strawberry_django from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType -from vpn import filtersets, models +from vpn import models +from .filters import * __all__ = ( 'IKEPolicyType', @@ -18,81 +20,91 @@ __all__ = ( ) +@strawberry_django.type( + models.TunnelGroup, + fields='__all__', + filters=TunnelGroupFilter +) class TunnelGroupType(OrganizationalObjectType): - - class Meta: - model = models.TunnelGroup - fields = '__all__' - filterset_class = filtersets.TunnelGroupFilterSet + pass +@strawberry_django.type( + models.TunnelTermination, + fields='__all__', + filters=TunnelTerminationFilter +) class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): - - class Meta: - model = models.TunnelTermination - fields = '__all__' - filterset_class = filtersets.TunnelTerminationFilterSet + pass +@strawberry_django.type( + models.Tunnel, + fields='__all__', + filters=TunnelFilter +) class TunnelType(NetBoxObjectType): - - class Meta: - model = models.Tunnel - fields = '__all__' - filterset_class = filtersets.TunnelFilterSet + pass +@strawberry_django.type( + models.IKEProposal, + fields='__all__', + filters=IKEProposalFilter +) class IKEProposalType(OrganizationalObjectType): - - class Meta: - model = models.IKEProposal - fields = '__all__' - filterset_class = filtersets.IKEProposalFilterSet + pass +@strawberry_django.type( + models.IKEPolicy, + fields='__all__', + filters=IKEPolicyFilter +) class IKEPolicyType(OrganizationalObjectType): - - class Meta: - model = models.IKEPolicy - fields = '__all__' - filterset_class = filtersets.IKEPolicyFilterSet + pass +@strawberry_django.type( + models.IPSecProposal, + fields='__all__', + filters=IPSecProposalFilter +) class IPSecProposalType(OrganizationalObjectType): - - class Meta: - model = models.IPSecProposal - fields = '__all__' - filterset_class = filtersets.IPSecProposalFilterSet + pass +@strawberry_django.type( + models.IPSecPolicy, + fields='__all__', + filters=IPSecPolicyFilter +) class IPSecPolicyType(OrganizationalObjectType): - - class Meta: - model = models.IPSecPolicy - fields = '__all__' - filterset_class = filtersets.IPSecPolicyFilterSet + pass +@strawberry_django.type( + models.IPSecProfile, + fields='__all__', + filters=IPSecProfileFilter +) class IPSecProfileType(OrganizationalObjectType): - - class Meta: - model = models.IPSecProfile - fields = '__all__' - filterset_class = filtersets.IPSecProfileFilterSet + pass +@strawberry_django.type( + models.L2VPN, + fields='__all__', + filters=L2VPNFilter +) class L2VPNType(ContactsMixin, NetBoxObjectType): - class Meta: - model = models.L2VPN - fields = '__all__' - filtersets_class = filtersets.L2VPNFilterSet + pass +@strawberry_django.type( + models.L2VPNTermination, + exclude=('assigned_object_type', 'assigned_object_id'), + filters=L2VPNTerminationFilter +) class L2VPNTerminationType(NetBoxObjectType): assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType') - - class Meta: - model = models.L2VPNTermination - exclude = ('assigned_object_type', 'assigned_object_id') - filtersets_class = filtersets.L2VPNTerminationFilterSet diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 2fc477dfa..7df0a46d3 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -1,5 +1,9 @@ -from wireless import filtersets, models +import strawberry +import strawberry_django + +from wireless import models from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType +from .filters import * __all__ = ( 'WirelessLANType', @@ -8,21 +12,22 @@ __all__ = ( ) +@strawberry_django.type( + models.WirelessLANGroup, + fields='__all__', + filters=WirelessLANGroupFilter +) class WirelessLANGroupType(OrganizationalObjectType): - - class Meta: - model = models.WirelessLANGroup - fields = '__all__' - filterset_class = filtersets.WirelessLANGroupFilterSet + pass +@strawberry_django.type( + models.WirelessLAN, + fields='__all__', + filters=WirelessLANFilter +) class WirelessLANType(NetBoxObjectType): - class Meta: - model = models.WirelessLAN - fields = '__all__' - filterset_class = filtersets.WirelessLANFilterSet - def resolve_auth_type(self, info): return self.auth_type or None @@ -30,13 +35,13 @@ class WirelessLANType(NetBoxObjectType): return self.auth_cipher or None +@strawberry_django.type( + models.WirelessLink, + fields='__all__', + filters=WirelessLinkFilter +) class WirelessLinkType(NetBoxObjectType): - class Meta: - model = models.WirelessLink - fields = '__all__' - filterset_class = filtersets.WirelessLinkFilterSet - def resolve_auth_type(self, info): return self.auth_type or None From fb4d63f8a236c7477a18b4d58754a9e6262d74f0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 6 Feb 2024 14:02:55 -0800 Subject: [PATCH 011/156] 9856 update strawberry types --- netbox/circuits/graphql/types.py | 2 +- netbox/dcim/graphql/types.py | 16 ++++++++++------ netbox/ipam/graphql/types.py | 17 ++++++++++------- netbox/netbox/graphql/schema.py | 2 +- netbox/tenancy/graphql/types.py | 14 +++++++------- netbox/vpn/graphql/types.py | 3 ++- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index fa7e5e3b4..b1c2fbbc7 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,7 +1,7 @@ import strawberry import strawberry_django -from circuits import filtersets, models +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 diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 145624145..5dc907bba 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -94,8 +94,8 @@ class ComponentTemplateObjectType( filters=CableFilter ) class CableType(NetBoxObjectType): - a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') + # a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') + # b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') class Meta: model = models.Cable @@ -121,7 +121,8 @@ class CableType(NetBoxObjectType): filters=CableTerminationFilter ) class CableTerminationType(NetBoxObjectType): - termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType') + # termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType') + pass @strawberry_django.type( @@ -206,7 +207,8 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): filters=InventoryItemTemplateFilter ) class InventoryItemTemplateType(ComponentTemplateObjectType): - component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType') + # component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType') + pass @strawberry_django.type( @@ -299,7 +301,8 @@ class InterfaceTemplateType(ComponentTemplateObjectType): filters=InventoryItemFilter ) class InventoryItemType(ComponentObjectType): - component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') + # component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') + pass @strawberry_django.type( @@ -512,7 +515,8 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): filters=SiteFilter ) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): - asn = graphene.Field(BigInt) + # asn = graphene.Field(BigInt) + pass @strawberry_django.type( diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 1c827b0c0..2fdec2273 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -28,8 +28,8 @@ __all__ = ( class IPAddressFamilyType(graphene.ObjectType): - value = graphene.Int() - label = graphene.String() + # value = graphene.Int() + # label = graphene.String() def __init__(self, value): self.value = value @@ -40,7 +40,7 @@ class BaseIPAddressFamilyType: """ Base type for models that need to expose their IPAddress family type. """ - family = graphene.Field(IPAddressFamilyType) + # family = graphene.Field(IPAddressFamilyType) def resolve_family(self, _): # Note that self, is an instance of models.IPAddress @@ -54,7 +54,8 @@ class BaseIPAddressFamilyType: filters=ProviderFilter ) class ASNType(NetBoxObjectType): - asn = graphene.Field(BigInt) + # asn = graphene.Field(BigInt) + pass @strawberry_django.type( @@ -92,7 +93,8 @@ class FHRPGroupType(NetBoxObjectType): filters=FHRPGroupAssignmentFilter ) class FHRPGroupAssignmentType(BaseObjectType): - interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType') + # interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType') + pass @strawberry_django.type( @@ -101,7 +103,7 @@ class FHRPGroupAssignmentType(BaseObjectType): filters=IPAddressFilter ) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): - assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType') + # assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType') def resolve_role(self, info): return self.role or None @@ -187,7 +189,8 @@ class VLANType(NetBoxObjectType): filters=VLANGroupFilter ) class VLANGroupType(OrganizationalObjectType): - scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType') + # scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType') + pass @strawberry_django.type( diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 5b33de143..f222a3f2c 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -11,7 +11,7 @@ from users.graphql.schema import UsersQuery @strawberry.type class Query( UsersQuery, - # CircuitsQuery, + CircuitsQuery, # CoreQuery, # DCIMQuery, # ExtrasQuery, diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index a1bdc4e47..45193e848 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -17,7 +17,7 @@ __all__ = ( class ContactAssignmentsMixin: - assignments = graphene.List('tenancy.graphql.types.ContactAssignmentType') + # assignments = graphene.List('tenancy.graphql.types.ContactAssignmentType') def resolve_assignments(self, info): return self.assignments.restrict(info.context.user, 'view') @@ -30,7 +30,7 @@ class ContactAssignmentsMixin: @strawberry_django.type( models.Tenant, fields='__all__', - filters=TenantFilter + # filters=TenantFilter ) class TenantType(NetBoxObjectType): pass @@ -39,7 +39,7 @@ class TenantType(NetBoxObjectType): @strawberry_django.type( models.TenantGroup, fields='__all__', - filters=TenantGroupFilter + # filters=TenantGroupFilter ) class TenantGroupType(OrganizationalObjectType): pass @@ -52,7 +52,7 @@ class TenantGroupType(OrganizationalObjectType): @strawberry_django.type( models.Contact, fields='__all__', - filters=ContactFilter + # filters=ContactFilter ) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): pass @@ -61,7 +61,7 @@ class ContactType(ContactAssignmentsMixin, NetBoxObjectType): @strawberry_django.type( models.ContactRole, fields='__all__', - filters=ContactRoleFilter + # filters=ContactRoleFilter ) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): pass @@ -70,7 +70,7 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): @strawberry_django.type( models.ContactGroup, fields='__all__', - filters=ContactGroupFilter + # filters=ContactGroupFilter ) class ContactGroupType(OrganizationalObjectType): pass @@ -79,7 +79,7 @@ class ContactGroupType(OrganizationalObjectType): @strawberry_django.type( models.ContactAssignment, fields='__all__', - filters=ContactAssignmentFilter + # filters=ContactAssignmentFilter ) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): pass diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 72919edfd..cf3cc6d27 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -107,4 +107,5 @@ class L2VPNType(ContactsMixin, NetBoxObjectType): filters=L2VPNTerminationFilter ) class L2VPNTerminationType(NetBoxObjectType): - assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType') + # assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType') + pass From cdcaa9055ef65721dda52a1f865e70fb94deda96 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 6 Feb 2024 14:52:29 -0800 Subject: [PATCH 012/156] 9856 core schema --- netbox/core/graphql/filters.py | 21 +++++++++++++++++++++ netbox/core/graphql/schema.py | 23 +++++++++-------------- netbox/core/graphql/types.py | 3 ++- netbox/netbox/graphql/schema.py | 21 +++++++++++---------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index e69de29bb..4e554331f 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -0,0 +1,21 @@ +import strawberry +import strawberry_django +from strawberry import auto +from core import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'DataFileFilter', + 'DataSourceFilter', +) + + +@strawberry_django.filter(models.DataFile, lookups=True) +class DataFileFilter(filtersets.DataFileFilterSet): + id: auto + + +@strawberry_django.filter(models.DataSource, lookups=True) +class DataSourceFilter(filtersets.DataSourceFilterSet): + id: auto diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py index 876faa442..7118da11b 100644 --- a/netbox/core/graphql/schema.py +++ b/netbox/core/graphql/schema.py @@ -1,20 +1,15 @@ -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: + data_file: DataFileType = strawberry_django.field() + 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) + data_source: DataSourceType = strawberry_django.field() + data_source_list: List[DataSourceType] = strawberry_django.field() diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index 02d8ab483..f4f1ebd46 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -13,7 +13,8 @@ __all__ = ( @strawberry_django.type( models.DataFile, - fields='__all__', + # fields='__all__', + exclude=('data',), # bug - temp filters=DataFileFilter ) class DataFileType(BaseObjectType): diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index f222a3f2c..5daec5973 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -2,6 +2,7 @@ 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 from users.graphql.schema import UsersQuery # from virtualization.graphql.schema import VirtualizationQuery # from vpn.graphql.schema import VPNQuery @@ -12,16 +13,16 @@ from users.graphql.schema import UsersQuery class Query( UsersQuery, CircuitsQuery, - # CoreQuery, - # DCIMQuery, - # ExtrasQuery, - # IPAMQuery, - # TenancyQuery, - # VirtualizationQuery, - # VPNQuery, - # WirelessQuery, - # *registry['plugins']['graphql_schemas'], # Append plugin schemas - # graphene.ObjectType + CoreQuery, + # DCIMQuery, + # ExtrasQuery, + # IPAMQuery, + # TenancyQuery, + # VirtualizationQuery, + # VPNQuery, + # WirelessQuery, + # *registry['plugins']['graphql_schemas'], # Append plugin schemas + # graphene.ObjectType ): pass From 460b57dbf78fdd00b14e7d67cf9dbe9546475c8c Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 7 Feb 2024 07:32:10 -0800 Subject: [PATCH 013/156] 9856 dcim schema --- netbox/dcim/graphql/filters.py | 255 ++++++++++++++++++++++++++++ netbox/dcim/graphql/schema.py | 292 ++++++++++---------------------- netbox/dcim/graphql/types.py | 111 +++++++----- netbox/netbox/graphql/schema.py | 3 +- 4 files changed, 416 insertions(+), 245 deletions(-) diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index e69de29bb..d70552924 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -0,0 +1,255 @@ +import strawberry +import strawberry_django +from strawberry import auto +from dcim import models, filtersets +from netbox.graphql import filters + + +__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) +class CableFilter(filtersets.CableFilterSet): + id: auto + + +@strawberry_django.filter(models.CableTermination, lookups=True) +class CableTerminationFilter(filtersets.CableTerminationFilterSet): + id: auto + + +@strawberry_django.filter(models.ConsolePort, lookups=True) +class ConsolePortFilter(filtersets.ConsolePortFilterSet): + id: auto + + +@strawberry_django.filter(models.ConsolePortTemplate, lookups=True) +class ConsolePortTemplateFilter(filtersets.ConsolePortTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.ConsoleServerPort, lookups=True) +class ConsoleServerPortFilter(filtersets.ConsoleServerPortFilterSet): + id: auto + + +@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) +class ConsoleServerPortTemplateFilter(filtersets.ConsoleServerPortTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.Device, lookups=True) +class DeviceFilter(filtersets.DeviceFilterSet): + id: auto + + +@strawberry_django.filter(models.DeviceBay, lookups=True) +class DeviceBayFilter(filtersets.DeviceBayFilterSet): + id: auto + + +@strawberry_django.filter(models.DeviceBayTemplate, lookups=True) +class DeviceBayTemplateFilter(filtersets.DeviceBayTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.InventoryItemTemplate, lookups=True) +class InventoryItemTemplateFilter(filtersets.InventoryItemTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.DeviceRole, lookups=True) +class DeviceRoleFilter(filtersets.DeviceRoleFilterSet): + id: auto + + +@strawberry_django.filter(models.DeviceType, lookups=True) +class DeviceTypeFilter(filtersets.DeviceTypeFilterSet): + id: auto + + +@strawberry_django.filter(models.FrontPort, lookups=True) +class FrontPortFilter(filtersets.FrontPortFilterSet): + id: auto + + +@strawberry_django.filter(models.FrontPortTemplate, lookups=True) +class FrontPortTemplateFilter(filtersets.FrontPortTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.Interface, lookups=True) +class InterfaceFilter(filtersets.InterfaceFilterSet): + id: auto + + +@strawberry_django.filter(models.InterfaceTemplate, lookups=True) +class InterfaceTemplateFilter(filtersets.InterfaceTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.InventoryItem, lookups=True) +class InventoryItemFilter(filtersets.InventoryItemFilterSet): + id: auto + + +@strawberry_django.filter(models.InventoryItemRole, lookups=True) +class InventoryItemRoleFilter(filtersets.InventoryItemRoleFilterSet): + id: auto + + +@strawberry_django.filter(models.Location, lookups=True) +class LocationFilter(filtersets.LocationFilterSet): + id: auto + + +@strawberry_django.filter(models.Manufacturer, lookups=True) +class ManufacturerFilter(filtersets.ManufacturerFilterSet): + id: auto + + +@strawberry_django.filter(models.Module, lookups=True) +class ModuleFilter(filtersets.ModuleFilterSet): + id: auto + + +@strawberry_django.filter(models.ModuleBay, lookups=True) +class ModuleBayFilter(filtersets.ModuleBayFilterSet): + id: auto + + +@strawberry_django.filter(models.ModuleBayTemplate, lookups=True) +class ModuleBayTemplateFilter(filtersets.ModuleBayTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.ModuleType, lookups=True) +class ModuleTypeFilter(filtersets.ModuleTypeFilterSet): + id: auto + + +@strawberry_django.filter(models.Platform, lookups=True) +class PlatformFilter(filtersets.PlatformFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerFeed, lookups=True) +class PowerFeedFilter(filtersets.PowerFeedFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerOutlet, lookups=True) +class PowerOutletFilter(filtersets.PowerOutletFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerOutletTemplate, lookups=True) +class PowerOutletTemplateFilter(filtersets.PowerOutletTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerPanel, lookups=True) +class PowerPanelFilter(filtersets.PowerPanelFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerPort, lookups=True) +class PowerPortFilter(filtersets.PowerPortFilterSet): + id: auto + + +@strawberry_django.filter(models.PowerPortTemplate, lookups=True) +class PowerPortTemplateFilter(filtersets.PowerPortTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.Rack, lookups=True) +class RackFilter(filtersets.RackFilterSet): + id: auto + + +@strawberry_django.filter(models.RackReservation, lookups=True) +class RackReservationFilter(filtersets.RackReservationFilterSet): + id: auto + + +@strawberry_django.filter(models.RackRole, lookups=True) +class RackRoleFilter(filtersets.RackRoleFilterSet): + id: auto + + +@strawberry_django.filter(models.RearPort, lookups=True) +class RearPortFilter(filtersets.RearPortFilterSet): + id: auto + + +@strawberry_django.filter(models.RearPortTemplate, lookups=True) +class RearPortTemplateFilter(filtersets.RearPortTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.Region, lookups=True) +class RegionFilter(filtersets.RegionFilterSet): + id: auto + + +@strawberry_django.filter(models.Site, lookups=True) +class SiteFilter(filtersets.SiteFilterSet): + id: auto + + +@strawberry_django.filter(models.SiteGroup, lookups=True) +class SiteGroupFilter(filtersets.SiteGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.VirtualChassis, lookups=True) +class VirtualChassisFilter(filtersets.VirtualChassisFilterSet): + id: auto + + +@strawberry_django.filter(models.VirtualDeviceContext, lookups=True) +class VirtualDeviceContextFilter(filtersets.VirtualDeviceContextFilterSet): + id: auto diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index 6d689ac2d..41a273868 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -1,249 +1,129 @@ -import graphene +from typing import List +import strawberry +import strawberry_django -from netbox.graphql.fields import ObjectField, ObjectListField +from circuits import models 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) +@strawberry.type +class DCIMQuery: + cable: CableType = strawberry_django.field() + cable_list: List[CableType] = strawberry_django.field() - def resolve_cable_list(root, info, **kwargs): - return gql_query_optimizer(models.Cable.objects.all(), info) + console_port: ConsolePortType = strawberry_django.field() + console_port_list: List[ConsolePortType] = strawberry_django.field() - console_port = ObjectField(ConsolePortType) - console_port_list = ObjectListField(ConsolePortType) + console_port_template: ConsolePortTemplateType = strawberry_django.field() + console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field() - def resolve_console_port_list(root, info, **kwargs): - return gql_query_optimizer(models.ConsolePort.objects.all(), info) + console_server_port: ConsoleServerPortType = strawberry_django.field() + console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field() - console_port_template = ObjectField(ConsolePortTemplateType) - console_port_template_list = ObjectListField(ConsolePortTemplateType) + console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field() + console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field() - def resolve_console_port_template_list(root, info, **kwargs): - return gql_query_optimizer(models.ConsolePortTemplate.objects.all(), info) + device: DeviceType = strawberry_django.field() + device_list: List[DeviceType] = strawberry_django.field() - console_server_port = ObjectField(ConsoleServerPortType) - console_server_port_list = ObjectListField(ConsoleServerPortType) + device_bay: DeviceBayType = strawberry_django.field() + device_bay_list: List[DeviceBayType] = strawberry_django.field() - def resolve_console_server_port_list(root, info, **kwargs): - return gql_query_optimizer(models.ConsoleServerPort.objects.all(), info) + device_bay_template: DeviceBayTemplateType = strawberry_django.field() + device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field() - console_server_port_template = ObjectField(ConsoleServerPortTemplateType) - console_server_port_template_list = ObjectListField(ConsoleServerPortTemplateType) + device_role: DeviceRoleType = strawberry_django.field() + device_role_list: List[DeviceRoleType] = strawberry_django.field() - def resolve_console_server_port_template_list(root, info, **kwargs): - return gql_query_optimizer(models.ConsoleServerPortTemplate.objects.all(), info) + device_type: DeviceTypeType = strawberry_django.field() + device_type_list: List[DeviceTypeType] = strawberry_django.field() - device = ObjectField(DeviceType) - device_list = ObjectListField(DeviceType) + front_port: FrontPortType = strawberry_django.field() + front_port_list: List[FrontPortType] = strawberry_django.field() - def resolve_device_list(root, info, **kwargs): - return gql_query_optimizer(models.Device.objects.all(), info) + front_port_template: FrontPortTemplateType = strawberry_django.field() + front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field() - device_bay = ObjectField(DeviceBayType) - device_bay_list = ObjectListField(DeviceBayType) + interface: InterfaceType = strawberry_django.field() + interface_list: List[InterfaceType] = strawberry_django.field() - def resolve_device_bay_list(root, info, **kwargs): - return gql_query_optimizer(models.DeviceBay.objects.all(), info) + interface_template: InterfaceTemplateType = strawberry_django.field() + interface_template_list: List[InterfaceTemplateType] = strawberry_django.field() - device_bay_template = ObjectField(DeviceBayTemplateType) - device_bay_template_list = ObjectListField(DeviceBayTemplateType) + inventory_item: InventoryItemType = strawberry_django.field() + inventory_item_list: List[InventoryItemType] = strawberry_django.field() - def resolve_device_bay_template_list(root, info, **kwargs): - return gql_query_optimizer(models.DeviceBayTemplate.objects.all(), info) + inventory_item_role: InventoryItemRoleType = strawberry_django.field() + inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field() - device_role = ObjectField(DeviceRoleType) - device_role_list = ObjectListField(DeviceRoleType) + inventory_item_template: InventoryItemTemplateType = strawberry_django.field() + inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field() - def resolve_device_role_list(root, info, **kwargs): - return gql_query_optimizer(models.DeviceRole.objects.all(), info) + location: LocationType = strawberry_django.field() + location_list: List[LocationType] = strawberry_django.field() - device_type = ObjectField(DeviceTypeType) - device_type_list = ObjectListField(DeviceTypeType) + manufacturer: ManufacturerType = strawberry_django.field() + manufacturer_list: List[ManufacturerType] = strawberry_django.field() - def resolve_device_type_list(root, info, **kwargs): - return gql_query_optimizer(models.DeviceType.objects.all(), info) + module: ModuleType = strawberry_django.field() + module_list: List[ModuleType] = strawberry_django.field() - front_port = ObjectField(FrontPortType) - front_port_list = ObjectListField(FrontPortType) + module_bay: ModuleBayType = strawberry_django.field() + module_bay_list: List[ModuleBayType] = strawberry_django.field() - def resolve_front_port_list(root, info, **kwargs): - return gql_query_optimizer(models.FrontPort.objects.all(), info) + module_bay_template: ModuleBayTemplateType = strawberry_django.field() + module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field() - front_port_template = ObjectField(FrontPortTemplateType) - front_port_template_list = ObjectListField(FrontPortTemplateType) + module_type: ModuleTypeType = strawberry_django.field() + module_type_list: List[ModuleTypeType] = strawberry_django.field() - def resolve_front_port_template_list(root, info, **kwargs): - return gql_query_optimizer(models.FrontPortTemplate.objects.all(), info) + platform: PlatformType = strawberry_django.field() + platform_list: List[PlatformType] = strawberry_django.field() - interface = ObjectField(InterfaceType) - interface_list = ObjectListField(InterfaceType) + power_feed: PowerFeedType = strawberry_django.field() + power_feed_list: List[PowerFeedType] = strawberry_django.field() - def resolve_interface_list(root, info, **kwargs): - return gql_query_optimizer(models.Interface.objects.all(), info) + power_outlet: PowerOutletType = strawberry_django.field() + power_outlet_list: List[PowerOutletType] = strawberry_django.field() - interface_template = ObjectField(InterfaceTemplateType) - interface_template_list = ObjectListField(InterfaceTemplateType) + power_outlet_template: PowerOutletTemplateType = strawberry_django.field() + power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field() - def resolve_interface_template_list(root, info, **kwargs): - return gql_query_optimizer(models.InterfaceTemplate.objects.all(), info) + power_panel: PowerPanelType = strawberry_django.field() + power_panel_list: List[PowerPanelType] = strawberry_django.field() - inventory_item = ObjectField(InventoryItemType) - inventory_item_list = ObjectListField(InventoryItemType) + power_port: PowerPortType = strawberry_django.field() + power_port_list: List[PowerPortType] = strawberry_django.field() - def resolve_inventory_item_list(root, info, **kwargs): - return gql_query_optimizer(models.InventoryItem.objects.all(), info) + power_port_template: PowerPortTemplateType = strawberry_django.field() + power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field() - inventory_item_role = ObjectField(InventoryItemRoleType) - inventory_item_role_list = ObjectListField(InventoryItemRoleType) + rack: RackType = strawberry_django.field() + rack_list: List[RackType] = strawberry_django.field() - def resolve_inventory_item_role_list(root, info, **kwargs): - return gql_query_optimizer(models.InventoryItemRole.objects.all(), info) + rack_reservation: RackReservationType = strawberry_django.field() + rack_reservation_list: List[RackReservationType] = strawberry_django.field() - inventory_item_template = ObjectField(InventoryItemTemplateType) - inventory_item_template_list = ObjectListField(InventoryItemTemplateType) + rack_role: RackRoleType = strawberry_django.field() + rack_role_list: List[RackRoleType] = strawberry_django.field() - def resolve_inventory_item_template_list(root, info, **kwargs): - return gql_query_optimizer(models.InventoryItemTemplate.objects.all(), info) + rear_port: RearPortType = strawberry_django.field() + rear_port_list: List[RearPortType] = strawberry_django.field() - location = ObjectField(LocationType) - location_list = ObjectListField(LocationType) + rear_port_template: RearPortTemplateType = strawberry_django.field() + rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field() - def resolve_location_list(root, info, **kwargs): - return gql_query_optimizer(models.Location.objects.all(), info) + region: RegionType = strawberry_django.field() + region_list: List[RegionType] = strawberry_django.field() - manufacturer = ObjectField(ManufacturerType) - manufacturer_list = ObjectListField(ManufacturerType) + site: SiteType = strawberry_django.field() + site_list: List[SiteType] = strawberry_django.field() - def resolve_manufacturer_list(root, info, **kwargs): - return gql_query_optimizer(models.Manufacturer.objects.all(), info) + site_group: SiteGroupType = strawberry_django.field() + site_group_list: List[SiteGroupType] = strawberry_django.field() - module = ObjectField(ModuleType) - module_list = ObjectListField(ModuleType) + virtual_chassis: VirtualChassisType = strawberry_django.field() + virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field() - 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) + virtual_device_context: VirtualDeviceContextType = strawberry_django.field() + virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field() diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 5dc907bba..455ef616f 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -52,6 +52,7 @@ __all__ = ( 'SiteType', 'SiteGroupType', 'VirtualChassisType', + 'VirtualDeviceContextType', ) @@ -90,18 +91,14 @@ class ComponentTemplateObjectType( @strawberry_django.type( models.Cable, - fields='__all__', + # fields='__all__', + exclude=('color', ), # bug - temp filters=CableFilter ) class CableType(NetBoxObjectType): # a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') # b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - class Meta: - model = models.Cable - fields = '__all__' - filterset_class = filtersets.CableFilterSet - def resolve_type(self, info): return self.type or None @@ -127,7 +124,8 @@ class CableTerminationType(NetBoxObjectType): @strawberry_django.type( models.ConsolePort, - exclude=('_path',), + # exclude=('_path',), + exclude=('_path', '_name',), # bug - temp filters=ConsolePortFilter ) class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -138,7 +136,8 @@ class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) @strawberry_django.type( models.ConsolePortTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=ConsolePortTemplateFilter ) class ConsolePortTemplateType(ComponentTemplateObjectType): @@ -149,7 +148,8 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.ConsoleServerPort, - exclude=('_path',), + # exclude=('_path',), + exclude=('_path', '_name',), # bug - temp filters=ConsoleServerPortFilter ) class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -160,7 +160,8 @@ class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpoint @strawberry_django.type( models.ConsoleServerPortTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=ConsoleServerPortTemplateFilter ) class ConsoleServerPortTemplateType(ComponentTemplateObjectType): @@ -171,7 +172,12 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Device, - fields='__all__', + # fields='__all__', + exclude=( + '_name', 'console_port_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', + 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', + 'inventory_item_count' + ), # bug - temp filters=DeviceFilter ) class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): @@ -185,7 +191,8 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo @strawberry_django.type( models.DeviceBay, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=DeviceBayFilter ) class DeviceBayType(ComponentObjectType): @@ -194,7 +201,8 @@ class DeviceBayType(ComponentObjectType): @strawberry_django.type( models.DeviceBayTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=DeviceBayTemplateFilter ) class DeviceBayTemplateType(ComponentTemplateObjectType): @@ -203,7 +211,7 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.InventoryItemTemplate, - exclude=('component_type', 'component_id'), + exclude=('component_type', 'component_id', '_name', 'parent'), filters=InventoryItemTemplateFilter ) class InventoryItemTemplateType(ComponentTemplateObjectType): @@ -213,7 +221,8 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.DeviceRole, - fields='__all__', + # fields='__all__', + exclude=('color',), # bug - temp filters=DeviceRoleFilter ) class DeviceRoleType(OrganizationalObjectType): @@ -222,7 +231,13 @@ class DeviceRoleType(OrganizationalObjectType): @strawberry_django.type( models.DeviceType, - fields='__all__', + # fields='__all__', + exclude=( + 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', + 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', + 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', + 'inventory_item_template_count', + ), # bug - temp filters=DeviceTypeFilter ) class DeviceTypeType(NetBoxObjectType): @@ -239,7 +254,8 @@ class DeviceTypeType(NetBoxObjectType): @strawberry_django.type( models.FrontPort, - fields='__all__', + # fields='__all__', + exclude=('_name', 'color'), # bug - temp filters=FrontPortFilter ) class FrontPortType(ComponentObjectType, CabledObjectMixin): @@ -248,7 +264,8 @@ class FrontPortType(ComponentObjectType, CabledObjectMixin): @strawberry_django.type( models.FrontPortTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name', 'color'), # bug - temp filters=FrontPortTemplateFilter ) class FrontPortTemplateType(ComponentTemplateObjectType): @@ -257,7 +274,8 @@ class FrontPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Interface, - exclude=('_path',), + # fields='__all__', + exclude=('mac_address', '_name', 'wwn'), # bug - temp filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -280,7 +298,8 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa @strawberry_django.type( models.InterfaceTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=InterfaceTemplateFilter ) class InterfaceTemplateType(ComponentTemplateObjectType): @@ -297,7 +316,7 @@ class InterfaceTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.InventoryItem, - exclude=('component_type', 'component_id'), + exclude=('component_type', 'component_id', '_name', 'parent'), filters=InventoryItemFilter ) class InventoryItemType(ComponentObjectType): @@ -307,7 +326,8 @@ class InventoryItemType(ComponentObjectType): @strawberry_django.type( models.InventoryItemRole, - fields='__all__', + # fields='__all__', + exclude=('color', '_name'), # bug - temp filters=InventoryItemRoleFilter ) class InventoryItemRoleType(OrganizationalObjectType): @@ -316,7 +336,8 @@ class InventoryItemRoleType(OrganizationalObjectType): @strawberry_django.type( models.Location, - fields='__all__', + # fields='__all__', + exclude=('parent',), # bug - temp filters=LocationFilter ) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): @@ -343,7 +364,8 @@ class ModuleType(ComponentObjectType): @strawberry_django.type( models.ModuleBay, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=ModuleBayFilter ) class ModuleBayType(ComponentObjectType): @@ -352,7 +374,8 @@ class ModuleBayType(ComponentObjectType): @strawberry_django.type( models.ModuleBayTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=ModuleBayTemplateFilter ) class ModuleBayTemplateType(ComponentTemplateObjectType): @@ -390,7 +413,8 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.type( models.PowerOutlet, - exclude=('_path',), + # fields='__all__', + exclude=('_name',), # bug - temp filters=PowerOutletFilter ) class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -404,7 +428,8 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) @strawberry_django.type( models.PowerOutletTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=PowerOutletTemplateFilter ) class PowerOutletTemplateType(ComponentTemplateObjectType): @@ -427,7 +452,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): @strawberry_django.type( models.PowerPort, - exclude=('_path',), + exclude=('_path', '_name'), filters=PowerPortFilter ) class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -438,7 +463,8 @@ class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.type( models.PowerPortTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=PowerPortTemplateFilter ) class PowerPortTemplateType(ComponentTemplateObjectType): @@ -449,7 +475,8 @@ class PowerPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Rack, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=RackFilter ) class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): @@ -466,7 +493,8 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje @strawberry_django.type( models.RackReservation, - fields='__all__', + # fields='__all__', + exclude=('units',), # bug - temp filters=RackReservationFilter ) class RackReservationType(NetBoxObjectType): @@ -475,7 +503,8 @@ class RackReservationType(NetBoxObjectType): @strawberry_django.type( models.RackRole, - fields='__all__', + # fields='__all__', + exclude=('color',), # bug - temp filters=RackRoleFilter ) class RackRoleType(OrganizationalObjectType): @@ -484,7 +513,8 @@ class RackRoleType(OrganizationalObjectType): @strawberry_django.type( models.RearPort, - fields='__all__', + # fields='__all__', + exclude=('_name', 'color'), # bug - temp filters=RearPortFilter ) class RearPortType(ComponentObjectType, CabledObjectMixin): @@ -493,7 +523,8 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): @strawberry_django.type( models.RearPortTemplate, - fields='__all__', + # fields='__all__', + exclude=('_name', 'color'), # bug - temp filters=RearPortTemplateFilter ) class RearPortTemplateType(ComponentTemplateObjectType): @@ -502,7 +533,8 @@ class RearPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Region, - fields='__all__', + # fields='__all__', + exclude=('parent',), # bug - temp filters=RegionFilter ) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @@ -511,7 +543,8 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @strawberry_django.type( models.Site, - fields='__all__', + # fields='__all__', + exclude=('_name', 'time_zone'), # bug - temp filters=SiteFilter ) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): @@ -521,7 +554,8 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje @strawberry_django.type( models.SiteGroup, - fields='__all__', + # fields='__all__', + exclude=('parent',), # bug - temp filters=SiteGroupFilter ) class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @@ -530,7 +564,8 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @strawberry_django.type( models.VirtualChassis, - fields='__all__', + # fields='__all__', + exclude=('member_count',), # bug - temp filters=VirtualChassisFilter ) class VirtualChassisType(NetBoxObjectType): diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 5daec5973..43f1a5eb0 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -3,6 +3,7 @@ from strawberry_django.optimizer import DjangoOptimizerExtension from strawberry.schema.config import StrawberryConfig from circuits.graphql.schema import CircuitsQuery from core.graphql.schema import CoreQuery +from dcim.graphql.schema import DCIMQuery from users.graphql.schema import UsersQuery # from virtualization.graphql.schema import VirtualizationQuery # from vpn.graphql.schema import VPNQuery @@ -14,7 +15,7 @@ class Query( UsersQuery, CircuitsQuery, CoreQuery, - # DCIMQuery, + DCIMQuery, # ExtrasQuery, # IPAMQuery, # TenancyQuery, From ed1e1ae93938d33bca74f996f04e4def111d29f0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 7 Feb 2024 09:03:22 -0800 Subject: [PATCH 014/156] 9856 extras schema --- netbox/extras/graphql/schema.py | 93 ++++++++++----------------------- netbox/extras/graphql/types.py | 3 +- netbox/netbox/graphql/schema.py | 3 +- 3 files changed, 33 insertions(+), 66 deletions(-) diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index 09e399e37..14069021b 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -1,80 +1,45 @@ -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: + config_context: ConfigContextType = strawberry_django.field() + 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) + config_template: ConfigTemplateType = strawberry_django.field() + config_template_list: List[ConfigTemplateType] = strawberry_django.field() - config_template = ObjectField(ConfigTemplateType) - config_template_list = ObjectListField(ConfigTemplateType) + custom_field: CustomFieldType = strawberry_django.field() + 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) + custom_field_choice_set: CustomFieldChoiceSetType = strawberry_django.field() + custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field() - custom_field = ObjectField(CustomFieldType) - custom_field_list = ObjectListField(CustomFieldType) + custom_link: CustomLinkType = strawberry_django.field() + 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) + export_template: ExportTemplateType = strawberry_django.field() + export_template_list: List[ExportTemplateType] = strawberry_django.field() - custom_field_choice_set = ObjectField(CustomFieldChoiceSetType) - custom_field_choice_set_list = ObjectListField(CustomFieldChoiceSetType) + image_attachment: ImageAttachmentType = strawberry_django.field() + 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) + saved_filter: SavedFilterType = strawberry_django.field() + saved_filter_list: List[SavedFilterType] = strawberry_django.field() - custom_link = ObjectField(CustomLinkType) - custom_link_list = ObjectListField(CustomLinkType) + journal_entry: JournalEntryType = strawberry_django.field() + 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) + tag: TagType = strawberry_django.field() + tag_list: List[TagType] = strawberry_django.field() - export_template = ObjectField(ExportTemplateType) - export_template_list = ObjectListField(ExportTemplateType) + webhook: WebhookType = strawberry_django.field() + 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) + event_rule: EventRuleType = strawberry_django.field() + event_rule_list: List[EventRuleType] = strawberry_django.field() diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 5e69afa9d..e89802b85 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -56,7 +56,8 @@ class CustomFieldType(ObjectType): @strawberry_django.type( models.CustomFieldChoiceSet, - fields='__all__', + # fields='__all__', + exclude=('extra_choices', ), # bug - temp filters=CustomFieldChoiceSetFilter ) class CustomFieldChoiceSetType(ObjectType): diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 43f1a5eb0..9ccc0b8a7 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -4,6 +4,7 @@ from strawberry.schema.config import StrawberryConfig from circuits.graphql.schema import CircuitsQuery from core.graphql.schema import CoreQuery from dcim.graphql.schema import DCIMQuery +from extras.graphql.schema import ExtrasQuery from users.graphql.schema import UsersQuery # from virtualization.graphql.schema import VirtualizationQuery # from vpn.graphql.schema import VPNQuery @@ -16,7 +17,7 @@ class Query( CircuitsQuery, CoreQuery, DCIMQuery, - # ExtrasQuery, + ExtrasQuery, # IPAMQuery, # TenancyQuery, # VirtualizationQuery, From d3fc026b5d1164167ec7e9df3e66d77253cd1faf Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 7 Feb 2024 15:43:29 -0800 Subject: [PATCH 015/156] 9856 ipam and tenant schema --- netbox/ipam/graphql/filters.py | 105 ++++++++++++++++++++++++++ netbox/ipam/graphql/schema.py | 121 +++++++++--------------------- netbox/ipam/graphql/types.py | 27 ++++--- netbox/netbox/graphql/schema.py | 11 ++- netbox/tenancy/graphql/filters.py | 45 +++++++++++ netbox/tenancy/graphql/schema.py | 53 +++++-------- netbox/tenancy/graphql/types.py | 18 +++-- 7 files changed, 239 insertions(+), 141 deletions(-) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index e69de29bb..04254cbf0 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -0,0 +1,105 @@ +import strawberry +import strawberry_django +from strawberry import auto +from ipam import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'ASNFilter', + 'ASNRangeFilter', + 'AggregateFilter', + 'FHRPGroupFilter', + 'FHRPGroupAssignmentFilter', + 'IPAddressFilter', + 'IPRangeFilter', + 'PrefixFilter', + 'RIRFilter', + 'RoleFilter', + 'RouteTargetFilter', + 'ServiceFilter', + 'ServiceTemplateFilter', + 'VLANFilter', + 'VLANGroupFilter', + 'VRFFilter', +) + + +@strawberry_django.filter(models.ASN, lookups=True) +class ASNFilter(filtersets.ASNFilterSet): + id: auto + + +@strawberry_django.filter(models.ASNRange, lookups=True) +class ASNRangeFilter(filtersets.ASNRangeFilterSet): + id: auto + + +@strawberry_django.filter(models.Aggregate, lookups=True) +class AggregateFilter(filtersets.AggregateFilterSet): + id: auto + + +@strawberry_django.filter(models.FHRPGroup, lookups=True) +class FHRPGroupFilter(filtersets.FHRPGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True) +class FHRPGroupAssignmentFilter(filtersets.FHRPGroupAssignmentFilterSet): + id: auto + + +@strawberry_django.filter(models.IPAddress, lookups=True) +class IPAddressFilter(filtersets.IPAddressFilterSet): + id: auto + + +@strawberry_django.filter(models.IPRange, lookups=True) +class IPRangeFilter(filtersets.IPRangeFilterSet): + id: auto + + +@strawberry_django.filter(models.Prefix, lookups=True) +class PrefixFilter(filtersets.PrefixFilterSet): + id: auto + + +@strawberry_django.filter(models.RIR, lookups=True) +class RIRFilter(filtersets.RIRFilterSet): + id: auto + + +@strawberry_django.filter(models.Role, lookups=True) +class RoleFilter(filtersets.RoleFilterSet): + id: auto + + +@strawberry_django.filter(models.RouteTarget, lookups=True) +class RouteTargetFilter(filtersets.RouteTargetFilterSet): + id: auto + + +@strawberry_django.filter(models.Service, lookups=True) +class ServiceFilter(filtersets.ServiceFilterSet): + id: auto + + +@strawberry_django.filter(models.ServiceTemplate, lookups=True) +class ServiceTemplateFilter(filtersets.ServiceTemplateFilterSet): + id: auto + + +@strawberry_django.filter(models.VLAN, lookups=True) +class VLANFilter(filtersets.VLANFilterSet): + id: auto + + +@strawberry_django.filter(models.VLANGroup, lookups=True) +class VLANGroupFilter(filtersets.VLANGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.VRF, lookups=True) +class VRFFilter(filtersets.VRFFilterSet): + id: auto diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index 6627c540e..230520f5d 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -1,104 +1,57 @@ -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: + asn: ASNType = strawberry_django.field() + asn_list: List[ASNType] = strawberry_django.field() - def resolve_asn_list(root, info, **kwargs): - return gql_query_optimizer(models.ASN.objects.all(), info) + asn_range: ASNRangeType = strawberry_django.field() + asn_range_list: List[ASNRangeType] = strawberry_django.field() - asn_range = ObjectField(ASNRangeType) - asn_range_list = ObjectListField(ASNRangeType) + aggregate: AggregateType = strawberry_django.field() + aggregate_list: List[AggregateType] = strawberry_django.field() - def resolve_asn_range_list(root, info, **kwargs): - return gql_query_optimizer(models.ASNRange.objects.all(), info) + ip_address: IPAddressType = strawberry_django.field() + ip_address_list: List[IPAddressType] = strawberry_django.field() - aggregate = ObjectField(AggregateType) - aggregate_list = ObjectListField(AggregateType) + ip_range: IPRangeType = strawberry_django.field() + ip_range_list: List[IPRangeType] = strawberry_django.field() - def resolve_aggregate_list(root, info, **kwargs): - return gql_query_optimizer(models.Aggregate.objects.all(), info) + prefix: PrefixType = strawberry_django.field() + prefix_list: List[PrefixType] = strawberry_django.field() - ip_address = ObjectField(IPAddressType) - ip_address_list = ObjectListField(IPAddressType) + rir: RIRType = strawberry_django.field() + rir_list: List[RIRType] = strawberry_django.field() - def resolve_ip_address_list(root, info, **kwargs): - return gql_query_optimizer(models.IPAddress.objects.all(), info) + role: RoleType = strawberry_django.field() + role_list: List[RoleType] = strawberry_django.field() - ip_range = ObjectField(IPRangeType) - ip_range_list = ObjectListField(IPRangeType) + route_target: RouteTargetType = strawberry_django.field() + 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) + service: ServiceType = strawberry_django.field() + service_list: List[ServiceType] = strawberry_django.field() - prefix = ObjectField(PrefixType) - prefix_list = ObjectListField(PrefixType) + service_template: ServiceTemplateType = strawberry_django.field() + service_template_list: List[ServiceTemplateType] = strawberry_django.field() - def resolve_prefix_list(root, info, **kwargs): - return gql_query_optimizer(models.Prefix.objects.all(), info) + fhrp_group: FHRPGroupType = strawberry_django.field() + fhrp_group_list: List[FHRPGroupType] = strawberry_django.field() - rir = ObjectField(RIRType) - rir_list = ObjectListField(RIRType) + fhrp_group_assignment: FHRPGroupAssignmentType = strawberry_django.field() + 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) + vlan: VLANType = strawberry_django.field() + vlan_list: List[VLANType] = strawberry_django.field() - role = ObjectField(RoleType) - role_list = ObjectListField(RoleType) + vlan_group: VLANGroupType = strawberry_django.field() + 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) + vrf: VRFType = strawberry_django.field() + vrf_list: List[VRFType] = strawberry_django.field() diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 2fdec2273..3f3991f92 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -26,7 +26,7 @@ __all__ = ( ) -class IPAddressFamilyType(graphene.ObjectType): +class IPAddressFamilyType: # value = graphene.Int() # label = graphene.String() @@ -50,8 +50,9 @@ class BaseIPAddressFamilyType: @strawberry_django.type( models.ASN, - fields='__all__', - filters=ProviderFilter + # fields='__all__', + exclude=('asn',), # bug - temp + filters=ASNFilter ) class ASNType(NetBoxObjectType): # asn = graphene.Field(BigInt) @@ -60,7 +61,8 @@ class ASNType(NetBoxObjectType): @strawberry_django.type( models.ASNRange, - fields='__all__', + # fields='__all__', + exclude=('start', 'end',), # bug - temp filters=ASNRangeFilter ) class ASNRangeType(NetBoxObjectType): @@ -69,7 +71,8 @@ class ASNRangeType(NetBoxObjectType): @strawberry_django.type( models.Aggregate, - fields='__all__', + # fields='__all__', + exclude=('prefix',), # bug - temp filters=AggregateFilter ) class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): @@ -99,7 +102,7 @@ class FHRPGroupAssignmentType(BaseObjectType): @strawberry_django.type( models.IPAddress, - exclude=('assigned_object_type', 'assigned_object_id'), + exclude=('assigned_object_type', 'assigned_object_id', 'address'), filters=IPAddressFilter ) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): @@ -111,7 +114,8 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): @strawberry_django.type( models.IPRange, - fields='__all__', + # fields='__all__', + exclude=('start_address', 'end_address',), # bug - temp filters=IPRangeFilter ) class IPRangeType(NetBoxObjectType): @@ -122,7 +126,8 @@ class IPRangeType(NetBoxObjectType): @strawberry_django.type( models.Prefix, - fields='__all__', + # fields='__all__', + exclude=('prefix',), # bug - temp filters=PrefixFilter ) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): @@ -158,7 +163,8 @@ class RouteTargetType(NetBoxObjectType): @strawberry_django.type( models.Service, - fields='__all__', + # fields='__all__', + exclude=('ports',), # bug - temp filters=ServiceFilter ) class ServiceType(NetBoxObjectType): @@ -167,7 +173,8 @@ class ServiceType(NetBoxObjectType): @strawberry_django.type( models.ServiceTemplate, - fields='__all__', + # fields='__all__', + exclude=('ports',), # bug - temp filters=ServiceTemplateFilter ) class ServiceTemplateType(NetBoxObjectType): diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 9ccc0b8a7..018869bbe 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -1,10 +1,14 @@ 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 from dcim.graphql.schema import DCIMQuery from extras.graphql.schema import ExtrasQuery +from ipam.graphql.schema import IPAMQuery +from netbox.registry import registry +from tenancy.graphql.schema import TenancyQuery from users.graphql.schema import UsersQuery # from virtualization.graphql.schema import VirtualizationQuery # from vpn.graphql.schema import VPNQuery @@ -18,13 +22,12 @@ class Query( CoreQuery, DCIMQuery, ExtrasQuery, - # IPAMQuery, - # TenancyQuery, + IPAMQuery, + TenancyQuery, # VirtualizationQuery, # VPNQuery, # WirelessQuery, - # *registry['plugins']['graphql_schemas'], # Append plugin schemas - # graphene.ObjectType + *registry['plugins']['graphql_schemas'], # Append plugin schemas ): pass diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index e69de29bb..a7d3c2f26 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -0,0 +1,45 @@ +import strawberry +import strawberry_django +from strawberry import auto +from tenancy import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'TenantFilter', + 'TenantGroupFilter', + 'ContactFilter', + 'ContactRoleFilter', + 'ContactGroupFilter', + 'ContactAssignmentFilter', +) + + +@strawberry_django.filter(models.Tenant, lookups=True) +class TenantFilter(filtersets.TenantFilterSet): + id: auto + + +@strawberry_django.filter(models.TenantGroup, lookups=True) +class TenantGroupFilter(filtersets.TenantGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.Contact, lookups=True) +class ContactFilter(filtersets.ContactFilterSet): + id: auto + + +@strawberry_django.filter(models.ContactRole, lookups=True) +class ContactRoleFilter(filtersets.ContactRoleFilterSet): + id: auto + + +@strawberry_django.filter(models.ContactGroup, lookups=True) +class ContactGroupFilter(filtersets.ContactGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.ContactAssignment, lookups=True) +class ContactAssignmentFilter(filtersets.ContactAssignmentFilterSet): + id: auto diff --git a/netbox/tenancy/graphql/schema.py b/netbox/tenancy/graphql/schema.py index 8c4648820..e82b3ad9c 100644 --- a/netbox/tenancy/graphql/schema.py +++ b/netbox/tenancy/graphql/schema.py @@ -1,44 +1,27 @@ -import graphene +from typing import List +import strawberry +import strawberry_django -from netbox.graphql.fields import ObjectField, ObjectListField -from tenancy import models +from circuits import models from .types import * -from utilities.graphql_optimizer import gql_query_optimizer -class TenancyQuery(graphene.ObjectType): - tenant = ObjectField(TenantType) - tenant_list = ObjectListField(TenantType) +@strawberry.type +class TenancyQuery: + tenant: TenantType = strawberry_django.field() + tenant_list: List[TenantType] = strawberry_django.field() - def resolve_tenant_list(root, info, **kwargs): - return gql_query_optimizer(models.Tenant.objects.all(), info) + tenant_group: TenantGroupType = strawberry_django.field() + tenant_group_list: List[TenantGroupType] = strawberry_django.field() - tenant_group = ObjectField(TenantGroupType) - tenant_group_list = ObjectListField(TenantGroupType) + contact: ContactType = strawberry_django.field() + contact_list: List[ContactType] = strawberry_django.field() - def resolve_tenant_group_list(root, info, **kwargs): - return gql_query_optimizer(models.TenantGroup.objects.all(), info) + contact_role: ContactRoleType = strawberry_django.field() + contact_role_list: List[ContactRoleType] = strawberry_django.field() - contact = ObjectField(ContactType) - contact_list = ObjectListField(ContactType) + contact_group: ContactGroupType = strawberry_django.field() + contact_group_list: List[ContactGroupType] = strawberry_django.field() - def resolve_contact_list(root, info, **kwargs): - return gql_query_optimizer(models.Contact.objects.all(), info) - - contact_role = ObjectField(ContactRoleType) - contact_role_list = ObjectListField(ContactRoleType) - - def resolve_contact_role_list(root, info, **kwargs): - return gql_query_optimizer(models.ContactRole.objects.all(), info) - - contact_group = ObjectField(ContactGroupType) - contact_group_list = ObjectListField(ContactGroupType) - - def resolve_contact_group_list(root, info, **kwargs): - return gql_query_optimizer(models.ContactGroup.objects.all(), info) - - contact_assignment = ObjectField(ContactAssignmentType) - contact_assignment_list = ObjectListField(ContactAssignmentType) - - def resolve_contact_assignment_list(root, info, **kwargs): - return gql_query_optimizer(models.ContactAssignment.objects.all(), info) + contact_assignment: ContactAssignmentType = strawberry_django.field() + contact_assignment_list: List[ContactAssignmentType] = strawberry_django.field() diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 45193e848..307a9000d 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -30,7 +30,7 @@ class ContactAssignmentsMixin: @strawberry_django.type( models.Tenant, fields='__all__', - # filters=TenantFilter + filters=TenantFilter ) class TenantType(NetBoxObjectType): pass @@ -38,8 +38,9 @@ class TenantType(NetBoxObjectType): @strawberry_django.type( models.TenantGroup, - fields='__all__', - # filters=TenantGroupFilter + # fields='__all__', + exclude=('parent',), # bug - temp + filters=TenantGroupFilter ) class TenantGroupType(OrganizationalObjectType): pass @@ -52,7 +53,7 @@ class TenantGroupType(OrganizationalObjectType): @strawberry_django.type( models.Contact, fields='__all__', - # filters=ContactFilter + filters=ContactFilter ) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): pass @@ -61,7 +62,7 @@ class ContactType(ContactAssignmentsMixin, NetBoxObjectType): @strawberry_django.type( models.ContactRole, fields='__all__', - # filters=ContactRoleFilter + filters=ContactRoleFilter ) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): pass @@ -69,8 +70,9 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): @strawberry_django.type( models.ContactGroup, - fields='__all__', - # filters=ContactGroupFilter + # fields='__all__', + exclude=('parent',), # bug - temp + filters=ContactGroupFilter ) class ContactGroupType(OrganizationalObjectType): pass @@ -79,7 +81,7 @@ class ContactGroupType(OrganizationalObjectType): @strawberry_django.type( models.ContactAssignment, fields='__all__', - # filters=ContactAssignmentFilter + filters=ContactAssignmentFilter ) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): pass From 48b0cdd04ab1be99b62d5b4798d498beaa8baf4d Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 7 Feb 2024 16:07:15 -0800 Subject: [PATCH 016/156] 9856 virtualization, vpn, wireless schema --- netbox/netbox/graphql/schema.py | 12 ++-- netbox/virtualization/graphql/filters.py | 45 +++++++++++++ netbox/virtualization/graphql/schema.py | 53 ++++++---------- netbox/virtualization/graphql/types.py | 9 ++- netbox/vpn/graphql/filters.py | 69 ++++++++++++++++++++ netbox/vpn/graphql/schema.py | 80 ++++++++---------------- netbox/wireless/graphql/filters.py | 27 ++++++++ netbox/wireless/graphql/schema.py | 32 ++++------ netbox/wireless/graphql/types.py | 3 +- 9 files changed, 210 insertions(+), 120 deletions(-) diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index 018869bbe..2b4c83405 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -10,9 +10,9 @@ from ipam.graphql.schema import IPAMQuery from netbox.registry import registry from tenancy.graphql.schema import TenancyQuery from users.graphql.schema import UsersQuery -# from virtualization.graphql.schema import VirtualizationQuery -# from vpn.graphql.schema import VPNQuery -# from wireless.graphql.schema import WirelessQuery +from virtualization.graphql.schema import VirtualizationQuery +from vpn.graphql.schema import VPNQuery +from wireless.graphql.schema import WirelessQuery @strawberry.type @@ -24,9 +24,9 @@ class Query( ExtrasQuery, IPAMQuery, TenancyQuery, - # VirtualizationQuery, - # VPNQuery, - # WirelessQuery, + VirtualizationQuery, + VPNQuery, + WirelessQuery, *registry['plugins']['graphql_schemas'], # Append plugin schemas ): pass diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py index e69de29bb..51671f7f0 100644 --- a/netbox/virtualization/graphql/filters.py +++ b/netbox/virtualization/graphql/filters.py @@ -0,0 +1,45 @@ +import strawberry +import strawberry_django +from strawberry import auto +from virtualization import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'ClusterFilter', + 'ClusterGroupFilter', + 'ClusterTypeFilter', + 'VirtualMachineFilter', + 'VMInterfaceFilter', + 'VirtualDiskFilter', +) + + +@strawberry_django.filter(models.Cluster, lookups=True) +class ClusterFilter(filtersets.ClusterFilterSet): + id: auto + + +@strawberry_django.filter(models.ClusterGroup, lookups=True) +class ClusterGroupFilter(filtersets.ClusterGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.ClusterType, lookups=True) +class ClusterTypeFilter(filtersets.ClusterTypeFilterSet): + id: auto + + +@strawberry_django.filter(models.VirtualMachine, lookups=True) +class VirtualMachineFilter(filtersets.VirtualMachineFilterSet): + id: auto + + +@strawberry_django.filter(models.VMInterface, lookups=True) +class VMInterfaceFilter(filtersets.VMInterfaceFilterSet): + id: auto + + +@strawberry_django.filter(models.VirtualDisk, lookups=True) +class VirtualDiskFilter(filtersets.VirtualDiskFilterSet): + id: auto diff --git a/netbox/virtualization/graphql/schema.py b/netbox/virtualization/graphql/schema.py index 1461faaeb..d226e2fea 100644 --- a/netbox/virtualization/graphql/schema.py +++ b/netbox/virtualization/graphql/schema.py @@ -1,44 +1,27 @@ -import graphene +from typing import List +import strawberry +import strawberry_django -from netbox.graphql.fields import ObjectField, ObjectListField -from .types import * -from utilities.graphql_optimizer import gql_query_optimizer from virtualization import models +from .types import * -class VirtualizationQuery(graphene.ObjectType): - cluster = ObjectField(ClusterType) - cluster_list = ObjectListField(ClusterType) +@strawberry.type +class VirtualizationQuery: + cluster: ClusterType = strawberry_django.field() + cluster_list: List[ClusterType] = strawberry_django.field() - def resolve_cluster_list(root, info, **kwargs): - return gql_query_optimizer(models.Cluster.objects.all(), info) + cluster_group: ClusterGroupType = strawberry_django.field() + cluster_group_list: List[ClusterGroupType] = strawberry_django.field() - cluster_group = ObjectField(ClusterGroupType) - cluster_group_list = ObjectListField(ClusterGroupType) + cluster_type: ClusterTypeType = strawberry_django.field() + cluster_type_list: List[ClusterTypeType] = strawberry_django.field() - def resolve_cluster_group_list(root, info, **kwargs): - return gql_query_optimizer(models.ClusterGroup.objects.all(), info) + virtual_machine: VirtualMachineType = strawberry_django.field() + virtual_machine_list: List[VirtualMachineType] = strawberry_django.field() - cluster_type = ObjectField(ClusterTypeType) - cluster_type_list = ObjectListField(ClusterTypeType) + vm_interface: VMInterfaceType = strawberry_django.field() + vm_interface_list: List[VMInterfaceType] = strawberry_django.field() - def resolve_cluster_type_list(root, info, **kwargs): - return gql_query_optimizer(models.ClusterType.objects.all(), info) - - virtual_machine = ObjectField(VirtualMachineType) - virtual_machine_list = ObjectListField(VirtualMachineType) - - def resolve_virtual_machine_list(root, info, **kwargs): - return gql_query_optimizer(models.VirtualMachine.objects.all(), info) - - vm_interface = ObjectField(VMInterfaceType) - vm_interface_list = ObjectListField(VMInterfaceType) - - def resolve_vm_interface_list(root, info, **kwargs): - return gql_query_optimizer(models.VMInterface.objects.all(), info) - - virtual_disk = ObjectField(VirtualDiskType) - virtual_disk_list = ObjectListField(VirtualDiskType) - - def resolve_virtual_disk_list(root, info, **kwargs): - return gql_query_optimizer(models.VirtualDisk.objects.all(), info) + virtual_disk: VirtualDiskType = strawberry_django.field() + virtual_disk_list: List[VirtualDiskType] = strawberry_django.field() diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 03d7ba37b..f4c92035b 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -47,7 +47,8 @@ class ClusterTypeType(OrganizationalObjectType): @strawberry_django.type( models.VirtualMachine, - fields='__all__', + # fields='__all__', + exclude=('_name', 'interface_count', 'virtual_disk_count',), # bug - temp filters=VirtualMachineFilter ) class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): @@ -56,7 +57,8 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): @strawberry_django.type( models.VMInterface, - fields='__all__', + # fields='__all__', + exclude=('mac_address', '_name',), # bug - temp filters=VMInterfaceFilter ) class VMInterfaceType(IPAddressesMixin, ComponentObjectType): @@ -67,7 +69,8 @@ class VMInterfaceType(IPAddressesMixin, ComponentObjectType): @strawberry_django.type( models.VirtualDisk, - fields='__all__', + # fields='__all__', + exclude=('_name',), # bug - temp filters=VirtualDiskFilter ) class VirtualDiskType(ComponentObjectType): diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py index e69de29bb..c3868cb52 100644 --- a/netbox/vpn/graphql/filters.py +++ b/netbox/vpn/graphql/filters.py @@ -0,0 +1,69 @@ +import strawberry +import strawberry_django +from strawberry import auto +from vpn import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'TunnelGroupFilter', + 'TunnelTerminationFilter', + 'TunnelFilter', + 'IKEProposalFilter', + 'IKEPolicyFilter', + 'IPSecProposalFilter', + 'IPSecPolicyFilter', + 'IPSecProfileFilter', + 'L2VPNFilter', + 'L2VPNTerminationFilter', +) + + +@strawberry_django.filter(models.TunnelGroup, lookups=True) +class TunnelGroupFilter(filtersets.TunnelGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.TunnelTermination, lookups=True) +class TunnelTerminationFilter(filtersets.TunnelTerminationFilterSet): + id: auto + + +@strawberry_django.filter(models.Tunnel, lookups=True) +class TunnelFilter(filtersets.TunnelFilterSet): + id: auto + + +@strawberry_django.filter(models.IKEProposal, lookups=True) +class IKEProposalFilter(filtersets.IKEProposalFilterSet): + id: auto + + +@strawberry_django.filter(models.IKEPolicy, lookups=True) +class IKEPolicyFilter(filtersets.IKEPolicyFilterSet): + id: auto + + +@strawberry_django.filter(models.IPSecProposal, lookups=True) +class IPSecProposalFilter(filtersets.IPSecProposalFilterSet): + id: auto + + +@strawberry_django.filter(models.IPSecPolicy, lookups=True) +class IPSecPolicyFilter(filtersets.IPSecPolicyFilterSet): + id: auto + + +@strawberry_django.filter(models.IPSecProfile, lookups=True) +class IPSecProfileFilter(filtersets.IPSecProfileFilterSet): + id: auto + + +@strawberry_django.filter(models.L2VPN, lookups=True) +class L2VPNFilter(filtersets.L2VPNFilterSet): + id: auto + + +@strawberry_django.filter(models.L2VPNTermination, lookups=True) +class L2VPNTerminationFilter(filtersets.L2VPNTerminationFilterSet): + id: auto diff --git a/netbox/vpn/graphql/schema.py b/netbox/vpn/graphql/schema.py index 6737957d4..996e29af3 100644 --- a/netbox/vpn/graphql/schema.py +++ b/netbox/vpn/graphql/schema.py @@ -1,69 +1,39 @@ -import graphene +from typing import List +import strawberry +import strawberry_django -from netbox.graphql.fields import ObjectField, ObjectListField -from utilities.graphql_optimizer import gql_query_optimizer from vpn import models from .types import * -class VPNQuery(graphene.ObjectType): +@strawberry.type +class VPNQuery: + ike_policy: IKEPolicyType = strawberry_django.field() + ike_policy_list: List[IKEPolicyType] = strawberry_django.field() - ike_policy = ObjectField(IKEPolicyType) - ike_policy_list = ObjectListField(IKEPolicyType) + ike_proposal: IKEProposalType = strawberry_django.field() + ike_proposal_list: List[IKEProposalType] = strawberry_django.field() - def resolve_ike_policy_list(root, info, **kwargs): - return gql_query_optimizer(models.IKEPolicy.objects.all(), info) + ipsec_policy: IPSecPolicyType = strawberry_django.field() + ipsec_policy_list: List[IPSecPolicyType] = strawberry_django.field() - ike_proposal = ObjectField(IKEProposalType) - ike_proposal_list = ObjectListField(IKEProposalType) + ipsec_profile: IPSecProfileType = strawberry_django.field() + ipsec_profile_list: List[IPSecProfileType] = strawberry_django.field() - def resolve_ike_proposal_list(root, info, **kwargs): - return gql_query_optimizer(models.IKEProposal.objects.all(), info) + ipsec_proposal: IPSecProposalType = strawberry_django.field() + ipsec_proposal_list: List[IPSecProposalType] = strawberry_django.field() - ipsec_policy = ObjectField(IPSecPolicyType) - ipsec_policy_list = ObjectListField(IPSecPolicyType) + l2vpn: L2VPNType = strawberry_django.field() + l2vpn_list: List[L2VPNType] = strawberry_django.field() - def resolve_ipsec_policy_list(root, info, **kwargs): - return gql_query_optimizer(models.IPSecPolicy.objects.all(), info) + l2vpn_termination: L2VPNTerminationType = strawberry_django.field() + l2vpn_termination_list: List[L2VPNTerminationType] = strawberry_django.field() - ipsec_profile = ObjectField(IPSecProfileType) - ipsec_profile_list = ObjectListField(IPSecProfileType) + tunnel: TunnelType = strawberry_django.field() + tunnel_list: List[TunnelType] = strawberry_django.field() - def resolve_ipsec_profile_list(root, info, **kwargs): - return gql_query_optimizer(models.IPSecProfile.objects.all(), info) + tunnel_group: TunnelGroupType = strawberry_django.field() + tunnel_group_list: List[TunnelGroupType] = strawberry_django.field() - ipsec_proposal = ObjectField(IPSecProposalType) - ipsec_proposal_list = ObjectListField(IPSecProposalType) - - def resolve_ipsec_proposal_list(root, info, **kwargs): - return gql_query_optimizer(models.IPSecProposal.objects.all(), info) - - l2vpn = ObjectField(L2VPNType) - l2vpn_list = ObjectListField(L2VPNType) - - def resolve_l2vpn_list(root, info, **kwargs): - return gql_query_optimizer(models.L2VPN.objects.all(), info) - - l2vpn_termination = ObjectField(L2VPNTerminationType) - l2vpn_termination_list = ObjectListField(L2VPNTerminationType) - - def resolve_l2vpn_termination_list(root, info, **kwargs): - return gql_query_optimizer(models.L2VPNTermination.objects.all(), info) - - tunnel = ObjectField(TunnelType) - tunnel_list = ObjectListField(TunnelType) - - def resolve_tunnel_list(root, info, **kwargs): - return gql_query_optimizer(models.Tunnel.objects.all(), info) - - tunnel_group = ObjectField(TunnelGroupType) - tunnel_group_list = ObjectListField(TunnelGroupType) - - def resolve_tunnel_group_list(root, info, **kwargs): - return gql_query_optimizer(models.TunnelGroup.objects.all(), info) - - tunnel_termination = ObjectField(TunnelTerminationType) - tunnel_termination_list = ObjectListField(TunnelTerminationType) - - def resolve_tunnel_termination_list(root, info, **kwargs): - return gql_query_optimizer(models.TunnelTermination.objects.all(), info) + tunnel_termination: TunnelTerminationType = strawberry_django.field() + tunnel_termination_list: List[TunnelTerminationType] = strawberry_django.field() diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py index e69de29bb..834f962c0 100644 --- a/netbox/wireless/graphql/filters.py +++ b/netbox/wireless/graphql/filters.py @@ -0,0 +1,27 @@ +import strawberry +import strawberry_django +from strawberry import auto +from wireless import models, filtersets +from netbox.graphql import filters + + +__all__ = ( + 'WirelessLANGroupFilter', + 'WirelessLANFilter', + 'WirelessLinkFilter', +) + + +@strawberry_django.filter(models.WirelessLANGroup, lookups=True) +class WirelessLANGroupFilter(filtersets.WirelessLANGroupFilterSet): + id: auto + + +@strawberry_django.filter(models.WirelessLAN, lookups=True) +class WirelessLANFilter(filtersets.WirelessLANFilterSet): + id: auto + + +@strawberry_django.filter(models.WirelessLink, lookups=True) +class WirelessLinkFilter(filtersets.WirelessLinkFilterSet): + id: auto diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index e6e46be3f..bd16cba3d 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -1,26 +1,18 @@ -import graphene +from typing import List +import strawberry +import strawberry_django -from netbox.graphql.fields import ObjectField, ObjectListField -from .types import * -from utilities.graphql_optimizer import gql_query_optimizer from wireless import models +from .types import * -class WirelessQuery(graphene.ObjectType): - wireless_lan = ObjectField(WirelessLANType) - wireless_lan_list = ObjectListField(WirelessLANType) +@strawberry.type +class WirelessQuery: + wireless_lan: WirelessLANType = strawberry_django.field() + wireless_lan_list: List[WirelessLANType] = strawberry_django.field() - def resolve_wireless_lan_list(root, info, **kwargs): - return gql_query_optimizer(models.WirelessLAN.objects.all(), info) + wireless_lan_group: WirelessLANGroupType = strawberry_django.field() + wireless_lan_group_list: List[WirelessLANGroupType] = strawberry_django.field() - wireless_lan_group = ObjectField(WirelessLANGroupType) - wireless_lan_group_list = ObjectListField(WirelessLANGroupType) - - def resolve_wireless_lan_group_list(root, info, **kwargs): - return gql_query_optimizer(models.WirelessLANGroup.objects.all(), info) - - wireless_link = ObjectField(WirelessLinkType) - wireless_link_list = ObjectListField(WirelessLinkType) - - def resolve_wireless_link_list(root, info, **kwargs): - return gql_query_optimizer(models.WirelessLink.objects.all(), info) + wireless_link: WirelessLinkType = strawberry_django.field() + wireless_link_list: List[WirelessLinkType] = strawberry_django.field() diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 7df0a46d3..e7afe00c7 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -14,7 +14,8 @@ __all__ = ( @strawberry_django.type( models.WirelessLANGroup, - fields='__all__', + # fields='__all__', + exclude=('parent',), # bug - temp filters=WirelessLANGroupFilter ) class WirelessLANGroupType(OrganizationalObjectType): From 6d7678f017510fced8aadaa9b05c0a77ec034f12 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 7 Feb 2024 16:45:13 -0800 Subject: [PATCH 017/156] 9856 fix old decorator --- netbox/extras/graphql/filters.py | 27 ++++++++++++++------------- netbox/users/graphql/filters.py | 5 +++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index 16ee63cd9..67b679b51 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from strawberry import auto from extras import models, filtersets @@ -19,7 +20,7 @@ __all__ = ( ) -@strawberry.django.filter(models.ConfigContext, lookups=True) +@strawberry_django.filter(models.ConfigContext, lookups=True) class ConfigContextFilter(filtersets.ConfigContextFilterSet): id: auto name: auto @@ -27,7 +28,7 @@ class ConfigContextFilter(filtersets.ConfigContextFilterSet): data_synced: auto -@strawberry.django.filter(models.ConfigTemplate, lookups=True) +@strawberry_django.filter(models.ConfigTemplate, lookups=True) class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet): id: auto name: auto @@ -35,7 +36,7 @@ class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet): data_synced: auto -@strawberry.django.filter(models.CustomField, lookups=True) +@strawberry_django.filter(models.CustomField, lookups=True) class CustomFieldFilter(filtersets.CustomFieldFilterSet): id: auto content_types: auto @@ -49,7 +50,7 @@ class CustomFieldFilter(filtersets.CustomFieldFilterSet): description: auto -@strawberry.django.filter(models.CustomFieldChoiceSet, lookups=True) +@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True) class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet): id: auto name: auto @@ -58,7 +59,7 @@ class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet): order_alphabetically: auto -@strawberry.django.filter(models.CustomLink, lookups=True) +@strawberry_django.filter(models.CustomLink, lookups=True) class CustomLinkFilter(filtersets.CustomLinkFilterSet): id: auto content_types: auto @@ -71,7 +72,7 @@ class CustomLinkFilter(filtersets.CustomLinkFilterSet): new_window: auto -@strawberry.django.filter(models.ExportTemplate, lookups=True) +@strawberry_django.filter(models.ExportTemplate, lookups=True) class ExportTemplateFilter(filtersets.ExportTemplateFilterSet): id: auto content_types: auto @@ -80,7 +81,7 @@ class ExportTemplateFilter(filtersets.ExportTemplateFilterSet): data_synced: auto -@strawberry.django.filter(models.ImageAttachment, lookups=True) +@strawberry_django.filter(models.ImageAttachment, lookups=True) class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet): id: auto content_type_id: auto @@ -88,7 +89,7 @@ class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet): name: auto -@strawberry.django.filter(models.JournalEntry, lookups=True) +@strawberry_django.filter(models.JournalEntry, lookups=True) class JournalEntryFilter(filtersets.JournalEntryFilterSet): id: auto assigned_object_type_id: auto @@ -97,7 +98,7 @@ class JournalEntryFilter(filtersets.JournalEntryFilterSet): kind: auto -@strawberry.django.filter(models.ObjectChange, lookups=True) +@strawberry_django.filter(models.ObjectChange, lookups=True) class ObjectChangeFilter(filtersets.ObjectChangeFilterSet): id: auto user: auto @@ -109,7 +110,7 @@ class ObjectChangeFilter(filtersets.ObjectChangeFilterSet): object_repr: auto -@strawberry.django.filter(models.SavedFilter, lookups=True) +@strawberry_django.filter(models.SavedFilter, lookups=True) class SavedFilterFilter(filtersets.SavedFilterFilterSet): id: auto content_types: auto @@ -121,7 +122,7 @@ class SavedFilterFilter(filtersets.SavedFilterFilterSet): weight: auto -@strawberry.django.filter(models.Tag, lookups=True) +@strawberry_django.filter(models.Tag, lookups=True) class TagFilter(filtersets.TagFilterSet): id: auto name: auto @@ -131,7 +132,7 @@ class TagFilter(filtersets.TagFilterSet): object_types: auto -@strawberry.django.filter(models.Webhook, lookups=True) +@strawberry_django.filter(models.Webhook, lookups=True) class WebhookFilter(filtersets.WebhookFilterSet): id: auto name: auto @@ -143,7 +144,7 @@ class WebhookFilter(filtersets.WebhookFilterSet): ca_file_path: auto -@strawberry.django.filter(models.EventRule, lookups=True) +@strawberry_django.filter(models.EventRule, lookups=True) class EventRuleFilter(filtersets.EventRuleFilterSet): id: auto name: auto diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index 4630cd70e..392a56850 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -1,4 +1,5 @@ import strawberry +import strawberry_django from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from strawberry import auto @@ -10,13 +11,13 @@ __all__ = ( ) -@strawberry.django.filter(Group, lookups=True) +@strawberry_django.filter(Group, lookups=True) class GroupFilter(filtersets.GroupFilterSet): id: auto name: auto -@strawberry.django.filter(get_user_model(), lookups=True) +@strawberry_django.filter(get_user_model(), lookups=True) class UserFilter(filtersets.UserFilterSet): id: auto username: auto From 7779e87ff397422de1eeca9de30be00d21e051e0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 12 Feb 2024 13:01:27 -0800 Subject: [PATCH 018/156] 9856 cleanup --- netbox/dcim/graphql/types.py | 1 - netbox/ipam/graphql/types.py | 1 - netbox/netbox/graphql/__init__.py | 69 ------- netbox/netbox/graphql/fields.py | 70 ------- netbox/netbox/graphql/scalars.py | 23 --- netbox/utilities/graphql_optimizer.py | 252 -------------------------- netbox/utilities/testing/api.py | 5 +- 7 files changed, 4 insertions(+), 417 deletions(-) delete mode 100644 netbox/netbox/graphql/fields.py delete mode 100644 netbox/netbox/graphql/scalars.py delete mode 100644 netbox/utilities/graphql_optimizer.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 455ef616f..6f61f2139 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -6,7 +6,6 @@ from extras.graphql.mixins import ( ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin, ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin -from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 3f3991f92..a711ac2b1 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -2,7 +2,6 @@ import strawberry import strawberry_django from ipam import models -from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from .filters import * diff --git a/netbox/netbox/graphql/__init__.py b/netbox/netbox/graphql/__init__.py index bd8e3cb88..e69de29bb 100644 --- a/netbox/netbox/graphql/__init__.py +++ b/netbox/netbox/graphql/__init__.py @@ -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) diff --git a/netbox/netbox/graphql/fields.py b/netbox/netbox/graphql/fields.py deleted file mode 100644 index 0f5221b47..000000000 --- a/netbox/netbox/graphql/fields.py +++ /dev/null @@ -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 diff --git a/netbox/netbox/graphql/scalars.py b/netbox/netbox/graphql/scalars.py deleted file mode 100644 index 8fc186b4d..000000000 --- a/netbox/netbox/graphql/scalars.py +++ /dev/null @@ -1,23 +0,0 @@ -from graphene import Scalar -from graphql.language import ast -from graphene.types.scalars import MAX_INT, MIN_INT - - -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) diff --git a/netbox/utilities/graphql_optimizer.py b/netbox/utilities/graphql_optimizer.py deleted file mode 100644 index 9af96a83c..000000000 --- a/netbox/utilities/graphql_optimizer.py +++ /dev/null @@ -1,252 +0,0 @@ -import functools - -from django.core.exceptions import FieldDoesNotExist -from django.db.models import ForeignKey -from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields.reverse_related import ManyToOneRel -from graphene import InputObjectType -from graphene.types.generic import GenericScalar -from graphene.types.resolver import default_resolver -from graphene_django import DjangoObjectType -from graphql import GraphQLResolveInfo, GraphQLSchema -from graphql.execution.execute import get_field_def -from graphql.language.ast import FragmentSpreadNode, InlineFragmentNode, VariableNode -from graphql.pyutils import Path -from graphql.type.definition import GraphQLInterfaceType, GraphQLUnionType - -__all__ = ( - 'gql_query_optimizer', -) - - -def gql_query_optimizer(queryset, info, **options): - return QueryOptimizer(info).optimize(queryset) - - -class QueryOptimizer(object): - def __init__(self, info, **options): - self.root_info = info - - def optimize(self, queryset): - info = self.root_info - field_def = get_field_def(info.schema, info.parent_type, info.field_nodes[0]) - - field_names = self._optimize_gql_selections( - self._get_type(field_def), - info.field_nodes[0], - ) - - qs = queryset.prefetch_related(*field_names) - return qs - - def _get_type(self, field_def): - a_type = field_def.type - while hasattr(a_type, "of_type"): - a_type = a_type.of_type - return a_type - - def _get_graphql_schema(self, schema): - if isinstance(schema, GraphQLSchema): - return schema - else: - return schema.graphql_schema - - def _get_possible_types(self, graphql_type): - if isinstance(graphql_type, (GraphQLInterfaceType, GraphQLUnionType)): - graphql_schema = self._get_graphql_schema(self.root_info.schema) - return graphql_schema.get_possible_types(graphql_type) - else: - return (graphql_type,) - - def _get_base_model(self, graphql_types): - models = tuple(t.graphene_type._meta.model for t in graphql_types) - for model in models: - if all(issubclass(m, model) for m in models): - return model - return None - - def handle_inline_fragment(self, selection, schema, possible_types, field_names): - fragment_type_name = selection.type_condition.name.value - graphql_schema = self._get_graphql_schema(schema) - fragment_type = graphql_schema.get_type(fragment_type_name) - fragment_possible_types = self._get_possible_types(fragment_type) - for fragment_possible_type in fragment_possible_types: - fragment_model = fragment_possible_type.graphene_type._meta.model - parent_model = self._get_base_model(possible_types) - if not parent_model: - continue - path_from_parent = fragment_model._meta.get_path_from_parent(parent_model) - select_related_name = LOOKUP_SEP.join(p.join_field.name for p in path_from_parent) - if not select_related_name: - continue - sub_field_names = self._optimize_gql_selections( - fragment_possible_type, - selection, - ) - field_names.append(select_related_name) - return - - def handle_fragment_spread(self, field_names, name, field_type): - fragment = self.root_info.fragments[name] - sub_field_names = self._optimize_gql_selections( - field_type, - fragment, - ) - - def _optimize_gql_selections(self, field_type, field_ast): - field_names = [] - selection_set = field_ast.selection_set - if not selection_set: - return field_names - optimized_fields_by_model = {} - schema = self.root_info.schema - graphql_schema = self._get_graphql_schema(schema) - graphql_type = graphql_schema.get_type(field_type.name) - - possible_types = self._get_possible_types(graphql_type) - for selection in selection_set.selections: - if isinstance(selection, InlineFragmentNode): - self.handle_inline_fragment(selection, schema, possible_types, field_names) - else: - name = selection.name.value - if isinstance(selection, FragmentSpreadNode): - self.handle_fragment_spread(field_names, name, field_type) - else: - for possible_type in possible_types: - selection_field_def = possible_type.fields.get(name) - if not selection_field_def: - continue - - graphene_type = possible_type.graphene_type - model = getattr(graphene_type._meta, "model", None) - if model and name not in optimized_fields_by_model: - field_model = optimized_fields_by_model[name] = model - if field_model == model: - self._optimize_field( - field_names, - model, - selection, - selection_field_def, - possible_type, - ) - return field_names - - def _get_field_info(self, field_names, model, selection, field_def): - name = None - model_field = None - name = self._get_name_from_resolver(field_def.resolve) - if not name and callable(field_def.resolve) and not isinstance(field_def.resolve, functools.partial): - name = selection.name.value - if name: - model_field = self._get_model_field_from_name(model, name) - - return (name, model_field) - - def _optimize_field(self, field_names, model, selection, field_def, parent_type): - name, model_field = self._get_field_info(field_names, model, selection, field_def) - if model_field: - self._optimize_field_by_name(field_names, model, selection, field_def, name, model_field) - - return - - def _optimize_field_by_name(self, field_names, model, selection, field_def, name, model_field): - if model_field.many_to_one or model_field.one_to_one: - sub_field_names = self._optimize_gql_selections( - self._get_type(field_def), - selection, - ) - if name not in field_names: - field_names.append(name) - - for field in sub_field_names: - prefetch_key = f"{name}__{field}" - if prefetch_key not in field_names: - field_names.append(prefetch_key) - - if model_field.one_to_many or model_field.many_to_many: - sub_field_names = self._optimize_gql_selections( - self._get_type(field_def), - selection, - ) - - if isinstance(model_field, ManyToOneRel): - sub_field_names.append(model_field.field.name) - - field_names.append(name) - for field in sub_field_names: - prefetch_key = f"{name}__{field}" - if prefetch_key not in field_names: - field_names.append(prefetch_key) - - return - - def _get_optimization_hints(self, resolver): - return getattr(resolver, "optimization_hints", None) - - def _get_value(self, info, value): - if isinstance(value, VariableNode): - var_name = value.name.value - value = info.variable_values.get(var_name) - return value - elif isinstance(value, InputObjectType): - return value.__dict__ - else: - return GenericScalar.parse_literal(value) - - def _get_name_from_resolver(self, resolver): - optimization_hints = self._get_optimization_hints(resolver) - if optimization_hints: - name_fn = optimization_hints.model_field - if name_fn: - return name_fn() - if self._is_resolver_for_id_field(resolver): - return "id" - elif isinstance(resolver, functools.partial): - resolver_fn = resolver - if resolver_fn.func != default_resolver: - # Some resolvers have the partial function as the second - # argument. - for arg in resolver_fn.args: - if isinstance(arg, (str, functools.partial)): - break - else: - # No suitable instances found, default to first arg - arg = resolver_fn.args[0] - resolver_fn = arg - if isinstance(resolver_fn, functools.partial) and resolver_fn.func == default_resolver: - return resolver_fn.args[0] - if self._is_resolver_for_id_field(resolver_fn): - return "id" - return resolver_fn - - def _is_resolver_for_id_field(self, resolver): - resolve_id = DjangoObjectType.resolve_id - return resolver == resolve_id - - def _get_model_field_from_name(self, model, name): - try: - return model._meta.get_field(name) - except FieldDoesNotExist: - descriptor = model.__dict__.get(name) - if not descriptor: - return None - return getattr(descriptor, "rel", None) or getattr(descriptor, "related", None) # Django < 1.9 - - def _is_foreign_key_id(self, model_field, name): - return isinstance(model_field, ForeignKey) and model_field.name != name and model_field.get_attname() == name - - def _create_resolve_info(self, field_name, field_asts, return_type, parent_type): - return GraphQLResolveInfo( - field_name, - field_asts, - return_type, - parent_type, - Path(None, 0, None), - schema=self.root_info.schema, - fragments=self.root_info.fragments, - root_value=self.root_info.root_value, - operation=self.root_info.operation, - variable_values=self.root_info.variable_values, - context=self.root_info.context, - is_awaitable=self.root_info.is_awaitable, - ) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 20c607906..2c86fde8e 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -446,7 +446,10 @@ class APIViewTestCases: # Compile list of fields to include fields_string = '' - for field_name, field in type_class._meta.fields.items(): + + for field_name, field in type_class.__dataclass_fields__.items(): + # for field_name, field in type_class._meta.fields.items(): + print(f"field_name: {field_name} field: {field}") is_string_array = False if type(field.type) is GQLList: if field.type.of_type is GQLString: From eca0966d929414166e228975a3955872f94424e3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 12 Feb 2024 13:05:57 -0800 Subject: [PATCH 019/156] 9856 cleanup --- base_requirements.txt | 5 ----- netbox/netbox/graphql/utils.py | 25 ------------------------- netbox/netbox/settings.py | 11 ----------- netbox/utilities/testing/api.py | 2 +- requirements.txt | 1 - 5 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 netbox/netbox/graphql/utils.py diff --git a/base_requirements.txt b/base_requirements.txt index 580c5f6ad..e20a4a32b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -75,11 +75,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 diff --git a/netbox/netbox/graphql/utils.py b/netbox/netbox/graphql/utils.py deleted file mode 100644 index c71d49204..000000000 --- a/netbox/netbox/graphql/utils.py +++ /dev/null @@ -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 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 743233cbe..d840f991f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -674,17 +674,6 @@ SPECTACULAR_SETTINGS = { 'POSTPROCESSING_HOOKS': [], } -# -# Graphene -# - -GRAPHENE = { - # Avoids naming collision on models with 'type' field; see - # https://github.com/graphql-python/graphene-django/issues/185 - 'DJANGO_CHOICE_FIELD_ENUM_V3_NAMING': True, -} - - # # Django RQ (events backend) # diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 2c86fde8e..ce2817777 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.test import override_settings -from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLUnion, String as GQLString, NonNull as GQLNonNull +# from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLUnion, String as GQLString, NonNull as GQLNonNull from rest_framework import status from rest_framework.test import APIClient diff --git a/requirements.txt b/requirements.txt index ecd26bc15..d29fd5622 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ djangorestframework==3.14.0 drf-spectacular==0.27.1 drf-spectacular-sidecar==2024.2.1 feedparser==6.0.11 -graphene-django==3.0.0 gunicorn==21.2.0 Jinja2==3.1.3 Markdown==3.5.2 From 99b01981d493de40983e1894359e8a1ec73957f7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 13 Feb 2024 08:32:21 -0800 Subject: [PATCH 020/156] 9856 fixes to circuits type specifiers --- netbox/circuits/graphql/types.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index b1c2fbbc7..36c6a6f3e 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -5,6 +5,7 @@ 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 tenancy.graphql.types import TenantType from .filters import * __all__ = ( @@ -53,15 +54,6 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob pass -@strawberry_django.type( - models.Circuit, - fields='__all__', - filters=CircuitFilter -) -class CircuitType(NetBoxObjectType, ContactsMixin): - provider: ProviderType - - @strawberry_django.type( models.CircuitType, # fields='__all__', @@ -70,3 +62,17 @@ class CircuitType(NetBoxObjectType, ContactsMixin): ) class CircuitTypeType(OrganizationalObjectType): pass + + +@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 From 4fab68a13830c59f02e378a2d109ad8d69777e99 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 13 Feb 2024 10:28:26 -0800 Subject: [PATCH 021/156] 9856 fixes to circuits type specifiers --- netbox/circuits/graphql/types.py | 13 ++++++++++--- netbox/netbox/settings.py | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 36c6a6f3e..f8c73cac2 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,11 +1,14 @@ +from typing import List + 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 tenancy.graphql.types import TenantType + +from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType + from .filters import * __all__ = ( @@ -76,3 +79,7 @@ class CircuitType(NetBoxObjectType, ContactsMixin): termination_z: CircuitTerminationType | None type: CircuitTypeType tenant: TenantType | None + + @strawberry_django.field + def terminations(self) -> List[CircuitTerminationType]: + return self.terminations.all() diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d840f991f..082e29e35 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -738,6 +738,14 @@ if not ENABLE_LOCALIZATION: USE_I18N = False USE_L10N = False +# +# Strawberry (GraphQL) +# +STRAWBERRY_DJANGO = { + "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, + # "GENERATE_ENUMS_FROM_CHOICES": True, +} + # # Plugins # From d37414d69a4a6896c5e52c314eca1e5e2843dfa6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 15 Feb 2024 11:06:41 -0800 Subject: [PATCH 022/156] 9856 update types --- netbox/ipam/graphql/types.py | 56 ++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index a711ac2b1..0fd7f086f 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,8 +1,18 @@ +from typing import List + import strawberry import strawberry_django - +from circuits.graphql.types import ProviderType +from dcim.graphql.types import SiteType from ipam import models -from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType + +from netbox.graphql.scalars import BigInt +from netbox.graphql.types import ( + BaseObjectType, + NetBoxObjectType, + OrganizationalObjectType, +) + from .filters import * __all__ = ( @@ -25,57 +35,59 @@ __all__ = ( ) +@strawberry.type class IPAddressFamilyType: - - # value = graphene.Int() - # label = graphene.String() - - def __init__(self, value): - self.value = value - self.label = f'IPv{value}' + 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.value, label=f'IPv{self.value}') @strawberry_django.type( models.ASN, - # fields='__all__', - exclude=('asn',), # bug - temp + fields='__all__', filters=ASNFilter ) class ASNType(NetBoxObjectType): - # asn = graphene.Field(BigInt) - pass + asn: BigInt + + @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__', - exclude=('start', 'end',), # bug - temp + fields='__all__', filters=ASNRangeFilter ) class ASNRangeType(NetBoxObjectType): - pass + start: BigInt + end: BigInt @strawberry_django.type( models.Aggregate, - # fields='__all__', - exclude=('prefix',), # bug - temp + fields='__all__', filters=AggregateFilter ) class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): - pass + prefix: str @strawberry_django.type( From 1aa5b0d5a159d049d20ed0e506e76932f8333f2a Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 15 Feb 2024 14:54:04 -0800 Subject: [PATCH 023/156] 9856 GFK working --- netbox/dcim/graphql/gfk_mixins.py | 72 ------------------------------- netbox/dcim/graphql/mixins.py | 42 +++++++++++++----- netbox/dcim/graphql/types.py | 67 ++++++++++++++++++---------- 3 files changed, 76 insertions(+), 105 deletions(-) diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index 2f669fb87..2a596a6f6 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -37,78 +37,6 @@ 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 Meta: types = ( diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 8241b7de5..3d6a9fb1a 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -1,20 +1,40 @@ -import graphene +import strawberry +import strawberry_django +from typing import TYPE_CHECKING, Annotated, List, Union + +__all__ = ( + 'CabledObjectMixin', + 'PathEndpointMixin', +) +@strawberry.type class CabledObjectMixin: - link_peers = graphene.List('dcim.graphql.gfk_mixins.LinkPeerType') - def resolve_cable_end(self, info): - # Handle empty values - return self.cable_end or None + # @strawberry_django.field + # def cable_end(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: + # # 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 - return self.connected_endpoints or None + pass + # @strawberry_django.field + # def connected_endpoints(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: + # # Handle empty values + # return self.connected_endpoints or None diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 6f61f2139..016c1a557 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,12 +1,24 @@ +from typing import Annotated, List, Union + import strawberry import strawberry_django - from dcim import models from extras.graphql.mixins import ( - ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin, + ChangelogMixin, + ConfigContextMixin, + ContactsMixin, + CustomFieldsMixin, + ImageAttachmentsMixin, + TagsMixin, ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin -from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType + +from netbox.graphql.types import ( + BaseObjectType, + NetBoxObjectType, + OrganizationalObjectType, +) + from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -88,6 +100,29 @@ class ComponentTemplateObjectType( # Model types # +@strawberry_django.type( + models.CableTermination, + exclude=('termination_type', 'termination_id'), + filters=CableTerminationFilter +) +class CableTerminationType(NetBoxObjectType): + + @strawberry_django.field + def termination(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("CableTerminationTerminationType")]]: + return self.termination + + @strawberry_django.type( models.Cable, # fields='__all__', @@ -95,32 +130,20 @@ class ComponentTemplateObjectType( filters=CableFilter ) class CableType(NetBoxObjectType): - # a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - # b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - def resolve_type(self, info): - return self.type or None + @strawberry_django.field + def terminations(self) -> List[CableTerminationType]: + return self.terminations - def resolve_length_unit(self, info): - return self.length_unit or None - - def resolve_a_terminations(self, info): + @strawberry_django.field + def a_terminations(self) -> List[CableTerminationType]: return self.a_terminations - def resolve_b_terminations(self, info): + @strawberry_django.field + def b_terminations(self) -> List[CableTerminationType]: return self.b_terminations -@strawberry_django.type( - models.CableTermination, - exclude=('termination_type', 'termination_id'), - filters=CableTerminationFilter -) -class CableTerminationType(NetBoxObjectType): - # termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType') - pass - - @strawberry_django.type( models.ConsolePort, # exclude=('_path',), From c3cbefc62507904c2a101e0b969f065998647875 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 15 Feb 2024 14:54:29 -0800 Subject: [PATCH 024/156] 9856 GFK working --- netbox/netbox/graphql/scalars.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 netbox/netbox/graphql/scalars.py diff --git a/netbox/netbox/graphql/scalars.py b/netbox/netbox/graphql/scalars.py new file mode 100644 index 000000000..d14549f65 --- /dev/null +++ b/netbox/netbox/graphql/scalars.py @@ -0,0 +1,10 @@ +from typing import Union + +import strawberry + +BigInt = strawberry.scalar( + Union[int, str], # type: ignore + serialize=lambda v: int(v), + parse_value=lambda v: str(v), + description="BigInt field", +) From ff03abf23ed151733e3e6b6ad33b695e3bb93084 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 15 Feb 2024 15:33:51 -0800 Subject: [PATCH 025/156] 9856 _name --- netbox/dcim/graphql/types.py | 106 +++++++++++++++++------------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 016c1a557..123e7deee 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -13,6 +13,7 @@ from extras.graphql.mixins import ( ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin +from netbox.graphql.scalars import BigInt from netbox.graphql.types import ( BaseObjectType, NetBoxObjectType, @@ -72,6 +73,7 @@ __all__ = ( # +@strawberry.type class ComponentObjectType( ChangelogMixin, CustomFieldsMixin, @@ -81,8 +83,7 @@ class ComponentObjectType( """ Base type for device/VM components """ - class Meta: - abstract = True + _name: str class ComponentTemplateObjectType( @@ -92,8 +93,7 @@ class ComponentTemplateObjectType( """ Base type for device/VM components """ - class Meta: - abstract = True + _name: str # @@ -147,7 +147,7 @@ class CableType(NetBoxObjectType): @strawberry_django.type( models.ConsolePort, # exclude=('_path',), - exclude=('_path', '_name',), # bug - temp + exclude=('_path',), # bug - temp filters=ConsolePortFilter ) class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -158,8 +158,7 @@ class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) @strawberry_django.type( models.ConsolePortTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=ConsolePortTemplateFilter ) class ConsolePortTemplateType(ComponentTemplateObjectType): @@ -171,7 +170,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.ConsoleServerPort, # exclude=('_path',), - exclude=('_path', '_name',), # bug - temp + exclude=('_path',), # bug - temp filters=ConsoleServerPortFilter ) class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -182,8 +181,7 @@ class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpoint @strawberry_django.type( models.ConsoleServerPortTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=ConsoleServerPortTemplateFilter ) class ConsoleServerPortTemplateType(ComponentTemplateObjectType): @@ -194,15 +192,21 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Device, - # fields='__all__', - exclude=( - '_name', 'console_port_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', - 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', - 'inventory_item_count' - ), # bug - temp + fields='__all__', filters=DeviceFilter ) class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): + _name: str + console_port_count: BigInt + console_server_port_count: BigInt + power_port_count: BigInt + power_outlet_count: BigInt + interface_count: BigInt + front_port_count: BigInt + rear_port_count: BigInt + device_bay_count: BigInt + module_bay_count: BigInt + inventory_item_count: BigInt def resolve_face(self, info): return self.face or None @@ -213,8 +217,7 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo @strawberry_django.type( models.DeviceBay, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=DeviceBayFilter ) class DeviceBayType(ComponentObjectType): @@ -223,8 +226,7 @@ class DeviceBayType(ComponentObjectType): @strawberry_django.type( models.DeviceBayTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=DeviceBayTemplateFilter ) class DeviceBayTemplateType(ComponentTemplateObjectType): @@ -233,7 +235,7 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.InventoryItemTemplate, - exclude=('component_type', 'component_id', '_name', 'parent'), + exclude=('component_type', 'component_id', 'parent'), filters=InventoryItemTemplateFilter ) class InventoryItemTemplateType(ComponentTemplateObjectType): @@ -253,16 +255,20 @@ class DeviceRoleType(OrganizationalObjectType): @strawberry_django.type( models.DeviceType, - # fields='__all__', - exclude=( - 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', - 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', - 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', - 'inventory_item_template_count', - ), # bug - temp + fields='__all__', filters=DeviceTypeFilter ) class DeviceTypeType(NetBoxObjectType): + console_port_template_count: BigInt + console_server_port_template_count: BigInt + power_port_template_count: BigInt + power_outlet_template_count: BigInt + interface_template_count: BigInt + front_port_template_count: BigInt + rear_port_template_count: BigInt + device_bay_template_count: BigInt + module_bay_template_count: BigInt + inventory_item_template_count: BigInt def resolve_subdevice_role(self, info): return self.subdevice_role or None @@ -277,7 +283,7 @@ class DeviceTypeType(NetBoxObjectType): @strawberry_django.type( models.FrontPort, # fields='__all__', - exclude=('_name', 'color'), # bug - temp + exclude=('color',), # bug - temp filters=FrontPortFilter ) class FrontPortType(ComponentObjectType, CabledObjectMixin): @@ -287,7 +293,7 @@ class FrontPortType(ComponentObjectType, CabledObjectMixin): @strawberry_django.type( models.FrontPortTemplate, # fields='__all__', - exclude=('_name', 'color'), # bug - temp + exclude=('color',), # bug - temp filters=FrontPortTemplateFilter ) class FrontPortTemplateType(ComponentTemplateObjectType): @@ -297,7 +303,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Interface, # fields='__all__', - exclude=('mac_address', '_name', 'wwn'), # bug - temp + exclude=('mac_address', 'wwn'), # bug - temp filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -320,8 +326,7 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa @strawberry_django.type( models.InterfaceTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=InterfaceTemplateFilter ) class InterfaceTemplateType(ComponentTemplateObjectType): @@ -338,7 +343,7 @@ class InterfaceTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.InventoryItem, - exclude=('component_type', 'component_id', '_name', 'parent'), + exclude=('component_type', 'component_id', 'parent'), filters=InventoryItemFilter ) class InventoryItemType(ComponentObjectType): @@ -349,7 +354,7 @@ class InventoryItemType(ComponentObjectType): @strawberry_django.type( models.InventoryItemRole, # fields='__all__', - exclude=('color', '_name'), # bug - temp + exclude=('color',), # bug - temp filters=InventoryItemRoleFilter ) class InventoryItemRoleType(OrganizationalObjectType): @@ -386,8 +391,7 @@ class ModuleType(ComponentObjectType): @strawberry_django.type( models.ModuleBay, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=ModuleBayFilter ) class ModuleBayType(ComponentObjectType): @@ -396,8 +400,7 @@ class ModuleBayType(ComponentObjectType): @strawberry_django.type( models.ModuleBayTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=ModuleBayTemplateFilter ) class ModuleBayTemplateType(ComponentTemplateObjectType): @@ -435,8 +438,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.type( models.PowerOutlet, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=PowerOutletFilter ) class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -450,8 +452,7 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) @strawberry_django.type( models.PowerOutletTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=PowerOutletTemplateFilter ) class PowerOutletTemplateType(ComponentTemplateObjectType): @@ -474,7 +475,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): @strawberry_django.type( models.PowerPort, - exclude=('_path', '_name'), + exclude=('_path',), filters=PowerPortFilter ) class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -485,8 +486,7 @@ class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.type( models.PowerPortTemplate, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=PowerPortTemplateFilter ) class PowerPortTemplateType(ComponentTemplateObjectType): @@ -497,11 +497,11 @@ class PowerPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Rack, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=RackFilter ) class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): + _name: str def resolve_type(self, info): return self.type or None @@ -536,7 +536,7 @@ class RackRoleType(OrganizationalObjectType): @strawberry_django.type( models.RearPort, # fields='__all__', - exclude=('_name', 'color'), # bug - temp + exclude=('color', ), # bug - temp filters=RearPortFilter ) class RearPortType(ComponentObjectType, CabledObjectMixin): @@ -546,7 +546,7 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): @strawberry_django.type( models.RearPortTemplate, # fields='__all__', - exclude=('_name', 'color'), # bug - temp + exclude=('color', ), # bug - temp filters=RearPortTemplateFilter ) class RearPortTemplateType(ComponentTemplateObjectType): @@ -566,12 +566,12 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @strawberry_django.type( models.Site, # fields='__all__', - exclude=('_name', 'time_zone'), # bug - temp + exclude=('time_zone',), # bug - temp filters=SiteFilter ) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): - # asn = graphene.Field(BigInt) - pass + _name: str + asn: BigInt @strawberry_django.type( From a0d0ab1e78974808e697f14f624ace964bc3a8e9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 15 Feb 2024 16:03:47 -0800 Subject: [PATCH 026/156] 9856 misc fixes --- base_requirements.txt | 6 +++--- netbox/dcim/graphql/types.py | 6 ++---- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/base_requirements.txt b/base_requirements.txt index e20a4a32b..f6132d435 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -131,9 +131,9 @@ social-auth-core # https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md social-auth-app-django -# Enhanced Strawberry GraphQL integration with Django -# https://github.com/blb-ventures/strawberry-django-plus/blob/main/CHANGELOG.md -strawberry-graphql-django +# Strawberry GraphQL Django extension +# https://github.com/strawberry-graphql/strawberry-django/blob/main/CHANGELOG.md +strawberry-django # SVG image rendering (used for rack elevations) # https://github.com/mozman/svgwrite/blob/master/NEWS.rst diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 123e7deee..028d12091 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -118,7 +118,6 @@ class CableTerminationType(NetBoxObjectType): Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], - ], strawberry.union("CableTerminationTerminationType")]]: return self.termination @@ -586,12 +585,11 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @strawberry_django.type( models.VirtualChassis, - # fields='__all__', - exclude=('member_count',), # bug - temp + fields='__all__', filters=VirtualChassisFilter ) class VirtualChassisType(NetBoxObjectType): - pass + member_count: BigInt @strawberry_django.type( diff --git a/requirements.txt b/requirements.txt index d29fd5622..0d2933ac0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 social-auth-core[openidconnect]==4.5.2 -strawberry-graphql-django==0.30.1 +strawberry-graphql-django==0.31.0 svgwrite==1.4.3 tablib==3.5.0 tzdata==2023.4 From d4812b28fd3ea0b79872f4e2d35dba5220906e9c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 22 Feb 2024 16:35:24 -0800 Subject: [PATCH 027/156] 9856 type updates --- netbox/circuits/graphql/types.py | 11 ++-- netbox/dcim/graphql/gfk_mixins.py | 5 +- netbox/dcim/graphql/mixins.py | 21 ++++++-- netbox/dcim/graphql/types.py | 5 +- netbox/extras/graphql/types.py | 71 +++++++++++++++++++++++++- netbox/netbox/graphql/types.py | 5 +- netbox/netbox/settings.py | 1 - netbox/virtualization/graphql/types.py | 29 +++++++++-- 8 files changed, 130 insertions(+), 18 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index f8c73cac2..83244a098 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Annotated, List import strawberry import strawberry_django @@ -59,12 +59,15 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob @strawberry_django.type( models.CircuitType, - # fields='__all__', - exclude=['color',], # bug - remove color from exclude + fields='__all__', filters=CircuitTypeFilter ) class CircuitTypeType(OrganizationalObjectType): - pass + color: str + + @strawberry_django.field + def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]: + return self.circuits.all() @strawberry_django.type( diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index 2a596a6f6..547cf24bc 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -1,4 +1,7 @@ -import graphene +from typing import TYPE_CHECKING, Annotated, List, Union + +import strawberry +import strawberry_django from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType from circuits.models import CircuitTermination, ProviderNetwork from dcim.graphql.types import ( diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 3d6a9fb1a..e6ddb7c1e 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -2,6 +2,7 @@ import strawberry import strawberry_django from typing import TYPE_CHECKING, Annotated, List, Union + __all__ = ( 'CabledObjectMixin', 'PathEndpointMixin', @@ -33,8 +34,18 @@ class CabledObjectMixin: @strawberry.type class PathEndpointMixin: - pass - # @strawberry_django.field - # def connected_endpoints(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: - # # Handle empty values - # return self.connected_endpoints or None + + @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["ProviderNetworkType", strawberry.lazy('dcim.graphql.types')], + Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("ConnectedEndpointType")]]: + return self.connected_endpoints or None diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 028d12091..c7e166f07 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -124,11 +124,11 @@ class CableTerminationType(NetBoxObjectType): @strawberry_django.type( models.Cable, - # fields='__all__', - exclude=('color', ), # bug - temp + fields='__all__', filters=CableFilter ) class CableType(NetBoxObjectType): + color: str @strawberry_django.field def terminations(self) -> List[CableTerminationType]: @@ -161,6 +161,7 @@ class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) filters=ConsolePortTemplateFilter ) class ConsolePortTemplateType(ComponentTemplateObjectType): + _name: str def resolve_type(self, info): return self.type or None diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index e89802b85..2056d01ee 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry import strawberry_django @@ -35,6 +37,58 @@ __all__ = ( class ConfigContextType(ObjectType): pass + @strawberry_django.field + def roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def device_types(self) -> List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def tags(self) -> List[Annotated["TagType", strawberry.lazy('extras.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def regions(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def cluster_groups(self) -> List[Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def tenant_groups(self) -> List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def cluster_types(self) -> List[Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def locations(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def tenants(self) -> List[Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def site_groups(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + @strawberry_django.type( models.ConfigTemplate, @@ -42,7 +96,22 @@ class ConfigContextType(ObjectType): filters=ConfigTemplateFilter ) class ConfigTemplateType(TagsMixin, ObjectType): - pass + + @strawberry_django.field + def virtualmachines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def device_roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]: + return self.vlan_groups.all() @strawberry_django.type( diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 25dedd696..f1a7bcfe5 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -31,7 +31,10 @@ class BaseObjectType: @classmethod def get_queryset(cls, queryset, info, **kwargs): # Enforce object permissions on the queryset - return queryset.restrict(info.context.request.user, 'view') + if hasattr(queryset, 'restrict'): + return queryset.restrict(info.context.request.user, 'view') + else: + return queryset @strawberry_django.field def display(self) -> str: diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 082e29e35..3cc352eb8 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -743,7 +743,6 @@ if not ENABLE_LOCALIZATION: # STRAWBERRY_DJANGO = { "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, - # "GENERATE_ENUMS_FROM_CHOICES": True, } # diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index f4c92035b..15c7766f7 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry import strawberry_django @@ -24,7 +26,18 @@ __all__ = ( filters=ClusterFilter ) class ClusterType(VLANGroupsMixin, NetBoxObjectType): - pass + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() @strawberry_django.type( @@ -33,7 +46,14 @@ class ClusterType(VLANGroupsMixin, NetBoxObjectType): filters=ClusterGroupFilter ) class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): - pass + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: + return self.clusters.all() @strawberry_django.type( @@ -42,7 +62,10 @@ class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): filters=ClusterTypeFilter ) class ClusterTypeType(OrganizationalObjectType): - pass + + @strawberry_django.field + def clusters(self) -> List[ClusterType]: + return self.clusters.all() @strawberry_django.type( From a5445bb61aec9efd8e98baff0df05095c691c47d Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 22 Feb 2024 16:56:54 -0800 Subject: [PATCH 028/156] 9856 _name to types --- netbox/dcim/graphql/mixins.py | 4 ++-- netbox/dcim/graphql/types.py | 27 +++++++++++++------------- netbox/virtualization/graphql/types.py | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index e6ddb7c1e..8e79b4c0b 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -36,7 +36,7 @@ class CabledObjectMixin: class PathEndpointMixin: @strawberry_django.field - def link_peers(self) -> List[Annotated[Union[ + 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')], @@ -45,7 +45,7 @@ class PathEndpointMixin: 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('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 diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index c7e166f07..b92c82e6f 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -185,6 +185,7 @@ class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpoint filters=ConsoleServerPortTemplateFilter ) class ConsoleServerPortTemplateType(ComponentTemplateObjectType): + _name: str def resolve_type(self, info): return self.type or None @@ -230,7 +231,7 @@ class DeviceBayType(ComponentObjectType): filters=DeviceBayTemplateFilter ) class DeviceBayTemplateType(ComponentTemplateObjectType): - pass + _name: str @strawberry_django.type( @@ -239,8 +240,7 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): filters=InventoryItemTemplateFilter ) class InventoryItemTemplateType(ComponentTemplateObjectType): - # component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType') - pass + _name: str @strawberry_django.type( @@ -282,22 +282,21 @@ class DeviceTypeType(NetBoxObjectType): @strawberry_django.type( models.FrontPort, - # fields='__all__', - exclude=('color',), # bug - temp + fields='__all__', filters=FrontPortFilter ) class FrontPortType(ComponentObjectType, CabledObjectMixin): - pass + color: str @strawberry_django.type( models.FrontPortTemplate, - # fields='__all__', - exclude=('color',), # bug - temp + fields='__all__', filters=FrontPortTemplateFilter ) class FrontPortTemplateType(ComponentTemplateObjectType): - pass + _name: str + color: str @strawberry_django.type( @@ -330,6 +329,7 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa filters=InterfaceTemplateFilter ) class InterfaceTemplateType(ComponentTemplateObjectType): + _name: str def resolve_poe_mode(self, info): return self.poe_mode or None @@ -347,8 +347,7 @@ class InterfaceTemplateType(ComponentTemplateObjectType): filters=InventoryItemFilter ) class InventoryItemType(ComponentObjectType): - # component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') - pass + _name: str @strawberry_django.type( @@ -404,7 +403,7 @@ class ModuleBayType(ComponentObjectType): filters=ModuleBayTemplateFilter ) class ModuleBayTemplateType(ComponentTemplateObjectType): - pass + _name: str @strawberry_django.type( @@ -456,6 +455,7 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) filters=PowerOutletTemplateFilter ) class PowerOutletTemplateType(ComponentTemplateObjectType): + _name: str def resolve_feed_leg(self, info): return self.feed_leg or None @@ -490,6 +490,7 @@ class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): filters=PowerPortTemplateFilter ) class PowerPortTemplateType(ComponentTemplateObjectType): + _name: str def resolve_type(self, info): return self.type or None @@ -550,7 +551,7 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): filters=RearPortTemplateFilter ) class RearPortTemplateType(ComponentTemplateObjectType): - pass + _name: str @strawberry_django.type( diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 15c7766f7..bfac3ef44 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -75,7 +75,7 @@ class ClusterTypeType(OrganizationalObjectType): filters=VirtualMachineFilter ) class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): - pass + _name: str @strawberry_django.type( From 69134dbb5091f12eb26a7ee57b4a146f34d8622b Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 23 Feb 2024 14:36:41 -0800 Subject: [PATCH 029/156] 9856 update types --- netbox/core/graphql/types.py | 7 +- netbox/dcim/graphql/gfk_mixins.py | 6 +- netbox/dcim/graphql/types.py | 122 +++++++++++++++++++++++++++++- netbox/extras/graphql/types.py | 9 ++- netbox/tenancy/graphql/types.py | 25 +++++- 5 files changed, 160 insertions(+), 9 deletions(-) diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index f4f1ebd46..636c322e4 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry import strawberry_django @@ -27,4 +29,7 @@ class DataFileType(BaseObjectType): filters=DataSourceFilter ) class DataSourceType(NetBoxObjectType): - pass + + @strawberry_django.field + def datafiles(self) -> List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]: + return self.datafiles.all() diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index 547cf24bc..8bd435284 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -40,7 +40,7 @@ from dcim.models import ( ) -class InventoryItemTemplateComponentType(graphene.Union): +class InventoryItemTemplateComponentType: class Meta: types = ( ConsolePortTemplateType, @@ -70,7 +70,7 @@ class InventoryItemTemplateComponentType(graphene.Union): return RearPortTemplateType -class InventoryItemComponentType(graphene.Union): +class InventoryItemComponentType: class Meta: types = ( ConsolePortType, @@ -100,7 +100,7 @@ class InventoryItemComponentType(graphene.Union): return RearPortType -class ConnectedEndpointType(graphene.Union): +class ConnectedEndpointType: class Meta: types = ( CircuitTerminationType, diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index b92c82e6f..ac7134e56 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -215,6 +215,74 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo def resolve_airflow(self, info): return self.airflow or None + @strawberry_django.field + def devicebays(self) -> List[Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def vc_master_for(self) -> Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')]: + return self.vc_master_for + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def modules(self) -> List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]: + return self.modules.all() + + @strawberry_django.field + def parent_bay(self) -> Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]: + return self.parent_bay + + @strawberry_django.field + def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def rearports(self) -> List[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def consoleports(self) -> List[Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def powerports(self) -> List[Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def consoleserverports(self) -> List[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def poweroutlets(self) -> List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def frontports(self) -> List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def modulebays(self) -> List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def services(self) -> List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def inventoryitems(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: + return self.vdcs.all() + @strawberry_django.type( models.DeviceBay, @@ -250,7 +318,15 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): filters=DeviceRoleFilter ) class DeviceRoleType(OrganizationalObjectType): - pass + color: str + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() @strawberry_django.type( @@ -279,6 +355,50 @@ class DeviceTypeType(NetBoxObjectType): def resolve_weight_unit(self, info): return self.weight_unit or None + @strawberry_django.field + def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def modulebaytemplates(self) -> List[Annotated["ModuleBayTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def instances(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def poweroutlettemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def powerporttemplates(self) -> List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def inventoryitemtemplates(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def rearporttemplates(self) -> List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def consoleserverporttemplates(self) -> List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def interfacetemplates(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def devicebaytemplates(self) -> List[Annotated["DeviceBayTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + + @strawberry_django.field + def consoleporttemplates(self) -> List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_bays.all() + @strawberry_django.type( models.FrontPort, diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 2056d01ee..a66dd2cba 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -130,7 +130,14 @@ class CustomFieldType(ObjectType): filters=CustomFieldChoiceSetFilter ) class CustomFieldChoiceSetType(ObjectType): - pass + + @strawberry_django.field + def choices_for(self) -> List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]: + return self.assignments.all() + + @strawberry_django.field + def extra_choices(self) -> List[str]: + return self.assignments.all() @strawberry_django.type( diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 307a9000d..828a36930 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry import strawberry_django @@ -56,7 +58,10 @@ class TenantGroupType(OrganizationalObjectType): filters=ContactFilter ) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): - pass + + @strawberry_django.field + def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: + return self.assignments.all() @strawberry_django.type( @@ -65,7 +70,10 @@ class ContactType(ContactAssignmentsMixin, NetBoxObjectType): filters=ContactRoleFilter ) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): - pass + + @strawberry_django.field + def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: + return self.assignments.all() @strawberry_django.type( @@ -75,7 +83,18 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): filters=ContactGroupFilter ) class ContactGroupType(OrganizationalObjectType): - pass + + @strawberry_django.field + def parent(self) -> Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')]: + return self.parent + + @strawberry_django.field + def contacts(self) -> List[ContactType]: + return self.clusters.all() + + @strawberry_django.field + def children(self) -> List[Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')]]: + return self.children.all() @strawberry_django.type( From 6bb9d68f60b85455008e7f37f30048f296281bb0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 23 Feb 2024 15:56:21 -0800 Subject: [PATCH 030/156] 9856 update types --- netbox/ipam/graphql/types.py | 10 ++++++++-- netbox/vpn/graphql/types.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 0fd7f086f..31bd0a9d9 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,4 +1,4 @@ -from typing import List +from typing import TYPE_CHECKING, Annotated, List, Union import strawberry import strawberry_django @@ -108,7 +108,13 @@ class FHRPGroupType(NetBoxObjectType): ) class FHRPGroupAssignmentType(BaseObjectType): # interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType') - pass + + @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( diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index cf3cc6d27..68fe39403 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry import strawberry_django @@ -62,7 +64,14 @@ class IKEProposalType(OrganizationalObjectType): filters=IKEPolicyFilter ) class IKEPolicyType(OrganizationalObjectType): - pass + + @strawberry_django.field + def proposals(self) -> List[Annotated["IKEProposalType", strawberry.lazy('vpn.graphql.types')]]: + return self.proposals.all() + + @strawberry_django.field + def ipsec_profiles(self) -> List[Annotated["IPSecProposalType", strawberry.lazy('vpn.graphql.types')]]: + return self.ipsec_profiles.all() @strawberry_django.type( From ce003b2b1c37e2bd1933820e27121876fc8504d6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 08:20:32 -0800 Subject: [PATCH 031/156] 9856 update types --- netbox/dcim/graphql/types.py | 101 ++++++++++++++++++++++++----------- netbox/ipam/graphql/types.py | 33 +++++++++--- netbox/vpn/graphql/types.py | 24 +++++++-- 3 files changed, 114 insertions(+), 44 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index ac7134e56..c1f7f03f1 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -237,47 +237,47 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.interaces.all() @strawberry_django.field def rearports(self) -> List[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.rearports.all() @strawberry_django.field def consoleports(self) -> List[Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.consoleports.all() @strawberry_django.field def powerports(self) -> List[Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.powerports.all() @strawberry_django.field def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.cabletermination_set.all() @strawberry_django.field def consoleserverports(self) -> List[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.consoleserverports.all() @strawberry_django.field def poweroutlets(self) -> List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.poweroutlets.all() @strawberry_django.field def frontports(self) -> List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.frontports.all() @strawberry_django.field def modulebays(self) -> List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.modulebays.all() @strawberry_django.field def services(self) -> List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]: - return self.clusters.all() + return self.services.all() @strawberry_django.field def inventoryitems(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: - return self.clusters.all() + return self.inventoryitems.all() @strawberry_django.field def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: @@ -310,6 +310,26 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): class InventoryItemTemplateType(ComponentTemplateObjectType): _name: str + @strawberry_django.field + def parent(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.parent + + @strawberry_django.field + def child_items(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.child_items.all() + + @strawberry_django.field + def component(self) -> List[Annotated[Union[ + 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["PowerOutletType", strawberry.lazy('dcim.graphql.types')], + Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], + Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("InventoryItemComponentType")]]: + return self.component + @strawberry_django.type( models.DeviceRole, @@ -426,21 +446,36 @@ class FrontPortTemplateType(ComponentTemplateObjectType): filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): + mac_address: str + wwn: str - def resolve_poe_mode(self, info): - return self.poe_mode or None + @strawberry_django.field + def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: + return self.vdcs.all() - def resolve_poe_type(self, info): - return self.poe_type or None + @strawberry_django.field + def tagged_vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: + return self.tagged_vlans.all() - def resolve_mode(self, info): - return self.mode or None + @strawberry_django.field + def bridge_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.bridge_interfaces.all() - def resolve_rf_role(self, info): - return self.rf_role or None + @strawberry_django.field + def wireless_lans(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]: + return self.wireless_lans.all() - def resolve_rf_channel(self, info): - return self.rf_channel or None + @strawberry_django.field + def member_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.member_interfaces.all() + + @strawberry_django.field + def child_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.child_interfaces.all() + + @strawberry_django.field + def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: + return self.ip_addresses.all() @strawberry_django.type( @@ -451,14 +486,9 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa class InterfaceTemplateType(ComponentTemplateObjectType): _name: str - def resolve_poe_mode(self, info): - return self.poe_mode or None - - def resolve_poe_type(self, info): - return self.poe_type or None - - def resolve_rf_role(self, info): - return self.rf_role or None + @strawberry_django.field + def bridge_interfaces(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.bridge_interfaces.all() @strawberry_django.type( @@ -472,12 +502,19 @@ class InventoryItemType(ComponentObjectType): @strawberry_django.type( models.InventoryItemRole, - # fields='__all__', - exclude=('color',), # bug - temp + fields='__all__', filters=InventoryItemRoleFilter ) class InventoryItemRoleType(OrganizationalObjectType): - pass + color: str + + @strawberry_django.field + def inventory_items(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + return self.inventory_items.all() + + @strawberry_django.field + def inventory_item_templates(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.inventory_item_templates.all() @strawberry_django.type( diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 31bd0a9d9..5676ecb16 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -97,8 +97,9 @@ class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): ) class FHRPGroupType(NetBoxObjectType): - 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( @@ -123,10 +124,27 @@ class FHRPGroupAssignmentType(BaseObjectType): filters=IPAddressFilter ) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): - # assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType') + address: str - def resolve_role(self, info): - return self.role or None + @strawberry_django.field + def nat_outside(self) -> Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]: + return self.nat_outside + + @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( @@ -136,9 +154,8 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): filters=IPRangeFilter ) class IPRangeType(NetBoxObjectType): - - def resolve_role(self, info): - return self.role or None + start_address: str + end_address: str @strawberry_django.type( diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 68fe39403..2e57fe4bf 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -55,7 +55,10 @@ class TunnelType(NetBoxObjectType): filters=IKEProposalFilter ) class IKEProposalType(OrganizationalObjectType): - pass + + @strawberry_django.field + def ike_policies(self) -> List[Annotated["IKEPolicyType", strawberry.lazy('vpn.graphql.types')]]: + return self.ike_policies.all() @strawberry_django.type( @@ -80,7 +83,10 @@ class IKEPolicyType(OrganizationalObjectType): filters=IPSecProposalFilter ) class IPSecProposalType(OrganizationalObjectType): - pass + + @strawberry_django.field + def ipsec_policies(self) -> List[Annotated["IPSecPolicyType", strawberry.lazy('vpn.graphql.types')]]: + return self.ipsec_policies.all() @strawberry_django.type( @@ -89,7 +95,14 @@ class IPSecProposalType(OrganizationalObjectType): filters=IPSecPolicyFilter ) class IPSecPolicyType(OrganizationalObjectType): - pass + + @strawberry_django.field + def proposals(self) -> List[Annotated["IKEProposalType", strawberry.lazy('vpn.graphql.types')]]: + return self.proposals.all() + + @strawberry_django.field + def ipsec_profiles(self) -> List[Annotated["IPSecProposalType", strawberry.lazy('vpn.graphql.types')]]: + return self.ipsec_profiles.all() @strawberry_django.type( @@ -98,7 +111,10 @@ class IPSecPolicyType(OrganizationalObjectType): filters=IPSecProfileFilter ) class IPSecProfileType(OrganizationalObjectType): - pass + + @strawberry_django.field + def tunnels(self) -> List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]: + return self.tunnels.all() @strawberry_django.type( From 82c08d98208e004c147f1031430b1f025b30cebe Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 09:46:03 -0800 Subject: [PATCH 032/156] 9856 update types --- netbox/dcim/graphql/types.py | 151 +++++++++++++++++++++++++++++-- netbox/vpn/graphql/gfk_mixins.py | 30 ------ netbox/vpn/graphql/types.py | 25 ++++- 3 files changed, 164 insertions(+), 42 deletions(-) delete mode 100644 netbox/vpn/graphql/gfk_mixins.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index c1f7f03f1..87363ee16 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -499,6 +499,26 @@ class InterfaceTemplateType(ComponentTemplateObjectType): class InventoryItemType(ComponentObjectType): _name: str + @strawberry_django.field + def parent(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + return self.parent + + @strawberry_django.field + def child_items(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + return self.child_items.all() + + @strawberry_django.field + def component(self) -> List[Annotated[Union[ + 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["PowerOutletType", strawberry.lazy('dcim.graphql.types')], + Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], + Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("InventoryItemComponentType")]]: + return self.component + @strawberry_django.type( models.InventoryItemRole, @@ -524,7 +544,34 @@ class InventoryItemRoleType(OrganizationalObjectType): filters=LocationFilter ) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): - pass + + @strawberry_django.field + def children(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + + @strawberry_django.field + def powerpanel_set(self) -> List[Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerpanel_set.all() + + @strawberry_django.field + def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: + return self.cabletermination_set.all() + + @strawberry_django.field + def racks(self) -> List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]: + return self.racks.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() + + @strawberry_django.field + def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]: + return self.parent + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() @strawberry_django.type( @@ -533,7 +580,26 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi filters=ManufacturerFilter ) class ManufacturerType(OrganizationalObjectType, ContactsMixin): - pass + + @strawberry_django.field + def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]: + return self.platforms.all() + + @strawberry_django.field + def device_types(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.device_types.all() + + @strawberry_django.field + def inventory_item_templates(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.inventory_item_templates.all() + + @strawberry_django.field + def inventory_items(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + return self.inventory_items.all() + + @strawberry_django.field + def module_types(self) -> List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]: + return self.module_types.all() @strawberry_django.type( @@ -542,7 +608,34 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin): filters=ModuleFilter ) class ModuleType(ComponentObjectType): - pass + + @strawberry_django.field + def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def powerports(self) -> List[Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerports.all() + + @strawberry_django.field + def consoleserverports(self) -> List[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.consoleserverports.all() + + @strawberry_django.field + def consoleports(self) -> List[Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')]]: + return self.consoleports.all() + + @strawberry_django.field + def poweroutlets(self) -> List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]: + return self.poweroutlets.all() + + @strawberry_django.field + def rearports(self) -> List[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.rearports.all() + + @strawberry_django.field + def frontports(self) -> List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.frontports.all() @strawberry_django.type( @@ -551,7 +644,10 @@ class ModuleType(ComponentObjectType): filters=ModuleBayFilter ) class ModuleBayType(ComponentObjectType): - pass + + @strawberry_django.field + def installed_module(self) -> Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]: + return self.installed_module @strawberry_django.type( @@ -570,8 +666,37 @@ class ModuleBayTemplateType(ComponentTemplateObjectType): ) class ModuleTypeType(NetBoxObjectType): - def resolve_weight_unit(self, info): - return self.weight_unit or None + @strawberry_django.field + def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def consoleserverporttemplates(self) -> List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def interfacetemplates(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def powerporttemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def poweroutlettemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def rearporttemplates(self) -> List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def instances(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def consoleporttemplates(self) -> List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() @strawberry_django.type( @@ -580,7 +705,14 @@ class ModuleTypeType(NetBoxObjectType): filters=PlatformFilter ) class PlatformType(OrganizationalObjectType): - pass + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() @strawberry_django.type( @@ -627,7 +759,10 @@ class PowerOutletTemplateType(ComponentTemplateObjectType): filters=PowerPanelFilter ) class PowerPanelType(NetBoxObjectType, ContactsMixin): - pass + + @strawberry_django.field + def powerfeeds(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerfeeds.all() @strawberry_django.type( diff --git a/netbox/vpn/graphql/gfk_mixins.py b/netbox/vpn/graphql/gfk_mixins.py deleted file mode 100644 index 72272f7ad..000000000 --- a/netbox/vpn/graphql/gfk_mixins.py +++ /dev/null @@ -1,30 +0,0 @@ -import graphene - -from dcim.graphql.types import InterfaceType -from dcim.models import Interface -from ipam.graphql.types import VLANType -from ipam.models import VLAN -from virtualization.graphql.types import VMInterfaceType -from virtualization.models import VMInterface - -__all__ = ( - 'L2VPNAssignmentType', -) - - -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 diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 2e57fe4bf..b986fdf83 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated, List, Union import strawberry import strawberry_django @@ -123,7 +123,18 @@ class IPSecProfileType(OrganizationalObjectType): filters=L2VPNFilter ) class L2VPNType(ContactsMixin, NetBoxObjectType): - pass + + @strawberry_django.field + def export_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]: + return self.export_targets.all() + + @strawberry_django.field + def terminations(self) -> List[Annotated["L2VPNTerminationType", strawberry.lazy('vpn.graphql.types')]]: + return self.terminations.all() + + @strawberry_django.field + def import_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]: + return self.import_targets.all() @strawberry_django.type( @@ -132,5 +143,11 @@ class L2VPNType(ContactsMixin, NetBoxObjectType): filters=L2VPNTerminationFilter ) class L2VPNTerminationType(NetBoxObjectType): - # assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType') - pass + + @strawberry_django.field + def assigned_object(self) -> Annotated[Union[ + Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], + Annotated["VLANType", strawberry.lazy('ipam.graphql.types')], + Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')], + ], strawberry.union("L2VPNAssignmentType")]: + return self.assigned_object From 0387cb0a48c9ace49a80c6eacd03f9359f80cbcb Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 11:04:29 -0800 Subject: [PATCH 033/156] 9856 update types --- netbox/circuits/graphql/types.py | 27 ++++++++-- netbox/dcim/graphql/types.py | 89 ++++++++++++-------------------- netbox/ipam/graphql/types.py | 10 ++-- netbox/users/graphql/types.py | 6 +++ netbox/vpn/graphql/types.py | 10 +++- netbox/wireless/graphql/types.py | 17 +++--- 6 files changed, 84 insertions(+), 75 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 83244a098..420e13772 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -27,7 +27,22 @@ __all__ = ( filters=ProviderFilter ) class ProviderType(NetBoxObjectType, ContactsMixin): - pass + + @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( @@ -36,7 +51,10 @@ class ProviderType(NetBoxObjectType, ContactsMixin): filters=ProviderAccountFilter ) class ProviderAccountType(NetBoxObjectType): - pass + + @strawberry_django.field + def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]: + return self.circuits.all() @strawberry_django.type( @@ -45,7 +63,10 @@ class ProviderAccountType(NetBoxObjectType): filters=ProviderNetworkFilter ) class ProviderNetworkType(NetBoxObjectType): - pass + + @strawberry_django.field + def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]: + return self.circuit_terminations.all() @strawberry_django.type( diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 87363ee16..26855cab5 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -150,9 +150,7 @@ class CableType(NetBoxObjectType): filters=ConsolePortFilter ) class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - - def resolve_type(self, info): - return self.type or None + pass @strawberry_django.type( @@ -163,9 +161,6 @@ class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) class ConsolePortTemplateType(ComponentTemplateObjectType): _name: str - def resolve_type(self, info): - return self.type or None - @strawberry_django.type( models.ConsoleServerPort, @@ -174,9 +169,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): filters=ConsoleServerPortFilter ) class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - - def resolve_type(self, info): - return self.type or None + pass @strawberry_django.type( @@ -187,9 +180,6 @@ class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpoint class ConsoleServerPortTemplateType(ComponentTemplateObjectType): _name: str - def resolve_type(self, info): - return self.type or None - @strawberry_django.type( models.Device, @@ -209,12 +199,6 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo module_bay_count: BigInt inventory_item_count: BigInt - def resolve_face(self, info): - return self.face or None - - def resolve_airflow(self, info): - return self.airflow or None - @strawberry_django.field def devicebays(self) -> List[Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]]: return self.device_bays.all() @@ -366,15 +350,6 @@ class DeviceTypeType(NetBoxObjectType): module_bay_template_count: BigInt inventory_item_template_count: BigInt - def resolve_subdevice_role(self, info): - return self.subdevice_role or None - - def resolve_airflow(self, info): - return self.airflow or None - - def resolve_weight_unit(self, info): - return self.weight_unit or None - @strawberry_django.field def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: return self.device_bays.all() @@ -730,12 +705,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): filters=PowerOutletFilter ) class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - - def resolve_feed_leg(self, info): - return self.feed_leg or None - - def resolve_type(self, info): - return self.type or None + pass @strawberry_django.type( @@ -746,12 +716,6 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) class PowerOutletTemplateType(ComponentTemplateObjectType): _name: str - def resolve_feed_leg(self, info): - return self.feed_leg or None - - def resolve_type(self, info): - return self.type or None - @strawberry_django.type( models.PowerPanel, @@ -772,8 +736,9 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): ) class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - def resolve_type(self, info): - return self.type or None + @strawberry_django.field + def poweroutlets(self) -> List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]: + return self.poweroutlets.all() @strawberry_django.type( @@ -784,8 +749,9 @@ class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): class PowerPortTemplateType(ComponentTemplateObjectType): _name: str - def resolve_type(self, info): - return self.type or None + @strawberry_django.field + def poweroutlet_templates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.poweroutlet_templates.all() @strawberry_django.type( @@ -796,15 +762,6 @@ class PowerPortTemplateType(ComponentTemplateObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str - def resolve_type(self, info): - return self.type or None - - def resolve_outer_unit(self, info): - return self.outer_unit or None - - def resolve_weight_unit(self, info): - return self.weight_unit or None - @strawberry_django.type( models.RackReservation, @@ -813,7 +770,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje filters=RackReservationFilter ) class RackReservationType(NetBoxObjectType): - pass + units: List[int] @strawberry_django.type( @@ -823,7 +780,11 @@ class RackReservationType(NetBoxObjectType): filters=RackRoleFilter ) class RackRoleType(OrganizationalObjectType): - pass + color: str + + @strawberry_django.field + def racks(self) -> List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]: + return self.racks.all() @strawberry_django.type( @@ -833,7 +794,11 @@ class RackRoleType(OrganizationalObjectType): filters=RearPortFilter ) class RearPortType(ComponentObjectType, CabledObjectMixin): - pass + color: str + + @strawberry_django.field + def frontports(self) -> List[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]: + return self.frontports.all() @strawberry_django.type( @@ -844,6 +809,11 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): ) class RearPortTemplateType(ComponentTemplateObjectType): _name: str + color: str + + @strawberry_django.field + def frontport_templates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: + return self.frontport_templates.all() @strawberry_django.type( @@ -885,6 +855,10 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): class VirtualChassisType(NetBoxObjectType): member_count: BigInt + @strawberry_django.field + def members(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.members.all() + @strawberry_django.type( models.VirtualDeviceContext, @@ -892,4 +866,7 @@ class VirtualChassisType(NetBoxObjectType): filters=VirtualDeviceContextFilter ) class VirtualDeviceContextType(NetBoxObjectType): - pass + + @strawberry_django.field + def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 5676ecb16..dbefe50e1 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -165,7 +165,7 @@ class IPRangeType(NetBoxObjectType): filters=PrefixFilter ) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): - pass + prefix: str @strawberry_django.type( @@ -202,7 +202,11 @@ class RouteTargetType(NetBoxObjectType): filters=ServiceFilter ) class ServiceType(NetBoxObjectType): - pass + ports: List[int] + + @strawberry_django.field + def ipaddresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: + return self.ipaddresses.all() @strawberry_django.type( @@ -212,7 +216,7 @@ class ServiceType(NetBoxObjectType): filters=ServiceTemplateFilter ) class ServiceTemplateType(NetBoxObjectType): - pass + ports: List[int] @strawberry_django.type( diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index d6f67badd..5463ee2a8 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,3 +1,5 @@ +from typing import List + import strawberry import strawberry_django from django.contrib.auth import get_user_model @@ -36,3 +38,7 @@ class UserType: @classmethod def get_queryset(cls, queryset, info, **kwargs): return RestrictedQuerySet(model=get_user_model()).restrict(info.context.request.user, 'view') + + @strawberry_django.field + def groups(self) -> List[GroupType]: + return self.groups.all() diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index b986fdf83..fbd9683e7 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -28,7 +28,10 @@ __all__ = ( filters=TunnelGroupFilter ) class TunnelGroupType(OrganizationalObjectType): - pass + + @strawberry_django.field + def tunnels(self) -> List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]: + return self.tunnels.all() @strawberry_django.type( @@ -46,7 +49,10 @@ class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): filters=TunnelFilter ) class TunnelType(NetBoxObjectType): - pass + + @strawberry_django.field + def terminations(self) -> List[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]: + return self.terminations.all() @strawberry_django.type( diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index e7afe00c7..c3204aecb 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List, Union + import strawberry import strawberry_django @@ -29,11 +31,9 @@ class WirelessLANGroupType(OrganizationalObjectType): ) class WirelessLANType(NetBoxObjectType): - def resolve_auth_type(self, info): - return self.auth_type or None - - def resolve_auth_cipher(self, info): - return self.auth_cipher or None + @strawberry_django.field + def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() @strawberry_django.type( @@ -42,9 +42,4 @@ class WirelessLANType(NetBoxObjectType): filters=WirelessLinkFilter ) class WirelessLinkType(NetBoxObjectType): - - def resolve_auth_type(self, info): - return self.auth_type or None - - def resolve_auth_cipher(self, info): - return self.auth_cipher or None + pass From 4d0d19bb76a026f164dce974fc5d84ebb40ed822 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 11:18:03 -0800 Subject: [PATCH 034/156] 9856 update types --- netbox/dcim/graphql/types.py | 37 ++++++++++++++++++++++++++++++- netbox/ipam/graphql/types.py | 43 +++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 26855cab5..c8614e67d 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -762,6 +762,26 @@ class PowerPortTemplateType(ComponentTemplateObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str + @strawberry_django.field + def reservations(self) -> List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]: + return self.reservations.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() + + @strawberry_django.field + def powerfeed_set(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerfeed_set.all() + + @strawberry_django.field + def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: + return self.cabletermination_set.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() + @strawberry_django.type( models.RackReservation, @@ -823,7 +843,22 @@ class RearPortTemplateType(ComponentTemplateObjectType): filters=RegionFilter ) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - pass + + @strawberry_django.field + def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]: + return self.region + + @strawberry_django.field + def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: + return self.sites.all() + + @strawberry_django.field + def children(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() @strawberry_django.type( diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index dbefe50e1..b98e1d7a2 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -174,7 +174,18 @@ class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): filters=RIRFilter ) class RIRType(OrganizationalObjectType): - pass + + @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( @@ -183,7 +194,18 @@ class RIRType(OrganizationalObjectType): filters=RoleFilter ) class RoleType(OrganizationalObjectType): - pass + + @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( @@ -192,7 +214,22 @@ class RoleType(OrganizationalObjectType): filters=RouteTargetFilter ) class RouteTargetType(NetBoxObjectType): - pass + + @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( From 3e284c59d87f355f7f1de4b913fcf28bfd1e6e93 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 11:26:50 -0800 Subject: [PATCH 035/156] 9856 update types --- netbox/dcim/graphql/types.py | 66 +++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index c8614e67d..bb64471fb 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -870,6 +870,55 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str asn: BigInt + time_zone: str + + @strawberry_django.field + def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]: + return self.prefixes.all() + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def racks(self) -> List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]: + return self.racks.all() + + @strawberry_django.field + def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: + return self.cabletermiantion_set.all() + + @strawberry_django.field + def powerpanel_set(self) -> List[Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerpanel_set.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() + + @strawberry_django.field + def locations(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: + return self.locations.all() + + @strawberry_django.field + def asns(self) -> List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]: + return self.asns.all() + + @strawberry_django.field + def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]: + return self.circuit_terminations.all() + + @strawberry_django.field + def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlans.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() @strawberry_django.type( @@ -879,7 +928,22 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje filters=SiteGroupFilter ) class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - pass + + @strawberry_django.field + def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]: + return self.region + + @strawberry_django.field + def children(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + + @strawberry_django.field + def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: + return self.sites.all() + + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() @strawberry_django.type( From 44f4d60f5d7f74bff95bd33672fb7443cc7a7d54 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 12:57:53 -0800 Subject: [PATCH 036/156] 9856 update types --- netbox/extras/graphql/types.py | 8 ++- netbox/tenancy/graphql/types.py | 110 +++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index a66dd2cba..f148a250b 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -9,7 +9,7 @@ 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__ = ( @@ -200,7 +200,11 @@ class SavedFilterType(ObjectType): filters=TagFilter ) class TagType(ObjectType): - pass + color: str + + @strawberry_django.field + def object_types(self) -> List[ContentTypeType]: + return self.object_types.all() @strawberry_django.type( diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 828a36930..27a260e3d 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -35,7 +35,102 @@ class ContactAssignmentsMixin: filters=TenantFilter ) class TenantType(NetBoxObjectType): - pass + + @strawberry_django.field + def asns(self) -> List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]: + return self.asns.all() + + @strawberry_django.field + def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]: + return self.circuits.all() + + @strawberry_django.field + def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: + return self.sites.all() + + @strawberry_django.field + def vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlans.all() + + @strawberry_django.field + def wireless_lans(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]: + return self.wireless_lans.all() + + @strawberry_django.field + def route_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]: + return self.route_targets.all() + + @strawberry_django.field + def locations(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: + return self.locations.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 rackreservations(self) -> List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]: + return self.rackreservations.all() + + @strawberry_django.field + def racks(self) -> List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]: + return self.racks.all() + + @strawberry_django.field + def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: + return self.vdcs.all() + + @strawberry_django.field + def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]: + return self.prefixes.all() + + @strawberry_django.field + def cables(self) -> List[Annotated["CableType", strawberry.lazy('dcim.graphql.types')]]: + return self.cables.all() + + @strawberry_django.field + def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtual_machines.all() + + @strawberry_django.field + def vrfs(self) -> List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]]: + return self.vrfs.all() + + @strawberry_django.field + def asn_ranges(self) -> List[Annotated["ASNRangeType", strawberry.lazy('ipam.graphql.types')]]: + return self.asn_ranges.all() + + @strawberry_django.field + def wireless_links(self) -> List[Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')]]: + return self.wireless_links.all() + + @strawberry_django.field + def aggregates(self) -> List[Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')]]: + return self.aggregates.all() + + @strawberry_django.field + def power_feeds(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: + return self.power_feeds.all() + + @strawberry_django.field + def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: + return self.devices.all() + + @strawberry_django.field + def tunnels(self) -> List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]: + return self.tunnels.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 clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: + return self.clusters.all() + + @strawberry_django.field + def l2vpns(self) -> List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]: + return self.l2vpns.all() @strawberry_django.type( @@ -45,7 +140,18 @@ class TenantType(NetBoxObjectType): filters=TenantGroupFilter ) class TenantGroupType(OrganizationalObjectType): - pass + + @strawberry_django.field + def parent(self) -> Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]: + return self.parent + + @strawberry_django.field + def tenants(self) -> List[TenantType]: + return self.tenants.all() + + @strawberry_django.field + def children(self) -> List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]: + return self.children.all() # From 497de46ad9b745a2cab6f34cf42f924a398edec6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Feb 2024 14:13:44 -0800 Subject: [PATCH 037/156] 9856 update types --- netbox/dcim/graphql/types.py | 2 +- netbox/ipam/graphql/types.py | 78 +++++++++++++++++++++++--- netbox/virtualization/graphql/types.py | 51 +++++++++++++---- netbox/wireless/graphql/types.py | 13 ++++- 4 files changed, 123 insertions(+), 21 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index bb64471fb..7f238b987 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -311,7 +311,7 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], - ], strawberry.union("InventoryItemComponentType")]]: + ], strawberry.union("InventoryItemTemplateComponentType")]]: return self.component diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index b98e1d7a2..283dd3e30 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -234,8 +234,7 @@ class RouteTargetType(NetBoxObjectType): @strawberry_django.type( models.Service, - # fields='__all__', - exclude=('ports',), # bug - temp + fields='__all__', filters=ServiceFilter ) class ServiceType(NetBoxObjectType): @@ -248,8 +247,7 @@ class ServiceType(NetBoxObjectType): @strawberry_django.type( models.ServiceTemplate, - # fields='__all__', - exclude=('ports',), # bug - temp + fields='__all__', filters=ServiceTemplateFilter ) class ServiceTemplateType(NetBoxObjectType): @@ -262,7 +260,30 @@ class ServiceTemplateType(NetBoxObjectType): filters=VLANFilter ) class VLANType(NetBoxObjectType): - pass + + @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( @@ -271,8 +292,22 @@ class VLANType(NetBoxObjectType): filters=VLANGroupFilter ) class VLANGroupType(OrganizationalObjectType): - # scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType') - pass + + @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( @@ -281,4 +316,31 @@ class VLANGroupType(OrganizationalObjectType): filters=VRFFilter ) class VRFType(NetBoxObjectType): - pass + + @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() diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index bfac3ef44..a263a8d9d 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -6,6 +6,7 @@ import strawberry_django from dcim.graphql.types import ComponentObjectType from extras.graphql.mixins import ConfigContextMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin +from netbox.graphql.scalars import BigInt from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType from virtualization import models from .filters import * @@ -70,33 +71,61 @@ class ClusterTypeType(OrganizationalObjectType): @strawberry_django.type( models.VirtualMachine, - # fields='__all__', - exclude=('_name', 'interface_count', 'virtual_disk_count',), # bug - temp + fields='__all__', filters=VirtualMachineFilter ) class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): _name: str + interface_count: BigInt + virtual_disk_count: BigInt + + @strawberry_django.field + def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.interfaces.all() + + @strawberry_django.field + def services(self) -> List[Annotated["ServiceType", strawberry.lazy('ipam.graphql.types')]]: + return self.services.all() + + @strawberry_django.field + def virtualdisks(self) -> List[Annotated["VirtualDiskType", strawberry.lazy('virtualization.graphql.types')]]: + return self.virtualdisks.all() @strawberry_django.type( models.VMInterface, - # fields='__all__', - exclude=('mac_address', '_name',), # bug - temp + fields='__all__', filters=VMInterfaceFilter ) class VMInterfaceType(IPAddressesMixin, ComponentObjectType): + _name: str + mac_address: str - def resolve_mode(self, info): - return self.mode or None + @strawberry_django.field + def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: + return self.ip_addresses.all() + + @strawberry_django.field + def tagged_vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: + return self.tagged_vlans.all() + + @strawberry_django.field + def mac_address(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: + return self.mac_address.all() + + @strawberry_django.field + def bridge_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.bridge_interfaces.all() + + @strawberry_django.field + def child_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: + return self.child_interfaces.all() @strawberry_django.type( models.VirtualDisk, - # fields='__all__', - exclude=('_name',), # bug - temp + fields='__all__', filters=VirtualDiskFilter ) class VirtualDiskType(ComponentObjectType): - - def resolve_mode(self, info): - return self.mode or None + _name: str diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index c3204aecb..189c565ec 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -21,7 +21,18 @@ __all__ = ( filters=WirelessLANGroupFilter ) class WirelessLANGroupType(OrganizationalObjectType): - pass + + @strawberry_django.field + def parent(self) -> Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]: + return self.parent + + @strawberry_django.field + def wireless_lans(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]: + return self.wireless_lans.all() + + @strawberry_django.field + def children(self) -> List[Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]]: + return self.children.all() @strawberry_django.type( From 14f04453bbc7def7cbd8db971c3f097fa4ec9aa2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 5 Mar 2024 08:30:34 -0800 Subject: [PATCH 038/156] 9856 GraphQLView --- netbox/dcim/graphql/types.py | 2 +- netbox/extras/graphql/mixins.py | 12 +++++----- netbox/ipam/graphql/mixins.py | 4 ++-- netbox/netbox/graphql/views.py | 8 ++++--- netbox/netbox/urls.py | 5 ++-- netbox/utilities/testing/api.py | 41 ++++++++++++++------------------- 6 files changed, 33 insertions(+), 39 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 7f238b987..81df74d4a 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -541,7 +541,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi return self.vlan_groups.all() @strawberry_django.field - def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]: + def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @strawberry_django.field diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 04c06c9c3..91014fbbd 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -24,13 +24,13 @@ if TYPE_CHECKING: class ChangelogMixin: @strawberry_django.field - def changelog(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: + 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 @@ -53,16 +53,16 @@ class CustomFieldsMixin: class ImageAttachmentsMixin: @strawberry_django.field - def image_attachments(self) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]: - return self.images.restrict(info.context.user, 'view') + def image_attachments(self, info) -> List[Annotated["ImageAttachmentType", strawberry.lazy('.types')]]: + return self.images.restrict(info.context.request.user, 'view') @strawberry.type class JournalEntriesMixin: @strawberry_django.field - def journal_entries(self) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]: - return self.journal_entries.restrict(info.context.user, 'view') + def journal_entries(self, info) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]: + return self.journal_entries.restrict(info.context.request.user, 'view') @strawberry.type diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py index 283414df3..38c7657a5 100644 --- a/netbox/ipam/graphql/mixins.py +++ b/netbox/ipam/graphql/mixins.py @@ -10,11 +10,11 @@ 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') + return self.ip_addresses.restrict(info.context.request.user, 'view') 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') + return self.vlan_groups.restrict(info.context.request.user, 'view') diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py index e1573dba6..d39f13807 100644 --- a/netbox/netbox/graphql/views.py +++ b/netbox/netbox/graphql/views.py @@ -2,19 +2,21 @@ from django.conf import settings from django.contrib.auth.views import redirect_to_login from django.http import HttpResponseNotFound, HttpResponseForbidden 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() diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index cf1086f99..1ce929513 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,14 +1,13 @@ from django.conf import settings from django.conf.urls import include from django.urls import path -from django.views.decorators.csrf import csrf_exempt from django.views.static import serve from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from account.views import LoginView, LogoutView from netbox.api.views import APIRootView, StatusView from netbox.graphql.schema import schema -from netbox.graphql.views import GraphQLView +from netbox.graphql.views import NetBoxGraphQLView from netbox.plugins.urls import plugin_patterns, plugin_api_patterns from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx from strawberry.django.views import GraphQLView @@ -61,7 +60,7 @@ _patterns = [ path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'), # GraphQL - path('graphql/', GraphQLView.as_view(schema=schema), name='graphql'), + path('graphql/', NetBoxGraphQLView.as_view(schema=schema), name='graphql'), # Serving static media in Django to pipe it through LoginRequiredMiddleware path('media/', serve, {'document_root': settings.MEDIA_ROOT}), diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index ce2817777..384d67707 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -1,5 +1,6 @@ import inspect import json +import strawberry_django from django.conf import settings from django.contrib.auth import get_user_model @@ -18,7 +19,7 @@ from .base import ModelTestCase from .utils import disable_warnings from ipam.graphql.types import IPAddressFamilyType - +from strawberry.type import StrawberryList __all__ = ( 'APITestCase', @@ -447,36 +448,26 @@ class APIViewTestCases: # Compile list of fields to include fields_string = '' - for field_name, field in type_class.__dataclass_fields__.items(): + for field in type_class.__strawberry_definition__.fields: # for field_name, field in type_class._meta.fields.items(): - print(f"field_name: {field_name} field: {field}") - is_string_array = False - if type(field.type) is GQLList: - if field.type.of_type is GQLString: - is_string_array = True - elif type(field.type.of_type) is GQLNonNull and field.type.of_type.of_type is GQLString: - is_string_array = True + print(f"field_name: {field.name} type: {field.type}") - if type(field) is GQLDynamic: + if type(field.type) is StrawberryList: + fields_string += f'{field.name} {{ id }}\n' + elif field.type is strawberry_django.fields.types.DjangoModelType: # Dynamic fields must specify a subselection - fields_string += f'{field_name} {{ id }}\n' + fields_string += f'{field.name} {{ id }}\n' # TODO: Improve field detection logic to avoid nested ArrayFields - elif field_name == 'extra_choices': + elif field.name == 'extra_choices': continue - elif inspect.isclass(field.type) and issubclass(field.type, GQLUnion): - # Union types dont' have an id or consistent values - continue - elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion): - # Union types dont' have an id or consistent values - continue - elif type(field.type) is GQLList and not is_string_array: - # TODO: Come up with something more elegant - # Temporary hack to support automated testing of reverse generic relations - fields_string += f'{field_name} {{ id }}\n' + # elif type(field.type) is GQLList and not is_string_array: + # # TODO: Come up with something more elegant + # # Temporary hack to support automated testing of reverse generic relations + # fields_string += f'{field_name} {{ id }}\n' elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType): - fields_string += f'{field_name} {{ value, label }}\n' + fields_string += f'{field.name} {{ value, label }}\n' else: - fields_string += f'{field_name}\n' + fields_string += f'{field.name}\n' query = f""" {{ @@ -486,6 +477,7 @@ class APIViewTestCases: }} """ + print(query) return query @override_settings(LOGIN_REQUIRED=True) @@ -498,6 +490,7 @@ class APIViewTestCases: # Non-authenticated requests should fail with disable_warnings('django.request'): + print(f"url: {url}") self.assertHttpStatus(self.client.post(url, data={'query': query}), status.HTTP_403_FORBIDDEN) # Add object-level permission From f8748011f3feaead102ba10cd8b986bf89b8eabe Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 5 Mar 2024 16:27:34 -0800 Subject: [PATCH 039/156] 9856 GraphQLView --- netbox/extras/graphql/mixins.py | 2 +- netbox/netbox/graphql/views.py | 5 +++-- netbox/utilities/testing/api.py | 35 ++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 91014fbbd..df8d06b3b 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -62,7 +62,7 @@ class JournalEntriesMixin: @strawberry_django.field def journal_entries(self, info) -> List[Annotated["JournalEntryType", strawberry.lazy('.types')]]: - return self.journal_entries.restrict(info.context.request.user, 'view') + return self.journal_entries.all() @strawberry.type diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py index d39f13807..b61157403 100644 --- a/netbox/netbox/graphql/views.py +++ b/netbox/netbox/graphql/views.py @@ -38,8 +38,9 @@ class NetBoxGraphQLView(GraphQLView): 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): - return redirect_to_login(reverse('graphql')) + # bug - todo? + # if self.request_wants_html(request): + # return redirect_to_login(reverse('graphql')) return HttpResponseForbidden("No credentials provided.") diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 384d67707..61899aebf 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -19,7 +19,8 @@ from .base import ModelTestCase from .utils import disable_warnings from ipam.graphql.types import IPAddressFamilyType -from strawberry.type import StrawberryList +from strawberry.lazy_type import LazyType +from strawberry.type import StrawberryList, StrawberryOptional __all__ = ( 'APITestCase', @@ -450,13 +451,22 @@ class APIViewTestCases: for field in type_class.__strawberry_definition__.fields: # for field_name, field in type_class._meta.fields.items(): - print(f"field_name: {field.name} type: {field.type}") + # print(f"field_name: {field.name} type: {field.type}") + + if field.name == 'site': + # breakpoint() + pass if type(field.type) is StrawberryList: fields_string += f'{field.name} {{ id }}\n' elif field.type is strawberry_django.fields.types.DjangoModelType: # Dynamic fields must specify a subselection - fields_string += f'{field.name} {{ id }}\n' + fields_string += f'{field.name} {{ pk }}\n' + elif type(field.type) is StrawberryOptional: + if type(field.type.of_type) is LazyType: + fields_string += f'{field.name} {{ id }}\n' + elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: + fields_string += f'{field.name} {{ pk }}\n' # TODO: Improve field detection logic to avoid nested ArrayFields elif field.name == 'extra_choices': continue @@ -477,7 +487,15 @@ class APIViewTestCases: }} """ - print(query) + if "_list" not in name: + query = f""" + {{ + {name}_list {{ + {fields_string} + }} + }} + """ + return query @override_settings(LOGIN_REQUIRED=True) @@ -490,8 +508,7 @@ class APIViewTestCases: # Non-authenticated requests should fail with disable_warnings('django.request'): - print(f"url: {url}") - self.assertHttpStatus(self.client.post(url, data={'query': query}), status.HTTP_403_FORBIDDEN) + self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json"), status.HTTP_403_FORBIDDEN) # Add object-level permission obj_perm = ObjectPermission( @@ -502,7 +519,7 @@ class APIViewTestCases: obj_perm.users.add(self.user) obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) - response = self.client.post(url, data={'query': query}, **self.header) + response = self.client.post(url, data={'query': query}, format="json", **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) data = json.loads(response.content) self.assertNotIn('errors', data) @@ -516,7 +533,7 @@ class APIViewTestCases: # Non-authenticated requests should fail with disable_warnings('django.request'): - self.assertHttpStatus(self.client.post(url, data={'query': query}), status.HTTP_403_FORBIDDEN) + self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json"), status.HTTP_403_FORBIDDEN) # Add object-level permission obj_perm = ObjectPermission( @@ -527,7 +544,7 @@ class APIViewTestCases: obj_perm.users.add(self.user) obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) - response = self.client.post(url, data={'query': query}, **self.header) + response = self.client.post(url, data={'query': query}, format="json", **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) data = json.loads(response.content) self.assertNotIn('errors', data) From 7c289aebc744682370d6a5acac5175de0dbec93d Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 07:52:42 -0800 Subject: [PATCH 040/156] 9856 fix OrganizationalObjectType --- netbox/dcim/graphql/types.py | 24 ------------------------ netbox/netbox/graphql/types.py | 10 +++++++++- netbox/tenancy/graphql/types.py | 16 ---------------- netbox/wireless/graphql/types.py | 8 -------- 4 files changed, 9 insertions(+), 49 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 81df74d4a..06d3ec425 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -520,10 +520,6 @@ class InventoryItemRoleType(OrganizationalObjectType): ) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): - @strawberry_django.field - def children(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: - return self.children.all() - @strawberry_django.field def powerpanel_set(self) -> List[Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]]: return self.powerpanel_set.all() @@ -540,10 +536,6 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: return self.vlan_groups.all() - @strawberry_django.field - def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: - return self.parent - @strawberry_django.field def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: return self.devices.all() @@ -844,18 +836,10 @@ class RearPortTemplateType(ComponentTemplateObjectType): ) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - @strawberry_django.field - def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]: - return self.region - @strawberry_django.field def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: return self.sites.all() - @strawberry_django.field - def children(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]: - return self.children.all() - @strawberry_django.field def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: return self.vlan_groups.all() @@ -929,14 +913,6 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje ) class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): - @strawberry_django.field - def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]: - return self.region - - @strawberry_django.field - def children(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]: - return self.children.all() - @strawberry_django.field def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: return self.sites.all() diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index f1a7bcfe5..7d7fea992 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,3 +1,5 @@ +from typing import Annotated, List + import strawberry from strawberry import auto import strawberry_django @@ -64,7 +66,13 @@ class OrganizationalObjectType( """ Base type for organizational models """ - pass + @strawberry_django.field + def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent + + @strawberry_django.field + def children(self) -> List[Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]]: + return self.children.all() class NetBoxObjectType( diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 27a260e3d..77c7cfea8 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -141,18 +141,10 @@ class TenantType(NetBoxObjectType): ) class TenantGroupType(OrganizationalObjectType): - @strawberry_django.field - def parent(self) -> Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]: - return self.parent - @strawberry_django.field def tenants(self) -> List[TenantType]: return self.tenants.all() - @strawberry_django.field - def children(self) -> List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]: - return self.children.all() - # # Contacts @@ -190,18 +182,10 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): ) class ContactGroupType(OrganizationalObjectType): - @strawberry_django.field - def parent(self) -> Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')]: - return self.parent - @strawberry_django.field def contacts(self) -> List[ContactType]: return self.clusters.all() - @strawberry_django.field - def children(self) -> List[Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')]]: - return self.children.all() - @strawberry_django.type( models.ContactAssignment, diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 189c565ec..12e9c57ae 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -22,18 +22,10 @@ __all__ = ( ) class WirelessLANGroupType(OrganizationalObjectType): - @strawberry_django.field - def parent(self) -> Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]: - return self.parent - @strawberry_django.field def wireless_lans(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]: return self.wireless_lans.all() - @strawberry_django.field - def children(self) -> List[Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]]: - return self.children.all() - @strawberry_django.type( models.WirelessLAN, From 2f719269e812567e3665d31893e3a3b4becb026f Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 09:53:46 -0800 Subject: [PATCH 041/156] 9856 single item query for schema --- netbox/circuits/graphql/schema.py | 24 +++- netbox/core/graphql/schema.py | 8 +- netbox/dcim/graphql/schema.py | 160 ++++++++++++++++++------ netbox/extras/graphql/schema.py | 48 +++++-- netbox/ipam/graphql/schema.py | 64 +++++++--- netbox/tenancy/graphql/schema.py | 24 +++- netbox/users/graphql/schema.py | 8 +- netbox/virtualization/graphql/schema.py | 24 +++- netbox/vpn/graphql/schema.py | 40 ++++-- netbox/wireless/graphql/schema.py | 12 +- 10 files changed, 309 insertions(+), 103 deletions(-) diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 7b0356920..48eff5413 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -8,20 +8,32 @@ from .types import * @strawberry.type class CircuitsQuery: - circuit: CircuitType = strawberry_django.field() + @strawberry.field + def circuit(self, id: int) -> CircuitType: + return models.Circuit.objects.get(id=id) circuit_list: List[CircuitType] = strawberry_django.field() - circuit_termination: CircuitTerminationType = strawberry_django.field() + @strawberry.field + def circuit_termination(self, id: int) -> CircuitTerminationType: + return models.CircuitTermination.objects.get(id=id) circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field() - circuit_type: CircuitTypeType = strawberry_django.field() + @strawberry.field + def circuit_type(self, id: int) -> CircuitTypeType: + return models.CircuitType.objects.get(id=id) circuit_type_list: List[CircuitTypeType] = strawberry_django.field() - provider: ProviderType = strawberry_django.field() + @strawberry.field + def provider(self, id: int) -> ProviderType: + return models.Provider.objects.get(id=id) provider_list: List[ProviderType] = strawberry_django.field() - provider_account: ProviderAccountType = strawberry_django.field() + @strawberry.field + def provider_account(self, id: int) -> ProviderAccountType: + return models.ProviderAccount.objects.get(id=id) provider_account_list: List[ProviderAccountType] = strawberry_django.field() - provider_network: ProviderNetworkType = strawberry_django.field() + @strawberry.field + def provider_network(self, id: int) -> ProviderNetworkType: + return models.ProviderNetwork.objects.get(id=id) provider_network_list: List[ProviderNetworkType] = strawberry_django.field() diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py index 7118da11b..3dc4c7806 100644 --- a/netbox/core/graphql/schema.py +++ b/netbox/core/graphql/schema.py @@ -8,8 +8,12 @@ from .types import * @strawberry.type class CoreQuery: - data_file: DataFileType = strawberry_django.field() + @strawberry.field + def data_file(self, id: int) -> DataFileType: + return models.DataFile.objects.get(id=id) data_file_list: List[DataFileType] = strawberry_django.field() - data_source: DataSourceType = strawberry_django.field() + @strawberry.field + def data_source(self, id: int) -> DataSourceType: + return models.DataSource.objects.get(id=id) data_source_list: List[DataSourceType] = strawberry_django.field() diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index 41a273868..9dc3388b7 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -8,122 +8,202 @@ from .types import * @strawberry.type class DCIMQuery: - cable: CableType = strawberry_django.field() + @strawberry.field + def cable(self, id: int) -> CableType: + return models.Cable.objects.get(id=id) cable_list: List[CableType] = strawberry_django.field() - console_port: ConsolePortType = strawberry_django.field() + @strawberry.field + def console_port(self, id: int) -> ConsolePortType: + return models.ConsolePort.objects.get(id=id) console_port_list: List[ConsolePortType] = strawberry_django.field() - console_port_template: ConsolePortTemplateType = strawberry_django.field() + @strawberry.field + def console_port_template(self, id: int) -> ConsolePortTemplateType: + return models.ConsolePortTemplate.objects.get(id=id) console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field() - console_server_port: ConsoleServerPortType = strawberry_django.field() + @strawberry.field + def console_server_port(self, id: int) -> ConsoleServerPortType: + return models.ConsoleServerPort.objects.get(id=id) console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field() - console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field() + @strawberry.field + def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType: + return models.ConsoleServerPortTemplate.objects.get(id=id) console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field() - device: DeviceType = strawberry_django.field() + @strawberry.field + def device(self, id: int) -> DeviceType: + return models.Device.objects.get(id=id) device_list: List[DeviceType] = strawberry_django.field() - device_bay: DeviceBayType = strawberry_django.field() + @strawberry.field + def device_bay(self, id: int) -> DeviceBayType: + return models.DeviceBay.objects.get(id=id) device_bay_list: List[DeviceBayType] = strawberry_django.field() - device_bay_template: DeviceBayTemplateType = strawberry_django.field() + @strawberry.field + def device_bay_template(self, id: int) -> DeviceBayTemplateType: + return models.DeviceBayTemplate.objects.get(id=id) device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field() - device_role: DeviceRoleType = strawberry_django.field() + @strawberry.field + def device_role(self, id: int) -> DeviceRoleType: + return models.DeviceRole.objects.get(id=id) device_role_list: List[DeviceRoleType] = strawberry_django.field() - device_type: DeviceTypeType = strawberry_django.field() + @strawberry.field + def device_type(self, id: int) -> DeviceTypeType: + return models.DeviceType.objects.get(id=id) device_type_list: List[DeviceTypeType] = strawberry_django.field() - front_port: FrontPortType = strawberry_django.field() + @strawberry.field + def front_port(self, id: int) -> FrontPortType: + return models.FrontPort.objects.get(id=id) front_port_list: List[FrontPortType] = strawberry_django.field() - front_port_template: FrontPortTemplateType = strawberry_django.field() + @strawberry.field + def front_port_template(self, id: int) -> FrontPortTemplateType: + return models.FrontPortTemplate.objects.get(id=id) front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field() - interface: InterfaceType = strawberry_django.field() + @strawberry.field + def interface(self, id: int) -> InterfaceType: + return models.Interface.objects.get(id=id) interface_list: List[InterfaceType] = strawberry_django.field() - interface_template: InterfaceTemplateType = strawberry_django.field() + @strawberry.field + def interface_template(self, id: int) -> InterfaceTemplateType: + return models.InterfaceTemplate.objects.get(id=id) interface_template_list: List[InterfaceTemplateType] = strawberry_django.field() - inventory_item: InventoryItemType = strawberry_django.field() + @strawberry.field + def inventory_item(self, id: int) -> InventoryItemType: + return models.InventoryItem.objects.get(id=id) inventory_item_list: List[InventoryItemType] = strawberry_django.field() - inventory_item_role: InventoryItemRoleType = strawberry_django.field() + @strawberry.field + def inventory_item_role(self, id: int) -> InventoryItemRoleType: + return models.InventoryItemRole.objects.get(id=id) inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field() - inventory_item_template: InventoryItemTemplateType = strawberry_django.field() + @strawberry.field + def inventory_item_template(self, id: int) -> InventoryItemTemplateType: + return models.InventoryItemTemplate.objects.get(id=id) inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field() - location: LocationType = strawberry_django.field() + @strawberry.field + def location(self, id: int) -> LocationType: + return models.Location.objects.get(id=id) location_list: List[LocationType] = strawberry_django.field() - manufacturer: ManufacturerType = strawberry_django.field() + @strawberry.field + def manufacturer(self, id: int) -> ManufacturerType: + return models.Manufacturer.objects.get(id=id) manufacturer_list: List[ManufacturerType] = strawberry_django.field() - module: ModuleType = strawberry_django.field() + @strawberry.field + def module(self, id: int) -> ModuleType: + return models.Module.objects.get(id=id) module_list: List[ModuleType] = strawberry_django.field() - module_bay: ModuleBayType = strawberry_django.field() + @strawberry.field + def module_bay(self, id: int) -> ModuleBayType: + return models.ModuleBay.objects.get(id=id) module_bay_list: List[ModuleBayType] = strawberry_django.field() - module_bay_template: ModuleBayTemplateType = strawberry_django.field() + @strawberry.field + def module_bay_template(self, id: int) -> ModuleBayTemplateType: + return models.ModuleBayTemplate.objects.get(id=id) module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field() - module_type: ModuleTypeType = strawberry_django.field() + @strawberry.field + def module_type(self, id: int) -> ModuleTypeType: + return models.ModuleType.objects.get(id=id) module_type_list: List[ModuleTypeType] = strawberry_django.field() - platform: PlatformType = strawberry_django.field() + @strawberry.field + def platform(self, id: int) -> PlatformType: + return models.Platform.objects.get(id=id) platform_list: List[PlatformType] = strawberry_django.field() - power_feed: PowerFeedType = strawberry_django.field() + @strawberry.field + def power_feed(self, id: int) -> PowerFeedType: + return models.PowerFeed.objects.get(id=id) power_feed_list: List[PowerFeedType] = strawberry_django.field() - power_outlet: PowerOutletType = strawberry_django.field() + @strawberry.field + def power_outlet(self, id: int) -> PowerOutletType: + return models.PowerOutlet.objects.get(id=id) power_outlet_list: List[PowerOutletType] = strawberry_django.field() - power_outlet_template: PowerOutletTemplateType = strawberry_django.field() + @strawberry.field + def power_outlet_template(self, id: int) -> PowerOutletTemplateType: + return models.PowerOutletTemplate.objects.get(id=id) power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field() - power_panel: PowerPanelType = 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() - power_port: PowerPortType = 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() - power_port_template: PowerPortTemplateType = 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() - rack: RackType = 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() - rack_reservation: RackReservationType = 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() - rack_role: RackRoleType = 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() - rear_port: RearPortType = 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() - rear_port_template: RearPortTemplateType = 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() - region: RegionType = 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() - site: SiteType = 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() - site_group: SiteGroupType = 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() - virtual_chassis: VirtualChassisType = 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() - virtual_device_context: VirtualDeviceContextType = 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() diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index 14069021b..0f1e99c47 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -8,38 +8,62 @@ from .types import * @strawberry.type class ExtrasQuery: - config_context: ConfigContextType = strawberry_django.field() + @strawberry.field + def config_context(self, id: int) -> ConfigContextType: + return models.ConfigContext.objects.get(id=id) config_context_list: List[ConfigContextType] = strawberry_django.field() - config_template: ConfigTemplateType = strawberry_django.field() + @strawberry.field + def config_template(self, id: int) -> ConfigTemplateType: + return models.ConfigTemplate.objects.get(id=id) config_template_list: List[ConfigTemplateType] = strawberry_django.field() - custom_field: CustomFieldType = strawberry_django.field() + @strawberry.field + def custom_field(self, id: int) -> CustomFieldType: + return models.CustomField.objects.get(id=id) custom_field_list: List[CustomFieldType] = strawberry_django.field() - custom_field_choice_set: CustomFieldChoiceSetType = strawberry_django.field() + @strawberry.field + def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType: + return models.CustomFieldChoiceSet.objects.get(id=id) custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field() - custom_link: CustomLinkType = strawberry_django.field() + @strawberry.field + def custom_link(self, id: int) -> CustomLinkType: + return models.CustomLink.objects.get(id=id) custom_link_list: List[CustomLinkType] = strawberry_django.field() - export_template: ExportTemplateType = strawberry_django.field() + @strawberry.field + def export_template(self, id: int) -> ExportTemplateType: + return models.ExportTemplate.objects.get(id=id) export_template_list: List[ExportTemplateType] = strawberry_django.field() - image_attachment: ImageAttachmentType = strawberry_django.field() + @strawberry.field + def image_attachment(self, id: int) -> ImageAttachmentType: + return models.ImageAttachment.objects.get(id=id) image_attachment_list: List[ImageAttachmentType] = strawberry_django.field() - saved_filter: SavedFilterType = strawberry_django.field() + @strawberry.field + def saved_filter(self, id: int) -> SavedFilterType: + return models.SavedFilter.objects.get(id=id) saved_filter_list: List[SavedFilterType] = strawberry_django.field() - journal_entry: JournalEntryType = strawberry_django.field() + @strawberry.field + def journal_entry(self, id: int) -> JournalEntryType: + return models.JournalEntry.objects.get(id=id) journal_entry_list: List[JournalEntryType] = strawberry_django.field() - tag: TagType = strawberry_django.field() + @strawberry.field + def tag(self, id: int) -> TagType: + return models.Tag.objects.get(id=id) tag_list: List[TagType] = strawberry_django.field() - webhook: WebhookType = strawberry_django.field() + @strawberry.field + def webhook(self, id: int) -> WebhookType: + return models.Webhook.objects.get(id=id) webhook_list: List[WebhookType] = strawberry_django.field() - event_rule: EventRuleType = strawberry_django.field() + @strawberry.field + def event_rule(self, id: int) -> EventRuleType: + return models.EventRule.objects.get(id=id) event_rule_list: List[EventRuleType] = strawberry_django.field() diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index 230520f5d..29fff5a7b 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -8,50 +8,82 @@ from .types import * @strawberry.type class IPAMQuery: - asn: ASNType = strawberry_django.field() + @strawberry.field + def asn(self, id: int) -> ASNType: + return models.ASN.objects.get(id=id) asn_list: List[ASNType] = strawberry_django.field() - asn_range: ASNRangeType = strawberry_django.field() + @strawberry.field + def asn_range(self, id: int) -> ASNRangeType: + return models.ASNRange.objects.get(id=id) asn_range_list: List[ASNRangeType] = strawberry_django.field() - aggregate: AggregateType = strawberry_django.field() + @strawberry.field + def aggregate(self, id: int) -> AggregateType: + return models.Aggregate.objects.get(id=id) aggregate_list: List[AggregateType] = strawberry_django.field() - ip_address: IPAddressType = strawberry_django.field() + @strawberry.field + def ip_address(self, id: int) -> IPAddressType: + return models.IPAddress.objects.get(id=id) ip_address_list: List[IPAddressType] = strawberry_django.field() - ip_range: IPRangeType = strawberry_django.field() + @strawberry.field + def ip_range(self, id: int) -> IPRangeType: + return models.IPRange.objects.get(id=id) ip_range_list: List[IPRangeType] = strawberry_django.field() - prefix: PrefixType = strawberry_django.field() + @strawberry.field + def prefix(self, id: int) -> PrefixType: + return models.Prefix.objects.get(id=id) prefix_list: List[PrefixType] = strawberry_django.field() - rir: RIRType = strawberry_django.field() + @strawberry.field + def rir(self, id: int) -> RIRType: + return models.RIR.objects.get(id=id) rir_list: List[RIRType] = strawberry_django.field() - role: RoleType = strawberry_django.field() + @strawberry.field + def role(self, id: int) -> RoleType: + return models.Role.objects.get(id=id) role_list: List[RoleType] = strawberry_django.field() - route_target: RouteTargetType = strawberry_django.field() + @strawberry.field + def route_target(self, id: int) -> RouteTargetType: + return models.RouteTarget.objects.get(id=id) route_target_list: List[RouteTargetType] = strawberry_django.field() - service: ServiceType = strawberry_django.field() + @strawberry.field + def service(self, id: int) -> ServiceType: + return models.Service.objects.get(id=id) service_list: List[ServiceType] = strawberry_django.field() - service_template: ServiceTemplateType = strawberry_django.field() + @strawberry.field + def service_template(self, id: int) -> ServiceTemplateType: + return models.ServiceTemplate.objects.get(id=id) service_template_list: List[ServiceTemplateType] = strawberry_django.field() - fhrp_group: FHRPGroupType = strawberry_django.field() + @strawberry.field + def fhrp_group(self, id: int) -> FHRPGroupType: + return models.FHRPGroup.objects.get(id=id) fhrp_group_list: List[FHRPGroupType] = strawberry_django.field() - fhrp_group_assignment: FHRPGroupAssignmentType = strawberry_django.field() + @strawberry.field + def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType: + return models.FHRPGroupAssignment.objects.get(id=id) fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field() - vlan: VLANType = strawberry_django.field() + @strawberry.field + def vlan(self, id: int) -> VLANType: + return models.VLAN.objects.get(id=id) vlan_list: List[VLANType] = strawberry_django.field() - vlan_group: VLANGroupType = strawberry_django.field() + @strawberry.field + def vlan_group(self, id: int) -> VLANGroupType: + return models.VLANGroup.objects.get(id=id) vlan_group_list: List[VLANGroupType] = strawberry_django.field() - vrf: VRFType = strawberry_django.field() + @strawberry.field + def vrf(self, id: int) -> VRFType: + return models.VRF.objects.get(id=id) vrf_list: List[VRFType] = strawberry_django.field() diff --git a/netbox/tenancy/graphql/schema.py b/netbox/tenancy/graphql/schema.py index e82b3ad9c..c5dc63a7b 100644 --- a/netbox/tenancy/graphql/schema.py +++ b/netbox/tenancy/graphql/schema.py @@ -8,20 +8,32 @@ from .types import * @strawberry.type class TenancyQuery: - tenant: TenantType = strawberry_django.field() + @strawberry.field + def circutenantit(self, id: int) -> TenantType: + return models.Tenant.objects.get(id=id) tenant_list: List[TenantType] = strawberry_django.field() - tenant_group: TenantGroupType = strawberry_django.field() + @strawberry.field + def tenant_group(self, id: int) -> TenantGroupType: + return models.TenantGroup.objects.get(id=id) tenant_group_list: List[TenantGroupType] = strawberry_django.field() - contact: ContactType = strawberry_django.field() + @strawberry.field + def contact(self, id: int) -> ContactType: + return models.Contact.objects.get(id=id) contact_list: List[ContactType] = strawberry_django.field() - contact_role: ContactRoleType = strawberry_django.field() + @strawberry.field + def contact_role(self, id: int) -> ContactRoleType: + return models.ContactRole.objects.get(id=id) contact_role_list: List[ContactRoleType] = strawberry_django.field() - contact_group: ContactGroupType = strawberry_django.field() + @strawberry.field + def contact_group(self, id: int) -> ContactGroupType: + return models.ContactGroup.objects.get(id=id) contact_group_list: List[ContactGroupType] = strawberry_django.field() - contact_assignment: ContactAssignmentType = strawberry_django.field() + @strawberry.field + def contact_assignment(self, id: int) -> ContactAssignmentType: + return models.ContactAssignment.objects.get(id=id) contact_assignment_list: List[ContactAssignmentType] = strawberry_django.field() diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index 575bafecd..c9a8211d4 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -9,8 +9,12 @@ from .types import * @strawberry.type class UsersQuery: - group: GroupType = strawberry_django.field() + @strawberry.field + def group(self, id: int) -> GroupType: + return models.Group.objects.get(id=id) group_list: List[GroupType] = strawberry_django.field() - user: UserType = strawberry_django.field() + @strawberry.field + def user(self, id: int) -> UserType: + return models.User.objects.get(id=id) user_list: List[UserType] = strawberry_django.field() diff --git a/netbox/virtualization/graphql/schema.py b/netbox/virtualization/graphql/schema.py index d226e2fea..0d6e0b28b 100644 --- a/netbox/virtualization/graphql/schema.py +++ b/netbox/virtualization/graphql/schema.py @@ -8,20 +8,32 @@ from .types import * @strawberry.type class VirtualizationQuery: - cluster: ClusterType = strawberry_django.field() + @strawberry.field + def cluster(self, id: int) -> ClusterType: + return models.Cluster.objects.get(id=id) cluster_list: List[ClusterType] = strawberry_django.field() - cluster_group: ClusterGroupType = strawberry_django.field() + @strawberry.field + def cluster_group(self, id: int) -> ClusterGroupType: + return models.ClusterGroup.objects.get(id=id) cluster_group_list: List[ClusterGroupType] = strawberry_django.field() - cluster_type: ClusterTypeType = strawberry_django.field() + @strawberry.field + def cluster_type(self, id: int) -> ClusterTypeType: + return models.ClusterType.objects.get(id=id) cluster_type_list: List[ClusterTypeType] = strawberry_django.field() - virtual_machine: VirtualMachineType = strawberry_django.field() + @strawberry.field + def virtual_machine(self, id: int) -> VirtualMachineType: + return models.VirtualMachine.objects.get(id=id) virtual_machine_list: List[VirtualMachineType] = strawberry_django.field() - vm_interface: VMInterfaceType = strawberry_django.field() + @strawberry.field + def vm_interface(self, id: int) -> VMInterfaceType: + return models.VMInterface.objects.get(id=id) vm_interface_list: List[VMInterfaceType] = strawberry_django.field() - virtual_disk: VirtualDiskType = strawberry_django.field() + @strawberry.field + def virtual_disk(self, id: int) -> VirtualDiskType: + return models.VirtualDisk.objects.get(id=id) virtual_disk_list: List[VirtualDiskType] = strawberry_django.field() diff --git a/netbox/vpn/graphql/schema.py b/netbox/vpn/graphql/schema.py index 996e29af3..e10cbbbd5 100644 --- a/netbox/vpn/graphql/schema.py +++ b/netbox/vpn/graphql/schema.py @@ -8,32 +8,52 @@ from .types import * @strawberry.type class VPNQuery: - ike_policy: IKEPolicyType = strawberry_django.field() + @strawberry.field + def ike_policy(self, id: int) -> IKEPolicyType: + return models.IKEPolicy.objects.get(id=id) ike_policy_list: List[IKEPolicyType] = strawberry_django.field() - ike_proposal: IKEProposalType = strawberry_django.field() + @strawberry.field + def ike_proposal(self, id: int) -> IKEProposalType: + return models.IKEProposal.objects.get(id=id) ike_proposal_list: List[IKEProposalType] = strawberry_django.field() - ipsec_policy: IPSecPolicyType = strawberry_django.field() + @strawberry.field + def ipsec_policy(self, id: int) -> IPSecPolicyType: + return models.IPSecPolicy.objects.get(id=id) ipsec_policy_list: List[IPSecPolicyType] = strawberry_django.field() - ipsec_profile: IPSecProfileType = strawberry_django.field() + @strawberry.field + def ipsec_profile(self, id: int) -> IPSecProfileType: + return models.IPSecProfile.objects.get(id=id) ipsec_profile_list: List[IPSecProfileType] = strawberry_django.field() - ipsec_proposal: IPSecProposalType = strawberry_django.field() + @strawberry.field + def ipsec_proposal(self, id: int) -> IPSecProposalType: + return models.IPSecProposal.objects.get(id=id) ipsec_proposal_list: List[IPSecProposalType] = strawberry_django.field() - l2vpn: L2VPNType = strawberry_django.field() + @strawberry.field + def l2vpn(self, id: int) -> L2VPNType: + return models.L2VPN.objects.get(id=id) l2vpn_list: List[L2VPNType] = strawberry_django.field() - l2vpn_termination: L2VPNTerminationType = strawberry_django.field() + @strawberry.field + def l2vpn_termination(self, id: int) -> L2VPNTerminationType: + return models.L2VPNTermination.objects.get(id=id) l2vpn_termination_list: List[L2VPNTerminationType] = strawberry_django.field() - tunnel: TunnelType = strawberry_django.field() + @strawberry.field + def tunnel(self, id: int) -> TunnelType: + return models.Tunnel.objects.get(id=id) tunnel_list: List[TunnelType] = strawberry_django.field() - tunnel_group: TunnelGroupType = strawberry_django.field() + @strawberry.field + def tunnel_group(self, id: int) -> TunnelGroupType: + return models.TunnelGroup.objects.get(id=id) tunnel_group_list: List[TunnelGroupType] = strawberry_django.field() - tunnel_termination: TunnelTerminationType = strawberry_django.field() + @strawberry.field + def tunnel_termination(self, id: int) -> TunnelTerminationType: + return models.TunnelTermination.objects.get(id=id) tunnel_termination_list: List[TunnelTerminationType] = strawberry_django.field() diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index bd16cba3d..0447ad85c 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -8,11 +8,17 @@ from .types import * @strawberry.type class WirelessQuery: - wireless_lan: WirelessLANType = strawberry_django.field() + @strawberry.field + def wireless_lan(self, id: int) -> WirelessLANType: + return models.WirelessLAN.objects.get(id=id) wireless_lan_list: List[WirelessLANType] = strawberry_django.field() - wireless_lan_group: WirelessLANGroupType = strawberry_django.field() + @strawberry.field + def wireless_lan_group(self, id: int) -> WirelessLANGroupType: + return models.WirelessLANGroup.objects.get(id=id) wireless_lan_group_list: List[WirelessLANGroupType] = strawberry_django.field() - wireless_link: WirelessLinkType = strawberry_django.field() + @strawberry.field + def wireless_link(self, id: int) -> WirelessLinkType: + return models.WirelessLink.objects.get(id=id) wireless_link_list: List[WirelessLinkType] = strawberry_django.field() From aa7c00ec325e66520910a94b2f5a6ec659f9afcb Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 10:21:23 -0800 Subject: [PATCH 042/156] 9856 circuits graphql tests working --- netbox/utilities/testing/api.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 61899aebf..0625da5fc 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -21,6 +21,7 @@ from .utils import disable_warnings from ipam.graphql.types import IPAddressFamilyType from strawberry.lazy_type import LazyType from strawberry.type import StrawberryList, StrawberryOptional +from strawberry.union import StrawberryUnion __all__ = ( 'APITestCase', @@ -450,14 +451,18 @@ class APIViewTestCases: fields_string = '' for field in type_class.__strawberry_definition__.fields: - # for field_name, field in type_class._meta.fields.items(): - # print(f"field_name: {field.name} type: {field.type}") + """ + print(f"field_name: {field.name} type: {field.type}") - if field.name == 'site': - # breakpoint() + if field.name == 'provider': + breakpoint() pass + """ if type(field.type) is StrawberryList: + if type(field.type.of_type) is StrawberryUnion: + # this would require a fragment query + continue fields_string += f'{field.name} {{ id }}\n' elif field.type is strawberry_django.fields.types.DjangoModelType: # Dynamic fields must specify a subselection @@ -467,6 +472,8 @@ class APIViewTestCases: fields_string += f'{field.name} {{ id }}\n' elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: fields_string += f'{field.name} {{ pk }}\n' + elif field.is_relation: + fields_string += f'{field.name} {{ id }}\n' # TODO: Improve field detection logic to avoid nested ArrayFields elif field.name == 'extra_choices': continue @@ -487,15 +494,6 @@ class APIViewTestCases: }} """ - if "_list" not in name: - query = f""" - {{ - {name}_list {{ - {fields_string} - }} - }} - """ - return query @override_settings(LOGIN_REQUIRED=True) From a5aad5359dfac16fd5dffc9e6f8dca1c47ce8404 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 10:35:44 -0800 Subject: [PATCH 043/156] 9856 test fixes --- netbox/dcim/graphql/schema.py | 2 +- netbox/dcim/graphql/types.py | 5 ++--- netbox/tenancy/graphql/schema.py | 2 +- netbox/users/graphql/schema.py | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index 9dc3388b7..4a895c3a7 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -2,7 +2,7 @@ from typing import List import strawberry import strawberry_django -from circuits import models +from dcim import models from .types import * diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 06d3ec425..97b4ab2f6 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -853,8 +853,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): ) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str - asn: BigInt - time_zone: str + time_zone: str | None @strawberry_django.field def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]: @@ -870,7 +869,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje @strawberry_django.field def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: - return self.cabletermiantion_set.all() + return self.cabletermination_set.all() @strawberry_django.field def powerpanel_set(self) -> List[Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]]: diff --git a/netbox/tenancy/graphql/schema.py b/netbox/tenancy/graphql/schema.py index c5dc63a7b..80e77cc82 100644 --- a/netbox/tenancy/graphql/schema.py +++ b/netbox/tenancy/graphql/schema.py @@ -2,7 +2,7 @@ from typing import List import strawberry import strawberry_django -from circuits import models +from tenancy import models from .types import * diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index c9a8211d4..66a9e8c93 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -4,6 +4,7 @@ import strawberry_django from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from users import models from .types import * From 888d9ecec65e8d955fecc1ba47b6528eece56b23 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 13:27:01 -0800 Subject: [PATCH 044/156] 9856 test fixes --- netbox/dcim/graphql/types.py | 149 +++++++++++++++++++------------- netbox/netbox/graphql/types.py | 8 +- netbox/utilities/testing/api.py | 13 +-- 3 files changed, 100 insertions(+), 70 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 97b4ab2f6..03b6a96b5 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -132,21 +132,40 @@ class CableType(NetBoxObjectType): @strawberry_django.field def terminations(self) -> List[CableTerminationType]: - return self.terminations + return self.terminations.all() @strawberry_django.field - def a_terminations(self) -> List[CableTerminationType]: + def a_terminations(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("CableTerminationTerminationType")]]: return self.a_terminations @strawberry_django.field - def b_terminations(self) -> List[CableTerminationType]: + def b_terminations(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("CableTerminationTerminationType")]]: return self.b_terminations @strawberry_django.type( models.ConsolePort, - # exclude=('_path',), - exclude=('_path',), # bug - temp + exclude=('_path',), filters=ConsolePortFilter ) class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -164,8 +183,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.ConsoleServerPort, - # exclude=('_path',), - exclude=('_path',), # bug - temp + exclude=('_path',), filters=ConsoleServerPortFilter ) class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): @@ -200,12 +218,8 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo inventory_item_count: BigInt @strawberry_django.field - def devicebays(self) -> List[Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() - - @strawberry_django.field - def vc_master_for(self) -> Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')]: - return self.vc_master_for + def vc_master_for(self) -> Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')] | None: + return self.vc_master_for if hasattr(self, 'vc_master_for') else None @strawberry_django.field def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: @@ -216,12 +230,12 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo return self.modules.all() @strawberry_django.field - def parent_bay(self) -> Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]: - return self.parent_bay + def parent_bay(self) -> Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent_bay if hasattr(self, 'parent_bay') else None @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: - return self.interaces.all() + return self.interfaces.all() @strawberry_django.field def rearports(self) -> List[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]: @@ -295,7 +309,7 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): _name: str @strawberry_django.field - def parent(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: + def parent(self) -> Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @strawberry_django.field @@ -317,8 +331,7 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.DeviceRole, - # fields='__all__', - exclude=('color',), # bug - temp + fields='__all__', filters=DeviceRoleFilter ) class DeviceRoleType(OrganizationalObjectType): @@ -349,50 +362,52 @@ class DeviceTypeType(NetBoxObjectType): device_bay_template_count: BigInt module_bay_template_count: BigInt inventory_item_template_count: BigInt + front_image: strawberry_django.fields.types.DjangoImageType | None + rear_image: strawberry_django.fields.types.DjangoImageType | None @strawberry_django.field def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.frontporttemplates.all() @strawberry_django.field def modulebaytemplates(self) -> List[Annotated["ModuleBayTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.modulebaytemplates.all() @strawberry_django.field def instances(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.instances.all() @strawberry_django.field def poweroutlettemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.poweroutlettemplates.all() @strawberry_django.field def powerporttemplates(self) -> List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.powerporttemplates.all() @strawberry_django.field def inventoryitemtemplates(self) -> List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.inventoryitemtemplates.all() @strawberry_django.field def rearporttemplates(self) -> List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.rearporttemplates.all() @strawberry_django.field def consoleserverporttemplates(self) -> List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.consoleserverporttemplates.all() @strawberry_django.field def interfacetemplates(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.interfacetemplates.all() @strawberry_django.field def devicebaytemplates(self) -> List[Annotated["DeviceBayTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.devicebaytemplates.all() @strawberry_django.field def consoleporttemplates(self) -> List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.device_bays.all() + return self.consoleporttemplates.all() @strawberry_django.type( @@ -416,13 +431,12 @@ class FrontPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Interface, - # fields='__all__', - exclude=('mac_address', 'wwn'), # bug - temp + fields='__all__', filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): - mac_address: str - wwn: str + mac_address: str | None + wwn: str | None @strawberry_django.field def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: @@ -475,7 +489,7 @@ class InventoryItemType(ComponentObjectType): _name: str @strawberry_django.field - def parent(self) -> List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]: + def parent(self) -> Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @strawberry_django.field @@ -540,6 +554,14 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: return self.devices.all() + @strawberry_django.field + def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent + + @strawberry_django.field + def children(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + @strawberry_django.type( models.Manufacturer, @@ -613,8 +635,8 @@ class ModuleType(ComponentObjectType): class ModuleBayType(ComponentObjectType): @strawberry_django.field - def installed_module(self) -> Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]: - return self.installed_module + def installed_module(self) -> Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None: + return self.installed_module if hasattr(self, 'installed_module') else None @strawberry_django.type( @@ -635,35 +657,35 @@ class ModuleTypeType(NetBoxObjectType): @strawberry_django.field def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.frontporttemplates.all() @strawberry_django.field def consoleserverporttemplates(self) -> List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.consoleserverporttemplates.all() @strawberry_django.field def interfacetemplates(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.interfacetemplates.all() @strawberry_django.field def powerporttemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.powerporttemplates.all() @strawberry_django.field def poweroutlettemplates(self) -> List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.poweroutlettemplates.all() @strawberry_django.field def rearporttemplates(self) -> List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.rearporttemplates.all() @strawberry_django.field def instances(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.instances.all() @strawberry_django.field def consoleporttemplates(self) -> List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]: - return self.interfaces.all() + return self.consoleporttemplates.all() @strawberry_django.type( @@ -763,8 +785,8 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje return self.devices.all() @strawberry_django.field - def powerfeed_set(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: - return self.powerfeed_set.all() + def powerfeeds(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: + return self.powerfeeds.all() @strawberry_django.field def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: @@ -777,8 +799,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje @strawberry_django.type( models.RackReservation, - # fields='__all__', - exclude=('units',), # bug - temp + fields='__all__', filters=RackReservationFilter ) class RackReservationType(NetBoxObjectType): @@ -787,8 +808,7 @@ class RackReservationType(NetBoxObjectType): @strawberry_django.type( models.RackRole, - # fields='__all__', - exclude=('color',), # bug - temp + fields='__all__', filters=RackRoleFilter ) class RackRoleType(OrganizationalObjectType): @@ -801,8 +821,7 @@ class RackRoleType(OrganizationalObjectType): @strawberry_django.type( models.RearPort, - # fields='__all__', - exclude=('color', ), # bug - temp + fields='__all__', filters=RearPortFilter ) class RearPortType(ComponentObjectType, CabledObjectMixin): @@ -815,8 +834,7 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): @strawberry_django.type( models.RearPortTemplate, - # fields='__all__', - exclude=('color', ), # bug - temp + fields='__all__', filters=RearPortTemplateFilter ) class RearPortTemplateType(ComponentTemplateObjectType): @@ -830,8 +848,8 @@ class RearPortTemplateType(ComponentTemplateObjectType): @strawberry_django.type( models.Region, + exclude=('parent',), # fields='__all__', - exclude=('parent',), # bug - temp filters=RegionFilter ) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @@ -844,11 +862,18 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: return self.vlan_groups.all() + @strawberry_django.field + def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent + + @strawberry_django.field + def children(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + @strawberry_django.type( models.Site, - # fields='__all__', - exclude=('time_zone',), # bug - temp + fields='__all__', filters=SiteFilter ) class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): @@ -920,6 +945,14 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: return self.vlan_groups.all() + @strawberry_django.field + def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None: + return self.parent + + @strawberry_django.field + def children(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]: + return self.children.all() + @strawberry_django.type( models.VirtualChassis, diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 7d7fea992..f084c8140 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -66,13 +66,7 @@ class OrganizationalObjectType( """ Base type for organizational models """ - @strawberry_django.field - def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: - return self.parent - - @strawberry_django.field - def children(self) -> List[Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')]]: - return self.children.all() + pass class NetBoxObjectType( diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 0625da5fc..13564ad19 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -454,16 +454,19 @@ class APIViewTestCases: """ print(f"field_name: {field.name} type: {field.type}") - if field.name == 'provider': + if field.name == 'front_image': breakpoint() pass """ - if type(field.type) is StrawberryList: - if type(field.type.of_type) is StrawberryUnion: - # this would require a fragment query - continue + if field.type in (strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType): + fields_string += f'{field.name} {{ name }}\n' + elif type(field.type) is StrawberryList and type(field.type.of_type) is LazyType: + # List of related objects (queryset) fields_string += f'{field.name} {{ id }}\n' + elif type(field.type) is StrawberryList and type(field.type.of_type) is StrawberryUnion: + # this would require a fragment query + continue elif field.type is strawberry_django.fields.types.DjangoModelType: # Dynamic fields must specify a subselection fields_string += f'{field.name} {{ pk }}\n' From 0ca46e349f5df47af7f1f509998deb56162afbb7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 13:43:40 -0800 Subject: [PATCH 045/156] 9856 test fixes --- netbox/dcim/graphql/types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 03b6a96b5..7c10d3747 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -486,8 +486,6 @@ class InterfaceTemplateType(ComponentTemplateObjectType): filters=InventoryItemFilter ) class InventoryItemType(ComponentObjectType): - _name: str - @strawberry_django.field def parent(self) -> Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -596,7 +594,7 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin): fields='__all__', filters=ModuleFilter ) -class ModuleType(ComponentObjectType): +class ModuleType(NetBoxObjectType): @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: From 1052ea5dd4260c1b6c918f15ffae39e49c0778c6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 14:00:53 -0800 Subject: [PATCH 046/156] 9856 test fix vpn --- netbox/utilities/testing/api.py | 5 ++++- netbox/vpn/graphql/types.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 13564ad19..d07b54a36 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -454,7 +454,7 @@ class APIViewTestCases: """ print(f"field_name: {field.name} type: {field.type}") - if field.name == 'front_image': + if field.name == 'assigned_object': breakpoint() pass """ @@ -467,6 +467,9 @@ class APIViewTestCases: elif type(field.type) is StrawberryList and type(field.type.of_type) is StrawberryUnion: # this would require a fragment query continue + elif type(field.type) is StrawberryUnion: + # this would require a fragment query + continue elif field.type is strawberry_django.fields.types.DjangoModelType: # Dynamic fields must specify a subselection fields_string += f'{field.name} {{ pk }}\n' diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index fbd9683e7..93024d126 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -103,11 +103,11 @@ class IPSecProposalType(OrganizationalObjectType): class IPSecPolicyType(OrganizationalObjectType): @strawberry_django.field - def proposals(self) -> List[Annotated["IKEProposalType", strawberry.lazy('vpn.graphql.types')]]: + def proposals(self) -> List[Annotated["IPSecProposalType", strawberry.lazy('vpn.graphql.types')]]: return self.proposals.all() @strawberry_django.field - def ipsec_profiles(self) -> List[Annotated["IPSecProposalType", strawberry.lazy('vpn.graphql.types')]]: + def ipsec_profiles(self) -> List[Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')]]: return self.ipsec_profiles.all() From 5ff2c1806dc5ba9e33d33cb55b6f735374172da7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 14:34:51 -0800 Subject: [PATCH 047/156] 9856 test fixes --- netbox/ipam/graphql/types.py | 6 +++--- netbox/tenancy/graphql/schema.py | 2 +- netbox/tenancy/graphql/types.py | 2 +- netbox/utilities/testing/api.py | 11 +++-------- netbox/virtualization/graphql/types.py | 4 ---- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 283dd3e30..bae8ec686 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -51,7 +51,7 @@ class BaseIPAddressFamilyType: def family(self) -> IPAddressFamilyType: # Note that self, is an instance of models.IPAddress # thus resolves to the address family value. - return IPAddressFamilyType(value=self.value, label=f'IPv{self.value}') + return IPAddressFamilyType(value=self.family, label=f'IPv{self.family}') @strawberry_django.type( @@ -127,8 +127,8 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): address: str @strawberry_django.field - def nat_outside(self) -> Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]: - return self.nat_outside + def nat_outside(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: + return self.nat_outside.all() @strawberry_django.field def tunnel_terminations(self) -> List[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]: diff --git a/netbox/tenancy/graphql/schema.py b/netbox/tenancy/graphql/schema.py index 80e77cc82..0fd541c5d 100644 --- a/netbox/tenancy/graphql/schema.py +++ b/netbox/tenancy/graphql/schema.py @@ -9,7 +9,7 @@ from .types import * @strawberry.type class TenancyQuery: @strawberry.field - def circutenantit(self, id: int) -> TenantType: + def tenant(self, id: int) -> TenantType: return models.Tenant.objects.get(id=id) tenant_list: List[TenantType] = strawberry_django.field() diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 77c7cfea8..f64015f33 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -184,7 +184,7 @@ class ContactGroupType(OrganizationalObjectType): @strawberry_django.field def contacts(self) -> List[ContactType]: - return self.clusters.all() + return self.contacts.all() @strawberry_django.type( diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index d07b54a36..15c96d568 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -19,6 +19,7 @@ from .base import ModelTestCase from .utils import disable_warnings from ipam.graphql.types import IPAddressFamilyType +from strawberry.field import StrawberryField from strawberry.lazy_type import LazyType from strawberry.type import StrawberryList, StrawberryOptional from strawberry.union import StrawberryUnion @@ -478,15 +479,9 @@ class APIViewTestCases: fields_string += f'{field.name} {{ id }}\n' elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: fields_string += f'{field.name} {{ pk }}\n' - elif field.is_relation: + elif hasattr(field, 'is_relation') and field.is_relation: + # Note: StrawberryField types do not have is_relation fields_string += f'{field.name} {{ id }}\n' - # TODO: Improve field detection logic to avoid nested ArrayFields - elif field.name == 'extra_choices': - continue - # elif type(field.type) is GQLList and not is_string_array: - # # TODO: Come up with something more elegant - # # Temporary hack to support automated testing of reverse generic relations - # fields_string += f'{field_name} {{ id }}\n' elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType): fields_string += f'{field.name} {{ value, label }}\n' else: diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index a263a8d9d..86a7d4b2c 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -109,10 +109,6 @@ class VMInterfaceType(IPAddressesMixin, ComponentObjectType): def tagged_vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: return self.tagged_vlans.all() - @strawberry_django.field - def mac_address(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: - return self.mac_address.all() - @strawberry_django.field def bridge_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: return self.bridge_interfaces.all() From 77eb1018e83a51f30b852f5e7150367431bda2b3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 15:00:48 -0800 Subject: [PATCH 048/156] 9856 test fixes --- netbox/extras/graphql/types.py | 38 +++++++++++++------------- netbox/virtualization/graphql/types.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index f148a250b..e78ba84ca 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,55 +39,55 @@ class ConfigContextType(ObjectType): @strawberry_django.field def roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.roles.all() @strawberry_django.field def device_types(self) -> List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.device_types.all() @strawberry_django.field def tags(self) -> List[Annotated["TagType", strawberry.lazy('extras.graphql.types')]]: - return self.vlan_groups.all() + return self.tags.all() @strawberry_django.field def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.platforms.all() @strawberry_django.field def regions(self) -> List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.regions.all() @strawberry_django.field def cluster_groups(self) -> List[Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')]]: - return self.vlan_groups.all() + return self.cluster_groups.all() @strawberry_django.field def tenant_groups(self) -> List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]]: - return self.vlan_groups.all() + return self.tenant_groups.all() @strawberry_django.field def cluster_types(self) -> List[Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')]]: - return self.vlan_groups.all() + return self.cluster_types.all() @strawberry_django.field def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: - return self.vlan_groups.all() + return self.clusters.all() @strawberry_django.field def locations(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.locations.all() @strawberry_django.field def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.sites.all() @strawberry_django.field def tenants(self) -> List[Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')]]: - return self.vlan_groups.all() + return self.tenants.all() @strawberry_django.field def site_groups(self) -> List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.site_groups.all() @strawberry_django.type( @@ -99,19 +99,19 @@ class ConfigTemplateType(TagsMixin, ObjectType): @strawberry_django.field def virtualmachines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: - return self.vlan_groups.all() + return self.virtualmachines.all() @strawberry_django.field def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.devices.all() @strawberry_django.field def platforms(self) -> List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.platforms.all() @strawberry_django.field def device_roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]: - return self.vlan_groups.all() + return self.device_roles.all() @strawberry_django.type( @@ -133,11 +133,11 @@ class CustomFieldChoiceSetType(ObjectType): @strawberry_django.field def choices_for(self) -> List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]: - return self.assignments.all() + return self.choices_for.all() @strawberry_django.field def extra_choices(self) -> List[str]: - return self.assignments.all() + return self.extra_choices @strawberry_django.type( diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 86a7d4b2c..6bfe4016f 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -99,7 +99,7 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): ) class VMInterfaceType(IPAddressesMixin, ComponentObjectType): _name: str - mac_address: str + mac_address: str | None @strawberry_django.field def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: From 7c66a6aba8457458bfbd781cb3fbec43e3d88c04 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 15:25:06 -0800 Subject: [PATCH 049/156] 9856 test fixes --- netbox/netbox/tests/dummy_plugin/graphql.py | 33 +++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py index 27ecd9ce0..bc6643ba9 100644 --- a/netbox/netbox/tests/dummy_plugin/graphql.py +++ b/netbox/netbox/tests/dummy_plugin/graphql.py @@ -1,21 +1,28 @@ -import graphene -from graphene_django import DjangoObjectType - -from netbox.graphql.fields import ObjectField, ObjectListField +from typing import List +import strawberry +import strawberry_django +from strawberry.schema.config import StrawberryConfig from . import models -class DummyModelType(DjangoObjectType): - - class Meta: - model = models.DummyModel - fields = '__all__' +@strawberry_django.type( + models.DummyModel, + fields='__all__', +) +class DummyModelType: + pass -class DummyQuery(graphene.ObjectType): - dummymodel = ObjectField(DummyModelType) - dummymodel_list = ObjectListField(DummyModelType) +@strawberry.type +class DummyQuery: + @strawberry.field + def dummymodel(self, id: int) -> DummyModelType: + return None + dummymodel_list: List[DummyModelType] = strawberry_django.field() -schema = DummyQuery +schema = strawberry.Schema( + query=DummyQuery, + config=StrawberryConfig(auto_camel_case=False), +) From 28ac66b0fb3cfd04a2d4cc417313dfe34cc8dfa4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 15:44:40 -0800 Subject: [PATCH 050/156] 9856 circuits test sans DjangoModelType --- netbox/circuits/graphql/types.py | 6 +++++- netbox/dcim/graphql/mixins.py | 6 +----- netbox/netbox/tests/dummy_plugin/graphql.py | 10 +++++----- netbox/utilities/testing/api.py | 12 ++++++++++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 420e13772..6a96a54b9 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -51,6 +51,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin): filters=ProviderAccountFilter ) class ProviderAccountType(NetBoxObjectType): + provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] @strawberry_django.field def circuits(self) -> List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]: @@ -63,6 +64,7 @@ class ProviderAccountType(NetBoxObjectType): filters=ProviderNetworkFilter ) class ProviderNetworkType(NetBoxObjectType): + provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] @strawberry_django.field def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]: @@ -75,7 +77,9 @@ class ProviderNetworkType(NetBoxObjectType): filters=CircuitTerminationFilter ) class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): - pass + 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( diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 8e79b4c0b..19fd62b14 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -11,11 +11,7 @@ __all__ = ( @strawberry.type class CabledObjectMixin: - - # @strawberry_django.field - # def cable_end(self) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: - # # Handle empty values - # return self.cable_end or None + cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def link_peers(self) -> List[Annotated[Union[ diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py index bc6643ba9..640c12959 100644 --- a/netbox/netbox/tests/dummy_plugin/graphql.py +++ b/netbox/netbox/tests/dummy_plugin/graphql.py @@ -21,8 +21,8 @@ class DummyQuery: return None dummymodel_list: List[DummyModelType] = strawberry_django.field() - -schema = strawberry.Schema( - query=DummyQuery, - config=StrawberryConfig(auto_camel_case=False), -) +# bug - temp - FIXME! +# schema = strawberry.Schema( +# query=DummyQuery, +# config=StrawberryConfig(auto_camel_case=False), +# ) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 15c96d568..9be4cfd5e 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -472,13 +472,21 @@ class APIViewTestCases: # this would require a fragment query continue elif field.type is strawberry_django.fields.types.DjangoModelType: + print("") + print("DjangoModelType") + print(f"{self.model} -> {field.name}") + print("") # Dynamic fields must specify a subselection - fields_string += f'{field.name} {{ pk }}\n' + fields_string += f'{field.name} {{ id }}\n' elif type(field.type) is StrawberryOptional: if type(field.type.of_type) is LazyType: fields_string += f'{field.name} {{ id }}\n' elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: - fields_string += f'{field.name} {{ pk }}\n' + print("") + print("DjangoModelType") + print(f"{self.model} -> {field.name}") + print("") + fields_string += f'{field.name} {{ id }}\n' elif hasattr(field, 'is_relation') and field.is_relation: # Note: StrawberryField types do not have is_relation fields_string += f'{field.name} {{ id }}\n' From 47848294773c80a325d60ccc72200a77248a61eb Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 6 Mar 2024 15:48:02 -0800 Subject: [PATCH 051/156] 9856 core test sans DjangoModelType --- netbox/core/graphql/types.py | 2 +- netbox/utilities/testing/api.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index 636c322e4..aeabf5310 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -20,7 +20,7 @@ __all__ = ( filters=DataFileFilter ) class DataFileType(BaseObjectType): - pass + source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] @strawberry_django.type( diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 9be4cfd5e..59ebc812e 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -472,8 +472,10 @@ class APIViewTestCases: # this would require a fragment query continue elif field.type is strawberry_django.fields.types.DjangoModelType: + print("") print("") print("DjangoModelType") + print("--------------------------") print(f"{self.model} -> {field.name}") print("") # Dynamic fields must specify a subselection @@ -482,8 +484,10 @@ class APIViewTestCases: if type(field.type.of_type) is LazyType: fields_string += f'{field.name} {{ id }}\n' elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: + print("") print("") print("DjangoModelType") + print("--------------------------") print(f"{self.model} -> {field.name}") print("") fields_string += f'{field.name} {{ id }}\n' From 005a33974526641c7e719bccbb8c6d235b54dc63 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 07:39:15 -0800 Subject: [PATCH 052/156] 9856 temp checkin --- netbox/dcim/graphql/types.py | 1 + netbox/utilities/testing/api.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 7c10d3747..43c7da413 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -129,6 +129,7 @@ class CableTerminationType(NetBoxObjectType): ) class CableType(NetBoxObjectType): color: str + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def terminations(self) -> List[CableTerminationType]: diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 59ebc812e..7445a0edd 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -472,25 +472,21 @@ class APIViewTestCases: # this would require a fragment query continue elif field.type is strawberry_django.fields.types.DjangoModelType: - print("") - print("") print("DjangoModelType") print("--------------------------") print(f"{self.model} -> {field.name}") print("") # Dynamic fields must specify a subselection - fields_string += f'{field.name} {{ id }}\n' + fields_string += f'{field.name} {{ pk }}\n' elif type(field.type) is StrawberryOptional: if type(field.type.of_type) is LazyType: fields_string += f'{field.name} {{ id }}\n' elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: - print("") - print("") print("DjangoModelType") print("--------------------------") print(f"{self.model} -> {field.name}") print("") - fields_string += f'{field.name} {{ id }}\n' + fields_string += f'{field.name} {{ pk }}\n' elif hasattr(field, 'is_relation') and field.is_relation: # Note: StrawberryField types do not have is_relation fields_string += f'{field.name} {{ id }}\n' From 12cca5d0a025497fa9199f822aae9903bff67940 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 08:58:56 -0800 Subject: [PATCH 053/156] 9856 fix extas FK --- netbox/extras/graphql/types.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index e78ba84ca..f8c5d85fa 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -35,7 +35,8 @@ __all__ = ( filters=ConfigContextFilter ) class ConfigContextType(ObjectType): - pass + data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None + data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None @strawberry_django.field def roles(self) -> List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]: @@ -96,6 +97,8 @@ class ConfigContextType(ObjectType): 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 @strawberry_django.field def virtualmachines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: @@ -120,7 +123,8 @@ class ConfigTemplateType(TagsMixin, ObjectType): filters=CustomFieldFilter ) class CustomFieldType(ObjectType): - pass + object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None @strawberry_django.type( @@ -155,7 +159,8 @@ class CustomLinkType(ObjectType): filters=ExportTemplateFilter ) class ExportTemplateType(ObjectType): - pass + data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None + data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None @strawberry_django.type( @@ -164,7 +169,7 @@ class ExportTemplateType(ObjectType): filters=ImageAttachmentFilter ) class ImageAttachmentType(BaseObjectType): - pass + content_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None @strawberry_django.type( @@ -173,7 +178,8 @@ class ImageAttachmentType(BaseObjectType): filters=JournalEntryFilter ) class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType): - pass + assigned_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None @strawberry_django.type( @@ -191,7 +197,7 @@ class ObjectChangeType(BaseObjectType): filters=SavedFilterFilter ) class SavedFilterType(ObjectType): - pass + user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None @strawberry_django.type( @@ -222,4 +228,4 @@ class WebhookType(OrganizationalObjectType): filters=EventRuleFilter ) class EventRuleType(OrganizationalObjectType): - pass + action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None From 4b56f0b000a8a1c97ee61a37b74d5b1490f0b3f7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 09:07:31 -0800 Subject: [PATCH 054/156] 9856 fix tenancy FK --- netbox/tenancy/graphql/types.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index f64015f33..39f99bda8 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -35,6 +35,7 @@ class ContactAssignmentsMixin: filters=TenantFilter ) class TenantType(NetBoxObjectType): + group: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def asns(self) -> List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]: @@ -156,6 +157,7 @@ class TenantGroupType(OrganizationalObjectType): filters=ContactFilter ) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): + group: Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: @@ -193,4 +195,6 @@ class ContactGroupType(OrganizationalObjectType): filters=ContactAssignmentFilter ) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): - pass + content_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + contact: Annotated["ContactType", strawberry.lazy('tenancy.graphql.types')] | None + role: Annotated["ContactRoleType", strawberry.lazy('tenancy.graphql.types')] | None From 26c8ee36fa4b6d5ae6078eb06a1f39778d7b4fb4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 09:42:52 -0800 Subject: [PATCH 055/156] 9856 fix virtualization FK --- netbox/virtualization/graphql/types.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 6bfe4016f..832facf85 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -27,6 +27,10 @@ __all__ = ( filters=ClusterFilter ) class ClusterType(VLANGroupsMixin, NetBoxObjectType): + type: Annotated["ClusterTypeType", strawberry.lazy('virtualization.graphql.types')] | None + group: Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: @@ -78,6 +82,15 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): _name: str interface_count: BigInt virtual_disk_count: BigInt + config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None + cluster: Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')] | None + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + platform: Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')] | None + role: Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')] | None + primary_ip4: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + primary_ip6: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: @@ -100,6 +113,11 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): class VMInterfaceType(IPAddressesMixin, ComponentObjectType): _name: str mac_address: str | None + parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None + bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None + virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] + untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None + vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @strawberry_django.field def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: @@ -125,3 +143,4 @@ class VMInterfaceType(IPAddressesMixin, ComponentObjectType): ) class VirtualDiskType(ComponentObjectType): _name: str + virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] From 38c7d76646a63a1be7656ad3aa527e9572125d44 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 09:53:11 -0800 Subject: [PATCH 056/156] 9856 fix vpn FK --- netbox/vpn/graphql/types.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 93024d126..ca65deec0 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -40,7 +40,9 @@ class TunnelGroupType(OrganizationalObjectType): filters=TunnelTerminationFilter ) class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): - pass + tunnel: Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')] + termination_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + outside_ip: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None @strawberry_django.type( @@ -49,6 +51,9 @@ class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): filters=TunnelFilter ) class TunnelType(NetBoxObjectType): + group: Annotated["TunnelGroupType", strawberry.lazy('vpn.graphql.types')] | None + ipsec_profile: Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def terminations(self) -> List[Annotated["TunnelTerminationType", strawberry.lazy('vpn.graphql.types')]]: @@ -117,6 +122,8 @@ class IPSecPolicyType(OrganizationalObjectType): filters=IPSecProfileFilter ) class IPSecProfileType(OrganizationalObjectType): + ike_policy: Annotated["IKEPolicyType", strawberry.lazy('vpn.graphql.types')] + ipsec_policy: Annotated["IPSecPolicyType", strawberry.lazy('vpn.graphql.types')] @strawberry_django.field def tunnels(self) -> List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]]: @@ -129,6 +136,7 @@ class IPSecProfileType(OrganizationalObjectType): filters=L2VPNFilter ) class L2VPNType(ContactsMixin, NetBoxObjectType): + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def export_targets(self) -> List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]]: @@ -149,6 +157,7 @@ class L2VPNType(ContactsMixin, NetBoxObjectType): filters=L2VPNTerminationFilter ) class L2VPNTerminationType(NetBoxObjectType): + l2vpn: Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')] @strawberry_django.field def assigned_object(self) -> Annotated[Union[ From 5f56e2daffa1d4600a0a4e688d787d54103a340c Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 09:58:26 -0800 Subject: [PATCH 057/156] 9856 fix wireless FK --- netbox/wireless/graphql/types.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index 12e9c57ae..babffba83 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -33,6 +33,9 @@ class WirelessLANGroupType(OrganizationalObjectType): filters=WirelessLANFilter ) class WirelessLANType(NetBoxObjectType): + group: Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')] | None + vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: @@ -45,4 +48,8 @@ class WirelessLANType(NetBoxObjectType): filters=WirelessLinkFilter ) class WirelessLinkType(NetBoxObjectType): - pass + interface_a: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] + interface_b: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + _interface_a_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None + _interface_b_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None From e53475a63f7766be707b55fbebc8424c8fb0e071 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 10:11:28 -0800 Subject: [PATCH 058/156] 9856 fix ipam FK --- netbox/ipam/graphql/types.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index bae8ec686..8e7e63303 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -61,6 +61,8 @@ class BaseIPAddressFamilyType: ) class ASNType(NetBoxObjectType): asn: BigInt + rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def sites(self) -> List[SiteType]: @@ -79,6 +81,8 @@ class ASNType(NetBoxObjectType): class ASNRangeType(NetBoxObjectType): 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( @@ -88,6 +92,8 @@ class ASNRangeType(NetBoxObjectType): ) class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): prefix: str + rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.type( @@ -108,7 +114,7 @@ class FHRPGroupType(NetBoxObjectType): filters=FHRPGroupAssignmentFilter ) class FHRPGroupAssignmentType(BaseObjectType): - # interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType') + group: Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')] @strawberry_django.field def interface(self) -> Annotated[Union[ @@ -125,6 +131,9 @@ class FHRPGroupAssignmentType(BaseObjectType): ) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): 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 @strawberry_django.field def nat_outside(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: @@ -156,6 +165,9 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): class IPRangeType(NetBoxObjectType): 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( @@ -166,6 +178,11 @@ class IPRangeType(NetBoxObjectType): ) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): 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( @@ -214,6 +231,7 @@ class RoleType(OrganizationalObjectType): filters=RouteTargetFilter ) class RouteTargetType(NetBoxObjectType): + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def exporting_l2vpns(self) -> List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]: @@ -239,6 +257,8 @@ class RouteTargetType(NetBoxObjectType): ) 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 @strawberry_django.field def ipaddresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: @@ -260,6 +280,10 @@ class ServiceTemplateType(NetBoxObjectType): 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 @strawberry_django.field def interfaces_as_untagged(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: @@ -316,6 +340,7 @@ class VLANGroupType(OrganizationalObjectType): filters=VRFFilter ) class VRFType(NetBoxObjectType): + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: From 676764a6617eac7ad51137c74d111150ccf47b50 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 10:48:50 -0800 Subject: [PATCH 059/156] 9856 fix partial dcim FK --- netbox/dcim/graphql/types.py | 67 +++++++++++++++++--------- netbox/ipam/graphql/types.py | 6 +-- netbox/virtualization/graphql/types.py | 8 ++- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 43c7da413..e08e39ae6 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -25,7 +25,7 @@ from .mixins import CabledObjectMixin, PathEndpointMixin __all__ = ( 'CableType', - 'ComponentObjectType', + 'ComponentType', 'ConsolePortType', 'ConsolePortTemplateType', 'ConsoleServerPortType', @@ -44,6 +44,7 @@ __all__ = ( 'InventoryItemTemplateType', 'LocationType', 'ManufacturerType', + 'ModularComponentType', 'ModuleType', 'ModuleBayType', 'ModuleBayTemplateType', @@ -74,7 +75,7 @@ __all__ = ( @strawberry.type -class ComponentObjectType( +class ComponentType( ChangelogMixin, CustomFieldsMixin, TagsMixin, @@ -84,9 +85,16 @@ class ComponentObjectType( Base type for device/VM components """ _name: str + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] -class ComponentTemplateObjectType( +@strawberry.type +class ModularComponentType(ComponentType): + module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None + + +@strawberry.type +class ComponentTemplateType( ChangelogMixin, BaseObjectType ): @@ -94,12 +102,22 @@ class ComponentTemplateObjectType( Base type for device/VM components """ _name: str + device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')] +@strawberry.type +class ModularComponentTemplateType(ComponentTemplateType): + """ + Base type for ComponentTemplateModel which supports optional assignment to a ModuleType. + """ + device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')] | None + module_type: Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')] | None + # # Model types # + @strawberry_django.type( models.CableTermination, exclude=('termination_type', 'termination_id'), @@ -169,7 +187,7 @@ class CableType(NetBoxObjectType): exclude=('_path',), filters=ConsolePortFilter ) -class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): +class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): pass @@ -178,7 +196,7 @@ class ConsolePortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) fields='__all__', filters=ConsolePortTemplateFilter ) -class ConsolePortTemplateType(ComponentTemplateObjectType): +class ConsolePortTemplateType(ModularComponentTemplateType): _name: str @@ -187,7 +205,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType): exclude=('_path',), filters=ConsoleServerPortFilter ) -class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): +class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): pass @@ -196,7 +214,7 @@ class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin, PathEndpoint fields='__all__', filters=ConsoleServerPortTemplateFilter ) -class ConsoleServerPortTemplateType(ComponentTemplateObjectType): +class ConsoleServerPortTemplateType(ModularComponentTemplateType): _name: str @@ -288,8 +306,8 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo fields='__all__', filters=DeviceBayFilter ) -class DeviceBayType(ComponentObjectType): - pass +class DeviceBayType(ComponentType): + installed_device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.type( @@ -297,7 +315,7 @@ class DeviceBayType(ComponentObjectType): fields='__all__', filters=DeviceBayTemplateFilter ) -class DeviceBayTemplateType(ComponentTemplateObjectType): +class DeviceBayTemplateType(ComponentTemplateType): _name: str @@ -306,7 +324,7 @@ class DeviceBayTemplateType(ComponentTemplateObjectType): exclude=('component_type', 'component_id', 'parent'), filters=InventoryItemTemplateFilter ) -class InventoryItemTemplateType(ComponentTemplateObjectType): +class InventoryItemTemplateType(ComponentTemplateType): _name: str @strawberry_django.field @@ -337,6 +355,7 @@ class InventoryItemTemplateType(ComponentTemplateObjectType): ) class DeviceRoleType(OrganizationalObjectType): color: str + config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None @strawberry_django.field def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: @@ -416,7 +435,7 @@ class DeviceTypeType(NetBoxObjectType): fields='__all__', filters=FrontPortFilter ) -class FrontPortType(ComponentObjectType, CabledObjectMixin): +class FrontPortType(ModularComponentType, CabledObjectMixin): color: str @@ -425,7 +444,7 @@ class FrontPortType(ComponentObjectType, CabledObjectMixin): fields='__all__', filters=FrontPortTemplateFilter ) -class FrontPortTemplateType(ComponentTemplateObjectType): +class FrontPortTemplateType(ModularComponentTemplateType): _name: str color: str @@ -435,7 +454,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType): fields='__all__', filters=InterfaceFilter ) -class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, PathEndpointMixin): +class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin): mac_address: str | None wwn: str | None @@ -473,7 +492,7 @@ class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin, Pa fields='__all__', filters=InterfaceTemplateFilter ) -class InterfaceTemplateType(ComponentTemplateObjectType): +class InterfaceTemplateType(ModularComponentTemplateType): _name: str @strawberry_django.field @@ -486,7 +505,7 @@ class InterfaceTemplateType(ComponentTemplateObjectType): exclude=('component_type', 'component_id', 'parent'), filters=InventoryItemFilter ) -class InventoryItemType(ComponentObjectType): +class InventoryItemType(ComponentType): @strawberry_django.field def parent(self) -> Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -631,7 +650,7 @@ class ModuleType(NetBoxObjectType): fields='__all__', filters=ModuleBayFilter ) -class ModuleBayType(ComponentObjectType): +class ModuleBayType(ComponentType): @strawberry_django.field def installed_module(self) -> Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None: @@ -643,7 +662,7 @@ class ModuleBayType(ComponentObjectType): fields='__all__', filters=ModuleBayTemplateFilter ) -class ModuleBayTemplateType(ComponentTemplateObjectType): +class ModuleBayTemplateType(ComponentTemplateType): _name: str @@ -717,7 +736,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): fields='__all__', filters=PowerOutletFilter ) -class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): +class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): pass @@ -726,7 +745,7 @@ class PowerOutletType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin) fields='__all__', filters=PowerOutletTemplateFilter ) -class PowerOutletTemplateType(ComponentTemplateObjectType): +class PowerOutletTemplateType(ModularComponentTemplateType): _name: str @@ -747,7 +766,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): exclude=('_path',), filters=PowerPortFilter ) -class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): +class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.field def poweroutlets(self) -> List[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]: @@ -759,7 +778,7 @@ class PowerPortType(ComponentObjectType, CabledObjectMixin, PathEndpointMixin): fields='__all__', filters=PowerPortTemplateFilter ) -class PowerPortTemplateType(ComponentTemplateObjectType): +class PowerPortTemplateType(ModularComponentTemplateType): _name: str @strawberry_django.field @@ -823,7 +842,7 @@ class RackRoleType(OrganizationalObjectType): fields='__all__', filters=RearPortFilter ) -class RearPortType(ComponentObjectType, CabledObjectMixin): +class RearPortType(ModularComponentType, CabledObjectMixin): color: str @strawberry_django.field @@ -836,7 +855,7 @@ class RearPortType(ComponentObjectType, CabledObjectMixin): fields='__all__', filters=RearPortTemplateFilter ) -class RearPortTemplateType(ComponentTemplateObjectType): +class RearPortTemplateType(ModularComponentTemplateType): _name: str color: str diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 8e7e63303..9fd646562 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -158,8 +158,7 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): @strawberry_django.type( models.IPRange, - # fields='__all__', - exclude=('start_address', 'end_address',), # bug - temp + fields='__all__', filters=IPRangeFilter ) class IPRangeType(NetBoxObjectType): @@ -172,8 +171,7 @@ class IPRangeType(NetBoxObjectType): @strawberry_django.type( models.Prefix, - # fields='__all__', - exclude=('prefix',), # bug - temp + fields='__all__', filters=PrefixFilter ) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 832facf85..08ea36b4c 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -3,7 +3,7 @@ from typing import Annotated, List import strawberry import strawberry_django -from dcim.graphql.types import ComponentObjectType +from dcim.graphql.types import ComponentType from extras.graphql.mixins import ConfigContextMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt @@ -110,8 +110,7 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): fields='__all__', filters=VMInterfaceFilter ) -class VMInterfaceType(IPAddressesMixin, ComponentObjectType): - _name: str +class VMInterfaceType(IPAddressesMixin, ComponentType): mac_address: str | None parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None @@ -141,6 +140,5 @@ class VMInterfaceType(IPAddressesMixin, ComponentObjectType): fields='__all__', filters=VirtualDiskFilter ) -class VirtualDiskType(ComponentObjectType): - _name: str +class VirtualDiskType(ComponentType): virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] From a32f4b82e9cc4d868561439ca1aa33a5df3e24cd Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 11:40:50 -0800 Subject: [PATCH 060/156] 9856 fix dcim FK --- netbox/dcim/graphql/types.py | 70 +++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index e08e39ae6..f44d0bc4f 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -235,6 +235,19 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBo device_bay_count: BigInt module_bay_count: BigInt inventory_item_count: BigInt + config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None + device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')] + role: Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')] + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + platform: Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')] | None + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] + location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None + rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] | None + primary_ip4: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + primary_ip6: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + oob_ip: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + cluster: Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')] | None + virtual_chassis: Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def vc_master_for(self) -> Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')] | None: @@ -326,6 +339,8 @@ class DeviceBayTemplateType(ComponentTemplateType): ) class InventoryItemTemplateType(ComponentTemplateType): _name: str + role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.field def parent(self) -> Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')] | None: @@ -384,6 +399,8 @@ class DeviceTypeType(NetBoxObjectType): inventory_item_template_count: BigInt front_image: strawberry_django.fields.types.DjangoImageType | None rear_image: strawberry_django.fields.types.DjangoImageType | None + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] + default_platform: Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: @@ -437,6 +454,7 @@ class DeviceTypeType(NetBoxObjectType): ) class FrontPortType(ModularComponentType, CabledObjectMixin): color: str + rear_port: Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.type( @@ -447,16 +465,23 @@ class FrontPortType(ModularComponentType, CabledObjectMixin): class FrontPortTemplateType(ModularComponentTemplateType): _name: str color: str + rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.type( models.Interface, - fields='__all__', + exclude=('_path',), filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin): mac_address: str | None wwn: str | None + parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None + bridge: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None + lag: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None + wireless_link: Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')] | None + untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None + vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @strawberry_django.field def vdcs(self) -> List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]: @@ -494,6 +519,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P ) class InterfaceTemplateType(ModularComponentTemplateType): _name: str + bridge: Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def bridge_interfaces(self) -> List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]: @@ -506,6 +532,9 @@ class InterfaceTemplateType(ModularComponentTemplateType): filters=InventoryItemFilter ) class InventoryItemType(ComponentType): + role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] + @strawberry_django.field def parent(self) -> Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -551,6 +580,9 @@ class InventoryItemRoleType(OrganizationalObjectType): filters=LocationFilter ) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + parent: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def powerpanel_set(self) -> List[Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]]: @@ -572,10 +604,6 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: return self.devices.all() - @strawberry_django.field - def parent(self) -> Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None: - return self.parent - @strawberry_django.field def children(self) -> List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]: return self.children.all() @@ -615,6 +643,9 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin): filters=ModuleFilter ) class ModuleType(NetBoxObjectType): + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] + module_bay: Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] + module_type: Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: @@ -672,6 +703,7 @@ class ModuleBayTemplateType(ComponentTemplateType): filters=ModuleTypeFilter ) class ModuleTypeType(NetBoxObjectType): + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.field def frontporttemplates(self) -> List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]: @@ -712,6 +744,8 @@ class ModuleTypeType(NetBoxObjectType): filters=PlatformFilter ) class PlatformType(OrganizationalObjectType): + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None + config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None @strawberry_django.field def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: @@ -728,16 +762,18 @@ class PlatformType(OrganizationalObjectType): filters=PowerFeedFilter ) class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): - pass + power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')] + rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.type( models.PowerOutlet, - fields='__all__', + exclude=('_path',), filters=PowerOutletFilter ) class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): - pass + power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.type( @@ -747,6 +783,7 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin ) class PowerOutletTemplateType(ModularComponentTemplateType): _name: str + power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.type( @@ -755,6 +792,8 @@ class PowerOutletTemplateType(ModularComponentTemplateType): filters=PowerPanelFilter ) class PowerPanelType(NetBoxObjectType, ContactsMixin): + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] + location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def powerfeeds(self) -> List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]: @@ -793,6 +832,10 @@ class PowerPortTemplateType(ModularComponentTemplateType): ) class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str + site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] + location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + role: Annotated["RackRoleType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def reservations(self) -> List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]: @@ -822,6 +865,9 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje ) class RackReservationType(NetBoxObjectType): units: List[int] + rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None + user: Annotated["UserType", strawberry.lazy('users.graphql.types')] @strawberry_django.type( @@ -897,6 +943,9 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): _name: str time_zone: str | None + region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None + group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def prefixes(self) -> List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]]: @@ -979,6 +1028,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): ) class VirtualChassisType(NetBoxObjectType): member_count: BigInt + master: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None @strawberry_django.field def members(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: @@ -991,6 +1041,10 @@ class VirtualChassisType(NetBoxObjectType): filters=VirtualDeviceContextFilter ) class VirtualDeviceContextType(NetBoxObjectType): + device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None + primary_ip4: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + primary_ip6: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None + tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: From 133d6bfcb3b732c7b42836d5914c1e11781fa021 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 11:55:35 -0800 Subject: [PATCH 061/156] 9856 fix virtualization FK --- netbox/virtualization/graphql/types.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 08ea36b4c..447e79db4 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -3,7 +3,6 @@ from typing import Annotated, List import strawberry import strawberry_django -from dcim.graphql.types import ComponentType from extras.graphql.mixins import ConfigContextMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt @@ -21,6 +20,15 @@ __all__ = ( ) +@strawberry.type +class ComponentType(NetBoxObjectType): + """ + Base type for device/VM components + """ + _name: str + virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] + + @strawberry_django.type( models.Cluster, fields='__all__', @@ -114,7 +122,6 @@ class VMInterfaceType(IPAddressesMixin, ComponentType): mac_address: str | None parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None - virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @@ -141,4 +148,4 @@ class VMInterfaceType(IPAddressesMixin, ComponentType): filters=VirtualDiskFilter ) class VirtualDiskType(ComponentType): - virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] + pass From 7b6a603111eb7c6666498e935f8e859fe6aadbae Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 12:48:57 -0800 Subject: [PATCH 062/156] 9856 fix tests / remove debug code --- netbox/utilities/testing/api.py | 34 +++++++++------------------------ 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 7445a0edd..1c4d98e2f 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -452,15 +452,13 @@ class APIViewTestCases: fields_string = '' for field in type_class.__strawberry_definition__.fields: - """ - print(f"field_name: {field.name} type: {field.type}") - - if field.name == 'assigned_object': - breakpoint() - pass - """ - - if field.type in (strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType): + if ( + type(field.type) in ( + strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) or + type(field.type) is StrawberryOptional and field.type.of_type in ( + strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) + ): + # image / file fields nullable or not... fields_string += f'{field.name} {{ name }}\n' elif type(field.type) is StrawberryList and type(field.type.of_type) is LazyType: # List of related objects (queryset) @@ -471,22 +469,8 @@ class APIViewTestCases: elif type(field.type) is StrawberryUnion: # this would require a fragment query continue - elif field.type is strawberry_django.fields.types.DjangoModelType: - print("DjangoModelType") - print("--------------------------") - print(f"{self.model} -> {field.name}") - print("") - # Dynamic fields must specify a subselection - fields_string += f'{field.name} {{ pk }}\n' - elif type(field.type) is StrawberryOptional: - if type(field.type.of_type) is LazyType: - fields_string += f'{field.name} {{ id }}\n' - elif field.type.of_type == strawberry_django.fields.types.DjangoModelType: - print("DjangoModelType") - print("--------------------------") - print(f"{self.model} -> {field.name}") - print("") - fields_string += f'{field.name} {{ pk }}\n' + elif type(field.type) is StrawberryOptional and type(field.type.of_type) is LazyType: + fields_string += f'{field.name} {{ id }}\n' elif hasattr(field, 'is_relation') and field.is_relation: # Note: StrawberryField types do not have is_relation fields_string += f'{field.name} {{ id }}\n' From 7fa36cada56114a564472af9915cbdaf0161f148 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 7 Mar 2024 12:59:56 -0800 Subject: [PATCH 063/156] 9856 fix test imagefield --- netbox/utilities/testing/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 1c4d98e2f..698892536 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -453,7 +453,7 @@ class APIViewTestCases: for field in type_class.__strawberry_definition__.fields: if ( - type(field.type) in ( + field.type in ( strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) or type(field.type) is StrawberryOptional and field.type.of_type in ( strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) From ccc81e73d12450dfd343e18d75fd1c4feafee280 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 07:31:38 -0700 Subject: [PATCH 064/156] 9856 cleanup graphene --- netbox/dcim/graphql/types.py | 24 ------ netbox/ipam/graphql/gfk_mixins.py | 95 --------------------- netbox/ipam/graphql/mixins.py | 20 +++-- netbox/netbox/tests/dummy_plugin/graphql.py | 21 +++-- netbox/tenancy/graphql/types.py | 16 ++-- netbox/utilities/testing/api.py | 1 - netbox/virtualization/graphql/types.py | 12 --- 7 files changed, 32 insertions(+), 157 deletions(-) delete mode 100644 netbox/ipam/graphql/gfk_mixins.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index f44d0bc4f..abbd87528 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -507,10 +507,6 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P def child_interfaces(self) -> List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]: return self.child_interfaces.all() - @strawberry_django.field - def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: - return self.ip_addresses.all() - @strawberry_django.type( models.InterfaceTemplate, @@ -596,10 +592,6 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi def racks(self) -> List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]]: return self.racks.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.field def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: return self.devices.all() @@ -853,10 +845,6 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje def cabletermination_set(self) -> List[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]: return self.cabletermination_set.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.type( models.RackReservation, @@ -922,10 +910,6 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: return self.sites.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.field def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -991,10 +975,6 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje def vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: return self.vlans.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.type( models.SiteGroup, @@ -1008,10 +988,6 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): def sites(self) -> List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]: return self.sites.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.field def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent diff --git a/netbox/ipam/graphql/gfk_mixins.py b/netbox/ipam/graphql/gfk_mixins.py deleted file mode 100644 index 01c79690a..000000000 --- a/netbox/ipam/graphql/gfk_mixins.py +++ /dev/null @@ -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 diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py index 38c7657a5..4fe41adde 100644 --- a/netbox/ipam/graphql/mixins.py +++ b/netbox/ipam/graphql/mixins.py @@ -1,4 +1,6 @@ -import graphene +import strawberry +import strawberry_django +from typing import TYPE_CHECKING, Annotated, List, Union __all__ = ( 'IPAddressesMixin', @@ -6,15 +8,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.request.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.request.user, 'view') + @strawberry_django.field + def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: + return self.vlan_groups.all() diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py index 640c12959..f57f7d333 100644 --- a/netbox/netbox/tests/dummy_plugin/graphql.py +++ b/netbox/netbox/tests/dummy_plugin/graphql.py @@ -14,6 +14,7 @@ class DummyModelType: pass +""" @strawberry.type class DummyQuery: @strawberry.field @@ -21,8 +22,18 @@ class DummyQuery: return None dummymodel_list: List[DummyModelType] = strawberry_django.field() -# bug - temp - FIXME! -# schema = strawberry.Schema( -# query=DummyQuery, -# config=StrawberryConfig(auto_camel_case=False), -# ) +schema = strawberry.Schema( + query=DummyQuery, + # config=StrawberryConfig(auto_camel_case=False), +) +""" + + +@strawberry.type +class Query: + fruits: list[int] = strawberry_django.field() + + +schema2 = strawberry.Schema( + query=Query, +) diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 39f99bda8..e873aee51 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -18,11 +18,12 @@ __all__ = ( ) +@strawberry.type class ContactAssignmentsMixin: - # assignments = graphene.List('tenancy.graphql.types.ContactAssignmentType') - def resolve_assignments(self, info): - return self.assignments.restrict(info.context.user, 'view') + @strawberry_django.field + def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: + return self.assignments.all() # @@ -159,10 +160,6 @@ class TenantGroupType(OrganizationalObjectType): class ContactType(ContactAssignmentsMixin, NetBoxObjectType): group: Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')] | None - @strawberry_django.field - def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: - return self.assignments.all() - @strawberry_django.type( models.ContactRole, @@ -170,10 +167,7 @@ class ContactType(ContactAssignmentsMixin, NetBoxObjectType): filters=ContactRoleFilter ) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): - - @strawberry_django.field - def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: - return self.assignments.all() + pass @strawberry_django.type( diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 698892536..6df317b49 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -7,7 +7,6 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.test import override_settings -# from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLUnion, String as GQLString, NonNull as GQLNonNull from rest_framework import status from rest_framework.test import APIClient diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 447e79db4..0756ce824 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -44,10 +44,6 @@ class ClusterType(VLANGroupsMixin, NetBoxObjectType): def virtual_machines(self) -> List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]: return self.virtual_machines.all() - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.field def devices(self) -> List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]: return self.devices.all() @@ -60,10 +56,6 @@ class ClusterType(VLANGroupsMixin, NetBoxObjectType): ) class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): - @strawberry_django.field - def vlan_groups(self) -> List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]: - return self.vlan_groups.all() - @strawberry_django.field def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: return self.clusters.all() @@ -125,10 +117,6 @@ class VMInterfaceType(IPAddressesMixin, ComponentType): untagged_vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None - @strawberry_django.field - def ip_addresses(self) -> List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]: - return self.ip_addresses.all() - @strawberry_django.field def tagged_vlans(self) -> List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]: return self.tagged_vlans.all() From f960d5a4829df989f652b9f1b9b2381c60b4c79c Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 08:33:32 -0700 Subject: [PATCH 065/156] 9856 fix plugin schema --- netbox/netbox/plugins/registration.py | 2 +- netbox/netbox/tests/dummy_plugin/graphql.py | 18 +++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py index fd247a82a..d27bb67ca 100644 --- a/netbox/netbox/plugins/registration.py +++ b/netbox/netbox/plugins/registration.py @@ -73,7 +73,7 @@ def register_graphql_schema(graphql_schema): """ Register a GraphQL schema class for inclusion in NetBox's GraphQL API. """ - registry['plugins']['graphql_schemas'].append(graphql_schema) + registry['plugins']['graphql_schemas'].extend(graphql_schema) def register_user_preferences(plugin_name, preferences): diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py index f57f7d333..a4e6a1fdc 100644 --- a/netbox/netbox/tests/dummy_plugin/graphql.py +++ b/netbox/netbox/tests/dummy_plugin/graphql.py @@ -14,7 +14,6 @@ class DummyModelType: pass -""" @strawberry.type class DummyQuery: @strawberry.field @@ -22,18 +21,7 @@ class DummyQuery: return None dummymodel_list: List[DummyModelType] = strawberry_django.field() -schema = strawberry.Schema( - query=DummyQuery, - # config=StrawberryConfig(auto_camel_case=False), -) -""" - -@strawberry.type -class Query: - fruits: list[int] = strawberry_django.field() - - -schema2 = strawberry.Schema( - query=Query, -) +schema = [ + DummyQuery, +] From ba79078378c4baa1f4385967e8209ddde1dc38c1 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 08:38:06 -0700 Subject: [PATCH 066/156] 9856 fix requirements --- requirements.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 70564e18e..96030ebb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,12 +29,8 @@ psycopg[binary,pool]==3.1.18 PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 -<<<<<<< HEAD -social-auth-core[openidconnect]==4.5.2 -strawberry-graphql-django==0.31.0 -======= social-auth-core[openidconnect]==4.5.3 ->>>>>>> feature +strawberry-graphql-django==0.31.0 svgwrite==1.4.3 tablib==3.5.0 tzdata==2024.1 From 36e6f0d28e7a0071101c7f1d7de7a99794f861aa Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 08:39:01 -0700 Subject: [PATCH 067/156] 9856 fix requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 96030ebb5..5311eb0e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 social-auth-core[openidconnect]==4.5.3 -strawberry-graphql-django==0.31.0 +strawberry-graphql-django==0.33.0 svgwrite==1.4.3 tablib==3.5.0 tzdata==2024.1 From 916722780cca2d1e0b07e61448fc7e57b78969f9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 08:50:56 -0700 Subject: [PATCH 068/156] 9856 fix docs --- docs/plugins/development/graphql-api.md | 35 +++++++++++++-------- netbox/netbox/tests/dummy_plugin/graphql.py | 1 - 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/plugins/development/graphql-api.md b/docs/plugins/development/graphql-api.md index f802e8025..decbe485c 100644 --- a/docs/plugins/development/graphql-api.md +++ b/docs/plugins/development/graphql-api.md @@ -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 diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py index a4e6a1fdc..2651f4e9e 100644 --- a/netbox/netbox/tests/dummy_plugin/graphql.py +++ b/netbox/netbox/tests/dummy_plugin/graphql.py @@ -1,7 +1,6 @@ from typing import List import strawberry import strawberry_django -from strawberry.schema.config import StrawberryConfig from . import models From 02fbea53a7a368996d7a4dc8bae4dc98d34db74e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 08:57:41 -0700 Subject: [PATCH 069/156] 9856 fix docs --- docs/plugins/development/graphql-api.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/plugins/development/graphql-api.md b/docs/plugins/development/graphql-api.md index decbe485c..5f960fa1b 100644 --- a/docs/plugins/development/graphql-api.md +++ b/docs/plugins/development/graphql-api.md @@ -47,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 From be2a814b3838f80f53e1642c5c71fc01a2bc4061 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Mar 2024 09:17:58 -0700 Subject: [PATCH 070/156] 9856 temp fix tests --- netbox/netbox/tests/test_graphql.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 2cf9ee87b..a3f4df782 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -15,6 +15,8 @@ class GraphQLTestCase(TestCase): response = self.client.get(url) self.assertHttpStatus(response, 404) + ''' + BUG TODO - Re-enable @override_settings(LOGIN_REQUIRED=True) def test_graphiql_interface(self): """ @@ -34,3 +36,4 @@ class GraphQLTestCase(TestCase): response = self.client.get(url, **header) with disable_warnings('django.request'): self.assertHttpStatus(response, 302) # Redirect to login page + ''' From a36cc0abb666c224c18c4a2b87d621fe18408179 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Mar 2024 09:35:53 -0700 Subject: [PATCH 071/156] 9856 first filterset --- netbox/circuits/graphql/filters.py | 83 ++++++++++++++++-------- netbox/core/graphql/filters.py | 4 +- netbox/dcim/graphql/filters.py | 4 +- netbox/ipam/graphql/filters.py | 1 - netbox/netbox/graphql/filters.py | 21 ------ netbox/tenancy/graphql/filters.py | 4 +- netbox/virtualization/graphql/filters.py | 1 - netbox/vpn/graphql/filters.py | 4 +- netbox/wireless/graphql/filters.py | 4 +- 9 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 netbox/netbox/graphql/filters.py diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index c9c88ea01..2d6e75d14 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -1,9 +1,13 @@ +from typing import List + import strawberry import strawberry_django +from circuits import filtersets, models from strawberry import auto -from circuits import models, filtersets -from netbox.graphql import filters +from strawberry_django.filters import FilterLookup +from tenancy.graphql.filter_mixins import ContactModelFilterMixin, TenancyFilterMixin +from netbox.graphql.filter_mixins import NetBoxModelFilterMixin __all__ = ( 'CircuitTerminationFilter', @@ -32,37 +36,64 @@ class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): @strawberry_django.filter(models.Circuit, lookups=True) -class CircuitFilter(filtersets.CircuitFilterSet, filters.NetBoxModelFilter): - # NetBoxModelFilterSet - q: str | None - # tag: - # ChangeLoggedModelFilterSet - created: auto - last_updated: auto - created_by_request: str | None - updated_by_request: str | None - modified_by_request: str | None +class CircuitFilter(NetBoxModelFilterMixin, TenancyFilterMixin, ContactModelFilterMixin): + filterset = filtersets.CircuitFilterSet - id: auto cid: auto description: auto install_date: auto termination_date: auto commit_rate: auto - provider_id: auto - provider: auto - provider_account_id: auto - type_id: auto - # provider_network_id: auto - type_id: auto - type: auto + + provider_id: List[str] | None + provider: List[str] | None + provider_account_id: List[str] | None + provider_network_id: List[str] | None + type_id: List[str] | None + type: List[str] | None status: auto - # region_id: auto - # region: auto - # site_group_id: auto - # site_group: auto - # site_id: auto - # site: auto + region_id: List[str] | None + region: List[str] | None + site_group_id: List[str] | None + site_group: List[str] | None + site_id: List[str] | None + site: List[str] | None + + def filter_provider_id(self, queryset): + return self.filter_by_filterset(queryset, 'provider_id') + + def filter_provider(self, queryset): + return self.filter_by_filterset(queryset, 'provider') + + def filter_provider_account_id(self, queryset): + return self.filter_by_filterset(queryset, 'provider_account_id') + + def filter_provider_network_id(self, queryset): + return self.filter_by_filterset(queryset, 'provider_network_id') + + def filter_type_id(self, queryset): + return self.filter_by_filterset(queryset, 'type_id') + + def filter_type(self, queryset): + return self.filter_by_filterset(queryset, 'type') + + def filter_region_id(self, queryset): + return self.filter_by_filterset(queryset, 'region_id') + + def filter_region(self, queryset): + return self.filter_by_filterset(queryset, 'region') + + def filter_site_group_id(self, queryset): + return self.filter_by_filterset(queryset, 'site_group_id') + + def filter_site_group(self, queryset): + return self.filter_by_filterset(queryset, 'site_group') + + def filter_site_id(self, queryset): + return self.filter_by_filterset(queryset, 'site_id') + + def filter_site(self, queryset): + return self.filter_by_filterset(queryset, 'site') # @strawberry_django.filter(models.Circuit, lookups=True) diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index 4e554331f..94a5eaf26 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -1,9 +1,7 @@ import strawberry import strawberry_django +from core import filtersets, models from strawberry import auto -from core import models, filtersets -from netbox.graphql import filters - __all__ = ( 'DataFileFilter', diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index d70552924..b0e478a2b 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -1,9 +1,7 @@ import strawberry import strawberry_django +from dcim import filtersets, models from strawberry import auto -from dcim import models, filtersets -from netbox.graphql import filters - __all__ = ( 'CableFilter', diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 04254cbf0..08a9adb93 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -2,7 +2,6 @@ import strawberry import strawberry_django from strawberry import auto from ipam import models, filtersets -from netbox.graphql import filters __all__ = ( diff --git a/netbox/netbox/graphql/filters.py b/netbox/netbox/graphql/filters.py deleted file mode 100644 index c9e3c5776..000000000 --- a/netbox/netbox/graphql/filters.py +++ /dev/null @@ -1,21 +0,0 @@ -import strawberry -import strawberry_django -from strawberry import auto - - -class ChangeLoggedModelFilter: - - def created_by_request(self, queryset): - return self.filter_by_request(queryset, "created_by_request", self.created_by_request) - - def updated_by_request(self, queryset): - return self.filter_by_request(queryset, "updated_by_request", self.updated_by_request) - - def modified_by_request(self, queryset): - return self.filter_by_request(queryset, "modified_by_request", self.modified_by_request) - - -class NetBoxModelFilter(ChangeLoggedModelFilter): - - def filter_q(self, queryset): - return self.search(queryset, None, self.q) diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index a7d3c2f26..9e646b1f8 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -1,9 +1,9 @@ import strawberry import strawberry_django from strawberry import auto -from tenancy import models, filtersets -from netbox.graphql import filters +from tenancy import filtersets, models +from netbox.graphql import filter_mixins __all__ = ( 'TenantFilter', diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py index 51671f7f0..22fb4d226 100644 --- a/netbox/virtualization/graphql/filters.py +++ b/netbox/virtualization/graphql/filters.py @@ -2,7 +2,6 @@ import strawberry import strawberry_django from strawberry import auto from virtualization import models, filtersets -from netbox.graphql import filters __all__ = ( diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py index c3868cb52..2b347a1c1 100644 --- a/netbox/vpn/graphql/filters.py +++ b/netbox/vpn/graphql/filters.py @@ -1,9 +1,7 @@ import strawberry import strawberry_django from strawberry import auto -from vpn import models, filtersets -from netbox.graphql import filters - +from vpn import filtersets, models __all__ = ( 'TunnelGroupFilter', diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py index 834f962c0..117c4823b 100644 --- a/netbox/wireless/graphql/filters.py +++ b/netbox/wireless/graphql/filters.py @@ -1,9 +1,7 @@ import strawberry import strawberry_django from strawberry import auto -from wireless import models, filtersets -from netbox.graphql import filters - +from wireless import filtersets, models __all__ = ( 'WirelessLANGroupFilter', From 8aca8f84b41a3774783860d5c204edd84b38c5e7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Mar 2024 10:01:34 -0700 Subject: [PATCH 072/156] 9856 first filterset --- netbox/netbox/graphql/filter_mixins.py | 45 +++++++++++++++++++++++ netbox/tenancy/graphql/filter_mixins.py | 48 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 netbox/netbox/graphql/filter_mixins.py create mode 100644 netbox/tenancy/graphql/filter_mixins.py diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py new file mode 100644 index 000000000..af2f3e049 --- /dev/null +++ b/netbox/netbox/graphql/filter_mixins.py @@ -0,0 +1,45 @@ +from typing import List +import strawberry +import strawberry_django +from strawberry import auto + + +@strawberry.input +class BaseFilterMixin: + id: auto + + def filter_by_filterset(self, queryset, key): + return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs + + +@strawberry.input +class ChangeLoggedModelFilterMixin(BaseFilterMixin): + created: auto + last_updated: auto + created_by_request: str | None + updated_by_request: str | None + modified_by_request: str | None + + def filter_created_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'created_by_request') + + def filter_updated_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'updated_by_request') + + def filter_modified_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'modified_by_request') + + +@strawberry.input +class NetBoxModelFilterMixin(ChangeLoggedModelFilterMixin): + q: str | None + tag: List[str] | None + + def filter_q(self, queryset): + # return self.search(queryset, None, self.q) + return self.filter_by_filterset(queryset, 'q') + + def filter_tag(self, queryset, info): + # return self.filterset(data={'tag': self.tag}, queryset=queryset).qs + # return self.filterset(data={'tag': getattr(self, 'tag')}, queryset=queryset).qs + return self.filter_by_filterset(queryset, 'tag') diff --git a/netbox/tenancy/graphql/filter_mixins.py b/netbox/tenancy/graphql/filter_mixins.py new file mode 100644 index 000000000..c33dc24aa --- /dev/null +++ b/netbox/tenancy/graphql/filter_mixins.py @@ -0,0 +1,48 @@ +from typing import List +import strawberry +import strawberry_django +from strawberry import auto +from netbox.graphql.filter_mixins import BaseFilterMixin + +__all__ = ( + 'ContactModelFilterMixin', + 'TenancyFilterMixin', +) + + +@strawberry.input +class TenancyFilterMixin(BaseFilterMixin): + created: auto + last_updated: auto + created_by_request: str | None + updated_by_request: str | None + modified_by_request: str | None + + def filter_created_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'created_by_request') + + def filter_updated_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'updated_by_request') + + def filter_modified_by_request(self, queryset): + return self.filter_by_filterset(queryset, 'modified_by_request') + + +@strawberry.input +class ContactModelFilterMixin(BaseFilterMixin): + tenant_group_id: List[str] | None + tenant_group: List[str] | None + tenant_id: List[str] | None + tenant: List[str] | None + + def filter_tenant_group_id(self, queryset): + return self.filter_by_filterset(queryset, 'tenant_group_id') + + def filter_tenant_group(self, queryset): + return self.filter_by_filterset(queryset, 'tenant_group') + + def filter_tenant_id(self, queryset): + return self.filter_by_filterset(queryset, 'tenant_id') + + def filter_tenant(self, queryset): + return self.filter_by_filterset(queryset, 'tenant') From d6fd0b88af4b627bdb71eeb397e679bddf04c0c5 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Mar 2024 10:39:00 -0700 Subject: [PATCH 073/156] 9856 fix tests --- netbox/extras/graphql/types.py | 2 +- netbox/tenancy/graphql/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index f8c5d85fa..1361250cd 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -123,7 +123,7 @@ class ConfigTemplateType(TagsMixin, ObjectType): filters=CustomFieldFilter ) class CustomFieldType(ObjectType): - object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index e873aee51..90c35d47e 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -189,6 +189,6 @@ class ContactGroupType(OrganizationalObjectType): filters=ContactAssignmentFilter ) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): - content_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None contact: Annotated["ContactType", strawberry.lazy('tenancy.graphql.types')] | None role: Annotated["ContactRoleType", strawberry.lazy('tenancy.graphql.types')] | None From 960f3407f57640b732dbd89b84ed7d8a1b04fc61 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Mar 2024 10:52:20 -0700 Subject: [PATCH 074/156] 9856 fix tests --- netbox/extras/graphql/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 1361250cd..f6c22c30d 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -169,7 +169,7 @@ class ExportTemplateType(ObjectType): filters=ImageAttachmentFilter ) class ImageAttachmentType(BaseObjectType): - content_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None + object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None @strawberry_django.type( From fe3f2c895829ee4e050646b0d2ed95d1f36585d9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Mar 2024 16:35:17 -0700 Subject: [PATCH 075/156] 9856 working auto filter generation --- netbox/circuits/graphql/filters.py | 178 ++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index 2d6e75d14..ce478f0aa 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -1,11 +1,14 @@ from typing import List +import django_filters import strawberry import strawberry_django from circuits import filtersets, models +from functools import partial, partialmethod, wraps from strawberry import auto from strawberry_django.filters import FilterLookup from tenancy.graphql.filter_mixins import ContactModelFilterMixin, TenancyFilterMixin +from utilities.filters import * from netbox.graphql.filter_mixins import NetBoxModelFilterMixin @@ -18,6 +21,167 @@ __all__ = ( 'ProviderNetworkFilter', ) +# def filter_by_filterset(self, queryset, key, cls, filterset): +# breakpoint() +# return filterset(data={key: getattr(cls, key)}, queryset=queryset).qs + + +def autotype_decorator(filterset): + + def wrapper(cls): + cls.filterset = filterset + fields = filterset.get_fields() + print(f"fields: {fields}") + for fieldname in fields.keys(): + if fieldname not in cls.__annotations__: + cls.__annotations__[fieldname] = auto + + # fields = list(filterset.get_fields().keys()) + declared_filters = filterset.declared_filters + print(f"declared_filters: {declared_filters}") + print("") + for fieldname, v in declared_filters.items(): + create_function = False + attr_type = None + print(f"{fieldname}: {v}") + + if isinstance(v, ContentTypeFilter): + print("ContentTypeFilter") + elif isinstance(v, MACAddressFilter): + print("MACAddressFilter") + elif isinstance(v, MultiValueArrayFilter): + print("MultiValueArrayFilter") + elif isinstance(v, MultiValueCharFilter): + print("MultiValueCharFilter") + elif isinstance(v, MultiValueDateFilter): + print("MultiValueDateFilter") + elif isinstance(v, MultiValueDateTimeFilter): + print("MultiValueDateTimeFilter") + elif isinstance(v, MultiValueDecimalFilter): + print("MultiValueDecimalFilter") + elif isinstance(v, MultiValueMACAddressFilter): + print("MultiValueMACAddressFilter") + elif isinstance(v, MultiValueNumberFilter): + print("MultiValueNumberFilter") + elif isinstance(v, MultiValueTimeFilter): + print("MultiValueTimeFilter") + elif isinstance(v, MultiValueWWNFilter): + print("MultiValueWWNFilter") + elif isinstance(v, NullableCharFieldFilter): + print("NullableCharFieldFilter") + elif isinstance(v, NumericArrayFilter): + print("NumericArrayFilter") + elif isinstance(v, TreeNodeMultipleChoiceFilter): + print("TreeNodeMultipleChoiceFilter") + + elif issubclass(type(v), django_filters.CharFilter): + print("CharFilter") + elif issubclass(type(v), django_filters.UUIDFilter): + print("UUIDFilter") + elif issubclass(type(v), django_filters.BooleanFilter): + print("BooleanFilter") + elif issubclass(type(v), django_filters.ChoiceFilter): + print("ChoiceFilter") + elif issubclass(type(v), django_filters.TypedChoiceFilter): + print("TypedChoiceFilter") + elif issubclass(type(v), django_filters.DateFilter): + print("DateFilter") + elif issubclass(type(v), django_filters.TimeFilter): + print("TimeFilter") + elif issubclass(type(v), django_filters.DateTimeFilter): + print("DateTimeFilter") + elif issubclass(type(v), django_filters.IsoDateTimeFilter): + print("IsoDateTimeFilter") + elif issubclass(type(v), django_filters.DurationFilter): + print("DurationFilter") + elif issubclass(type(v), django_filters.ModelChoiceFilter): + print("ModelChoiceFilter") + elif issubclass(type(v), django_filters.ModelMultipleChoiceFilter): + create_function = True + attr_type = List[str] | None + print("ModelMultipleChoiceFilter") + elif issubclass(type(v), django_filters.NumberFilter): + print("NumberFilter") + elif issubclass(type(v), django_filters.NumericRangeFilter): + print("NumericRangeFilter") + elif issubclass(type(v), django_filters.RangeFilter): + print("RangeFilter") + elif issubclass(type(v), django_filters.DateRangeFilter): + print("DateRangeFilter") + elif issubclass(type(v), django_filters.DateFromToRangeFilter): + print("DateFromToRangeFilter") + elif issubclass(type(v), django_filters.DateTimeFromToRangeFilter): + print("DateTimeFromToRangeFilter") + elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): + print("IsoDateTimeFromToRangeFilter") + elif issubclass(type(v), django_filters.TimeRangeFilter): + print("TimeRangeFilter") + elif issubclass(type(v), django_filters.AllValuesFilter): + print("AllValuesFilter") + elif issubclass(type(v), django_filters.AllValuesMultipleFilter): + print("AllValuesMultipleFilter") + elif issubclass(type(v), django_filters.LookupChoiceFilter): + print("LookupChoiceFilter") + elif issubclass(type(v), django_filters.BaseInFilter): + print("BaseInFilter") + elif issubclass(type(v), django_filters.BaseRangeFilter): + print("BaseRangeFilter") + elif issubclass(type(v), django_filters.OrderingFilter): + print("OrderingFilter") + elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): + print("TypedMultipleChoiceFilter") + elif issubclass(type(v), django_filters.MultipleChoiceFilter): + print("MultipleChoiceFilter") + else: + print("unknown type!") + + if fieldname not in cls.__annotations__ and attr_type: + print(f"adding {fieldname} to class") + cls.__annotations__[fieldname] = attr_type + + fname = f"filter_{fieldname}" + if create_function and not hasattr(cls, fname): + print(f"creating function {fname}") + filter_by_filterset = getattr(cls, 'filter_by_filterset') + setattr(cls, fname, partialmethod(filter_by_filterset, key=fieldname)) + # setattr(cls, fname, partial(filter_by_filterset, key=fieldname, cls=cls, filterset=filterset)) + + print("") + return cls + + return wrapper + + +""" +class autotype_decorator(object): + def __init__(self, filterset): + self.filterset = filterset + def __call__(self, cls): + class Wrapped(cls): + ''' + cls.filterset = filterset + fields = filterset.get_fields() + print(fields) + fields = list(filterset.get_fields().keys()) + declared_filters = filterset.declared_filters + print(declared_filters) + fields.extend(list(filterset.declared_filters.keys())) + for field in fields: + print(field) + + ''' + print(f"cls: {cls}") + print(f"self: {self}") + vars()['cid'] = strawberry.unset.UnsetType + # setattr(cls, 'cid', strawberry.unset.UnsetType) + pass + + setattr(Wrapped, 'cid', strawberry.unset.UnsetType) + print(f"hasattr: {hasattr(Wrapped, 'cid')}") + print(Wrapped) + return Wrapped +""" + @strawberry_django.filter(models.CircuitTermination, lookups=True) class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): @@ -36,10 +200,17 @@ class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): @strawberry_django.filter(models.Circuit, lookups=True) -class CircuitFilter(NetBoxModelFilterMixin, TenancyFilterMixin, ContactModelFilterMixin): - filterset = filtersets.CircuitFilterSet +@autotype_decorator(filtersets.CircuitFilterSet) +class CircuitFilter: + # class CircuitFilter(NetBoxModelFilterMixin, TenancyFilterMixin, ContactModelFilterMixin): - cid: auto + def filter_by_filterset(self, queryset, key): + return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs + + pass + """ + # vars()['cid'] = strawberry.unset.UnsetType + # cid: auto description: auto install_date: auto termination_date: auto @@ -94,6 +265,7 @@ class CircuitFilter(NetBoxModelFilterMixin, TenancyFilterMixin, ContactModelFilt def filter_site(self, queryset): return self.filter_by_filterset(queryset, 'site') + """ # @strawberry_django.filter(models.Circuit, lookups=True) From 2c9bea9ab9fbe81d05fc8165263fc342206f3180 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 10:07:28 -0700 Subject: [PATCH 076/156] 9856 filter types --- netbox/circuits/graphql/filters.py | 317 ++----------------------- netbox/core/graphql/filters.py | 13 +- netbox/dcim/graphql/filters.py | 208 +++++++++------- netbox/extras/graphql/filters.py | 141 ++++------- netbox/ipam/graphql/filters.py | 83 ++++--- netbox/netbox/graphql/filter_mixins.py | 170 ++++++++++++- 6 files changed, 410 insertions(+), 522 deletions(-) diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index ce478f0aa..10887ce3f 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -1,16 +1,8 @@ -from typing import List - -import django_filters import strawberry import strawberry_django from circuits import filtersets, models -from functools import partial, partialmethod, wraps -from strawberry import auto -from strawberry_django.filters import FilterLookup -from tenancy.graphql.filter_mixins import ContactModelFilterMixin, TenancyFilterMixin -from utilities.filters import * -from netbox.graphql.filter_mixins import NetBoxModelFilterMixin +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'CircuitTerminationFilter', @@ -21,313 +13,38 @@ __all__ = ( 'ProviderNetworkFilter', ) -# def filter_by_filterset(self, queryset, key, cls, filterset): -# breakpoint() -# return filterset(data={key: getattr(cls, key)}, queryset=queryset).qs - - -def autotype_decorator(filterset): - - def wrapper(cls): - cls.filterset = filterset - fields = filterset.get_fields() - print(f"fields: {fields}") - for fieldname in fields.keys(): - if fieldname not in cls.__annotations__: - cls.__annotations__[fieldname] = auto - - # fields = list(filterset.get_fields().keys()) - declared_filters = filterset.declared_filters - print(f"declared_filters: {declared_filters}") - print("") - for fieldname, v in declared_filters.items(): - create_function = False - attr_type = None - print(f"{fieldname}: {v}") - - if isinstance(v, ContentTypeFilter): - print("ContentTypeFilter") - elif isinstance(v, MACAddressFilter): - print("MACAddressFilter") - elif isinstance(v, MultiValueArrayFilter): - print("MultiValueArrayFilter") - elif isinstance(v, MultiValueCharFilter): - print("MultiValueCharFilter") - elif isinstance(v, MultiValueDateFilter): - print("MultiValueDateFilter") - elif isinstance(v, MultiValueDateTimeFilter): - print("MultiValueDateTimeFilter") - elif isinstance(v, MultiValueDecimalFilter): - print("MultiValueDecimalFilter") - elif isinstance(v, MultiValueMACAddressFilter): - print("MultiValueMACAddressFilter") - elif isinstance(v, MultiValueNumberFilter): - print("MultiValueNumberFilter") - elif isinstance(v, MultiValueTimeFilter): - print("MultiValueTimeFilter") - elif isinstance(v, MultiValueWWNFilter): - print("MultiValueWWNFilter") - elif isinstance(v, NullableCharFieldFilter): - print("NullableCharFieldFilter") - elif isinstance(v, NumericArrayFilter): - print("NumericArrayFilter") - elif isinstance(v, TreeNodeMultipleChoiceFilter): - print("TreeNodeMultipleChoiceFilter") - - elif issubclass(type(v), django_filters.CharFilter): - print("CharFilter") - elif issubclass(type(v), django_filters.UUIDFilter): - print("UUIDFilter") - elif issubclass(type(v), django_filters.BooleanFilter): - print("BooleanFilter") - elif issubclass(type(v), django_filters.ChoiceFilter): - print("ChoiceFilter") - elif issubclass(type(v), django_filters.TypedChoiceFilter): - print("TypedChoiceFilter") - elif issubclass(type(v), django_filters.DateFilter): - print("DateFilter") - elif issubclass(type(v), django_filters.TimeFilter): - print("TimeFilter") - elif issubclass(type(v), django_filters.DateTimeFilter): - print("DateTimeFilter") - elif issubclass(type(v), django_filters.IsoDateTimeFilter): - print("IsoDateTimeFilter") - elif issubclass(type(v), django_filters.DurationFilter): - print("DurationFilter") - elif issubclass(type(v), django_filters.ModelChoiceFilter): - print("ModelChoiceFilter") - elif issubclass(type(v), django_filters.ModelMultipleChoiceFilter): - create_function = True - attr_type = List[str] | None - print("ModelMultipleChoiceFilter") - elif issubclass(type(v), django_filters.NumberFilter): - print("NumberFilter") - elif issubclass(type(v), django_filters.NumericRangeFilter): - print("NumericRangeFilter") - elif issubclass(type(v), django_filters.RangeFilter): - print("RangeFilter") - elif issubclass(type(v), django_filters.DateRangeFilter): - print("DateRangeFilter") - elif issubclass(type(v), django_filters.DateFromToRangeFilter): - print("DateFromToRangeFilter") - elif issubclass(type(v), django_filters.DateTimeFromToRangeFilter): - print("DateTimeFromToRangeFilter") - elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): - print("IsoDateTimeFromToRangeFilter") - elif issubclass(type(v), django_filters.TimeRangeFilter): - print("TimeRangeFilter") - elif issubclass(type(v), django_filters.AllValuesFilter): - print("AllValuesFilter") - elif issubclass(type(v), django_filters.AllValuesMultipleFilter): - print("AllValuesMultipleFilter") - elif issubclass(type(v), django_filters.LookupChoiceFilter): - print("LookupChoiceFilter") - elif issubclass(type(v), django_filters.BaseInFilter): - print("BaseInFilter") - elif issubclass(type(v), django_filters.BaseRangeFilter): - print("BaseRangeFilter") - elif issubclass(type(v), django_filters.OrderingFilter): - print("OrderingFilter") - elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): - print("TypedMultipleChoiceFilter") - elif issubclass(type(v), django_filters.MultipleChoiceFilter): - print("MultipleChoiceFilter") - else: - print("unknown type!") - - if fieldname not in cls.__annotations__ and attr_type: - print(f"adding {fieldname} to class") - cls.__annotations__[fieldname] = attr_type - - fname = f"filter_{fieldname}" - if create_function and not hasattr(cls, fname): - print(f"creating function {fname}") - filter_by_filterset = getattr(cls, 'filter_by_filterset') - setattr(cls, fname, partialmethod(filter_by_filterset, key=fieldname)) - # setattr(cls, fname, partial(filter_by_filterset, key=fieldname, cls=cls, filterset=filterset)) - - print("") - return cls - - return wrapper - - -""" -class autotype_decorator(object): - def __init__(self, filterset): - self.filterset = filterset - def __call__(self, cls): - class Wrapped(cls): - ''' - cls.filterset = filterset - fields = filterset.get_fields() - print(fields) - fields = list(filterset.get_fields().keys()) - declared_filters = filterset.declared_filters - print(declared_filters) - fields.extend(list(filterset.declared_filters.keys())) - for field in fields: - print(field) - - ''' - print(f"cls: {cls}") - print(f"self: {self}") - vars()['cid'] = strawberry.unset.UnsetType - # setattr(cls, 'cid', strawberry.unset.UnsetType) - pass - - setattr(Wrapped, 'cid', strawberry.unset.UnsetType) - print(f"hasattr: {hasattr(Wrapped, 'cid')}") - print(Wrapped) - return Wrapped -""" - @strawberry_django.filter(models.CircuitTermination, lookups=True) -class CircuitTerminationFilter(filtersets.CircuitTerminationFilterSet): - id: auto - term_side: auto - port_speed: auto - upstream_speed: auto - xconnect_id: auto - description: auto - cable_end: auto - # q: auto - circuit_id: auto - site_id: auto - site: auto - # provider_network_id: auto +@autotype_decorator(filtersets.CircuitTerminationFilterSet) +class CircuitTerminationFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Circuit, lookups=True) @autotype_decorator(filtersets.CircuitFilterSet) -class CircuitFilter: - # class CircuitFilter(NetBoxModelFilterMixin, TenancyFilterMixin, ContactModelFilterMixin): - - def filter_by_filterset(self, queryset, key): - return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs - +class CircuitFilter(BaseFilterMixin): pass - """ - # vars()['cid'] = strawberry.unset.UnsetType - # cid: auto - description: auto - install_date: auto - termination_date: auto - commit_rate: auto - - provider_id: List[str] | None - provider: List[str] | None - provider_account_id: List[str] | None - provider_network_id: List[str] | None - type_id: List[str] | None - type: List[str] | None - status: auto - region_id: List[str] | None - region: List[str] | None - site_group_id: List[str] | None - site_group: List[str] | None - site_id: List[str] | None - site: List[str] | None - - def filter_provider_id(self, queryset): - return self.filter_by_filterset(queryset, 'provider_id') - - def filter_provider(self, queryset): - return self.filter_by_filterset(queryset, 'provider') - - def filter_provider_account_id(self, queryset): - return self.filter_by_filterset(queryset, 'provider_account_id') - - def filter_provider_network_id(self, queryset): - return self.filter_by_filterset(queryset, 'provider_network_id') - - def filter_type_id(self, queryset): - return self.filter_by_filterset(queryset, 'type_id') - - def filter_type(self, queryset): - return self.filter_by_filterset(queryset, 'type') - - def filter_region_id(self, queryset): - return self.filter_by_filterset(queryset, 'region_id') - - def filter_region(self, queryset): - return self.filter_by_filterset(queryset, 'region') - - def filter_site_group_id(self, queryset): - return self.filter_by_filterset(queryset, 'site_group_id') - - def filter_site_group(self, queryset): - return self.filter_by_filterset(queryset, 'site_group') - - def filter_site_id(self, queryset): - return self.filter_by_filterset(queryset, 'site_id') - - def filter_site(self, queryset): - return self.filter_by_filterset(queryset, 'site') - """ - - -# @strawberry_django.filter(models.Circuit, lookups=True) -# class CircuitFilter(filtersets.CircuitFilterSet): -# id: auto -# cid: auto -# description: auto -# install_date: auto -# termination_date: auto -# commit_rate: auto -# provider_id: auto -# provider: auto -# provider_account_id: auto -# # provider_network_id: auto -# type_id: auto -# type: auto -# status: auto -# # region_id: auto -# # region: auto -# # site_group_id: auto -# # site_group: auto -# # site_id: auto -# # site: auto @strawberry_django.filter(models.CircuitType, lookups=True) -class CircuitTypeFilter(filtersets.CircuitTypeFilterSet): - id: auto - name: auto - slug: auto - description: auto +@autotype_decorator(filtersets.CircuitTypeFilterSet) +class CircuitTypeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Provider, lookups=True) -class ProviderFilter(filtersets.ProviderFilterSet): - id: auto - name: auto - slug: auto - # region_id: auto - # region: auto - # site_group_id: auto - # site_group: auto - # site_id: auto - # site: auto - # asn_id: auto +@autotype_decorator(filtersets.ProviderFilterSet) +class ProviderFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ProviderAccount, lookups=True) -class ProviderAccountFilter(filtersets.ProviderAccountFilterSet): - id: auto - name: auto - account: auto - description: auto - # provider_id: auto - # provider: auto +@autotype_decorator(filtersets.ProviderAccountFilterSet) +class ProviderAccountFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ProviderNetwork, lookups=True) -class ProviderNetworkFilter(filtersets.ProviderNetworkFilterSet): - id: auto - name: auto - service_id: auto - description: auto - # provider_id: auto - # provider: auto +@autotype_decorator(filtersets.ProviderNetworkFilterSet) +class ProviderNetworkFilter(BaseFilterMixin): + pass diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index 94a5eaf26..fd3f9e459 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -1,7 +1,8 @@ import strawberry import strawberry_django from core import filtersets, models -from strawberry import auto + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'DataFileFilter', @@ -10,10 +11,12 @@ __all__ = ( @strawberry_django.filter(models.DataFile, lookups=True) -class DataFileFilter(filtersets.DataFileFilterSet): - id: auto +@autotype_decorator(filtersets.DataFileFilterSet) +class DataFileFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.DataSource, lookups=True) -class DataSourceFilter(filtersets.DataSourceFilterSet): - id: auto +@autotype_decorator(filtersets.DataSourceFilterSet) +class DataSourceFilter(BaseFilterMixin): + pass diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index b0e478a2b..2874b4418 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -1,7 +1,8 @@ import strawberry import strawberry_django from dcim import filtersets, models -from strawberry import auto + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'CableFilter', @@ -49,205 +50,246 @@ __all__ = ( @strawberry_django.filter(models.Cable, lookups=True) -class CableFilter(filtersets.CableFilterSet): - id: auto +@autotype_decorator(filtersets.CableFilterSet) +class CableFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.CableTermination, lookups=True) -class CableTerminationFilter(filtersets.CableTerminationFilterSet): - id: auto +@autotype_decorator(filtersets.CableTerminationFilterSet) +class CableTerminationFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ConsolePort, lookups=True) -class ConsolePortFilter(filtersets.ConsolePortFilterSet): - id: auto +@autotype_decorator(filtersets.ConsolePortFilterSet) +class ConsolePortFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ConsolePortTemplate, lookups=True) -class ConsolePortTemplateFilter(filtersets.ConsolePortTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.ConsolePortTemplateFilterSet) +class ConsolePortTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ConsoleServerPort, lookups=True) -class ConsoleServerPortFilter(filtersets.ConsoleServerPortFilterSet): - id: auto +@autotype_decorator(filtersets.ConsoleServerPortFilterSet) +class ConsoleServerPortFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) -class ConsoleServerPortTemplateFilter(filtersets.ConsoleServerPortTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.ConsoleServerPortTemplateFilterSet) +class ConsoleServerPortTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Device, lookups=True) -class DeviceFilter(filtersets.DeviceFilterSet): - id: auto +@autotype_decorator(filtersets.DeviceFilterSet) +class DeviceFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.DeviceBay, lookups=True) -class DeviceBayFilter(filtersets.DeviceBayFilterSet): - id: auto +@autotype_decorator(filtersets.DeviceBayFilterSet) +class DeviceBayFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.DeviceBayTemplate, lookups=True) -class DeviceBayTemplateFilter(filtersets.DeviceBayTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.DeviceBayTemplateFilterSet) +class DeviceBayTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.InventoryItemTemplate, lookups=True) -class InventoryItemTemplateFilter(filtersets.InventoryItemTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.InventoryItemTemplateFilterSet) +class InventoryItemTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.DeviceRole, lookups=True) -class DeviceRoleFilter(filtersets.DeviceRoleFilterSet): - id: auto +@autotype_decorator(filtersets.DeviceRoleFilterSet) +class DeviceRoleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.DeviceType, lookups=True) -class DeviceTypeFilter(filtersets.DeviceTypeFilterSet): - id: auto +@autotype_decorator(filtersets.DeviceTypeFilterSet) +class DeviceTypeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.FrontPort, lookups=True) -class FrontPortFilter(filtersets.FrontPortFilterSet): - id: auto +@autotype_decorator(filtersets.FrontPortFilterSet) +class FrontPortFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.FrontPortTemplate, lookups=True) -class FrontPortTemplateFilter(filtersets.FrontPortTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.FrontPortTemplateFilterSet) +class FrontPortTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Interface, lookups=True) -class InterfaceFilter(filtersets.InterfaceFilterSet): - id: auto +@autotype_decorator(filtersets.InterfaceFilterSet) +class InterfaceFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.InterfaceTemplate, lookups=True) -class InterfaceTemplateFilter(filtersets.InterfaceTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.InterfaceTemplateFilterSet) +class InterfaceTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.InventoryItem, lookups=True) -class InventoryItemFilter(filtersets.InventoryItemFilterSet): - id: auto +@autotype_decorator(filtersets.InventoryItemFilterSet) +class InventoryItemFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.InventoryItemRole, lookups=True) -class InventoryItemRoleFilter(filtersets.InventoryItemRoleFilterSet): - id: auto +@autotype_decorator(filtersets.InventoryItemRoleFilterSet) +class InventoryItemRoleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Location, lookups=True) -class LocationFilter(filtersets.LocationFilterSet): - id: auto +@autotype_decorator(filtersets.LocationFilterSet) +class LocationFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Manufacturer, lookups=True) -class ManufacturerFilter(filtersets.ManufacturerFilterSet): - id: auto +@autotype_decorator(filtersets.ManufacturerFilterSet) +class ManufacturerFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Module, lookups=True) -class ModuleFilter(filtersets.ModuleFilterSet): - id: auto +@autotype_decorator(filtersets.ModuleFilterSet) +class ModuleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ModuleBay, lookups=True) -class ModuleBayFilter(filtersets.ModuleBayFilterSet): - id: auto +@autotype_decorator(filtersets.ModuleBayFilterSet) +class ModuleBayFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ModuleBayTemplate, lookups=True) -class ModuleBayTemplateFilter(filtersets.ModuleBayTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.ModuleBayTemplateFilterSet) +class ModuleBayTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ModuleType, lookups=True) -class ModuleTypeFilter(filtersets.ModuleTypeFilterSet): - id: auto +@autotype_decorator(filtersets.ModuleTypeFilterSet) +class ModuleTypeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Platform, lookups=True) -class PlatformFilter(filtersets.PlatformFilterSet): - id: auto +@autotype_decorator(filtersets.PlatformFilterSet) +class PlatformFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerFeed, lookups=True) -class PowerFeedFilter(filtersets.PowerFeedFilterSet): - id: auto +@autotype_decorator(filtersets.PowerFeedFilterSet) +class PowerFeedFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerOutlet, lookups=True) -class PowerOutletFilter(filtersets.PowerOutletFilterSet): - id: auto +@autotype_decorator(filtersets.PowerOutletFilterSet) +class PowerOutletFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerOutletTemplate, lookups=True) -class PowerOutletTemplateFilter(filtersets.PowerOutletTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.PowerOutletTemplateFilterSet) +class PowerOutletTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerPanel, lookups=True) -class PowerPanelFilter(filtersets.PowerPanelFilterSet): - id: auto +@autotype_decorator(filtersets.PowerPanelFilterSet) +class PowerPanelFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerPort, lookups=True) -class PowerPortFilter(filtersets.PowerPortFilterSet): - id: auto +@autotype_decorator(filtersets.PowerPortFilterSet) +class PowerPortFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.PowerPortTemplate, lookups=True) -class PowerPortTemplateFilter(filtersets.PowerPortTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.PowerPortTemplateFilterSet) +class PowerPortTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Rack, lookups=True) -class RackFilter(filtersets.RackFilterSet): - id: auto +@autotype_decorator(filtersets.RackFilterSet) +class RackFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RackReservation, lookups=True) -class RackReservationFilter(filtersets.RackReservationFilterSet): - id: auto +@autotype_decorator(filtersets.RackReservationFilterSet) +class RackReservationFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RackRole, lookups=True) -class RackRoleFilter(filtersets.RackRoleFilterSet): - id: auto +@autotype_decorator(filtersets.RackRoleFilterSet) +class RackRoleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RearPort, lookups=True) -class RearPortFilter(filtersets.RearPortFilterSet): - id: auto +@autotype_decorator(filtersets.RearPortFilterSet) +class RearPortFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RearPortTemplate, lookups=True) -class RearPortTemplateFilter(filtersets.RearPortTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.RearPortTemplateFilterSet) +class RearPortTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Region, lookups=True) -class RegionFilter(filtersets.RegionFilterSet): - id: auto +@autotype_decorator(filtersets.RegionFilterSet) +class RegionFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Site, lookups=True) -class SiteFilter(filtersets.SiteFilterSet): - id: auto +@autotype_decorator(filtersets.SiteFilterSet) +class SiteFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.SiteGroup, lookups=True) -class SiteGroupFilter(filtersets.SiteGroupFilterSet): - id: auto +@autotype_decorator(filtersets.SiteGroupFilterSet) +class SiteGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VirtualChassis, lookups=True) -class VirtualChassisFilter(filtersets.VirtualChassisFilterSet): - id: auto +@autotype_decorator(filtersets.VirtualChassisFilterSet) +class VirtualChassisFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VirtualDeviceContext, lookups=True) -class VirtualDeviceContextFilter(filtersets.VirtualDeviceContextFilterSet): - id: auto +@autotype_decorator(filtersets.VirtualDeviceContextFilterSet) +class VirtualDeviceContextFilter(BaseFilterMixin): + pass diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index 568ec6ded..c20509fa3 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -1,7 +1,8 @@ import strawberry import strawberry_django -from strawberry import auto -from extras import models, filtersets +from extras import filtersets, models + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'ConfigContextFilter', @@ -21,136 +22,78 @@ __all__ = ( @strawberry_django.filter(models.ConfigContext, lookups=True) -class ConfigContextFilter(filtersets.ConfigContextFilterSet): - id: auto - name: auto - is_active: auto - data_synced: auto +@autotype_decorator(filtersets.ConfigContextFilterSet) +class ConfigContextFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ConfigTemplate, lookups=True) -class ConfigTemplateFilter(filtersets.ConfigTemplateFilterSet): - id: auto - name: auto - description: auto - data_synced: auto +@autotype_decorator(filtersets.ConfigTemplateFilterSet) +class ConfigTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.CustomField, lookups=True) -class CustomFieldFilter(filtersets.CustomFieldFilterSet): - id: auto - object_types: auto - name: auto - group_name: auto - required: auto - search_weight: auto - filter_logic: auto - weight: auto - is_cloneable: auto - description: auto +@autotype_decorator(filtersets.CustomFieldFilterSet) +class CustomFieldFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True) -class CustomFieldChoiceSetFilter(filtersets.CustomFieldChoiceSetFilterSet): - id: auto - name: auto - description: auto - base_choices: auto - order_alphabetically: auto +@autotype_decorator(filtersets.CustomFieldChoiceSetFilterSet) +class CustomFieldChoiceSetFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.CustomLink, lookups=True) -class CustomLinkFilter(filtersets.CustomLinkFilterSet): - id: auto - object_types: auto - name: auto - enabled: auto - link_text: auto - link_url: auto - weight: auto - group_name: auto - new_window: auto +@autotype_decorator(filtersets.CustomLinkFilterSet) +class CustomLinkFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ExportTemplate, lookups=True) -class ExportTemplateFilter(filtersets.ExportTemplateFilterSet): - id: auto - object_types: auto - name: auto - description: auto - data_synced: auto +@autotype_decorator(filtersets.ExportTemplateFilterSet) +class ExportTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ImageAttachment, lookups=True) -class ImageAttachmentFilter(filtersets.ImageAttachmentFilterSet): - id: auto - object_type_id: auto - object_id: auto - name: auto +@autotype_decorator(filtersets.ImageAttachmentFilterSet) +class ImageAttachmentFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.JournalEntry, lookups=True) -class JournalEntryFilter(filtersets.JournalEntryFilterSet): - id: auto - assigned_object_type_id: auto - assigned_object_id: auto - created: auto - kind: auto +@autotype_decorator(filtersets.JournalEntryFilterSet) +class JournalEntryFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ObjectChange, lookups=True) -class ObjectChangeFilter(filtersets.ObjectChangeFilterSet): - id: auto - user: auto - user_name: auto - request_id: auto - action: auto - changed_object_type_id: auto - changed_object_id: auto - object_repr: auto +@autotype_decorator(filtersets.ObjectChangeFilterSet) +class ObjectChangeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.SavedFilter, lookups=True) -class SavedFilterFilter(filtersets.SavedFilterFilterSet): - id: auto - object_types: auto - name: auto - slug: auto - description: auto - enabled: auto - shared: auto - weight: auto +@autotype_decorator(filtersets.SavedFilterFilterSet) +class SavedFilterFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Tag, lookups=True) -class TagFilter(filtersets.TagFilterSet): - id: auto - name: auto - slug: auto - # color: auto - description: auto - object_types: auto +@autotype_decorator(filtersets.TagFilterSet) +class TagFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Webhook, lookups=True) -class WebhookFilter(filtersets.WebhookFilterSet): - id: auto - name: auto - payload_url: auto - http_method: auto - http_content_type: auto - secret: auto - ssl_verification: auto - ca_file_path: auto +@autotype_decorator(filtersets.WebhookFilterSet) +class WebhookFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.EventRule, lookups=True) -class EventRuleFilter(filtersets.EventRuleFilterSet): - id: auto - name: auto - enabled: auto - type_create: auto - type_update: auto - type_delete: auto - type_job_start: auto - type_job_end: auto +@autotype_decorator(filtersets.EventRuleFilterSet) +class EventRuleFilter(BaseFilterMixin): + pass diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 08a9adb93..80d8c8fec 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -1,7 +1,8 @@ import strawberry import strawberry_django -from strawberry import auto -from ipam import models, filtersets +from ipam import filtersets, models + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( @@ -25,80 +26,94 @@ __all__ = ( @strawberry_django.filter(models.ASN, lookups=True) -class ASNFilter(filtersets.ASNFilterSet): - id: auto +class ASNFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ASNRange, lookups=True) -class ASNRangeFilter(filtersets.ASNRangeFilterSet): - id: auto +class ASNRangeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Aggregate, lookups=True) -class AggregateFilter(filtersets.AggregateFilterSet): - id: auto +@autotype_decorator(filtersets.AggregateFilterSet) +class AggregateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.FHRPGroup, lookups=True) -class FHRPGroupFilter(filtersets.FHRPGroupFilterSet): - id: auto +@autotype_decorator(filtersets.FHRPGroupFilterSet) +class FHRPGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.FHRPGroupAssignment, lookups=True) -class FHRPGroupAssignmentFilter(filtersets.FHRPGroupAssignmentFilterSet): - id: auto +@autotype_decorator(filtersets.FHRPGroupAssignmentFilterSet) +class FHRPGroupAssignmentFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IPAddress, lookups=True) -class IPAddressFilter(filtersets.IPAddressFilterSet): - id: auto +@autotype_decorator(filtersets.IPAddressFilterSet) +class IPAddressFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IPRange, lookups=True) -class IPRangeFilter(filtersets.IPRangeFilterSet): - id: auto +@autotype_decorator(filtersets.IPRangeFilterSet) +class IPRangeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Prefix, lookups=True) -class PrefixFilter(filtersets.PrefixFilterSet): - id: auto +@autotype_decorator(filtersets.PrefixFilterSet) +class PrefixFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RIR, lookups=True) -class RIRFilter(filtersets.RIRFilterSet): - id: auto +@autotype_decorator(filtersets.RIRFilterSet) +class RIRFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Role, lookups=True) -class RoleFilter(filtersets.RoleFilterSet): - id: auto +@autotype_decorator(filtersets.RoleFilterSet) +class RoleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.RouteTarget, lookups=True) -class RouteTargetFilter(filtersets.RouteTargetFilterSet): - id: auto +@autotype_decorator(filtersets.RouteTargetFilterSet) +class RouteTargetFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Service, lookups=True) -class ServiceFilter(filtersets.ServiceFilterSet): - id: auto +@autotype_decorator(filtersets.ServiceFilterSet) +class ServiceFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ServiceTemplate, lookups=True) -class ServiceTemplateFilter(filtersets.ServiceTemplateFilterSet): - id: auto +@autotype_decorator(filtersets.ServiceTemplateFilterSet) +class ServiceTemplateFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VLAN, lookups=True) -class VLANFilter(filtersets.VLANFilterSet): - id: auto +@autotype_decorator(filtersets.VLANFilterSet) +class VLANFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VLANGroup, lookups=True) -class VLANGroupFilter(filtersets.VLANGroupFilterSet): - id: auto +@autotype_decorator(filtersets.VLANGroupFilterSet) +class VLANGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VRF, lookups=True) -class VRFFilter(filtersets.VRFFilterSet): - id: auto +@autotype_decorator(filtersets.VRFFilterSet) +class VRFFilter(BaseFilterMixin): + pass diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index af2f3e049..aba4ff791 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -1,12 +1,180 @@ +from functools import partial, partialmethod, wraps from typing import List + +import django_filters import strawberry import strawberry_django from strawberry import auto +from utilities.fields import ColorField +from utilities.filters import * + + +def autotype_decorator(filterset): + + def wrapper(cls): + print(f"cls: {cls}") + cls.filterset = filterset + fields = filterset.get_fields() + model = filterset._meta.model + for fieldname in fields.keys(): + attr_type = auto + if fieldname not in cls.__annotations__: + field = model._meta.get_field(fieldname) + if isinstance(field, ColorField): + attr_type = List[str] | None + + cls.__annotations__[fieldname] = attr_type + + declared_filters = filterset.declared_filters + for fieldname, v in declared_filters.items(): + create_function = False + attr_type = None + + # NetBox Filter types - put base classes after derived classes + if isinstance(v, ContentTypeFilter): + create_function = True + attr_type = str | None + elif isinstance(v, MACAddressFilter): + print(f"{fieldname}: {v}") + print("MACAddressFilter") + elif isinstance(v, MultiValueArrayFilter): + print(f"{fieldname}: {v}") + print("MultiValueArrayFilter") + elif isinstance(v, MultiValueCharFilter): + create_function = True + attr_type = List[str] | None + elif isinstance(v, MultiValueDateFilter): + attr_type = auto + elif isinstance(v, MultiValueDateTimeFilter): + attr_type = auto + elif isinstance(v, MultiValueDecimalFilter): + print(f"{fieldname}: {v}") + print("MultiValueDecimalFilter") + elif isinstance(v, MultiValueMACAddressFilter): + create_function = True + attr_type = List[str] | None + elif isinstance(v, MultiValueNumberFilter): + create_function = True + attr_type = List[str] | None + elif isinstance(v, MultiValueTimeFilter): + print(f"{fieldname}: {v}") + print("MultiValueTimeFilter") + elif isinstance(v, MultiValueWWNFilter): + create_function = True + attr_type = List[str] | None + elif isinstance(v, NullableCharFieldFilter): + print(f"{fieldname}: {v}") + print("NullableCharFieldFilter") + elif isinstance(v, NumericArrayFilter): + print(f"{fieldname}: {v}") + print("NumericArrayFilter") + elif isinstance(v, TreeNodeMultipleChoiceFilter): + 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 + elif issubclass(type(v), django_filters.OrderingFilter): + print(f"{fieldname}: {v}") + print("OrderingFilter") + elif issubclass(type(v), django_filters.BaseRangeFilter): + print(f"{fieldname}: {v}") + print("BaseRangeFilter") + elif issubclass(type(v), django_filters.BaseInFilter): + print(f"{fieldname}: {v}") + print("BaseInFilter") + elif issubclass(type(v), django_filters.LookupChoiceFilter): + print(f"{fieldname}: {v}") + print("LookupChoiceFilter") + elif issubclass(type(v), django_filters.AllValuesMultipleFilter): + print(f"{fieldname}: {v}") + print("AllValuesMultipleFilter") + elif issubclass(type(v), django_filters.AllValuesFilter): + print(f"{fieldname}: {v}") + print("AllValuesFilter") + elif issubclass(type(v), django_filters.TimeRangeFilter): + print(f"{fieldname}: {v}") + print("TimeRangeFilter") + elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.DateTimeFromToRangeFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.DateFromToRangeFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.DateRangeFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.RangeFilter): + print(f"{fieldname}: {v}") + print("RangeFilter") + elif issubclass(type(v), django_filters.NumericRangeFilter): + print(f"{fieldname}: {v}") + print("NumericRangeFilter") + elif issubclass(type(v), django_filters.NumberFilter): + print(f"{fieldname}: {v}") + print("NumberFilter") + elif issubclass(type(v), django_filters.ModelMultipleChoiceFilter): + create_function = True + attr_type = List[str] | None + elif issubclass(type(v), django_filters.ModelChoiceFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.DurationFilter): + print(f"{fieldname}: {v}") + print("DurationFilter") + elif issubclass(type(v), django_filters.IsoDateTimeFilter): + print(f"{fieldname}: {v}") + print("IsoDateTimeFilter") + elif issubclass(type(v), django_filters.DateTimeFilter): + attr_type = auto + elif issubclass(type(v), django_filters.TimeFilter): + attr_type = auto + elif issubclass(type(v), django_filters.DateFilter): + attr_type = auto + elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): + print(f"{fieldname}: {v}") + print("TypedMultipleChoiceFilter") + elif issubclass(type(v), django_filters.MultipleChoiceFilter): + create_function = True + attr_type = List[str] | None + elif issubclass(type(v), django_filters.TypedChoiceFilter): + print(f"{fieldname}: {v}") + print("TypedChoiceFilter") + elif issubclass(type(v), django_filters.ChoiceFilter): + print(f"{fieldname}: {v}") + print("ChoiceFilter") + elif issubclass(type(v), django_filters.BooleanFilter): + create_function = True + attr_type = bool | None + elif issubclass(type(v), django_filters.UUIDFilter): + create_function = True + attr_type = str | None + elif issubclass(type(v), django_filters.CharFilter): + # looks like only used by 'q' + create_function = True + attr_type = str | None + else: + print(f"{fieldname}: {v}") + print("unknown type!") + + if fieldname not in cls.__annotations__ and attr_type: + cls.__annotations__[fieldname] = attr_type + + fname = f"filter_{fieldname}" + if create_function and not hasattr(cls, fname): + filter_by_filterset = getattr(cls, 'filter_by_filterset') + setattr(cls, fname, partialmethod(filter_by_filterset, key=fieldname)) + + return cls + + return wrapper @strawberry.input class BaseFilterMixin: - id: auto def filter_by_filterset(self, queryset, key): return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs From 21b585e5e33bfca2661c305938e8b3d79fbba14f Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 10:39:08 -0700 Subject: [PATCH 077/156] 9856 filter types --- netbox/netbox/graphql/filter_mixins.py | 104 ++++++----------------- netbox/tenancy/graphql/filters.py | 32 ++++--- netbox/users/graphql/filters.py | 26 +++--- netbox/virtualization/graphql/filters.py | 35 +++++--- netbox/vpn/graphql/filters.py | 53 +++++++----- netbox/wireless/graphql/filters.py | 18 ++-- 6 files changed, 118 insertions(+), 150 deletions(-) diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index aba4ff791..2ab0850d4 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -11,8 +11,11 @@ from utilities.filters import * def autotype_decorator(filterset): - def wrapper(cls): + def show_field(fieldname, v, cls): print(f"cls: {cls}") + print(f"{fieldname}: {v}") + + def wrapper(cls): cls.filterset = filterset fields = filterset.get_fields() model = filterset._meta.model @@ -35,11 +38,9 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif isinstance(v, MACAddressFilter): - print(f"{fieldname}: {v}") - print("MACAddressFilter") + show_field("MACAddressFilter", v, cls) elif isinstance(v, MultiValueArrayFilter): - print(f"{fieldname}: {v}") - print("MultiValueArrayFilter") + show_field("MultiValueArrayFilter", v, cls) elif isinstance(v, MultiValueCharFilter): create_function = True attr_type = List[str] | None @@ -48,8 +49,7 @@ def autotype_decorator(filterset): elif isinstance(v, MultiValueDateTimeFilter): attr_type = auto elif isinstance(v, MultiValueDecimalFilter): - print(f"{fieldname}: {v}") - print("MultiValueDecimalFilter") + show_field("MultiValueDecimalFilter", v, cls) elif isinstance(v, MultiValueMACAddressFilter): create_function = True attr_type = List[str] | None @@ -57,17 +57,14 @@ def autotype_decorator(filterset): create_function = True attr_type = List[str] | None elif isinstance(v, MultiValueTimeFilter): - print(f"{fieldname}: {v}") - print("MultiValueTimeFilter") + show_field("MultiValueTimeFilter", v, cls) elif isinstance(v, MultiValueWWNFilter): create_function = True attr_type = List[str] | None elif isinstance(v, NullableCharFieldFilter): - print(f"{fieldname}: {v}") - print("NullableCharFieldFilter") + show_field("NullableCharFieldFilter", v, cls) elif isinstance(v, NumericArrayFilter): - print(f"{fieldname}: {v}") - print("NumericArrayFilter") + show_field("NumericArrayFilter", v, cls) elif isinstance(v, TreeNodeMultipleChoiceFilter): create_function = True attr_type = List[str] | None @@ -75,26 +72,19 @@ def autotype_decorator(filterset): # From django_filters - ordering of these matters as base classes must # come after derived classes so the base class doesn't get matched first elif issubclass(type(v), django_filters.OrderingFilter): - print(f"{fieldname}: {v}") - print("OrderingFilter") + show_field("OrderingFilter", v, cls) elif issubclass(type(v), django_filters.BaseRangeFilter): - print(f"{fieldname}: {v}") - print("BaseRangeFilter") + show_field("BaseRangeFilter", v, cls) elif issubclass(type(v), django_filters.BaseInFilter): - print(f"{fieldname}: {v}") - print("BaseInFilter") + show_field("BaseInFilter", v, cls) elif issubclass(type(v), django_filters.LookupChoiceFilter): - print(f"{fieldname}: {v}") - print("LookupChoiceFilter") + show_field("LookupChoiceFilter", v, cls) elif issubclass(type(v), django_filters.AllValuesMultipleFilter): - print(f"{fieldname}: {v}") - print("AllValuesMultipleFilter") + show_field("AllValuesMultipleFilter", v, cls) elif issubclass(type(v), django_filters.AllValuesFilter): - print(f"{fieldname}: {v}") - print("AllValuesFilter") + show_field("AllValuesFilter", v, cls) elif issubclass(type(v), django_filters.TimeRangeFilter): - print(f"{fieldname}: {v}") - print("TimeRangeFilter") + show_field("TimeRangeFilter", v, cls) elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): create_function = True attr_type = str | None @@ -108,14 +98,11 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif issubclass(type(v), django_filters.RangeFilter): - print(f"{fieldname}: {v}") - print("RangeFilter") + show_field("RangeFilter", v, cls) elif issubclass(type(v), django_filters.NumericRangeFilter): - print(f"{fieldname}: {v}") - print("NumericRangeFilter") + show_field("NumericRangeFilter", v, cls) elif issubclass(type(v), django_filters.NumberFilter): - print(f"{fieldname}: {v}") - print("NumberFilter") + show_field("NumberFilter", v, cls) elif issubclass(type(v), django_filters.ModelMultipleChoiceFilter): create_function = True attr_type = List[str] | None @@ -123,11 +110,9 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif issubclass(type(v), django_filters.DurationFilter): - print(f"{fieldname}: {v}") - print("DurationFilter") + show_field("DurationFilter", v, cls) elif issubclass(type(v), django_filters.IsoDateTimeFilter): - print(f"{fieldname}: {v}") - print("IsoDateTimeFilter") + show_field("IsoDateTimeFilter", v, cls) elif issubclass(type(v), django_filters.DateTimeFilter): attr_type = auto elif issubclass(type(v), django_filters.TimeFilter): @@ -135,17 +120,14 @@ def autotype_decorator(filterset): elif issubclass(type(v), django_filters.DateFilter): attr_type = auto elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): - print(f"{fieldname}: {v}") - print("TypedMultipleChoiceFilter") + show_field("TypedMultipleChoiceFilter", v, cls) elif issubclass(type(v), django_filters.MultipleChoiceFilter): create_function = True attr_type = List[str] | None elif issubclass(type(v), django_filters.TypedChoiceFilter): - print(f"{fieldname}: {v}") - print("TypedChoiceFilter") + show_field("TypedChoiceFilter", v, cls) elif issubclass(type(v), django_filters.ChoiceFilter): - print(f"{fieldname}: {v}") - print("ChoiceFilter") + show_field("ChoiceFilter", v, cls) elif issubclass(type(v), django_filters.BooleanFilter): create_function = True attr_type = bool | None @@ -157,8 +139,7 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None else: - print(f"{fieldname}: {v}") - print("unknown type!") + show_field("unknown type!", v, cls) if fieldname not in cls.__annotations__ and attr_type: cls.__annotations__[fieldname] = attr_type @@ -178,36 +159,3 @@ class BaseFilterMixin: def filter_by_filterset(self, queryset, key): return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs - - -@strawberry.input -class ChangeLoggedModelFilterMixin(BaseFilterMixin): - created: auto - last_updated: auto - created_by_request: str | None - updated_by_request: str | None - modified_by_request: str | None - - def filter_created_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'created_by_request') - - def filter_updated_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'updated_by_request') - - def filter_modified_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'modified_by_request') - - -@strawberry.input -class NetBoxModelFilterMixin(ChangeLoggedModelFilterMixin): - q: str | None - tag: List[str] | None - - def filter_q(self, queryset): - # return self.search(queryset, None, self.q) - return self.filter_by_filterset(queryset, 'q') - - def filter_tag(self, queryset, info): - # return self.filterset(data={'tag': self.tag}, queryset=queryset).qs - # return self.filterset(data={'tag': getattr(self, 'tag')}, queryset=queryset).qs - return self.filter_by_filterset(queryset, 'tag') diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index 9e646b1f8..1df2e3578 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -1,9 +1,8 @@ import strawberry import strawberry_django -from strawberry import auto from tenancy import filtersets, models -from netbox.graphql import filter_mixins +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'TenantFilter', @@ -16,30 +15,35 @@ __all__ = ( @strawberry_django.filter(models.Tenant, lookups=True) -class TenantFilter(filtersets.TenantFilterSet): - id: auto +@autotype_decorator(filtersets.TenantFilterSet) +class TenantFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.TenantGroup, lookups=True) -class TenantGroupFilter(filtersets.TenantGroupFilterSet): - id: auto +@autotype_decorator(filtersets.TenantGroupFilterSet) +class TenantGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Contact, lookups=True) -class ContactFilter(filtersets.ContactFilterSet): - id: auto +@autotype_decorator(filtersets.ContactFilterSet) +class ContactFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ContactRole, lookups=True) -class ContactRoleFilter(filtersets.ContactRoleFilterSet): - id: auto +@autotype_decorator(filtersets.ContactRoleFilterSet) +class ContactRoleFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ContactGroup, lookups=True) -class ContactGroupFilter(filtersets.ContactGroupFilterSet): - id: auto +@autotype_decorator(filtersets.ContactGroupFilterSet) +class ContactGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ContactAssignment, lookups=True) -class ContactAssignmentFilter(filtersets.ContactAssignmentFilterSet): - id: auto +class ContactAssignmentFilter(BaseFilterMixin): + pass diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index 392a56850..eb6c20203 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -1,9 +1,9 @@ import strawberry import strawberry_django from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from strawberry import auto -from users import filtersets +from users import filtersets, models + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( 'GroupFilter', @@ -11,19 +11,13 @@ __all__ = ( ) -@strawberry_django.filter(Group, lookups=True) -class GroupFilter(filtersets.GroupFilterSet): - id: auto - name: auto +@strawberry_django.filter(models.Group, lookups=True) +@autotype_decorator(filtersets.GroupFilterSet) +class GroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(get_user_model(), lookups=True) -class UserFilter(filtersets.UserFilterSet): - id: auto - username: auto - first_name: auto - last_name: auto - email: auto - is_staff: auto - is_active: auto - is_superuser: auto +@autotype_decorator(filtersets.UserFilterSet) +class UserFilter(BaseFilterMixin): + pass diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py index 22fb4d226..4ad9178cb 100644 --- a/netbox/virtualization/graphql/filters.py +++ b/netbox/virtualization/graphql/filters.py @@ -1,7 +1,8 @@ import strawberry import strawberry_django -from strawberry import auto -from virtualization import models, filtersets +from virtualization import filtersets, models + +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( @@ -15,30 +16,36 @@ __all__ = ( @strawberry_django.filter(models.Cluster, lookups=True) -class ClusterFilter(filtersets.ClusterFilterSet): - id: auto +@autotype_decorator(filtersets.ClusterFilterSet) +class ClusterFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ClusterGroup, lookups=True) -class ClusterGroupFilter(filtersets.ClusterGroupFilterSet): - id: auto +@autotype_decorator(filtersets.ClusterGroupFilterSet) +class ClusterGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.ClusterType, lookups=True) -class ClusterTypeFilter(filtersets.ClusterTypeFilterSet): - id: auto +@autotype_decorator(filtersets.ClusterTypeFilterSet) +class ClusterTypeFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VirtualMachine, lookups=True) -class VirtualMachineFilter(filtersets.VirtualMachineFilterSet): - id: auto +@autotype_decorator(filtersets.VirtualMachineFilterSet) +class VirtualMachineFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VMInterface, lookups=True) -class VMInterfaceFilter(filtersets.VMInterfaceFilterSet): - id: auto +@autotype_decorator(filtersets.VMInterfaceFilterSet) +class VMInterfaceFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.VirtualDisk, lookups=True) -class VirtualDiskFilter(filtersets.VirtualDiskFilterSet): - id: auto +@autotype_decorator(filtersets.VirtualDiskFilterSet) +class VirtualDiskFilter(BaseFilterMixin): + pass diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py index 2b347a1c1..ce80bbfb8 100644 --- a/netbox/vpn/graphql/filters.py +++ b/netbox/vpn/graphql/filters.py @@ -1,8 +1,9 @@ import strawberry import strawberry_django -from strawberry import auto from vpn import filtersets, models +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin + __all__ = ( 'TunnelGroupFilter', 'TunnelTerminationFilter', @@ -18,50 +19,60 @@ __all__ = ( @strawberry_django.filter(models.TunnelGroup, lookups=True) -class TunnelGroupFilter(filtersets.TunnelGroupFilterSet): - id: auto +@autotype_decorator(filtersets.TunnelGroupFilterSet) +class TunnelGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.TunnelTermination, lookups=True) -class TunnelTerminationFilter(filtersets.TunnelTerminationFilterSet): - id: auto +@autotype_decorator(filtersets.TunnelTerminationFilterSet) +class TunnelTerminationFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.Tunnel, lookups=True) -class TunnelFilter(filtersets.TunnelFilterSet): - id: auto +@autotype_decorator(filtersets.TunnelFilterSet) +class TunnelFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IKEProposal, lookups=True) -class IKEProposalFilter(filtersets.IKEProposalFilterSet): - id: auto +@autotype_decorator(filtersets.IKEProposalFilterSet) +class IKEProposalFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IKEPolicy, lookups=True) -class IKEPolicyFilter(filtersets.IKEPolicyFilterSet): - id: auto +@autotype_decorator(filtersets.IKEPolicyFilterSet) +class IKEPolicyFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IPSecProposal, lookups=True) -class IPSecProposalFilter(filtersets.IPSecProposalFilterSet): - id: auto +@autotype_decorator(filtersets.IPSecProposalFilterSet) +class IPSecProposalFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IPSecPolicy, lookups=True) -class IPSecPolicyFilter(filtersets.IPSecPolicyFilterSet): - id: auto +@autotype_decorator(filtersets.IPSecPolicyFilterSet) +class IPSecPolicyFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.IPSecProfile, lookups=True) -class IPSecProfileFilter(filtersets.IPSecProfileFilterSet): - id: auto +@autotype_decorator(filtersets.IPSecProfileFilterSet) +class IPSecProfileFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.L2VPN, lookups=True) -class L2VPNFilter(filtersets.L2VPNFilterSet): - id: auto +@autotype_decorator(filtersets.L2VPNFilterSet) +class L2VPNFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.L2VPNTermination, lookups=True) -class L2VPNTerminationFilter(filtersets.L2VPNTerminationFilterSet): - id: auto +@autotype_decorator(filtersets.L2VPNTerminationFilterSet) +class L2VPNTerminationFilter(BaseFilterMixin): + pass diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py index 117c4823b..f84ff9aa3 100644 --- a/netbox/wireless/graphql/filters.py +++ b/netbox/wireless/graphql/filters.py @@ -1,8 +1,9 @@ import strawberry import strawberry_django -from strawberry import auto from wireless import filtersets, models +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin + __all__ = ( 'WirelessLANGroupFilter', 'WirelessLANFilter', @@ -11,15 +12,18 @@ __all__ = ( @strawberry_django.filter(models.WirelessLANGroup, lookups=True) -class WirelessLANGroupFilter(filtersets.WirelessLANGroupFilterSet): - id: auto +@autotype_decorator(filtersets.WirelessLANGroupFilterSet) +class WirelessLANGroupFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.WirelessLAN, lookups=True) -class WirelessLANFilter(filtersets.WirelessLANFilterSet): - id: auto +@autotype_decorator(filtersets.WirelessLANFilterSet) +class WirelessLANFilter(BaseFilterMixin): + pass @strawberry_django.filter(models.WirelessLink, lookups=True) -class WirelessLinkFilter(filtersets.WirelessLinkFilterSet): - id: auto +@autotype_decorator(filtersets.WirelessLinkFilterSet) +class WirelessLinkFilter(BaseFilterMixin): + pass From 347e453b7d427e281e2d35c081c4aef6dbe61c8d Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 10:48:46 -0700 Subject: [PATCH 078/156] 9856 filter types --- netbox/ipam/graphql/filters.py | 2 + netbox/netbox/graphql/filter_mixins.py | 50 +++++++++++++------------ netbox/tenancy/graphql/filter_mixins.py | 48 ------------------------ netbox/tenancy/graphql/filters.py | 1 + 4 files changed, 30 insertions(+), 71 deletions(-) delete mode 100644 netbox/tenancy/graphql/filter_mixins.py diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 80d8c8fec..6031cfe07 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -25,11 +25,13 @@ __all__ = ( ) +# bug - fixme! @strawberry_django.filter(models.ASN, lookups=True) class ASNFilter(BaseFilterMixin): pass +# bug - fixme! @strawberry_django.filter(models.ASNRange, lookups=True) class ASNRangeFilter(BaseFilterMixin): pass diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 2ab0850d4..262dcdf65 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -11,9 +11,11 @@ from utilities.filters import * def autotype_decorator(filterset): - def show_field(fieldname, v, cls): + def show_field(field_type, fieldname, v, cls): print(f"cls: {cls}") print(f"{fieldname}: {v}") + print(field_type) + print("") def wrapper(cls): cls.filterset = filterset @@ -38,9 +40,9 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif isinstance(v, MACAddressFilter): - show_field("MACAddressFilter", v, cls) + show_field("MACAddressFilter", fieldname, v, cls) elif isinstance(v, MultiValueArrayFilter): - show_field("MultiValueArrayFilter", v, cls) + show_field("MultiValueArrayFilter", fieldname, v, cls) elif isinstance(v, MultiValueCharFilter): create_function = True attr_type = List[str] | None @@ -49,7 +51,7 @@ def autotype_decorator(filterset): elif isinstance(v, MultiValueDateTimeFilter): attr_type = auto elif isinstance(v, MultiValueDecimalFilter): - show_field("MultiValueDecimalFilter", v, cls) + show_field("MultiValueDecimalFilter", fieldname, v, cls) elif isinstance(v, MultiValueMACAddressFilter): create_function = True attr_type = List[str] | None @@ -57,14 +59,15 @@ def autotype_decorator(filterset): create_function = True attr_type = List[str] | None elif isinstance(v, MultiValueTimeFilter): - show_field("MultiValueTimeFilter", v, cls) + show_field("MultiValueTimeFilter", fieldname, v, cls) elif isinstance(v, MultiValueWWNFilter): create_function = True attr_type = List[str] | None elif isinstance(v, NullableCharFieldFilter): - show_field("NullableCharFieldFilter", v, cls) + show_field("NullableCharFieldFilter", fieldname, v, cls) elif isinstance(v, NumericArrayFilter): - show_field("NumericArrayFilter", v, cls) + create_function = True + attr_type = int elif isinstance(v, TreeNodeMultipleChoiceFilter): create_function = True attr_type = List[str] | None @@ -72,19 +75,19 @@ def autotype_decorator(filterset): # From django_filters - ordering of these matters as base classes must # come after derived classes so the base class doesn't get matched first elif issubclass(type(v), django_filters.OrderingFilter): - show_field("OrderingFilter", v, cls) + show_field("OrderingFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.BaseRangeFilter): - show_field("BaseRangeFilter", v, cls) + show_field("BaseRangeFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.BaseInFilter): - show_field("BaseInFilter", v, cls) + show_field("BaseInFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.LookupChoiceFilter): - show_field("LookupChoiceFilter", v, cls) + show_field("LookupChoiceFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.AllValuesMultipleFilter): - show_field("AllValuesMultipleFilter", v, cls) + show_field("AllValuesMultipleFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.AllValuesFilter): - show_field("AllValuesFilter", v, cls) + show_field("AllValuesFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.TimeRangeFilter): - show_field("TimeRangeFilter", v, cls) + show_field("TimeRangeFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): create_function = True attr_type = str | None @@ -98,11 +101,12 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif issubclass(type(v), django_filters.RangeFilter): - show_field("RangeFilter", v, cls) + show_field("RangeFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.NumericRangeFilter): - show_field("NumericRangeFilter", v, cls) + show_field("NumericRangeFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.NumberFilter): - show_field("NumberFilter", v, cls) + create_function = True + attr_type = int elif issubclass(type(v), django_filters.ModelMultipleChoiceFilter): create_function = True attr_type = List[str] | None @@ -110,9 +114,9 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None elif issubclass(type(v), django_filters.DurationFilter): - show_field("DurationFilter", v, cls) + show_field("DurationFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.IsoDateTimeFilter): - show_field("IsoDateTimeFilter", v, cls) + show_field("IsoDateTimeFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.DateTimeFilter): attr_type = auto elif issubclass(type(v), django_filters.TimeFilter): @@ -120,14 +124,14 @@ def autotype_decorator(filterset): elif issubclass(type(v), django_filters.DateFilter): attr_type = auto elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): - show_field("TypedMultipleChoiceFilter", v, cls) + show_field("TypedMultipleChoiceFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.MultipleChoiceFilter): create_function = True attr_type = List[str] | None elif issubclass(type(v), django_filters.TypedChoiceFilter): - show_field("TypedChoiceFilter", v, cls) + show_field("TypedChoiceFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.ChoiceFilter): - show_field("ChoiceFilter", v, cls) + show_field("ChoiceFilter", fieldname, v, cls) elif issubclass(type(v), django_filters.BooleanFilter): create_function = True attr_type = bool | None @@ -139,7 +143,7 @@ def autotype_decorator(filterset): create_function = True attr_type = str | None else: - show_field("unknown type!", v, cls) + show_field("unknown type!", fieldname, v, cls) if fieldname not in cls.__annotations__ and attr_type: cls.__annotations__[fieldname] = attr_type diff --git a/netbox/tenancy/graphql/filter_mixins.py b/netbox/tenancy/graphql/filter_mixins.py deleted file mode 100644 index c33dc24aa..000000000 --- a/netbox/tenancy/graphql/filter_mixins.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import List -import strawberry -import strawberry_django -from strawberry import auto -from netbox.graphql.filter_mixins import BaseFilterMixin - -__all__ = ( - 'ContactModelFilterMixin', - 'TenancyFilterMixin', -) - - -@strawberry.input -class TenancyFilterMixin(BaseFilterMixin): - created: auto - last_updated: auto - created_by_request: str | None - updated_by_request: str | None - modified_by_request: str | None - - def filter_created_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'created_by_request') - - def filter_updated_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'updated_by_request') - - def filter_modified_by_request(self, queryset): - return self.filter_by_filterset(queryset, 'modified_by_request') - - -@strawberry.input -class ContactModelFilterMixin(BaseFilterMixin): - tenant_group_id: List[str] | None - tenant_group: List[str] | None - tenant_id: List[str] | None - tenant: List[str] | None - - def filter_tenant_group_id(self, queryset): - return self.filter_by_filterset(queryset, 'tenant_group_id') - - def filter_tenant_group(self, queryset): - return self.filter_by_filterset(queryset, 'tenant_group') - - def filter_tenant_id(self, queryset): - return self.filter_by_filterset(queryset, 'tenant_id') - - def filter_tenant(self, queryset): - return self.filter_by_filterset(queryset, 'tenant') diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index 1df2e3578..bb710e7fd 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -44,6 +44,7 @@ class ContactGroupFilter(BaseFilterMixin): pass +# bug - fixme! @strawberry_django.filter(models.ContactAssignment, lookups=True) class ContactAssignmentFilter(BaseFilterMixin): pass From 634f35a9723ca6b2fcf7aa30691e63e2aa1aea49 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 11:19:54 -0700 Subject: [PATCH 079/156] 9856 fix graphiql test --- netbox/netbox/graphql/views.py | 8 +------- netbox/netbox/tests/test_graphql.py | 3 --- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py index b61157403..23827c9c4 100644 --- a/netbox/netbox/graphql/views.py +++ b/netbox/netbox/graphql/views.py @@ -36,12 +36,6 @@ class NetBoxGraphQLView(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 - # bug - todo? - # if self.request_wants_html(request): - # return redirect_to_login(reverse('graphql')) - - return HttpResponseForbidden("No credentials provided.") + return redirect_to_login(reverse('graphql')) return super().dispatch(request, *args, **kwargs) diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index a3f4df782..2cf9ee87b 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -15,8 +15,6 @@ class GraphQLTestCase(TestCase): response = self.client.get(url) self.assertHttpStatus(response, 404) - ''' - BUG TODO - Re-enable @override_settings(LOGIN_REQUIRED=True) def test_graphiql_interface(self): """ @@ -36,4 +34,3 @@ class GraphQLTestCase(TestCase): response = self.client.get(url, **header) with disable_warnings('django.request'): self.assertHttpStatus(response, 302) # Redirect to login page - ''' From 1da5219563aa21b4456ef64a3ea4df996bab9053 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 11:37:13 -0700 Subject: [PATCH 080/156] 9856 fix counter fields and merge feature --- netbox/netbox/graphql/filter_mixins.py | 28 +++++++++++++++++--------- netbox/virtualization/graphql/types.py | 1 + 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 262dcdf65..f50ff23ed 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -5,7 +5,8 @@ import django_filters import strawberry import strawberry_django from strawberry import auto -from utilities.fields import ColorField +from netbox.graphql.scalars import BigInt +from utilities.fields import ColorField, CounterCacheField from utilities.filters import * @@ -17,18 +18,31 @@ def autotype_decorator(filterset): print(field_type) print("") + def create_attribute_and_function(cls, fieldname, attr_type, create_function): + if fieldname not in cls.__annotations__ and attr_type: + cls.__annotations__[fieldname] = attr_type + + fname = f"filter_{fieldname}" + if create_function and not hasattr(cls, fname): + filter_by_filterset = getattr(cls, 'filter_by_filterset') + setattr(cls, fname, 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(): + create_function = False attr_type = auto if fieldname not in cls.__annotations__: field = model._meta.get_field(fieldname) - if isinstance(field, ColorField): + if isinstance(field, CounterCacheField): + create_function = True + attr_type = BigInt + elif isinstance(field, ColorField): attr_type = List[str] | None - cls.__annotations__[fieldname] = attr_type + create_attribute_and_function(cls, fieldname, attr_type, create_function) declared_filters = filterset.declared_filters for fieldname, v in declared_filters.items(): @@ -145,13 +159,7 @@ def autotype_decorator(filterset): else: show_field("unknown type!", fieldname, v, cls) - if fieldname not in cls.__annotations__ and attr_type: - cls.__annotations__[fieldname] = attr_type - - fname = f"filter_{fieldname}" - if create_function and not hasattr(cls, fname): - filter_by_filterset = getattr(cls, 'filter_by_filterset') - setattr(cls, fname, partialmethod(filter_by_filterset, key=fieldname)) + create_attribute_and_function(cls, fieldname, attr_type, create_function) return cls diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 0756ce824..b226d13f3 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -82,6 +82,7 @@ class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): _name: str interface_count: BigInt virtual_disk_count: BigInt + interface_count: BigInt config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None cluster: Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')] | None From 151717545aac1e2eeace8641e61aba61c718c57f Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 11:51:37 -0700 Subject: [PATCH 081/156] 9856 temp fix tests --- netbox/netbox/graphql/views.py | 3 ++- netbox/netbox/tests/test_graphql.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py index 23827c9c4..010cef1d3 100644 --- a/netbox/netbox/graphql/views.py +++ b/netbox/netbox/graphql/views.py @@ -36,6 +36,7 @@ class NetBoxGraphQLView(GraphQLView): # Enforce LOGIN_REQUIRED if settings.LOGIN_REQUIRED and not request.user.is_authenticated: - return redirect_to_login(reverse('graphql')) + # return redirect_to_login(reverse('graphql')) + return HttpResponseForbidden("No credentials provided.") return super().dispatch(request, *args, **kwargs) diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 2cf9ee87b..5bf9c4abb 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -33,4 +33,5 @@ class GraphQLTestCase(TestCase): self.client.logout() response = self.client.get(url, **header) with disable_warnings('django.request'): - self.assertHttpStatus(response, 302) # Redirect to login page + # self.assertHttpStatus(response, 302) # Redirect to login page + self.assertHttpStatus(response, 403) # Redirect to login page From b47c5ee1b84b3e2c7fc6f0551a69d36f1002a655 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 13:13:20 -0700 Subject: [PATCH 082/156] 9856 fix tests --- netbox/netbox/graphql/views.py | 6 ++++-- netbox/netbox/tests/test_graphql.py | 3 +-- netbox/utilities/testing/api.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py index 010cef1d3..85a01f025 100644 --- a/netbox/netbox/graphql/views.py +++ b/netbox/netbox/graphql/views.py @@ -36,7 +36,9 @@ class NetBoxGraphQLView(GraphQLView): # Enforce LOGIN_REQUIRED if settings.LOGIN_REQUIRED and not request.user.is_authenticated: - # return redirect_to_login(reverse('graphql')) - return HttpResponseForbidden("No credentials provided.") + if request.accepts("text/html"): + return redirect_to_login(reverse('graphql')) + else: + return HttpResponseForbidden("No credentials provided.") return super().dispatch(request, *args, **kwargs) diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 5bf9c4abb..2cf9ee87b 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -33,5 +33,4 @@ class GraphQLTestCase(TestCase): self.client.logout() response = self.client.get(url, **header) with disable_warnings('django.request'): - # self.assertHttpStatus(response, 302) # Redirect to login page - self.assertHttpStatus(response, 403) # Redirect to login page + self.assertHttpStatus(response, 302) # Redirect to login page diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 50591a95f..11007f77f 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -499,7 +499,10 @@ class APIViewTestCases: # Non-authenticated requests should fail with disable_warnings('django.request'): - self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json"), status.HTTP_403_FORBIDDEN) + header = { + 'HTTP_ACCEPT': 'application/json', + } + self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json", **header), status.HTTP_403_FORBIDDEN) # Add object-level permission obj_perm = ObjectPermission( @@ -524,7 +527,10 @@ class APIViewTestCases: # Non-authenticated requests should fail with disable_warnings('django.request'): - self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json"), status.HTTP_403_FORBIDDEN) + header = { + 'HTTP_ACCEPT': 'application/json', + } + self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json", **header), status.HTTP_403_FORBIDDEN) # Add object-level permission obj_perm = ObjectPermission( From 9c53f76d9e304e92abef76bf61d2942dc7c097bf Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 13:37:17 -0700 Subject: [PATCH 083/156] 9856 fix tenancy, ipam filter definitions --- netbox/ipam/graphql/filters.py | 3 ++- netbox/netbox/graphql/filter_mixins.py | 5 +++++ netbox/tenancy/filtersets.py | 3 +-- netbox/tenancy/graphql/filters.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 6031cfe07..dcc3ce4bf 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -25,14 +25,15 @@ __all__ = ( ) -# bug - fixme! @strawberry_django.filter(models.ASN, lookups=True) +@autotype_decorator(filtersets.ASNFilterSet) class ASNFilter(BaseFilterMixin): pass # bug - fixme! @strawberry_django.filter(models.ASNRange, lookups=True) +@autotype_decorator(filtersets.ASNRangeFilterSet) class ASNRangeFilter(BaseFilterMixin): pass diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index f50ff23ed..392ea55bb 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -5,6 +5,7 @@ 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 * @@ -39,7 +40,11 @@ def autotype_decorator(filterset): if isinstance(field, CounterCacheField): create_function = True attr_type = BigInt + elif isinstance(field, ASNField): + create_function = True + attr_type = List[str] | None elif isinstance(field, ColorField): + create_function = True attr_type = List[str] | None create_attribute_and_function(cls, fieldname, attr_type, create_function) diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index a7c52d3fb..75096b00e 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -127,11 +127,10 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): to_field_name='slug', label=_('Contact role (slug)'), ) - tag = TagFilter() class Meta: model = ContactAssignment - fields = ('id', 'object_type_id', 'object_id', 'priority', 'tag') + fields = ('id', 'object_type_id', 'object_id', 'priority') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index bb710e7fd..a9e0656ca 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -44,7 +44,7 @@ class ContactGroupFilter(BaseFilterMixin): pass -# bug - fixme! @strawberry_django.filter(models.ContactAssignment, lookups=True) +@autotype_decorator(filtersets.ContactAssignmentFilterSet) class ContactAssignmentFilter(BaseFilterMixin): pass From da0c23bc0cc0ea4e305cfeb4e25b3259ae763a7f Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 14:21:02 -0700 Subject: [PATCH 084/156] 9856 cleanup --- netbox/netbox/graphql/filter_mixins.py | 210 +++++++++++++------------ 1 file changed, 113 insertions(+), 97 deletions(-) diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 392ea55bb..707e3bfee 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -12,19 +12,32 @@ from utilities.filters import * 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: - def show_field(field_type, fieldname, v, cls): - print(f"cls: {cls}") - print(f"{fieldname}: {v}") - print(field_type) - print("") + @strawberry_django.filter(models.Example, lookups=True) + @autotype_decorator(filtersets.ExampleFilterSet) + class ExampleFilter(BaseFilterMixin): + pass - def create_attribute_and_function(cls, fieldname, attr_type, create_function): + 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 fname = f"filter_{fieldname}" - if create_function and not hasattr(cls, fname): + if should_create_function and not hasattr(cls, fname): filter_by_filterset = getattr(cls, 'filter_by_filterset') setattr(cls, fname, partialmethod(filter_by_filterset, key=fieldname)) @@ -33,138 +46,141 @@ def autotype_decorator(filterset): fields = filterset.get_fields() model = filterset._meta.model for fieldname in fields.keys(): - create_function = False + should_create_function = False attr_type = auto if fieldname not in cls.__annotations__: field = model._meta.get_field(fieldname) if isinstance(field, CounterCacheField): - create_function = True - attr_type = BigInt + should_create_function = True + attr_type = BigInt | None elif isinstance(field, ASNField): - create_function = True + should_create_function = True attr_type = List[str] | None elif isinstance(field, ColorField): - create_function = True + should_create_function = True attr_type = List[str] | None - create_attribute_and_function(cls, fieldname, attr_type, create_function) + create_attribute_and_function(cls, fieldname, attr_type, should_create_function) declared_filters = filterset.declared_filters - for fieldname, v in declared_filters.items(): - create_function = False + for fieldname, field in declared_filters.items(): + should_create_function = False attr_type = None # NetBox Filter types - put base classes after derived classes - if isinstance(v, ContentTypeFilter): - create_function = True + if isinstance(field, ContentTypeFilter): + should_create_function = True attr_type = str | None - elif isinstance(v, MACAddressFilter): - show_field("MACAddressFilter", fieldname, v, cls) - elif isinstance(v, MultiValueArrayFilter): - show_field("MultiValueArrayFilter", fieldname, v, cls) - elif isinstance(v, MultiValueCharFilter): - create_function = True + elif isinstance(field, MACAddressFilter): + pass + elif isinstance(field, MultiValueArrayFilter): + pass + elif isinstance(field, MultiValueCharFilter): + should_create_function = True attr_type = List[str] | None - elif isinstance(v, MultiValueDateFilter): + elif isinstance(field, MultiValueDateFilter): attr_type = auto - elif isinstance(v, MultiValueDateTimeFilter): + elif isinstance(field, MultiValueDateTimeFilter): attr_type = auto - elif isinstance(v, MultiValueDecimalFilter): - show_field("MultiValueDecimalFilter", fieldname, v, cls) - elif isinstance(v, MultiValueMACAddressFilter): - create_function = True + elif isinstance(field, MultiValueDecimalFilter): + pass + elif isinstance(field, MultiValueMACAddressFilter): + should_create_function = True attr_type = List[str] | None - elif isinstance(v, MultiValueNumberFilter): - create_function = True + elif isinstance(field, MultiValueNumberFilter): + should_create_function = True attr_type = List[str] | None - elif isinstance(v, MultiValueTimeFilter): - show_field("MultiValueTimeFilter", fieldname, v, cls) - elif isinstance(v, MultiValueWWNFilter): - create_function = True + elif isinstance(field, MultiValueTimeFilter): + pass + elif isinstance(field, MultiValueWWNFilter): + should_create_function = True attr_type = List[str] | None - elif isinstance(v, NullableCharFieldFilter): - show_field("NullableCharFieldFilter", fieldname, v, cls) - elif isinstance(v, NumericArrayFilter): - create_function = True + elif isinstance(field, NullableCharFieldFilter): + pass + elif isinstance(field, NumericArrayFilter): + should_create_function = True attr_type = int - elif isinstance(v, TreeNodeMultipleChoiceFilter): - create_function = True + 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 - elif issubclass(type(v), django_filters.OrderingFilter): - show_field("OrderingFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.BaseRangeFilter): - show_field("BaseRangeFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.BaseInFilter): - show_field("BaseInFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.LookupChoiceFilter): - show_field("LookupChoiceFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.AllValuesMultipleFilter): - show_field("AllValuesMultipleFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.AllValuesFilter): - show_field("AllValuesFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.TimeRangeFilter): - show_field("TimeRangeFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.IsoDateTimeFromToRangeFilter): - create_function = True + # 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(v), django_filters.DateTimeFromToRangeFilter): - create_function = True + elif issubclass(type(field), django_filters.DateTimeFromToRangeFilter): + should_create_function = True attr_type = str | None - elif issubclass(type(v), django_filters.DateFromToRangeFilter): - create_function = True + elif issubclass(type(field), django_filters.DateFromToRangeFilter): + should_create_function = True attr_type = str | None - elif issubclass(type(v), django_filters.DateRangeFilter): - create_function = True + elif issubclass(type(field), django_filters.DateRangeFilter): + should_create_function = True attr_type = str | None - elif issubclass(type(v), django_filters.RangeFilter): - show_field("RangeFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.NumericRangeFilter): - show_field("NumericRangeFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.NumberFilter): - create_function = True + 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(v), django_filters.ModelMultipleChoiceFilter): - create_function = True + elif issubclass(type(field), django_filters.ModelMultipleChoiceFilter): + should_create_function = True attr_type = List[str] | None - elif issubclass(type(v), django_filters.ModelChoiceFilter): - create_function = True + elif issubclass(type(field), django_filters.ModelChoiceFilter): + should_create_function = True attr_type = str | None - elif issubclass(type(v), django_filters.DurationFilter): - show_field("DurationFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.IsoDateTimeFilter): - show_field("IsoDateTimeFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.DateTimeFilter): + 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(v), django_filters.TimeFilter): + elif issubclass(type(field), django_filters.TimeFilter): attr_type = auto - elif issubclass(type(v), django_filters.DateFilter): + elif issubclass(type(field), django_filters.DateFilter): attr_type = auto - elif issubclass(type(v), django_filters.TypedMultipleChoiceFilter): - show_field("TypedMultipleChoiceFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.MultipleChoiceFilter): - create_function = True + 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(v), django_filters.TypedChoiceFilter): - show_field("TypedChoiceFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.ChoiceFilter): - show_field("ChoiceFilter", fieldname, v, cls) - elif issubclass(type(v), django_filters.BooleanFilter): - create_function = True + 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(v), django_filters.UUIDFilter): - create_function = True + elif issubclass(type(field), django_filters.UUIDFilter): + should_create_function = True attr_type = str | None - elif issubclass(type(v), django_filters.CharFilter): + elif issubclass(type(field), django_filters.CharFilter): # looks like only used by 'q' - create_function = True + should_create_function = True attr_type = str | None - else: - show_field("unknown type!", fieldname, v, cls) - create_attribute_and_function(cls, fieldname, attr_type, create_function) + 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 From 6090d41b34433928521be48fd5f85272c5e7a1a0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 14:50:41 -0700 Subject: [PATCH 085/156] 9856 cleanup --- netbox/core/graphql/types.py | 3 +-- netbox/extras/graphql/types.py | 6 +++--- netbox/ipam/graphql/filters.py | 1 - netbox/tenancy/graphql/types.py | 8 ++++---- netbox/wireless/graphql/types.py | 4 ++-- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index aeabf5310..013dcd416 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -15,8 +15,7 @@ __all__ = ( @strawberry_django.type( models.DataFile, - # fields='__all__', - exclude=('data',), # bug - temp + exclude=('data',), filters=DataFileFilter ) class DataFileType(BaseObjectType): diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index f6c22c30d..efb2551ac 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -129,11 +129,11 @@ class CustomFieldType(ObjectType): @strawberry_django.type( models.CustomFieldChoiceSet, - # fields='__all__', - exclude=('extra_choices', ), # bug - temp + fields='__all__', filters=CustomFieldChoiceSetFilter ) class CustomFieldChoiceSetType(ObjectType): + extra_choices: List[List[str]] @strawberry_django.field def choices_for(self) -> List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]: @@ -202,7 +202,7 @@ class SavedFilterType(ObjectType): @strawberry_django.type( models.Tag, - exclude=['extras_taggeditem_items', 'color'], # bug - remove color from exclude + exclude=['extras_taggeditem_items', ], filters=TagFilter ) class TagType(ObjectType): diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index dcc3ce4bf..a47f5adf5 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -31,7 +31,6 @@ class ASNFilter(BaseFilterMixin): pass -# bug - fixme! @strawberry_django.filter(models.ASNRange, lookups=True) @autotype_decorator(filtersets.ASNRangeFilterSet) class ASNRangeFilter(BaseFilterMixin): diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 90c35d47e..83655c534 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -137,11 +137,11 @@ class TenantType(NetBoxObjectType): @strawberry_django.type( models.TenantGroup, - # fields='__all__', - exclude=('parent',), # bug - temp + fields='__all__', filters=TenantGroupFilter ) class TenantGroupType(OrganizationalObjectType): + parent: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def tenants(self) -> List[TenantType]: @@ -172,11 +172,11 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): @strawberry_django.type( models.ContactGroup, - # fields='__all__', - exclude=('parent',), # bug - temp + fields='__all__', filters=ContactGroupFilter ) class ContactGroupType(OrganizationalObjectType): + parent: Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')] | None @strawberry_django.field def contacts(self) -> List[ContactType]: diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index babffba83..e333d2bcf 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -16,11 +16,11 @@ __all__ = ( @strawberry_django.type( models.WirelessLANGroup, - # fields='__all__', - exclude=('parent',), # bug - temp + fields='__all__', filters=WirelessLANGroupFilter ) class WirelessLANGroupType(OrganizationalObjectType): + parent: Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')] | None @strawberry_django.field def wireless_lans(self) -> List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]]: From 3b30aa965fabfbab0175e4f663968f77aeb35b01 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Mar 2024 15:21:18 -0700 Subject: [PATCH 086/156] 9856 cleanup --- netbox/extras/graphql/types.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index efb2551ac..c301b26b2 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -129,19 +129,18 @@ class CustomFieldType(ObjectType): @strawberry_django.type( models.CustomFieldChoiceSet, - fields='__all__', + exclude=('extra_choices', ), filters=CustomFieldChoiceSetFilter ) class CustomFieldChoiceSetType(ObjectType): - extra_choices: List[List[str]] @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]: - return self.extra_choices + def extra_choices(self) -> List[str] | None: + return list(self.extra_choices) @strawberry_django.type( From 93c9f8cc041001dd7dc894e842e4063922730a30 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 18 Mar 2024 11:26:53 -0700 Subject: [PATCH 087/156] 15193 use psycopg compiled --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 07add11a2..72b086912 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ mkdocstrings[python-legacy]==0.24.1 netaddr==1.2.1 nh3==0.2.15 Pillow==10.2.0 -psycopg[binary,pool]==3.1.18 +psycopg[c,pool]==3.1.18 PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 From b75b9e01eb588447e2c45e24ab8a60a45bab9585 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 19 Mar 2024 10:04:33 -0700 Subject: [PATCH 088/156] 9856 review changes --- base_requirements.txt | 4 + netbox/circuits/graphql/schema.py | 12 +- netbox/core/graphql/schema.py | 4 +- netbox/core/graphql/types.py | 2 +- netbox/dcim/graphql/schema.py | 54 +++--- netbox/extras/graphql/schema.py | 24 +-- netbox/ipam/graphql/schema.py | 32 ++-- netbox/netbox/graphql/filter_mixins.py | 236 ++++++++++++------------ netbox/tenancy/graphql/schema.py | 12 +- netbox/tenancy/graphql/types.py | 9 +- netbox/users/graphql/schema.py | 4 +- netbox/users/graphql/types.py | 8 +- netbox/utilities/testing/api.py | 8 +- netbox/virtualization/graphql/schema.py | 12 +- netbox/vpn/graphql/schema.py | 20 +- netbox/wireless/graphql/schema.py | 6 +- requirements.txt | 3 +- 17 files changed, 224 insertions(+), 226 deletions(-) diff --git a/base_requirements.txt b/base_requirements.txt index 1d6a2e97d..30c3e3302 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -131,6 +131,10 @@ 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-django diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 9aa7a0228..ac8626cc5 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -11,30 +11,30 @@ from .types import * class CircuitsQuery: @strawberry.field def circuit(self, id: int) -> CircuitType: - return models.Circuit.objects.get(id=id) + return models.Circuit.objects.get(pk=id) circuit_list: List[CircuitType] = strawberry_django.field() @strawberry.field def circuit_termination(self, id: int) -> CircuitTerminationType: - return models.CircuitTermination.objects.get(id=id) + return models.CircuitTermination.objects.get(pk=id) circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field() @strawberry.field def circuit_type(self, id: int) -> CircuitTypeType: - return models.CircuitType.objects.get(id=id) + return models.CircuitType.objects.get(pk=id) circuit_type_list: List[CircuitTypeType] = strawberry_django.field() @strawberry.field def provider(self, id: int) -> ProviderType: - return models.Provider.objects.get(id=id) + return models.Provider.objects.get(pk=id) provider_list: List[ProviderType] = strawberry_django.field() @strawberry.field def provider_account(self, id: int) -> ProviderAccountType: - return models.ProviderAccount.objects.get(id=id) + return models.ProviderAccount.objects.get(pk=id) provider_account_list: List[ProviderAccountType] = strawberry_django.field() @strawberry.field def provider_network(self, id: int) -> ProviderNetworkType: - return models.ProviderNetwork.objects.get(id=id) + return models.ProviderNetwork.objects.get(pk=id) provider_network_list: List[ProviderNetworkType] = strawberry_django.field() diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py index 64ed87985..34135cd47 100644 --- a/netbox/core/graphql/schema.py +++ b/netbox/core/graphql/schema.py @@ -11,10 +11,10 @@ from .types import * class CoreQuery: @strawberry.field def data_file(self, id: int) -> DataFileType: - return models.DataFile.objects.get(id=id) + return models.DataFile.objects.get(pk=id) data_file_list: List[DataFileType] = strawberry_django.field() @strawberry.field def data_source(self, id: int) -> DataSourceType: - return models.DataSource.objects.get(id=id) + return models.DataSource.objects.get(pk=id) data_source_list: List[DataSourceType] = strawberry_django.field() diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index 013dcd416..676c2aeec 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -15,7 +15,7 @@ __all__ = ( @strawberry_django.type( models.DataFile, - exclude=('data',), + exclude=['data',], filters=DataFileFilter ) class DataFileType(BaseObjectType): diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index c8c0ee777..c3962a87a 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -11,137 +11,137 @@ from .types import * class DCIMQuery: @strawberry.field def cable(self, id: int) -> CableType: - return models.Cable.objects.get(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + 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(id=id) + return models.PowerOutletTemplate.objects.get(pk=id) power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field() @strawberry.field diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index a607882b2..f78285035 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -11,60 +11,60 @@ from .types import * class ExtrasQuery: @strawberry.field def config_context(self, id: int) -> ConfigContextType: - return models.ConfigContext.objects.get(id=id) + return models.ConfigContext.objects.get(pk=id) config_context_list: List[ConfigContextType] = strawberry_django.field() @strawberry.field def config_template(self, id: int) -> ConfigTemplateType: - return models.ConfigTemplate.objects.get(id=id) + return models.ConfigTemplate.objects.get(pk=id) config_template_list: List[ConfigTemplateType] = strawberry_django.field() @strawberry.field def custom_field(self, id: int) -> CustomFieldType: - return models.CustomField.objects.get(id=id) + return models.CustomField.objects.get(pk=id) custom_field_list: List[CustomFieldType] = strawberry_django.field() @strawberry.field def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType: - return models.CustomFieldChoiceSet.objects.get(id=id) + return models.CustomFieldChoiceSet.objects.get(pk=id) custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field() @strawberry.field def custom_link(self, id: int) -> CustomLinkType: - return models.CustomLink.objects.get(id=id) + return models.CustomLink.objects.get(pk=id) custom_link_list: List[CustomLinkType] = strawberry_django.field() @strawberry.field def export_template(self, id: int) -> ExportTemplateType: - return models.ExportTemplate.objects.get(id=id) + return models.ExportTemplate.objects.get(pk=id) export_template_list: List[ExportTemplateType] = strawberry_django.field() @strawberry.field def image_attachment(self, id: int) -> ImageAttachmentType: - return models.ImageAttachment.objects.get(id=id) + return models.ImageAttachment.objects.get(pk=id) image_attachment_list: List[ImageAttachmentType] = strawberry_django.field() @strawberry.field def saved_filter(self, id: int) -> SavedFilterType: - return models.SavedFilter.objects.get(id=id) + return models.SavedFilter.objects.get(pk=id) saved_filter_list: List[SavedFilterType] = strawberry_django.field() @strawberry.field def journal_entry(self, id: int) -> JournalEntryType: - return models.JournalEntry.objects.get(id=id) + return models.JournalEntry.objects.get(pk=id) journal_entry_list: List[JournalEntryType] = strawberry_django.field() @strawberry.field def tag(self, id: int) -> TagType: - return models.Tag.objects.get(id=id) + return models.Tag.objects.get(pk=id) tag_list: List[TagType] = strawberry_django.field() @strawberry.field def webhook(self, id: int) -> WebhookType: - return models.Webhook.objects.get(id=id) + return models.Webhook.objects.get(pk=id) webhook_list: List[WebhookType] = strawberry_django.field() @strawberry.field def event_rule(self, id: int) -> EventRuleType: - return models.EventRule.objects.get(id=id) + return models.EventRule.objects.get(pk=id) event_rule_list: List[EventRuleType] = strawberry_django.field() diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index 4a977d07d..c02788c3a 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -11,80 +11,80 @@ from .types import * class IPAMQuery: @strawberry.field def asn(self, id: int) -> ASNType: - return models.ASN.objects.get(id=id) + return models.ASN.objects.get(pk=id) asn_list: List[ASNType] = strawberry_django.field() @strawberry.field def asn_range(self, id: int) -> ASNRangeType: - return models.ASNRange.objects.get(id=id) + return models.ASNRange.objects.get(pk=id) asn_range_list: List[ASNRangeType] = strawberry_django.field() @strawberry.field def aggregate(self, id: int) -> AggregateType: - return models.Aggregate.objects.get(id=id) + return models.Aggregate.objects.get(pk=id) aggregate_list: List[AggregateType] = strawberry_django.field() @strawberry.field def ip_address(self, id: int) -> IPAddressType: - return models.IPAddress.objects.get(id=id) + return models.IPAddress.objects.get(pk=id) ip_address_list: List[IPAddressType] = strawberry_django.field() @strawberry.field def ip_range(self, id: int) -> IPRangeType: - return models.IPRange.objects.get(id=id) + return models.IPRange.objects.get(pk=id) ip_range_list: List[IPRangeType] = strawberry_django.field() @strawberry.field def prefix(self, id: int) -> PrefixType: - return models.Prefix.objects.get(id=id) + return models.Prefix.objects.get(pk=id) prefix_list: List[PrefixType] = strawberry_django.field() @strawberry.field def rir(self, id: int) -> RIRType: - return models.RIR.objects.get(id=id) + return models.RIR.objects.get(pk=id) rir_list: List[RIRType] = strawberry_django.field() @strawberry.field def role(self, id: int) -> RoleType: - return models.Role.objects.get(id=id) + return models.Role.objects.get(pk=id) role_list: List[RoleType] = strawberry_django.field() @strawberry.field def route_target(self, id: int) -> RouteTargetType: - return models.RouteTarget.objects.get(id=id) + return models.RouteTarget.objects.get(pk=id) route_target_list: List[RouteTargetType] = strawberry_django.field() @strawberry.field def service(self, id: int) -> ServiceType: - return models.Service.objects.get(id=id) + return models.Service.objects.get(pk=id) service_list: List[ServiceType] = strawberry_django.field() @strawberry.field def service_template(self, id: int) -> ServiceTemplateType: - return models.ServiceTemplate.objects.get(id=id) + return models.ServiceTemplate.objects.get(pk=id) service_template_list: List[ServiceTemplateType] = strawberry_django.field() @strawberry.field def fhrp_group(self, id: int) -> FHRPGroupType: - return models.FHRPGroup.objects.get(id=id) + return models.FHRPGroup.objects.get(pk=id) fhrp_group_list: List[FHRPGroupType] = strawberry_django.field() @strawberry.field def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType: - return models.FHRPGroupAssignment.objects.get(id=id) + return models.FHRPGroupAssignment.objects.get(pk=id) fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field() @strawberry.field def vlan(self, id: int) -> VLANType: - return models.VLAN.objects.get(id=id) + return models.VLAN.objects.get(pk=id) vlan_list: List[VLANType] = strawberry_django.field() @strawberry.field def vlan_group(self, id: int) -> VLANGroupType: - return models.VLANGroup.objects.get(id=id) + return models.VLANGroup.objects.get(pk=id) vlan_group_list: List[VLANGroupType] = strawberry_django.field() @strawberry.field def vrf(self, id: int) -> VRFType: - return models.VRF.objects.get(id=id) + return models.VRF.objects.get(pk=id) vrf_list: List[VRFType] = strawberry_django.field() diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 707e3bfee..363e4fe84 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -11,6 +11,123 @@ 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, MACAddressFilter): + pass + 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. @@ -36,10 +153,10 @@ def autotype_decorator(filterset): if fieldname not in cls.__annotations__ and attr_type: cls.__annotations__[fieldname] = attr_type - fname = f"filter_{fieldname}" - if should_create_function and not hasattr(cls, fname): + 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, fname, partialmethod(filter_by_filterset, key=fieldname)) + setattr(cls, filter_name, partialmethod(filter_by_filterset, key=fieldname)) def wrapper(cls): cls.filterset = filterset @@ -64,119 +181,8 @@ def autotype_decorator(filterset): declared_filters = filterset.declared_filters for fieldname, field in declared_filters.items(): - 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, MACAddressFilter): - pass - 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 + should_create_function, attr_type = map_strawberry_type(field) if attr_type is None: raise NotImplementedError(f"GraphQL Filter field unknown: {fieldname}: {field}") diff --git a/netbox/tenancy/graphql/schema.py b/netbox/tenancy/graphql/schema.py index f33c4d6c1..79f8660d4 100644 --- a/netbox/tenancy/graphql/schema.py +++ b/netbox/tenancy/graphql/schema.py @@ -11,30 +11,30 @@ from .types import * class TenancyQuery: @strawberry.field def tenant(self, id: int) -> TenantType: - return models.Tenant.objects.get(id=id) + return models.Tenant.objects.get(pk=id) tenant_list: List[TenantType] = strawberry_django.field() @strawberry.field def tenant_group(self, id: int) -> TenantGroupType: - return models.TenantGroup.objects.get(id=id) + return models.TenantGroup.objects.get(pk=id) tenant_group_list: List[TenantGroupType] = strawberry_django.field() @strawberry.field def contact(self, id: int) -> ContactType: - return models.Contact.objects.get(id=id) + return models.Contact.objects.get(pk=id) contact_list: List[ContactType] = strawberry_django.field() @strawberry.field def contact_role(self, id: int) -> ContactRoleType: - return models.ContactRole.objects.get(id=id) + return models.ContactRole.objects.get(pk=id) contact_role_list: List[ContactRoleType] = strawberry_django.field() @strawberry.field def contact_group(self, id: int) -> ContactGroupType: - return models.ContactGroup.objects.get(id=id) + return models.ContactGroup.objects.get(pk=id) contact_group_list: List[ContactGroupType] = strawberry_django.field() @strawberry.field def contact_assignment(self, id: int) -> ContactAssignmentType: - return models.ContactAssignment.objects.get(id=id) + return models.ContactAssignment.objects.get(pk=id) contact_assignment_list: List[ContactAssignmentType] = strawberry_django.field() diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 8417ad1d5..7c7cd462a 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -6,6 +6,7 @@ import strawberry_django from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from tenancy import models +from .mixins import ContactAssignmentsMixin from .filters import * __all__ = ( @@ -18,14 +19,6 @@ __all__ = ( ) -@strawberry.type -class ContactAssignmentsMixin: - - @strawberry_django.field - def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: - return self.assignments.all() - - # # Tenants # diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index 66a9e8c93..840887ad2 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -12,10 +12,10 @@ from .types import * class UsersQuery: @strawberry.field def group(self, id: int) -> GroupType: - return models.Group.objects.get(id=id) + return models.Group.objects.get(pk=id) group_list: List[GroupType] = strawberry_django.field() @strawberry.field def user(self, id: int) -> UserType: - return models.User.objects.get(id=id) + return models.User.objects.get(pk=id) user_list: List[UserType] = strawberry_django.field() diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 89a5d99da..6fa15bacb 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -22,9 +22,7 @@ __all__ = ( filters=GroupFilter ) class GroupType: - @classmethod - def get_queryset(cls, queryset, info, **kwargs): - return RestrictedQuerySet(model=Group).restrict(info.context.request.user, 'view') + pass @strawberry_django.type( @@ -36,10 +34,6 @@ class GroupType: filters=UserFilter ) class UserType: - @classmethod - def get_queryset(cls, queryset, info, **kwargs): - return RestrictedQuerySet(model=get_user_model()).restrict(info.context.request.user, 'view') - @strawberry_django.field def groups(self) -> List[GroupType]: return self.groups.all() diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 11007f77f..a30235d93 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -451,12 +451,12 @@ class APIViewTestCases: # Compile list of fields to include fields_string = '' + file_fields = (strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) for field in type_class.__strawberry_definition__.fields: if ( - field.type in ( - strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) or - type(field.type) is StrawberryOptional and field.type.of_type in ( - strawberry_django.fields.types.DjangoFileType, strawberry_django.fields.types.DjangoImageType) + field.type in file_fields or ( + type(field.type) is StrawberryOptional and field.type.of_type in file_fields + ) ): # image / file fields nullable or not... fields_string += f'{field.name} {{ name }}\n' diff --git a/netbox/virtualization/graphql/schema.py b/netbox/virtualization/graphql/schema.py index 02dd888d7..72d83155d 100644 --- a/netbox/virtualization/graphql/schema.py +++ b/netbox/virtualization/graphql/schema.py @@ -11,30 +11,30 @@ from .types import * class VirtualizationQuery: @strawberry.field def cluster(self, id: int) -> ClusterType: - return models.Cluster.objects.get(id=id) + return models.Cluster.objects.get(pk=id) cluster_list: List[ClusterType] = strawberry_django.field() @strawberry.field def cluster_group(self, id: int) -> ClusterGroupType: - return models.ClusterGroup.objects.get(id=id) + return models.ClusterGroup.objects.get(pk=id) cluster_group_list: List[ClusterGroupType] = strawberry_django.field() @strawberry.field def cluster_type(self, id: int) -> ClusterTypeType: - return models.ClusterType.objects.get(id=id) + return models.ClusterType.objects.get(pk=id) cluster_type_list: List[ClusterTypeType] = strawberry_django.field() @strawberry.field def virtual_machine(self, id: int) -> VirtualMachineType: - return models.VirtualMachine.objects.get(id=id) + return models.VirtualMachine.objects.get(pk=id) virtual_machine_list: List[VirtualMachineType] = strawberry_django.field() @strawberry.field def vm_interface(self, id: int) -> VMInterfaceType: - return models.VMInterface.objects.get(id=id) + return models.VMInterface.objects.get(pk=id) vm_interface_list: List[VMInterfaceType] = strawberry_django.field() @strawberry.field def virtual_disk(self, id: int) -> VirtualDiskType: - return models.VirtualDisk.objects.get(id=id) + return models.VirtualDisk.objects.get(pk=id) virtual_disk_list: List[VirtualDiskType] = strawberry_django.field() diff --git a/netbox/vpn/graphql/schema.py b/netbox/vpn/graphql/schema.py index 93c6ded77..f37e444a2 100644 --- a/netbox/vpn/graphql/schema.py +++ b/netbox/vpn/graphql/schema.py @@ -11,50 +11,50 @@ from .types import * class VPNQuery: @strawberry.field def ike_policy(self, id: int) -> IKEPolicyType: - return models.IKEPolicy.objects.get(id=id) + return models.IKEPolicy.objects.get(pk=id) ike_policy_list: List[IKEPolicyType] = strawberry_django.field() @strawberry.field def ike_proposal(self, id: int) -> IKEProposalType: - return models.IKEProposal.objects.get(id=id) + return models.IKEProposal.objects.get(pk=id) ike_proposal_list: List[IKEProposalType] = strawberry_django.field() @strawberry.field def ipsec_policy(self, id: int) -> IPSecPolicyType: - return models.IPSecPolicy.objects.get(id=id) + return models.IPSecPolicy.objects.get(pk=id) ipsec_policy_list: List[IPSecPolicyType] = strawberry_django.field() @strawberry.field def ipsec_profile(self, id: int) -> IPSecProfileType: - return models.IPSecProfile.objects.get(id=id) + return models.IPSecProfile.objects.get(pk=id) ipsec_profile_list: List[IPSecProfileType] = strawberry_django.field() @strawberry.field def ipsec_proposal(self, id: int) -> IPSecProposalType: - return models.IPSecProposal.objects.get(id=id) + return models.IPSecProposal.objects.get(pk=id) ipsec_proposal_list: List[IPSecProposalType] = strawberry_django.field() @strawberry.field def l2vpn(self, id: int) -> L2VPNType: - return models.L2VPN.objects.get(id=id) + return models.L2VPN.objects.get(pk=id) l2vpn_list: List[L2VPNType] = strawberry_django.field() @strawberry.field def l2vpn_termination(self, id: int) -> L2VPNTerminationType: - return models.L2VPNTermination.objects.get(id=id) + return models.L2VPNTermination.objects.get(pk=id) l2vpn_termination_list: List[L2VPNTerminationType] = strawberry_django.field() @strawberry.field def tunnel(self, id: int) -> TunnelType: - return models.Tunnel.objects.get(id=id) + return models.Tunnel.objects.get(pk=id) tunnel_list: List[TunnelType] = strawberry_django.field() @strawberry.field def tunnel_group(self, id: int) -> TunnelGroupType: - return models.TunnelGroup.objects.get(id=id) + return models.TunnelGroup.objects.get(pk=id) tunnel_group_list: List[TunnelGroupType] = strawberry_django.field() @strawberry.field def tunnel_termination(self, id: int) -> TunnelTerminationType: - return models.TunnelTermination.objects.get(id=id) + return models.TunnelTermination.objects.get(pk=id) tunnel_termination_list: List[TunnelTerminationType] = strawberry_django.field() diff --git a/netbox/wireless/graphql/schema.py b/netbox/wireless/graphql/schema.py index 38f0b9c4e..80a40c063 100644 --- a/netbox/wireless/graphql/schema.py +++ b/netbox/wireless/graphql/schema.py @@ -11,15 +11,15 @@ from .types import * class WirelessQuery: @strawberry.field def wireless_lan(self, id: int) -> WirelessLANType: - return models.WirelessLAN.objects.get(id=id) + return models.WirelessLAN.objects.get(pk=id) wireless_lan_list: List[WirelessLANType] = strawberry_django.field() @strawberry.field def wireless_lan_group(self, id: int) -> WirelessLANGroupType: - return models.WirelessLANGroup.objects.get(id=id) + return models.WirelessLANGroup.objects.get(pk=id) wireless_lan_group_list: List[WirelessLANGroupType] = strawberry_django.field() @strawberry.field def wireless_link(self, id: int) -> WirelessLinkType: - return models.WirelessLink.objects.get(id=id) + return models.WirelessLink.objects.get(pk=id) wireless_link_list: List[WirelessLinkType] = strawberry_django.field() diff --git a/requirements.txt b/requirements.txt index 93aaed30c..c4b8c4173 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,8 @@ PyYAML==6.0.1 requests==2.31.0 social-auth-app-django==5.4.0 social-auth-core[openidconnect]==4.5.3 -strawberry-graphql-django==0.33.0 +strawberry-graphql==0.220.0 +strawberry-graphql-django==0.35.1 svgwrite==1.4.3 tablib==3.5.0 tzdata==2024.1 From 908150f9a1483f353208dfebc04fb9fed73f2ea0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 19 Mar 2024 10:10:36 -0700 Subject: [PATCH 089/156] 9856 review changes --- netbox/netbox/graphql/types.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index f084c8140..64aa3617a 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -4,6 +4,7 @@ import strawberry from strawberry import auto import strawberry_django +from core.models import ObjectType as ObjectType_ from django.contrib.contenttypes.models import ContentType from extras.graphql.mixins import ( ChangelogMixin, @@ -92,3 +93,11 @@ class NetBoxObjectType( ) class ContentTypeType: pass + + +@strawberry_django.type( + ObjectType_, + fields=['id', 'app_label', 'model'], +) +class ObjectTypeType: + pass From 783c4f2edcb8ca51d297f69f37dd2afdf5dc8ca2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 19 Mar 2024 10:11:13 -0700 Subject: [PATCH 090/156] 9856 review changes --- netbox/tenancy/graphql/mixins.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 netbox/tenancy/graphql/mixins.py diff --git a/netbox/tenancy/graphql/mixins.py b/netbox/tenancy/graphql/mixins.py new file mode 100644 index 000000000..4673396f5 --- /dev/null +++ b/netbox/tenancy/graphql/mixins.py @@ -0,0 +1,17 @@ +from typing import Annotated, List + +import strawberry +import strawberry_django + + +__all__ = ( + 'ContactAssignmentsMixin', +) + + +@strawberry.type +class ContactAssignmentsMixin: + + @strawberry_django.field + def assignments(self) -> List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]]: + return self.assignments.all() From f585c36d86bb4fe218c064e924edaa7013300e5a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Mar 2024 15:44:35 -0400 Subject: [PATCH 091/156] Introduce InlineFields for rendering fields side-by-side --- netbox/dcim/forms/model_forms.py | 17 ++++ netbox/dcim/views.py | 1 - netbox/templates/dcim/rack_edit.html | 90 ------------------- netbox/templates/htmx/form.html | 17 +--- netbox/utilities/forms/rendering.py | 10 +++ .../form_helpers/render_fieldset.html | 26 ++++++ netbox/utilities/templatetags/form_helpers.py | 24 +++++ 7 files changed, 79 insertions(+), 106 deletions(-) delete mode 100644 netbox/templates/dcim/rack_edit.html create mode 100644 netbox/utilities/forms/rendering.py create mode 100644 netbox/utilities/templates/form_helpers/render_fieldset.html diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 92740ec45..44c3bb40a 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,6 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) +from utilities.forms.rendering import InlineFields from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -227,6 +228,22 @@ class RackForm(TenancyForm, NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + (_('Rack'), ('site', 'location', 'name', 'status', 'role', 'description', 'tags')), + (_('Inventory Control'), ('facility_id', 'serial', 'asset_tag')), + (_('Tenancy'), ('tenant_group', 'tenant')), + (_('Dimensions'), ( + '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', + )), + ) + class Meta: model = Rack fields = [ diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 93e5f04dc..b447ae579 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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') diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html deleted file mode 100644 index 21bc8303d..000000000 --- a/netbox/templates/dcim/rack_edit.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Rack" %}
-
- {% render_field form.site %} - {% render_field form.location %} - {% render_field form.name %} - {% render_field form.status %} - {% render_field form.role %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Inventory Control" %}
-
- {% render_field form.facility_id %} - {% render_field form.serial %} - {% render_field form.asset_tag %} -
- -
-
-
{% trans "Tenancy" %}
-
- {% render_field form.tenant_group %} - {% render_field form.tenant %} -
- -
-
-
{% trans "Dimensions" %}
-
- {% render_field form.type %} - {% render_field form.width %} - {% render_field form.starting_unit %} - {% render_field form.u_height %} -
- -
- {{ form.outer_width }} -
{% trans "Width" %}
-
-
- {{ form.outer_depth }} -
{% trans "Depth" %}
-
-
- {{ form.outer_unit }} -
{% trans "Unit" %}
-
-
-
- -
- {{ form.weight }} -
{% trans "Weight" %}
-
-
- {{ form.max_weight }} -
{% trans "Maximum Weight" %}
-
-
- {{ form.weight_unit }} -
{% trans "Unit" %}
-
-
- {% render_field form.mounting_depth %} - {% render_field form.desc_units %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} - -
- {% render_field form.comments %} -
-{% endblock %} diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 3aafc2a21..0bfcb00ca 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -9,21 +9,8 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, fields in form.fieldsets %} -
- {% if group %} -
-
{{ group }}
-
- {% endif %} - {% for name in fields %} - {% with field=form|getfield:name %} - {% if field and not field.field.widget.is_hidden %} - {% render_field field %} - {% endif %} - {% endwith %} - {% endfor %} -
+ {% for group, items in form.fieldsets %} + {% render_fieldset form items heading=group %} {% endfor %} {% if form.custom_fields %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py new file mode 100644 index 000000000..498b1a2ce --- /dev/null +++ b/netbox/utilities/forms/rendering.py @@ -0,0 +1,10 @@ +__all__ = ( + 'InlineFields', +) + + +class InlineFields: + + def __init__(self, *field_names, label=None): + self.field_names = field_names + self.label = label diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html new file mode 100644 index 000000000..718a8f6a0 --- /dev/null +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -0,0 +1,26 @@ +{% load i18n %} +{% load form_helpers %} +
+ {% if heading %} +
+
{{ heading }}
+
+ {% endif %} + {% for layout, title, items in rows %} + {% if layout == 'field' %} + {# Single form field #} + {% render_field items.0 %} + {% elif layout == 'inline' %} + {# Multiple form fields on the same line #} +
+ + {% for field in items %} +
+ {{ field }} +
{% trans field.label %}
+
+ {% endfor %} +
+ {% endif %} + {% endfor %} +
diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index f4fd8b819..3f60627b4 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,5 +1,7 @@ from django import template +from utilities.forms.rendering import InlineFields + __all__ = ( 'getfield', 'render_custom_fields', @@ -45,6 +47,28 @@ def widget_type(field): # Inclusion tags # +@register.inclusion_tag('form_helpers/render_fieldset.html') +def render_fieldset(form, fieldset, heading=None): + """ + Render a group set of fields. + """ + rows = [] + for item in fieldset: + if type(item) is InlineFields: + rows.append( + ('inline', item.label, [form[name] for name in item.field_names]) + ) + else: + rows.append( + ('field', None, [form[item]]) + ) + + return { + 'heading': heading, + 'rows': rows, + } + + @register.inclusion_tag('form_helpers/render_field.html') def render_field(field, bulk_nullable=False, label=None): """ From 4c7b6fcec05f83eaf7fba65dc82dbe26527a5eaa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Mar 2024 17:02:26 -0400 Subject: [PATCH 092/156] Enable tabbed group fields in fieldsets --- netbox/dcim/forms/model_forms.py | 24 ++-- netbox/dcim/views.py | 2 - netbox/templates/dcim/inventoryitem_edit.html | 107 ------------------ netbox/utilities/forms/rendering.py | 37 +++++- .../form_helpers/render_fieldset.html | 26 +++++ netbox/utilities/templatetags/form_helpers.py | 17 ++- 6 files changed, 92 insertions(+), 121 deletions(-) delete mode 100644 netbox/templates/dcim/inventoryitem_edit.html diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 44c3bb40a..06f28b4e6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields +from utilities.forms.rendering import InlineFields, TabbedFieldGroups from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm): 'width', 'starting_unit', 'u_height', - InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), - InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), + InlineFields(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'), + InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'), 'mounting_depth', 'desc_units', )), @@ -1414,6 +1414,17 @@ class InventoryItemForm(DeviceComponentForm): fieldsets = ( (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), + (_('Component Assignment'), ( + TabbedFieldGroups( + (_('Interface'), 'interface'), + (_('Console Port'), 'consoleport'), + (_('Console Server Port'), 'consoleserverport'), + (_('Front Port'), 'frontport'), + (_('Rear Port'), 'rearport'), + (_('Power Port'), 'powerport'), + (_('Power Outlet'), 'poweroutlet'), + ), + )) ) class Meta: @@ -1429,22 +1440,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 diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b447ae579..49bbe9be1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2924,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') diff --git a/netbox/templates/dcim/inventoryitem_edit.html b/netbox/templates/dcim/inventoryitem_edit.html deleted file mode 100644 index 1dc46ddce..000000000 --- a/netbox/templates/dcim/inventoryitem_edit.html +++ /dev/null @@ -1,107 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Inventory Item" %}
-
- {% render_field form.device %} - {% render_field form.parent %} - {% render_field form.name %} - {% render_field form.label %} - {% render_field form.role %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Hardware" %}
-
- {% render_field form.manufacturer %} - {% render_field form.part_id %} - {% render_field form.serial %} - {% render_field form.asset_tag %} -
- -
-
-
{% trans "Component Assignment" %}
-
-
- -
-
-
- {% render_field form.consoleport %} -
-
- {% render_field form.consoleserverport %} -
-
- {% render_field form.frontport %} -
-
- {% render_field form.interface %} -
-
- {% render_field form.poweroutlet %} -
-
- {% render_field form.powerport %} -
-
- {% render_field form.rearport %} -
-
-
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index 498b1a2ce..ad87930a9 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -1,10 +1,43 @@ +import random +import string +from functools import cached_property + __all__ = ( + 'FieldGroup', 'InlineFields', + 'TabbedFieldGroups', ) -class InlineFields: +class FieldGroup: - def __init__(self, *field_names, label=None): + def __init__(self, label, *field_names): self.field_names = field_names self.label = label + + +class InlineFields(FieldGroup): + pass + + +class TabbedFieldGroups: + + def __init__(self, *groups): + self.groups = [ + FieldGroup(*group) for group in groups + ] + + # Initialize a random ID for the group (for tab selection) + self.id = ''.join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(8) + ) + + @cached_property + def tabs(self): + return [ + { + 'id': f'{self.id}_{i}', + 'title': group.label, + 'fields': group.field_names, + } for i, group in enumerate(self.groups, start=1) + ] diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index 718a8f6a0..ee1f50293 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -7,9 +7,11 @@ {% endif %} {% for layout, title, items in rows %} + {% if layout == 'field' %} {# Single form field #} {% render_field items.0 %} + {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
@@ -21,6 +23,30 @@
{% endfor %} + + {% elif layout == 'tabs' %} + {# Tabbed groups of fields #} +
+ +
+
+ {% for tab in items %} +
+ {% for field in tab.fields %} + {% render_field field %} + {% endfor %} +
+ {% endfor %} +
+ {% endif %} {% endfor %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 3f60627b4..47bbaafe8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields +from utilities.forms.rendering import InlineFields, TabbedFieldGroups __all__ = ( 'getfield', @@ -58,6 +58,21 @@ def render_fieldset(form, fieldset, heading=None): rows.append( ('inline', item.label, [form[name] for name in item.field_names]) ) + elif type(item) is TabbedFieldGroups: + tabs = [ + { + 'id': tab['id'], + 'title': tab['title'], + 'active': bool(form.initial.get(tab['fields'][0], False)), + 'fields': [form[name] for name in tab['fields']] + } for tab in item.tabs + ] + # If none of the tabs has been marked as active, activate the first one + if not any(tab['active'] for tab in tabs): + tabs[0]['active'] = True + rows.append( + ('tabs', None, tabs) + ) else: rows.append( ('field', None, [form[item]]) From 33b9ebb2019f9f084b350d3a33ee9770612d4509 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 09:43:01 -0400 Subject: [PATCH 093/156] Ignore fields which are not included on the form (dynamic rendering) --- netbox/utilities/templatetags/form_helpers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 47bbaafe8..c55a6b98b 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -54,17 +54,24 @@ def render_fieldset(form, fieldset, heading=None): """ rows = [] for item in fieldset: + + # Multiple fields side-by-side if type(item) is InlineFields: + fields = [ + form[name] for name in item.field_names if name in form.fields + ] rows.append( - ('inline', item.label, [form[name] for name in item.field_names]) + ('inline', item.label, fields) ) + + # Tabbed groups of fields elif type(item) is TabbedFieldGroups: tabs = [ { 'id': tab['id'], 'title': tab['title'], 'active': bool(form.initial.get(tab['fields'][0], False)), - 'fields': [form[name] for name in tab['fields']] + 'fields': [form[name] for name in tab['fields'] if name in form.fields] } for tab in item.tabs ] # If none of the tabs has been marked as active, activate the first one @@ -73,7 +80,9 @@ def render_fieldset(form, fieldset, heading=None): rows.append( ('tabs', None, tabs) ) - else: + + # A single form field + elif item in form.fields: rows.append( ('field', None, [form[item]]) ) From 8f03a19b5fdcffc534b6be38a734e1736ad7717c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 10:15:34 -0400 Subject: [PATCH 094/156] Introduce ObjectAttribute for displaying read-only instance attributes on forms --- netbox/extras/forms/model_forms.py | 4 +++ netbox/extras/views.py | 1 - netbox/ipam/forms/model_forms.py | 5 +++ netbox/ipam/views.py | 1 - .../extras/imageattachment_edit.html | 19 ---------- .../ipam/fhrpgroupassignment_edit.html | 19 ---------- .../tenancy/contactassignment_edit.html | 35 ------------------- netbox/tenancy/forms/model_forms.py | 5 +++ netbox/tenancy/views.py | 1 - netbox/utilities/forms/rendering.py | 8 ++++- .../form_helpers/render_fieldset.html | 11 ++++++ netbox/utilities/templatetags/form_helpers.py | 9 ++++- 12 files changed, 40 insertions(+), 78 deletions(-) delete mode 100644 netbox/templates/extras/imageattachment_edit.html delete mode 100644 netbox/templates/ipam/fhrpgroupassignment_edit.html delete mode 100644 netbox/templates/tenancy/contactassignment_edit.html diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 09d2d9535..4e62b3ab7 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -17,6 +17,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) +from utilities.forms.rendering import ObjectAttribute from utilities.forms.widgets import ChoicesWidget, HTMXSelect from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -526,6 +527,9 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm): + fieldsets = ( + (None, (ObjectAttribute('parent'), 'name', 'image')), + ) class Meta: model = ImageAttachment diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 1fa2a30aa..cb3fdd39c 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -759,7 +759,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: diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 47087139a..07f782f7f 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,6 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) +from utilities.forms.rendering import ObjectAttribute from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -502,6 +503,10 @@ class FHRPGroupAssignmentForm(forms.ModelForm): queryset=FHRPGroup.objects.all() ) + fieldsets = ( + (None, (ObjectAttribute('interface'), 'group', 'priority')), + ) + class Meta: model = FHRPGroupAssignment fields = ('group', 'priority') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 9c4a9a102..79716f082 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1059,7 +1059,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: diff --git a/netbox/templates/extras/imageattachment_edit.html b/netbox/templates/extras/imageattachment_edit.html deleted file mode 100644 index 75b2ce48b..000000000 --- a/netbox/templates/extras/imageattachment_edit.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block form %} -
-
- -
-
- {{ object.parent|linkify }} -
-
-
- {% render_form form %} -
-{% endblock form %} diff --git a/netbox/templates/ipam/fhrpgroupassignment_edit.html b/netbox/templates/ipam/fhrpgroupassignment_edit.html deleted file mode 100644 index bbc1505f2..000000000 --- a/netbox/templates/ipam/fhrpgroupassignment_edit.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "FHRP Group Assignment" %}
-
-
- -
- -
-
- {% render_field form.group %} - {% render_field form.priority %} -
-{% endblock %} diff --git a/netbox/templates/tenancy/contactassignment_edit.html b/netbox/templates/tenancy/contactassignment_edit.html deleted file mode 100644 index 342debcbb..000000000 --- a/netbox/templates/tenancy/contactassignment_edit.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} -
-
-
{% trans "Contact Assignment" %}
-
-
- -
- -
-
- {% render_field form.group %} - {% render_field form.contact %} - {% render_field form.role %} - {% render_field form.priority %} - {% render_field form.tags %} -
- -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
-{% endblock %} diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 140d9cf9a..7dcb4e433 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.rendering import ObjectAttribute __all__ = ( 'ContactAssignmentForm', @@ -140,6 +141,10 @@ class ContactAssignmentForm(NetBoxModelForm): queryset=ContactRole.objects.all() ) + fieldsets = ( + (None, (ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags')), + ) + class Meta: model = ContactAssignment fields = ( diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 4c4d263df..d30793a16 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -369,7 +369,6 @@ class ContactAssignmentListView(generic.ObjectListView): class ContactAssignmentEditView(generic.ObjectEditView): queryset = ContactAssignment.objects.all() form = forms.ContactAssignmentForm - template_name = 'tenancy/contactassignment_edit.html' def alter_object(self, instance, request, args, kwargs): if not instance.pk: diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index ad87930a9..d60f3f061 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -3,8 +3,8 @@ import string from functools import cached_property __all__ = ( - 'FieldGroup', 'InlineFields', + 'ObjectAttribute', 'TabbedFieldGroups', ) @@ -41,3 +41,9 @@ class TabbedFieldGroups: 'fields': group.field_names, } for i, group in enumerate(self.groups, start=1) ] + + +class ObjectAttribute: + + def __init__(self, name): + self.name = name diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index ee1f50293..d4c7981f7 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -12,6 +12,17 @@ {# Single form field #} {% render_field items.0 %} + {% elif layout == 'attribute' %} + {# A static attribute of the form's instance #} +
+ +
+
+ {{ items.0|linkify }} +
+
+
+ {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index c55a6b98b..e336ac21b 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups __all__ = ( 'getfield', @@ -81,6 +81,13 @@ def render_fieldset(form, fieldset, heading=None): ('tabs', None, tabs) ) + elif type(item) is ObjectAttribute: + value = getattr(form.instance, item.name) + label = value._meta.verbose_name if hasattr(value, '_meta') else item.name + rows.append( + ('attribute', label.title(), [value]) + ) + # A single form field elif item in form.fields: rows.append( From 2aaa552067cfa7f92391709f971d682190edc45c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Mar 2024 10:59:00 -0400 Subject: [PATCH 095/156] Replace custom form templates with TabbedFieldGroups --- netbox/circuits/forms/model_forms.py | 16 ++++ netbox/circuits/views.py | 1 - netbox/ipam/forms/model_forms.py | 46 ++++++++- netbox/ipam/views.py | 3 - .../circuits/circuittermination_edit.html | 58 ------------ netbox/templates/ipam/ipaddress_edit.html | 93 ------------------- netbox/templates/ipam/service_create.html | 79 ---------------- netbox/templates/ipam/service_edit.html | 66 ------------- .../templates/vpn/l2vpntermination_edit.html | 56 ----------- .../form_helpers/render_fieldset.html | 24 ++--- netbox/vpn/forms/model_forms.py | 13 +++ netbox/vpn/views.py | 1 - 12 files changed, 87 insertions(+), 369 deletions(-) delete mode 100644 netbox/templates/circuits/circuittermination_edit.html delete mode 100644 netbox/templates/ipam/ipaddress_edit.html delete mode 100644 netbox/templates/ipam/service_create.html delete mode 100644 netbox/templates/ipam/service_edit.html delete mode 100644 netbox/templates/vpn/l2vpntermination_edit.html diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 0809cb2f4..9e29f6477 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -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 TabbedFieldGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -146,6 +147,21 @@ class CircuitTerminationForm(NetBoxModelForm): selector=True ) + fieldsets = ( + (_('Circuit Termination'), ( + 'circuit', + 'term_side', + 'description', + 'tags', + TabbedFieldGroups( + (_('Site'), 'site'), + (_('Provider Network'), 'provider_network'), + ), + 'mark_connected', + )), + (_('Termination Details'), ('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info')), + ) + class Meta: model = CircuitTermination fields = [ diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 64dd82682..0c01d6eb9 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -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') diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 07f782f7f..85f9591a8 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -308,6 +308,20 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), + (_('Tenancy'), ('tenant_group', 'tenant')), + (_('Assignment'), ( + TabbedFieldGroups( + (_('Device'), 'interface'), + (_('Virtual Machine'), 'vminterface'), + (_('FHRP Group'), 'fhrpgroup'), + ), + 'primary_for_parent', + )), + (_('NAT IP (Inside)'), ('nat_inside',)), + ) + class Meta: model = IPAddress fields = [ @@ -709,6 +723,20 @@ class ServiceForm(NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + (_('Service'), ( + TabbedFieldGroups( + (_('Device'), 'device'), + (_('Virtual Machine'), 'virtual_machine'), + ), + 'name', + InlineFields(_('Port(s)'), 'protocol', 'ports'), + 'ipaddresses', + 'description', + 'tags', + )), + ) + class Meta: model = Service fields = [ @@ -723,6 +751,22 @@ class ServiceCreateForm(ServiceForm): required=False ) + fieldsets = ( + (_('Service'), ( + TabbedFieldGroups( + (_('Device'), 'device'), + (_('Virtual Machine'), 'virtual_machine'), + ), + TabbedFieldGroups( + (_('From Template'), 'service_template'), + (_('Custom'), 'name', 'protocol', 'ports'), + ), + 'ipaddresses', + 'description', + 'tags', + )), + ) + class Meta(ServiceForm.Meta): fields = [ 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description', diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 79716f082..6870d1e9e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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): @@ -1235,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') diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html deleted file mode 100644 index 18198cb72..000000000 --- a/netbox/templates/circuits/circuittermination_edit.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Circuit Termination" %}
-
- {% render_field form.circuit %} - {% render_field form.term_side %} - {% render_field form.tags %} - {% render_field form.mark_connected %} - {% with providernetwork_tab_active=form.initial.provider_network %} -
-
- -
-
-
-
- {% render_field form.site %} -
-
- {% render_field form.provider_network %} -
-
- {% endwith %} -
- -
-
-
{% trans "Termination Details" %}
-
- {% render_field form.port_speed %} - {% render_field form.upstream_speed %} - {% render_field form.xconnect_id %} - {% render_field form.pp_info %} - {% render_field form.description %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html deleted file mode 100644 index d9157f5ef..000000000 --- a/netbox/templates/ipam/ipaddress_edit.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load helpers %} -{% load i18n %} - -{% block tabs %} - {% include 'ipam/inc/ipaddress_edit_header.html' with active_tab='add' %} -{% endblock tabs %} - -{% block form %} -
-
-
{% trans "IP Address" %}
-
- {% render_field form.address %} - {% render_field form.status %} - {% render_field form.role %} - {% render_field form.vrf %} - {% render_field form.dns_name %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
{% trans "Tenancy" %}
-
- {% render_field form.tenant_group %} - {% render_field form.tenant %} -
- -
-
-
{% trans "Interface Assignment" %}
-
-
-
- -
-
-
-
- {% render_field form.interface %} -
-
- {% render_field form.vminterface %} -
-
- {% render_field form.fhrpgroup %} -
- {% render_field form.primary_for_parent %} -
-
- -
-
-
{% trans "NAT IP (Inside" %})
-
-
- {% render_field form.nat_inside %} -
-
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/service_create.html b/netbox/templates/ipam/service_create.html deleted file mode 100644 index d145999c0..000000000 --- a/netbox/templates/ipam/service_create.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Service" %}
-
- - {# Device/VM selection #} -
-
- -
-
-
-
- {% render_field form.device %} -
-
- {% render_field form.virtual_machine %} -
-
- - {# Template or custom #} -
-
- -
-
-
-
- {% render_field form.service_template %} -
-
- {% render_field form.name %} - {% render_field form.protocol %} - {% render_field form.ports %} -
-
- {% render_field form.ipaddresses %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} - {% endif %} -{% endblock %} diff --git a/netbox/templates/ipam/service_edit.html b/netbox/templates/ipam/service_edit.html deleted file mode 100644 index 33eda76e1..000000000 --- a/netbox/templates/ipam/service_edit.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Service" %}
-
- -
-
- -
-
-
-
- {% render_field form.device %} -
-
- {% render_field form.virtual_machine %} -
-
- {% render_field form.name %} -
- -
- {{ form.protocol }} -
-
- {{ form.ports }} -
-
-
-
-
- {{ form.ports.help_text }} -
-
- {% render_field form.ipaddresses %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
- {% render_field form.comments %} -
- - {% if form.custom_fields %} -
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} - {% endif %} -{% endblock %} diff --git a/netbox/templates/vpn/l2vpntermination_edit.html b/netbox/templates/vpn/l2vpntermination_edit.html deleted file mode 100644 index 14b30c78d..000000000 --- a/netbox/templates/vpn/l2vpntermination_edit.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load helpers %} -{% load form_helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "L2VPN Termination" %}
-
- {% render_field form.l2vpn %} -
-
- -
-
-
-
-
- {% render_field form.vlan %} -
-
- {% render_field form.interface %} -
-
- {% render_field form.vminterface %} -
- {% render_field form.tags %} -
-
-
- {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
-{% endif %} -{% endblock %} diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index d4c7981f7..d978997af 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -26,7 +26,7 @@ {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
- + {% for field in items %}
{{ field }} @@ -37,16 +37,18 @@ {% elif layout == 'tabs' %} {# Tabbed groups of fields #} -
- +
+
+ +
{% for tab in items %} diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 9e5e17a09..efb8a7eda 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,6 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.rendering import TabbedFieldGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -444,6 +445,18 @@ class L2VPNTerminationForm(NetBoxModelForm): label=_('Interface') ) + fieldsets = ( + (None, ( + 'l2vpn', + TabbedFieldGroups( + (_('VLAN'), 'vlan'), + (_('Device'), 'interface'), + (_('Virtual Machine'), 'vminterface'), + ), + 'tags', + )), + ) + class Meta: model = L2VPNTermination fields = ('l2vpn', 'tags') diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 9bf424af9..af1f653c8 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -479,7 +479,6 @@ class L2VPNTerminationView(generic.ObjectView): class L2VPNTerminationEditView(generic.ObjectEditView): queryset = L2VPNTermination.objects.all() form = forms.L2VPNTerminationForm - template_name = 'vpn/l2vpntermination_edit.html' @register_model_view(L2VPNTermination, 'delete') From 3b28e8e61596b9f9d87e78f0ee788365bc88c3c0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Mar 2024 12:59:42 -0400 Subject: [PATCH 096/156] Refactor form rendering components & add docstrings --- netbox/circuits/forms/model_forms.py | 4 +- netbox/dcim/forms/model_forms.py | 8 ++-- netbox/ipam/forms/model_forms.py | 12 +++--- netbox/templates/htmx/form.html | 4 +- netbox/utilities/forms/rendering.py | 41 ++++++++++++------- netbox/utilities/templatetags/form_helpers.py | 17 +++++--- netbox/vpn/forms/model_forms.py | 4 +- 7 files changed, 54 insertions(+), 36 deletions(-) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 9e29f6477..d73da3a02 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +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 TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -153,7 +153,7 @@ class CircuitTerminationForm(NetBoxModelForm): 'term_side', 'description', 'tags', - TabbedFieldGroups( + TabbedGroups( (_('Site'), 'site'), (_('Provider Network'), 'provider_network'), ), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 06f28b4e6..e0c25dbba 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, TabbedGroups from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup @@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm): 'width', 'starting_unit', 'u_height', - InlineFields(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'), - InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'), + InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), + InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), 'mounting_depth', 'desc_units', )), @@ -1415,7 +1415,7 @@ class InventoryItemForm(DeviceComponentForm): (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), (_('Component Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Interface'), 'interface'), (_('Console Port'), 'consoleport'), (_('Console Server Port'), 'consoleserverport'), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 85f9591a8..0aba37fb9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -312,7 +312,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), (_('Tenancy'), ('tenant_group', 'tenant')), (_('Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'), (_('FHRP Group'), 'fhrpgroup'), @@ -725,12 +725,12 @@ class ServiceForm(NetBoxModelForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), 'name', - InlineFields(_('Port(s)'), 'protocol', 'ports'), + InlineFields('protocol', 'ports', label=_('Port(s)')), 'ipaddresses', 'description', 'tags', @@ -753,11 +753,11 @@ class ServiceCreateForm(ServiceForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), - TabbedFieldGroups( + TabbedGroups( (_('From Template'), 'service_template'), (_('Custom'), 'name', 'protocol', 'ports'), ), diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 0bfcb00ca..f9eecc2b9 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -9,8 +9,8 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, items in form.fieldsets %} - {% render_fieldset form items heading=group %} + {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} {% endfor %} {% if form.custom_fields %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index d60f3f061..ea73c38ff 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -3,28 +3,39 @@ import string from functools import cached_property __all__ = ( + 'FieldSet', 'InlineFields', 'ObjectAttribute', - 'TabbedFieldGroups', + 'TabbedGroups', ) -class FieldGroup: +class FieldSet: + """ + A generic grouping of fields, with an optional name. Each field will be rendered + on its own row under the heading (name). + """ + def __init__(self, *fields, name=None): + self.fields = fields + self.name = name - def __init__(self, label, *field_names): - self.field_names = field_names + +class InlineFields: + """ + A set of fields rendered inline (side-by-side) with a shared label; typically nested within a FieldSet. + """ + def __init__(self, *fields, label=None): + self.fields = fields self.label = label -class InlineFields(FieldGroup): - pass - - -class TabbedFieldGroups: - +class TabbedGroups: + """ + Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate. + """ def __init__(self, *groups): self.groups = [ - FieldGroup(*group) for group in groups + FieldSet(*group, name=name) for name, *group in groups ] # Initialize a random ID for the group (for tab selection) @@ -37,13 +48,15 @@ class TabbedFieldGroups: return [ { 'id': f'{self.id}_{i}', - 'title': group.label, - 'fields': group.field_names, + 'title': group.name, + 'fields': group.fields, } for i, group in enumerate(self.groups, start=1) ] class ObjectAttribute: - + """ + Renders the value for a specific attribute on the form's instance. + """ def __init__(self, name): self.name = name diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index e336ac21b..48a1a5aa8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,6 +1,6 @@ from django import template -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups __all__ = ( 'getfield', @@ -48,24 +48,29 @@ def widget_type(field): # @register.inclusion_tag('form_helpers/render_fieldset.html') -def render_fieldset(form, fieldset, heading=None): +def render_fieldset(form, fieldset): """ Render a group set of fields. """ + # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3')) + if type(fieldset) is not FieldSet: + name, fields = fieldset + fieldset = FieldSet(*fields, name=name) + rows = [] - for item in fieldset: + for item in fieldset.fields: # Multiple fields side-by-side if type(item) is InlineFields: fields = [ - form[name] for name in item.field_names if name in form.fields + form[name] for name in item.fields if name in form.fields ] rows.append( ('inline', item.label, fields) ) # Tabbed groups of fields - elif type(item) is TabbedFieldGroups: + elif type(item) is TabbedGroups: tabs = [ { 'id': tab['id'], @@ -95,7 +100,7 @@ def render_fieldset(form, fieldset, heading=None): ) return { - 'heading': heading, + 'heading': fieldset.name, 'rows': rows, } diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index efb8a7eda..9674ee2f9 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -448,7 +448,7 @@ class L2VPNTerminationForm(NetBoxModelForm): fieldsets = ( (None, ( 'l2vpn', - TabbedFieldGroups( + TabbedGroups( (_('VLAN'), 'vlan'), (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'), From 72d3c17b482d0802aa33b8af6ea833d5af01caa7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Mar 2024 15:08:28 -0400 Subject: [PATCH 097/156] Use FieldSet instances for all forms --- netbox/circuits/forms/bulk_edit.py | 15 +- netbox/circuits/forms/filtersets.py | 33 +-- netbox/circuits/forms/model_forms.py | 33 +-- netbox/core/forms/bulk_edit.py | 3 +- netbox/core/forms/filtersets.py | 23 +- netbox/core/forms/model_forms.py | 37 ++- netbox/dcim/forms/bulk_edit.py | 113 ++++---- netbox/dcim/forms/filtersets.py | 258 +++++++++--------- netbox/dcim/forms/model_forms.py | 207 +++++++------- netbox/dcim/forms/object_create.py | 7 +- netbox/extras/forms/filtersets.py | 69 ++--- netbox/extras/forms/model_forms.py | 71 ++--- netbox/ipam/forms/bulk_edit.py | 43 +-- netbox/ipam/forms/filtersets.py | 93 ++++--- netbox/ipam/forms/model_forms.py | 114 ++++---- netbox/netbox/forms/base.py | 2 +- netbox/templates/generic/bulk_edit.html | 10 +- netbox/templates/inc/filter_list.html | 8 +- netbox/tenancy/forms/bulk_edit.py | 11 +- netbox/tenancy/forms/filtersets.py | 9 +- netbox/tenancy/forms/model_forms.py | 23 +- netbox/users/forms/bulk_edit.py | 7 +- netbox/users/forms/filtersets.py | 19 +- netbox/users/forms/model_forms.py | 38 ++- netbox/utilities/forms/rendering.py | 9 +- netbox/utilities/templatetags/form_helpers.py | 6 + netbox/virtualization/forms/bulk_edit.py | 23 +- netbox/virtualization/forms/filtersets.py | 42 +-- netbox/virtualization/forms/model_forms.py | 37 ++- netbox/vpn/forms/bulk_edit.py | 27 +- netbox/vpn/forms/filtersets.py | 48 ++-- netbox/vpn/forms/model_forms.py | 70 ++--- netbox/wireless/forms/bulk_edit.py | 11 +- netbox/wireless/forms/filtersets.py | 17 +- netbox/wireless/forms/model_forms.py | 21 +- 35 files changed, 800 insertions(+), 757 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 5c416bff9..3ac311c56 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -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', diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 1e1abd068..01445ff6f 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -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( diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index d73da3a02..ee5e47ce7 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +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 TabbedGroups +from utilities.forms.rendering import FieldSet, TabbedGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -30,7 +30,7 @@ class ProviderForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')), + FieldSet('name', 'slug', 'asns', 'description', 'tags'), ) class Meta: @@ -62,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: @@ -76,9 +76,7 @@ class CircuitTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Circuit Type'), ( - 'name', 'slug', 'color', 'description', 'tags', - )), + FieldSet('name', 'slug', 'color', 'description', 'tags'), ) class Meta: @@ -108,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: @@ -148,18 +146,15 @@ class CircuitTerminationForm(NetBoxModelForm): ) fieldsets = ( - (_('Circuit Termination'), ( - 'circuit', - 'term_side', - 'description', - 'tags', + FieldSet( + 'circuit', 'term_side', 'description', 'tags', TabbedGroups( - (_('Site'), 'site'), - (_('Provider Network'), 'provider_network'), + FieldSet('site', name=_('Site')), + FieldSet('provider_network', name=_('Provider Network')), ), - 'mark_connected', - )), - (_('Termination Details'), ('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info')), + 'mark_connected', name=_('Circuit Termination') + ), + FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')), ) class Meta: diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index bc2ef8fc9..c1f1fca4d 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -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', diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index bd74c0f14..60a3acc44 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -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'), ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index e0c71fe48..cbca0737a 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -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: diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 79ecc8383..978a5d0a1 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -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', ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e35055851..4e8e3491c 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -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(), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index e0c25dbba..3559aabc6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, TabbedGroups +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 @@ -78,9 +78,7 @@ class RegionForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Region'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags'), ) class Meta: @@ -99,9 +97,7 @@ class SiteGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Site Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags'), ) class Meta: @@ -136,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: @@ -180,8 +177,8 @@ class LocationForm(TenancyForm, NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'facility', '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: @@ -195,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: @@ -229,19 +224,15 @@ class RackForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Rack'), ('site', 'location', 'name', 'status', 'role', 'description', 'tags')), - (_('Inventory Control'), ('facility_id', 'serial', 'asset_tag')), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('Dimensions'), ( - 'type', - 'width', - 'starting_unit', - 'u_height', + 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', - )), + 'mounting_depth', 'desc_units', name=_('Dimensions') + ), ) class Meta: @@ -273,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: @@ -288,9 +279,7 @@ class ManufacturerForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Manufacturer'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')), ) class Meta: @@ -321,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: @@ -354,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: @@ -374,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: @@ -403,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: @@ -618,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: @@ -675,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: @@ -700,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: @@ -849,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: @@ -861,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: @@ -873,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: @@ -896,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: @@ -918,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: @@ -942,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: @@ -958,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: @@ -970,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: @@ -982,7 +974,7 @@ class ModuleBayTemplateForm(ComponentTemplateForm): class DeviceBayTemplateForm(ComponentTemplateForm): fieldsets = ( - (None, ('device_type', 'name', 'label', 'description')), + FieldSet('device_type', 'name', 'label', 'description'), ) class Meta: @@ -1023,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: @@ -1069,9 +1061,9 @@ class ModularDeviceComponentForm(DeviceComponentForm): class ConsolePortForm(ModularDeviceComponentForm): fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1082,11 +1074,10 @@ class ConsolePortForm(ModularDeviceComponentForm): class ConsoleServerPortForm(ModularDeviceComponentForm): - fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1097,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: @@ -1124,10 +1114,10 @@ class PowerOutletForm(ModularDeviceComponentForm): ) fieldsets = ( - (None, ( + FieldSet( 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'tags', - )), + ), ) class Meta: @@ -1223,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: @@ -1262,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: @@ -1278,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: @@ -1292,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: @@ -1304,7 +1297,7 @@ class ModuleBayForm(DeviceComponentForm): class DeviceBayForm(DeviceComponentForm): fieldsets = ( - (None, ('device', 'name', 'label', 'description', 'tags',)), + FieldSet('device', 'name', 'label', 'description', 'tags',), ) class Meta: @@ -1412,19 +1405,20 @@ class InventoryItemForm(DeviceComponentForm): ) fieldsets = ( - (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), - (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), - (_('Component Assignment'), ( + FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')), + FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')), + FieldSet( TabbedGroups( - (_('Interface'), 'interface'), - (_('Console Port'), 'consoleport'), - (_('Console Server Port'), 'consoleserverport'), - (_('Front Port'), 'frontport'), - (_('Rear Port'), 'rearport'), - (_('Power Port'), 'powerport'), - (_('Power Outlet'), 'poweroutlet'), + 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: @@ -1484,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: @@ -1522,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: diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index ea842508f..f811700b4 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -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): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 73751872f..d4235c465 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -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, diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 4e62b3ab7..680bec1e4 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -17,7 +17,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import FieldSet, ObjectAttribute from utilities.forms.widgets import ChoicesWidget, HTMXSelect from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -55,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: @@ -129,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: @@ -163,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: @@ -200,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: @@ -232,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: @@ -267,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: @@ -361,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: @@ -443,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: @@ -495,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: @@ -528,7 +537,7 @@ class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm): fieldsets = ( - (None, (ObjectAttribute('parent'), 'name', 'image')), + FieldSet(ObjectAttribute('parent'), 'name', 'image'), ) class Meta: diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 72d57e941..c7f64ab1d 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -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') diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index cf2e4d46e..6610bcaf3 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -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(), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 0aba37fb9..0db9576f1 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedGroups +from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -57,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: @@ -75,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() @@ -91,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: @@ -111,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: @@ -132,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: @@ -156,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: @@ -185,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: @@ -227,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: @@ -254,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: @@ -309,17 +310,17 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('Assignment'), ( + FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), + FieldSet( TabbedGroups( - (_('Device'), 'interface'), - (_('Virtual Machine'), 'vminterface'), - (_('FHRP Group'), 'fhrpgroup'), + FieldSet('interface', name=_('Device')), + FieldSet('vminterface', name=_('Virtual Machine')), + FieldSet('fhrpgroup', name=_('FHRP Group')), ), - 'primary_for_parent', - )), - (_('NAT IP (Inside)'), ('nat_inside',)), + 'primary_for_parent', name=_('Assignment') + ), + FieldSet('nat_inside', name=_('NAT IP (Inside)')), ) class Meta: @@ -458,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: @@ -518,7 +519,7 @@ class FHRPGroupAssignmentForm(forms.ModelForm): ) fieldsets = ( - (None, (ObjectAttribute('interface'), 'group', 'priority')), + FieldSet(ObjectAttribute('interface'), 'group', 'priority'), ) class Meta: @@ -606,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: @@ -681,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: @@ -724,17 +726,15 @@ class ServiceForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Service'), ( + FieldSet( TabbedGroups( - (_('Device'), 'device'), - (_('Virtual Machine'), 'virtual_machine'), + FieldSet('device', name=_('Device')), + FieldSet('virtual_machine', name=_('Virtual Machine')), ), 'name', InlineFields('protocol', 'ports', label=_('Port(s)')), - 'ipaddresses', - 'description', - 'tags', - )), + 'ipaddresses', 'description', 'tags', name=_('Service') + ), ) class Meta: @@ -752,19 +752,17 @@ class ServiceCreateForm(ServiceForm): ) fieldsets = ( - (_('Service'), ( + FieldSet( TabbedGroups( - (_('Device'), 'device'), - (_('Virtual Machine'), 'virtual_machine'), + FieldSet('device', name=_('Device')), + FieldSet('virtual_machine', name=_('Virtual Machine')), ), TabbedGroups( - (_('From Template'), 'service_template'), - (_('Custom'), 'name', 'protocol', 'ports'), + FieldSet('service_template', name=_('From Template')), + FieldSet('name', 'protocol', 'ports', name=_('Custom')), ), - 'ipaddresses', - 'description', - 'tags', - )), + 'ipaddresses', 'description', 'tags', name=_('Service') + ), ) class Meta(ServiceForm.Meta): diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 85064e79d..f63f56ff5 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -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 = () diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index 10788d62f..ebb0fbc0e 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -49,14 +49,18 @@ Context: {% if form.fieldsets %} {# Render grouped fields according to declared fieldsets #} - {% for group, fields in form.fieldsets %} + {% for fieldset in form.fieldsets %}
- {% if group %}{{ group }}{% else %}{{ model|meta:"verbose_name"|bettertitle }}{% endif %} + {% if fieldset.name %} + {{ fieldset.name }} + {% else %} + {{ model|meta:"verbose_name"|bettertitle }} + {% endif %}
- {% for name in fields %} + {% for name in fieldset.fields %} {% with field=form|getfield:name %} {% if field.name in form.nullable_fields %} {% render_field field bulk_nullable=True %} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index ac87f252d..407add929 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -9,14 +9,14 @@ {{ field }} {% endfor %} {# List filters by group #} - {% for heading, fields in filter_form.fieldsets %} + {% for fieldset in filter_form.fieldsets %}
- {% if heading %} + {% if fieldset.name %}
- {{ heading }} + {{ fieldset.name }}
{% endif %} - {% for name in fields %} + {% for name in fieldset.fields %} {% with field=filter_form|get_item:name %} {% render_field field %} {% endwith %} diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 49866ca3e..5af3f22ac 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -6,6 +6,7 @@ from tenancy.choices import ContactPriorityChoices from tenancy.models import * from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet __all__ = ( 'ContactAssignmentBulkEditForm', @@ -46,7 +47,7 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm): model = Tenant fieldsets = ( - (None, ('group',)), + FieldSet('group'), ) nullable_fields = ('group',) @@ -69,7 +70,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): model = ContactGroup fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -83,7 +84,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): model = ContactRole fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -126,7 +127,7 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): model = Contact fieldsets = ( - (None, ('group', 'title', 'phone', 'email', 'address', 'link', 'description')), + FieldSet('group', 'title', 'phone', 'email', 'address', 'link', 'description'), ) nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments') @@ -150,6 +151,6 @@ class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm): model = ContactAssignment fieldsets = ( - (None, ('contact', 'role', 'priority')), + FieldSet('contact', 'role', 'priority'), ) nullable_fields = ('priority',) diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index fbd0f2ad0..960ca45b1 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -9,6 +9,7 @@ from tenancy.forms import ContactModelFilterForm from utilities.forms.fields import ( ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) +from utilities.forms.rendering import FieldSet __all__ = ( 'ContactAssignmentFilterForm', @@ -37,8 +38,8 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Tenant fieldsets = ( - (None, ('q', 'filter_id', 'tag', 'group_id')), - ('Contacts', ('contact', 'contact_role', 'contact_group')) + FieldSet('q', 'filter_id', 'tag', 'group_id'), + FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) group_id = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), @@ -82,8 +83,8 @@ class ContactFilterForm(NetBoxModelFilterSetForm): class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): model = ContactAssignment fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Assignment'), ('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority', name=_('Assignment')), ) object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('contacts'), diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 7dcb4e433..bc18deed6 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField -from utilities.forms.rendering import ObjectAttribute +from utilities.forms.rendering import FieldSet, ObjectAttribute __all__ = ( 'ContactAssignmentForm', @@ -29,9 +29,7 @@ class TenantGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Tenant Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')), ) class Meta: @@ -51,7 +49,7 @@ class TenantForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Tenant'), ('name', 'slug', 'group', 'description', 'tags')), + FieldSet('name', 'slug', 'group', 'description', 'tags', name=_('Tenant')), ) class Meta: @@ -74,9 +72,7 @@ class ContactGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Contact Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')), ) class Meta: @@ -88,9 +84,7 @@ class ContactRoleForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Contact Role'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Contact Role')), ) class Meta: @@ -107,7 +101,10 @@ class ContactForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Contact'), ('group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags')), + FieldSet( + 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags', + name=_('Contact') + ), ) class Meta: @@ -142,7 +139,7 @@ class ContactAssignmentForm(NetBoxModelForm): ) fieldsets = ( - (None, (ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags')), + FieldSet(ObjectAttribute('object'), 'group', 'contact', 'role', 'priority', 'tags'), ) class Meta: diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index c56beff14..a26842d09 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -6,6 +6,7 @@ from ipam.formfields import IPNetworkFormField from ipam.validators import prefix_validator from users.models import * from utilities.forms import BulkEditForm +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker __all__ = ( @@ -48,7 +49,7 @@ class UserBulkEditForm(forms.Form): model = User fieldsets = ( - (None, ('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser')), + FieldSet('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser'), ) nullable_fields = ('first_name', 'last_name') @@ -71,7 +72,7 @@ class ObjectPermissionBulkEditForm(forms.Form): model = ObjectPermission fieldsets = ( - (None, ('enabled', 'description')), + FieldSet('enabled', 'description'), ) nullable_fields = ('description',) @@ -104,7 +105,7 @@ class TokenBulkEditForm(BulkEditForm): model = Token fieldsets = ( - (None, ('write_enabled', 'description', 'expires', 'allowed_ips')), + FieldSet('write_enabled', 'description', 'expires', 'allowed_ips'), ) nullable_fields = ( 'expires', 'description', 'allowed_ips', diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 23bbe45e1..2d5644b98 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -7,6 +7,7 @@ from netbox.forms.mixins import SavedFiltersMixin from users.models import Group, ObjectPermission, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker __all__ = ( @@ -20,16 +21,16 @@ __all__ = ( class GroupFilterForm(NetBoxModelFilterSetForm): model = Group fieldsets = ( - (None, ('q', 'filter_id',)), + FieldSet('q', 'filter_id',), ) class UserFilterForm(NetBoxModelFilterSetForm): model = User fieldsets = ( - (None, ('q', 'filter_id',)), - (_('Group'), ('group_id',)), - (_('Status'), ('is_active', 'is_staff', 'is_superuser')), + FieldSet('q', 'filter_id',), + FieldSet('group_id', name=_('Group')), + FieldSet('is_active', 'is_staff', 'is_superuser', name=_('Status')), ) group_id = DynamicModelMultipleChoiceField( queryset=Group.objects.all(), @@ -62,9 +63,9 @@ class UserFilterForm(NetBoxModelFilterSetForm): class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): model = ObjectPermission fieldsets = ( - (None, ('q', 'filter_id',)), - (_('Permission'), ('enabled', 'group_id', 'user_id')), - (_('Actions'), ('can_view', 'can_add', 'can_change', 'can_delete')), + FieldSet('q', 'filter_id',), + FieldSet('enabled', 'group_id', 'user_id', name=_('Permission')), + FieldSet('can_view', 'can_add', 'can_change', 'can_delete', name=_('Actions')), ) enabled = forms.NullBooleanField( label=_('Enabled'), @@ -116,8 +117,8 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): class TokenFilterForm(SavedFiltersMixin, FilterForm): model = Token fieldsets = ( - (None, ('q', 'filter_id',)), - (_('Token'), ('user_id', 'write_enabled', 'expires', 'last_used')), + FieldSet('q', 'filter_id',), + FieldSet('user_id', 'write_enabled', 'expires', 'last_used', name=_('Token')), ) user_id = DynamicModelMultipleChoiceField( queryset=get_user_model().objects.all(), diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 6c717d1ea..1f199d35c 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -13,6 +13,7 @@ from netbox.preferences import PREFERENCES from users.constants import * from users.models import * from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker from utilities.permissions import qs_filter_from_constraints from utilities.utils import flatten_dict @@ -53,15 +54,10 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass): class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass): fieldsets = ( - (_('User Interface'), ( - 'locale.language', - 'pagination.per_page', - 'pagination.placement', - 'ui.colormode', - )), - (_('Miscellaneous'), ( - 'data_format', - )), + FieldSet( + 'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', name=_('User Interface') + ), + FieldSet('data_format', name=_('Miscellaneous')), ) # List of clearable preferences pk = forms.MultipleChoiceField( @@ -189,10 +185,10 @@ class UserForm(forms.ModelForm): ) fieldsets = ( - (_('User'), ('username', 'password', 'confirm_password', 'first_name', 'last_name', 'email')), - (_('Groups'), ('groups', )), - (_('Status'), ('is_active', 'is_staff', 'is_superuser')), - (_('Permissions'), ('object_permissions',)), + FieldSet('username', 'password', 'confirm_password', 'first_name', 'last_name', 'email', name=_('User')), + FieldSet('groups', name=_('Groups')), + FieldSet('is_active', 'is_staff', 'is_superuser', name=_('Status')), + FieldSet('object_permissions', name=_('Permissions')), ) class Meta: @@ -246,9 +242,9 @@ class GroupForm(forms.ModelForm): ) fieldsets = ( - (None, ('name', )), - (_('Users'), ('users', )), - (_('Permissions'), ('object_permissions', )), + FieldSet('name'), + FieldSet('users', name=_('Users')), + FieldSet('object_permissions', name=_('Permissions')), ) class Meta: @@ -312,11 +308,11 @@ class ObjectPermissionForm(forms.ModelForm): ) fieldsets = ( - (None, ('name', 'description', 'enabled',)), - (_('Actions'), ('can_view', 'can_add', 'can_change', 'can_delete', 'actions')), - (_('Objects'), ('object_types', )), - (_('Assignment'), ('groups', 'users')), - (_('Constraints'), ('constraints',)) + FieldSet('name', 'description', 'enabled'), + FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')), + FieldSet('object_types', name=_('Objects')), + FieldSet('groups', 'users', name=_('Assignment')), + FieldSet('constraints', name=_('Constraints')) ) class Meta: diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index ea73c38ff..0d9344131 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -33,10 +33,11 @@ class TabbedGroups: """ Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate. """ - def __init__(self, *groups): - self.groups = [ - FieldSet(*group, name=name) for name, *group in groups - ] + def __init__(self, *fieldsets): + for fs in fieldsets: + if not fs.name: + raise ValueError(f"Grouped fieldset {fs} must have a name.") + self.groups = fieldsets # Initialize a random ID for the group (for tab selection) self.id = ''.join( diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 48a1a5aa8..5365e1c80 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,3 +1,5 @@ +import warnings + from django import template from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups @@ -52,8 +54,12 @@ def render_fieldset(form, fieldset): """ Render a group set of fields. """ + # TODO: Remove in NetBox v4.1 # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3')) if type(fieldset) is not FieldSet: + warnings.warn( + f"{form.__class__} fieldsets contains a non-FieldSet item: {fieldset}" + ) name, fields = fieldset fieldset = FieldSet(*fields, name=name) diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index eaf252824..2bd3434ac 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -10,6 +10,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkRenameForm, add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect from virtualization.choices import * from virtualization.models import * @@ -35,7 +36,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): model = ClusterType fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -49,7 +50,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): model = ClusterGroup fieldsets = ( - (None, ('description',)), + FieldSet('description'), ) nullable_fields = ('description',) @@ -104,8 +105,8 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): model = Cluster fieldsets = ( - (None, ('type', 'group', 'status', 'tenant', 'description')), - (_('Site'), ('region', 'site_group', 'site')), + FieldSet('type', 'group', 'status', 'tenant', 'description'), + FieldSet('region', 'site_group', 'site', name=_('Site')), ) nullable_fields = ( 'group', 'site', 'tenant', 'description', 'comments', @@ -185,9 +186,9 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): model = VirtualMachine fieldsets = ( - (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')), - (_('Resources'), ('vcpus', 'memory', 'disk')), - ('Configuration', ('config_template',)), + FieldSet('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description'), + FieldSet('vcpus', 'memory', 'disk', name=_('Resources')), + FieldSet('config_template', name=_('Configuration')), ) nullable_fields = ( 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments', @@ -262,9 +263,9 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): model = VMInterface fieldsets = ( - (None, ('mtu', 'enabled', 'vrf', 'description')), - (_('Related Interfaces'), ('parent', 'bridge')), - (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), + FieldSet('mtu', 'enabled', 'vrf', 'description'), + FieldSet('parent', 'bridge', name=_('Related Interfaces')), + FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')), ) nullable_fields = ( 'parent', 'bridge', 'mtu', 'vrf', 'description', @@ -340,7 +341,7 @@ class VirtualDiskBulkEditForm(NetBoxModelBulkEditForm): model = VirtualDisk fieldsets = ( - (None, ('size', 'description')), + FieldSet('size', 'description'), ) nullable_fields = ('description',) diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 5b0d097f8..1cb652a1b 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -9,6 +9,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from virtualization.choices import * from virtualization.models import * from vpn.models import L2VPN @@ -32,19 +33,19 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = ClusterGroup tag = TagFilterField(model) 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')), ) class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Cluster fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('group_id', 'type_id', 'status')), - (_('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('group_id', 'type_id', 'status', 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', 'group_id') type_id = DynamicModelMultipleChoiceField( @@ -94,12 +95,15 @@ class VirtualMachineFilterForm( ): model = VirtualMachine fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')), - (_('Location'), ('region_id', 'site_group_id', 'site_id')), - (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')), - (_('Tenant'), ('tenant_group_id', 'tenant_id')), - (_('Contacts'), ('contact', 'contact_role', 'contact_group')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')), + FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), + FieldSet( + 'status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', + 'local_context_data', name=_('Attributes') + ), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) cluster_group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), @@ -185,9 +189,9 @@ class VirtualMachineFilterForm( class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')), - (_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')), + FieldSet('enabled', 'mac_address', 'vrf_id', 'l2vpn_id', name=_('Attributes')), ) selector_fields = ('filter_id', 'q', 'virtual_machine_id') cluster_id = DynamicModelMultipleChoiceField( @@ -230,9 +234,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): class VirtualDiskFilterForm(NetBoxModelFilterSetForm): model = VirtualDisk fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Virtual Machine'), ('virtual_machine_id',)), - (_('Attributes'), ('size',)), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('virtual_machine_id', name=_('Virtual Machine')), + FieldSet('size', name=_('Attributes')), ) virtual_machine_id = DynamicModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 186ab8182..bfdfc9ada 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -13,6 +13,7 @@ from utilities.forms import ConfirmationForm from utilities.forms.fields import ( CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect from virtualization.models import * @@ -32,9 +33,7 @@ class ClusterTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Cluster Type'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Type')), ) class Meta: @@ -48,9 +47,7 @@ class ClusterGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Cluster Group'), ( - 'name', 'slug', 'description', 'tags', - )), + FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Group')), ) class Meta: @@ -79,8 +76,8 @@ class ClusterForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Cluster'), ('name', 'type', 'group', 'site', 'status', 'description', 'tags')), - (_('Tenancy'), ('tenant_group', 'tenant')), + FieldSet('name', 'type', 'group', 'site', 'status', 'description', 'tags', name=_('Cluster')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) class Meta: @@ -220,12 +217,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Virtual Machine'), ('name', 'role', 'status', 'description', 'tags')), - (_('Site/Cluster'), ('site', 'cluster', 'device')), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('Management'), ('platform', 'primary_ip4', 'primary_ip6', 'config_template')), - (_('Resources'), ('vcpus', 'memory', 'disk')), - (_('Config Context'), ('local_context_data',)), + FieldSet('name', 'role', 'status', 'description', 'tags', name=_('Virtual Machine')), + FieldSet('site', 'cluster', 'device', name=_('Site/Cluster')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), + FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')), + FieldSet('vcpus', 'memory', 'disk', name=_('Resources')), + FieldSet('local_context_data', name=_('Config Context')), ) class Meta: @@ -348,11 +345,11 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm): ) fieldsets = ( - (_('Interface'), ('virtual_machine', 'name', 'description', 'tags')), - (_('Addressing'), ('vrf', 'mac_address')), - (_('Operation'), ('mtu', 'enabled')), - (_('Related Interfaces'), ('parent', 'bridge')), - (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), + FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')), + FieldSet('vrf', 'mac_address', name=_('Addressing')), + FieldSet('mtu', 'enabled', name=_('Operation')), + FieldSet('parent', 'bridge', name=_('Related Interfaces')), + FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')), ) class Meta: @@ -372,7 +369,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm): class VirtualDiskForm(VMComponentForm): fieldsets = ( - (_('Disk'), ('virtual_machine', 'name', 'size', 'description', 'tags')), + FieldSet('virtual_machine', 'name', 'size', 'description', 'tags', name=_('Disk')), ) class Meta: diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py index c3e8eb3ca..a7595a2a7 100644 --- a/netbox/vpn/forms/bulk_edit.py +++ b/netbox/vpn/forms/bulk_edit.py @@ -5,6 +5,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.rendering import FieldSet from vpn.choices import * from vpn.models import * @@ -72,9 +73,9 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm): model = Tunnel fieldsets = ( - (_('Tunnel'), ('status', 'group', 'encapsulation', 'tunnel_id', 'description')), - (_('Security'), ('ipsec_profile',)), - (_('Tenancy'), ('tenant',)), + FieldSet('status', 'group', 'encapsulation', 'tunnel_id', 'description', name=_('Tunnel')), + FieldSet('ipsec_profile', name=_('Security')), + FieldSet('tenant', name=_('Tenancy')), ) nullable_fields = ( 'group', 'ipsec_profile', 'tunnel_id', 'tenant', 'description', 'comments', @@ -125,10 +126,10 @@ class IKEProposalBulkEditForm(NetBoxModelBulkEditForm): model = IKEProposal fieldsets = ( - (None, ( + FieldSet( 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', 'description', - )), + ), ) nullable_fields = ( 'sa_lifetime', 'description', 'comments', @@ -159,9 +160,7 @@ class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm): model = IKEPolicy fieldsets = ( - (None, ( - 'version', 'mode', 'preshared_key', 'description', - )), + FieldSet('version', 'mode', 'preshared_key', 'description'), ) nullable_fields = ( 'mode', 'preshared_key', 'description', 'comments', @@ -196,10 +195,10 @@ class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm): model = IPSecProposal fieldsets = ( - (None, ( + FieldSet( 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', 'description', - )), + ), ) nullable_fields = ( 'sa_lifetime_seconds', 'sa_lifetime_data', 'description', 'comments', @@ -221,7 +220,7 @@ class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm): model = IPSecPolicy fieldsets = ( - (None, ('pfs_group', 'description',)), + FieldSet('pfs_group', 'description'), ) nullable_fields = ( 'pfs_group', 'description', 'comments', @@ -253,9 +252,7 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): model = IPSecProfile fieldsets = ( - (_('Profile'), ( - 'mode', 'ike_policy', 'ipsec_policy', 'description', - )), + FieldSet('mode', 'ike_policy', 'ipsec_policy', 'description', name=_('Profile')), ) nullable_fields = ( 'description', 'comments', @@ -282,7 +279,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): model = L2VPN fieldsets = ( - (None, ('type', 'tenant', 'description')), + FieldSet('type', 'tenant', 'description'), ) nullable_fields = ('tenant', 'description', 'comments') diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index a9326c4bc..d25719d06 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -9,6 +9,7 @@ from tenancy.forms import TenancyFilterForm from utilities.forms.fields import ( ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) +from utilities.forms.rendering import FieldSet from utilities.forms.utils import add_blank_choice from virtualization.models import VirtualMachine from vpn.choices import * @@ -37,10 +38,10 @@ class TunnelGroupFilterForm(NetBoxModelFilterSetForm): class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Tunnel fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')), - (_('Security'), ('ipsec_profile_id',)), - (_('Tenancy'), ('tenant_group_id', 'tenant_id')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')), + FieldSet('ipsec_profile_id', name=_('Security')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')), ) status = forms.MultipleChoiceField( label=_('Status'), @@ -72,8 +73,8 @@ class TunnelFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): model = TunnelTermination fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Termination'), ('tunnel_id', 'role')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('tunnel_id', 'role', name=_('Termination')), ) tunnel_id = DynamicModelMultipleChoiceField( queryset=Tunnel.objects.all(), @@ -91,8 +92,10 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): class IKEProposalFilterForm(NetBoxModelFilterSetForm): model = IKEProposal fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Parameters'), ('authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet( + 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters') + ), ) authentication_method = forms.MultipleChoiceField( label=_('Authentication method'), @@ -120,8 +123,8 @@ class IKEProposalFilterForm(NetBoxModelFilterSetForm): class IKEPolicyFilterForm(NetBoxModelFilterSetForm): model = IKEPolicy fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Parameters'), ('version', 'mode', 'proposal_id')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')), ) version = forms.MultipleChoiceField( label=_('IKE version'), @@ -144,8 +147,8 @@ class IKEPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProposalFilterForm(NetBoxModelFilterSetForm): model = IPSecProposal fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Parameters'), ('encryption_algorithm', 'authentication_algorithm')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')), ) encryption_algorithm = forms.MultipleChoiceField( label=_('Encryption algorithm'), @@ -163,8 +166,8 @@ class IPSecProposalFilterForm(NetBoxModelFilterSetForm): class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): model = IPSecPolicy fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Parameters'), ('proposal_id', 'pfs_group')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('proposal_id', 'pfs_group', name=_('Parameters')), ) proposal_id = DynamicModelMultipleChoiceField( queryset=IKEProposal.objects.all(), @@ -182,8 +185,8 @@ class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProfileFilterForm(NetBoxModelFilterSetForm): model = IPSecProfile fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Profile'), ('mode', 'ike_policy_id', 'ipsec_policy_id')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')), ) mode = forms.MultipleChoiceField( label=_('Mode'), @@ -206,9 +209,9 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm): class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = L2VPN fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('type', 'import_target_id', 'export_target_id')), - (_('Tenant'), ('tenant_group_id', 'tenant_id')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('type', 'import_target_id', 'export_target_id', name=_('Attributes')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) type = forms.ChoiceField( label=_('Type'), @@ -231,10 +234,11 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - (None, ('filter_id', 'l2vpn_id',)), - (_('Assigned Object'), ( + FieldSet('filter_id', 'l2vpn_id',), + FieldSet( 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id', - )), + name=_('Assigned Object') + ), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 9674ee2f9..eb2f839d5 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedGroups +from utilities.forms.rendering import FieldSet, TabbedGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -33,7 +33,7 @@ class TunnelGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Tunnel Group'), ('name', 'slug', 'description', 'tags')), + FieldSet('name', 'slug', 'description', 'tags', name=_('Tunnel Group')), ) class Meta: @@ -57,9 +57,9 @@ class TunnelForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Tunnel'), ('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags')), - (_('Security'), ('ipsec_profile',)), - (_('Tenancy'), ('tenant_group', 'tenant')), + FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')), + FieldSet('ipsec_profile', name=_('Security')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) class Meta: @@ -142,17 +142,15 @@ class TunnelCreateForm(TunnelForm): ) fieldsets = ( - (_('Tunnel'), ('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags')), - (_('Security'), ('ipsec_profile',)), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('First Termination'), ( + FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')), + FieldSet('ipsec_profile', name=_('Security')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), + FieldSet( 'termination1_role', 'termination1_type', 'termination1_parent', 'termination1_termination', - 'termination1_outside_ip', - )), - (_('Second Termination'), ( + 'termination1_outside_ip', name=_('First Termination')), + FieldSet( 'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_termination', - 'termination2_outside_ip', - )), + 'termination2_outside_ip', name=_('Second Termination')), ) def __init__(self, *args, initial=None, **kwargs): @@ -254,7 +252,7 @@ class TunnelTerminationForm(NetBoxModelForm): ) fieldsets = ( - (None, ('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags')), + FieldSet('tunnel', 'role', 'type', 'parent', 'termination', 'outside_ip', 'tags'), ) class Meta: @@ -297,10 +295,11 @@ class TunnelTerminationForm(NetBoxModelForm): class IKEProposalForm(NetBoxModelForm): fieldsets = ( - (_('Proposal'), ('name', 'description', 'tags')), - (_('Parameters'), ( + FieldSet('name', 'description', 'tags', name=_('Proposal')), + FieldSet( 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', - )), + name=_('Parameters') + ), ) class Meta: @@ -318,8 +317,8 @@ class IKEPolicyForm(NetBoxModelForm): ) fieldsets = ( - (_('Policy'), ('name', 'description', 'tags')), - (_('Parameters'), ('version', 'mode', 'proposals', 'preshared_key')), + FieldSet('name', 'description', 'tags', name=_('Policy')), + FieldSet('version', 'mode', 'proposals', 'preshared_key', name=_('Parameters')), ) class Meta: @@ -332,10 +331,11 @@ class IKEPolicyForm(NetBoxModelForm): class IPSecProposalForm(NetBoxModelForm): fieldsets = ( - (_('Proposal'), ('name', 'description', 'tags')), - (_('Parameters'), ( + FieldSet('name', 'description', 'tags', name=_('Proposal')), + FieldSet( 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', - )), + name=_('Parameters') + ), ) class Meta: @@ -353,8 +353,8 @@ class IPSecPolicyForm(NetBoxModelForm): ) fieldsets = ( - (_('Policy'), ('name', 'description', 'tags')), - (_('Parameters'), ('proposals', 'pfs_group')), + FieldSet('name', 'description', 'tags', name=_('Policy')), + FieldSet('proposals', 'pfs_group', name=_('Parameters')), ) class Meta: @@ -376,8 +376,8 @@ class IPSecProfileForm(NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Profile'), ('name', 'description', 'tags')), - (_('Parameters'), ('mode', 'ike_policy', 'ipsec_policy')), + FieldSet('name', 'description', 'tags', name=_('Profile')), + FieldSet('mode', 'ike_policy', 'ipsec_policy', name=_('Parameters')), ) class Meta: @@ -406,9 +406,9 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')), - (_('Route Targets'), ('import_targets', 'export_targets')), - (_('Tenancy'), ('tenant_group', 'tenant')), + FieldSet('name', 'slug', 'type', 'identifier', 'description', 'tags', name=_('L2VPN')), + FieldSet('import_targets', 'export_targets', name=_('Route Targets')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) class Meta: @@ -446,15 +446,15 @@ class L2VPNTerminationForm(NetBoxModelForm): ) fieldsets = ( - (None, ( + FieldSet( 'l2vpn', TabbedGroups( - (_('VLAN'), 'vlan'), - (_('Device'), 'interface'), - (_('Virtual Machine'), 'vminterface'), + FieldSet('vlan', name=_('VLAN')), + FieldSet('interface', name=_('Device')), + FieldSet('vminterface', name=_('Virtual Machine')), ), 'tags', - )), + ), ) class Meta: diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 43e804345..84916e8d9 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -7,6 +7,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.rendering import FieldSet from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH from wireless.models import * @@ -32,7 +33,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLANGroup fieldsets = ( - (None, ('parent', 'description')), + FieldSet('parent', 'description'), ) nullable_fields = ('parent', 'description') @@ -86,8 +87,8 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLAN fieldsets = ( - (None, ('group', 'ssid', 'status', 'vlan', 'tenant', 'description')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')), + FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), ) nullable_fields = ( 'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments', @@ -133,8 +134,8 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): model = WirelessLink fieldsets = ( - (None, ('ssid', 'status', 'tenant', 'description')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')) + FieldSet('ssid', 'status', 'tenant', 'description'), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')) ) nullable_fields = ( 'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments', diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index f4c1cb523..2458d7b48 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -6,6 +6,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import add_blank_choice from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.rendering import FieldSet from wireless.choices import * from wireless.models import * @@ -29,10 +30,10 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLAN fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('ssid', 'group_id', 'status')), - (_('Tenant'), ('tenant_group_id', 'tenant_id')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('ssid', 'group_id', 'status', name=_('Attributes')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), ) ssid = forms.CharField( required=False, @@ -69,10 +70,10 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLink fieldsets = ( - (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('ssid', 'status',)), - (_('Tenant'), ('tenant_group_id', 'tenant_id')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('ssid', 'status', name=_('Attributes')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), ) ssid = forms.CharField( required=False, diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 04e6fce83..05debf8bf 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -6,6 +6,7 @@ from ipam.models import VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.rendering import FieldSet from wireless.models import * __all__ = ( @@ -24,9 +25,7 @@ class WirelessLANGroupForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - (_('Wireless LAN Group'), ( - 'parent', 'name', 'slug', 'description', 'tags', - )), + FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')), ) class Meta: @@ -51,9 +50,9 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Wireless LAN'), ('ssid', 'group', 'vlan', 'status', 'description', 'tags')), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')), + FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), ) class Meta: @@ -158,11 +157,11 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - (_('Side A'), ('site_a', 'location_a', 'device_a', 'interface_a')), - (_('Side B'), ('site_b', 'location_b', 'device_b', 'interface_b')), - (_('Link'), ('status', 'ssid', 'description', 'tags')), - (_('Tenancy'), ('tenant_group', 'tenant')), - (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')), + FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')), + FieldSet('site_b', 'location_b', 'device_b', 'interface_b', name=_('Side B')), + FieldSet('status', 'ssid', 'description', 'tags', name=_('Link')), + FieldSet('tenant_group', 'tenant', name=_('Tenancy')), + FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), ) class Meta: From 708d93c9e0dc4ef0ee724a6b8d564526e0d698de Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Mar 2024 15:48:17 -0400 Subject: [PATCH 098/156] Use render_fieldset() for bulk edit & filter forms --- netbox/templates/account/preferences.html | 11 ++-------- netbox/templates/generic/bulk_edit.html | 21 +------------------ netbox/templates/inc/filter_list.html | 11 +--------- netbox/utilities/templatetags/form_helpers.py | 8 +++++-- 4 files changed, 10 insertions(+), 41 deletions(-) diff --git a/netbox/templates/account/preferences.html b/netbox/templates/account/preferences.html index 93ca5dfc2..c5a93c162 100644 --- a/netbox/templates/account/preferences.html +++ b/netbox/templates/account/preferences.html @@ -10,15 +10,8 @@ {% csrf_token %} {# Built-in preferences #} - {% for group, fields in form.fieldsets %} -
-
-
{{ group }}
-
- {% for name in fields %} - {% render_field form|getfield:name %} - {% endfor %} -
+ {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} {% endfor %} {# Plugin preferences #} diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index ebb0fbc0e..90b68b25b 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -50,26 +50,7 @@ Context: {# Render grouped fields according to declared fieldsets #} {% for fieldset in form.fieldsets %} -
-
-
- {% if fieldset.name %} - {{ fieldset.name }} - {% else %} - {{ model|meta:"verbose_name"|bettertitle }} - {% endif %} -
-
- {% for name in fieldset.fields %} - {% with field=form|getfield:name %} - {% if field.name in form.nullable_fields %} - {% render_field field bulk_nullable=True %} - {% else %} - {% render_field field %} - {% endif %} - {% endwith %} - {% endfor %} -
+ {% render_fieldset form fieldset %} {% endfor %} {# Render tag add/remove fields #} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index 407add929..b8c93ca4c 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -11,16 +11,7 @@ {# List filters by group #} {% for fieldset in filter_form.fieldsets %}
- {% if fieldset.name %} -
- {{ fieldset.name }} -
- {% endif %} - {% for name in fieldset.fields %} - {% with field=filter_form|get_item:name %} - {% render_field field %} - {% endwith %} - {% endfor %} + {% render_fieldset filter_form fieldset %}
{% empty %} {# List all non-customfield filters as declared in the form class #} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 5365e1c80..723c5206a 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -101,8 +101,12 @@ def render_fieldset(form, fieldset): # A single form field elif item in form.fields: + field = form[item] + # Annotate nullability for bulk editing + if field.name in getattr(form, 'nullable_fields', []): + field._nullable = True rows.append( - ('field', None, [form[item]]) + ('field', None, [field]) ) return { @@ -119,7 +123,7 @@ def render_field(field, bulk_nullable=False, label=None): return { 'field': field, 'label': label or field.label, - 'bulk_nullable': bulk_nullable, + 'bulk_nullable': bulk_nullable or getattr(field, '_nullable', False), } From 89150f4b27d7ed7007d4e2b439f98f669561ee36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 08:50:42 -0400 Subject: [PATCH 099/156] Add form rendering utilities to plugins dev docs --- docs/development/internationalization.md | 5 +-- docs/plugins/development/forms.md | 39 ++++++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index bebc97470..df0176b89 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -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')), ) ``` diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index 31751855e..332544df7 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -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 From 32edb8dfe6cbfbbc037a45b11bf46486800055c9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 09:20:49 -0400 Subject: [PATCH 100/156] Misc cleanup & documentation for FieldSets --- netbox/utilities/forms/rendering.py | 39 +++++++++++++++---- netbox/utilities/templatetags/form_helpers.py | 2 +- netbox/vpn/forms/filtersets.py | 2 +- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index 0d9344131..723e911e6 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -12,17 +12,31 @@ __all__ = ( class FieldSet: """ - A generic grouping of fields, with an optional name. Each field will be rendered - on its own row under the heading (name). + A generic grouping of fields, with an optional name. Each item will be rendered + on its own row under the provided heading (name), if any. The following types + may be passed as items: + + * Field name (string) + * InlineFields instance + * TabbedGroups instance + * ObjectAttribute instance + + Parameters: + items: An iterable of items to be rendered (one per row) + name: The fieldset's name, displayed as a heading (optional) """ - def __init__(self, *fields, name=None): - self.fields = fields + def __init__(self, *items, name=None): + self.items = items self.name = name class InlineFields: """ - A set of fields rendered inline (side-by-side) with a shared label; typically nested within a FieldSet. + A set of fields rendered inline (side-by-side) with a shared label. + + Parameters: + fields: An iterable of form field names + label: The label text to render for the row (optional) """ def __init__(self, *fields, label=None): self.fields = fields @@ -31,7 +45,11 @@ class InlineFields: class TabbedGroups: """ - Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate. + Two or more groups of fields (FieldSets) arranged under tabs among which the user can toggle. + + Parameters: + fieldsets: An iterable of FieldSet instances, one per tab. Each FieldSet *must* have a + name assigned, which will be employed as the tab's label. """ def __init__(self, *fieldsets): for fs in fieldsets: @@ -50,14 +68,19 @@ class TabbedGroups: { 'id': f'{self.id}_{i}', 'title': group.name, - 'fields': group.fields, + 'fields': group.items, } for i, group in enumerate(self.groups, start=1) ] class ObjectAttribute: """ - Renders the value for a specific attribute on the form's instance. + Renders the value for a specific attribute on the form's instance. This may be used to + display a read-only value and convey additional context to the user. If the attribute has + a `get_absolute_url()` method, it will be rendered as a hyperlink. + + Parameters: + name: The name of the attribute to be displayed """ def __init__(self, name): self.name = name diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 723c5206a..e9edfed31 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -64,7 +64,7 @@ def render_fieldset(form, fieldset): fieldset = FieldSet(*fields, name=name) rows = [] - for item in fieldset.fields: + for item in fieldset.items: # Multiple fields side-by-side if type(item) is InlineFields: diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index d25719d06..10dc441e2 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -234,7 +234,7 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - FieldSet('filter_id', 'l2vpn_id',), + FieldSet('filter_id', 'l2vpn_id'), FieldSet( 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id', name=_('Assigned Object') From 849a9d32d1c5535c2776588a12ee591cdc5d9478 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 14:06:24 -0400 Subject: [PATCH 101/156] Fixes #15340: Fix flicker on page load with dark mode enabled (#15475) --- netbox/project-static/js/setmode.js | 5 +++-- netbox/templates/base/base.html | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/netbox/project-static/js/setmode.js b/netbox/project-static/js/setmode.js index 8441a542f..ff1c5366b 100644 --- a/netbox/project-static/js/setmode.js +++ b/netbox/project-static/js/setmode.js @@ -5,10 +5,11 @@ * @param inferred {boolean} Value is inferred from browser/system preference. */ function setMode(mode, inferred) { - document.documentElement.setAttribute("data-netbox-color-mode", mode); + document.documentElement.setAttribute("data-bs-theme", mode); localStorage.setItem("netbox-color-mode", mode); localStorage.setItem("netbox-color-mode-inferred", inferred); } + /** * Determine the best initial color mode to use prior to rendering. */ @@ -69,4 +70,4 @@ function initMode() { console.error(error); } return setMode("light", true); -}; +} diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index 1c58047ef..bb35cd3bf 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -9,13 +9,7 @@ data-netbox-url-name="{{ request.resolver_match.url_name }}" data-netbox-base-path="{{ settings.BASE_PATH }}" {% with preferences|get_key:'ui.colormode' as color_mode %} - {% if color_mode == 'dark'%} - data-netbox-color-mode="dark" - {% elif color_mode == 'light' %} - data-netbox-color-mode="light" - {% else %} - data-netbox-color-mode="unset" - {% endif %} + data-netbox-color-mode="{{ color_mode|default:"unset" }}" {% endwith %} > @@ -25,7 +19,16 @@ {# Page title #} {% block title %}{% trans "Home" %}{% endblock %} | NetBox + {# Initialize color mode #} + @@ -53,13 +56,9 @@ {# Additional content #} {% block head %}{% endblock %} - - + + {# Page layout #} {% block layout %}{% endblock %} From a3ce14ad3c1cc1c5b209dff011b52873d2759332 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Mar 2024 14:18:15 -0400 Subject: [PATCH 102/156] Update release notes --- docs/release-notes/version-4.0.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 60b3115f0..b5889f8cd 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -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) From 371a2a29ca3ab8afe396640861d206179b611b25 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 19 Mar 2024 13:38:42 -0700 Subject: [PATCH 103/156] 9856 fix base-requirements --- base_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_requirements.txt b/base_requirements.txt index 30c3e3302..574abac41 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -137,7 +137,7 @@ strawberry-graphql # Strawberry GraphQL Django extension # https://github.com/strawberry-graphql/strawberry-django/blob/main/CHANGELOG.md -strawberry-django +strawberry-graphql-django # SVG image rendering (used for rack elevations) # https://github.com/mozman/svgwrite/blob/master/NEWS.rst From a83b233341a02b9a81e9645aec6d368cd381ba27 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Mar 2024 08:26:04 -0400 Subject: [PATCH 104/156] Closes #15339: Consume entire viewport (#15480) * Closes #15339: Consume entire viewport, except for object detail views * Use fluid containers for all views --- netbox/templates/base/layout.html | 6 +++--- netbox/templates/core/rq_task_list.html | 2 +- netbox/templates/core/rq_worker_list.html | 2 +- netbox/templates/extras/script_result.html | 2 +- netbox/templates/generic/_base.html | 4 ++-- netbox/templates/generic/bulk_delete.html | 2 +- netbox/templates/generic/bulk_remove.html | 8 +++++--- netbox/templates/generic/object.html | 2 +- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index fff12c1e8..071396575 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -41,7 +41,7 @@ Blocks: {# Top menu #}