From 8fd88b357e8ccb9ba3419f83e4a273868a2db64a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Oct 2025 15:27:47 -0400 Subject: [PATCH 01/40] Introduce the Owner model --- contrib/openapi.json | 2277 +++++++++++++++++------ netbox/netbox/navigation/menu.py | 6 + netbox/templates/users/owner.html | 46 + netbox/users/api/serializers.py | 1 + netbox/users/api/serializers_/owners.py | 30 + netbox/users/api/urls.py | 8 +- netbox/users/api/views.py | 12 +- netbox/users/filtersets.py | 44 +- netbox/users/forms/bulk_edit.py | 19 + netbox/users/forms/bulk_import.py | 21 + netbox/users/forms/filtersets.py | 21 +- netbox/users/forms/model_forms.py | 17 +- netbox/users/graphql/filters.py | 9 + netbox/users/graphql/schema.py | 3 + netbox/users/graphql/types.py | 13 +- netbox/users/migrations/0015_owner.py | 43 + netbox/users/models/__init__.py | 1 + netbox/users/models/owners.py | 49 + netbox/users/tables.py | 27 +- netbox/users/urls.py | 3 + netbox/users/views.py | 59 +- 21 files changed, 2085 insertions(+), 624 deletions(-) create mode 100644 netbox/templates/users/owner.html create mode 100644 netbox/users/api/serializers_/owners.py create mode 100644 netbox/users/migrations/0015_owner.py create mode 100644 netbox/users/models/owners.py diff --git a/contrib/openapi.json b/contrib/openapi.json index 4e159af9a9d..14eba1d6e96 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -164474,39 +164474,11 @@ } } }, - "/api/users/permissions/": { + "/api/users/owners/": { "get": { - "operationId": "users_permissions_list", - "description": "Get a list of permission objects.", + "operationId": "users_owners_list", + "description": "Get a list of owner objects.", "parameters": [ - { - "in": "query", - "name": "can_add", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_change", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_delete", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_view", - "schema": { - "type": "boolean" - } - }, { "in": "query", "name": "description", @@ -164658,13 +164630,6 @@ "explode": true, "style": "form" }, - { - "in": "query", - "name": "enabled", - "schema": { - "type": "boolean" - } - }, { "in": "query", "name": "group", @@ -164700,7 +164665,7 @@ "type": "integer" } }, - "description": "Group", + "description": "Group (ID)", "explode": true, "style": "form" }, @@ -164713,7 +164678,7 @@ "type": "integer" } }, - "description": "Group", + "description": "Group (ID)", "explode": true, "style": "form" }, @@ -164962,138 +164927,6 @@ "explode": true, "style": "form" }, - { - "in": "query", - "name": "object_type", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__ic", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__ie", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__iew", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__iregex", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__isw", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__n", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__nic", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__nie", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__niew", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__nisw", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__regex", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type_id", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "object_type_id__n", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "object_types", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "object_types__n", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "explode": true, - "style": "form" - }, { "name": "offset", "required": false, @@ -165129,7 +164962,7 @@ "type": "string" } }, - "description": "User (name)", + "description": "User (username)", "explode": true, "style": "form" }, @@ -165142,7 +164975,7 @@ "type": "string" } }, - "description": "User (name)", + "description": "User (username)", "explode": true, "style": "form" }, @@ -165155,7 +164988,7 @@ "type": "integer" } }, - "description": "User", + "description": "User (ID)", "explode": true, "style": "form" }, @@ -165168,7 +165001,7 @@ "type": "integer" } }, - "description": "User", + "description": "User (ID)", "explode": true, "style": "form" } @@ -165189,7 +165022,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedObjectPermissionList" + "$ref": "#/components/schemas/PaginatedOwnerList" } } }, @@ -165198,8 +165031,8 @@ } }, "post": { - "operationId": "users_permissions_create", - "description": "Post a list of permission objects.", + "operationId": "users_owners_create", + "description": "Post a list of owner objects.", "tags": [ "users" ], @@ -165207,12 +165040,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -165231,7 +165064,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -165240,8 +165073,8 @@ } }, "put": { - "operationId": "users_permissions_bulk_update", - "description": "Put a list of permission objects.", + "operationId": "users_owners_bulk_update", + "description": "Put a list of owner objects.", "tags": [ "users" ], @@ -165251,7 +165084,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -165259,7 +165092,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -165281,7 +165114,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } } @@ -165291,8 +165124,8 @@ } }, "patch": { - "operationId": "users_permissions_bulk_partial_update", - "description": "Patch a list of permission objects.", + "operationId": "users_owners_bulk_partial_update", + "description": "Patch a list of owner objects.", "tags": [ "users" ], @@ -165302,7 +165135,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -165310,7 +165143,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -165332,7 +165165,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } } @@ -165342,8 +165175,8 @@ } }, "delete": { - "operationId": "users_permissions_bulk_destroy", - "description": "Delete a list of permission objects.", + "operationId": "users_owners_bulk_destroy", + "description": "Delete a list of owner objects.", "tags": [ "users" ], @@ -165353,7 +165186,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -165361,7 +165194,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -165383,10 +165216,10 @@ } } }, - "/api/users/permissions/{id}/": { + "/api/users/owners/{id}/": { "get": { - "operationId": "users_permissions_retrieve", - "description": "Get a permission object.", + "operationId": "users_owners_retrieve", + "description": "Get a owner object.", "parameters": [ { "in": "path", @@ -165394,7 +165227,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -165414,7 +165247,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -165423,8 +165256,8 @@ } }, "put": { - "operationId": "users_permissions_update", - "description": "Put a permission object.", + "operationId": "users_owners_update", + "description": "Put a owner object.", "parameters": [ { "in": "path", @@ -165432,7 +165265,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -165443,12 +165276,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -165467,7 +165300,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -165476,8 +165309,8 @@ } }, "patch": { - "operationId": "users_permissions_partial_update", - "description": "Patch a permission object.", + "operationId": "users_owners_partial_update", + "description": "Patch a owner object.", "parameters": [ { "in": "path", @@ -165485,7 +165318,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -165496,12 +165329,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + "$ref": "#/components/schemas/PatchedOwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + "$ref": "#/components/schemas/PatchedOwnerRequest" } } } @@ -165519,7 +165352,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -165528,8 +165361,8 @@ } }, "delete": { - "operationId": "users_permissions_destroy", - "description": "Delete a permission object.", + "operationId": "users_owners_destroy", + "description": "Delete a owner object.", "parameters": [ { "in": "path", @@ -165537,7 +165370,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -165559,33 +165392,37 @@ } } }, - "/api/users/tokens/": { + "/api/users/permissions/": { "get": { - "operationId": "users_tokens_list", - "description": "Get a list of token objects.", + "operationId": "users_permissions_list", + "description": "Get a list of permission objects.", "parameters": [ { "in": "query", - "name": "created", + "name": "can_add", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "created__gte", + "name": "can_change", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "created__lte", + "name": "can_delete", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" + } + }, + { + "in": "query", + "name": "can_view", + "schema": { + "type": "boolean" } }, { @@ -165741,27 +165578,1108 @@ }, { "in": "query", - "name": "expires", + "name": "enabled", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "expires__gte", + "name": "group", "schema": { - "type": "string", - "format": "date-time" - } + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Group (name)", + "explode": true, + "style": "form" }, { "in": "query", - "name": "expires__lte", + "name": "group__n", "schema": { - "type": "string", - "format": "date-time" - } + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Group", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Group", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "id__gt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__gte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__lt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__lte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "name", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "name__ic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__ie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__iew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__iregex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__isw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__nic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__nie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__niew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__nisw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "name__regex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "object_type", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__ic", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__ie", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__iew", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__iregex", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__isw", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__n", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__nic", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__nie", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__niew", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__nisw", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__regex", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "object_type_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "object_types", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "object_types__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "explode": true, + "style": "form" + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "schema": { + "type": "string" + }, + "description": "Search" + }, + { + "in": "query", + "name": "user", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "User (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "User (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "User", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "User", + "explode": true, + "style": "form" + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedObjectPermissionList" + } + } + }, + "description": "" + } + } + }, + "post": { + "operationId": "users_permissions_create", + "description": "Post a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "users_permissions_bulk_update", + "description": "Put a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "users_permissions_bulk_partial_update", + "description": "Patch a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "users_permissions_bulk_destroy", + "description": "Delete a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, + "/api/users/permissions/{id}/": { + "get": { + "operationId": "users_permissions_retrieve", + "description": "Get a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "users_permissions_update", + "description": "Put a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "users_permissions_partial_update", + "description": "Patch a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + } + } + } + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "users_permissions_destroy", + "description": "Delete a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, + "/api/users/tokens/": { + "get": { + "operationId": "users_tokens_list", + "description": "Get a list of token objects.", + "parameters": [ + { + "in": "query", + "name": "created", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "created__gte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "created__lte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "description", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "description__ic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__ie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__iew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__iregex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__isw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__niew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nisw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__regex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "expires", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "expires__gte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "expires__lte", + "schema": { + "type": "string", + "format": "date-time" + } }, { "in": "query", @@ -221597,14 +222515,464 @@ "url" ] }, - "NotificationGroupRequest": { + "NotificationGroupRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "groups": { + "type": "array", + "items": { + "type": "integer" + } + }, + "users": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "name" + ] + }, + "NotificationRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "object_type": { + "type": "string" + }, + "object_id": { + "type": "integer", + "maximum": 9223372036854775807, + "minimum": 0, + "format": "int64" + }, + "user": { + "oneOf": [ + { + "type": "integer" + }, + { + "$ref": "#/components/schemas/BriefUserRequest" + } + ] + }, + "read": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "event_type": { + "enum": [ + "object_created", + "object_updated", + "object_deleted", + "job_started", + "job_completed", + "job_failed", + "job_errored" + ], + "type": "string", + "description": "* `object_created` - Object created\n* `object_updated` - Object updated\n* `object_deleted` - Object deleted\n* `job_started` - Job started\n* `job_completed` - Job completed\n* `job_failed` - Job failed\n* `job_errored` - Job errored", + "x-spec-enum-id": "80d172232f4af424", + "title": "Event" + } + }, + "required": [ + "event_type", + "object_id", + "object_type", + "user" + ] + }, + "ObjectChange": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display_url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "time": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "user": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefUser" + } + ], + "readOnly": true + }, + "user_name": { + "type": "string", + "readOnly": true + }, + "request_id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "action": { + "type": "object", + "properties": { + "value": { + "enum": [ + "create", + "update", + "delete" + ], + "type": "string", + "description": "* `create` - Created\n* `update` - Updated\n* `delete` - Deleted", + "x-spec-enum-id": "544f9b3b28b7ce6a" + }, + "label": { + "type": "string", + "enum": [ + "Created", + "Updated", + "Deleted" + ] + } + }, + "readOnly": true + }, + "changed_object_type": { + "type": "string", + "readOnly": true + }, + "changed_object_id": { + "type": "integer", + "maximum": 9223372036854775807, + "minimum": 0, + "format": "int64" + }, + "changed_object": { + "nullable": true, + "readOnly": true + }, + "message": { + "type": "string", + "readOnly": true + }, + "prechange_data": { + "readOnly": true, + "nullable": true + }, + "postchange_data": { + "readOnly": true, + "nullable": true + } + }, + "required": [ + "action", + "changed_object", + "changed_object_id", + "changed_object_type", + "display", + "display_url", + "id", + "message", + "postchange_data", + "prechange_data", + "request_id", + "time", + "url", + "user", + "user_name" + ] + }, + "ObjectPermission": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display_url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "object_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "actions": { + "type": "array", + "items": { + "type": "string", + "maxLength": 30 + }, + "description": "The list of actions granted by this permission" + }, + "constraints": { + "nullable": true, + "description": "Queryset filter matching the applicable objects of the selected type(s)" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedGroup" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedUser" + } + } + }, + "required": [ + "actions", + "display", + "display_url", + "id", + "name", + "object_types", + "url" + ] + }, + "ObjectPermissionRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "object_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "actions": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 30 + }, + "description": "The list of actions granted by this permission" + }, + "constraints": { + "nullable": true, + "description": "Queryset filter matching the applicable objects of the selected type(s)" + }, + "groups": { + "type": "array", + "items": { + "type": "integer" + } + }, + "users": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "actions", + "name", + "object_types" + ] + }, + "ObjectType": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "app_label": { + "type": "string", + "maxLength": 100 + }, + "app_name": { + "type": "string", + "readOnly": true + }, + "model": { + "type": "string", + "title": "Python model class name", + "maxLength": 100 + }, + "model_name": { + "type": "string", + "readOnly": true + }, + "model_name_plural": { + "type": "string", + "readOnly": true + }, + "public": { + "type": "boolean", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": "string", + "maxLength": 50 + }, + "readOnly": true + }, + "is_plugin_model": { + "type": "boolean", + "readOnly": true + }, + "rest_api_endpoint": { + "type": "string", + "readOnly": true + }, + "description": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "app_label", + "app_name", + "description", + "display", + "features", + "id", + "is_plugin_model", + "model", + "model_name", + "model_name_plural", + "public", + "rest_api_endpoint", + "url" + ] + }, + "Owner": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display_url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "maxLength": 150 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": [ + "display", + "display_url", + "id", + "name", + "url" + ] + }, + "OwnerRequest": { "type": "object", "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", "properties": { "name": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 150 }, "description": { "type": "string", @@ -221627,375 +222995,6 @@ "name" ] }, - "NotificationRequest": { - "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", - "properties": { - "object_type": { - "type": "string" - }, - "object_id": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" - }, - "user": { - "oneOf": [ - { - "type": "integer" - }, - { - "$ref": "#/components/schemas/BriefUserRequest" - } - ] - }, - "read": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "event_type": { - "enum": [ - "object_created", - "object_updated", - "object_deleted", - "job_started", - "job_completed", - "job_failed", - "job_errored" - ], - "type": "string", - "description": "* `object_created` - Object created\n* `object_updated` - Object updated\n* `object_deleted` - Object deleted\n* `job_started` - Job started\n* `job_completed` - Job completed\n* `job_failed` - Job failed\n* `job_errored` - Job errored", - "x-spec-enum-id": "80d172232f4af424", - "title": "Event" - } - }, - "required": [ - "event_type", - "object_id", - "object_type", - "user" - ] - }, - "ObjectChange": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display_url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display": { - "type": "string", - "readOnly": true - }, - "time": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "user": { - "allOf": [ - { - "$ref": "#/components/schemas/BriefUser" - } - ], - "readOnly": true - }, - "user_name": { - "type": "string", - "readOnly": true - }, - "request_id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "action": { - "type": "object", - "properties": { - "value": { - "enum": [ - "create", - "update", - "delete" - ], - "type": "string", - "description": "* `create` - Created\n* `update` - Updated\n* `delete` - Deleted", - "x-spec-enum-id": "544f9b3b28b7ce6a" - }, - "label": { - "type": "string", - "enum": [ - "Created", - "Updated", - "Deleted" - ] - } - }, - "readOnly": true - }, - "changed_object_type": { - "type": "string", - "readOnly": true - }, - "changed_object_id": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" - }, - "changed_object": { - "nullable": true, - "readOnly": true - }, - "message": { - "type": "string", - "readOnly": true - }, - "prechange_data": { - "readOnly": true, - "nullable": true - }, - "postchange_data": { - "readOnly": true, - "nullable": true - } - }, - "required": [ - "action", - "changed_object", - "changed_object_id", - "changed_object_type", - "display", - "display_url", - "id", - "message", - "postchange_data", - "prechange_data", - "request_id", - "time", - "url", - "user", - "user_name" - ] - }, - "ObjectPermission": { - "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", - "properties": { - "id": { - "type": "integer", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display_url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display": { - "type": "string", - "readOnly": true - }, - "name": { - "type": "string", - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 200 - }, - "enabled": { - "type": "boolean" - }, - "object_types": { - "type": "array", - "items": { - "type": "string" - } - }, - "actions": { - "type": "array", - "items": { - "type": "string", - "maxLength": 30 - }, - "description": "The list of actions granted by this permission" - }, - "constraints": { - "nullable": true, - "description": "Queryset filter matching the applicable objects of the selected type(s)" - }, - "groups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NestedGroup" - } - }, - "users": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NestedUser" - } - } - }, - "required": [ - "actions", - "display", - "display_url", - "id", - "name", - "object_types", - "url" - ] - }, - "ObjectPermissionRequest": { - "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 200 - }, - "enabled": { - "type": "boolean" - }, - "object_types": { - "type": "array", - "items": { - "type": "string" - } - }, - "actions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 30 - }, - "description": "The list of actions granted by this permission" - }, - "constraints": { - "nullable": true, - "description": "Queryset filter matching the applicable objects of the selected type(s)" - }, - "groups": { - "type": "array", - "items": { - "type": "integer" - } - }, - "users": { - "type": "array", - "items": { - "type": "integer" - } - } - }, - "required": [ - "actions", - "name", - "object_types" - ] - }, - "ObjectType": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display": { - "type": "string", - "readOnly": true - }, - "app_label": { - "type": "string", - "maxLength": 100 - }, - "app_name": { - "type": "string", - "readOnly": true - }, - "model": { - "type": "string", - "title": "Python model class name", - "maxLength": 100 - }, - "model_name": { - "type": "string", - "readOnly": true - }, - "model_name_plural": { - "type": "string", - "readOnly": true - }, - "public": { - "type": "boolean", - "readOnly": true - }, - "features": { - "type": "array", - "items": { - "type": "string", - "maxLength": 50 - }, - "readOnly": true - }, - "is_plugin_model": { - "type": "boolean", - "readOnly": true - }, - "rest_api_endpoint": { - "type": "string", - "readOnly": true - }, - "description": { - "type": "string", - "readOnly": true - } - }, - "required": [ - "app_label", - "app_name", - "description", - "display", - "features", - "id", - "is_plugin_model", - "model", - "model_name", - "model_name_plural", - "public", - "rest_api_endpoint", - "url" - ] - }, "PaginatedASNList": { "type": "object", "required": [ @@ -224228,6 +225227,37 @@ } } }, + "PaginatedOwnerList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 + }, + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" + }, + "previous": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Owner" + } + } + } + }, "PaginatedPlatformList": { "type": "object", "required": [ @@ -227533,6 +228563,33 @@ } } }, + "PatchedOwnerRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 150 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "groups": { + "type": "array", + "items": { + "type": "integer" + } + }, + "users": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, "PatchedPowerPanelRequest": { "type": "object", "description": "Adds support for custom fields and tags.", diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index ac4c2b49210..0c7fc22c149 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -36,6 +36,12 @@ get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=['bulk_import']), ), ), + MenuGroup( + label=_('Ownership'), + items=( + get_model_item('users', 'owner', _('Owners')), + ), + ), ), ) diff --git a/netbox/templates/users/owner.html b/netbox/templates/users/owner.html new file mode 100644 index 00000000000..b840c3b678b --- /dev/null +++ b/netbox/templates/users/owner.html @@ -0,0 +1,46 @@ +{% extends 'generic/object.html' %} +{% load i18n %} + +{% block subtitle %}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Owner" %}

+ + + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
+
+
+
+
+

{% trans "Groups" %}

+
+ {% for group in object.groups.all %} + {{ group }} + {% empty %} +
{% trans "None" %}
+ {% endfor %} +
+
+
+

{% trans "Users" %}

+
+ {% for user in object.users.all %} + {{ user }} + {% empty %} +
{% trans "None" %}
+ {% endfor %} +
+
+
+
+{% endblock %} diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 700061b8cfe..9e64515c218 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,3 +1,4 @@ from .serializers_.users import * from .serializers_.permissions import * from .serializers_.tokens import * +from .serializers_.owners import * diff --git a/netbox/users/api/serializers_/owners.py b/netbox/users/api/serializers_/owners.py new file mode 100644 index 00000000000..b67d5b6c8c0 --- /dev/null +++ b/netbox/users/api/serializers_/owners.py @@ -0,0 +1,30 @@ +from netbox.api.fields import SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from users.models import Group, Owner, User +from .users import GroupSerializer, UserSerializer + +__all__ = ( + 'OwnerSerializer', +) + + +class OwnerSerializer(ValidatedModelSerializer): + groups = SerializedPKRelatedField( + queryset=Group.objects.all(), + serializer=GroupSerializer, + nested=True, + required=False, + many=True + ) + users = SerializedPKRelatedField( + queryset=User.objects.all(), + serializer=UserSerializer, + nested=True, + required=False, + many=True + ) + + class Meta: + model = Owner + fields = ('id', 'url', 'display_url', 'display', 'name', 'description', 'groups', 'users') + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/users/api/urls.py b/netbox/users/api/urls.py index 599d0bb6158..87a5fde090f 100644 --- a/netbox/users/api/urls.py +++ b/netbox/users/api/urls.py @@ -7,17 +7,11 @@ router = NetBoxRouter() router.APIRootView = views.UsersRootView -# Users and groups router.register('users', views.UserViewSet) router.register('groups', views.GroupViewSet) - -# Tokens router.register('tokens', views.TokenViewSet) - -# Permissions router.register('permissions', views.ObjectPermissionViewSet) - -# User preferences +router.register('owners', views.OwnerViewSet) router.register('config', views.UserConfigViewSet, basename='userconfig') app_name = 'users-api' diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index bba9a4ec3e2..651c2c8a756 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -12,7 +12,7 @@ from netbox.api.viewsets import NetBoxModelViewSet from users import filtersets -from users.models import Group, ObjectPermission, Token, User, UserConfig +from users.models import Group, ObjectPermission, Owner, Token, User, UserConfig from utilities.data import deepmerge from utilities.querysets import RestrictedQuerySet from . import serializers @@ -88,6 +88,16 @@ class ObjectPermissionViewSet(NetBoxModelViewSet): filterset_class = filtersets.ObjectPermissionFilterSet +# +# Owners +# + +class OwnerViewSet(NetBoxModelViewSet): + queryset = Owner.objects.all() + serializer_class = serializers.OwnerSerializer + filterset_class = filtersets.OwnerFilterSet + + # # User preferences # diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 36fbdcb0df9..8d9abd3dc22 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -6,12 +6,13 @@ from core.models import ObjectType from extras.models import NotificationGroup from netbox.filtersets import BaseFilterSet -from users.models import Group, ObjectPermission, Token, User +from users.models import Group, ObjectPermission, Owner, Token, User from utilities.filters import ContentTypeFilter __all__ = ( 'GroupFilterSet', 'ObjectPermissionFilterSet', + 'OwnerFilterSet', 'TokenFilterSet', 'UserFilterSet', ) @@ -221,3 +222,44 @@ def _check_action(self, queryset, name, value): return queryset.filter(actions__contains=[action]) else: return queryset.exclude(actions__contains=[action]) + + +class OwnerFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + group_id = django_filters.ModelMultipleChoiceFilter( + field_name='groups', + queryset=Group.objects.all(), + label=_('Group (ID)'), + ) + group = django_filters.ModelMultipleChoiceFilter( + field_name='groups__name', + queryset=Group.objects.all(), + to_field_name='name', + label=_('Group (name)'), + ) + user_id = django_filters.ModelMultipleChoiceFilter( + field_name='users', + queryset=User.objects.all(), + label=_('User (ID)'), + ) + user = django_filters.ModelMultipleChoiceFilter( + field_name='users__username', + queryset=User.objects.all(), + to_field_name='username', + label=_('User (username)'), + ) + + class Meta: + model = Owner + fields = ('id', 'name', 'description') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index bca417b3dd2..a31593e734e 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -12,6 +12,7 @@ __all__ = ( 'GroupBulkEditForm', 'ObjectPermissionBulkEditForm', + 'OwnerBulkEditForm', 'UserBulkEditForm', 'TokenBulkEditForm', ) @@ -124,3 +125,21 @@ class TokenBulkEditForm(BulkEditForm): nullable_fields = ( 'expires', 'description', 'allowed_ips', ) + + +class OwnerBulkEditForm(BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Owner.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + + model = Owner + fieldsets = ( + FieldSet('description',), + ) + nullable_fields = ('description',) diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index bdda61a44d4..045461239ab 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -3,10 +3,12 @@ from users.models import * from users.choices import TokenVersionChoices from utilities.forms import CSVModelForm +from utilities.forms.fields import CSVModelMultipleChoiceField __all__ = ( 'GroupImportForm', + 'OwnerImportForm', 'UserImportForm', 'TokenImportForm', ) @@ -50,3 +52,22 @@ class TokenImportForm(CSVModelForm): class Meta: model = Token fields = ('user', 'version', 'token', 'write_enabled', 'expires', 'description',) + + +class OwnerImportForm(CSVModelForm): + groups = CSVModelMultipleChoiceField( + queryset=Group.objects.all(), + required=False, + to_field_name='name', + ) + users = CSVModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + to_field_name='username', + ) + + class Meta: + model = Owner + fields = ( + 'name', 'description', 'groups', 'users', + ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 32e52b5f966..96a7eb317cd 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -4,7 +4,7 @@ from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from users.choices import TokenVersionChoices -from users.models import Group, ObjectPermission, Token, User +from users.models import Group, ObjectPermission, Owner, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet @@ -14,6 +14,7 @@ __all__ = ( 'GroupFilterForm', 'ObjectPermissionFilterForm', + 'OwnerFilterForm', 'TokenFilterForm', 'UserFilterForm', ) @@ -140,3 +141,21 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm): label=_('Last Used'), widget=DateTimePicker() ) + + +class OwnerFilterForm(NetBoxModelFilterSetForm): + model = Owner + fieldsets = ( + FieldSet('q', 'filter_id',), + FieldSet('group_id', 'user_id', name=_('Members')), + ) + group_id = DynamicModelMultipleChoiceField( + queryset=Group.objects.all(), + required=False, + label=_('Group') + ) + user_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User') + ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index cae19433170..4656129b5f8 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -23,11 +23,11 @@ __all__ = ( 'GroupForm', 'ObjectPermissionForm', + 'OwnerForm', 'TokenForm', 'UserConfigForm', 'UserForm', 'UserTokenForm', - 'TokenForm', ) @@ -431,3 +431,18 @@ def save(self, *args, **kwargs): instance.groups.set(self.cleaned_data['groups']) return instance + + +class OwnerForm(forms.ModelForm): + + fieldsets = ( + FieldSet('name', 'description', name=_('Owner')), + FieldSet('groups', name=_('Groups')), + FieldSet('users', name=_('Users')), + ) + + class Meta: + model = Owner + fields = [ + 'name', 'description', 'groups', 'users', + ] diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index 07f28bb8878..bfec7d5fc4d 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -10,6 +10,7 @@ __all__ = ( 'GroupFilter', + 'OwnerFilter', 'UserFilter', ) @@ -31,3 +32,11 @@ class UserFilter(BaseObjectTypeFilterMixin): date_joined: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() last_login: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + + +@strawberry_django.filter_type(models.Owner, lookups=True) +class OwnerFilter(BaseObjectTypeFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index b59266c57eb..cb35f9284fc 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -13,3 +13,6 @@ class UsersQuery: user: UserType = strawberry_django.field() user_list: List[UserType] = strawberry_django.field() + + owner: OwnerType = strawberry_django.field() + owner_list: List[OwnerType] = strawberry_django.field() diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 5231194e561..d8edfcb44b2 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -3,11 +3,12 @@ import strawberry_django from netbox.graphql.types import BaseObjectType -from users.models import Group, User +from users.models import Group, Owner, User from .filters import * __all__ = ( 'GroupType', + 'OwnerType', 'UserType', ) @@ -32,3 +33,13 @@ class GroupType(BaseObjectType): ) class UserType(BaseObjectType): groups: List[GroupType] + + +@strawberry_django.type( + Owner, + fields=['id', 'name', 'description', 'groups', 'users'], + filters=OwnerFilter, + pagination=True +) +class OwnerType(BaseObjectType): + pass diff --git a/netbox/users/migrations/0015_owner.py b/netbox/users/migrations/0015_owner.py new file mode 100644 index 00000000000..cec3034e2f2 --- /dev/null +++ b/netbox/users/migrations/0015_owner.py @@ -0,0 +1,43 @@ +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0014_users_token_v2'), + ] + + operations = [ + migrations.CreateModel( + name='Owner', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=150, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ( + 'groups', + models.ManyToManyField( + blank=True, + related_name='owners', + related_query_name='owner', + to='users.group', + ) + ), + ( + 'users', + models.ManyToManyField( + blank=True, + related_name='owners', + related_query_name='owner', + to=settings.AUTH_USER_MODEL, + ) + ), + ], + options={ + 'verbose_name': 'owner', + 'verbose_name_plural': 'owners', + 'ordering': ('name',), + }, + ), + ] diff --git a/netbox/users/models/__init__.py b/netbox/users/models/__init__.py index 62a7b93fe8e..c6223e99652 100644 --- a/netbox/users/models/__init__.py +++ b/netbox/users/models/__init__.py @@ -2,3 +2,4 @@ from .preferences import * from .tokens import * from .permissions import * +from .owners import * diff --git a/netbox/users/models/owners.py b/netbox/users/models/owners.py new file mode 100644 index 00000000000..6765d3034ef --- /dev/null +++ b/netbox/users/models/owners.py @@ -0,0 +1,49 @@ +from django.db import models +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from utilities.querysets import RestrictedQuerySet + +__all__ = ( + 'Owner', +) + + +class Owner(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=150, + unique=True, + ) + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True + ) + groups = models.ManyToManyField( + to='users.Group', + verbose_name=_('groups'), + blank=True, + related_name='owners', + related_query_name='owner', + ) + users = models.ManyToManyField( + to='users.User', + verbose_name=_('users'), + blank=True, + related_name='owners', + related_query_name='owner', + ) + + objects = RestrictedQuerySet.as_manager() + + class Meta: + ordering = ('name',) + verbose_name = _('owner') + verbose_name_plural = _('owners') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('users:owner', args=[self.pk]) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 2b4bd745fc9..17460dc77af 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -2,11 +2,12 @@ from django.utils.translation import gettext as _ from netbox.tables import NetBoxTable, columns -from users.models import Group, ObjectPermission, Token, User +from users.models import Group, ObjectPermission, Owner, Token, User __all__ = ( 'GroupTable', 'ObjectPermissionTable', + 'OwnerTable', 'TokenTable', 'UserTable', ) @@ -143,3 +144,27 @@ class Meta(NetBoxTable.Meta): default_columns = ( 'pk', 'name', 'enabled', 'object_types', 'can_view', 'can_add', 'can_change', 'can_delete', 'description', ) + + +class OwnerTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + groups = columns.ManyToManyColumn( + verbose_name=_('Groups'), + linkify_item=('users:group', {'pk': tables.A('pk')}) + ) + users = columns.ManyToManyColumn( + verbose_name=_('Groups'), + linkify_item=('users:group', {'pk': tables.A('pk')}) + ) + actions = columns.ActionsColumn( + actions=('edit', 'delete'), + ) + + class Meta(NetBoxTable.Meta): + model = Owner + fields = ( + 'pk', 'id', 'name', 'description', 'groups', 'users', + ) diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 83f12070201..9fa24bc7ecc 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -18,4 +18,7 @@ path('permissions/', include(get_model_urls('users', 'objectpermission', detail=False))), path('permissions//', include(get_model_urls('users', 'objectpermission'))), + path('owners/', include(get_model_urls('users', 'owner', detail=False))), + path('owners//', include(get_model_urls('users', 'owner'))), + ] diff --git a/netbox/users/views.py b/netbox/users/views.py index 9071c6c8bfa..60d1cdfc137 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -6,7 +6,7 @@ from netbox.views import generic from utilities.views import register_model_view from . import filtersets, forms, tables -from .models import Group, User, ObjectPermission, Token +from .models import Group, User, ObjectPermission, Owner, Token # @@ -231,3 +231,60 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView): queryset = ObjectPermission.objects.all() filterset = filtersets.ObjectPermissionFilterSet table = tables.ObjectPermissionTable + + +# +# Owners +# + +@register_model_view(Owner, 'list', path='', detail=False) +class OwnerListView(generic.ObjectListView): + queryset = Owner.objects.all() + filterset = filtersets.OwnerFilterSet + filterset_form = forms.OwnerFilterForm + table = tables.OwnerTable + + +@register_model_view(Owner) +class OwnerView(generic.ObjectView): + queryset = Owner.objects.all() + template_name = 'users/owner.html' + + +@register_model_view(Owner, 'add', detail=False) +@register_model_view(Owner, 'edit') +class OwnerEditView(generic.ObjectEditView): + queryset = Owner.objects.all() + form = forms.OwnerForm + + +@register_model_view(Owner, 'delete') +class OwnerDeleteView(generic.ObjectDeleteView): + queryset = Owner.objects.all() + + +@register_model_view(Owner, 'bulk_import', path='import', detail=False) +class OwnerBulkImportView(generic.BulkImportView): + queryset = Owner.objects.all() + model_form = forms.OwnerImportForm + + +@register_model_view(Owner, 'bulk_edit', path='edit', detail=False) +class OwnerBulkEditView(generic.BulkEditView): + queryset = Owner.objects.all() + filterset = filtersets.OwnerFilterSet + table = tables.OwnerTable + form = forms.OwnerBulkEditForm + + +@register_model_view(Owner, 'bulk_rename', path='rename', detail=False) +class OwnerBulkRenameView(generic.BulkRenameView): + queryset = Owner.objects.all() + field_name = 'ownername' + + +@register_model_view(Owner, 'bulk_delete', path='delete', detail=False) +class OwnerBulkDeleteView(generic.BulkDeleteView): + queryset = Owner.objects.all() + filterset = filtersets.OwnerFilterSet + table = tables.OwnerTable From 27ddccbdf86c389720524c481692743fc0d6cd7b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 17 Oct 2025 16:15:04 -0400 Subject: [PATCH 02/40] Add owner fields to applicable models --- netbox/circuits/forms/model_forms.py | 16 +- netbox/circuits/migrations/0053_owner.py | 68 +++++ netbox/core/forms/model_forms.py | 3 +- netbox/core/migrations/0020_owner.py | 24 ++ netbox/dcim/forms/model_forms.py | 72 +++--- netbox/dcim/migrations/0216_owner.py | 243 ++++++++++++++++++ netbox/dcim/models/device_components.py | 3 +- netbox/extras/forms/model_forms.py | 11 +- netbox/extras/migrations/0134_owner.py | 89 +++++++ netbox/extras/models/configs.py | 11 +- netbox/extras/models/customfields.py | 5 +- netbox/extras/models/models.py | 18 +- netbox/extras/models/tags.py | 3 +- netbox/ipam/forms/model_forms.py | 32 +-- netbox/ipam/migrations/0083_owner.py | 124 +++++++++ netbox/netbox/forms/base.py | 13 +- netbox/netbox/forms/mixins.py | 15 +- netbox/netbox/models/__init__.py | 7 +- netbox/netbox/models/features.py | 1 + netbox/netbox/models/mixins.py | 17 ++ netbox/templates/htmx/form.html | 9 + netbox/tenancy/forms/model_forms.py | 10 +- netbox/tenancy/migrations/0021_owner.py | 47 ++++ netbox/virtualization/forms/model_forms.py | 12 +- .../virtualization/migrations/0049_owner.py | 54 ++++ .../virtualization/models/virtualmachines.py | 3 +- netbox/vpn/forms/model_forms.py | 18 +- netbox/vpn/migrations/0010_owner.py | 68 +++++ netbox/wireless/forms/model_forms.py | 6 +- netbox/wireless/migrations/0016_owner.py | 33 +++ 30 files changed, 926 insertions(+), 109 deletions(-) create mode 100644 netbox/circuits/migrations/0053_owner.py create mode 100644 netbox/core/migrations/0020_owner.py create mode 100644 netbox/dcim/migrations/0216_owner.py create mode 100644 netbox/extras/migrations/0134_owner.py create mode 100644 netbox/ipam/migrations/0083_owner.py create mode 100644 netbox/tenancy/migrations/0021_owner.py create mode 100644 netbox/virtualization/migrations/0049_owner.py create mode 100644 netbox/vpn/migrations/0010_owner.py create mode 100644 netbox/wireless/migrations/0016_owner.py diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index ce09862ae2a..1b6c3feb4c1 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -52,7 +52,7 @@ class ProviderForm(NetBoxModelForm): class Meta: model = Provider fields = [ - 'name', 'slug', 'asns', 'description', 'comments', 'tags', + 'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags', ] @@ -68,7 +68,7 @@ class ProviderAccountForm(NetBoxModelForm): class Meta: model = ProviderAccount fields = [ - 'provider', 'name', 'account', 'description', 'comments', 'tags', + 'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags', ] @@ -88,7 +88,7 @@ class ProviderNetworkForm(NetBoxModelForm): class Meta: model = ProviderNetwork fields = [ - 'provider', 'name', 'service_id', 'description', 'comments', 'tags', + 'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags', ] @@ -96,7 +96,7 @@ class CircuitTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - FieldSet('name', 'slug', 'color', 'description', 'tags'), + FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'), ) class Meta: @@ -147,7 +147,7 @@ class Meta: model = Circuit fields = [ 'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate', - 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags', + 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags', ] widgets = { 'install_date': DatePicker(), @@ -244,7 +244,7 @@ class CircuitGroupForm(TenancyForm, NetBoxModelForm): class Meta: model = CircuitGroup fields = [ - 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags', + 'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags', ] @@ -317,7 +317,7 @@ class VirtualCircuitTypeForm(NetBoxModelForm): class Meta: model = VirtualCircuitType fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -350,7 +350,7 @@ class Meta: model = VirtualCircuit fields = [ 'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] diff --git a/netbox/circuits/migrations/0053_owner.py b/netbox/circuits/migrations/0053_owner.py new file mode 100644 index 00000000000..04fe46c6172 --- /dev/null +++ b/netbox/circuits/migrations/0053_owner.py @@ -0,0 +1,68 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('circuits', '0052_extend_circuit_abs_distance_upper_limit'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='circuit', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='circuitgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='circuittype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='provider', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='provideraccount', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='providernetwork', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualcircuit', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualcircuittype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 0a683a381e3..6796b4f8599 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -36,7 +36,8 @@ class DataSourceForm(NetBoxModelForm): class Meta: model = DataSource fields = [ - 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'comments', 'tags', + 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'owner', + 'comments', 'tags', ] widgets = { 'ignore_rules': forms.Textarea( diff --git a/netbox/core/migrations/0020_owner.py b/netbox/core/migrations/0020_owner.py new file mode 100644 index 00000000000..ecb30fa3e63 --- /dev/null +++ b/netbox/core/migrations/0020_owner.py @@ -0,0 +1,24 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_configrevision_active'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='datasource', + name='owner', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='+', + to='users.owner', + ), + ), + ] diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 32ea2d26361..4e3037cfb46 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -91,7 +91,7 @@ class RegionForm(NetBoxModelForm): class Meta: model = Region fields = ( - 'parent', 'name', 'slug', 'description', 'tags', 'comments', + 'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments', ) @@ -111,7 +111,7 @@ class SiteGroupForm(NetBoxModelForm): class Meta: model = SiteGroup fields = ( - 'parent', 'name', 'slug', 'description', 'comments', 'tags', + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ) @@ -154,7 +154,7 @@ class Meta: model = Site fields = ( 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags', ) widgets = { 'physical_address': forms.Textarea( @@ -195,8 +195,8 @@ class LocationForm(TenancyForm, NetBoxModelForm): class Meta: model = Location fields = ( - 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', - 'facility', 'tags', 'comments', + 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner', + 'comments', 'tags', ) @@ -210,7 +210,7 @@ class RackRoleForm(NetBoxModelForm): class Meta: model = RackRole fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -242,7 +242,7 @@ class Meta: fields = [ 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'weight_unit', 'description', 'owner', 'comments', 'tags', ] @@ -288,7 +288,7 @@ class Meta: 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'weight_unit', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -343,7 +343,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): class Meta: model = RackReservation fields = [ - 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] @@ -357,7 +357,7 @@ class ManufacturerForm(NetBoxModelForm): class Meta: model = Manufacturer fields = [ - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ] @@ -396,7 +396,7 @@ class Meta: fields = [ 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', - 'description', 'comments', 'tags', + 'description', 'owner', 'comments', 'tags', ] widgets = { 'front_image': ClearableFileInput(attrs={ @@ -423,7 +423,7 @@ class ModuleTypeProfileForm(NetBoxModelForm): class Meta: model = ModuleTypeProfile fields = [ - 'name', 'description', 'schema', 'comments', 'tags', + 'name', 'description', 'schema', 'owner', 'comments', 'tags', ] @@ -452,7 +452,7 @@ class Meta: model = ModuleType fields = [ 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -531,7 +531,7 @@ class DeviceRoleForm(NetBoxModelForm): class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags', + 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags', ] @@ -567,7 +567,7 @@ class PlatformForm(NetBoxModelForm): class Meta: model = Platform fields = [ - 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'comments', 'tags', + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags', ] @@ -677,7 +677,7 @@ class Meta: 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', - 'comments', 'tags', 'local_context_data', + 'owner', 'comments', 'tags', 'local_context_data', ] def __init__(self, *args, **kwargs): @@ -788,7 +788,7 @@ class Meta: model = Module fields = [ 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components', - 'adopt_components', 'description', 'comments', + 'adopt_components', 'description', 'owner', 'comments', ] def __init__(self, *args, **kwargs): @@ -828,7 +828,7 @@ class Meta: model = Cable fields = [ 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', - 'length', 'length_unit', 'description', 'comments', 'tags', + 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', ] @@ -855,7 +855,7 @@ class PowerPanelForm(NetBoxModelForm): class Meta: model = PowerPanel fields = [ - 'site', 'location', 'name', 'description', 'comments', 'tags', + 'site', 'location', 'name', 'description', 'owner', 'comments', 'tags', ] @@ -887,7 +887,7 @@ class Meta: model = PowerFeed fields = [ 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', - 'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags' + 'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' ] @@ -906,7 +906,7 @@ class VirtualChassisForm(NetBoxModelForm): class Meta: model = VirtualChassis fields = [ - 'name', 'domain', 'master', 'description', 'comments', 'tags', + 'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags', ] widgets = { 'master': SelectWithPK(), @@ -1396,7 +1396,7 @@ class ConsolePortForm(ModularDeviceComponentForm): class Meta: model = ConsolePort fields = [ - 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', ] @@ -1410,7 +1410,7 @@ class ConsoleServerPortForm(ModularDeviceComponentForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', ] @@ -1426,7 +1426,7 @@ class Meta: model = PowerPort fields = [ 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ] @@ -1443,7 +1443,7 @@ class PowerOutletForm(ModularDeviceComponentForm): fieldsets = ( FieldSet( 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ), ) @@ -1587,7 +1587,7 @@ class Meta: 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address', - 'tags', + 'owner', 'tags', ] widgets = { 'speed': NumberWithOptions( @@ -1619,7 +1619,7 @@ class Meta: model = FrontPort fields = [ 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ] @@ -1633,7 +1633,8 @@ class RearPortForm(ModularDeviceComponentForm): class Meta: model = RearPort fields = [ - 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', + 'tags', ] @@ -1645,7 +1646,7 @@ class ModuleBayForm(ModularDeviceComponentForm): class Meta: model = ModuleBay fields = [ - 'device', 'module', 'name', 'label', 'position', 'description', 'tags', + 'device', 'module', 'name', 'label', 'position', 'description', 'owner', 'tags', ] @@ -1657,7 +1658,7 @@ class DeviceBayForm(DeviceComponentForm): class Meta: model = DeviceBay fields = [ - 'device', 'name', 'label', 'description', 'tags', + 'device', 'name', 'label', 'description', 'owner', 'tags', ] @@ -1782,7 +1783,7 @@ class Meta: model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'status', 'description', 'tags', + 'status', 'description', 'owner', 'tags', ] def __init__(self, *args, **kwargs): @@ -1828,9 +1829,6 @@ def clean(self): self.instance.component = None -# Device component roles -# - class InventoryItemRoleForm(NetBoxModelForm): slug = SlugField() @@ -1841,7 +1839,7 @@ class InventoryItemRoleForm(NetBoxModelForm): class Meta: model = InventoryItemRole fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -1881,7 +1879,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): class Meta: model = VirtualDeviceContext fields = [ - 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', + 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner', 'comments', 'tags' ] @@ -1929,7 +1927,7 @@ class MACAddressForm(NetBoxModelForm): class Meta: model = MACAddress fields = [ - 'mac_address', 'interface', 'vminterface', 'description', 'tags', + 'mac_address', 'interface', 'vminterface', 'description', 'owner', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0216_owner.py b/netbox/dcim/migrations/0216_owner.py new file mode 100644 index 00000000000..a7c5aa899af --- /dev/null +++ b/netbox/dcim/migrations/0216_owner.py @@ -0,0 +1,243 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0215_rackreservation_status'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='cable', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='consoleport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='consoleserverport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='device', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicebay', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicerole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicetype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='frontport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='interface', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='inventoryitem', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='inventoryitemrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='location', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='macaddress', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='manufacturer', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='module', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='modulebay', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='moduletype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='moduletypeprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='platform', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerfeed', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='poweroutlet', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerpanel', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rack', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rackreservation', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rackrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='racktype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rearport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='region', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='site', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='sitegroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualchassis', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualdevicecontext', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 9c44e04942c..3e801a8e9d4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -14,6 +14,7 @@ from dcim.models.mixins import InterfaceValidationMixin from netbox.choices import ColorChoices from netbox.models import OrganizationalModel, NetBoxModel +from netbox.models.mixins import OwnerMixin from utilities.fields import ColorField, NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface @@ -40,7 +41,7 @@ ) -class ComponentModel(NetBoxModel): +class ComponentModel(OwnerMixin, NetBoxModel): """ An abstract model inherited by any model which has a parent Device. """ diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 37ee10604e3..39f02f5b1b2 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -179,7 +179,7 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, forms.ModelForm): class Meta: model = CustomFieldChoiceSet - fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically') + fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner') def __init__(self, *args, initial=None, **kwargs): super().__init__(*args, initial=initial, **kwargs) @@ -480,7 +480,7 @@ class Meta: model = EventRule fields = ( 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type', - 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags' + 'action_object_type', 'action_object_id', 'action_data', 'owner', 'comments', 'tags' ) widgets = { 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), @@ -582,7 +582,7 @@ class TagForm(ChangelogMessageMixin, forms.ModelForm): class Meta: model = Tag fields = [ - 'name', 'slug', 'color', 'weight', 'description', 'object_types', + 'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner', ] @@ -606,7 +606,8 @@ class ConfigContextProfileForm(SyncedDataMixin, NetBoxModelForm): class Meta: model = ConfigContextProfile fields = ( - 'name', 'description', 'schema', 'data_source', 'data_file', 'auto_sync_enabled', 'comments', 'tags', + 'name', 'description', 'schema', 'data_source', 'data_file', 'auto_sync_enabled', 'owner', 'comments', + 'tags', ) @@ -701,7 +702,7 @@ class Meta: fields = ( 'name', 'weight', 'profile', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', - 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled', + 'tenant_groups', 'tenants', 'owner', 'tags', 'data_source', 'data_file', 'auto_sync_enabled', ) def __init__(self, *args, initial=None, **kwargs): diff --git a/netbox/extras/migrations/0134_owner.py b/netbox/extras/migrations/0134_owner.py new file mode 100644 index 00000000000..1a01fd95b12 --- /dev/null +++ b/netbox/extras/migrations/0134_owner.py @@ -0,0 +1,89 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0133_make_cf_minmax_decimal'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='configcontextprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='configtemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customfield', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customfieldchoiceset', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customlink', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='eventrule', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='exporttemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='savedfilter', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tag', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='webhook', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index a9d233568e6..ea861e67384 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -13,6 +13,7 @@ from extras.querysets import ConfigContextQuerySet from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin +from netbox.models.mixins import OwnerMixin from utilities.data import deepmerge from utilities.jsonschema import validate_schema @@ -68,7 +69,7 @@ def sync_data(self): sync_data.alters_data = True -class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel): +class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, OwnerMixin, ChangeLoggedModel): """ A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B @@ -266,7 +267,13 @@ def clean(self): # class ConfigTemplate( - RenderTemplateMixin, SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel + RenderTemplateMixin, + SyncedDataMixin, + CustomLinksMixin, + ExportTemplatesMixin, + OwnerMixin, + TagsMixin, + ChangeLoggedModel, ): name = models.CharField( verbose_name=_('name'), diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index b1d22ee0b48..3dea7c1b0d8 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -21,6 +21,7 @@ from extras.data import CHOICE_SETS from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin +from netbox.models.mixins import OwnerMixin from netbox.search import FieldTypes from utilities import filters from utilities.datetime import datetime_from_timestamp @@ -70,7 +71,7 @@ def get_defaults_for_model(self, model): } -class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomField(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): object_types = models.ManyToManyField( to='contenttypes.ContentType', related_name='custom_fields', @@ -773,7 +774,7 @@ def validate(self, value): raise ValidationError(_("Required field cannot be empty.")) -class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ Represents a set of choices available for choice and multi-choice custom fields. """ diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 7361d087dec..52ced18351a 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -25,6 +25,7 @@ from netbox.models.features import ( CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, has_feature ) +from netbox.models.mixins import OwnerMixin from utilities.html import clean_html from utilities.jinja2 import render_jinja2 from utilities.querydict import dict_to_querydict @@ -44,7 +45,7 @@ ) -class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): +class EventRule(CustomFieldsMixin, ExportTemplatesMixin, OwnerMixin, TagsMixin, ChangeLoggedModel): """ An EventRule defines an action to be taken automatically in response to a specific set of events, such as when a specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a @@ -155,7 +156,7 @@ def eval_conditions(self, data): return False -class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): +class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, OwnerMixin, ChangeLoggedModel): """ A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or delete in NetBox. The request will contain a representation of the object, which the remote application can act on. @@ -294,7 +295,7 @@ def render_payload_url(self, context): return render_jinja2(self.payload_url, context) -class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomLink(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. @@ -394,7 +395,14 @@ def render(self, context): } -class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderTemplateMixin): +class ExportTemplate( + SyncedDataMixin, + CloningMixin, + ExportTemplatesMixin, + OwnerMixin, + ChangeLoggedModel, + RenderTemplateMixin, +): object_types = models.ManyToManyField( to='contenttypes.ContentType', related_name='export_templates', @@ -456,7 +464,7 @@ def get_context(self, context=None, queryset=None): return _context -class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class SavedFilter(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ A set of predefined keyword parameters that can be reused to filter for specific objects. """ diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 0df76d7b3a1..dc98ae65bd6 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -8,6 +8,7 @@ from netbox.choices import ColorChoices from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin +from netbox.models.mixins import OwnerMixin from utilities.fields import ColorField from utilities.querysets import RestrictedQuerySet @@ -21,7 +22,7 @@ # Tags # -class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): +class Tag(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel, TagBase): id = models.BigAutoField( primary_key=True ) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 399198c52b0..1e2f23da683 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -72,7 +72,7 @@ class Meta: model = VRF fields = [ 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] labels = { 'rd': "RD", @@ -89,7 +89,7 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm): class Meta: model = RouteTarget fields = [ - 'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'name', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] @@ -103,7 +103,7 @@ class RIRForm(NetBoxModelForm): class Meta: model = RIR fields = [ - 'name', 'slug', 'is_private', 'description', 'tags', + 'name', 'slug', 'is_private', 'description', 'owner', 'tags', ] @@ -123,7 +123,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm): class Meta: model = Aggregate fields = [ - 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] widgets = { 'date_added': DatePicker(), @@ -145,7 +145,7 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm): class Meta: model = ASNRange fields = [ - 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags' + 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'tags' ] @@ -170,7 +170,7 @@ class ASNForm(TenancyForm, NetBoxModelForm): class Meta: model = ASN fields = [ - 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags' + 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' ] widgets = { 'date_added': DatePicker(), @@ -198,7 +198,7 @@ class RoleForm(NetBoxModelForm): class Meta: model = Role fields = [ - 'name', 'slug', 'weight', 'description', 'tags', + 'name', 'slug', 'weight', 'description', 'owner', 'tags', ] @@ -238,7 +238,7 @@ class Meta: model = Prefix fields = [ 'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'scope_type', 'tenant_group', - 'tenant', 'description', 'comments', 'tags', + 'tenant', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -276,7 +276,7 @@ class Meta: model = IPRange fields = [ 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated', - 'mark_utilized', 'description', 'comments', 'tags', + 'mark_utilized', 'description', 'owner', 'comments', 'tags', ] @@ -344,7 +344,7 @@ class Meta: model = IPAddress fields = [ 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside', - 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -523,7 +523,7 @@ class Meta: model = FHRPGroup fields = ( 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ) def save(self, *args, **kwargs): @@ -628,7 +628,7 @@ class VLANGroupForm(TenancyForm, NetBoxModelForm): class Meta: model = VLANGroup fields = [ - 'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'tags', + 'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'owner', 'tags', ] def __init__(self, *args, **kwargs): @@ -704,7 +704,7 @@ class Meta: model = VLAN fields = [ 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan', - 'description', 'comments', 'tags', + 'description', 'owner', 'comments', 'tags', ] @@ -717,7 +717,7 @@ class VLANTranslationPolicyForm(NetBoxModelForm): class Meta: model = VLANTranslationPolicy fields = [ - 'name', 'description', 'tags', + 'name', 'description', 'owner', 'tags', ] @@ -756,7 +756,7 @@ class ServiceTemplateForm(NetBoxModelForm): class Meta: model = ServiceTemplate - fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags') + fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags') class ServiceForm(NetBoxModelForm): @@ -799,7 +799,7 @@ class ServiceForm(NetBoxModelForm): class Meta: model = Service fields = [ - 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags', + 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'owner', 'comments', 'tags', 'parent_object_type', ] diff --git a/netbox/ipam/migrations/0083_owner.py b/netbox/ipam/migrations/0083_owner.py new file mode 100644 index 00000000000..307963ba00b --- /dev/null +++ b/netbox/ipam/migrations/0083_owner.py @@ -0,0 +1,124 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('ipam', '0082_add_prefix_network_containment_indexes'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='aggregate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='asn', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='asnrange', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='fhrpgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipaddress', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='iprange', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='prefix', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rir', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='role', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='routetarget', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='service', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='servicetemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlan', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlangroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlantranslationpolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vrf', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 14916a7337f..2eaf8a83a28 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -11,7 +11,7 @@ from utilities.forms import BulkEditForm, CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.mixins import CheckLastUpdatedMixin -from .mixins import ChangelogMessageMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin +from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, SavedFiltersMixin, TagsMixin __all__ = ( 'NetBoxModelForm', @@ -21,7 +21,14 @@ ) -class NetBoxModelForm(ChangelogMessageMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): +class NetBoxModelForm( + ChangelogMessageMixin, + CheckLastUpdatedMixin, + CustomFieldsMixin, + OwnerMixin, + TagsMixin, + forms.ModelForm +): """ Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. @@ -100,7 +107,7 @@ def _get_form_field(self, customfield): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, BulkEditForm): +class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, BulkEditForm): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 4096ffb256f..4ee11b0bbf7 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -4,11 +4,13 @@ from core.models import ObjectType from extras.choices import * from extras.models import * -from utilities.forms.fields import DynamicModelMultipleChoiceField +from users.models import Owner +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( 'ChangelogMessageMixin', 'CustomFieldsMixin', + 'OwnerMixin', 'SavedFiltersMixin', 'TagsMixin', ) @@ -118,3 +120,14 @@ def __init__(self, *args, **kwargs): object_type = ObjectType.objects.get_for_model(self._meta.model) if object_type and hasattr(self.fields['tags'].widget, 'add_query_param'): self.fields['tags'].widget.add_query_param('for_object_type_id', object_type.pk) + + +class OwnerMixin(forms.Form): + """ + Add an `owner` field to forms for models which support Owner assignment. + """ + owner = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b067181366a..59475b06031 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -8,6 +8,7 @@ from mptt.models import MPTTModel, TreeForeignKey from netbox.models.features import * +from netbox.models.mixins import OwnerMixin from utilities.mptt import TreeManager from utilities.querysets import RestrictedQuerySet from utilities.views import get_viewname @@ -120,7 +121,7 @@ class Meta: # NetBox internal base models # -class PrimaryModel(NetBoxModel): +class PrimaryModel(OwnerMixin, NetBoxModel): """ Primary models represent real objects within the infrastructure being modeled. """ @@ -138,7 +139,7 @@ class Meta: abstract = True -class NestedGroupModel(NetBoxFeatureSet, MPTTModel): +class NestedGroupModel(NetBoxFeatureSet, OwnerMixin, MPTTModel): """ Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest recursively using MPTT. Within each parent, each child instance must have a unique name. @@ -190,7 +191,7 @@ def clean(self): }) -class OrganizationalModel(NetBoxModel): +class OrganizationalModel(OwnerMixin, NetBoxModel): """ Organizational models are those which are used solely to categorize and qualify other objects, and do not convey any real information about the infrastructure being modeled (for example, functional device roles). Organizational diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index e0d03d6e78b..df4284f28e1 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -641,6 +641,7 @@ def sync_data(self): register_model_feature('jobs', lambda model: issubclass(model, JobsMixin)) register_model_feature('journaling', lambda model: issubclass(model, JournalingMixin)) register_model_feature('notifications', lambda model: issubclass(model, NotificationsMixin)) +register_model_feature('owner', lambda model: issubclass(model, OwnerMixin)) register_model_feature('synced_data', lambda model: issubclass(model, SyncedDataMixin)) register_model_feature('tags', lambda model: issubclass(model, TagsMixin)) diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py index 13af8aaf55b..10798796ddf 100644 --- a/netbox/netbox/models/mixins.py +++ b/netbox/netbox/models/mixins.py @@ -7,10 +7,27 @@ __all__ = ( 'DistanceMixin', + 'OwnerMixin', 'WeightMixin', ) +class OwnerMixin(models.Model): + """ + Adds a ForeignKey to users.Owner to indicate an object's owner. + """ + owner = models.ForeignKey( + to='users.Owner', + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + + class Meta: + abstract = True + + class WeightMixin(models.Model): weight = models.DecimalField( verbose_name=_('weight'), diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 40b795d1178..c4022cbc2eb 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -22,6 +22,15 @@

{% trans "Custom Fields" %}

{% endif %} + {% if form.owner %} +
+
+

{% trans "Ownership" %}

+
+ {% render_field form.owner %} +
+ {% endif %} + {% if form.comments %}
{% render_field form.comments %} diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 5b1bd7339b0..e442a9418a5 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -36,7 +36,7 @@ class TenantGroupForm(NetBoxModelForm): class Meta: model = TenantGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', 'comments' + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ] @@ -56,7 +56,7 @@ class TenantForm(NetBoxModelForm): class Meta: model = Tenant fields = ( - 'name', 'slug', 'group', 'description', 'comments', 'tags', + 'name', 'slug', 'group', 'description', 'owner', 'comments', 'tags', ) @@ -79,7 +79,7 @@ class ContactGroupForm(NetBoxModelForm): class Meta: model = ContactGroup - fields = ('parent', 'name', 'slug', 'description', 'tags', 'comments') + fields = ('parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags') class ContactRoleForm(NetBoxModelForm): @@ -91,7 +91,7 @@ class ContactRoleForm(NetBoxModelForm): class Meta: model = ContactRole - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') class ContactForm(NetBoxModelForm): @@ -117,7 +117,7 @@ class ContactForm(NetBoxModelForm): class Meta: model = Contact fields = ( - 'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags', + 'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'owner', 'comments', 'tags', ) widgets = { 'address': forms.Textarea(attrs={'rows': 3}), diff --git a/netbox/tenancy/migrations/0021_owner.py b/netbox/tenancy/migrations/0021_owner.py new file mode 100644 index 00000000000..b6fedda8830 --- /dev/null +++ b/netbox/tenancy/migrations/0021_owner.py @@ -0,0 +1,47 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('tenancy', '0020_remove_contactgroupmembership'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='contact', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='contactgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='contactrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tenant', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tenantgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 82f8d83157b..2e47631a7ca 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -42,7 +42,7 @@ class ClusterTypeForm(NetBoxModelForm): class Meta: model = ClusterType fields = ( - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ) @@ -56,7 +56,7 @@ class ClusterGroupForm(NetBoxModelForm): class Meta: model = ClusterGroup fields = ( - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ) @@ -83,7 +83,7 @@ class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm): class Meta: model = Cluster fields = ( - 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'description', 'comments', 'tags', + 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'description', 'owner', 'comments', 'tags', ) @@ -236,7 +236,7 @@ class Meta: model = VirtualMachine fields = [ 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', - 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'comments', 'tags', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner', 'comments', 'tags', 'local_context_data', 'config_template', ] @@ -387,7 +387,7 @@ class Meta: fields = [ 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address', - 'tags', + 'owner', 'tags', ] labels = { 'mode': _('802.1Q Mode'), @@ -406,5 +406,5 @@ class VirtualDiskForm(VMComponentForm): class Meta: model = VirtualDisk fields = [ - 'virtual_machine', 'name', 'size', 'description', 'tags', + 'virtual_machine', 'name', 'size', 'description', 'owner', 'tags', ] diff --git a/netbox/virtualization/migrations/0049_owner.py b/netbox/virtualization/migrations/0049_owner.py new file mode 100644 index 00000000000..657d325ec08 --- /dev/null +++ b/netbox/virtualization/migrations/0049_owner.py @@ -0,0 +1,54 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.AddField( + model_name='cluster', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='clustergroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='clustertype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualdisk', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualmachine', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vminterface', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index aca2a7dbd19..de6fde745f9 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -15,6 +15,7 @@ from netbox.config import get_config from netbox.models import NetBoxModel, PrimaryModel from netbox.models.features import ContactsMixin, ImageAttachmentsMixin +from netbox.models.mixins import OwnerMixin from utilities.fields import CounterCacheField, NaturalOrderingField from utilities.ordering import naturalize_interface from utilities.query_functions import CollateAsChar @@ -263,7 +264,7 @@ def primary_ip(self): # -class ComponentModel(NetBoxModel): +class ComponentModel(OwnerMixin, NetBoxModel): """ An abstract model inherited by any model which has a parent VirtualMachine. """ diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 1bf5b580c07..241ac9c382c 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -39,7 +39,7 @@ class TunnelGroupForm(NetBoxModelForm): class Meta: model = TunnelGroup fields = [ - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ] @@ -67,7 +67,7 @@ class Meta: model = Tunnel fields = [ 'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group', - 'tenant', 'comments', 'tags', + 'tenant', 'owner', 'comments', 'tags', ] @@ -307,7 +307,7 @@ class Meta: model = IKEProposal fields = [ 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', - 'sa_lifetime', 'comments', 'tags', + 'sa_lifetime', 'owner', 'comments', 'tags', ] @@ -326,7 +326,7 @@ class IKEPolicyForm(NetBoxModelForm): class Meta: model = IKEPolicy fields = [ - 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', 'tags', + 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'owner', 'comments', 'tags', ] @@ -344,7 +344,7 @@ class Meta: model = IPSecProposal fields = [ 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', - 'sa_lifetime_data', 'comments', 'tags', + 'sa_lifetime_data', 'owner', 'comments', 'tags', ] @@ -363,7 +363,7 @@ class IPSecPolicyForm(NetBoxModelForm): class Meta: model = IPSecPolicy fields = [ - 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', + 'name', 'description', 'proposals', 'pfs_group', 'owner', 'comments', 'tags', ] @@ -386,7 +386,7 @@ class IPSecProfileForm(NetBoxModelForm): class Meta: model = IPSecProfile fields = [ - 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', + 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'owner', 'comments', 'tags', ] @@ -417,8 +417,8 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): class Meta: model = L2VPN fields = ( - 'name', 'slug', 'type', 'status', 'identifier', 'import_targets', 'export_targets', 'tenant', - 'description', 'comments', 'tags' + 'name', 'slug', 'type', 'status', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description', + 'owner', 'comments', 'tags' ) diff --git a/netbox/vpn/migrations/0010_owner.py b/netbox/vpn/migrations/0010_owner.py new file mode 100644 index 00000000000..135084f84f4 --- /dev/null +++ b/netbox/vpn/migrations/0010_owner.py @@ -0,0 +1,68 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('vpn', '0009_remove_redundant_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='ikepolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ikeproposal', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecpolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecproposal', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='l2vpn', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tunnel', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tunnelgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 08f418e3c75..b928745645d 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -34,7 +34,7 @@ class WirelessLANGroupForm(NetBoxModelForm): class Meta: model = WirelessLANGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', 'comments', + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ] @@ -64,7 +64,7 @@ class Meta: model = WirelessLAN fields = [ 'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'scope_type', 'description', 'comments', 'tags', + 'scope_type', 'description', 'owner', 'comments', 'tags', ] widgets = { 'auth_psk': PasswordInput( @@ -181,7 +181,7 @@ class Meta: fields = [ 'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b', 'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'distance', 'distance_unit', 'description', 'comments', 'tags', + 'distance', 'distance_unit', 'description', 'owner', 'comments', 'tags', ] widgets = { 'auth_psk': PasswordInput( diff --git a/netbox/wireless/migrations/0016_owner.py b/netbox/wireless/migrations/0016_owner.py new file mode 100644 index 00000000000..08167290c17 --- /dev/null +++ b/netbox/wireless/migrations/0016_owner.py @@ -0,0 +1,33 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('wireless', '0015_extend_wireless_link_abs_distance_upper_limit'), + ] + + operations = [ + migrations.AddField( + model_name='wirelesslan', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='wirelesslangroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='wirelesslink', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] From a2f8ddc80e5f60734ab85284a28923554c0944fc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 20 Oct 2025 14:22:31 -0400 Subject: [PATCH 03/40] Introduce PrimaryModelSerializer & OrganizationalModelSerializer; add owner field to serializers --- contrib/openapi.json | 4920 +++++++++++++++-- netbox/circuits/api/serializers_/circuits.py | 30 +- netbox/circuits/api/serializers_/providers.py | 18 +- netbox/core/api/serializers_/data.py | 8 +- netbox/dcim/api/serializers_/cables.py | 10 +- netbox/dcim/api/serializers_/devices.py | 22 +- netbox/dcim/api/serializers_/devicetypes.py | 18 +- netbox/dcim/api/serializers_/manufacturers.py | 6 +- netbox/dcim/api/serializers_/platforms.py | 2 +- netbox/dcim/api/serializers_/power.py | 13 +- netbox/dcim/api/serializers_/racks.py | 22 +- netbox/dcim/api/serializers_/roles.py | 10 +- netbox/dcim/api/serializers_/sites.py | 12 +- .../dcim/api/serializers_/virtualchassis.py | 8 +- .../extras/api/serializers_/configcontexts.py | 21 +- .../api/serializers_/configtemplates.py | 10 +- .../extras/api/serializers_/customfields.py | 11 +- netbox/extras/api/serializers_/customlinks.py | 5 +- netbox/extras/api/serializers_/events.py | 9 +- .../api/serializers_/exporttemplates.py | 5 +- .../extras/api/serializers_/savedfilters.py | 5 +- netbox/extras/api/serializers_/tags.py | 3 +- netbox/ipam/api/serializers_/asns.py | 14 +- netbox/ipam/api/serializers_/fhrpgroups.py | 6 +- netbox/ipam/api/serializers_/ip.py | 18 +- netbox/ipam/api/serializers_/roles.py | 8 +- netbox/ipam/api/serializers_/services.py | 12 +- netbox/ipam/api/serializers_/vlans.py | 15 +- netbox/ipam/api/serializers_/vrfs.py | 14 +- netbox/netbox/api/serializers/__init__.py | 31 +- netbox/netbox/api/serializers/bulk.py | 11 + netbox/netbox/api/serializers/features.py | 14 + netbox/netbox/api/serializers/models.py | 31 + netbox/netbox/models/features.py | 1 - netbox/tenancy/api/serializers_/contacts.py | 14 +- netbox/tenancy/api/serializers_/tenants.py | 8 +- netbox/users/api/serializers_/mixins.py | 18 + .../api/serializers_/clusters.py | 14 +- .../api/serializers_/virtualmachines.py | 8 +- netbox/vpn/api/serializers_/crypto.py | 26 +- netbox/vpn/api/serializers_/l2vpn.py | 7 +- netbox/vpn/api/serializers_/tunnels.py | 12 +- .../wireless/api/serializers_/wirelesslans.py | 14 +- .../api/serializers_/wirelesslinks.py | 8 +- 44 files changed, 4656 insertions(+), 816 deletions(-) create mode 100644 netbox/netbox/api/serializers/bulk.py create mode 100644 netbox/netbox/api/serializers/models.py create mode 100644 netbox/users/api/serializers_/mixins.py diff --git a/contrib/openapi.json b/contrib/openapi.json index 14eba1d6e96..d0a486f9768 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -199947,7 +199947,7 @@ "schemas": { "ASN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -199994,6 +199994,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -200044,7 +200052,7 @@ }, "ASNRange": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -200100,6 +200108,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -200144,7 +200160,7 @@ }, "ASNRangeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -200199,6 +200215,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -200220,7 +200252,7 @@ }, "ASNRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "asn": { "type": "integer", @@ -200265,6 +200297,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -200285,7 +200333,7 @@ }, "Aggregate": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -200350,6 +200398,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -200390,7 +200446,7 @@ }, "AggregateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -200431,6 +200487,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -200865,7 +200937,7 @@ }, "BriefCable": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -200897,7 +200969,7 @@ }, "BriefCableRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "label": { "type": "string", @@ -200911,7 +200983,7 @@ }, "BriefCircuit": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -200950,7 +201022,7 @@ }, "BriefCircuitGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -201064,7 +201136,7 @@ }, "BriefCircuitGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -201078,7 +201150,7 @@ }, "BriefCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -201109,7 +201181,7 @@ }, "BriefCircuitType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -201154,7 +201226,7 @@ }, "BriefCircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -201179,7 +201251,7 @@ }, "BriefCluster": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201218,7 +201290,7 @@ }, "BriefClusterGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -201263,7 +201335,7 @@ }, "BriefClusterGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -201288,7 +201360,7 @@ }, "BriefClusterRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -201306,7 +201378,7 @@ }, "BriefClusterType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -201351,7 +201423,7 @@ }, "BriefClusterTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -201376,7 +201448,7 @@ }, "BriefConfigContextProfile": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201409,7 +201481,7 @@ }, "BriefConfigContextProfileRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -201427,7 +201499,7 @@ }, "BriefConfigTemplate": { "type": "object", - "description": "Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment\non create() and update().", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -201460,7 +201532,7 @@ }, "BriefConfigTemplateRequest": { "type": "object", - "description": "Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment\non create() and update().", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -201478,7 +201550,7 @@ }, "BriefContact": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201511,7 +201583,7 @@ }, "BriefContactRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -201529,7 +201601,7 @@ }, "BriefContactRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -201568,7 +201640,7 @@ }, "BriefContactRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -201593,7 +201665,7 @@ }, "BriefCustomFieldChoiceSet": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -201631,7 +201703,7 @@ }, "BriefCustomFieldChoiceSetRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -201679,7 +201751,7 @@ }, "BriefDataSource": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201712,7 +201784,7 @@ }, "BriefDataSourceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -201730,7 +201802,7 @@ }, "BriefDevice": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201763,7 +201835,7 @@ }, "BriefDeviceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -201778,7 +201850,7 @@ }, "BriefDeviceRole": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -201835,7 +201907,7 @@ }, "BriefDeviceRoleRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -201860,7 +201932,7 @@ }, "BriefDeviceType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201909,7 +201981,7 @@ }, "BriefDeviceTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -201945,7 +202017,7 @@ }, "BriefFHRPGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -201994,7 +202066,7 @@ }, "BriefFHRPGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "protocol": { "enum": [ @@ -202027,7 +202099,7 @@ }, "BriefIKEPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202060,7 +202132,7 @@ }, "BriefIKEPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -202078,7 +202150,7 @@ }, "BriefIPAddress": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202133,7 +202205,7 @@ }, "BriefIPAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "address": { "type": "string", @@ -202150,7 +202222,7 @@ }, "BriefIPSecPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202183,7 +202255,7 @@ }, "BriefIPSecPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -202201,7 +202273,7 @@ }, "BriefIPSecProfile": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202234,7 +202306,7 @@ }, "BriefIPSecProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -202334,7 +202406,7 @@ }, "BriefInventoryItemRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -202379,7 +202451,7 @@ }, "BriefInventoryItemRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -202478,7 +202550,7 @@ }, "BriefL2VPN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202569,7 +202641,7 @@ }, "BriefL2VPNRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "identifier": { "type": "integer", @@ -202669,7 +202741,7 @@ }, "BriefLocation": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -202720,7 +202792,7 @@ }, "BriefLocationRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -202745,7 +202817,7 @@ }, "BriefMACAddress": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202777,7 +202849,7 @@ }, "BriefMACAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "mac_address": { "type": "string", @@ -202794,7 +202866,7 @@ }, "BriefManufacturer": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -202839,7 +202911,7 @@ }, "BriefManufacturerRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -202864,7 +202936,7 @@ }, "BriefModule": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202896,7 +202968,7 @@ }, "BriefModuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "device": { "oneOf": [ @@ -202919,7 +202991,7 @@ }, "BriefModuleType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202964,7 +203036,7 @@ }, "BriefModuleTypeProfile": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -202997,7 +203069,7 @@ }, "BriefModuleTypeProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203015,7 +203087,7 @@ }, "BriefModuleTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "profile": { "oneOf": [ @@ -203058,9 +203130,60 @@ "model" ] }, + "BriefOwner": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "maxLength": 150 + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "display", + "id", + "name", + "url" + ] + }, + "BriefOwnerRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 150 + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "name" + ] + }, "BriefPlatform": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -203117,7 +203240,7 @@ }, "BriefPlatformRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -203142,7 +203265,7 @@ }, "BriefPowerPanel": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203181,7 +203304,7 @@ }, "BriefPowerPanelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203334,7 +203457,7 @@ }, "BriefProvider": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203380,7 +203503,7 @@ }, "BriefProviderAccount": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203419,7 +203542,7 @@ }, "BriefProviderAccountRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203443,7 +203566,7 @@ }, "BriefProviderNetwork": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203476,7 +203599,7 @@ }, "BriefProviderNetworkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203494,7 +203617,7 @@ }, "BriefProviderRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203520,7 +203643,7 @@ }, "BriefRIR": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -203565,7 +203688,7 @@ }, "BriefRIRRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -203590,7 +203713,7 @@ }, "BriefRack": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203629,7 +203752,7 @@ }, "BriefRackRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -203647,7 +203770,7 @@ }, "BriefRackRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -203692,7 +203815,7 @@ }, "BriefRackRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -203717,7 +203840,7 @@ }, "BriefRackType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -203760,7 +203883,7 @@ }, "BriefRackTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -203849,7 +203972,7 @@ }, "BriefRegion": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -203900,7 +204023,7 @@ }, "BriefRegionRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -203925,7 +204048,7 @@ }, "BriefRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -203976,7 +204099,7 @@ }, "BriefRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -204001,7 +204124,7 @@ }, "BriefSite": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204041,7 +204164,7 @@ }, "BriefSiteGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -204092,7 +204215,7 @@ }, "BriefSiteGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -204117,7 +204240,7 @@ }, "BriefSiteRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204143,7 +204266,7 @@ }, "BriefTag": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -204187,7 +204310,7 @@ }, "BriefTenant": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204226,7 +204349,7 @@ }, "BriefTenantGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -204277,7 +204400,7 @@ }, "BriefTenantGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -204302,7 +204425,7 @@ }, "BriefTenantRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204327,7 +204450,7 @@ }, "BriefTunnel": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204360,7 +204483,7 @@ }, "BriefTunnelGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -204405,7 +204528,7 @@ }, "BriefTunnelGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -204430,7 +204553,7 @@ }, "BriefTunnelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204495,7 +204618,7 @@ }, "BriefVLAN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204536,7 +204659,7 @@ }, "BriefVLANGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -204581,7 +204704,7 @@ }, "BriefVLANGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -204606,7 +204729,7 @@ }, "BriefVLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "vid": { "type": "integer", @@ -204632,7 +204755,7 @@ }, "BriefVLANTranslationPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204665,7 +204788,7 @@ }, "BriefVLANTranslationPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204683,7 +204806,7 @@ }, "BriefVRF": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204729,7 +204852,7 @@ }, "BriefVRFRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204754,7 +204877,7 @@ }, "BriefVirtualChassis": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204800,7 +204923,7 @@ }, "BriefVirtualChassisRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -204826,7 +204949,7 @@ }, "BriefVirtualCircuit": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204865,7 +204988,7 @@ }, "BriefVirtualCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -204896,7 +205019,7 @@ }, "BriefVirtualCircuitType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -204941,7 +205064,7 @@ }, "BriefVirtualCircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -204966,7 +205089,7 @@ }, "BriefVirtualMachine": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -204999,7 +205122,7 @@ }, "BriefVirtualMachineRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -205017,7 +205140,7 @@ }, "BriefWirelessLANGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -205068,7 +205191,7 @@ }, "BriefWirelessLANGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -205093,7 +205216,7 @@ }, "Cable": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -205244,6 +205367,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -205281,7 +205412,7 @@ }, "CableRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "type": { "enum": [ @@ -205393,6 +205524,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -205480,7 +205627,7 @@ }, "Circuit": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -205636,6 +205783,14 @@ "readOnly": true, "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -205793,7 +205948,7 @@ }, "CircuitGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -205834,6 +205989,14 @@ ], "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206020,7 +206183,7 @@ }, "CircuitGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -206053,6 +206216,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206071,7 +206250,7 @@ }, "CircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -206192,6 +206371,22 @@ "x-spec-enum-id": "53542e7902f946af", "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -206456,7 +206651,7 @@ }, "CircuitType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -206494,6 +206689,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206536,7 +206739,7 @@ }, "CircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -206558,6 +206761,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206576,7 +206795,7 @@ }, "Cluster": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -206662,6 +206881,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -206734,7 +206961,7 @@ }, "ClusterGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -206767,6 +206994,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206809,7 +207044,7 @@ }, "ClusterGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -206826,6 +207061,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -206844,7 +207095,7 @@ }, "ClusterRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -206917,6 +207168,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -206938,7 +207205,7 @@ }, "ClusterType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -206971,6 +207238,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -207013,7 +207288,7 @@ }, "ClusterTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -207030,6 +207305,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -207048,7 +207339,7 @@ }, "ConfigContext": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -207164,6 +207455,14 @@ "$ref": "#/components/schemas/Tenant" } }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -207223,7 +207522,7 @@ }, "ConfigContextProfile": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -207258,9 +207557,17 @@ "tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/NestedTag" } }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -207315,7 +207622,7 @@ }, "ConfigContextProfileRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -207333,10 +207640,25 @@ "tags": { "type": "array", "items": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/NestedTagRequest" } }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -207357,7 +207679,7 @@ }, "ConfigContextRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -207464,6 +207786,22 @@ "type": "integer" } }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -207490,7 +207828,7 @@ }, "ConfigTemplate": { "type": "object", - "description": "Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment\non create() and update().", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -207564,6 +207902,14 @@ "nullable": true, "title": "Date synced" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -207598,7 +207944,7 @@ }, "ConfigTemplateRequest": { "type": "object", - "description": "Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment\non create() and update().", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -207648,6 +207994,22 @@ } ] }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -208700,7 +209062,7 @@ }, "Contact": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -208756,6 +209118,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -208965,7 +209335,7 @@ }, "ContactGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -209033,6 +209403,14 @@ "readOnly": true, "default": 0 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -209057,7 +209435,7 @@ }, "ContactGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -209092,6 +209470,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -209103,7 +209497,7 @@ }, "ContactRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "groups": { "type": "array", @@ -209142,6 +209536,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -209162,7 +209572,7 @@ }, "ContactRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -209195,6 +209605,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -209231,7 +209649,7 @@ }, "ContactRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -209248,6 +209666,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -209266,7 +209700,7 @@ }, "CustomField": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -209501,6 +209935,14 @@ ], "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -209532,7 +209974,7 @@ }, "CustomFieldChoiceSet": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -209600,6 +210042,14 @@ "type": "integer", "readOnly": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "created": { "type": "string", "format": "date-time", @@ -209627,7 +210077,7 @@ }, "CustomFieldChoiceSetRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -209660,6 +210110,22 @@ "order_alphabetically": { "type": "boolean", "description": "Choices are automatically ordered alphabetically" + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } }, "required": [ @@ -209669,7 +210135,7 @@ }, "CustomFieldRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -209829,6 +210295,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -209841,7 +210323,7 @@ }, "CustomLink": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -209918,6 +210400,14 @@ "type": "boolean", "description": "Force link to open in a new window" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "created": { "type": "string", "format": "date-time", @@ -209946,7 +210436,7 @@ }, "CustomLinkRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -210007,6 +210497,22 @@ "new_window": { "type": "boolean", "description": "Force link to open in a new window" + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } }, "required": [ @@ -210094,7 +210600,7 @@ }, "DataSource": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -210206,6 +210712,14 @@ "type": "string", "description": "Patterns (one per line) matching files to ignore when syncing" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -210254,7 +210768,7 @@ }, "DataSourceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -210308,6 +210822,22 @@ "type": "string", "description": "Patterns (one per line) matching files to ignore when syncing" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -210324,7 +210854,7 @@ }, "Device": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -210597,6 +211127,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -210933,7 +211471,7 @@ }, "DeviceRole": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -211023,6 +211561,14 @@ "readOnly": true, "default": 0 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -211048,7 +211594,7 @@ }, "DeviceRoleRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -211109,6 +211655,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -211120,7 +211682,7 @@ }, "DeviceType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -211297,6 +211859,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -211393,7 +211963,7 @@ }, "DeviceTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -211523,6 +212093,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -211545,7 +212131,7 @@ }, "DeviceWithConfigContext": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -211927,7 +212513,7 @@ }, "DeviceWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -212250,7 +212836,7 @@ }, "EventRule": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -212351,6 +212937,14 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -212387,7 +212981,7 @@ }, "EventRuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -212453,6 +213047,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -212470,7 +213080,7 @@ }, "ExportTemplate": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -212555,6 +213165,14 @@ "nullable": true, "title": "Date synced" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "created": { "type": "string", "format": "date-time", @@ -212585,7 +213203,7 @@ }, "ExportTemplateRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -212640,6 +213258,22 @@ "$ref": "#/components/schemas/BriefDataSourceRequest" } ] + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } }, "required": [ @@ -212650,7 +213284,7 @@ }, "FHRPGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -212715,6 +213349,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -212862,7 +213504,7 @@ }, "FHRPGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -212909,6 +213551,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -213880,7 +214538,7 @@ }, "IKEPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -213960,6 +214618,14 @@ "type": "string", "title": "Pre-shared key" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -213999,7 +214665,7 @@ }, "IKEPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -214038,6 +214704,22 @@ "type": "string", "title": "Pre-shared key" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214059,7 +214741,7 @@ }, "IKEProposal": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -214244,6 +214926,14 @@ "nullable": true, "description": "Security association lifetime (in seconds)" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214285,7 +214975,7 @@ }, "IKEProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -214372,6 +215062,22 @@ "nullable": true, "description": "Security association lifetime (in seconds)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214395,7 +215101,7 @@ }, "IPAddress": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -214557,6 +215263,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214598,7 +215312,7 @@ }, "IPAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "address": { "type": "string", @@ -214693,6 +215407,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214713,7 +215443,7 @@ }, "IPRange": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -214816,6 +215546,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214865,7 +215603,7 @@ }, "IPRangeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "start_address": { "type": "string", @@ -214937,6 +215675,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -214966,7 +215720,7 @@ }, "IPSecPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -215065,6 +215819,14 @@ } } }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -215103,7 +215865,7 @@ }, "IPSecPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -215151,6 +215913,22 @@ "description": "* `1` - Group 1\n* `2` - Group 2\n* `5` - Group 5\n* `14` - Group 14\n* `15` - Group 15\n* `16` - Group 16\n* `17` - Group 17\n* `18` - Group 18\n* `19` - Group 19\n* `20` - Group 20\n* `21` - Group 21\n* `22` - Group 22\n* `23` - Group 23\n* `24` - Group 24\n* `25` - Group 25\n* `26` - Group 26\n* `27` - Group 27\n* `28` - Group 28\n* `29` - Group 29\n* `30` - Group 30\n* `31` - Group 31\n* `32` - Group 32\n* `33` - Group 33\n* `34` - Group 34", "x-spec-enum-id": "dbef43be795462a8" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -215171,7 +215949,7 @@ }, "IPSecProfile": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -215226,6 +216004,14 @@ "ipsec_policy": { "$ref": "#/components/schemas/BriefIPSecPolicy" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -215267,7 +216053,7 @@ }, "IPSecProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -215307,6 +216093,22 @@ } ] }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -215330,7 +216132,7 @@ }, "IPSecProposal": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -215434,6 +216236,14 @@ "title": "SA lifetime (KB)", "description": "Security association lifetime (in kilobytes)" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -215472,7 +216282,7 @@ }, "IPSecProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -215526,6 +216336,22 @@ "title": "SA lifetime (KB)", "description": "Security association lifetime (in kilobytes)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -218900,7 +219726,7 @@ }, "InventoryItemRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -218938,6 +219764,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -218980,7 +219814,7 @@ }, "InventoryItemRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -219003,6 +219837,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -219495,7 +220345,7 @@ }, "L2VPN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -219615,193 +220465,217 @@ "type": "string", "maxLength": 200 }, - "comments": { - "type": "string" - }, - "tenant": { + "owner": { "allOf": [ { - "$ref": "#/components/schemas/BriefTenant" + "$ref": "#/components/schemas/BriefOwner" } ], "nullable": true }, - "tags": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NestedTag" - } - }, - "custom_fields": { - "type": "object", - "additionalProperties": {} - }, - "created": { - "type": "string", - "format": "date-time", - "readOnly": true, - "nullable": true - }, - "last_updated": { - "type": "string", - "format": "date-time", - "readOnly": true, - "nullable": true - } - }, - "required": [ - "created", - "display", - "display_url", - "id", - "last_updated", - "name", - "slug", - "url" - ] - }, - "L2VPNRequest": { - "type": "object", - "description": "Adds support for custom fields and tags.", - "properties": { - "identifier": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "format": "int64", - "nullable": true - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "slug": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "pattern": "^[-a-zA-Z0-9_]+$" - }, - "type": { - "enum": [ - "vpws", - "vpls", - "vxlan", - "vxlan-evpn", - "mpls-evpn", - "pbb-evpn", - "evpn-vpws", - "epl", - "evpl", - "ep-lan", - "evp-lan", - "ep-tree", - "evp-tree", - "spb" - ], - "type": "string", - "description": "* `vpws` - VPWS\n* `vpls` - VPLS\n* `vxlan` - VXLAN\n* `vxlan-evpn` - VXLAN-EVPN\n* `mpls-evpn` - MPLS EVPN\n* `pbb-evpn` - PBB EVPN\n* `evpn-vpws` - EVPN VPWS\n* `epl` - EPL\n* `evpl` - EVPL\n* `ep-lan` - Ethernet Private LAN\n* `evp-lan` - Ethernet Virtual Private LAN\n* `ep-tree` - Ethernet Private Tree\n* `evp-tree` - Ethernet Virtual Private Tree\n* `spb` - SPB", - "x-spec-enum-id": "730136816a2885f9" - }, - "status": { - "enum": [ - "active", - "planned", - "decommissioning" - ], - "type": "string", - "description": "* `active` - Active\n* `planned` - Planned\n* `decommissioning` - Decommissioning", - "x-spec-enum-id": "937bacafb9d5d4bb" - }, - "import_targets": { - "type": "array", - "items": { - "type": "integer" - } - }, - "export_targets": { - "type": "array", - "items": { - "type": "integer" - } - }, - "description": { - "type": "string", - "maxLength": 200 - }, "comments": { "type": "string" }, "tenant": { - "oneOf": [ - { - "type": "integer" - }, + "allOf": [ { - "allOf": [ - { - "$ref": "#/components/schemas/BriefTenantRequest" - } - ], - "nullable": true + "$ref": "#/components/schemas/BriefTenant" } ], "nullable": true }, - "tags": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NestedTagRequest" - } - }, - "custom_fields": { - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "name", - "slug" - ] - }, - "L2VPNTermination": { - "type": "object", - "description": "Adds support for custom fields and tags.", - "properties": { - "id": { - "type": "integer", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display_url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display": { - "type": "string", - "readOnly": true - }, - "l2vpn": { - "$ref": "#/components/schemas/BriefL2VPN" - }, - "assigned_object_type": { - "type": "string" - }, - "assigned_object_id": { - "type": "integer", - "maximum": 9223372036854775807, - "minimum": 0, - "format": "int64" - }, - "assigned_object": { - "nullable": true, - "readOnly": true - }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedTag" + } + }, + "custom_fields": { + "type": "object", + "additionalProperties": {} + }, + "created": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true + }, + "last_updated": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true + } + }, + "required": [ + "created", + "display", + "display_url", + "id", + "last_updated", + "name", + "slug", + "url" + ] + }, + "L2VPNRequest": { + "type": "object", + "description": "Base serializer class for models inheriting from PrimaryModel.", + "properties": { + "identifier": { + "type": "integer", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "format": "int64", + "nullable": true + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "type": { + "enum": [ + "vpws", + "vpls", + "vxlan", + "vxlan-evpn", + "mpls-evpn", + "pbb-evpn", + "evpn-vpws", + "epl", + "evpl", + "ep-lan", + "evp-lan", + "ep-tree", + "evp-tree", + "spb" + ], + "type": "string", + "description": "* `vpws` - VPWS\n* `vpls` - VPLS\n* `vxlan` - VXLAN\n* `vxlan-evpn` - VXLAN-EVPN\n* `mpls-evpn` - MPLS EVPN\n* `pbb-evpn` - PBB EVPN\n* `evpn-vpws` - EVPN VPWS\n* `epl` - EPL\n* `evpl` - EVPL\n* `ep-lan` - Ethernet Private LAN\n* `evp-lan` - Ethernet Virtual Private LAN\n* `ep-tree` - Ethernet Private Tree\n* `evp-tree` - Ethernet Virtual Private Tree\n* `spb` - SPB", + "x-spec-enum-id": "730136816a2885f9" + }, + "status": { + "enum": [ + "active", + "planned", + "decommissioning" + ], + "type": "string", + "description": "* `active` - Active\n* `planned` - Planned\n* `decommissioning` - Decommissioning", + "x-spec-enum-id": "937bacafb9d5d4bb" + }, + "import_targets": { + "type": "array", + "items": { + "type": "integer" + } + }, + "export_targets": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, + "comments": { + "type": "string" + }, + "tenant": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefTenantRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedTagRequest" + } + }, + "custom_fields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "name", + "slug" + ] + }, + "L2VPNTermination": { + "type": "object", + "description": "Adds support for custom fields and tags.", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display_url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "l2vpn": { + "$ref": "#/components/schemas/BriefL2VPN" + }, + "assigned_object_type": { + "type": "string" + }, + "assigned_object_id": { + "type": "integer", + "maximum": 9223372036854775807, + "minimum": 0, + "format": "int64" + }, + "assigned_object": { + "nullable": true, + "readOnly": true + }, "tags": { "type": "array", "items": { @@ -219880,7 +220754,7 @@ }, "Location": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -220001,6 +220875,14 @@ "format": "int64", "readOnly": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -220028,7 +220910,7 @@ }, "LocationRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -220106,6 +220988,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -220118,7 +221016,7 @@ }, "MACAddress": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -220160,6 +221058,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -220199,7 +221105,7 @@ }, "MACAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "mac_address": { "type": "string", @@ -220220,6 +221126,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -220240,7 +221162,7 @@ }, "Manufacturer": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -220273,6 +221195,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -220327,7 +221257,7 @@ }, "ManufacturerRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -220344,6 +221274,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -220362,7 +221308,7 @@ }, "Module": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -220435,6 +221381,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -220784,7 +221738,7 @@ }, "ModuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "device": { "oneOf": [ @@ -220837,6 +221791,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -220859,7 +221829,7 @@ }, "ModuleType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -220975,6 +221945,14 @@ "attributes": { "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -221014,7 +221992,7 @@ }, "ModuleTypeProfile": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -221045,6 +222023,14 @@ "schema": { "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -221083,7 +222069,7 @@ }, "ModuleTypeProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -221097,6 +222083,22 @@ "schema": { "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -221117,7 +222119,7 @@ }, "ModuleTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "profile": { "oneOf": [ @@ -221201,6 +222203,22 @@ "attributes": { "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -226996,7 +228014,7 @@ }, "PatchedASNRangeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227051,6 +228069,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227065,7 +228099,7 @@ }, "PatchedASNRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "asn": { "type": "integer", @@ -227110,6 +228144,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -227152,7 +228202,7 @@ }, "PatchedCircuitGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227185,6 +228235,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227279,7 +228345,7 @@ }, "PatchedCircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227301,6 +228367,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227315,7 +228397,7 @@ }, "PatchedClusterGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227332,6 +228414,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227346,7 +228444,7 @@ }, "PatchedClusterTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227363,6 +228461,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227377,7 +228491,7 @@ }, "PatchedConfigContextProfileRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -227395,10 +228509,25 @@ "tags": { "type": "array", "items": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/NestedTagRequest" } }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -227416,7 +228545,7 @@ }, "PatchedConfigContextRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -227523,6 +228652,22 @@ "type": "integer" } }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227545,7 +228690,7 @@ }, "PatchedConfigTemplateRequest": { "type": "object", - "description": "Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment\non create() and update().", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -227595,6 +228740,22 @@ } ] }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227605,7 +228766,7 @@ }, "PatchedContactRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "groups": { "type": "array", @@ -227644,6 +228805,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -227661,7 +228838,7 @@ }, "PatchedContactRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -227678,6 +228855,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -227692,7 +228885,7 @@ }, "PatchedCustomLinkRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -227753,6 +228946,22 @@ "new_window": { "type": "boolean", "description": "Force link to open in a new window" + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } } }, @@ -227852,7 +229061,7 @@ }, "PatchedExportTemplateRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -227907,6 +229116,22 @@ "$ref": "#/components/schemas/BriefDataSourceRequest" } ] + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } } }, @@ -227942,7 +229167,7 @@ }, "PatchedFHRPGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -227989,6 +229214,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228054,7 +229295,7 @@ }, "PatchedInventoryItemRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -228077,6 +229318,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -228209,7 +229466,7 @@ }, "PatchedMACAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "mac_address": { "type": "string", @@ -228230,6 +229487,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228247,7 +229520,7 @@ }, "PatchedManufacturerRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -228264,6 +229537,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -228413,7 +229702,7 @@ }, "PatchedModuleTypeProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -228427,6 +229716,22 @@ "schema": { "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228592,7 +229897,7 @@ }, "PatchedPowerPanelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "site": { "oneOf": [ @@ -228629,6 +229934,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228646,7 +229967,7 @@ }, "PatchedProviderAccountRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "provider": { "oneOf": [ @@ -228673,6 +229994,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228690,7 +230027,7 @@ }, "PatchedProviderNetworkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "provider": { "oneOf": [ @@ -228715,6 +230052,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228732,7 +230085,7 @@ }, "PatchedProviderRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -228756,6 +230109,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228779,7 +230148,7 @@ }, "PatchedRIRRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -228801,6 +230170,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -228815,7 +230200,7 @@ }, "PatchedRackRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -228838,6 +230223,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -228852,7 +230253,7 @@ }, "PatchedRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -228874,6 +230275,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -228888,7 +230305,7 @@ }, "PatchedRouteTargetRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -228916,6 +230333,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -228933,7 +230366,7 @@ }, "PatchedSavedFilterRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -228971,7 +230404,23 @@ "shared": { "type": "boolean" }, - "parameters": {} + "parameters": {}, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + } } }, "PatchedScriptInputRequest": { @@ -229074,7 +230523,7 @@ }, "PatchedTagRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -229112,7 +230561,7 @@ }, "PatchedTenantRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -229145,6 +230594,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -229218,7 +230683,7 @@ }, "PatchedTunnelGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -229235,6 +230700,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -229308,7 +230789,7 @@ }, "PatchedVLANGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -229355,6 +230836,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -229369,7 +230866,7 @@ }, "PatchedVLANTranslationPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -229379,6 +230876,25 @@ "description": { "type": "string", "maxLength": 200 + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, + "comments": { + "type": "string" } } }, @@ -229411,7 +230927,7 @@ }, "PatchedVRFRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -229450,6 +230966,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -229479,7 +231011,7 @@ }, "PatchedVirtualCircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -229501,6 +231033,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -229556,7 +231104,7 @@ }, "PatchedWebhookRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -229619,6 +231167,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -229629,7 +231193,7 @@ }, "PatchedWritableAggregateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -229670,6 +231234,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -229687,7 +231267,7 @@ }, "PatchedWritableCableRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "type": { "enum": [ @@ -229799,6 +231379,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -229861,7 +231457,7 @@ }, "PatchedWritableCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -229982,6 +231578,22 @@ "x-spec-enum-id": "53542e7902f946af", "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -230005,7 +231617,7 @@ }, "PatchedWritableClusterRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -230078,6 +231690,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -230524,7 +232152,7 @@ }, "PatchedWritableContactGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -230555,6 +232183,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -230562,7 +232206,7 @@ }, "PatchedWritableCustomFieldChoiceSetRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -230598,12 +232242,28 @@ "order_alphabetically": { "type": "boolean", "description": "Choices are automatically ordered alphabetically" + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } } }, "PatchedWritableCustomFieldRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -230763,6 +232423,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -230770,7 +232446,7 @@ }, "PatchedWritableDataSourceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -230819,6 +232495,22 @@ "type": "string", "description": "Patterns (one per line) matching files to ignore when syncing" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -230830,7 +232522,7 @@ }, "PatchedWritableDeviceRoleRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -230887,6 +232579,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -230894,7 +232602,7 @@ }, "PatchedWritableDeviceTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -231025,6 +232733,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -231042,7 +232766,7 @@ }, "PatchedWritableDeviceWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -231365,7 +233089,7 @@ }, "PatchedWritableEventRuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -231431,6 +233155,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -231718,7 +233458,7 @@ }, "PatchedWritableIKEPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -231762,6 +233502,22 @@ "type": "string", "title": "Pre-shared key" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -231779,7 +233535,7 @@ }, "PatchedWritableIKEProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -231871,6 +233627,22 @@ "nullable": true, "description": "Security association lifetime (in seconds)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -231888,7 +233660,7 @@ }, "PatchedWritableIPAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "address": { "type": "string", @@ -231983,6 +233755,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -232000,7 +233788,7 @@ }, "PatchedWritableIPRangeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "start_address": { "type": "string", @@ -232072,6 +233860,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -232097,7 +233901,7 @@ }, "PatchedWritableIPSecPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -232149,6 +233953,22 @@ "minimum": 0, "maximum": 32767 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -232166,7 +233986,7 @@ }, "PatchedWritableIPSecProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -232206,6 +234026,22 @@ } ] }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -232223,7 +234059,7 @@ }, "PatchedWritableIPSecProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -232285,6 +234121,22 @@ "title": "SA lifetime (KB)", "description": "Security association lifetime (in kilobytes)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -233505,7 +235357,7 @@ }, "PatchedWritableL2VPNRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "identifier": { "type": "integer", @@ -233572,6 +235424,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -233605,7 +235473,7 @@ }, "PatchedWritableLocationRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -233679,6 +235547,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -233686,7 +235570,7 @@ }, "PatchedWritableModuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "device": { "oneOf": [ @@ -233739,6 +235623,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -233756,7 +235656,7 @@ }, "PatchedWritableModuleTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "profile": { "oneOf": [ @@ -233840,6 +235740,22 @@ "attributes": { "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -233857,7 +235773,7 @@ }, "PatchedWritablePlatformRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "parent": { "type": "integer", @@ -233910,6 +235826,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -233927,7 +235859,7 @@ }, "PatchedWritablePowerFeedRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "power_panel": { "oneOf": [ @@ -234038,6 +235970,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -234830,7 +236778,7 @@ }, "PatchedWritablePrefixRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -234932,6 +236880,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -234949,7 +236913,7 @@ }, "PatchedWritableRackRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -235192,6 +237156,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -235209,7 +237189,7 @@ }, "PatchedWritableRackReservationRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "rack": { "oneOf": [ @@ -235270,6 +237250,22 @@ "minLength": 1, "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -235287,7 +237283,7 @@ }, "PatchedWritableRackTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -235429,6 +237425,22 @@ "nullable": true, "description": "Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the front and rear rails." }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -235710,7 +237722,7 @@ }, "PatchedWritableRegionRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -235741,6 +237753,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -235748,7 +237776,7 @@ }, "PatchedWritableServiceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "parent_object_type": { "type": "string" @@ -235793,6 +237821,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -235810,7 +237854,7 @@ }, "PatchedWritableServiceTemplateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -235840,6 +237884,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -235857,7 +237917,7 @@ }, "PatchedWritableSiteGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -235888,6 +237948,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -235895,7 +237971,7 @@ }, "PatchedWritableSiteRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -236013,6 +238089,22 @@ "nullable": true, "description": "GPS coordinate in decimal format (xx.yyyyyy)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236036,7 +238128,7 @@ }, "PatchedWritableTenantGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -236067,6 +238159,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -236074,7 +238182,7 @@ }, "PatchedWritableTunnelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -236165,6 +238273,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236244,7 +238368,7 @@ }, "PatchedWritableVLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "site": { "oneOf": [ @@ -236353,6 +238477,22 @@ "type": "integer", "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236524,7 +238664,7 @@ }, "PatchedWritableVirtualChassisRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -236543,6 +238683,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236560,7 +238716,7 @@ }, "PatchedWritableVirtualCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -236638,6 +238794,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236705,7 +238877,7 @@ }, "PatchedWritableVirtualDeviceContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -236790,6 +238962,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -236807,7 +238995,7 @@ }, "PatchedWritableVirtualMachineWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -237024,7 +239212,7 @@ }, "PatchedWritableWirelessLANGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -237055,6 +239243,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -237062,7 +239266,7 @@ }, "PatchedWritableWirelessLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "ssid": { "type": "string", @@ -237174,6 +239378,22 @@ "title": "Pre-shared key", "maxLength": 64 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -237191,7 +239411,7 @@ }, "PatchedWritableWirelessLinkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "interface_a": { "oneOf": [ @@ -237304,6 +239524,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -237321,7 +239557,7 @@ }, "Platform": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -237378,6 +239614,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -237435,7 +239679,7 @@ }, "PlatformRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "parent": { "allOf": [ @@ -237492,6 +239736,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -237513,7 +239773,7 @@ }, "PowerFeed": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -237707,6 +239967,14 @@ ], "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -237759,7 +240027,7 @@ }, "PowerFeedRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "power_panel": { "oneOf": [ @@ -237870,6 +240138,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -239039,7 +241323,7 @@ }, "PowerPanel": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -239078,6 +241362,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -239123,7 +241415,7 @@ }, "PowerPanelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "site": { "oneOf": [ @@ -239160,6 +241452,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240252,7 +242560,7 @@ }, "Prefix": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -240379,6 +242687,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240430,7 +242746,7 @@ }, "PrefixRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -240532,6 +242848,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240552,7 +242884,7 @@ }, "Provider": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -240592,6 +242924,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240643,7 +242983,7 @@ }, "ProviderAccount": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -240680,6 +243020,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240719,7 +243067,7 @@ }, "ProviderAccountRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "provider": { "oneOf": [ @@ -240746,6 +243094,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240767,7 +243131,7 @@ }, "ProviderNetwork": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -240802,6 +243166,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240841,7 +243213,7 @@ }, "ProviderNetworkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "provider": { "oneOf": [ @@ -240866,6 +243238,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240887,7 +243275,7 @@ }, "ProviderRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -240911,6 +243299,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -240938,7 +243342,7 @@ }, "RIR": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -240976,6 +243380,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -241018,7 +243430,7 @@ }, "RIRRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -241040,6 +243452,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -241058,7 +243486,7 @@ }, "Rack": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -241359,6 +243787,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -241410,7 +243846,7 @@ }, "RackRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -241649,6 +244085,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -241670,7 +244122,7 @@ }, "RackReservation": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -241751,6 +244203,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -241780,7 +244240,7 @@ }, "RackReservationRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "rack": { "oneOf": [ @@ -241841,6 +244301,22 @@ "minLength": 1, "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -241864,7 +244340,7 @@ }, "RackRole": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -241902,6 +244378,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -241944,7 +244428,7 @@ }, "RackRoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -241967,6 +244451,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -241985,7 +244485,7 @@ }, "RackType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -242194,6 +244694,14 @@ "nullable": true, "description": "Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the front and rear rails." }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -242234,7 +244742,7 @@ }, "RackTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -242377,6 +244885,22 @@ "nullable": true, "description": "Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the front and rear rails." }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -243213,7 +245737,7 @@ }, "Region": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -243286,6 +245810,14 @@ "format": "int64", "readOnly": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -243311,7 +245843,7 @@ }, "RegionRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -243346,6 +245878,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -243357,7 +245905,7 @@ }, "Role": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -243395,6 +245943,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -243443,7 +245999,7 @@ }, "RoleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -243465,6 +246021,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -243483,7 +246055,7 @@ }, "RouteTarget": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -243520,6 +246092,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -243558,7 +246138,7 @@ }, "RouteTargetRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -243586,6 +246166,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -243606,7 +246202,7 @@ }, "SavedFilter": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -243661,6 +246257,14 @@ "type": "boolean" }, "parameters": {}, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "created": { "type": "string", "format": "date-time", @@ -243689,7 +246293,7 @@ }, "SavedFilterRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -243727,7 +246331,23 @@ "shared": { "type": "boolean" }, - "parameters": {} + "parameters": {}, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + } }, "required": [ "name", @@ -243825,7 +246445,7 @@ }, "Service": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -243904,6 +246524,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -243946,7 +246574,7 @@ }, "ServiceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "parent_object_type": { "type": "string" @@ -243991,6 +246619,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244014,7 +246658,7 @@ }, "ServiceTemplate": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -244074,6 +246718,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244113,7 +246765,7 @@ }, "ServiceTemplateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -244143,6 +246795,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244164,7 +246832,7 @@ }, "Site": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -244288,6 +246956,14 @@ "nullable": true, "description": "GPS coordinate in decimal format (xx.yyyyyy)" }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244369,7 +247045,7 @@ }, "SiteGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -244442,6 +247118,14 @@ "format": "int64", "readOnly": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244467,7 +247151,7 @@ }, "SiteGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -244502,6 +247186,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -244513,7 +247213,7 @@ }, "SiteRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -244631,6 +247331,22 @@ "nullable": true, "description": "GPS coordinate in decimal format (xx.yyyyyy)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -244892,7 +247608,7 @@ }, "Tag": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -244973,7 +247689,7 @@ }, "TagRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -245062,7 +247778,7 @@ }, "Tenant": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -245103,6 +247819,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -245202,7 +247926,7 @@ }, "TenantGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -245270,6 +247994,14 @@ "readOnly": true, "default": 0 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -245294,7 +248026,7 @@ }, "TenantGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -245329,6 +248061,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -245340,7 +248088,7 @@ }, "TenantRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -245373,6 +248121,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -245670,7 +248434,7 @@ }, "Tunnel": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -245785,6 +248549,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -245831,7 +248603,7 @@ }, "TunnelGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -245864,6 +248636,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -245906,7 +248686,7 @@ }, "TunnelGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -245923,6 +248703,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -245941,7 +248737,7 @@ }, "TunnelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -246032,6 +248828,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -246367,7 +249179,7 @@ }, "VLAN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -246488,6 +249300,14 @@ ], "nullable": true }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -246543,7 +249363,7 @@ }, "VLANGroup": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -246602,6 +249422,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -246650,7 +249478,7 @@ }, "VLANGroupRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -246697,6 +249525,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -246715,7 +249559,7 @@ }, "VLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "site": { "oneOf": [ @@ -246826,6 +249670,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -246847,7 +249707,7 @@ }, "VLANTranslationPolicy": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -246876,6 +249736,17 @@ "$ref": "#/components/schemas/VLANTranslationRule" }, "readOnly": true + }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, + "comments": { + "type": "string" } }, "required": [ @@ -246888,7 +249759,7 @@ }, "VLANTranslationPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -246898,6 +249769,25 @@ "description": { "type": "string", "maxLength": 200 + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, + "comments": { + "type": "string" } }, "required": [ @@ -247347,7 +250237,7 @@ }, "VRF": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -247395,6 +250285,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -247457,7 +250355,7 @@ }, "VRFRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -247496,6 +250394,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -247528,7 +250442,7 @@ }, "VirtualChassis": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -247568,6 +250482,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -247619,7 +250541,7 @@ }, "VirtualChassisRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -247642,6 +250564,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -247662,7 +250600,7 @@ }, "VirtualCircuit": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -247743,6 +250681,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -247783,7 +250729,7 @@ }, "VirtualCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -247861,6 +250807,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -248026,7 +250988,7 @@ }, "VirtualCircuitType": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "id": { "type": "integer", @@ -248064,6 +251026,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -248106,7 +251076,7 @@ }, "VirtualCircuitTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from OrganizationalModel.", "properties": { "name": { "type": "string", @@ -248128,6 +251098,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -248146,7 +251132,7 @@ }, "VirtualDeviceContext": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -248239,6 +251225,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -248286,7 +251280,7 @@ }, "VirtualDeviceContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -248371,6 +251365,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -248513,7 +251523,7 @@ }, "VirtualMachineWithConfigContext": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -248738,7 +251748,7 @@ }, "VirtualMachineWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -248958,7 +251968,7 @@ }, "Webhook": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "id": { "type": "integer", @@ -249036,6 +252046,14 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -249068,7 +252086,7 @@ }, "WebhookRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -249131,6 +252149,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -249145,7 +252179,7 @@ }, "WirelessLAN": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -249290,6 +252324,14 @@ "title": "Pre-shared key", "maxLength": 64 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249329,7 +252371,7 @@ }, "WirelessLANGroup": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "id": { "type": "integer", @@ -249397,6 +252439,14 @@ "readOnly": true, "default": 0 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249421,7 +252471,7 @@ }, "WirelessLANGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -249456,6 +252506,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -249467,7 +252533,7 @@ }, "WirelessLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "ssid": { "type": "string", @@ -249574,6 +252640,22 @@ "title": "Pre-shared key", "maxLength": 64 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249594,7 +252676,7 @@ }, "WirelessLink": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "id": { "type": "integer", @@ -249751,6 +252833,14 @@ "type": "string", "maxLength": 200 }, + "owner": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwner" + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249790,7 +252880,7 @@ }, "WirelessLinkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "interface_a": { "oneOf": [ @@ -249897,6 +252987,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249918,7 +253024,7 @@ }, "WritableAggregateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -249959,6 +253065,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -249980,7 +253102,7 @@ }, "WritableCableRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "type": { "enum": [ @@ -250092,6 +253214,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -250159,7 +253297,7 @@ }, "WritableCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -250280,6 +253418,22 @@ "x-spec-enum-id": "53542e7902f946af", "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -250308,7 +253462,7 @@ }, "WritableClusterRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -250381,6 +253535,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -250850,7 +254020,7 @@ }, "WritableContactGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -250881,6 +254051,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -250892,7 +254078,7 @@ }, "WritableCustomFieldChoiceSetRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "name": { "type": "string", @@ -250928,6 +254114,22 @@ "order_alphabetically": { "type": "boolean", "description": "Choices are automatically ordered alphabetically" + }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true } }, "required": [ @@ -250937,7 +254139,7 @@ }, "WritableCustomFieldRequest": { "type": "object", - "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -251097,6 +254299,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -251108,7 +254326,7 @@ }, "WritableDataSourceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -251157,6 +254375,22 @@ "type": "string", "description": "Patterns (one per line) matching files to ignore when syncing" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -251173,7 +254407,7 @@ }, "WritableDeviceRoleRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -251230,6 +254464,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -251241,7 +254491,7 @@ }, "WritableDeviceTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -251372,6 +254622,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -251394,7 +254660,7 @@ }, "WritableDeviceWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -251722,7 +254988,7 @@ }, "WritableEventRuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Adds an `owner` field for models which have a ForeignKey to users.Owner.", "properties": { "object_types": { "type": "array", @@ -251788,6 +255054,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "tags": { "type": "array", "items": { @@ -252092,7 +255374,7 @@ }, "WritableIKEPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -252136,6 +255418,22 @@ "type": "string", "title": "Pre-shared key" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252156,7 +255454,7 @@ }, "WritableIKEProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -252248,6 +255546,22 @@ "nullable": true, "description": "Security association lifetime (in seconds)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252271,7 +255585,7 @@ }, "WritableIPAddressRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "address": { "type": "string", @@ -252366,6 +255680,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252386,7 +255716,7 @@ }, "WritableIPRangeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "start_address": { "type": "string", @@ -252458,6 +255788,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252487,7 +255833,7 @@ }, "WritableIPSecPolicyRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -252539,6 +255885,22 @@ "minimum": 0, "maximum": 32767 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252559,7 +255921,7 @@ }, "WritableIPSecProfileRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -252599,6 +255961,22 @@ } ] }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -252622,7 +256000,7 @@ }, "WritableIPSecProposalRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -252684,6 +256062,22 @@ "title": "SA lifetime (KB)", "description": "Security association lifetime (in kilobytes)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -253925,7 +257319,7 @@ }, "WritableL2VPNRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "identifier": { "type": "integer", @@ -253992,6 +257386,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -254030,7 +257440,7 @@ }, "WritableLocationRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -254104,6 +257514,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -254116,7 +257542,7 @@ }, "WritableModuleRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "device": { "oneOf": [ @@ -254169,6 +257595,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -254191,7 +257633,7 @@ }, "WritableModuleTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "profile": { "oneOf": [ @@ -254275,6 +257717,22 @@ "attributes": { "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -254296,7 +257754,7 @@ }, "WritablePlatformRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "parent": { "type": "integer", @@ -254349,6 +257807,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -254370,7 +257844,7 @@ }, "WritablePowerFeedRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "power_panel": { "oneOf": [ @@ -254481,6 +257955,22 @@ ], "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -255291,7 +258781,7 @@ }, "WritablePrefixRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "prefix": { "type": "string", @@ -255393,6 +258883,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -255413,7 +258919,7 @@ }, "WritableRackRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -255656,6 +259162,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -255677,7 +259199,7 @@ }, "WritableRackReservationRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "rack": { "oneOf": [ @@ -255738,6 +259260,22 @@ "minLength": 1, "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -255761,7 +259299,7 @@ }, "WritableRackTypeRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "manufacturer": { "oneOf": [ @@ -255903,6 +259441,22 @@ "nullable": true, "description": "Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the front and rear rails." }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -256199,7 +259753,7 @@ }, "WritableRegionRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -256230,6 +259784,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -256241,7 +259811,7 @@ }, "WritableServiceRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "parent_object_type": { "type": "string" @@ -256286,6 +259856,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -256310,7 +259896,7 @@ }, "WritableServiceTemplateRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -256340,6 +259926,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -256362,7 +259964,7 @@ }, "WritableSiteGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -256393,6 +259995,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -256404,7 +260022,7 @@ }, "WritableSiteRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -256522,6 +260140,22 @@ "nullable": true, "description": "GPS coordinate in decimal format (xx.yyyyyy)" }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -256549,7 +260183,7 @@ }, "WritableTenantGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -256580,6 +260214,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -256591,7 +260241,7 @@ }, "WritableTunnelRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -256682,6 +260332,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -256769,7 +260435,7 @@ }, "WritableVLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "site": { "oneOf": [ @@ -256878,6 +260544,22 @@ "type": "integer", "nullable": true }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -257057,7 +260739,7 @@ }, "WritableVirtualChassisRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -257076,6 +260758,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -257096,7 +260794,7 @@ }, "WritableVirtualCircuitRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "cid": { "type": "string", @@ -257174,6 +260872,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -257250,7 +260964,7 @@ }, "WritableVirtualDeviceContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -257335,6 +261049,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -257357,7 +261087,7 @@ }, "WritableVirtualMachineWithConfigContextRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "name": { "type": "string", @@ -257577,7 +261307,7 @@ }, "WritableWirelessLANGroupRequest": { "type": "object", - "description": "Extends PrimaryModelSerializer to include MPTT support.", + "description": "Base serializer class for models inheriting from NestedGroupModel.", "properties": { "name": { "type": "string", @@ -257608,6 +261338,22 @@ "type": "object", "additionalProperties": {} }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" } @@ -257619,7 +261365,7 @@ }, "WritableWirelessLANRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "ssid": { "type": "string", @@ -257731,6 +261477,22 @@ "title": "Pre-shared key", "maxLength": 64 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, @@ -257751,7 +261513,7 @@ }, "WritableWirelessLinkRequest": { "type": "object", - "description": "Adds support for custom fields and tags.", + "description": "Base serializer class for models inheriting from PrimaryModel.", "properties": { "interface_a": { "oneOf": [ @@ -257864,6 +261626,22 @@ "type": "string", "maxLength": 200 }, + "owner": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerRequest" + } + ], + "nullable": true + } + ], + "nullable": true + }, "comments": { "type": "string" }, diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index 70b57a6885a..bc84f66fa69 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -11,7 +11,9 @@ from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.api.serializers_.cables import CabledObjectSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from netbox.api.serializers import ( + NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer, +) from netbox.choices import DistanceUnitChoices from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model @@ -29,7 +31,7 @@ ) -class CircuitTypeSerializer(NetBoxModelSerializer): +class CircuitTypeSerializer(OrganizationalModelSerializer): # Related object counts circuit_count = RelatedObjectCountField('circuits') @@ -37,8 +39,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer): class Meta: model = CircuitType fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'circuit_count', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') @@ -71,15 +73,15 @@ def get_termination(self, obj): return serializer(obj.termination, nested=True, context=context).data -class CircuitGroupSerializer(NetBoxModelSerializer): +class CircuitGroupSerializer(OrganizationalModelSerializer): tenant = TenantSerializer(nested=True, required=False, allow_null=True) circuit_count = RelatedObjectCountField('assignments') class Meta: model = CircuitGroup fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', - 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count' + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count' ] brief_fields = ('id', 'url', 'display', 'name') @@ -99,7 +101,7 @@ class Meta: brief_fields = ('id', 'url', 'display', 'group', 'priority') -class CircuitSerializer(NetBoxModelSerializer): +class CircuitSerializer(PrimaryModelSerializer): provider = ProviderSerializer(nested=True) provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) status = ChoiceField(choices=CircuitStatusChoices, required=False) @@ -115,7 +117,7 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit', - 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'termination_a', 'termination_z', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments', ] brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description') @@ -176,7 +178,7 @@ def get_member(self, obj): return serializer(obj.member, nested=True, context=context).data -class VirtualCircuitTypeSerializer(NetBoxModelSerializer): +class VirtualCircuitTypeSerializer(OrganizationalModelSerializer): # Related object counts virtual_circuit_count = RelatedObjectCountField('virtual_circuits') @@ -184,13 +186,13 @@ class VirtualCircuitTypeSerializer(NetBoxModelSerializer): class Meta: model = VirtualCircuitType fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'virtual_circuit_count', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'virtual_circuit_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count') -class VirtualCircuitSerializer(NetBoxModelSerializer): +class VirtualCircuitSerializer(PrimaryModelSerializer): provider_network = ProviderNetworkSerializer(nested=True) provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) type = VirtualCircuitTypeSerializer(nested=True) @@ -201,7 +203,7 @@ class Meta: model = VirtualCircuit fields = [ 'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status', - 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description') diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py index 4e37871079b..875c5c7f2a2 100644 --- a/netbox/circuits/api/serializers_/providers.py +++ b/netbox/circuits/api/serializers_/providers.py @@ -4,7 +4,7 @@ from ipam.api.serializers_.asns import ASNSerializer from ipam.models import ASN from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from .nested import NestedProviderAccountSerializer __all__ = ( @@ -14,7 +14,7 @@ ) -class ProviderSerializer(NetBoxModelSerializer): +class ProviderSerializer(PrimaryModelSerializer): accounts = SerializedPKRelatedField( queryset=ProviderAccount.objects.all(), serializer=NestedProviderAccountSerializer, @@ -35,32 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer): class Meta: model = Provider fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'owner', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') -class ProviderAccountSerializer(NetBoxModelSerializer): +class ProviderAccountSerializer(PrimaryModelSerializer): provider = ProviderSerializer(nested=True) name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='') class Meta: model = ProviderAccount fields = [ - 'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') -class ProviderNetworkSerializer(NetBoxModelSerializer): +class ProviderNetworkSerializer(PrimaryModelSerializer): provider = ProviderSerializer(nested=True) class Meta: model = ProviderNetwork fields = [ - 'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/core/api/serializers_/data.py b/netbox/core/api/serializers_/data.py index 3f2ddb2a0e6..3130b7472fa 100644 --- a/netbox/core/api/serializers_/data.py +++ b/netbox/core/api/serializers_/data.py @@ -1,7 +1,7 @@ from core.choices import * from core.models import DataFile, DataSource from netbox.api.fields import ChoiceField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer from netbox.utils import get_data_backend_choices __all__ = ( @@ -10,7 +10,7 @@ ) -class DataSourceSerializer(NetBoxModelSerializer): +class DataSourceSerializer(PrimaryModelSerializer): type = ChoiceField( choices=get_data_backend_choices() ) @@ -26,8 +26,8 @@ class Meta: model = DataSource fields = [ 'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', - 'sync_interval', 'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', - 'last_synced', 'file_count', + 'sync_interval', 'parameters', 'ignore_rules', 'owner', 'comments', 'custom_fields', 'created', + 'last_updated', 'last_synced', 'file_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py index bb9a12462a1..d72b0cbec1d 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -5,7 +5,9 @@ from dcim.choices import * from dcim.models import Cable, CablePath, CableTermination from netbox.api.fields import ChoiceField, ContentTypeField -from netbox.api.serializers import BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer +from netbox.api.serializers import ( + BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer, PrimaryModelSerializer, +) from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model @@ -18,7 +20,7 @@ ) -class CableSerializer(NetBoxModelSerializer): +class CableSerializer(PrimaryModelSerializer): a_terminations = GenericObjectSerializer(many=True, required=False) b_terminations = GenericObjectSerializer(many=True, required=False) status = ChoiceField(choices=LinkStatusChoices, required=False) @@ -29,8 +31,8 @@ class Meta: model = Cable fields = [ 'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', - 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', + 'label', 'color', 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'label', 'description') diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py index c1e9c5f5129..2a3e0cd4212 100644 --- a/netbox/dcim/api/serializers_/devices.py +++ b/netbox/dcim/api/serializers_/devices.py @@ -11,15 +11,15 @@ from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from ipam.api.serializers_.ip import IPAddressSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from virtualization.api.serializers_.clusters import ClusterSerializer from .devicetypes import * +from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer from .platforms import PlatformSerializer from .racks import RackSerializer from .roles import DeviceRoleSerializer -from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer from .sites import LocationSerializer, SiteSerializer from .virtualchassis import VirtualChassisSerializer @@ -32,7 +32,7 @@ ) -class DeviceSerializer(NetBoxModelSerializer): +class DeviceSerializer(PrimaryModelSerializer): device_type = DeviceTypeSerializer(nested=True) role = DeviceRoleSerializer(nested=True) tenant = TenantSerializer( @@ -84,8 +84,8 @@ class Meta: 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'local_context_data', + 'tags', 'custom_fields', 'created', 'last_updated', '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', ] @@ -122,7 +122,7 @@ def get_config_context(self, obj): return obj.get_config_context() -class VirtualDeviceContextSerializer(NetBoxModelSerializer): +class VirtualDeviceContextSerializer(PrimaryModelSerializer): device = DeviceSerializer(nested=True) identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None) tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) @@ -138,13 +138,13 @@ class Meta: model = VirtualDeviceContext fields = [ 'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', + 'primary_ip4', 'primary_ip6', 'status', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count', ] brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') -class ModuleSerializer(NetBoxModelSerializer): +class ModuleSerializer(PrimaryModelSerializer): device = DeviceSerializer(nested=True) module_bay = NestedModuleBaySerializer() module_type = ModuleTypeSerializer(nested=True) @@ -154,12 +154,12 @@ class Meta: model = Module fields = [ 'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', - 'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'asset_tag', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') -class MACAddressSerializer(NetBoxModelSerializer): +class MACAddressSerializer(PrimaryModelSerializer): assigned_object_type = ContentTypeField( queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS), required=False, @@ -171,7 +171,7 @@ class Meta: model = MACAddress fields = [ 'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id', - 'assigned_object', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'assigned_object', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'mac_address', 'description') diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index 61e3833ec1b..59753847cc1 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -6,7 +6,7 @@ from dcim.choices import * from dcim.models import DeviceType, ModuleType, ModuleTypeProfile from netbox.api.fields import AttributesField, ChoiceField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from netbox.choices import * from .manufacturers import ManufacturerSerializer from .platforms import PlatformSerializer @@ -18,7 +18,7 @@ ) -class DeviceTypeSerializer(NetBoxModelSerializer): +class DeviceTypeSerializer(PrimaryModelSerializer): manufacturer = ManufacturerSerializer(nested=True) default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) u_height = serializers.DecimalField( @@ -54,7 +54,7 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', - 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', + 'weight_unit', 'front_image', 'rear_image', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', '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', @@ -63,18 +63,18 @@ class Meta: brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') -class ModuleTypeProfileSerializer(NetBoxModelSerializer): +class ModuleTypeProfileSerializer(PrimaryModelSerializer): class Meta: model = ModuleTypeProfile fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'owner', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') -class ModuleTypeSerializer(NetBoxModelSerializer): +class ModuleTypeSerializer(PrimaryModelSerializer): profile = ModuleTypeProfileSerializer( nested=True, required=False, @@ -105,7 +105,7 @@ class Meta: model = ModuleType fields = [ 'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow', - 'weight', 'weight_unit', 'description', 'attributes', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', + 'weight', 'weight_unit', 'description', 'attributes', 'owner', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description') diff --git a/netbox/dcim/api/serializers_/manufacturers.py b/netbox/dcim/api/serializers_/manufacturers.py index 1a1eea6ec35..bd6581389ae 100644 --- a/netbox/dcim/api/serializers_/manufacturers.py +++ b/netbox/dcim/api/serializers_/manufacturers.py @@ -1,13 +1,13 @@ from dcim.models import Manufacturer from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import OrganizationalModelSerializer __all__ = ( 'ManufacturerSerializer', ) -class ManufacturerSerializer(NetBoxModelSerializer): +class ManufacturerSerializer(OrganizationalModelSerializer): # Related object counts devicetype_count = RelatedObjectCountField('device_types') @@ -17,7 +17,7 @@ class ManufacturerSerializer(NetBoxModelSerializer): class Meta: model = Manufacturer fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py index 08f8a64a853..c86bd977323 100644 --- a/netbox/dcim/api/serializers_/platforms.py +++ b/netbox/dcim/api/serializers_/platforms.py @@ -24,7 +24,7 @@ class Meta: model = Platform fields = [ 'id', 'url', 'display_url', 'display', 'parent', 'name', 'slug', 'manufacturer', 'config_template', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', + 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', '_depth', ] brief_fields = ( diff --git a/netbox/dcim/api/serializers_/power.py b/netbox/dcim/api/serializers_/power.py index 4c2cf54fbcb..a9f83cdbe2e 100644 --- a/netbox/dcim/api/serializers_/power.py +++ b/netbox/dcim/api/serializers_/power.py @@ -1,7 +1,7 @@ from dcim.choices import * from dcim.models import PowerFeed, PowerPanel from netbox.api.fields import ChoiceField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from .base import ConnectedEndpointsSerializer from .cables import CabledObjectSerializer @@ -14,7 +14,7 @@ ) -class PowerPanelSerializer(NetBoxModelSerializer): +class PowerPanelSerializer(PrimaryModelSerializer): site = SiteSerializer(nested=True) location = LocationSerializer( nested=True, @@ -29,13 +29,13 @@ class PowerPanelSerializer(NetBoxModelSerializer): class Meta: model = PowerPanel fields = [ - 'id', 'url', 'display_url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', - 'custom_fields', 'powerfeed_count', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'site', 'location', 'name', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'powerfeed_count', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') -class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): +class PowerFeedSerializer(PrimaryModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): power_panel = PowerPanelSerializer(nested=True) rack = RackSerializer( nested=True, @@ -71,6 +71,7 @@ class Meta: 'id', 'url', 'display_url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', - 'description', 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + 'description', 'tenant', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + '_occupied', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index 9c2c739fe06..ef06dc5aa8c 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -5,7 +5,7 @@ from dcim.constants import * from dcim.models import Rack, RackReservation, RackRole, RackType from netbox.api.fields import ChoiceField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer from netbox.choices import * from netbox.config import ConfigItem from tenancy.api.serializers_.tenants import TenantSerializer @@ -22,7 +22,7 @@ ) -class RackRoleSerializer(NetBoxModelSerializer): +class RackRoleSerializer(OrganizationalModelSerializer): # Related object counts rack_count = RelatedObjectCountField('racks') @@ -30,13 +30,13 @@ class RackRoleSerializer(NetBoxModelSerializer): class Meta: model = RackRole fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'rack_count', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'rack_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') -class RackBaseSerializer(NetBoxModelSerializer): +class RackBaseSerializer(PrimaryModelSerializer): form_factor = ChoiceField( choices=RackFormFactorChoices, allow_blank=True, @@ -71,8 +71,8 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', - 'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description') @@ -130,13 +130,13 @@ class Meta: 'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', - 'mounting_depth', 'airflow', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'device_count', 'powerfeed_count', + 'mounting_depth', 'airflow', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', 'device_count', 'powerfeed_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') -class RackReservationSerializer(NetBoxModelSerializer): +class RackReservationSerializer(PrimaryModelSerializer): rack = RackSerializer( nested=True, ) @@ -157,7 +157,7 @@ class Meta: model = RackReservation fields = [ 'id', 'url', 'display_url', 'display', 'rack', 'units', 'status', 'created', 'last_updated', 'user', - 'tenant', 'description', 'comments', 'tags', 'custom_fields', + 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', ] brief_fields = ('id', 'url', 'display', 'status', 'user', 'description', 'units') diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py index 0f83655a63f..83622899c14 100644 --- a/netbox/dcim/api/serializers_/roles.py +++ b/netbox/dcim/api/serializers_/roles.py @@ -3,7 +3,7 @@ from dcim.models import DeviceRole, InventoryItemRole from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer from .nested import NestedDeviceRoleSerializer __all__ = ( @@ -23,14 +23,14 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'parent', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - 'comments', '_depth', + 'owner', 'comments', '_depth', ] brief_fields = ( 'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth' ) -class InventoryItemRoleSerializer(NetBoxModelSerializer): +class InventoryItemRoleSerializer(OrganizationalModelSerializer): # Related object counts inventoryitem_count = RelatedObjectCountField('inventory_items') @@ -38,7 +38,7 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer): class Meta: model = InventoryItemRole fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'inventoryitem_count', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'inventoryitem_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index 90f7b5d359e..d0f632945dd 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -6,7 +6,7 @@ from ipam.api.serializers_.asns import ASNSerializer from ipam.models import ASN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer @@ -27,7 +27,7 @@ class Meta: model = Region fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth', + 'created', 'last_updated', 'site_count', 'prefix_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') @@ -41,12 +41,12 @@ class Meta: model = SiteGroup fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth', + 'created', 'last_updated', 'site_count', 'prefix_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') -class SiteSerializer(NetBoxModelSerializer): +class SiteSerializer(PrimaryModelSerializer): status = ChoiceField(choices=SiteStatusChoices, required=False) region = RegionSerializer(nested=True, required=False, allow_null=True) group = SiteGroupSerializer(nested=True, required=False, allow_null=True) @@ -72,7 +72,7 @@ class Meta: model = Site fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', - 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', + 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', ] @@ -93,6 +93,6 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', - 'prefix_count', 'comments', '_depth', + 'prefix_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py index a93d2833f01..5e2ac7bec73 100644 --- a/netbox/dcim/api/serializers_/virtualchassis.py +++ b/netbox/dcim/api/serializers_/virtualchassis.py @@ -1,7 +1,7 @@ from rest_framework import serializers from dcim.models import VirtualChassis -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from .nested import NestedDeviceSerializer __all__ = ( @@ -9,7 +9,7 @@ ) -class VirtualChassisSerializer(NetBoxModelSerializer): +class VirtualChassisSerializer(PrimaryModelSerializer): master = NestedDeviceSerializer(required=False, allow_null=True, default=None) members = NestedDeviceSerializer(many=True, read_only=True) @@ -19,7 +19,7 @@ class VirtualChassisSerializer(NetBoxModelSerializer): class Meta: model = VirtualChassis fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', 'member_count', 'members', + 'id', 'url', 'display_url', 'display', 'name', 'domain', 'master', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'member_count', 'members', ] brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') diff --git a/netbox/extras/api/serializers_/configcontexts.py b/netbox/extras/api/serializers_/configcontexts.py index ff85f0fc69c..631dc461b10 100644 --- a/netbox/extras/api/serializers_/configcontexts.py +++ b/netbox/extras/api/serializers_/configcontexts.py @@ -8,7 +8,8 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.models import ConfigContext, ConfigContextProfile, Tag from netbox.api.fields import SerializedPKRelatedField -from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer +from netbox.api.serializers import ChangeLogMessageSerializer, PrimaryModelSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin from tenancy.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer from tenancy.models import Tenant, TenantGroup from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer @@ -20,13 +21,7 @@ ) -class ConfigContextProfileSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): - tags = serializers.SlugRelatedField( - queryset=Tag.objects.all(), - slug_field='slug', - required=False, - many=True - ) +class ConfigContextProfileSerializer(PrimaryModelSerializer): data_source = DataSourceSerializer( nested=True, required=False @@ -39,13 +34,13 @@ class ConfigContextProfileSerializer(ChangeLogMessageSerializer, ValidatedModelS class Meta: model = ConfigContextProfile fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'tags', 'comments', 'data_source', - 'data_path', 'data_file', 'data_synced', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'name', 'description', 'schema', 'tags', 'owner', 'comments', + 'data_source', 'data_path', 'data_file', 'data_synced', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') -class ConfigContextSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): profile = ConfigContextProfileSerializer( nested=True, required=False, @@ -156,7 +151,7 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'name', 'weight', 'profile', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', - 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', - 'data_synced', 'data', 'created', 'last_updated', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'owner', 'tags', 'data_source', 'data_path', + 'data_file', 'data_synced', 'data', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py index 244308535d8..068cdb4d4cc 100644 --- a/netbox/extras/api/serializers_/configtemplates.py +++ b/netbox/extras/api/serializers_/configtemplates.py @@ -2,13 +2,19 @@ from extras.models import ConfigTemplate from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer from netbox.api.serializers.features import TaggableModelSerializer +from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'ConfigTemplateSerializer', ) -class ConfigTemplateSerializer(ChangeLogMessageSerializer, TaggableModelSerializer, ValidatedModelSerializer): +class ConfigTemplateSerializer( + OwnerMixin, + ChangeLogMessageSerializer, + TaggableModelSerializer, + ValidatedModelSerializer +): data_source = DataSourceSerializer( nested=True, required=False @@ -23,6 +29,6 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', - 'data_synced', 'tags', 'created', 'last_updated', + 'data_synced', 'owner', 'tags', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py index 0d982a7e696..b1297943918 100644 --- a/netbox/extras/api/serializers_/customfields.py +++ b/netbox/extras/api/serializers_/customfields.py @@ -8,6 +8,7 @@ from extras.models import CustomField, CustomFieldChoiceSet from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'CustomFieldChoiceSetSerializer', @@ -15,7 +16,7 @@ ) -class CustomFieldChoiceSetSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class CustomFieldChoiceSetSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): base_choices = ChoiceField( choices=CustomFieldChoiceSetBaseChoices, required=False @@ -32,12 +33,12 @@ class Meta: model = CustomFieldChoiceSet fields = [ 'id', 'url', 'display_url', 'display', 'name', 'description', 'base_choices', 'extra_choices', - 'order_alphabetically', 'choices_count', 'created', 'last_updated', + 'order_alphabetically', 'choices_count', 'owner', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') -class CustomFieldSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class CustomFieldSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_fields'), many=True @@ -64,8 +65,8 @@ class Meta: 'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight', - 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'comments', 'created', - 'last_updated', + 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'owner', 'comments', + 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py index 951c3aded77..cca38f89f52 100644 --- a/netbox/extras/api/serializers_/customlinks.py +++ b/netbox/extras/api/serializers_/customlinks.py @@ -2,13 +2,14 @@ from extras.models import CustomLink from netbox.api.fields import ContentTypeField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'CustomLinkSerializer', ) -class CustomLinkSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class CustomLinkSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_links'), many=True @@ -18,6 +19,6 @@ class Meta: model = CustomLink fields = [ 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', - 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated', + 'weight', 'group_name', 'button_class', 'new_window', 'owner', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py index 926259cf3a8..ea33ef99df0 100644 --- a/netbox/extras/api/serializers_/events.py +++ b/netbox/extras/api/serializers_/events.py @@ -7,6 +7,7 @@ from extras.models import EventRule, Webhook from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer +from users.api.serializers_.mixins import OwnerMixin from utilities.api import get_serializer_for_model from .scripts import ScriptSerializer @@ -20,7 +21,7 @@ # Event Rules # -class EventRuleSerializer(NetBoxModelSerializer): +class EventRuleSerializer(OwnerMixin, NetBoxModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('event_rules'), many=True @@ -36,7 +37,7 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'event_types', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_object', 'description', 'custom_fields', - 'tags', 'created', 'last_updated', + 'owner', 'tags', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') @@ -56,13 +57,13 @@ def get_action_object(self, instance): # Webhooks # -class WebhookSerializer(NetBoxModelSerializer): +class WebhookSerializer(OwnerMixin, NetBoxModelSerializer): class Meta: model = Webhook fields = [ 'id', 'url', 'display_url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', - 'custom_fields', 'tags', 'created', 'last_updated', + 'custom_fields', 'owner', 'tags', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py index 0d3eed44239..8c4e453d6b5 100644 --- a/netbox/extras/api/serializers_/exporttemplates.py +++ b/netbox/extras/api/serializers_/exporttemplates.py @@ -3,13 +3,14 @@ from extras.models import ExportTemplate from netbox.api.fields import ContentTypeField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'ExportTemplateSerializer', ) -class ExportTemplateSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class ExportTemplateSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('export_templates'), many=True @@ -28,6 +29,6 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'environment_params', 'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source', - 'data_path', 'data_file', 'data_synced', 'created', 'last_updated', + 'data_path', 'data_file', 'data_synced', 'owner', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py index e7128389ca2..830453e6f55 100644 --- a/netbox/extras/api/serializers_/savedfilters.py +++ b/netbox/extras/api/serializers_/savedfilters.py @@ -2,13 +2,14 @@ from extras.models import SavedFilter from netbox.api.fields import ContentTypeField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'SavedFilterSerializer', ) -class SavedFilterSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class SavedFilterSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.all(), many=True @@ -18,6 +19,6 @@ class Meta: model = SavedFilter fields = [ 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', - 'enabled', 'shared', 'parameters', 'created', 'last_updated', + 'enabled', 'shared', 'parameters', 'owner', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py index 7567a454318..75ca4e9d268 100644 --- a/netbox/extras/api/serializers_/tags.py +++ b/netbox/extras/api/serializers_/tags.py @@ -6,6 +6,7 @@ from netbox.api.exceptions import SerializerNotFound from netbox.api.fields import ContentTypeField, RelatedObjectCountField from netbox.api.serializers import BaseModelSerializer, ChangeLogMessageSerializer, ValidatedModelSerializer +from users.api.serializers_.mixins import OwnerMixin from utilities.api import get_serializer_for_model __all__ = ( @@ -14,7 +15,7 @@ ) -class TagSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): +class TagSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer): object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('tags'), many=True, diff --git a/netbox/ipam/api/serializers_/asns.py b/netbox/ipam/api/serializers_/asns.py index 8baa073f558..b297ff59097 100644 --- a/netbox/ipam/api/serializers_/asns.py +++ b/netbox/ipam/api/serializers_/asns.py @@ -2,7 +2,7 @@ from ipam.models import ASN, ASNRange, RIR from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer __all__ = ( @@ -13,7 +13,7 @@ ) -class RIRSerializer(NetBoxModelSerializer): +class RIRSerializer(OrganizationalModelSerializer): # Related object counts aggregate_count = RelatedObjectCountField('aggregates') @@ -21,13 +21,13 @@ class RIRSerializer(NetBoxModelSerializer): class Meta: model = RIR fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'aggregate_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') -class ASNRangeSerializer(NetBoxModelSerializer): +class ASNRangeSerializer(OrganizationalModelSerializer): rir = RIRSerializer(nested=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True) asn_count = serializers.IntegerField(read_only=True) @@ -36,12 +36,12 @@ class Meta: model = ASNRange fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', - 'tags', 'custom_fields', 'created', 'last_updated', 'asn_count', + 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'asn_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') -class ASNSerializer(NetBoxModelSerializer): +class ASNSerializer(PrimaryModelSerializer): rir = RIRSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True) @@ -52,7 +52,7 @@ class ASNSerializer(NetBoxModelSerializer): class Meta: model = ASN fields = [ - 'id', 'url', 'display_url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', + 'id', 'url', 'display_url', 'display', 'asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count', ] brief_fields = ('id', 'url', 'display', 'asn', 'description') diff --git a/netbox/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py index b5bebbc9566..82750f1bab9 100644 --- a/netbox/ipam/api/serializers_/fhrpgroups.py +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -4,7 +4,7 @@ from ipam.models import FHRPGroup, FHRPGroupAssignment from netbox.api.fields import ContentTypeField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer from utilities.api import get_serializer_for_model from .ip import IPAddressSerializer @@ -14,14 +14,14 @@ ) -class FHRPGroupSerializer(NetBoxModelSerializer): +class FHRPGroupSerializer(PrimaryModelSerializer): ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True) class Meta: model = FHRPGroup fields = [ 'id', 'name', 'url', 'display_url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', + 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', ] brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py index 5337b86f1ad..7dd277479c0 100644 --- a/netbox/ipam/api/serializers_/ip.py +++ b/netbox/ipam/api/serializers_/ip.py @@ -7,7 +7,7 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS from ipam.models import Aggregate, IPAddress, IPRange, Prefix from netbox.api.fields import ChoiceField, ContentTypeField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from .asns import RIRSerializer @@ -28,7 +28,7 @@ ) -class AggregateSerializer(NetBoxModelSerializer): +class AggregateSerializer(PrimaryModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) rir = RIRSerializer(nested=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True) @@ -38,12 +38,12 @@ class Meta: model = Aggregate fields = [ 'id', 'url', 'display_url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') -class PrefixSerializer(NetBoxModelSerializer): +class PrefixSerializer(PrimaryModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) vrf = VRFSerializer(nested=True, required=False, allow_null=True) scope_type = ContentTypeField( @@ -68,7 +68,7 @@ class Meta: model = Prefix fields = [ 'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope', - 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', + 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', '_depth', ] brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') @@ -133,7 +133,7 @@ def to_representation(self, instance): # IP ranges # -class IPRangeSerializer(NetBoxModelSerializer): +class IPRangeSerializer(PrimaryModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) start_address = IPAddressField() end_address = IPAddressField() @@ -146,7 +146,7 @@ class Meta: model = IPRange fields = [ 'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', - 'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'mark_populated', 'mark_utilized', ] brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') @@ -156,7 +156,7 @@ class Meta: # IP addresses # -class IPAddressSerializer(NetBoxModelSerializer): +class IPAddressSerializer(PrimaryModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) address = IPAddressField() vrf = VRFSerializer(nested=True, required=False, allow_null=True) @@ -177,7 +177,7 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', - 'dns_name', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'dns_name', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') diff --git a/netbox/ipam/api/serializers_/roles.py b/netbox/ipam/api/serializers_/roles.py index 99fd6f4705a..b4fa2c1bcdd 100644 --- a/netbox/ipam/api/serializers_/roles.py +++ b/netbox/ipam/api/serializers_/roles.py @@ -1,13 +1,13 @@ from ipam.models import Role from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import OrganizationalModelSerializer __all__ = ( 'RoleSerializer', ) -class RoleSerializer(NetBoxModelSerializer): +class RoleSerializer(OrganizationalModelSerializer): # Related object counts prefix_count = RelatedObjectCountField('prefixes') @@ -16,7 +16,7 @@ class RoleSerializer(NetBoxModelSerializer): class Meta: model = Role fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'prefix_count', 'vlan_count', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'owner', 'tags', + 'custom_fields', 'created', 'last_updated', 'prefix_count', 'vlan_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py index c7c1bb1362e..ad7c3e00b19 100644 --- a/netbox/ipam/api/serializers_/services.py +++ b/netbox/ipam/api/serializers_/services.py @@ -6,7 +6,7 @@ from ipam.constants import SERVICE_ASSIGNMENT_MODELS from ipam.models import IPAddress, Service, ServiceTemplate from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from utilities.api import get_serializer_for_model from .ip import IPAddressSerializer @@ -16,19 +16,19 @@ ) -class ServiceTemplateSerializer(NetBoxModelSerializer): +class ServiceTemplateSerializer(PrimaryModelSerializer): protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) class Meta: model = ServiceTemplate fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') -class ServiceSerializer(NetBoxModelSerializer): +class ServiceSerializer(PrimaryModelSerializer): protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) ipaddresses = SerializedPKRelatedField( queryset=IPAddress.objects.all(), @@ -46,7 +46,7 @@ class Meta: model = Service fields = [ 'id', 'url', 'display_url', 'display', 'parent_object_type', 'parent_object_id', 'parent', 'name', - 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags', 'custom_fields', + 'protocol', 'ports', 'ipaddresses', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index 3eada3193bc..7f2633e273c 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -7,7 +7,7 @@ from ipam.constants import VLANGROUP_SCOPE_TYPES from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer @@ -24,7 +24,7 @@ ) -class VLANGroupSerializer(NetBoxModelSerializer): +class VLANGroupSerializer(OrganizationalModelSerializer): scope_type = ContentTypeField( queryset=ContentType.objects.filter( model__in=VLANGROUP_SCOPE_TYPES @@ -46,7 +46,8 @@ class Meta: model = VLANGroup fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges', - 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' + 'tenant', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', + 'utilization', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') validators = [] @@ -60,7 +61,7 @@ def get_scope(self, obj): return serializer(obj.scope, nested=True, context=context).data -class VLANSerializer(NetBoxModelSerializer): +class VLANSerializer(PrimaryModelSerializer): site = SiteSerializer(nested=True, required=False, allow_null=True) group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) tenant = TenantSerializer(nested=True, required=False, allow_null=True) @@ -77,7 +78,7 @@ class Meta: model = VLAN fields = [ 'id', 'url', 'display_url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', - 'description', 'qinq_role', 'qinq_svlan', 'comments', 'l2vpn_termination', 'tags', 'custom_fields', + 'description', 'qinq_role', 'qinq_svlan', 'owner', 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', ] brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') @@ -125,10 +126,10 @@ class Meta: fields = ['id', 'url', 'display', 'policy', 'local_vid', 'remote_vid', 'description'] -class VLANTranslationPolicySerializer(NetBoxModelSerializer): +class VLANTranslationPolicySerializer(PrimaryModelSerializer): rules = VLANTranslationRuleSerializer(many=True, read_only=True) class Meta: model = VLANTranslationPolicy - fields = ['id', 'url', 'display', 'name', 'description', 'display', 'rules'] + fields = ['id', 'url', 'display', 'name', 'description', 'display', 'rules', 'owner', 'comments'] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/ipam/api/serializers_/vrfs.py b/netbox/ipam/api/serializers_/vrfs.py index a23909108c1..67630f83cad 100644 --- a/netbox/ipam/api/serializers_/vrfs.py +++ b/netbox/ipam/api/serializers_/vrfs.py @@ -1,6 +1,6 @@ from ipam.models import RouteTarget, VRF from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer __all__ = ( @@ -9,19 +9,19 @@ ) -class RouteTargetSerializer(NetBoxModelSerializer): +class RouteTargetSerializer(PrimaryModelSerializer): tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = RouteTarget fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', + 'id', 'url', 'display_url', 'display', 'name', 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') -class VRFSerializer(NetBoxModelSerializer): +class VRFSerializer(PrimaryModelSerializer): tenant = TenantSerializer(nested=True, required=False, allow_null=True) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), @@ -43,8 +43,8 @@ class VRFSerializer(NetBoxModelSerializer): class Meta: model = VRF fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', - 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', - 'prefix_count', + 'id', 'url', 'display_url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'owner', + 'comments', 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', + 'ipaddress_count', 'prefix_count', ] brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') diff --git a/netbox/netbox/api/serializers/__init__.py b/netbox/netbox/api/serializers/__init__.py index d7ad19565f2..2d95e932b52 100644 --- a/netbox/netbox/api/serializers/__init__.py +++ b/netbox/netbox/api/serializers/__init__.py @@ -1,33 +1,6 @@ -from rest_framework import serializers - from .base import * from .features import * from .generic import * from .nested import * - - -# -# Base model serializers -# - -class NetBoxModelSerializer( - ChangeLogMessageSerializer, - TaggableModelSerializer, - CustomFieldModelSerializer, - ValidatedModelSerializer -): - """ - Adds support for custom fields and tags. - """ - pass - - -class NestedGroupModelSerializer(NetBoxModelSerializer): - """ - Extends PrimaryModelSerializer to include MPTT support. - """ - _depth = serializers.IntegerField(source='level', read_only=True) - - -class BulkOperationSerializer(ChangeLogMessageSerializer): - id = serializers.IntegerField() +from .models import * +from .bulk import * diff --git a/netbox/netbox/api/serializers/bulk.py b/netbox/netbox/api/serializers/bulk.py new file mode 100644 index 00000000000..c9fd2853492 --- /dev/null +++ b/netbox/netbox/api/serializers/bulk.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from .features import ChangeLogMessageSerializer + +__all__ = ( + 'BulkOperationSerializer', +) + + +class BulkOperationSerializer(ChangeLogMessageSerializer): + id = serializers.IntegerField() diff --git a/netbox/netbox/api/serializers/features.py b/netbox/netbox/api/serializers/features.py index 1ee92e828cc..00315868e3e 100644 --- a/netbox/netbox/api/serializers/features.py +++ b/netbox/netbox/api/serializers/features.py @@ -2,11 +2,13 @@ from rest_framework.fields import CreateOnlyDefault from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues +from .base import ValidatedModelSerializer from .nested import NestedTagSerializer __all__ = ( 'ChangeLogMessageSerializer', 'CustomFieldModelSerializer', + 'NetBoxModelSerializer', 'TaggableModelSerializer', ) @@ -76,3 +78,15 @@ def save(self, **kwargs): if self.instance is not None: self.instance._changelog_message = self.validated_data.get('changelog_message') return super().save(**kwargs) + + +class NetBoxModelSerializer( + ChangeLogMessageSerializer, + TaggableModelSerializer, + CustomFieldModelSerializer, + ValidatedModelSerializer +): + """ + Adds support for custom fields and tags. + """ + pass diff --git a/netbox/netbox/api/serializers/models.py b/netbox/netbox/api/serializers/models.py new file mode 100644 index 00000000000..5d81f5b9a53 --- /dev/null +++ b/netbox/netbox/api/serializers/models.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from .features import NetBoxModelSerializer +from users.api.serializers_.mixins import OwnerMixin + +__all__ = ( + 'NestedGroupModelSerializer', + 'OrganizationalModelSerializer', + 'PrimaryModelSerializer', +) + + +class PrimaryModelSerializer(OwnerMixin, NetBoxModelSerializer): + """ + Base serializer class for models inheriting from PrimaryModel. + """ + pass + + +class NestedGroupModelSerializer(OwnerMixin, NetBoxModelSerializer): + """ + Base serializer class for models inheriting from NestedGroupModel. + """ + _depth = serializers.IntegerField(source='level', read_only=True) + + +class OrganizationalModelSerializer(OwnerMixin, NetBoxModelSerializer): + """ + Base serializer class for models inheriting from OrganizationalModel. + """ + pass diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index df4284f28e1..e0d03d6e78b 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -641,7 +641,6 @@ def sync_data(self): register_model_feature('jobs', lambda model: issubclass(model, JobsMixin)) register_model_feature('journaling', lambda model: issubclass(model, JournalingMixin)) register_model_feature('notifications', lambda model: issubclass(model, NotificationsMixin)) -register_model_feature('owner', lambda model: issubclass(model, OwnerMixin)) register_model_feature('synced_data', lambda model: issubclass(model, SyncedDataMixin)) register_model_feature('tags', lambda model: issubclass(model, TagsMixin)) diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py index fd4d1ac8e5b..19c49613913 100644 --- a/netbox/tenancy/api/serializers_/contacts.py +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -4,7 +4,9 @@ from rest_framework import serializers from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.api.serializers import ( + NestedGroupModelSerializer, NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, +) from tenancy.choices import ContactPriorityChoices from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole from utilities.api import get_serializer_for_model @@ -26,23 +28,23 @@ class Meta: model = ContactGroup fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'contact_count', 'comments', '_depth', + 'created', 'last_updated', 'contact_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth') -class ContactRoleSerializer(NetBoxModelSerializer): +class ContactRoleSerializer(OrganizationalModelSerializer): class Meta: model = ContactRole fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') -class ContactSerializer(NetBoxModelSerializer): +class ContactSerializer(PrimaryModelSerializer): groups = SerializedPKRelatedField( queryset=ContactGroup.objects.all(), serializer=ContactGroupSerializer, @@ -55,7 +57,7 @@ class Meta: model = Contact fields = [ 'id', 'url', 'display_url', 'display', 'groups', 'name', 'title', 'phone', 'email', 'address', 'link', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/tenancy/api/serializers_/tenants.py b/netbox/tenancy/api/serializers_/tenants.py index 189397c70c2..277bdc98735 100644 --- a/netbox/tenancy/api/serializers_/tenants.py +++ b/netbox/tenancy/api/serializers_/tenants.py @@ -1,7 +1,7 @@ from rest_framework import serializers from netbox.api.fields import RelatedObjectCountField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from tenancy.models import Tenant, TenantGroup from .nested import NestedTenantGroupSerializer @@ -19,12 +19,12 @@ class Meta: model = TenantGroup fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'tenant_count', 'comments', '_depth', + 'created', 'last_updated', 'tenant_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth') -class TenantSerializer(NetBoxModelSerializer): +class TenantSerializer(PrimaryModelSerializer): group = TenantGroupSerializer(nested=True, required=False, allow_null=True, default=None) # Related object counts @@ -42,7 +42,7 @@ class TenantSerializer(NetBoxModelSerializer): class Meta: model = Tenant fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'group', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', diff --git a/netbox/users/api/serializers_/mixins.py b/netbox/users/api/serializers_/mixins.py new file mode 100644 index 00000000000..b5f94cafdde --- /dev/null +++ b/netbox/users/api/serializers_/mixins.py @@ -0,0 +1,18 @@ +from rest_framework import serializers + +from users.api.serializers_.owners import OwnerSerializer + +__all__ = ( + 'OwnerMixin', +) + + +class OwnerMixin(serializers.Serializer): + """ + Adds an `owner` field for models which have a ForeignKey to users.Owner. + """ + owner = OwnerSerializer( + nested=True, + required=False, + allow_null=True, + ) diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py index ff64db1cfcc..a48af9ce0ff 100644 --- a/netbox/virtualization/api/serializers_/clusters.py +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -16,7 +16,7 @@ ) -class ClusterTypeSerializer(NetBoxModelSerializer): +class ClusterTypeSerializer(OrganizationalModelSerializer): # Related object counts cluster_count = RelatedObjectCountField('clusters') @@ -24,13 +24,13 @@ class ClusterTypeSerializer(NetBoxModelSerializer): class Meta: model = ClusterType fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') -class ClusterGroupSerializer(NetBoxModelSerializer): +class ClusterGroupSerializer(OrganizationalModelSerializer): # Related object counts cluster_count = RelatedObjectCountField('clusters') @@ -38,13 +38,13 @@ class ClusterGroupSerializer(NetBoxModelSerializer): class Meta: model = ClusterGroup fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') -class ClusterSerializer(NetBoxModelSerializer): +class ClusterSerializer(PrimaryModelSerializer): type = ClusterTypeSerializer(nested=True) group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) status = ChoiceField(choices=ClusterStatusChoices, required=False) @@ -76,7 +76,7 @@ class Meta: model = Cluster fields = [ 'id', 'url', 'display_url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'scope_type', - 'scope_id', 'scope', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'scope_id', 'scope', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'allocated_vcpus', 'allocated_memory', 'allocated_disk' ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py index ed14b0a2956..c035a436a0a 100644 --- a/netbox/virtualization/api/serializers_/virtualmachines.py +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -13,7 +13,7 @@ from ipam.api.serializers_.vrfs import VRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from virtualization.choices import * from virtualization.models import VirtualDisk, VirtualMachine, VMInterface @@ -29,7 +29,7 @@ ) -class VirtualMachineSerializer(NetBoxModelSerializer): +class VirtualMachineSerializer(PrimaryModelSerializer): status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) cluster = ClusterSerializer(nested=True, required=False, allow_null=True, default=None) @@ -51,8 +51,8 @@ class Meta: fields = [ 'id', 'url', 'display_url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'serial', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', - 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', 'virtual_disk_count', + 'owner', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', + 'last_updated', 'interface_count', 'virtual_disk_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/vpn/api/serializers_/crypto.py b/netbox/vpn/api/serializers_/crypto.py index 50085884bd8..83b10af155c 100644 --- a/netbox/vpn/api/serializers_/crypto.py +++ b/netbox/vpn/api/serializers_/crypto.py @@ -1,5 +1,5 @@ from netbox.api.fields import ChoiceField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from vpn.choices import * from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal @@ -12,7 +12,7 @@ ) -class IKEProposalSerializer(NetBoxModelSerializer): +class IKEProposalSerializer(PrimaryModelSerializer): authentication_method = ChoiceField( choices=AuthenticationMethodChoices ) @@ -31,13 +31,13 @@ class Meta: model = IKEProposal fields = ( 'id', 'url', 'display_url', 'display', 'name', 'description', 'authentication_method', - 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', + 'encryption_algorithm', 'authentication_algorithm', 'group', 'sa_lifetime', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ) brief_fields = ('id', 'url', 'display', 'name', 'description') -class IKEPolicySerializer(NetBoxModelSerializer): +class IKEPolicySerializer(PrimaryModelSerializer): version = ChoiceField( choices=IKEVersionChoices ) @@ -57,12 +57,12 @@ class Meta: model = IKEPolicy fields = ( 'id', 'url', 'display_url', 'display', 'name', 'description', 'version', 'mode', 'proposals', - 'preshared_key', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'preshared_key', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ) brief_fields = ('id', 'url', 'display', 'name', 'description') -class IPSecProposalSerializer(NetBoxModelSerializer): +class IPSecProposalSerializer(PrimaryModelSerializer): encryption_algorithm = ChoiceField( choices=EncryptionAlgorithmChoices, required=False @@ -76,13 +76,13 @@ class Meta: model = IPSecProposal fields = ( 'id', 'url', 'display_url', 'display', 'name', 'description', 'encryption_algorithm', - 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', + 'authentication_algorithm', 'sa_lifetime_seconds', 'sa_lifetime_data', 'owner', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', ) brief_fields = ('id', 'url', 'display', 'name', 'description') -class IPSecPolicySerializer(NetBoxModelSerializer): +class IPSecPolicySerializer(PrimaryModelSerializer): proposals = SerializedPKRelatedField( queryset=IPSecProposal.objects.all(), serializer=IPSecProposalSerializer, @@ -98,13 +98,13 @@ class IPSecPolicySerializer(NetBoxModelSerializer): class Meta: model = IPSecPolicy fields = ( - 'id', 'url', 'display_url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display_url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ) brief_fields = ('id', 'url', 'display', 'name', 'description') -class IPSecProfileSerializer(NetBoxModelSerializer): +class IPSecProfileSerializer(PrimaryModelSerializer): mode = ChoiceField( choices=IPSecModeChoices ) @@ -118,7 +118,7 @@ class IPSecProfileSerializer(NetBoxModelSerializer): class Meta: model = IPSecProfile fields = ( - 'id', 'url', 'display_url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', + 'id', 'url', 'display_url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ) brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py index f7c27113a62..f9e9a9a9749 100644 --- a/netbox/vpn/api/serializers_/l2vpn.py +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -5,7 +5,7 @@ from ipam.api.serializers_.vrfs import RouteTargetSerializer from ipam.models import RouteTarget from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * @@ -17,7 +17,7 @@ ) -class L2VPNSerializer(NetBoxModelSerializer): +class L2VPNSerializer(PrimaryModelSerializer): type = ChoiceField(choices=L2VPNTypeChoices, required=False) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), @@ -40,7 +40,8 @@ class Meta: model = L2VPN fields = [ 'id', 'url', 'display_url', 'display', 'identifier', 'name', 'slug', 'type', 'status', 'import_targets', - 'export_targets', 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' + 'export_targets', 'description', 'owner', 'comments', 'tenant', 'tags', 'custom_fields', 'created', + 'last_updated', ] brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description') diff --git a/netbox/vpn/api/serializers_/tunnels.py b/netbox/vpn/api/serializers_/tunnels.py index a89766d3f5a..dfeb0339ff0 100644 --- a/netbox/vpn/api/serializers_/tunnels.py +++ b/netbox/vpn/api/serializers_/tunnels.py @@ -4,7 +4,7 @@ from ipam.api.serializers_.ip import IPAddressSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * @@ -22,7 +22,7 @@ # Tunnels # -class TunnelGroupSerializer(NetBoxModelSerializer): +class TunnelGroupSerializer(OrganizationalModelSerializer): # Related object counts tunnel_count = RelatedObjectCountField('tunnels') @@ -30,13 +30,13 @@ class TunnelGroupSerializer(NetBoxModelSerializer): class Meta: model = TunnelGroup fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', + 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'tunnel_count', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') -class TunnelSerializer(NetBoxModelSerializer): +class TunnelSerializer(PrimaryModelSerializer): status = ChoiceField( choices=TunnelStatusChoices ) @@ -67,8 +67,8 @@ class Meta: model = Tunnel fields = ( 'id', 'url', 'display_url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', - 'tenant', 'tunnel_id', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'terminations_count', + 'tenant', 'tunnel_id', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', 'terminations_count', ) brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py index 97d57f9f572..7403fe860f8 100644 --- a/netbox/wireless/api/serializers_/wirelesslans.py +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -1,11 +1,11 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from dcim.constants import LOCATION_SCOPE_TYPES -from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from ipam.api.serializers_.vlans import VLANSerializer from netbox.api.fields import ChoiceField, ContentTypeField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from wireless.choices import * @@ -26,12 +26,12 @@ class Meta: model = WirelessLANGroup fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'wirelesslan_count', 'comments', '_depth', + 'created', 'last_updated', 'wirelesslan_count', 'owner', 'comments', '_depth', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth') -class WirelessLANSerializer(NetBoxModelSerializer): +class WirelessLANSerializer(PrimaryModelSerializer): group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) vlan = VLANSerializer(nested=True, required=False, allow_null=True) @@ -53,8 +53,8 @@ class Meta: model = WirelessLAN fields = [ 'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'scope_type', - 'scope_id', 'scope', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', + 'scope_id', 'scope', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/api/serializers_/wirelesslinks.py b/netbox/wireless/api/serializers_/wirelesslinks.py index 62f9988f1d3..7b010d298a8 100644 --- a/netbox/wireless/api/serializers_/wirelesslinks.py +++ b/netbox/wireless/api/serializers_/wirelesslinks.py @@ -1,7 +1,7 @@ from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.choices import LinkStatusChoices from netbox.api.fields import ChoiceField -from netbox.api.serializers import NetBoxModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from netbox.choices import * from tenancy.api.serializers_.tenants import TenantSerializer from wireless.choices import * @@ -12,7 +12,7 @@ ) -class WirelessLinkSerializer(NetBoxModelSerializer): +class WirelessLinkSerializer(PrimaryModelSerializer): status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = InterfaceSerializer(nested=True) interface_b = InterfaceSerializer(nested=True) @@ -25,7 +25,7 @@ class Meta: model = WirelessLink fields = [ 'id', 'url', 'display_url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', - 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'owner', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'ssid', 'description') From 789139b88a229041bdaff6894d92a0b7256c0544 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 20 Oct 2025 15:19:53 -0400 Subject: [PATCH 04/40] Add 'owner' field to bulk operation forms --- netbox/circuits/forms/filtersets.py | 16 ++--- netbox/core/forms/filtersets.py | 5 +- netbox/dcim/forms/filtersets.py | 74 ++++++++++++++--------- netbox/extras/forms/filtersets.py | 7 +-- netbox/ipam/forms/filtersets.py | 35 ++++++----- netbox/netbox/forms/base.py | 23 ++++++- netbox/templates/generic/bulk_edit.html | 10 +++ netbox/tenancy/forms/filtersets.py | 18 +++++- netbox/virtualization/forms/filtersets.py | 13 ++-- netbox/vpn/forms/filtersets.py | 16 ++--- netbox/wireless/forms/filtersets.py | 8 ++- 11 files changed, 149 insertions(+), 76 deletions(-) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 9b212998977..73adb26c5f0 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -34,7 +34,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Provider fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('asn_id', name=_('ASN')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -69,7 +69,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = ProviderAccount fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('provider_id', 'account', name=_('Attributes')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) @@ -88,7 +88,7 @@ class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): model = ProviderNetwork fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('provider_id', 'service_id', name=_('Attributes')), ) provider_id = DynamicModelMultipleChoiceField( @@ -107,7 +107,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): class CircuitTypeFilterForm(NetBoxModelFilterSetForm): model = CircuitType fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('color', name=_('Attributes')), ) tag = TagFilterField(model) @@ -121,7 +121,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Circuit fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), FieldSet( 'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', @@ -274,7 +274,7 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm): class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = CircuitGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) tag = TagFilterField(model) @@ -312,7 +312,7 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm): class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm): model = VirtualCircuitType fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('color', name=_('Attributes')), ) tag = TagFilterField(model) @@ -326,7 +326,7 @@ class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm): class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = VirtualCircuit fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), FieldSet('type_id', 'status', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 0f25932e021..40ee399b5bc 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -9,7 +9,7 @@ from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ( - ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, + ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker @@ -26,7 +26,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm): model = DataSource fieldsets = ( - FieldSet('q', 'filter_id'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')), ) type = forms.MultipleChoiceField( @@ -51,6 +51,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm): choices=JobIntervalChoices, required=False ) + tag = TagFilterField(model) class DataFileFilterForm(NetBoxModelFilterSetForm): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index daa3eef6555..14a51ad78e9 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -142,7 +142,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Region fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'parent_id'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', name=_('Region')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) parent_id = DynamicModelMultipleChoiceField( @@ -156,7 +157,8 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = SiteGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'parent_id'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', name=_('Site Group')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) parent_id = DynamicModelMultipleChoiceField( @@ -170,7 +172,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Site fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), 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')), @@ -202,7 +204,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Location fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -249,6 +251,9 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF class RackRoleFilterForm(NetBoxModelFilterSetForm): model = RackRole + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + ) tag = TagFilterField(model) @@ -303,7 +308,7 @@ class RackBaseFilterForm(NetBoxModelFilterSetForm): class RackTypeFilterForm(RackBaseFilterForm): model = RackType fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', name=_('Rack Type')), FieldSet('starting_unit', 'desc_units', name=_('Numbering')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), @@ -320,7 +325,7 @@ class RackTypeFilterForm(RackBaseFilterForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm): model = Rack fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')), @@ -416,7 +421,7 @@ class RackElevationFilterForm(RackFilterForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RackReservation fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('status', 'user_id', name=_('Reservation')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), @@ -474,7 +479,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Manufacturer fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) tag = TagFilterField(model) @@ -483,7 +488,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm): model = DeviceType fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet( 'manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow', name=_('Hardware') ), @@ -611,7 +616,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm): model = ModuleTypeProfile fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), ) selector_fields = ('filter_id', 'q') @@ -619,7 +624,7 @@ class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm): model = ModuleType fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')), FieldSet( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', @@ -703,6 +708,10 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class DeviceRoleFilterForm(NetBoxModelFilterSetForm): model = DeviceRole + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', 'config_template_id', name=_('Device Role')) + ) config_template_id = DynamicModelMultipleChoiceField( queryset=ConfigTemplate.objects.all(), required=False, @@ -718,6 +727,10 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm): class PlatformFilterForm(NetBoxModelFilterSetForm): model = Platform + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')) + ) selector_fields = ('filter_id', 'q', 'manufacturer_id') parent_id = DynamicModelMultipleChoiceField( queryset=Platform.objects.all(), @@ -745,7 +758,7 @@ class DeviceFilterForm( ): model = Device fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), 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')), @@ -941,7 +954,7 @@ class VirtualDeviceContextFilterForm( ): model = VirtualDeviceContext fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -968,7 +981,7 @@ class VirtualDeviceContextFilterForm( class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Module fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')), FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')), ) @@ -1051,7 +1064,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualChassis fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -1080,7 +1093,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cable fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), 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')), @@ -1164,7 +1177,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = PowerPanel fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) @@ -1203,7 +1216,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = PowerFeed fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), 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')), @@ -1313,7 +1326,7 @@ class PathEndpointFilterForm(CabledFilterForm): class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsolePort fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1337,7 +1350,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsoleServerPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1362,7 +1375,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1381,7 +1394,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerOutlet fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1410,7 +1423,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = Interface fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), 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')), @@ -1535,7 +1548,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1559,7 +1572,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): model = RearPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1583,7 +1596,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm): model = ModuleBay fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', 'position', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1601,7 +1614,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'label', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1615,7 +1628,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet( 'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered', name=_('Attributes') @@ -1665,6 +1678,9 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): model = InventoryItemRole + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + ) tag = TagFilterField(model) @@ -1675,7 +1691,7 @@ class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): class MACAddressFilterForm(NetBoxModelFilterSetForm): model = MACAddress fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')), ) selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id') diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 85a043f3e83..759c1fc4b45 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -290,7 +290,7 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm): class WebhookFilterForm(NetBoxModelFilterSetForm): model = Webhook fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')), ) http_content_type = forms.CharField( @@ -311,10 +311,8 @@ class WebhookFilterForm(NetBoxModelFilterSetForm): class EventRuleFilterForm(NetBoxModelFilterSetForm): model = EventRule - tag = TagFilterField(model) - fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')), ) object_type_id = ContentTypeMultipleChoiceField( @@ -339,6 +337,7 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + tag = TagFilterField(model) class TagFilterForm(SavedFiltersMixin, FilterForm): diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index dcd9ab5e25f..cbded745048 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -45,7 +45,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VRF fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -65,7 +65,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RouteTarget fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -84,6 +84,10 @@ class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RIRFilterForm(NetBoxModelFilterSetForm): model = RIR + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('is_private', name=_('RIR')), + ) is_private = forms.NullBooleanField( required=False, label=_('Private'), @@ -97,7 +101,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm): class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Aggregate fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('family', 'rir_id', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -118,7 +122,7 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASNRange fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('rir_id', 'start', 'end', name=_('Range')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -141,7 +145,7 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), ) @@ -165,13 +169,16 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RoleFilterForm(NetBoxModelFilterSetForm): model = Role + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + ) tag = TagFilterField(model) class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm, ): model = Prefix fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet( 'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized', name=_('Addressing') @@ -277,7 +284,7 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFil class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = IPRange fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -324,7 +331,7 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFi class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = IPAddress fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet( 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name', name=_('Attributes') @@ -402,7 +409,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel class FHRPGroupFilterForm(NetBoxModelFilterSetForm): model = FHRPGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', 'protocol', 'group_id', name=_('Attributes')), FieldSet('auth_type', 'auth_key', name=_('Authentication')), ) @@ -434,7 +441,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')), FieldSet('cluster_group', 'cluster', name=_('Cluster')), FieldSet('contains_vid', name=_('VLANs')), @@ -488,7 +495,7 @@ class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm): model = VLANTranslationPolicy fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('name', name=_('Attributes')), ) name = forms.CharField( @@ -525,7 +532,7 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VLAN fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')), FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')), @@ -597,7 +604,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): model = ServiceTemplate fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('protocol', 'port', name=_('Attributes')), ) protocol = forms.ChoiceField( @@ -615,7 +622,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm): model = Service fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('protocol', 'port', name=_('Attributes')), FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 2eaf8a83a28..3c7cf03483d 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -8,8 +8,9 @@ from core.models import ObjectType from extras.choices import * from extras.models import CustomField, Tag +from users.models import Owner from utilities.forms import BulkEditForm, CSVModelForm -from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField, CSVModelChoiceField from utilities.forms.mixins import CheckLastUpdatedMixin from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, SavedFiltersMixin, TagsMixin @@ -87,8 +88,14 @@ def _post_clean(self): class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): """ - Base form for creating a NetBox objects from CSV data. Used for bulk importing. + Base form for creating NetBox objects from CSV data. Used for bulk importing. """ + owner = CSVModelChoiceField( + queryset=Owner.objects.all(), + required=False, + to_field_name='name', + help_text=_("Name of the object's owner") + ) tags = CSVModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), @@ -149,11 +156,16 @@ def _get_form_field(self, customfield): return customfield.to_form_field(set_initial=False, enforce_required=False) def _extend_nullable_fields(self): + nullable_common_fields = ['owner'] nullable_custom_fields = [ name for name, customfield in self.custom_fields.items() if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES) ] - self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) + self.nullable_fields = ( + *self.nullable_fields, + *nullable_common_fields, + *nullable_custom_fields, + ) class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form): @@ -172,6 +184,11 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form) required=False, label=_('Search') ) + owner_id = DynamicModelMultipleChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) selector_fields = ('filter_id', 'q') diff --git a/netbox/templates/generic/bulk_edit.html b/netbox/templates/generic/bulk_edit.html index ffb0d59a9e4..2b38cdaef46 100644 --- a/netbox/templates/generic/bulk_edit.html +++ b/netbox/templates/generic/bulk_edit.html @@ -58,6 +58,16 @@ {% render_fieldset form fieldset %} {% endfor %} + {# Render owner field (if defined) #} + {% if form.owner %} +
+
+

{% trans "Owner" %}

+
+ {% render_field form.owner bulk_nullable=True %} +
+ {% endif %} + {# Render tag add/remove fields #} {% if form.add_tags and form.remove_tags %}
diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 6541d969339..90dca58484a 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -27,6 +27,10 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): model = TenantGroup + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', name=_('Tenant Group')), + ) parent_id = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), required=False, @@ -38,7 +42,8 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = Tenant fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'group_id'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('group_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) group_id = DynamicModelMultipleChoiceField( @@ -56,6 +61,10 @@ class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ContactGroupFilterForm(NetBoxModelFilterSetForm): model = ContactGroup + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', name=_('Contact Group')), + ) parent_id = DynamicModelMultipleChoiceField( queryset=ContactGroup.objects.all(), required=False, @@ -66,11 +75,18 @@ class ContactGroupFilterForm(NetBoxModelFilterSetForm): class ContactRoleFilterForm(NetBoxModelFilterSetForm): model = ContactRole + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + ) tag = TagFilterField(model) class ContactFilterForm(NetBoxModelFilterSetForm): model = Contact + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('group_id', name=_('Contact')), + ) group_id = DynamicModelMultipleChoiceField( queryset=ContactGroup.objects.all(), required=False, diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index b081fa8c610..43f10b09e3a 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -27,6 +27,9 @@ class ClusterTypeFilterForm(NetBoxModelFilterSetForm): model = ClusterType + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + ) tag = TagFilterField(model) @@ -34,7 +37,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = ClusterGroup tag = TagFilterField(model) fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) @@ -42,7 +45,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): model = Cluster fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('group_id', 'type_id', 'status', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), @@ -101,7 +104,7 @@ class VirtualMachineFilterForm( ): model = VirtualMachine fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id', name=_('Cluster')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet( @@ -199,7 +202,7 @@ class VirtualMachineFilterForm( class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('cluster_id', 'virtual_machine_id', name=_('Virtual Machine')), FieldSet('enabled', name=_('Attributes')), FieldSet('vrf_id', 'l2vpn_id', 'mac_address', name=_('Addressing')), @@ -256,7 +259,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): class VirtualDiskFilterForm(NetBoxModelFilterSetForm): model = VirtualDisk fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('virtual_machine_id', name=_('Virtual Machine')), FieldSet('size', name=_('Attributes')), ) diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index 4f814f70964..6a4d91a0cae 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -33,7 +33,7 @@ class TunnelGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): model = TunnelGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) tag = TagFilterField(model) @@ -42,7 +42,7 @@ class TunnelGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Tunnel fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('status', 'encapsulation', 'tunnel_id', name=_('Tunnel')), FieldSet('ipsec_profile_id', name=_('Security')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenancy')), @@ -97,7 +97,7 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): class IKEProposalFilterForm(NetBoxModelFilterSetForm): model = IKEProposal fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet( 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', name=_('Parameters') ), @@ -128,7 +128,7 @@ class IKEProposalFilterForm(NetBoxModelFilterSetForm): class IKEPolicyFilterForm(NetBoxModelFilterSetForm): model = IKEPolicy fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('version', 'mode', 'proposal_id', name=_('Parameters')), ) version = forms.MultipleChoiceField( @@ -152,7 +152,7 @@ class IKEPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProposalFilterForm(NetBoxModelFilterSetForm): model = IPSecProposal fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('encryption_algorithm', 'authentication_algorithm', name=_('Parameters')), ) encryption_algorithm = forms.MultipleChoiceField( @@ -171,7 +171,7 @@ class IPSecProposalFilterForm(NetBoxModelFilterSetForm): class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): model = IPSecPolicy fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('proposal_id', 'pfs_group', name=_('Parameters')), ) proposal_id = DynamicModelMultipleChoiceField( @@ -190,7 +190,7 @@ class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): class IPSecProfileFilterForm(NetBoxModelFilterSetForm): model = IPSecProfile fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('mode', 'ike_policy_id', 'ipsec_policy_id', name=_('Profile')), ) mode = forms.MultipleChoiceField( @@ -214,7 +214,7 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm): class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = L2VPN fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('type', 'status', 'import_target_id', 'export_target_id', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index f62a3be0671..4311eb2f71a 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -21,6 +21,10 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): model = WirelessLANGroup + fieldsets = ( + FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('parent_id', name=_('Wireless LAN group')), + ) parent_id = DynamicModelMultipleChoiceField( queryset=WirelessLANGroup.objects.all(), required=False, @@ -32,7 +36,7 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLAN fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('ssid', 'group_id', 'status', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), @@ -98,7 +102,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = WirelessLink fieldsets = ( - FieldSet('q', 'filter_id', 'tag'), + FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')), From 800cf5cf3d1678d33085446165e194f9b27ad76e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 10:26:48 -0400 Subject: [PATCH 05/40] Add owner filters --- netbox/circuits/filtersets.py | 12 ++--- netbox/core/filtersets.py | 4 +- netbox/dcim/filtersets.py | 83 ++++++++++------------------- netbox/extras/filtersets.py | 3 +- netbox/ipam/filtersets.py | 28 +++++----- netbox/netbox/filtersets.py | 18 +++++-- netbox/tenancy/filtersets.py | 8 +-- netbox/users/filterset_mixins.py | 24 +++++++++ netbox/virtualization/filtersets.py | 10 ++-- netbox/vpn/filtersets.py | 16 +++--- netbox/wireless/filtersets.py | 6 +-- 11 files changed, 111 insertions(+), 101 deletions(-) create mode 100644 netbox/users/filterset_mixins.py diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 7775255fca3..d693c5fb923 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -6,7 +6,7 @@ from dcim.filtersets import CabledObjectFilterSet from dcim.models import Interface, Location, Region, Site, SiteGroup from ipam.models import ASN -from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet +from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter, @@ -29,7 +29,7 @@ ) -class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): +class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='circuits__terminations___region', @@ -95,7 +95,7 @@ def search(self, queryset, name, value): ) -class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): +class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( queryset=Provider.objects.all(), label=_('Provider (ID)'), @@ -122,7 +122,7 @@ def search(self, queryset, name, value): ).distinct() -class ProviderNetworkFilterSet(NetBoxModelFilterSet): +class ProviderNetworkFilterSet(PrimaryModelFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( queryset=Provider.objects.all(), label=_('Provider (ID)'), @@ -156,7 +156,7 @@ class Meta: fields = ('id', 'name', 'slug', 'color', 'description') -class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( queryset=Provider.objects.all(), label=_('Provider (ID)'), @@ -475,7 +475,7 @@ class Meta: fields = ('id', 'name', 'slug', 'color', 'description') -class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet): provider_id = django_filters.ModelMultipleChoiceFilter( field_name='provider_network__provider', queryset=Provider.objects.all(), diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index 391ac02f7d0..0c660fd3e72 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -3,7 +3,7 @@ from django.db.models import Q from django.utils.translation import gettext as _ -from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet from netbox.utils import get_data_backend_choices from users.models import User from utilities.filters import ContentTypeFilter @@ -20,7 +20,7 @@ ) -class DataSourceFilterSet(NetBoxModelFilterSet): +class DataSourceFilterSet(PrimaryModelFilterSet): type = django_filters.MultipleChoiceFilter( choices=get_data_backend_choices, null_value=None diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 04ba3b00d53..ba7aa30c1cf 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -11,8 +11,8 @@ from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF from netbox.choices import ColorChoices from netbox.filtersets import ( - AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet, - OrganizationalModelFilterSet, + AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, + OrganizationalModelFilterSet, PrimaryModelFilterSet, ) from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.models import * @@ -143,7 +143,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): status = django_filters.MultipleChoiceFilter( choices=SiteStatusChoices, null_value=None @@ -293,7 +293,7 @@ class Meta: fields = ('id', 'name', 'slug', 'color', 'description') -class RackTypeFilterSet(NetBoxModelFilterSet): +class RackTypeFilterSet(PrimaryModelFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( queryset=Manufacturer.objects.all(), label=_('Manufacturer (ID)'), @@ -328,7 +328,7 @@ def search(self, queryset, name, value): ) -class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region', @@ -444,7 +444,7 @@ def search(self, queryset, name, value): ) -class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet): rack_id = django_filters.ModelMultipleChoiceFilter( queryset=Rack.objects.all(), label=_('Rack (ID)'), @@ -540,7 +540,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class DeviceTypeFilterSet(NetBoxModelFilterSet): +class DeviceTypeFilterSet(PrimaryModelFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( queryset=Manufacturer.objects.all(), label=_('Manufacturer (ID)'), @@ -682,7 +682,7 @@ def _inventory_items(self, queryset, name, value): return queryset.exclude(inventoryitemtemplates__isnull=value) -class ModuleTypeProfileFilterSet(NetBoxModelFilterSet): +class ModuleTypeProfileFilterSet(PrimaryModelFilterSet): class Meta: model = ModuleTypeProfile @@ -698,7 +698,7 @@ def search(self, queryset, name, value): ) -class ModuleTypeFilterSet(AttributeFiltersMixin, NetBoxModelFilterSet): +class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet): profile_id = django_filters.ModelMultipleChoiceFilter( queryset=ModuleTypeProfile.objects.all(), label=_('Profile (ID)'), @@ -1043,7 +1043,7 @@ def get_for_device_type(self, queryset, name, value): class DeviceFilterSet( - NetBoxModelFilterSet, + PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet, @@ -1345,7 +1345,7 @@ def _has_virtual_device_context(self, queryset, name, value): return queryset.exclude(params) -class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet): +class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet): device_id = django_filters.ModelMultipleChoiceFilter( field_name='device', queryset=Device.objects.all(), @@ -1394,7 +1394,7 @@ def _has_primary_ip(self, queryset, name, value): return queryset.exclude(params) -class ModuleFilterSet(NetBoxModelFilterSet): +class ModuleFilterSet(PrimaryModelFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='module_type__manufacturer', queryset=Manufacturer.objects.all(), @@ -1516,7 +1516,7 @@ def search(self, queryset, name, value): ).distinct() -class DeviceComponentFilterSet(django_filters.FilterSet): +class DeviceComponentFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -1682,12 +1682,7 @@ def filter_connected(self, queryset, name, value): return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) -class ConsolePortFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet, - PathEndpointFilterSet -): +class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -1698,12 +1693,7 @@ class Meta: fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') -class ConsoleServerPortFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet, - PathEndpointFilterSet -): +class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -1714,12 +1704,7 @@ class Meta: fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end') -class PowerPortFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet, - PathEndpointFilterSet -): +class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerPortTypeChoices, null_value=None @@ -1732,12 +1717,7 @@ class Meta: ) -class PowerOutletFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet, - PathEndpointFilterSet -): +class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerOutletTypeChoices, null_value=None @@ -1762,7 +1742,7 @@ class Meta: ) -class MACAddressFilterSet(NetBoxModelFilterSet): +class MACAddressFilterSet(PrimaryModelFilterSet): mac_address = MultiValueMACAddressFilter() assigned_object_type = ContentTypeFilter() device = MultiValueCharFilter( @@ -1914,7 +1894,6 @@ def filter_vlan(self, queryset, name, value): class InterfaceFilterSet( ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, CommonInterfaceFilterSet @@ -2075,11 +2054,7 @@ def filter_occupied(self, queryset, name, value): ) -class FrontPortFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet -): +class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -2095,11 +2070,7 @@ class Meta: ) -class RearPortFilterSet( - ModularDeviceComponentFilterSet, - NetBoxModelFilterSet, - CabledObjectFilterSet -): +class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -2112,7 +2083,7 @@ class Meta: ) -class ModuleBayFilterSet(ModularDeviceComponentFilterSet, NetBoxModelFilterSet): +class ModuleBayFilterSet(ModularDeviceComponentFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=ModuleBay.objects.all(), label=_('Parent module bay (ID)'), @@ -2128,7 +2099,7 @@ class Meta: fields = ('id', 'name', 'label', 'position', 'description') -class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): +class DeviceBayFilterSet(DeviceComponentFilterSet): installed_device_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label=_('Installed device (ID)'), @@ -2145,7 +2116,7 @@ class Meta: fields = ('id', 'name', 'label', 'description') -class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): +class InventoryItemFilterSet(DeviceComponentFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=InventoryItem.objects.all(), label=_('Parent inventory item (ID)'), @@ -2204,7 +2175,7 @@ class Meta: fields = ('id', 'name', 'slug', 'color', 'description') -class VirtualChassisFilterSet(NetBoxModelFilterSet): +class VirtualChassisFilterSet(PrimaryModelFilterSet): master_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label=_('Master (ID)'), @@ -2280,7 +2251,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter).distinct() -class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): +class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): termination_a_type = ContentTypeFilter( field_name='terminations__termination_type' ) @@ -2457,7 +2428,7 @@ class Meta: fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id') -class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): +class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region', @@ -2515,7 +2486,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter) -class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet): +class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='power_panel__site__region', diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index f34b2137087..c4e3502c08a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -7,6 +7,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from tenancy.models import Tenant, TenantGroup +from users.filterset_mixins import OwnerFilterMixin from users.models import Group, User from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter @@ -619,7 +620,7 @@ def search(self, queryset, name, value): ) -class ConfigContextFilterSet(ChangeLoggedModelFilterSet): +class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 1e2ed91ed68..3b738e5a774 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -11,7 +11,9 @@ from circuits.models import Provider from dcim.models import Device, Interface, Region, Site, SiteGroup -from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import ( + ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet, +) from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import ( @@ -45,7 +47,7 @@ ) -class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet): import_target_id = django_filters.ModelMultipleChoiceFilter( field_name='import_targets', queryset=RouteTarget.objects.all(), @@ -83,7 +85,7 @@ class Meta: fields = ('id', 'name', 'rd', 'enforce_unique', 'description') -class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet): importing_vrf_id = django_filters.ModelMultipleChoiceFilter( field_name='importing_vrfs', queryset=VRF.objects.all(), @@ -149,7 +151,7 @@ class Meta: fields = ('id', 'name', 'slug', 'is_private', 'description') -class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): family = django_filters.NumberFilter( field_name='prefix', lookup_expr='family' @@ -221,7 +223,7 @@ def search(self, queryset, name, value): ) -class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): +class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet): rir_id = django_filters.ModelMultipleChoiceFilter( queryset=RIR.objects.all(), label=_('RIR (ID)'), @@ -290,7 +292,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description', 'weight') -class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet): +class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet): family = django_filters.NumberFilter( field_name='prefix', lookup_expr='family' @@ -456,7 +458,7 @@ def filter_present_in_vrf(self, queryset, name, vrf): ).distinct() -class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilterSet): +class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): family = django_filters.NumberFilter( field_name='start_address', lookup_expr='family' @@ -548,7 +550,7 @@ def search_by_parent(self, queryset, name, value): return queryset.filter(q) -class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): family = django_filters.NumberFilter( field_name='address', lookup_expr='family' @@ -784,7 +786,7 @@ def _assigned(self, queryset, name, value): ) -class FHRPGroupFilterSet(NetBoxModelFilterSet): +class FHRPGroupFilterSet(PrimaryModelFilterSet): protocol = django_filters.MultipleChoiceFilter( choices=FHRPGroupProtocolChoices ) @@ -934,7 +936,7 @@ def filter_scope(self, queryset, name, value): ) -class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region', @@ -1085,7 +1087,7 @@ def filter_vminterface_id(self, queryset, name, value): ).distinct() -class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet): +class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet): class Meta: model = VLANTranslationPolicy @@ -1132,7 +1134,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter) -class ServiceTemplateFilterSet(NetBoxModelFilterSet): +class ServiceTemplateFilterSet(PrimaryModelFilterSet): port = NumericArrayFilter( field_name='ports', lookup_expr='contains' @@ -1152,7 +1154,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter) -class ServiceFilterSet(ContactModelFilterSet, NetBoxModelFilterSet): +class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet): parent_object_type = ContentTypeFilter() device = MultiValueCharFilter( method='filter_device', diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ea24efe48ae..24405cb6c1e 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -14,6 +14,7 @@ from extras.choices import CustomFieldFilterLogicChoices from extras.filters import TagFilter, TagIDFilter from extras.models import CustomField, SavedFilter +from users.filterset_mixins import OwnerFilterMixin from utilities.constants import ( FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, FILTER_NUMERIC_BASED_LOOKUP_MAP @@ -25,8 +26,10 @@ 'AttributeFiltersMixin', 'BaseFilterSet', 'ChangeLoggedModelFilterSet', + 'NestedGroupModelFilterSet', 'NetBoxModelFilterSet', 'OrganizationalModelFilterSet', + 'PrimaryModelFilterSet', ) STANDARD_LOOKUPS = ( @@ -328,9 +331,16 @@ def search(self, queryset, name, value): return queryset -class OrganizationalModelFilterSet(NetBoxModelFilterSet): +class PrimaryModelFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): """ - A base class for adding the search method to models which only expose the `name` and `slug` fields + Base filterset for models inheriting from PrimaryModel. + """ + pass + + +class OrganizationalModelFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): + """ + Base filterset for models inheriting from OrganizationalModel. """ def search(self, queryset, name, value): if not value.strip(): @@ -342,9 +352,9 @@ def search(self, queryset, name, value): ) -class NestedGroupModelFilterSet(NetBoxModelFilterSet): +class NestedGroupModelFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): """ - A base FilterSet for models that inherit from NestedGroupModel + Base filterset for models inheriting from NestedGroupModel. """ def search(self, queryset, name, value): if value.strip(): diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index ca0142db6cf..b650ea88255 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -2,7 +2,9 @@ from django.db.models import Q from django.utils.translation import gettext as _ -from netbox.filtersets import NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet +from netbox.filtersets import ( + NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet, +) from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter from .models import * @@ -64,7 +66,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class ContactFilterSet(NetBoxModelFilterSet): +class ContactFilterSet(PrimaryModelFilterSet): group_id = TreeNodeMultipleChoiceFilter( queryset=ContactGroup.objects.all(), field_name='groups', @@ -198,7 +200,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): +class TenantFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): group_id = TreeNodeMultipleChoiceFilter( queryset=TenantGroup.objects.all(), field_name='group', diff --git a/netbox/users/filterset_mixins.py b/netbox/users/filterset_mixins.py new file mode 100644 index 00000000000..33eb6f74330 --- /dev/null +++ b/netbox/users/filterset_mixins.py @@ -0,0 +1,24 @@ +import django_filters +from django.utils.translation import gettext as _ + +from users.models import Owner + +__all__ = ( + 'OwnerFilterMixin', +) + + +class OwnerFilterMixin(django_filters.FilterSet): + """ + Adds owner & owner_id filters for models which inherit from OwnerMixin. + """ + owner_id = django_filters.ModelMultipleChoiceFilter( + queryset=Owner.objects.all(), + label=_('Owner (ID)'), + ) + owner = django_filters.ModelMultipleChoiceFilter( + field_name='owner__name', + queryset=Owner.objects.all(), + to_field_name='name', + label=_('Owner (name)'), + ) diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 802e34e005b..567b20b6da6 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -9,7 +9,7 @@ from extras.filtersets import LocalConfigContextFilterSet from extras.models import ConfigTemplate from ipam.filtersets import PrimaryIPFilterSet -from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from .choices import * @@ -39,7 +39,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet): +class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet): group_id = django_filters.ModelMultipleChoiceFilter( queryset=ClusterGroup.objects.all(), label=_('Parent group (ID)'), @@ -80,7 +80,7 @@ def search(self, queryset, name, value): class VirtualMachineFilterSet( - NetBoxModelFilterSet, + PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet, @@ -235,7 +235,7 @@ def _has_primary_ip(self, queryset, name, value): return queryset.exclude(params) -class VMInterfaceFilterSet(NetBoxModelFilterSet, CommonInterfaceFilterSet): +class VMInterfaceFilterSet(PrimaryModelFilterSet, CommonInterfaceFilterSet): cluster_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_machine__cluster', queryset=Cluster.objects.all(), @@ -297,7 +297,7 @@ def search(self, queryset, name, value): ) -class VirtualDiskFilterSet(NetBoxModelFilterSet): +class VirtualDiskFilterSet(PrimaryModelFilterSet): virtual_machine_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_machine', queryset=VirtualMachine.objects.all(), diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py index d35831e2fa7..9010c5e2c7f 100644 --- a/netbox/vpn/filtersets.py +++ b/netbox/vpn/filtersets.py @@ -4,7 +4,7 @@ from dcim.models import Device, Interface from ipam.models import IPAddress, RouteTarget, VLAN -from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet +from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter from virtualization.models import VirtualMachine, VMInterface @@ -32,7 +32,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): status = django_filters.MultipleChoiceFilter( choices=TunnelStatusChoices ) @@ -123,7 +123,7 @@ class Meta: fields = ('id', 'termination_id') -class IKEProposalFilterSet(NetBoxModelFilterSet): +class IKEProposalFilterSet(PrimaryModelFilterSet): ike_policy_id = django_filters.ModelMultipleChoiceFilter( field_name='ike_policies', queryset=IKEPolicy.objects.all(), @@ -162,7 +162,7 @@ def search(self, queryset, name, value): ) -class IKEPolicyFilterSet(NetBoxModelFilterSet): +class IKEPolicyFilterSet(PrimaryModelFilterSet): version = django_filters.MultipleChoiceFilter( choices=IKEVersionChoices ) @@ -193,7 +193,7 @@ def search(self, queryset, name, value): ) -class IPSecProposalFilterSet(NetBoxModelFilterSet): +class IPSecProposalFilterSet(PrimaryModelFilterSet): ipsec_policy_id = django_filters.ModelMultipleChoiceFilter( field_name='ipsec_policies', queryset=IPSecPolicy.objects.all(), @@ -226,7 +226,7 @@ def search(self, queryset, name, value): ) -class IPSecPolicyFilterSet(NetBoxModelFilterSet): +class IPSecPolicyFilterSet(PrimaryModelFilterSet): pfs_group = django_filters.MultipleChoiceFilter( choices=DHGroupChoices ) @@ -254,7 +254,7 @@ def search(self, queryset, name, value): ) -class IPSecProfileFilterSet(NetBoxModelFilterSet): +class IPSecProfileFilterSet(PrimaryModelFilterSet): mode = django_filters.MultipleChoiceFilter( choices=IPSecModeChoices ) @@ -293,7 +293,7 @@ def search(self, queryset, name, value): ) -class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): +class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet): type = django_filters.MultipleChoiceFilter( choices=L2VPNTypeChoices, null_value=None diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index bd96865adb5..afd963a5a55 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -5,7 +5,7 @@ from dcim.base_filtersets import ScopedFilterSet from dcim.models import Interface from ipam.models import VLAN -from netbox.filtersets import NestedGroupModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import NestedGroupModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import TreeNodeMultipleChoiceFilter from .choices import * @@ -44,7 +44,7 @@ class Meta: fields = ('id', 'name', 'slug', 'description') -class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet): +class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet): group_id = TreeNodeMultipleChoiceFilter( queryset=WirelessLANGroup.objects.all(), field_name='group', @@ -87,7 +87,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter) -class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet): +class WirelessLinkFilterSet(PrimaryModelFilterSet, TenancyFilterSet): interface_a_id = django_filters.ModelMultipleChoiceFilter( queryset=Interface.objects.all() ) From a4d52b46ed99fab30e49b5220ba2366eaa2356ff Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 10:48:07 -0400 Subject: [PATCH 06/40] NestedGroupModel should inherit from NetBoxModel --- netbox/netbox/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 59475b06031..e6f1ecfd406 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -139,7 +139,7 @@ class Meta: abstract = True -class NestedGroupModel(NetBoxFeatureSet, OwnerMixin, MPTTModel): +class NestedGroupModel(OwnerMixin, NetBoxModel, MPTTModel): """ Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest recursively using MPTT. Within each parent, each child instance must have a unique name. From 7d0f68c97f8f1095bff82f5ff7726b5dacc94902 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 12:22:40 -0400 Subject: [PATCH 07/40] Update GraphQL types to support owner assignment --- netbox/circuits/graphql/types.py | 17 ++++------ netbox/core/graphql/types.py | 5 ++- netbox/dcim/graphql/types.py | 46 ++++++++++++++------------ netbox/extras/graphql/types.py | 25 +++++++------- netbox/ipam/graphql/types.py | 29 ++++++++-------- netbox/netbox/graphql/types.py | 38 ++++++++++++++++++--- netbox/tenancy/graphql/types.py | 10 +++--- netbox/users/graphql/mixins.py | 15 +++++++++ netbox/virtualization/graphql/types.py | 9 ++--- netbox/vpn/graphql/types.py | 20 +++++------ netbox/wireless/graphql/types.py | 8 ++--- 11 files changed, 131 insertions(+), 91 deletions(-) create mode 100644 netbox/users/graphql/mixins.py diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 89d2a33b6cc..8592e929d13 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -6,7 +6,7 @@ from circuits import models from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin -from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType +from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType from tenancy.graphql.types import TenantType from .filters import * @@ -35,8 +35,7 @@ filters=ProviderFilter, pagination=True ) -class ProviderType(NetBoxObjectType, ContactsMixin): - +class ProviderType(ContactsMixin, PrimaryObjectType): networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]] circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]] asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]] @@ -49,9 +48,8 @@ class ProviderType(NetBoxObjectType, ContactsMixin): filters=ProviderAccountFilter, pagination=True ) -class ProviderAccountType(ContactsMixin, NetBoxObjectType): +class ProviderAccountType(ContactsMixin, PrimaryObjectType): provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] - circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]] @@ -61,9 +59,8 @@ class ProviderAccountType(ContactsMixin, NetBoxObjectType): filters=ProviderNetworkFilter, pagination=True ) -class ProviderNetworkType(NetBoxObjectType): +class ProviderNetworkType(PrimaryObjectType): provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] - circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]] @@ -105,14 +102,13 @@ class CircuitTypeType(OrganizationalObjectType): filters=CircuitFilter, pagination=True ) -class CircuitType(NetBoxObjectType, ContactsMixin): +class CircuitType(PrimaryObjectType, ContactsMixin): provider: ProviderType provider_account: ProviderAccountType | None termination_a: CircuitTerminationType | None termination_z: CircuitTerminationType | None type: CircuitTypeType tenant: TenantType | None - terminations: List[CircuitTerminationType] @@ -178,12 +174,11 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): filters=VirtualCircuitFilter, pagination=True ) -class VirtualCircuitType(NetBoxObjectType): +class VirtualCircuitType(PrimaryObjectType): provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"]) provider_account: ProviderAccountType | None type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field( select_related=["type"] ) tenant: TenantType | None - terminations: List[VirtualCircuitTerminationType] diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index ffaa244116d..12407b5c7e3 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType as DjangoContentType from core import models -from netbox.graphql.types import BaseObjectType, NetBoxObjectType +from netbox.graphql.types import BaseObjectType, PrimaryObjectType from .filters import * __all__ = ( @@ -32,8 +32,7 @@ class DataFileType(BaseObjectType): filters=DataSourceFilter, pagination=True ) -class DataSourceType(NetBoxObjectType): - +class DataSourceType(PrimaryObjectType): datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 0cd5e8fd11b..568772247d6 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -14,7 +14,10 @@ ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt -from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType +from netbox.graphql.types import ( + BaseObjectType, NestedGroupObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType, +) +from users.graphql.mixins import OwnerMixin from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -94,6 +97,7 @@ class ComponentType( ChangelogMixin, CustomFieldsMixin, + OwnerMixin, TagsMixin, BaseObjectType ): @@ -159,7 +163,7 @@ class CableTerminationType(NetBoxObjectType): filters=CableFilter, pagination=True ) -class CableType(NetBoxObjectType): +class CableType(PrimaryObjectType): color: str tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -236,7 +240,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType): filters=DeviceFilter, pagination=True ) -class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): +class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType): console_port_count: BigInt console_server_port_count: BigInt power_port_count: BigInt @@ -339,7 +343,7 @@ def parent(self) -> Annotated["InventoryItemTemplateType", strawberry.lazy('dcim filters=DeviceRoleFilter, pagination=True ) -class DeviceRoleType(OrganizationalObjectType): +class DeviceRoleType(NestedGroupObjectType): parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]] color: str @@ -355,7 +359,7 @@ class DeviceRoleType(OrganizationalObjectType): filters=DeviceTypeFilter, pagination=True ) -class DeviceTypeType(NetBoxObjectType): +class DeviceTypeType(PrimaryObjectType): console_port_template_count: BigInt console_server_port_template_count: BigInt power_port_template_count: BigInt @@ -412,7 +416,7 @@ class FrontPortTemplateType(ModularComponentTemplateType): filters=MACAddressFilter, pagination=True ) -class MACAddressType(NetBoxObjectType): +class MACAddressType(PrimaryObjectType): mac_address: str @strawberry_django.field @@ -512,7 +516,7 @@ class InventoryItemRoleType(OrganizationalObjectType): filters=LocationFilter, pagination=True ) -class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): +class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NestedGroupObjectType): 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 @@ -555,7 +559,7 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin): filters=ModuleFilter, pagination=True ) -class ModuleType(NetBoxObjectType): +class ModuleType(PrimaryObjectType): 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')] @@ -602,7 +606,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType): filters=ModuleTypeProfileFilter, pagination=True ) -class ModuleTypeProfileType(NetBoxObjectType): +class ModuleTypeProfileType(PrimaryObjectType): module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]] @@ -612,7 +616,7 @@ class ModuleTypeProfileType(NetBoxObjectType): filters=ModuleTypeFilter, pagination=True ) -class ModuleTypeType(NetBoxObjectType): +class ModuleTypeType(PrimaryObjectType): profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @@ -632,7 +636,7 @@ class ModuleTypeType(NetBoxObjectType): filters=PlatformFilter, pagination=True ) -class PlatformType(OrganizationalObjectType): +class PlatformType(NestedGroupObjectType): parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None children: List[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]] manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None @@ -648,7 +652,7 @@ class PlatformType(OrganizationalObjectType): filters=PowerFeedFilter, pagination=True ) -class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): +class PowerFeedType(CabledObjectMixin, PathEndpointMixin, PrimaryObjectType): 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 @@ -681,7 +685,7 @@ class PowerOutletTemplateType(ModularComponentTemplateType): filters=PowerPanelFilter, pagination=True ) -class PowerPanelType(NetBoxObjectType, ContactsMixin): +class PowerPanelType(ContactsMixin, PrimaryObjectType): site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None @@ -715,7 +719,7 @@ class PowerPortTemplateType(ModularComponentTemplateType): filters=RackTypeFilter, pagination=True ) -class RackTypeType(NetBoxObjectType): +class RackTypeType(PrimaryObjectType): manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @@ -725,7 +729,7 @@ class RackTypeType(NetBoxObjectType): filters=RackFilter, pagination=True ) -class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): +class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType): 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 @@ -744,7 +748,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje filters=RackReservationFilter, pagination=True ) -class RackReservationType(NetBoxObjectType): +class RackReservationType(PrimaryObjectType): units: List[int] rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -793,7 +797,7 @@ class RearPortTemplateType(ModularComponentTemplateType): filters=RegionFilter, pagination=True ) -class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): +class RegionType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]] @@ -819,7 +823,7 @@ def circuit_terminations(self) -> List[ filters=SiteFilter, pagination=True ) -class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType): +class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType): time_zone: str | None region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None @@ -854,7 +858,7 @@ def circuit_terminations(self) -> List[ filters=SiteGroupFilter, pagination=True ) -class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): +class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]] @@ -880,7 +884,7 @@ def circuit_terminations(self) -> List[ filters=VirtualChassisFilter, pagination=True ) -class VirtualChassisType(NetBoxObjectType): +class VirtualChassisType(PrimaryObjectType): member_count: BigInt master: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None @@ -893,7 +897,7 @@ class VirtualChassisType(NetBoxObjectType): filters=VirtualDeviceContextFilter, pagination=True ) -class VirtualDeviceContextType(NetBoxObjectType): +class VirtualDeviceContextType(PrimaryObjectType): 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 diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 97637684e15..8230edea8e2 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -6,7 +6,8 @@ from core.graphql.mixins import SyncedDataMixin from extras import models from extras.graphql.mixins import CustomFieldsMixin, TagsMixin -from netbox.graphql.types import BaseObjectType, ContentTypeType, NetBoxObjectType, ObjectType, OrganizationalObjectType +from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, PrimaryObjectType +from users.graphql.mixins import OwnerMixin from .filters import * if TYPE_CHECKING: @@ -51,7 +52,7 @@ filters=ConfigContextProfileFilter, pagination=True ) -class ConfigContextProfileType(SyncedDataMixin, NetBoxObjectType): +class ConfigContextProfileType(SyncedDataMixin, PrimaryObjectType): pass @@ -61,7 +62,7 @@ class ConfigContextProfileType(SyncedDataMixin, NetBoxObjectType): filters=ConfigContextFilter, pagination=True ) -class ConfigContextType(SyncedDataMixin, ObjectType): +class ConfigContextType(SyncedDataMixin, OwnerMixin, ObjectType): profile: ConfigContextProfileType | None roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]] device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]] @@ -84,7 +85,7 @@ class ConfigContextType(SyncedDataMixin, ObjectType): filters=ConfigTemplateFilter, pagination=True ) -class ConfigTemplateType(SyncedDataMixin, TagsMixin, ObjectType): +class ConfigTemplateType(SyncedDataMixin, OwnerMixin, TagsMixin, ObjectType): virtualmachines: List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]] devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]] @@ -97,7 +98,7 @@ class ConfigTemplateType(SyncedDataMixin, TagsMixin, ObjectType): filters=CustomFieldFilter, pagination=True ) -class CustomFieldType(ObjectType): +class CustomFieldType(OwnerMixin, ObjectType): related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None @@ -108,7 +109,7 @@ class CustomFieldType(ObjectType): filters=CustomFieldChoiceSetFilter, pagination=True ) -class CustomFieldChoiceSetType(ObjectType): +class CustomFieldChoiceSetType(OwnerMixin, ObjectType): choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]] extra_choices: List[List[str]] | None @@ -120,7 +121,7 @@ class CustomFieldChoiceSetType(ObjectType): filters=CustomLinkFilter, pagination=True ) -class CustomLinkType(ObjectType): +class CustomLinkType(OwnerMixin, ObjectType): pass @@ -130,7 +131,7 @@ class CustomLinkType(ObjectType): filters=ExportTemplateFilter, pagination=True ) -class ExportTemplateType(SyncedDataMixin, ObjectType): +class ExportTemplateType(SyncedDataMixin, OwnerMixin, ObjectType): pass @@ -180,7 +181,7 @@ class NotificationGroupType(ObjectType): filters=SavedFilterFilter, pagination=True ) -class SavedFilterType(ObjectType): +class SavedFilterType(OwnerMixin, ObjectType): user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None @@ -209,7 +210,7 @@ class TableConfigType(ObjectType): filters=TagFilter, pagination=True ) -class TagType(ObjectType): +class TagType(OwnerMixin, ObjectType): color: str object_types: List[ContentTypeType] @@ -221,7 +222,7 @@ class TagType(ObjectType): filters=WebhookFilter, pagination=True ) -class WebhookType(OrganizationalObjectType): +class WebhookType(OwnerMixin, CustomFieldsMixin, TagsMixin, ObjectType): pass @@ -231,5 +232,5 @@ class WebhookType(OrganizationalObjectType): filters=EventRuleFilter, pagination=True ) -class EventRuleType(OrganizationalObjectType): +class EventRuleType(OwnerMixin, CustomFieldsMixin, TagsMixin, ObjectType): action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index a07316fe432..e63f0bff3d3 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -8,7 +8,7 @@ from extras.graphql.mixins import ContactsMixin from ipam import models from netbox.graphql.scalars import BigInt -from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType +from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType from .filters import * from .mixins import IPAddressesMixin @@ -74,7 +74,7 @@ def family(self) -> IPAddressFamilyType: filters=ASNFilter, pagination=True ) -class ASNType(NetBoxObjectType, ContactsMixin): +class ASNType(ContactsMixin, PrimaryObjectType): asn: BigInt rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -89,7 +89,7 @@ class ASNType(NetBoxObjectType, ContactsMixin): filters=ASNRangeFilter, pagination=True ) -class ASNRangeType(NetBoxObjectType): +class ASNRangeType(OrganizationalObjectType): start: BigInt end: BigInt rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None @@ -102,7 +102,7 @@ class ASNRangeType(NetBoxObjectType): filters=AggregateFilter, pagination=True ) -class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): +class AggregateType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType): prefix: str rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -114,8 +114,7 @@ class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): filters=FHRPGroupFilter, pagination=True ) -class FHRPGroupType(NetBoxObjectType, IPAddressesMixin): - +class FHRPGroupType(IPAddressesMixin, PrimaryObjectType): fhrpgroupassignment_set: List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]] @@ -142,7 +141,7 @@ def interface(self) -> Annotated[Union[ filters=IPAddressFilter, pagination=True ) -class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): +class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType): address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -167,7 +166,7 @@ def assigned_object(self) -> Annotated[Union[ filters=IPRangeFilter, pagination=True ) -class IPRangeType(NetBoxObjectType, ContactsMixin): +class IPRangeType(ContactsMixin, PrimaryObjectType): start_address: str end_address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @@ -181,7 +180,7 @@ class IPRangeType(NetBoxObjectType, ContactsMixin): filters=PrefixFilter, pagination=True ) -class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): +class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType): prefix: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -230,7 +229,7 @@ class RoleType(OrganizationalObjectType): filters=RouteTargetFilter, pagination=True ) -class RouteTargetType(NetBoxObjectType): +class RouteTargetType(PrimaryObjectType): tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None importing_l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]] @@ -245,7 +244,7 @@ class RouteTargetType(NetBoxObjectType): filters=ServiceFilter, pagination=True ) -class ServiceType(NetBoxObjectType, ContactsMixin): +class ServiceType(ContactsMixin, PrimaryObjectType): ports: List[int] ipaddresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]] @@ -264,7 +263,7 @@ def parent(self) -> Annotated[Union[ filters=ServiceTemplateFilter, pagination=True ) -class ServiceTemplateType(NetBoxObjectType): +class ServiceTemplateType(PrimaryObjectType): ports: List[int] @@ -274,7 +273,7 @@ class ServiceTemplateType(NetBoxObjectType): filters=VLANFilter, pagination=True ) -class VLANType(NetBoxObjectType): +class VLANType(PrimaryObjectType): 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 @@ -323,7 +322,7 @@ def scope(self) -> Annotated[Union[ filters=VLANTranslationPolicyFilter, pagination=True ) -class VLANTranslationPolicyType(NetBoxObjectType): +class VLANTranslationPolicyType(PrimaryObjectType): rules: List[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]] @@ -346,7 +345,7 @@ class VLANTranslationRuleType(NetBoxObjectType): filters=VRFFilter, pagination=True ) -class VRFType(NetBoxObjectType): +class VRFType(PrimaryObjectType): tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index bdc38b34977..df569390df2 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -6,13 +6,16 @@ from core.graphql.mixins import ChangelogMixin from core.models import ObjectType as ObjectType_ from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin +from users.graphql.mixins import OwnerMixin __all__ = ( 'BaseObjectType', 'ContentTypeType', + 'NestedGroupObjectType', + 'NetBoxObjectType', 'ObjectType', 'OrganizationalObjectType', - 'NetBoxObjectType', + 'PrimaryObjectType', ) @@ -53,31 +56,58 @@ class ObjectType( pass +class PrimaryObjectType( + ChangelogMixin, + CustomFieldsMixin, + JournalEntriesMixin, + TagsMixin, + OwnerMixin, + BaseObjectType +): + """ + Base GraphQL type for models which inherit from PrimaryModel. + """ + pass + + class OrganizationalObjectType( ChangelogMixin, CustomFieldsMixin, + JournalEntriesMixin, TagsMixin, + OwnerMixin, BaseObjectType ): """ - Base type for organizational models + Base GraphQL type for models which inherit from OrganizationalModel. """ pass -class NetBoxObjectType( +class NestedGroupObjectType( ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin, + OwnerMixin, BaseObjectType ): """ - GraphQL type for most NetBox models. Includes support for custom fields, change logging, journaling, and tags. + Base GraphQL type for models which inherit from NestedGroupModel. """ pass +class NetBoxObjectType( + ChangelogMixin, + CustomFieldsMixin, + JournalEntriesMixin, + TagsMixin, + BaseObjectType +): + pass + + # # Miscellaneous types # diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index a3713da930b..89d2bb971fd 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -4,7 +4,7 @@ import strawberry_django from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin -from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType +from netbox.graphql.types import BaseObjectType, NestedGroupObjectType, OrganizationalObjectType, PrimaryObjectType from tenancy import models from .filters import * from .mixins import ContactAssignmentsMixin @@ -57,7 +57,7 @@ filters=TenantFilter, pagination=True ) -class TenantType(ContactsMixin, NetBoxObjectType): +class TenantType(ContactsMixin, PrimaryObjectType): group: Annotated['TenantGroupType', strawberry.lazy('tenancy.graphql.types')] | None asns: List[Annotated['ASNType', strawberry.lazy('ipam.graphql.types')]] circuits: List[Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]] @@ -91,7 +91,7 @@ class TenantType(ContactsMixin, NetBoxObjectType): filters=TenantGroupFilter, pagination=True ) -class TenantGroupType(OrganizationalObjectType): +class TenantGroupType(NestedGroupObjectType): parent: Annotated['TenantGroupType', strawberry.lazy('tenancy.graphql.types')] | None tenants: List[TenantType] @@ -108,7 +108,7 @@ class TenantGroupType(OrganizationalObjectType): filters=ContactFilter, pagination=True ) -class ContactType(ContactAssignmentsMixin, NetBoxObjectType): +class ContactType(ContactAssignmentsMixin, PrimaryObjectType): groups: List[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]] @@ -128,7 +128,7 @@ class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): filters=ContactGroupFilter, pagination=True ) -class ContactGroupType(OrganizationalObjectType): +class ContactGroupType(NestedGroupObjectType): parent: Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')] | None contacts: List[ContactType] diff --git a/netbox/users/graphql/mixins.py b/netbox/users/graphql/mixins.py new file mode 100644 index 00000000000..f185eba6682 --- /dev/null +++ b/netbox/users/graphql/mixins.py @@ -0,0 +1,15 @@ +from typing import Annotated, TYPE_CHECKING + +import strawberry + +if TYPE_CHECKING: + from users.graphql.types import OwnerType + +__all__ = ( + 'OwnerMixin', +) + + +@strawberry.type +class OwnerMixin: + owner: Annotated['OwnerType', strawberry.lazy('users.graphql.types')] | None diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index ba20e684483..b1a47a52ac6 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -6,7 +6,8 @@ from extras.graphql.mixins import ConfigContextMixin, ContactsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt -from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType +from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType +from users.graphql.mixins import OwnerMixin from virtualization import models from .filters import * @@ -36,7 +37,7 @@ @strawberry.type -class ComponentType(NetBoxObjectType): +class ComponentType(OwnerMixin, NetBoxObjectType): """ Base type for device/VM components """ @@ -49,7 +50,7 @@ class ComponentType(NetBoxObjectType): filters=ClusterFilter, pagination=True ) -class ClusterType(ContactsMixin, VLANGroupsMixin, NetBoxObjectType): +class ClusterType(ContactsMixin, VLANGroupsMixin, PrimaryObjectType): 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 @@ -94,7 +95,7 @@ class ClusterTypeType(OrganizationalObjectType): filters=VirtualMachineFilter, pagination=True ) -class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType): +class VirtualMachineType(ConfigContextMixin, ContactsMixin, PrimaryObjectType): interface_count: BigInt virtual_disk_count: BigInt interface_count: BigInt diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index e1b46f9c4e5..9028805118e 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -4,7 +4,7 @@ import strawberry_django from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin -from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType +from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType, PrimaryObjectType from vpn import models from .filters import * @@ -58,7 +58,7 @@ class TunnelTerminationType(CustomFieldsMixin, TagsMixin, ObjectType): filters=TunnelFilter, pagination=True ) -class TunnelType(ContactsMixin, NetBoxObjectType): +class TunnelType(ContactsMixin, PrimaryObjectType): 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 @@ -72,8 +72,7 @@ class TunnelType(ContactsMixin, NetBoxObjectType): filters=IKEProposalFilter, pagination=True ) -class IKEProposalType(OrganizationalObjectType): - +class IKEProposalType(PrimaryObjectType): ike_policies: List[Annotated["IKEPolicyType", strawberry.lazy('vpn.graphql.types')]] @@ -83,8 +82,7 @@ class IKEProposalType(OrganizationalObjectType): filters=IKEPolicyFilter, pagination=True ) -class IKEPolicyType(OrganizationalObjectType): - +class IKEPolicyType(PrimaryObjectType): proposals: List[Annotated["IKEProposalType", strawberry.lazy('vpn.graphql.types')]] ipsec_profiles: List[Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')]] @@ -95,8 +93,7 @@ class IKEPolicyType(OrganizationalObjectType): filters=IPSecProposalFilter, pagination=True ) -class IPSecProposalType(OrganizationalObjectType): - +class IPSecProposalType(PrimaryObjectType): ipsec_policies: List[Annotated["IPSecPolicyType", strawberry.lazy('vpn.graphql.types')]] @@ -106,8 +103,7 @@ class IPSecProposalType(OrganizationalObjectType): filters=IPSecPolicyFilter, pagination=True ) -class IPSecPolicyType(OrganizationalObjectType): - +class IPSecPolicyType(PrimaryObjectType): proposals: List[Annotated["IPSecProposalType", strawberry.lazy('vpn.graphql.types')]] ipsec_profiles: List[Annotated["IPSecProfileType", strawberry.lazy('vpn.graphql.types')]] @@ -118,7 +114,7 @@ class IPSecPolicyType(OrganizationalObjectType): filters=IPSecProfileFilter, pagination=True ) -class IPSecProfileType(OrganizationalObjectType): +class IPSecProfileType(PrimaryObjectType): ike_policy: Annotated["IKEPolicyType", strawberry.lazy('vpn.graphql.types')] ipsec_policy: Annotated["IPSecPolicyType", strawberry.lazy('vpn.graphql.types')] @@ -131,7 +127,7 @@ class IPSecProfileType(OrganizationalObjectType): filters=L2VPNFilter, pagination=True ) -class L2VPNType(ContactsMixin, NetBoxObjectType): +class L2VPNType(ContactsMixin, PrimaryObjectType): tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None export_targets: List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]] diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index eeca6a82b75..124056b91cd 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -3,7 +3,7 @@ import strawberry import strawberry_django -from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType +from netbox.graphql.types import NestedGroupObjectType, PrimaryObjectType from wireless import models from .filters import * @@ -25,7 +25,7 @@ filters=WirelessLANGroupFilter, pagination=True ) -class WirelessLANGroupType(OrganizationalObjectType): +class WirelessLANGroupType(NestedGroupObjectType): parent: Annotated["WirelessLANGroupType", strawberry.lazy('wireless.graphql.types')] | None wireless_lans: List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]] @@ -38,7 +38,7 @@ class WirelessLANGroupType(OrganizationalObjectType): filters=WirelessLANFilter, pagination=True ) -class WirelessLANType(NetBoxObjectType): +class WirelessLANType(PrimaryObjectType): 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 @@ -61,7 +61,7 @@ def scope(self) -> Annotated[Union[ filters=WirelessLinkFilter, pagination=True ) -class WirelessLinkType(NetBoxObjectType): +class WirelessLinkType(PrimaryObjectType): 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 From 6f1a84542cc2a78814d6b5cd81b5a5ddcfd4dc9b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 13:05:07 -0400 Subject: [PATCH 08/40] Add missing filters --- netbox/extras/filtersets.py | 20 ++++++++++---------- netbox/users/filtersets.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index c4e3502c08a..fd2476f20b8 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -62,7 +62,7 @@ def search(self, queryset, name, value): ) -class WebhookFilterSet(NetBoxModelFilterSet): +class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -91,7 +91,7 @@ def search(self, queryset, name, value): ) -class EventRuleFilterSet(NetBoxModelFilterSet): +class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -131,7 +131,7 @@ def filter_event_type(self, queryset, name, value): return queryset.filter(event_types__overlap=value) -class CustomFieldFilterSet(ChangeLoggedModelFilterSet): +class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -180,7 +180,7 @@ def search(self, queryset, name, value): ) -class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet): +class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -208,7 +208,7 @@ def filter_by_choice(self, queryset, name, value): return queryset.filter(extra_choices__overlap=value) -class CustomLinkFilterSet(ChangeLoggedModelFilterSet): +class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -238,7 +238,7 @@ def search(self, queryset, name, value): ) -class ExportTemplateFilterSet(ChangeLoggedModelFilterSet): +class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -276,7 +276,7 @@ def search(self, queryset, name, value): ) -class SavedFilterFilterSet(ChangeLoggedModelFilterSet): +class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -495,7 +495,7 @@ def search(self, queryset, name, value): return queryset.filter(comments__icontains=value) -class TagFilterSet(ChangeLoggedModelFilterSet): +class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -590,7 +590,7 @@ def search(self, queryset, name, value): ) -class ConfigContextProfileFilterSet(NetBoxModelFilterSet): +class ConfigContextProfileFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), @@ -789,7 +789,7 @@ def search(self, queryset, name, value): ) -class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet): +class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 8d9abd3dc22..f94681443e7 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -28,6 +28,17 @@ class GroupFilterSet(BaseFilterSet): queryset=User.objects.all(), label=_('User (ID)'), ) + owner_id = django_filters.ModelMultipleChoiceFilter( + field_name='owner', + queryset=Owner.objects.all(), + label=_('Owner (ID)'), + ) + owner = django_filters.ModelMultipleChoiceFilter( + field_name='owner__name', + queryset=Owner.objects.all(), + to_field_name='name', + label=_('Owner (name)'), + ) permission_id = django_filters.ModelMultipleChoiceFilter( field_name='object_permissions', queryset=ObjectPermission.objects.all(), @@ -68,6 +79,17 @@ class UserFilterSet(BaseFilterSet): to_field_name='name', label=_('Group (name)'), ) + owner_id = django_filters.ModelMultipleChoiceFilter( + field_name='owner', + queryset=Owner.objects.all(), + label=_('Owner (ID)'), + ) + owner = django_filters.ModelMultipleChoiceFilter( + field_name='owner__name', + queryset=Owner.objects.all(), + to_field_name='name', + label=_('Owner (name)'), + ) permission_id = django_filters.ModelMultipleChoiceFilter( field_name='object_permissions', queryset=ObjectPermission.objects.all(), From 67469134a2b4cb1e0fb99cac9a302fd2c2665e11 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 13:50:22 -0400 Subject: [PATCH 09/40] ComponentType should inherit from PrimaryObjectType --- netbox/dcim/graphql/types.py | 17 ++--------------- netbox/virtualization/graphql/types.py | 5 ++--- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 568772247d6..81aedcf1e5a 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -5,19 +5,12 @@ from core.graphql.mixins import ChangelogMixin from dcim import models -from extras.graphql.mixins import ( - ConfigContextMixin, - ContactsMixin, - CustomFieldsMixin, - ImageAttachmentsMixin, - TagsMixin, -) +from extras.graphql.mixins import ConfigContextMixin, ContactsMixin, ImageAttachmentsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt from netbox.graphql.types import ( BaseObjectType, NestedGroupObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType, ) -from users.graphql.mixins import OwnerMixin from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -94,13 +87,7 @@ @strawberry.type -class ComponentType( - ChangelogMixin, - CustomFieldsMixin, - OwnerMixin, - TagsMixin, - BaseObjectType -): +class ComponentType(PrimaryObjectType): """ Base type for device/VM components """ diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index b1a47a52ac6..870fab9e288 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -6,8 +6,7 @@ from extras.graphql.mixins import ConfigContextMixin, ContactsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt -from netbox.graphql.types import NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType -from users.graphql.mixins import OwnerMixin +from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType from virtualization import models from .filters import * @@ -37,7 +36,7 @@ @strawberry.type -class ComponentType(OwnerMixin, NetBoxObjectType): +class ComponentType(PrimaryObjectType): """ Base type for device/VM components """ From 3a212cc19261a62494d875be0e4c7b1c2eced5d9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 13:53:20 -0400 Subject: [PATCH 10/40] Misc fixes --- netbox/users/tables.py | 2 +- netbox/users/views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 17460dc77af..233db59db98 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -157,7 +157,7 @@ class OwnerTable(NetBoxTable): ) users = columns.ManyToManyColumn( verbose_name=_('Groups'), - linkify_item=('users:group', {'pk': tables.A('pk')}) + linkify_item=('users:user', {'pk': tables.A('pk')}) ) actions = columns.ActionsColumn( actions=('edit', 'delete'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 60d1cdfc137..aa0efdd72a0 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -280,7 +280,6 @@ class OwnerBulkEditView(generic.BulkEditView): @register_model_view(Owner, 'bulk_rename', path='rename', detail=False) class OwnerBulkRenameView(generic.BulkRenameView): queryset = Owner.objects.all() - field_name = 'ownername' @register_model_view(Owner, 'bulk_delete', path='delete', detail=False) From 1fdfff6be20739f480c6de1cf86e1819b0f2828d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 14:02:58 -0400 Subject: [PATCH 11/40] Split base form classes into separate modules under netbox.forms --- netbox/extras/forms/filtersets.py | 2 +- netbox/netbox/forms/__init__.py | 62 +-------- netbox/netbox/forms/base.py | 202 ----------------------------- netbox/netbox/forms/bulk_edit.py | 67 ++++++++++ netbox/netbox/forms/bulk_import.py | 40 ++++++ netbox/netbox/forms/filtersets.py | 46 +++++++ netbox/netbox/forms/model_forms.py | 76 +++++++++++ netbox/netbox/forms/search.py | 55 ++++++++ 8 files changed, 290 insertions(+), 260 deletions(-) delete mode 100644 netbox/netbox/forms/base.py create mode 100644 netbox/netbox/forms/bulk_edit.py create mode 100644 netbox/netbox/forms/bulk_import.py create mode 100644 netbox/netbox/forms/filtersets.py create mode 100644 netbox/netbox/forms/model_forms.py create mode 100644 netbox/netbox/forms/search.py diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 759c1fc4b45..c542b64042b 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -6,7 +6,7 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms.base import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from tenancy.models import Tenant, TenantGroup from users.models import Group, User diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index f88fb18bc92..fa06fafa0f8 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,57 +1,5 @@ -import re - -from django import forms -from django.utils.translation import gettext_lazy as _ - -from netbox.search import LookupTypes -from netbox.search.backends import search_backend - -from .base import * - -LOOKUP_CHOICES = ( - ('', _('Partial match')), - (LookupTypes.EXACT, _('Exact match')), - (LookupTypes.STARTSWITH, _('Starts with')), - (LookupTypes.ENDSWITH, _('Ends with')), - (LookupTypes.REGEX, _('Regex')), -) - - -class SearchForm(forms.Form): - q = forms.CharField( - label=_('Search'), - widget=forms.TextInput( - attrs={ - 'hx-get': '', - 'hx-target': '#object_list', - 'hx-trigger': 'keyup[target.value.length >= 3] changed delay:500ms', - } - ) - ) - obj_types = forms.MultipleChoiceField( - choices=[], - required=False, - label=_('Object type(s)') - ) - lookup = forms.ChoiceField( - choices=LOOKUP_CHOICES, - initial=LookupTypes.PARTIAL, - required=False, - label=_('Lookup') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields['obj_types'].choices = search_backend.get_object_types() - - def clean(self): - - # Validate regular expressions - if self.cleaned_data['lookup'] == LookupTypes.REGEX: - try: - re.compile(self.cleaned_data['q']) - except re.error as e: - raise forms.ValidationError({ - 'q': f'Invalid regular expression: {e}' - }) +from .model_forms import * +from .bulk_import import * +from .bulk_edit import * +from .filtersets import * +from .search import * diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py deleted file mode 100644 index 3c7cf03483d..00000000000 --- a/netbox/netbox/forms/base.py +++ /dev/null @@ -1,202 +0,0 @@ -import json - -from django import forms -from django.contrib.contenttypes.models import ContentType -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ - -from core.models import ObjectType -from extras.choices import * -from extras.models import CustomField, Tag -from users.models import Owner -from utilities.forms import BulkEditForm, CSVModelForm -from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField, CSVModelChoiceField -from utilities.forms.mixins import CheckLastUpdatedMixin -from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, SavedFiltersMixin, TagsMixin - -__all__ = ( - 'NetBoxModelForm', - 'NetBoxModelImportForm', - 'NetBoxModelBulkEditForm', - 'NetBoxModelFilterSetForm', -) - - -class NetBoxModelForm( - ChangelogMessageMixin, - CheckLastUpdatedMixin, - CustomFieldsMixin, - OwnerMixin, - TagsMixin, - forms.ModelForm -): - """ - Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. - - Attributes: - 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 = () - - def _get_content_type(self): - return ContentType.objects.get_for_model(self._meta.model) - - def _get_form_field(self, customfield): - if self.instance.pk: - form_field = customfield.to_form_field(set_initial=False) - initial = self.instance.custom_field_data.get(customfield.name) - if customfield.type == CustomFieldTypeChoices.TYPE_JSON: - form_field.initial = json.dumps(initial) - else: - form_field.initial = initial - return form_field - - return customfield.to_form_field() - - def clean(self): - - # Save custom field data on instance - for cf_name, customfield in self.custom_fields.items(): - if cf_name not in self.fields: - # Custom fields may be absent when performing bulk updates via import - continue - key = cf_name[3:] # Strip "cf_" from field name - value = self.cleaned_data.get(cf_name) - - # Convert "empty" values to null - if value in self.fields[cf_name].empty_values: - self.instance.custom_field_data[key] = None - else: - if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str: - value = json.loads(value) - self.instance.custom_field_data[key] = customfield.serialize(value) - - return super().clean() - - def _post_clean(self): - """ - Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance. - """ - self.instance._m2m_values = {} - for field in self.instance._meta.local_many_to_many: - if field.name in self.cleaned_data: - self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name]) - - return super()._post_clean() - - -class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): - """ - Base form for creating NetBox objects from CSV data. Used for bulk importing. - """ - owner = CSVModelChoiceField( - queryset=Owner.objects.all(), - required=False, - to_field_name='name', - help_text=_("Name of the object's owner") - ) - tags = CSVModelMultipleChoiceField( - label=_('Tags'), - queryset=Tag.objects.all(), - required=False, - to_field_name='slug', - help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")') - ) - - def _get_custom_fields(self, content_type): - return CustomField.objects.filter( - object_types=content_type, - ui_editable=CustomFieldUIEditableChoices.YES - ) - - def _get_form_field(self, customfield): - return customfield.to_form_field(for_csv_import=True) - - -class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, BulkEditForm): - """ - Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom - fields and adding/removing tags. - - Attributes: - fieldsets: An iterable of two-tuples which define a heading and field set to display per section of - the rendered form (optional). If not defined, the all fields will be rendered as a single section. - """ - fieldsets = None - - pk = forms.ModelMultipleChoiceField( - queryset=None, # Set from self.model on init - widget=forms.MultipleHiddenInput - ) - add_tags = DynamicModelMultipleChoiceField( - label=_('Add tags'), - queryset=Tag.objects.all(), - required=False - ) - remove_tags = DynamicModelMultipleChoiceField( - label=_('Remove tags'), - queryset=Tag.objects.all(), - required=False - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields['pk'].queryset = self.model.objects.all() - - # Restrict tag fields by model - object_type = ObjectType.objects.get_for_model(self.model) - self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk) - self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk) - - self._extend_nullable_fields() - - def _get_form_field(self, customfield): - return customfield.to_form_field(set_initial=False, enforce_required=False) - - def _extend_nullable_fields(self): - nullable_common_fields = ['owner'] - nullable_custom_fields = [ - name for name, customfield in self.custom_fields.items() - if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES) - ] - self.nullable_fields = ( - *self.nullable_fields, - *nullable_common_fields, - *nullable_custom_fields, - ) - - -class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form): - """ - Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the - corresponding FilterSet *must* provide a `q` filter. - - Attributes: - model: The model class associated with the form - fieldsets: An iterable of two-tuples which define a heading and field set to display per section of - the rendered form (optional). If not defined, the all fields will be rendered as a single section. - selector_fields: An iterable of names of fields to display by default when rendering the form as - a selector widget - """ - q = forms.CharField( - required=False, - label=_('Search') - ) - owner_id = DynamicModelMultipleChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) - - selector_fields = ('filter_id', 'q') - - def _get_custom_fields(self, content_type): - return super()._get_custom_fields(content_type).exclude( - Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | - Q(type=CustomFieldTypeChoices.TYPE_JSON) - ) - - def _get_form_field(self, customfield): - return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False) diff --git a/netbox/netbox/forms/bulk_edit.py b/netbox/netbox/forms/bulk_edit.py new file mode 100644 index 00000000000..e0d3ad348e0 --- /dev/null +++ b/netbox/netbox/forms/bulk_edit.py @@ -0,0 +1,67 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from core.models import ObjectType +from extras.choices import * +from extras.models import Tag +from utilities.forms import BulkEditForm +from utilities.forms.fields import DynamicModelMultipleChoiceField +from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin + +__all__ = ( + 'NetBoxModelBulkEditForm', +) + + +class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, BulkEditForm): + """ + Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom + fields and adding/removing tags. + + Attributes: + fieldsets: An iterable of two-tuples which define a heading and field set to display per section of + the rendered form (optional). If not defined, the all fields will be rendered as a single section. + """ + fieldsets = None + + pk = forms.ModelMultipleChoiceField( + queryset=None, # Set from self.model on init + widget=forms.MultipleHiddenInput + ) + add_tags = DynamicModelMultipleChoiceField( + label=_('Add tags'), + queryset=Tag.objects.all(), + required=False + ) + remove_tags = DynamicModelMultipleChoiceField( + label=_('Remove tags'), + queryset=Tag.objects.all(), + required=False + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['pk'].queryset = self.model.objects.all() + + # Restrict tag fields by model + object_type = ObjectType.objects.get_for_model(self.model) + self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk) + self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk) + + self._extend_nullable_fields() + + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False) + + def _extend_nullable_fields(self): + nullable_common_fields = ['owner'] + nullable_custom_fields = [ + name for name, customfield in self.custom_fields.items() + if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES) + ] + self.nullable_fields = ( + *self.nullable_fields, + *nullable_common_fields, + *nullable_custom_fields, + ) diff --git a/netbox/netbox/forms/bulk_import.py b/netbox/netbox/forms/bulk_import.py new file mode 100644 index 00000000000..3504844dee3 --- /dev/null +++ b/netbox/netbox/forms/bulk_import.py @@ -0,0 +1,40 @@ +from django.utils.translation import gettext_lazy as _ + +from extras.choices import * +from extras.models import CustomField, Tag +from users.models import Owner +from utilities.forms import CSVModelForm +from utilities.forms.fields import CSVModelMultipleChoiceField, CSVModelChoiceField +from .model_forms import NetBoxModelForm + +__all__ = ( + 'NetBoxModelImportForm', +) + + +class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): + """ + Base form for creating NetBox objects from CSV data. Used for bulk importing. + """ + owner = CSVModelChoiceField( + queryset=Owner.objects.all(), + required=False, + to_field_name='name', + help_text=_("Name of the object's owner") + ) + tags = CSVModelMultipleChoiceField( + label=_('Tags'), + queryset=Tag.objects.all(), + required=False, + to_field_name='slug', + help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")') + ) + + def _get_custom_fields(self, content_type): + return CustomField.objects.filter( + object_types=content_type, + ui_editable=CustomFieldUIEditableChoices.YES + ) + + def _get_form_field(self, customfield): + return customfield.to_form_field(for_csv_import=True) diff --git a/netbox/netbox/forms/filtersets.py b/netbox/netbox/forms/filtersets.py new file mode 100644 index 00000000000..fb4e496c845 --- /dev/null +++ b/netbox/netbox/forms/filtersets.py @@ -0,0 +1,46 @@ +from django import forms +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ + +from extras.choices import * +from users.models import Owner +from utilities.forms.fields import DynamicModelMultipleChoiceField +from .mixins import CustomFieldsMixin, SavedFiltersMixin + +__all__ = ( + 'NetBoxModelFilterSetForm', +) + + +class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form): + """ + Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the + corresponding FilterSet *must* provide a `q` filter. + + Attributes: + model: The model class associated with the form + fieldsets: An iterable of two-tuples which define a heading and field set to display per section of + the rendered form (optional). If not defined, the all fields will be rendered as a single section. + selector_fields: An iterable of names of fields to display by default when rendering the form as + a selector widget + """ + q = forms.CharField( + required=False, + label=_('Search') + ) + owner_id = DynamicModelMultipleChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) + + selector_fields = ('filter_id', 'q') + + def _get_custom_fields(self, content_type): + return super()._get_custom_fields(content_type).exclude( + Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | + Q(type=CustomFieldTypeChoices.TYPE_JSON) + ) + + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False) diff --git a/netbox/netbox/forms/model_forms.py b/netbox/netbox/forms/model_forms.py new file mode 100644 index 00000000000..6766abc6de0 --- /dev/null +++ b/netbox/netbox/forms/model_forms.py @@ -0,0 +1,76 @@ +import json + +from django import forms +from django.contrib.contenttypes.models import ContentType + +from extras.choices import * +from utilities.forms.mixins import CheckLastUpdatedMixin +from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, TagsMixin + +__all__ = ( + 'NetBoxModelForm', +) + + +class NetBoxModelForm( + ChangelogMessageMixin, + CheckLastUpdatedMixin, + CustomFieldsMixin, + OwnerMixin, + TagsMixin, + forms.ModelForm +): + """ + Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. + + Attributes: + 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 = () + + def _get_content_type(self): + return ContentType.objects.get_for_model(self._meta.model) + + def _get_form_field(self, customfield): + if self.instance.pk: + form_field = customfield.to_form_field(set_initial=False) + initial = self.instance.custom_field_data.get(customfield.name) + if customfield.type == CustomFieldTypeChoices.TYPE_JSON: + form_field.initial = json.dumps(initial) + else: + form_field.initial = initial + return form_field + + return customfield.to_form_field() + + def clean(self): + + # Save custom field data on instance + for cf_name, customfield in self.custom_fields.items(): + if cf_name not in self.fields: + # Custom fields may be absent when performing bulk updates via import + continue + key = cf_name[3:] # Strip "cf_" from field name + value = self.cleaned_data.get(cf_name) + + # Convert "empty" values to null + if value in self.fields[cf_name].empty_values: + self.instance.custom_field_data[key] = None + else: + if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str: + value = json.loads(value) + self.instance.custom_field_data[key] = customfield.serialize(value) + + return super().clean() + + def _post_clean(self): + """ + Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance. + """ + self.instance._m2m_values = {} + for field in self.instance._meta.local_many_to_many: + if field.name in self.cleaned_data: + self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name]) + + return super()._post_clean() diff --git a/netbox/netbox/forms/search.py b/netbox/netbox/forms/search.py new file mode 100644 index 00000000000..855c8e27300 --- /dev/null +++ b/netbox/netbox/forms/search.py @@ -0,0 +1,55 @@ +import re + +from django import forms +from django.utils.translation import gettext_lazy as _ + +from netbox.search import LookupTypes +from netbox.search.backends import search_backend + +LOOKUP_CHOICES = ( + ('', _('Partial match')), + (LookupTypes.EXACT, _('Exact match')), + (LookupTypes.STARTSWITH, _('Starts with')), + (LookupTypes.ENDSWITH, _('Ends with')), + (LookupTypes.REGEX, _('Regex')), +) + + +class SearchForm(forms.Form): + q = forms.CharField( + label=_('Search'), + widget=forms.TextInput( + attrs={ + 'hx-get': '', + 'hx-target': '#object_list', + 'hx-trigger': 'keyup[target.value.length >= 3] changed delay:500ms', + } + ) + ) + obj_types = forms.MultipleChoiceField( + choices=[], + required=False, + label=_('Object type(s)') + ) + lookup = forms.ChoiceField( + choices=LOOKUP_CHOICES, + initial=LookupTypes.PARTIAL, + required=False, + label=_('Lookup') + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['obj_types'].choices = search_backend.get_object_types() + + def clean(self): + + # Validate regular expressions + if self.cleaned_data['lookup'] == LookupTypes.REGEX: + try: + re.compile(self.cleaned_data['q']) + except re.error as e: + raise forms.ValidationError({ + 'q': f'Invalid regular expression: {e}' + }) From e2163b9a3bf4e2b1b6459ec64443e27a88b7fbc2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 14:43:01 -0400 Subject: [PATCH 12/40] Add owner field to all applicable model forms --- netbox/circuits/forms/model_forms.py | 31 +++------ netbox/core/forms/model_forms.py | 7 +- netbox/dcim/forms/model_forms.py | 76 +++++++--------------- netbox/extras/forms/model_forms.py | 26 ++++---- netbox/ipam/forms/model_forms.py | 55 ++++++---------- netbox/netbox/forms/model_forms.py | 27 +++++++- netbox/tenancy/forms/model_forms.py | 22 ++----- netbox/virtualization/forms/model_forms.py | 20 ++---- netbox/vpn/forms/model_forms.py | 25 +++---- netbox/wireless/forms/model_forms.py | 16 ++--- 10 files changed, 124 insertions(+), 181 deletions(-) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 1b6c3feb4c1..4e8f54773fe 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -10,11 +10,11 @@ from circuits.models import * from dcim.models import Interface, Site from ipam.models import ASN -from netbox.forms import NetBoxModelForm +from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from tenancy.forms import TenancyForm from utilities.forms import get_field_value from utilities.forms.fields import ( - CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, + ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, ) from utilities.forms.mixins import DistanceValidationMixin from utilities.forms.rendering import FieldSet, InlineFields @@ -36,14 +36,13 @@ ) -class ProviderForm(NetBoxModelForm): +class ProviderForm(PrimaryModelForm): slug = SlugField() asns = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), label=_('ASNs'), required=False ) - comments = CommentField() fieldsets = ( FieldSet('name', 'slug', 'asns', 'description', 'tags'), @@ -56,14 +55,13 @@ class Meta: ] -class ProviderAccountForm(NetBoxModelForm): +class ProviderAccountForm(PrimaryModelForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), selector=True, quick_add=True ) - comments = CommentField() class Meta: model = ProviderAccount @@ -72,14 +70,13 @@ class Meta: ] -class ProviderNetworkForm(NetBoxModelForm): +class ProviderNetworkForm(PrimaryModelForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), selector=True, quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet('provider', 'name', 'service_id', 'description', 'tags'), @@ -92,9 +89,7 @@ class Meta: ] -class CircuitTypeForm(NetBoxModelForm): - slug = SlugField() - +class CircuitTypeForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'), ) @@ -106,7 +101,7 @@ class Meta: ] -class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): +class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -125,7 +120,6 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): queryset=CircuitType.objects.all(), quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet( @@ -233,9 +227,7 @@ def clean(self): self.instance.termination = self.cleaned_data.get('termination') -class CircuitGroupForm(TenancyForm, NetBoxModelForm): - slug = SlugField() - +class CircuitGroupForm(TenancyForm, OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), @@ -307,9 +299,7 @@ def clean(self): self.instance.member = self.cleaned_data.get('member') -class VirtualCircuitTypeForm(NetBoxModelForm): - slug = SlugField() - +class VirtualCircuitTypeForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'color', 'description', 'tags'), ) @@ -321,7 +311,7 @@ class Meta: ] -class VirtualCircuitForm(TenancyForm, NetBoxModelForm): +class VirtualCircuitForm(TenancyForm, PrimaryModelForm): provider_network = DynamicModelChoiceField( label=_('Provider network'), queryset=ProviderNetwork.objects.all(), @@ -336,7 +326,6 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm): queryset=VirtualCircuitType.objects.all(), quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet( diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 6796b4f8599..7437c25d669 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -9,11 +9,11 @@ from core.forms.mixins import SyncedDataMixin from core.models import * from netbox.config import get_config, PARAMS -from netbox.forms import NetBoxModelForm +from netbox.forms import NetBoxModelForm, PrimaryModelForm 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, JSONField +from utilities.forms.fields import JSONField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect @@ -26,12 +26,11 @@ EMPTY_VALUES = ('', None, [], ()) -class DataSourceForm(NetBoxModelForm): +class DataSourceForm(PrimaryModelForm): type = forms.ChoiceField( choices=get_data_backend_choices, widget=HTMXSelect() ) - comments = CommentField() class Meta: model = DataSource diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 4e3037cfb46..a7af27e1d41 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -10,13 +10,13 @@ from extras.models import ConfigTemplate from ipam.choices import VLANQinQRoleChoices from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF -from netbox.forms import NetBoxModelForm +from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from netbox.forms.mixins import ChangelogMessageMixin from tenancy.forms import TenancyForm from users.models import User from utilities.forms import add_blank_choice, get_field_value from utilities.forms.fields import ( - CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, + DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK @@ -75,14 +75,12 @@ ) -class RegionForm(NetBoxModelForm): +class RegionForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=Region.objects.all(), required=False ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags'), @@ -95,14 +93,12 @@ class Meta: ) -class SiteGroupForm(NetBoxModelForm): +class SiteGroupForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=SiteGroup.objects.all(), required=False ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags'), @@ -115,7 +111,7 @@ class Meta: ) -class SiteForm(TenancyForm, NetBoxModelForm): +class SiteForm(TenancyForm, PrimaryModelForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), @@ -139,7 +135,6 @@ class SiteForm(TenancyForm, NetBoxModelForm): choices=add_blank_choice(TimeZoneFormField().choices), required=False ) - comments = CommentField() fieldsets = ( FieldSet( @@ -170,7 +165,7 @@ class Meta: } -class LocationForm(TenancyForm, NetBoxModelForm): +class LocationForm(TenancyForm, NestedGroupModelForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -184,8 +179,6 @@ class LocationForm(TenancyForm, NetBoxModelForm): 'site_id': '$site' } ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')), @@ -200,9 +193,7 @@ class Meta: ) -class RackRoleForm(NetBoxModelForm): - slug = SlugField() - +class RackRoleForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')), ) @@ -214,13 +205,12 @@ class Meta: ] -class RackTypeForm(NetBoxModelForm): +class RackTypeForm(PrimaryModelForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), quick_add=True ) - comments = CommentField() slug = SlugField( label=_('Slug'), slug_source='model' @@ -246,7 +236,7 @@ class Meta: ] -class RackForm(TenancyForm, NetBoxModelForm): +class RackForm(TenancyForm, PrimaryModelForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -271,7 +261,6 @@ class RackForm(TenancyForm, NetBoxModelForm): required=False, help_text=_("Select a pre-defined rack type, or set physical characteristics below.") ) - comments = CommentField() fieldsets = ( FieldSet( @@ -333,7 +322,6 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): label=_('User'), queryset=User.objects.order_by('username') ) - comments = CommentField() fieldsets = ( FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')), @@ -347,9 +335,7 @@ class Meta: ] -class ManufacturerForm(NetBoxModelForm): - slug = SlugField() - +class ManufacturerForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')), ) @@ -361,7 +347,7 @@ class Meta: ] -class DeviceTypeForm(NetBoxModelForm): +class DeviceTypeForm(PrimaryModelForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -380,7 +366,6 @@ class DeviceTypeForm(NetBoxModelForm): label=_('Slug'), slug_source='model' ) - comments = CommentField() fieldsets = ( FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')), @@ -408,13 +393,12 @@ class Meta: } -class ModuleTypeProfileForm(NetBoxModelForm): +class ModuleTypeProfileForm(PrimaryModelForm): schema = JSONField( label=_('Schema'), required=False, help_text=_("Enter a valid JSON schema to define supported attributes.") ) - comments = CommentField() fieldsets = ( FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')), @@ -427,7 +411,7 @@ class Meta: ] -class ModuleTypeForm(NetBoxModelForm): +class ModuleTypeForm(PrimaryModelForm): profile = forms.ModelChoiceField( queryset=ModuleTypeProfile.objects.all(), label=_('Profile'), @@ -438,7 +422,6 @@ class ModuleTypeForm(NetBoxModelForm): label=_('Manufacturer'), queryset=Manufacturer.objects.all() ) - comments = CommentField() @property def fieldsets(self): @@ -507,19 +490,17 @@ def _post_clean(self): return super()._post_clean() -class DeviceRoleForm(NetBoxModelForm): +class DeviceRoleForm(NestedGroupModelForm): config_template = DynamicModelChoiceField( label=_('Config template'), queryset=ConfigTemplate.objects.all(), required=False ) - slug = SlugField() parent = DynamicModelChoiceField( label=_('Parent'), queryset=DeviceRole.objects.all(), required=False, ) - comments = CommentField() fieldsets = ( FieldSet( @@ -535,7 +516,7 @@ class Meta: ] -class PlatformForm(NetBoxModelForm): +class PlatformForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=Platform.objects.all(), @@ -556,7 +537,6 @@ class PlatformForm(NetBoxModelForm): label=_('Slug'), max_length=64 ) - comments = CommentField() fieldsets = ( FieldSet( @@ -571,7 +551,7 @@ class Meta: ] -class DeviceForm(TenancyForm, NetBoxModelForm): +class DeviceForm(TenancyForm, PrimaryModelForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -641,7 +621,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm): 'site_id': ['$site', 'null'] }, ) - comments = CommentField() local_context_data = JSONField( required=False, label='' @@ -742,7 +721,7 @@ def __init__(self, *args, **kwargs): self.fields['position'].widget.choices = [(position, f'U{position}')] -class ModuleForm(ModuleCommonForm, NetBoxModelForm): +class ModuleForm(ModuleCommonForm, PrimaryModelForm): device = DynamicModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -765,7 +744,6 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm): }, selector=True ) - comments = CommentField() replicate_components = forms.BooleanField( label=_('Replicate components'), required=False, @@ -809,7 +787,7 @@ def get_termination_type_choices(): ]) -class CableForm(TenancyForm, NetBoxModelForm): +class CableForm(TenancyForm, PrimaryModelForm): a_terminations_type = forms.ChoiceField( choices=get_termination_type_choices, required=False, @@ -822,7 +800,6 @@ class CableForm(TenancyForm, NetBoxModelForm): widget=HTMXSelect(), label=_('Type') ) - comments = CommentField() class Meta: model = Cable @@ -832,7 +809,7 @@ class Meta: ] -class PowerPanelForm(NetBoxModelForm): +class PowerPanelForm(PrimaryModelForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -846,7 +823,6 @@ class PowerPanelForm(NetBoxModelForm): 'site_id': '$site' } ) - comments = CommentField() fieldsets = ( FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')), @@ -859,7 +835,7 @@ class Meta: ] -class PowerFeedForm(TenancyForm, NetBoxModelForm): +class PowerFeedForm(TenancyForm, PrimaryModelForm): power_panel = DynamicModelChoiceField( label=_('Power panel'), queryset=PowerPanel.objects.all(), @@ -872,7 +848,6 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm): required=False, selector=True ) - comments = CommentField() fieldsets = ( FieldSet( @@ -895,13 +870,12 @@ class Meta: # Virtual chassis # -class VirtualChassisForm(NetBoxModelForm): +class VirtualChassisForm(PrimaryModelForm): master = forms.ModelChoiceField( label=_('Master'), queryset=Device.objects.all(), required=False, ) - comments = CommentField() class Meta: model = VirtualChassis @@ -1829,9 +1803,7 @@ def clean(self): self.instance.component = None -class InventoryItemRoleForm(NetBoxModelForm): - slug = SlugField() - +class InventoryItemRoleForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')), ) @@ -1843,7 +1815,7 @@ class Meta: ] -class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): +class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm): device = DynamicModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1888,7 +1860,7 @@ class Meta: # Addressing # -class MACAddressForm(NetBoxModelForm): +class MACAddressForm(PrimaryModelForm): mac_address = forms.CharField( required=True, label=_('MAC address') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 39f02f5b1b2..0bce793f815 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -12,8 +12,8 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms import NetBoxModelForm -from netbox.forms.mixins import ChangelogMessageMixin +from netbox.forms import NetBoxModelForm, PrimaryModelForm +from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin from tenancy.models import Tenant, TenantGroup from users.models import Group, User from utilities.forms import get_field_value @@ -47,7 +47,7 @@ ) -class CustomFieldForm(ChangelogMessageMixin, forms.ModelForm): +class CustomFieldForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields'), @@ -166,7 +166,7 @@ def __init__(self, *args, **kwargs): del self.fields['choice_set'] -class CustomFieldChoiceSetForm(ChangelogMessageMixin, forms.ModelForm): +class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm): # TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model extra_choices = forms.CharField( widget=ChoicesWidget(), @@ -219,7 +219,7 @@ def clean_extra_choices(self): return data -class CustomLinkForm(ChangelogMessageMixin, forms.ModelForm): +class CustomLinkForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links') @@ -251,7 +251,7 @@ class Meta: } -class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm): +class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates') @@ -293,7 +293,7 @@ def clean(self): return self.cleaned_data -class SavedFilterForm(ChangelogMessageMixin, forms.ModelForm): +class SavedFilterForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( label=_('Object types'), @@ -427,7 +427,7 @@ class Meta: fields = ('object_type', 'object_id') -class WebhookForm(NetBoxModelForm): +class WebhookForm(OwnerMixin, NetBoxModelForm): fieldsets = ( FieldSet('name', 'description', 'tags', name=_('Webhook')), @@ -447,7 +447,7 @@ class Meta: } -class EventRuleForm(NetBoxModelForm): +class EventRuleForm(OwnerMixin, NetBoxModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), @@ -563,7 +563,7 @@ def clean(self): return self.cleaned_data -class TagForm(ChangelogMessageMixin, forms.ModelForm): +class TagForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( label=_('Object types'), @@ -586,7 +586,7 @@ class Meta: ] -class ConfigContextProfileForm(SyncedDataMixin, NetBoxModelForm): +class ConfigContextProfileForm(SyncedDataMixin, PrimaryModelForm): schema = JSONField( label=_('Schema'), required=False, @@ -611,7 +611,7 @@ class Meta: ) -class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm): +class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm): profile = DynamicModelChoiceField( label=_('Profile'), queryset=ConfigContextProfile.objects.all(), @@ -728,7 +728,7 @@ def clean(self): return self.cleaned_data -class ConfigTemplateForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm): +class ConfigTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm): tags = DynamicModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 1e2f23da683..f792d1befa8 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -9,13 +9,13 @@ from ipam.constants import * from ipam.formfields import IPNetworkFormField from ipam.models import * -from netbox.forms import NetBoxModelForm +from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from tenancy.forms import TenancyForm from utilities.exceptions import PermissionsViolation from utilities.forms import add_blank_choice from utilities.forms.fields import ( - CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, - NumericRangeArrayField, SlugField + ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, + NumericRangeArrayField, ) from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.utils import get_field_value @@ -49,7 +49,7 @@ ) -class VRFForm(TenancyForm, NetBoxModelForm): +class VRFForm(TenancyForm, PrimaryModelForm): import_targets = DynamicModelMultipleChoiceField( label=_('Import targets'), queryset=RouteTarget.objects.all(), @@ -60,7 +60,6 @@ class VRFForm(TenancyForm, NetBoxModelForm): queryset=RouteTarget.objects.all(), required=False ) - comments = CommentField() fieldsets = ( FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')), @@ -79,12 +78,11 @@ class Meta: } -class RouteTargetForm(TenancyForm, NetBoxModelForm): +class RouteTargetForm(TenancyForm, PrimaryModelForm): fieldsets = ( FieldSet('name', 'description', 'tags', name=_('Route Target')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) - comments = CommentField() class Meta: model = RouteTarget @@ -93,9 +91,7 @@ class Meta: ] -class RIRForm(NetBoxModelForm): - slug = SlugField() - +class RIRForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')), ) @@ -107,13 +103,12 @@ class Meta: ] -class AggregateForm(TenancyForm, NetBoxModelForm): +class AggregateForm(TenancyForm, PrimaryModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label=_('RIR'), quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')), @@ -130,13 +125,12 @@ class Meta: } -class ASNRangeForm(TenancyForm, NetBoxModelForm): +class ASNRangeForm(TenancyForm, OrganizationalModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label=_('RIR'), quick_add=True ) - slug = SlugField() fieldsets = ( FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), @@ -149,7 +143,7 @@ class Meta: ] -class ASNForm(TenancyForm, NetBoxModelForm): +class ASNForm(TenancyForm, PrimaryModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label=_('RIR'), @@ -160,7 +154,6 @@ class ASNForm(TenancyForm, NetBoxModelForm): label=_('Sites'), required=False ) - comments = CommentField() fieldsets = ( FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')), @@ -188,9 +181,7 @@ def save(self, *args, **kwargs): return instance -class RoleForm(NetBoxModelForm): - slug = SlugField() - +class RoleForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')), ) @@ -202,7 +193,7 @@ class Meta: ] -class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): +class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -223,7 +214,6 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): required=False, quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet( @@ -250,7 +240,7 @@ def __init__(self, *args, **kwargs): self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None) -class IPRangeForm(TenancyForm, NetBoxModelForm): +class IPRangeForm(TenancyForm, PrimaryModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -262,7 +252,6 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): required=False, quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet( @@ -280,7 +269,7 @@ class Meta: ] -class IPAddressForm(TenancyForm, NetBoxModelForm): +class IPAddressForm(TenancyForm, PrimaryModelForm): interface = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, @@ -324,7 +313,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): required=False, label=_('Make this the out-of-band IP for the device') ) - comments = CommentField() fieldsets = ( FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), @@ -494,7 +482,7 @@ class IPAddressAssignForm(forms.Form): ) -class FHRPGroupForm(NetBoxModelForm): +class FHRPGroupForm(PrimaryModelForm): # Optionally create a new IPAddress along with the FHRPGroup ip_vrf = DynamicModelChoiceField( @@ -511,7 +499,6 @@ class FHRPGroupForm(NetBoxModelForm): required=False, label=_('Status') ) - comments = CommentField() fieldsets = ( FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')), @@ -599,8 +586,7 @@ def clean_group(self): return group -class VLANGroupForm(TenancyForm, NetBoxModelForm): - slug = SlugField() +class VLANGroupForm(TenancyForm, OrganizationalModelForm): vid_ranges = NumericRangeArrayField( label=_('VLAN IDs') ) @@ -662,7 +648,7 @@ def clean(self): self.instance.scope = self.cleaned_data.get('scope') -class VLANForm(TenancyForm, NetBoxModelForm): +class VLANForm(TenancyForm, PrimaryModelForm): group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, @@ -698,7 +684,6 @@ class VLANForm(TenancyForm, NetBoxModelForm): 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE, } ) - comments = CommentField() class Meta: model = VLAN @@ -708,7 +693,7 @@ class Meta: ] -class VLANTranslationPolicyForm(NetBoxModelForm): +class VLANTranslationPolicyForm(PrimaryModelForm): fieldsets = ( FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')), @@ -739,7 +724,7 @@ class Meta: ] -class ServiceTemplateForm(NetBoxModelForm): +class ServiceTemplateForm(PrimaryModelForm): ports = NumericArrayField( label=_('Ports'), base_field=forms.IntegerField( @@ -748,7 +733,6 @@ class ServiceTemplateForm(NetBoxModelForm): ), help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.") ) - comments = CommentField() fieldsets = ( FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Application Service Template')), @@ -759,7 +743,7 @@ class Meta: fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags') -class ServiceForm(NetBoxModelForm): +class ServiceForm(PrimaryModelForm): parent_object_type = ContentTypeChoiceField( queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS), widget=HTMXSelect(), @@ -786,7 +770,6 @@ class ServiceForm(NetBoxModelForm): required=False, label=_('IP Addresses'), ) - comments = CommentField() fieldsets = ( FieldSet( diff --git a/netbox/netbox/forms/model_forms.py b/netbox/netbox/forms/model_forms.py index 6766abc6de0..c76dbd77b38 100644 --- a/netbox/netbox/forms/model_forms.py +++ b/netbox/netbox/forms/model_forms.py @@ -4,11 +4,15 @@ from django.contrib.contenttypes.models import ContentType from extras.choices import * +from utilities.forms.fields import CommentField, SlugField from utilities.forms.mixins import CheckLastUpdatedMixin from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, TagsMixin __all__ = ( + 'NestedGroupModelForm', 'NetBoxModelForm', + 'OrganizationalModelForm', + 'PrimaryModelForm', ) @@ -16,7 +20,6 @@ class NetBoxModelForm( ChangelogMessageMixin, CheckLastUpdatedMixin, CustomFieldsMixin, - OwnerMixin, TagsMixin, forms.ModelForm ): @@ -74,3 +77,25 @@ def _post_clean(self): self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name]) return super()._post_clean() + + +class PrimaryModelForm(OwnerMixin, NetBoxModelForm): + """ + Form for models which inherit from PrimaryModel. + """ + comments = CommentField() + + +class OrganizationalModelForm(OwnerMixin, NetBoxModelForm): + """ + Form for models which inherit from OrganizationalModel. + """ + slug = SlugField() + + +class NestedGroupModelForm(OwnerMixin, NetBoxModelForm): + """ + Form for models which inherit from NestedGroupModel. + """ + slug = SlugField() + comments = CommentField() diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index e442a9418a5..719ea8a93b1 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -1,9 +1,9 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from netbox.forms import NetBoxModelForm +from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from tenancy.models import * -from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField from utilities.forms.rendering import FieldSet, ObjectAttribute __all__ = ( @@ -20,14 +20,12 @@ # Tenants # -class TenantGroupForm(NetBoxModelForm): +class TenantGroupForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=TenantGroup.objects.all(), required=False ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')), @@ -40,14 +38,13 @@ class Meta: ] -class TenantForm(NetBoxModelForm): +class TenantForm(PrimaryModelForm): slug = SlugField() group = DynamicModelChoiceField( label=_('Group'), queryset=TenantGroup.objects.all(), required=False ) - comments = CommentField() fieldsets = ( FieldSet('name', 'slug', 'group', 'description', 'tags', name=_('Tenant')), @@ -64,14 +61,12 @@ class Meta: # Contacts # -class ContactGroupForm(NetBoxModelForm): +class ContactGroupForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=ContactGroup.objects.all(), required=False ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')), @@ -82,9 +77,7 @@ class Meta: fields = ('parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags') -class ContactRoleForm(NetBoxModelForm): - slug = SlugField() - +class ContactRoleForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Contact Role')), ) @@ -94,7 +87,7 @@ class Meta: fields = ('name', 'slug', 'description', 'owner', 'tags') -class ContactForm(NetBoxModelForm): +class ContactForm(PrimaryModelForm): groups = DynamicModelMultipleChoiceField( label=_('Groups'), queryset=ContactGroup.objects.all(), @@ -105,7 +98,6 @@ class ContactForm(NetBoxModelForm): assume_scheme='https', required=False, ) - comments = CommentField() fieldsets = ( FieldSet( diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 2e47631a7ca..9b4c21783e1 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -10,12 +10,10 @@ from extras.models import ConfigTemplate from ipam.choices import VLANQinQRoleChoices from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF -from netbox.forms import NetBoxModelForm +from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from tenancy.forms import TenancyForm from utilities.forms import ConfirmationForm -from utilities.forms.fields import ( - CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, -) +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import HTMXSelect from virtualization.models import * @@ -32,9 +30,7 @@ ) -class ClusterTypeForm(NetBoxModelForm): - slug = SlugField() - +class ClusterTypeForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Type')), ) @@ -46,9 +42,7 @@ class Meta: ) -class ClusterGroupForm(NetBoxModelForm): - slug = SlugField() - +class ClusterGroupForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Group')), ) @@ -60,7 +54,7 @@ class Meta: ) -class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm): +class ClusterForm(TenancyForm, ScopedForm, PrimaryModelForm): type = DynamicModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -72,7 +66,6 @@ class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm): required=False, quick_add=True ) - comments = CommentField() fieldsets = ( FieldSet('name', 'type', 'group', 'status', 'description', 'tags', name=_('Cluster')), @@ -173,7 +166,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm): ) -class VirtualMachineForm(TenancyForm, NetBoxModelForm): +class VirtualMachineForm(TenancyForm, PrimaryModelForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -221,7 +214,6 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): required=False, label=_('Config template') ) - comments = CommentField() fieldsets = ( FieldSet('name', 'role', 'status', 'description', 'serial', 'tags', name=_('Virtual Machine')), diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 241ac9c382c..ad9d73901f1 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -4,9 +4,9 @@ from dcim.models import Device, Interface from ipam.models import IPAddress, RouteTarget, VLAN -from netbox.forms import NetBoxModelForm +from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm from tenancy.forms import TenancyForm -from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField from utilities.forms.rendering import FieldSet, TabbedGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect @@ -29,9 +29,7 @@ ) -class TunnelGroupForm(NetBoxModelForm): - slug = SlugField() - +class TunnelGroupForm(OrganizationalModelForm): fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('Tunnel Group')), ) @@ -43,7 +41,7 @@ class Meta: ] -class TunnelForm(TenancyForm, NetBoxModelForm): +class TunnelForm(TenancyForm, PrimaryModelForm): group = DynamicModelChoiceField( queryset=TunnelGroup.objects.all(), label=_('Tunnel Group'), @@ -55,7 +53,6 @@ class TunnelForm(TenancyForm, NetBoxModelForm): label=_('IPSec Profile'), required=False ) - comments = CommentField() fieldsets = ( FieldSet('name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'tags', name=_('Tunnel')), @@ -293,7 +290,7 @@ def clean(self): self.instance.termination = self.cleaned_data.get('termination') -class IKEProposalForm(NetBoxModelForm): +class IKEProposalForm(PrimaryModelForm): fieldsets = ( FieldSet('name', 'description', 'tags', name=_('Proposal')), @@ -311,7 +308,7 @@ class Meta: ] -class IKEPolicyForm(NetBoxModelForm): +class IKEPolicyForm(PrimaryModelForm): proposals = DynamicModelMultipleChoiceField( queryset=IKEProposal.objects.all(), label=_('Proposals'), @@ -330,7 +327,7 @@ class Meta: ] -class IPSecProposalForm(NetBoxModelForm): +class IPSecProposalForm(PrimaryModelForm): fieldsets = ( FieldSet('name', 'description', 'tags', name=_('Proposal')), @@ -348,7 +345,7 @@ class Meta: ] -class IPSecPolicyForm(NetBoxModelForm): +class IPSecPolicyForm(PrimaryModelForm): proposals = DynamicModelMultipleChoiceField( queryset=IPSecProposal.objects.all(), label=_('Proposals'), @@ -367,7 +364,7 @@ class Meta: ] -class IPSecProfileForm(NetBoxModelForm): +class IPSecProfileForm(PrimaryModelForm): ike_policy = DynamicModelChoiceField( queryset=IKEPolicy.objects.all(), label=_('IKE policy') @@ -376,7 +373,6 @@ class IPSecProfileForm(NetBoxModelForm): queryset=IPSecPolicy.objects.all(), label=_('IPSec policy') ) - comments = CommentField() fieldsets = ( FieldSet('name', 'description', 'tags', name=_('Profile')), @@ -394,7 +390,7 @@ class Meta: # L2VPN # -class L2VPNForm(TenancyForm, NetBoxModelForm): +class L2VPNForm(TenancyForm, PrimaryModelForm): slug = SlugField() import_targets = DynamicModelMultipleChoiceField( label=_('Import targets'), @@ -406,7 +402,6 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): queryset=RouteTarget.objects.all(), required=False ) - comments = CommentField() fieldsets = ( FieldSet('name', 'slug', 'type', 'status', 'identifier', 'description', 'tags', name=_('L2VPN')), diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index b928745645d..0cd107ba6e4 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -1,12 +1,12 @@ from django.forms import PasswordInput from django.utils.translation import gettext_lazy as _ -from dcim.models import Device, Interface, Location, Site from dcim.forms.mixins import ScopedForm +from dcim.models import Device, Interface, Location, Site from ipam.models import VLAN -from netbox.forms import NetBoxModelForm +from netbox.forms import NestedGroupModelForm, PrimaryModelForm from tenancy.forms import TenancyForm -from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.fields import DynamicModelChoiceField from utilities.forms.mixins import DistanceValidationMixin from utilities.forms.rendering import FieldSet, InlineFields from wireless.models import * @@ -18,14 +18,12 @@ ) -class WirelessLANGroupForm(NetBoxModelForm): +class WirelessLANGroupForm(NestedGroupModelForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=WirelessLANGroup.objects.all(), required=False ) - slug = SlugField() - comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')), @@ -38,7 +36,7 @@ class Meta: ] -class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm): +class WirelessLANForm(ScopedForm, TenancyForm, PrimaryModelForm): group = DynamicModelChoiceField( label=_('Group'), queryset=WirelessLANGroup.objects.all(), @@ -51,7 +49,6 @@ class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm): selector=True, label=_('VLAN') ) - comments = CommentField() fieldsets = ( FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')), @@ -74,7 +71,7 @@ class Meta: } -class WirelessLinkForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): +class WirelessLinkForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm): site_a = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, @@ -159,7 +156,6 @@ class WirelessLinkForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): }, label=_('Interface') ) - comments = CommentField() fieldsets = ( FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')), From ab092f2d6ad514b764daa7b121409ce257babd2c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 15:07:09 -0400 Subject: [PATCH 13/40] Add owner field to all applicable bulk edit forms --- netbox/circuits/forms/bulk_edit.py | 65 ++------ netbox/core/forms/bulk_edit.py | 11 +- netbox/dcim/forms/bulk_edit.py | 186 ++++------------------- netbox/extras/forms/bulk_edit.py | 32 ++-- netbox/ipam/forms/bulk_edit.py | 120 +++------------ netbox/netbox/forms/bulk_edit.py | 42 ++++- netbox/tenancy/forms/bulk_edit.py | 45 ++---- netbox/virtualization/forms/bulk_edit.py | 36 +---- netbox/vpn/forms/bulk_edit.py | 68 ++------- netbox/wireless/forms/bulk_edit.py | 28 +--- 10 files changed, 140 insertions(+), 493 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 8d6e8dec19f..58b2642eabf 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -11,11 +11,11 @@ from dcim.models import Site from ipam.models import ASN from netbox.choices import DistanceUnitChoices -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice, get_field_value from utilities.forms.fields import ( - ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions @@ -36,18 +36,12 @@ ) -class ProviderBulkEditForm(NetBoxModelBulkEditForm): +class ProviderBulkEditForm(PrimaryModelBulkEditForm): asns = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), label=_('ASNs'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Provider fieldsets = ( @@ -58,18 +52,12 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): ) -class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): +class ProviderAccountBulkEditForm(PrimaryModelBulkEditForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ProviderAccount fieldsets = ( @@ -80,7 +68,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): ) -class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): +class ProviderNetworkBulkEditForm(PrimaryModelBulkEditForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -91,12 +79,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('Service ID') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ProviderNetwork fieldsets = ( @@ -107,16 +89,11 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): ) -class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): +class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm): color = ColorField( label=_('Color'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = CircuitType fieldsets = ( @@ -125,7 +102,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('color', 'description') -class CircuitBulkEditForm(NetBoxModelBulkEditForm): +class CircuitBulkEditForm(PrimaryModelBulkEditForm): type = DynamicModelChoiceField( label=_('Type'), queryset=CircuitType.objects.all(), @@ -183,12 +160,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=100, - required=False - ) - comments = CommentField() model = Circuit fieldsets = ( @@ -261,12 +232,7 @@ def __init__(self, *args, **kwargs): pass -class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) +class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm): tenant = DynamicModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -298,16 +264,11 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('priority',) -class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm): +class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm): color = ColorField( label=_('Color'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = VirtualCircuitType fieldsets = ( @@ -316,7 +277,7 @@ class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('color', 'description') -class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm): +class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm): provider_network = DynamicModelChoiceField( label=_('Provider network'), queryset=ProviderNetwork.objects.all(), @@ -343,12 +304,6 @@ class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=100, - required=False - ) - comments = CommentField() model = VirtualCircuit fieldsets = ( diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index 73618826d88..3111ac2689e 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -3,9 +3,8 @@ from core.choices import JobIntervalChoices from core.models import * -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import PrimaryModelBulkEditForm 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 @@ -14,7 +13,7 @@ ) -class DataSourceBulkEditForm(NetBoxModelBulkEditForm): +class DataSourceBulkEditForm(PrimaryModelBulkEditForm): type = forms.ChoiceField( label=_('Type'), choices=get_data_backend_choices, @@ -25,17 +24,11 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm): widget=BulkEditNullBooleanSelect(), label=_('Enabled') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) sync_interval = forms.ChoiceField( choices=JobIntervalChoices, required=False, label=_('Sync interval') ) - comments = CommentField() parameters = forms.JSONField( label=_('Parameters'), required=False diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 0f55506662e..cfc6140736a 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -10,14 +10,14 @@ from ipam.choices import VLANQinQRoleChoices from ipam.models import ASN, VLAN, VLANGroup, VRF from netbox.choices import * -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import ( + NestedGroupModelBulkEditForm, NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm, +) from netbox.forms.mixins import ChangelogMessageMixin from tenancy.models import Tenant from users.models import User from utilities.forms import BulkEditForm, add_blank_choice, form_from_model -from utilities.forms.fields import ( - ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, -) +from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from virtualization.models import Cluster @@ -71,18 +71,12 @@ ) -class RegionBulkEditForm(NetBoxModelBulkEditForm): +class RegionBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=Region.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Region fieldsets = ( @@ -91,18 +85,12 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'description', 'comments') -class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): +class SiteGroupBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=SiteGroup.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = SiteGroup fieldsets = ( @@ -111,7 +99,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'description', 'comments') -class SiteBulkEditForm(NetBoxModelBulkEditForm): +class SiteBulkEditForm(PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(SiteStatusChoices), @@ -162,12 +150,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): choices=add_blank_choice(TimeZoneFormField().choices), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Site fieldsets = ( @@ -178,7 +160,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): ) -class LocationBulkEditForm(NetBoxModelBulkEditForm): +class LocationBulkEditForm(NestedGroupModelBulkEditForm): site = DynamicModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -208,12 +190,6 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): max_length=50, required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Location fieldsets = ( @@ -222,16 +198,11 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'tenant', 'facility', 'description', 'comments') -class RackRoleBulkEditForm(NetBoxModelBulkEditForm): +class RackRoleBulkEditForm(OrganizationalModelBulkEditForm): color = ColorField( label=_('Color'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = RackRole fieldsets = ( @@ -240,7 +211,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('color', 'description') -class RackTypeBulkEditForm(NetBoxModelBulkEditForm): +class RackTypeBulkEditForm(PrimaryModelBulkEditForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -310,12 +281,6 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = RackType fieldsets = ( @@ -334,7 +299,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm): ) -class RackBulkEditForm(NetBoxModelBulkEditForm): +class RackBulkEditForm(PrimaryModelBulkEditForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), @@ -464,12 +429,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Rack fieldsets = ( @@ -485,7 +444,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): ) -class RackReservationBulkEditForm(NetBoxModelBulkEditForm): +class RackReservationBulkEditForm(PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(RackReservationStatusChoices), @@ -502,12 +461,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = RackReservation fieldsets = ( @@ -516,13 +469,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('comments',) -class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class ManufacturerBulkEditForm(OrganizationalModelBulkEditForm): model = Manufacturer fieldsets = ( FieldSet('description'), @@ -530,7 +477,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): +class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -576,12 +523,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = DeviceType fieldsets = ( @@ -594,17 +535,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments') -class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm): +class ModuleTypeProfileBulkEditForm(PrimaryModelBulkEditForm): schema = JSONField( label=_('Schema'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ModuleTypeProfile fieldsets = ( @@ -613,7 +548,7 @@ class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description', 'comments') -class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): +class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm): profile = DynamicModelChoiceField( label=_('Profile'), queryset=ModuleTypeProfile.objects.all(), @@ -644,12 +579,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ModuleType fieldsets = ( @@ -663,7 +592,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'description', 'comments') -class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): +class DeviceRoleBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=DeviceRole.objects.all(), @@ -683,12 +612,6 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): queryset=ConfigTemplate.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = DeviceRole fieldsets = ( @@ -697,7 +620,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'color', 'config_template', 'description', 'comments') -class PlatformBulkEditForm(NetBoxModelBulkEditForm): +class PlatformBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=Platform.objects.all(), @@ -713,12 +636,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): queryset=ConfigTemplate.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Platform fieldsets = ( @@ -727,7 +644,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'manufacturer', 'config_template', 'description', 'comments') -class DeviceBulkEditForm(NetBoxModelBulkEditForm): +class DeviceBulkEditForm(PrimaryModelBulkEditForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -787,11 +704,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('Serial Number') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) config_template = DynamicModelChoiceField( label=_('Config template'), queryset=ConfigTemplate.objects.all(), @@ -805,7 +717,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): 'site_id': ['$site', 'null'] }, ) - comments = CommentField() model = Device fieldsets = ( @@ -820,7 +731,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ) -class ModuleBulkEditForm(NetBoxModelBulkEditForm): +class ModuleBulkEditForm(PrimaryModelBulkEditForm): manufacturer = DynamicModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -848,12 +759,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('Serial Number') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Module fieldsets = ( @@ -862,7 +767,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('serial', 'description', 'comments') -class CableBulkEditForm(NetBoxModelBulkEditForm): +class CableBulkEditForm(PrimaryModelBulkEditForm): type = forms.ChoiceField( label=_('Type'), choices=add_blank_choice(CableTypeChoices), @@ -900,12 +805,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Cable fieldsets = ( @@ -917,18 +816,12 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): ) -class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): +class VirtualChassisBulkEditForm(PrimaryModelBulkEditForm): domain = forms.CharField( label=_('Domain'), max_length=30, required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = VirtualChassis fieldsets = ( @@ -937,7 +830,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('domain', 'description', 'comments') -class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): +class PowerPanelBulkEditForm(PrimaryModelBulkEditForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), @@ -971,12 +864,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): 'site_id': '$site' } ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = PowerPanel fieldsets = ( @@ -985,7 +872,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('location', 'description', 'comments') -class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): +class PowerFeedBulkEditForm(PrimaryModelBulkEditForm): power_panel = DynamicModelChoiceField( label=_('Power panel'), queryset=PowerPanel.objects.all(), @@ -1041,12 +928,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = PowerFeed fieldsets = ( @@ -1818,16 +1699,11 @@ def __init__(self, *args, **kwargs): # Device component roles # -class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): +class InventoryItemRoleBulkEditForm(OrganizationalModelBulkEditForm): color = ColorField( label=_('Color'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = InventoryItemRole fieldsets = ( @@ -1836,7 +1712,7 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('color', 'description') -class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm): +class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm): device = DynamicModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1852,6 +1728,7 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) + model = VirtualDeviceContext fieldsets = ( FieldSet('device', 'status', 'tenant'), @@ -1863,14 +1740,7 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm): # Addressing # -class MACAddressBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() - +class MACAddressBulkEditForm(PrimaryModelBulkEditForm): model = MACAddress fieldsets = ( FieldSet('description'), diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 9cbfbae3268..db0f95484b9 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -4,8 +4,8 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms import NetBoxModelBulkEditForm -from netbox.forms.mixins import ChangelogMessageMixin +from netbox.forms import NetBoxModelBulkEditForm, PrimaryModelBulkEditForm +from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin from utilities.forms import BulkEditForm, add_blank_choice from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField from utilities.forms.rendering import FieldSet @@ -30,7 +30,7 @@ ) -class CustomFieldBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class CustomFieldBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=CustomField.objects.all(), widget=forms.MultipleHiddenInput @@ -98,7 +98,7 @@ class CustomFieldBulkEditForm(ChangelogMessageMixin, BulkEditForm): nullable_fields = ('group_name', 'description', 'choice_set') -class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=CustomFieldChoiceSet.objects.all(), widget=forms.MultipleHiddenInput @@ -118,7 +118,7 @@ class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, BulkEditForm): nullable_fields = ('base_choices', 'description') -class CustomLinkBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class CustomLinkBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=CustomLink.objects.all(), widget=forms.MultipleHiddenInput @@ -144,7 +144,7 @@ class CustomLinkBulkEditForm(ChangelogMessageMixin, BulkEditForm): ) -class ExportTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class ExportTemplateBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ExportTemplate.objects.all(), widget=forms.MultipleHiddenInput @@ -177,7 +177,7 @@ class ExportTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm): nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension') -class SavedFilterBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class SavedFilterBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=SavedFilter.objects.all(), widget=forms.MultipleHiddenInput @@ -233,7 +233,7 @@ class TableConfigBulkEditForm(BulkEditForm): nullable_fields = ('description',) -class WebhookBulkEditForm(NetBoxModelBulkEditForm): +class WebhookBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): model = Webhook pk = forms.ModelMultipleChoiceField( @@ -271,7 +271,7 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('secret', 'ca_file_path') -class EventRuleBulkEditForm(NetBoxModelBulkEditForm): +class EventRuleBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): model = EventRule pk = forms.ModelMultipleChoiceField( @@ -297,7 +297,7 @@ class EventRuleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description', 'conditions') -class TagBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class TagBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Tag.objects.all(), widget=forms.MultipleHiddenInput @@ -319,17 +319,11 @@ class TagBulkEditForm(ChangelogMessageMixin, BulkEditForm): nullable_fields = ('description',) -class ConfigContextProfileBulkEditForm(NetBoxModelBulkEditForm): +class ConfigContextProfileBulkEditForm(PrimaryModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConfigContextProfile.objects.all(), widget=forms.MultipleHiddenInput ) - description = forms.CharField( - label=_('Description'), - required=False, - max_length=100 - ) - comments = CommentField() model = ConfigContextProfile fieldsets = ( @@ -338,7 +332,7 @@ class ConfigContextProfileBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ConfigContextBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class ConfigContextBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConfigContext.objects.all(), widget=forms.MultipleHiddenInput @@ -369,7 +363,7 @@ class ConfigContextBulkEditForm(ChangelogMessageMixin, BulkEditForm): nullable_fields = ('profile', 'description') -class ConfigTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm): +class ConfigTemplateBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ConfigTemplate.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 864630bd451..8a85a908b9a 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -9,11 +9,11 @@ from ipam.constants import * from ipam.models import * from ipam.models import ASN -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice, get_field_value from utilities.forms.fields import ( - CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, + ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, NumericRangeArrayField, ) from utilities.forms.rendering import FieldSet @@ -41,7 +41,7 @@ ) -class VRFBulkEditForm(NetBoxModelBulkEditForm): +class VRFBulkEditForm(PrimaryModelBulkEditForm): tenant = DynamicModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -52,12 +52,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): widget=BulkEditNullBooleanSelect(), label=_('Enforce unique space') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = VRF fieldsets = ( @@ -66,18 +60,12 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('tenant', 'description', 'comments') -class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): +class RouteTargetBulkEditForm(PrimaryModelBulkEditForm): tenant = DynamicModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = RouteTarget fieldsets = ( @@ -86,17 +74,12 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('tenant', 'description', 'comments') -class RIRBulkEditForm(NetBoxModelBulkEditForm): +class RIRBulkEditForm(OrganizationalModelBulkEditForm): is_private = forms.NullBooleanField( label=_('Is private'), required=False, widget=BulkEditNullBooleanSelect ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = RIR fieldsets = ( @@ -105,7 +88,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('is_private', 'description') -class ASNRangeBulkEditForm(NetBoxModelBulkEditForm): +class ASNRangeBulkEditForm(OrganizationalModelBulkEditForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), required=False, @@ -116,11 +99,6 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = ASNRange fieldsets = ( @@ -129,7 +107,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ASNBulkEditForm(NetBoxModelBulkEditForm): +class ASNBulkEditForm(PrimaryModelBulkEditForm): sites = DynamicModelMultipleChoiceField( label=_('Sites'), queryset=Site.objects.all(), @@ -145,12 +123,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ASN fieldsets = ( @@ -159,7 +131,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('tenant', 'description', 'comments') -class AggregateBulkEditForm(NetBoxModelBulkEditForm): +class AggregateBulkEditForm(PrimaryModelBulkEditForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), required=False, @@ -174,12 +146,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): label=_('Date added'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Aggregate fieldsets = ( @@ -188,16 +154,11 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('date_added', 'description', 'comments') -class RoleBulkEditForm(NetBoxModelBulkEditForm): +class RoleBulkEditForm(OrganizationalModelBulkEditForm): weight = forms.IntegerField( label=_('Weight'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = Role fieldsets = ( @@ -206,7 +167,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): +class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm): vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, @@ -256,12 +217,6 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): widget=BulkEditNullBooleanSelect(), label=_('Treat as fully utilized') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Prefix fieldsets = ( @@ -275,7 +230,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): ) -class IPRangeBulkEditForm(NetBoxModelBulkEditForm): +class IPRangeBulkEditForm(PrimaryModelBulkEditForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -306,12 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): widget=BulkEditNullBooleanSelect(), label=_('Treat as fully utilized') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IPRange fieldsets = ( @@ -322,7 +271,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): ) -class IPAddressBulkEditForm(NetBoxModelBulkEditForm): +class IPAddressBulkEditForm(PrimaryModelBulkEditForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -354,12 +303,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('DNS name') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IPAddress fieldsets = ( @@ -371,7 +314,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): ) -class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): +class FHRPGroupBulkEditForm(PrimaryModelBulkEditForm): protocol = forms.ChoiceField( label=_('Protocol'), choices=add_blank_choice(FHRPGroupProtocolChoices), @@ -397,12 +340,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): max_length=100, required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = FHRPGroup fieldsets = ( @@ -412,12 +349,7 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments') -class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) +class VLANGroupBulkEditForm(OrganizationalModelBulkEditForm): scope_type = ContentTypeChoiceField( queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}), @@ -464,7 +396,7 @@ def __init__(self, *args, **kwargs): pass -class VLANBulkEditForm(NetBoxModelBulkEditForm): +class VLANBulkEditForm(PrimaryModelBulkEditForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), @@ -507,11 +439,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): queryset=Role.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) qinq_role = forms.ChoiceField( label=_('Q-in-Q role'), choices=add_blank_choice(VLANQinQRoleChoices), @@ -525,7 +452,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE, } ) - comments = CommentField() model = VLAN fieldsets = ( @@ -538,13 +464,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): ) -class VLANTranslationPolicyBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class VLANTranslationPolicyBulkEditForm(PrimaryModelBulkEditForm): model = VLANTranslationPolicy fieldsets = ( FieldSet('description'), @@ -568,7 +488,7 @@ class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm): fields = ('policy', 'local_vid', 'remote_vid') -class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): +class ServiceTemplateBulkEditForm(PrimaryModelBulkEditForm): protocol = forms.ChoiceField( label=_('Protocol'), choices=add_blank_choice(ServiceProtocolChoices), @@ -582,12 +502,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): ), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ServiceTemplate fieldsets = ( diff --git a/netbox/netbox/forms/bulk_edit.py b/netbox/netbox/forms/bulk_edit.py index e0d3ad348e0..5ad95b15013 100644 --- a/netbox/netbox/forms/bulk_edit.py +++ b/netbox/netbox/forms/bulk_edit.py @@ -5,15 +5,18 @@ from extras.choices import * from extras.models import Tag from utilities.forms import BulkEditForm -from utilities.forms.fields import DynamicModelMultipleChoiceField +from utilities.forms.fields import CommentField, DynamicModelMultipleChoiceField from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin __all__ = ( + 'NestedGroupModelBulkEditForm', 'NetBoxModelBulkEditForm', + 'OrganizationalModelBulkEditForm', + 'PrimaryModelBulkEditForm', ) -class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, BulkEditForm): +class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, BulkEditForm): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. @@ -65,3 +68,38 @@ def _extend_nullable_fields(self): *nullable_common_fields, *nullable_custom_fields, ) + + +class PrimaryModelBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): + """ + Bulk edit form for models which inherit from PrimaryModel. + """ + description = forms.CharField( + label=_('Description'), + max_length=100, + required=False + ) + comments = CommentField() + + +class OrganizationalModelBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): + """ + Bulk edit form for models which inherit from OrganizationalModel. + """ + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + + +class NestedGroupModelBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): + """ + Bulk edit form for models which inherit from NestedGroupModel. + """ + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + comments = CommentField() diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 22aba810fb5..2ad442d22ad 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -1,11 +1,13 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import ( + NestedGroupModelBulkEditForm, NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm, +) from tenancy.choices import ContactPriorityChoices from tenancy.models import * from utilities.forms import add_blank_choice -from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet __all__ = ( @@ -22,34 +24,23 @@ # Tenants # -class TenantGroupBulkEditForm(NetBoxModelBulkEditForm): +class TenantGroupBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=TenantGroup.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = TenantGroup nullable_fields = ('parent', 'description', 'comments') -class TenantBulkEditForm(NetBoxModelBulkEditForm): +class TenantBulkEditForm(PrimaryModelBulkEditForm): group = DynamicModelChoiceField( label=_('Group'), queryset=TenantGroup.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) model = Tenant fieldsets = ( @@ -62,18 +53,12 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm): # Contacts # -class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): +class ContactGroupBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=ContactGroup.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = ContactGroup fieldsets = ( @@ -82,13 +67,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'description', 'comments') -class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class ContactRoleBulkEditForm(OrganizationalModelBulkEditForm): model = ContactRole fieldsets = ( FieldSet('description'), @@ -96,7 +75,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ContactBulkEditForm(NetBoxModelBulkEditForm): +class ContactBulkEditForm(PrimaryModelBulkEditForm): add_groups = DynamicModelMultipleChoiceField( label=_('Add groups'), queryset=ContactGroup.objects.all(), @@ -131,12 +110,6 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): assume_scheme='https', required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Contact fieldsets = ( diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 80b66504731..d72d7c2bed9 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -7,10 +7,10 @@ from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VRF -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkRenameForm, add_blank_choice -from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect from virtualization.choices import * @@ -28,13 +28,7 @@ ) -class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class ClusterTypeBulkEditForm(OrganizationalModelBulkEditForm): model = ClusterType fieldsets = ( FieldSet('description'), @@ -42,13 +36,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm): model = ClusterGroup fieldsets = ( FieldSet('description'), @@ -56,7 +44,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('description',) -class ClusterBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): +class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm): type = DynamicModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -78,12 +66,6 @@ class ClusterBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = Cluster fieldsets = ( @@ -95,7 +77,7 @@ class ClusterBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): ) -class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): +class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(VirtualMachineStatusChoices), @@ -155,16 +137,10 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('Disk (MB)') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) config_template = DynamicModelChoiceField( queryset=ConfigTemplate.objects.all(), required=False ) - comments = CommentField() model = VirtualMachine fieldsets = ( diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py index 700dadb70bc..487ca51c92e 100644 --- a/netbox/vpn/forms/bulk_edit.py +++ b/netbox/vpn/forms/bulk_edit.py @@ -1,10 +1,10 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice -from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet from vpn.choices import * from vpn.models import * @@ -23,18 +23,12 @@ ) -class TunnelGroupBulkEditForm(NetBoxModelBulkEditForm): - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - +class TunnelGroupBulkEditForm(OrganizationalModelBulkEditForm): model = TunnelGroup nullable_fields = ('description',) -class TunnelBulkEditForm(NetBoxModelBulkEditForm): +class TunnelBulkEditForm(PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(TunnelStatusChoices), @@ -60,16 +54,10 @@ class TunnelBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) tunnel_id = forms.IntegerField( label=_('Tunnel ID'), required=False ) - comments = CommentField() model = Tunnel fieldsets = ( @@ -92,7 +80,7 @@ class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm): model = TunnelTermination -class IKEProposalBulkEditForm(NetBoxModelBulkEditForm): +class IKEProposalBulkEditForm(PrimaryModelBulkEditForm): authentication_method = forms.ChoiceField( label=_('Authentication method'), choices=add_blank_choice(AuthenticationMethodChoices), @@ -117,12 +105,6 @@ class IKEProposalBulkEditForm(NetBoxModelBulkEditForm): label=_('SA lifetime'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IKEProposal fieldsets = ( @@ -136,7 +118,7 @@ class IKEProposalBulkEditForm(NetBoxModelBulkEditForm): ) -class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm): +class IKEPolicyBulkEditForm(PrimaryModelBulkEditForm): version = forms.ChoiceField( label=_('Version'), choices=add_blank_choice(IKEVersionChoices), @@ -151,12 +133,6 @@ class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm): label=_('Pre-shared key'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IKEPolicy fieldsets = ( @@ -167,7 +143,7 @@ class IKEPolicyBulkEditForm(NetBoxModelBulkEditForm): ) -class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm): +class IPSecProposalBulkEditForm(PrimaryModelBulkEditForm): encryption_algorithm = forms.ChoiceField( label=_('Encryption algorithm'), choices=add_blank_choice(EncryptionAlgorithmChoices), @@ -186,12 +162,6 @@ class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm): label=_('SA lifetime (KB)'), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IPSecProposal fieldsets = ( @@ -205,18 +175,12 @@ class IPSecProposalBulkEditForm(NetBoxModelBulkEditForm): ) -class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm): +class IPSecPolicyBulkEditForm(PrimaryModelBulkEditForm): pfs_group = forms.ChoiceField( label=_('PFS group'), choices=add_blank_choice(DHGroupChoices), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IPSecPolicy fieldsets = ( @@ -227,7 +191,7 @@ class IPSecPolicyBulkEditForm(NetBoxModelBulkEditForm): ) -class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): +class IPSecProfileBulkEditForm(PrimaryModelBulkEditForm): mode = forms.ChoiceField( label=_('Mode'), choices=add_blank_choice(IPSecModeChoices), @@ -243,12 +207,6 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): queryset=IPSecPolicy.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = IPSecProfile fieldsets = ( @@ -259,7 +217,7 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): ) -class L2VPNBulkEditForm(NetBoxModelBulkEditForm): +class L2VPNBulkEditForm(PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=L2VPNStatusChoices, @@ -274,12 +232,6 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = L2VPN fieldsets = ( diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 1a75512e1a6..98645dfb8cb 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -5,10 +5,10 @@ from dcim.forms.mixins import ScopedBulkEditForm from ipam.models import VLAN from netbox.choices import * -from netbox.forms import NetBoxModelBulkEditForm +from netbox.forms import NestedGroupModelBulkEditForm, PrimaryModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice -from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.fields import DynamicModelChoiceField from utilities.forms.rendering import FieldSet from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH @@ -21,18 +21,12 @@ ) -class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): +class WirelessLANGroupBulkEditForm(NestedGroupModelBulkEditForm): parent = DynamicModelChoiceField( label=_('Parent'), queryset=WirelessLANGroup.objects.all(), required=False ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = WirelessLANGroup fieldsets = ( @@ -41,7 +35,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('parent', 'description', 'comments') -class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): +class WirelessLANBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm): status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(WirelessLANStatusChoices), @@ -81,12 +75,6 @@ class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): required=False, label=_('Pre-shared key') ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = WirelessLAN fieldsets = ( @@ -99,7 +87,7 @@ class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): ) -class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): +class WirelessLinkBulkEditForm(PrimaryModelBulkEditForm): ssid = forms.CharField( max_length=SSID_MAX_LENGTH, required=False, @@ -140,12 +128,6 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): required=False, initial='' ) - description = forms.CharField( - label=_('Description'), - max_length=200, - required=False - ) - comments = CommentField() model = WirelessLink fieldsets = ( From a848d3b81645879bd613f1d0679c0f6bf0ec7f84 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 16:15:26 -0400 Subject: [PATCH 14/40] Add owner field to all applicable bulk import forms --- netbox/circuits/forms/bulk_import.py | 37 ++++--- netbox/core/forms/bulk_import.py | 6 +- netbox/dcim/forms/bulk_import.py | 108 +++++++++++---------- netbox/extras/forms/bulk_import.py | 42 ++++---- netbox/ipam/forms/bulk_import.py | 70 +++++++------ netbox/netbox/forms/bulk_import.py | 43 ++++++-- netbox/tenancy/forms/bulk_import.py | 30 +++--- netbox/virtualization/forms/bulk_import.py | 23 +++-- netbox/vpn/forms/bulk_import.py | 40 ++++---- netbox/wireless/forms/bulk_import.py | 18 ++-- 10 files changed, 223 insertions(+), 194 deletions(-) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 43700d16b41..4f529598266 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -7,7 +7,7 @@ from circuits.models import * from dcim.models import Interface from netbox.choices import DistanceUnitChoices -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField @@ -28,17 +28,17 @@ ) -class ProviderImportForm(NetBoxModelImportForm): +class ProviderImportForm(PrimaryModelBulkImportForm): slug = SlugField() class Meta: model = Provider fields = ( - 'name', 'slug', 'description', 'comments', 'tags', + 'name', 'slug', 'description', 'owner', 'comments', 'tags', ) -class ProviderAccountImportForm(NetBoxModelImportForm): +class ProviderAccountImportForm(PrimaryModelBulkImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -49,11 +49,11 @@ class ProviderAccountImportForm(NetBoxModelImportForm): class Meta: model = ProviderAccount fields = ( - 'provider', 'name', 'account', 'description', 'comments', 'tags', + 'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags', ) -class ProviderNetworkImportForm(NetBoxModelImportForm): +class ProviderNetworkImportForm(PrimaryModelBulkImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -64,19 +64,19 @@ class ProviderNetworkImportForm(NetBoxModelImportForm): class Meta: model = ProviderNetwork fields = [ - 'provider', 'name', 'service_id', 'description', 'comments', 'tags' + 'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags' ] -class CircuitTypeImportForm(NetBoxModelImportForm): +class CircuitTypeImportForm(OrganizationalModelBulkImportForm): slug = SlugField() class Meta: model = CircuitType - fields = ('name', 'slug', 'color', 'description', 'tags') + fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class CircuitImportForm(NetBoxModelImportForm): +class CircuitImportForm(PrimaryModelBulkImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -119,7 +119,7 @@ class Meta: model = Circuit fields = [ 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', - 'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags' + 'commit_rate', 'distance', 'distance_unit', 'description', 'owner', 'comments', 'tags' ] @@ -165,7 +165,7 @@ class Meta: } -class CircuitGroupImportForm(NetBoxModelImportForm): +class CircuitGroupImportForm(OrganizationalModelBulkImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -176,7 +176,7 @@ class CircuitGroupImportForm(NetBoxModelImportForm): class Meta: model = CircuitGroup - fields = ('name', 'slug', 'description', 'tenant', 'tags') + fields = ('name', 'slug', 'description', 'tenant', 'owner', 'tags') class CircuitGroupAssignmentImportForm(NetBoxModelImportForm): @@ -195,15 +195,14 @@ class Meta: fields = ('member_type', 'member_id', 'group', 'priority') -class VirtualCircuitTypeImportForm(NetBoxModelImportForm): - slug = SlugField() +class VirtualCircuitTypeImportForm(OrganizationalModelBulkImportForm): class Meta: model = VirtualCircuitType - fields = ('name', 'slug', 'color', 'description', 'tags') + fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class VirtualCircuitImportForm(NetBoxModelImportForm): +class VirtualCircuitImportForm(PrimaryModelBulkImportForm): provider_network = CSVModelChoiceField( label=_('Provider network'), queryset=ProviderNetwork.objects.all(), @@ -239,8 +238,8 @@ class VirtualCircuitImportForm(NetBoxModelImportForm): class Meta: model = VirtualCircuit fields = [ - 'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments', - 'tags', + 'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'owner', + 'comments', 'tags', ] diff --git a/netbox/core/forms/bulk_import.py b/netbox/core/forms/bulk_import.py index a5791c94514..c9311a61df6 100644 --- a/netbox/core/forms/bulk_import.py +++ b/netbox/core/forms/bulk_import.py @@ -1,16 +1,16 @@ from core.models import * -from netbox.forms import NetBoxModelImportForm +from netbox.forms import PrimaryModelBulkImportForm __all__ = ( 'DataSourceImportForm', ) -class DataSourceImportForm(NetBoxModelImportForm): +class DataSourceImportForm(PrimaryModelBulkImportForm): class Meta: model = DataSource fields = ( 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules', - 'comments', + 'owner', 'comments', ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 10ae701bbe2..65abd9502c3 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -11,7 +11,10 @@ from extras.models import ConfigTemplate from ipam.models import VRF, IPAddress from netbox.choices import * -from netbox.forms import NetBoxModelImportForm +from netbox.forms import ( + NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, + PrimaryModelBulkImportForm, +) from tenancy.models import Tenant from utilities.forms.fields import ( CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField, @@ -58,7 +61,7 @@ ) -class RegionImportForm(NetBoxModelImportForm): +class RegionImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=Region.objects.all(), @@ -69,10 +72,10 @@ class RegionImportForm(NetBoxModelImportForm): class Meta: model = Region - fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') + fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class SiteGroupImportForm(NetBoxModelImportForm): +class SiteGroupImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=SiteGroup.objects.all(), @@ -83,10 +86,10 @@ class SiteGroupImportForm(NetBoxModelImportForm): class Meta: model = SiteGroup - fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags') + fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class SiteImportForm(NetBoxModelImportForm): +class SiteImportForm(PrimaryModelBulkImportForm): status = CSVChoiceField( label=_('Status'), choices=SiteStatusChoices, @@ -118,7 +121,7 @@ class Meta: model = Site fields = ( 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', - 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags' + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags' ) help_texts = { 'time_zone': mark_safe( @@ -129,7 +132,7 @@ class Meta: } -class LocationImportForm(NetBoxModelImportForm): +class LocationImportForm(NestedGroupModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -162,8 +165,8 @@ class LocationImportForm(NetBoxModelImportForm): class Meta: model = Location fields = ( - 'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', - 'tags', 'comments', + 'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'owner', 'comments', + 'tags', ) def __init__(self, data=None, *args, **kwargs): @@ -175,15 +178,14 @@ def __init__(self, data=None, *args, **kwargs): self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params) -class RackRoleImportForm(NetBoxModelImportForm): - slug = SlugField() +class RackRoleImportForm(OrganizationalModelBulkImportForm): class Meta: model = RackRole - fields = ('name', 'slug', 'color', 'description', 'tags') + fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class RackTypeImportForm(NetBoxModelImportForm): +class RackTypeImportForm(PrimaryModelBulkImportForm): manufacturer = forms.ModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -224,14 +226,14 @@ class Meta: fields = ( 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'weight_unit', 'description', 'owner', 'comments', 'tags', ) def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) -class RackImportForm(NetBoxModelImportForm): +class RackImportForm(PrimaryModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -309,7 +311,8 @@ class Meta: fields = ( 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', - 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', + 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'owner', 'comments', + 'tags', ) def __init__(self, data=None, *args, **kwargs): @@ -332,7 +335,7 @@ def clean(self): raise forms.ValidationError(_("U height must be set if not specifying a rack type.")) -class RackReservationImportForm(NetBoxModelImportForm): +class RackReservationImportForm(PrimaryModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -373,7 +376,7 @@ class RackReservationImportForm(NetBoxModelImportForm): class Meta: model = RackReservation - fields = ('site', 'location', 'rack', 'units', 'status', 'tenant', 'description', 'comments', 'tags') + fields = ('site', 'location', 'rack', 'units', 'status', 'tenant', 'description', 'owner', 'comments', 'tags') def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) @@ -392,14 +395,14 @@ def __init__(self, data=None, *args, **kwargs): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class ManufacturerImportForm(NetBoxModelImportForm): +class ManufacturerImportForm(OrganizationalModelBulkImportForm): class Meta: model = Manufacturer - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') -class DeviceTypeImportForm(NetBoxModelImportForm): +class DeviceTypeImportForm(PrimaryModelBulkImportForm): manufacturer = CSVModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -429,20 +432,21 @@ class Meta: model = DeviceType fields = [ 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', - 'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags', + 'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'owner', 'comments', + 'tags', ] -class ModuleTypeProfileImportForm(NetBoxModelImportForm): +class ModuleTypeProfileImportForm(PrimaryModelBulkImportForm): class Meta: model = ModuleTypeProfile fields = [ - 'name', 'description', 'schema', 'comments', 'tags', + 'name', 'description', 'schema', 'owner', 'comments', 'tags', ] -class ModuleTypeImportForm(NetBoxModelImportForm): +class ModuleTypeImportForm(PrimaryModelBulkImportForm): profile = forms.ModelChoiceField( label=_('Profile'), queryset=ModuleTypeProfile.objects.all(), @@ -476,11 +480,11 @@ class Meta: model = ModuleType fields = [ 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile', - 'comments', 'tags' + 'owner', 'comments', 'tags' ] -class DeviceRoleImportForm(NetBoxModelImportForm): +class DeviceRoleImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=DeviceRole.objects.all(), @@ -498,17 +502,15 @@ class DeviceRoleImportForm(NetBoxModelImportForm): required=False, help_text=_('Config template') ) - slug = SlugField() class Meta: model = DeviceRole fields = ( - 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags' + 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags' ) -class PlatformImportForm(NetBoxModelImportForm): - slug = SlugField() +class PlatformImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=Platform.objects.all(), @@ -537,11 +539,11 @@ class PlatformImportForm(NetBoxModelImportForm): class Meta: model = Platform fields = ( - 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags', ) -class BaseDeviceImportForm(NetBoxModelImportForm): +class BaseDeviceImportForm(PrimaryModelBulkImportForm): role = CSVModelChoiceField( label=_('Device role'), queryset=DeviceRole.objects.all(), @@ -667,8 +669,8 @@ class Meta(BaseDeviceImportForm.Meta): fields = [ 'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow', - 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', - 'tags', + 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'owner', + 'comments', 'tags', ] def __init__(self, data=None, *args, **kwargs): @@ -715,7 +717,7 @@ def clean(self): self.instance.parent_bay = device_bay -class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm): +class ModuleImportForm(ModuleCommonForm, PrimaryModelBulkImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -753,7 +755,7 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm): class Meta: model = Module fields = ( - 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'comments', + 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'status', 'description', 'owner', 'comments', 'replicate_components', 'adopt_components', 'tags', ) @@ -1148,7 +1150,7 @@ def __init__(self, *args, **kwargs): self.fields['installed_device'].queryset = Device.objects.none() -class InventoryItemImportForm(NetBoxModelImportForm): +class InventoryItemImportForm(OrganizationalModelBulkImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1195,7 +1197,7 @@ class Meta: model = InventoryItem fields = ( 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', - 'discovered', 'description', 'tags', 'component_type', 'component_name', + 'discovered', 'description', 'owner', 'tags', 'component_type', 'component_name', ) def __init__(self, *args, **kwargs): @@ -1270,7 +1272,7 @@ class Meta: # Addressing # -class MACAddressImportForm(NetBoxModelImportForm): +class MACAddressImportForm(PrimaryModelBulkImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1301,7 +1303,8 @@ class MACAddressImportForm(NetBoxModelImportForm): class Meta: model = MACAddress fields = [ - 'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'comments', 'tags', + 'mac_address', 'device', 'virtual_machine', 'interface', 'is_primary', 'description', 'owner', 'comments', + 'tags', ] def __init__(self, data=None, *args, **kwargs): @@ -1354,7 +1357,7 @@ def save(self, *args, **kwargs): # Cables # -class CableImportForm(NetBoxModelImportForm): +class CableImportForm(PrimaryModelBulkImportForm): # Termination A side_a_site = CSVModelChoiceField( label=_('Side A site'), @@ -1443,7 +1446,7 @@ class Meta: fields = [ 'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type', 'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] def __init__(self, data=None, *args, **kwargs): @@ -1537,7 +1540,7 @@ def clean_color(self): # -class VirtualChassisImportForm(NetBoxModelImportForm): +class VirtualChassisImportForm(PrimaryModelBulkImportForm): master = CSVModelChoiceField( label=_('Master'), queryset=Device.objects.all(), @@ -1548,14 +1551,14 @@ class VirtualChassisImportForm(NetBoxModelImportForm): class Meta: model = VirtualChassis - fields = ('name', 'domain', 'master', 'description', 'comments', 'tags') + fields = ('name', 'domain', 'master', 'description', 'owner', 'comments', 'tags') # # Power # -class PowerPanelImportForm(NetBoxModelImportForm): +class PowerPanelImportForm(PrimaryModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -1571,7 +1574,7 @@ class PowerPanelImportForm(NetBoxModelImportForm): class Meta: model = PowerPanel - fields = ('site', 'location', 'name', 'description', 'comments', 'tags') + fields = ('site', 'location', 'name', 'description', 'owner', 'comments', 'tags') def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) @@ -1583,7 +1586,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) -class PowerFeedImportForm(NetBoxModelImportForm): +class PowerFeedImportForm(PrimaryModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -1641,7 +1644,7 @@ class Meta: model = PowerFeed fields = ( 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', - 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags', + 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'owner', 'comments', 'tags', ) def __init__(self, data=None, *args, **kwargs): @@ -1665,8 +1668,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class VirtualDeviceContextImportForm(NetBoxModelImportForm): - +class VirtualDeviceContextImportForm(PrimaryModelBulkImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1701,7 +1703,7 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm): class Meta: fields = [ - 'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6', + 'name', 'device', 'status', 'tenant', 'identifier', 'owner', 'comments', 'primary_ip4', 'primary_ip6', ] model = VirtualDeviceContext diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 4f7c85e8581..8b6420b4c6a 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -9,7 +9,7 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NetBoxModelImportForm, OwnerCSVMixin, PrimaryModelBulkImportForm from users.models import Group, User from utilities.forms import CSVModelForm from utilities.forms.fields import ( @@ -33,7 +33,7 @@ ) -class CustomFieldImportForm(CSVModelForm): +class CustomFieldImportForm(OwnerCSVMixin, CSVModelForm): object_types = CSVMultipleContentTypeField( label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields'), @@ -75,11 +75,11 @@ class Meta: fields = ( 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique', 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', - 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments', + 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'owner', 'comments', ) -class CustomFieldChoiceSetImportForm(CSVModelForm): +class CustomFieldChoiceSetImportForm(OwnerCSVMixin, CSVModelForm): base_choices = CSVChoiceField( choices=CustomFieldChoiceSetBaseChoices, required=False, @@ -97,7 +97,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): class Meta: model = CustomFieldChoiceSet fields = ( - 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', + 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner', ) def clean_extra_choices(self): @@ -114,7 +114,7 @@ def clean_extra_choices(self): return data -class CustomLinkImportForm(CSVModelForm): +class CustomLinkImportForm(OwnerCSVMixin, CSVModelForm): object_types = CSVMultipleContentTypeField( label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), @@ -131,11 +131,11 @@ class Meta: model = CustomLink fields = ( 'name', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', - 'link_url', + 'link_url', 'owner', ) -class ExportTemplateImportForm(CSVModelForm): +class ExportTemplateImportForm(OwnerCSVMixin, CSVModelForm): object_types = CSVMultipleContentTypeField( label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates'), @@ -146,30 +146,30 @@ class Meta: model = ExportTemplate fields = ( 'name', 'object_types', 'description', 'environment_params', 'mime_type', 'file_name', 'file_extension', - 'as_attachment', 'template_code', + 'as_attachment', 'template_code', 'owner', ) -class ConfigContextProfileImportForm(NetBoxModelImportForm): +class ConfigContextProfileImportForm(PrimaryModelBulkImportForm): class Meta: model = ConfigContextProfile fields = [ - 'name', 'description', 'schema', 'comments', 'tags', + 'name', 'description', 'schema', 'owner', 'comments', 'tags', ] -class ConfigTemplateImportForm(CSVModelForm): +class ConfigTemplateImportForm(OwnerCSVMixin, CSVModelForm): class Meta: model = ConfigTemplate fields = ( 'name', 'description', 'template_code', 'environment_params', 'mime_type', 'file_name', 'file_extension', - 'as_attachment', 'tags', + 'as_attachment', 'owner', 'tags', ) -class SavedFilterImportForm(CSVModelForm): +class SavedFilterImportForm(OwnerCSVMixin, CSVModelForm): object_types = CSVMultipleContentTypeField( label=_('Object types'), queryset=ObjectType.objects.all(), @@ -179,21 +179,21 @@ class SavedFilterImportForm(CSVModelForm): class Meta: model = SavedFilter fields = ( - 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters', + 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters', 'owner', ) -class WebhookImportForm(NetBoxModelImportForm): +class WebhookImportForm(OwnerCSVMixin, NetBoxModelImportForm): class Meta: model = Webhook fields = ( 'name', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', - 'secret', 'ssl_verification', 'ca_file_path', 'description', 'tags' + 'secret', 'ssl_verification', 'ca_file_path', 'description', 'owner', 'tags' ) -class EventRuleImportForm(NetBoxModelImportForm): +class EventRuleImportForm(OwnerCSVMixin, NetBoxModelImportForm): object_types = CSVMultipleContentTypeField( label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), @@ -214,7 +214,7 @@ class Meta: model = EventRule fields = ( 'name', 'description', 'enabled', 'conditions', 'object_types', 'event_types', 'action_type', - 'comments', 'tags' + 'owner', 'comments', 'tags' ) def clean(self): @@ -242,7 +242,7 @@ def clean(self): self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False) -class TagImportForm(CSVModelForm): +class TagImportForm(OwnerCSVMixin, CSVModelForm): slug = SlugField() weight = forms.IntegerField( label=_('Weight'), @@ -258,7 +258,7 @@ class TagImportForm(CSVModelForm): class Meta: model = Tag fields = ( - 'name', 'slug', 'color', 'weight', 'description', 'object_types', + 'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner', ) diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index c0aa4346190..f2db5a52b5f 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -7,7 +7,7 @@ from ipam.choices import * from ipam.constants import * from ipam.models import * -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm from tenancy.models import Tenant from utilities.forms.fields import ( CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField, @@ -36,7 +36,7 @@ ) -class VRFImportForm(NetBoxModelImportForm): +class VRFImportForm(PrimaryModelBulkImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -60,12 +60,12 @@ class VRFImportForm(NetBoxModelImportForm): class Meta: model = VRF fields = ( - 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments', - 'tags', + 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'owner', + 'comments', 'tags', ) -class RouteTargetImportForm(NetBoxModelImportForm): +class RouteTargetImportForm(PrimaryModelBulkImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -76,18 +76,18 @@ class RouteTargetImportForm(NetBoxModelImportForm): class Meta: model = RouteTarget - fields = ('name', 'tenant', 'description', 'comments', 'tags') + fields = ('name', 'tenant', 'description', 'owner', 'comments', 'tags') -class RIRImportForm(NetBoxModelImportForm): +class RIRImportForm(OrganizationalModelBulkImportForm): slug = SlugField() class Meta: model = RIR - fields = ('name', 'slug', 'is_private', 'description', 'tags') + fields = ('name', 'slug', 'is_private', 'description', 'owner', 'tags') -class AggregateImportForm(NetBoxModelImportForm): +class AggregateImportForm(PrimaryModelBulkImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -104,10 +104,10 @@ class AggregateImportForm(NetBoxModelImportForm): class Meta: model = Aggregate - fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags') + fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'owner', 'comments', 'tags') -class ASNRangeImportForm(NetBoxModelImportForm): +class ASNRangeImportForm(OrganizationalModelBulkImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -124,10 +124,10 @@ class ASNRangeImportForm(NetBoxModelImportForm): class Meta: model = ASNRange - fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags') + fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'owner', 'tags') -class ASNImportForm(NetBoxModelImportForm): +class ASNImportForm(PrimaryModelBulkImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -144,18 +144,17 @@ class ASNImportForm(NetBoxModelImportForm): class Meta: model = ASN - fields = ('asn', 'rir', 'tenant', 'description', 'comments', 'tags') + fields = ('asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags') -class RoleImportForm(NetBoxModelImportForm): - slug = SlugField() +class RoleImportForm(OrganizationalModelBulkImportForm): class Meta: model = Role - fields = ('name', 'slug', 'weight', 'description', 'tags') + fields = ('name', 'slug', 'weight', 'description', 'owner', 'tags') -class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm): +class PrefixImportForm(ScopedImportForm, PrimaryModelBulkImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -208,7 +207,7 @@ class Meta: model = Prefix fields = ( 'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_id', - 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', + 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags', ) labels = { 'scope_id': _('Scope ID'), @@ -244,7 +243,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['vlan'].queryset = queryset -class IPRangeImportForm(NetBoxModelImportForm): +class IPRangeImportForm(PrimaryModelBulkImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -276,11 +275,11 @@ class Meta: model = IPRange fields = ( 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_populated', 'mark_utilized', - 'description', 'comments', 'tags', + 'description', 'owner', 'comments', 'tags', ) -class IPAddressImportForm(NetBoxModelImportForm): +class IPAddressImportForm(PrimaryModelBulkImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -349,7 +348,7 @@ class Meta: model = IPAddress fields = [ 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group', - 'is_primary', 'is_oob', 'dns_name', 'description', 'comments', 'tags', + 'is_primary', 'is_oob', 'dns_name', 'description', 'owner', 'comments', 'tags', ] def __init__(self, data=None, *args, **kwargs): @@ -428,7 +427,7 @@ def save(self, *args, **kwargs): return ipaddress -class FHRPGroupImportForm(NetBoxModelImportForm): +class FHRPGroupImportForm(PrimaryModelBulkImportForm): protocol = CSVChoiceField( label=_('Protocol'), choices=FHRPGroupProtocolChoices @@ -441,11 +440,10 @@ class FHRPGroupImportForm(NetBoxModelImportForm): class Meta: model = FHRPGroup - fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'comments', 'tags') + fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'owner', 'comments', 'tags') -class VLANGroupImportForm(NetBoxModelImportForm): - slug = SlugField() +class VLANGroupImportForm(OrganizationalModelBulkImportForm): scope_type = CSVContentTypeField( queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), required=False, @@ -464,13 +462,13 @@ class VLANGroupImportForm(NetBoxModelImportForm): class Meta: model = VLANGroup - fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'tags') + fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner', 'tags') labels = { 'scope_id': 'Scope ID', } -class VLANImportForm(NetBoxModelImportForm): +class VLANImportForm(PrimaryModelBulkImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -522,15 +520,15 @@ class Meta: model = VLAN fields = ( 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan', - 'comments', 'tags', + 'owner', 'comments', 'tags', ) -class VLANTranslationPolicyImportForm(NetBoxModelImportForm): +class VLANTranslationPolicyImportForm(PrimaryModelBulkImportForm): class Meta: model = VLANTranslationPolicy - fields = ('name', 'description', 'tags') + fields = ('name', 'description', 'owner', 'comments', 'tags') class VLANTranslationRuleImportForm(NetBoxModelImportForm): @@ -546,7 +544,7 @@ class Meta: fields = ('policy', 'local_vid', 'remote_vid') -class ServiceTemplateImportForm(NetBoxModelImportForm): +class ServiceTemplateImportForm(PrimaryModelBulkImportForm): protocol = CSVChoiceField( label=_('Protocol'), choices=ServiceProtocolChoices, @@ -555,10 +553,10 @@ class ServiceTemplateImportForm(NetBoxModelImportForm): class Meta: model = ServiceTemplate - fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags') + fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags') -class ServiceImportForm(NetBoxModelImportForm): +class ServiceImportForm(PrimaryModelBulkImportForm): parent_object_type = CSVContentTypeField( queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS), required=True, @@ -590,7 +588,7 @@ class ServiceImportForm(NetBoxModelImportForm): class Meta: model = Service fields = ( - 'ipaddresses', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', + 'ipaddresses', 'name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags', ) def __init__(self, data=None, *args, **kwargs): diff --git a/netbox/netbox/forms/bulk_import.py b/netbox/netbox/forms/bulk_import.py index 3504844dee3..062a8e7886f 100644 --- a/netbox/netbox/forms/bulk_import.py +++ b/netbox/netbox/forms/bulk_import.py @@ -1,14 +1,19 @@ +from django import forms from django.utils.translation import gettext_lazy as _ from extras.choices import * from extras.models import CustomField, Tag from users.models import Owner from utilities.forms import CSVModelForm -from utilities.forms.fields import CSVModelMultipleChoiceField, CSVModelChoiceField +from utilities.forms.fields import CSVModelMultipleChoiceField, CSVModelChoiceField, SlugField from .model_forms import NetBoxModelForm __all__ = ( + 'NestedGroupModelBulkImportForm', 'NetBoxModelImportForm', + 'OrganizationalModelBulkImportForm', + 'OwnerCSVMixin', + 'PrimaryModelBulkImportForm' ) @@ -16,12 +21,6 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): """ Base form for creating NetBox objects from CSV data. Used for bulk importing. """ - owner = CSVModelChoiceField( - queryset=Owner.objects.all(), - required=False, - to_field_name='name', - help_text=_("Name of the object's owner") - ) tags = CSVModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), @@ -38,3 +37,33 @@ def _get_custom_fields(self, content_type): def _get_form_field(self, customfield): return customfield.to_form_field(for_csv_import=True) + + +class OwnerCSVMixin(forms.Form): + owner = CSVModelChoiceField( + queryset=Owner.objects.all(), + required=False, + to_field_name='name', + help_text=_("Name of the object's owner") + ) + + +class PrimaryModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): + """ + Bulk import form for models which inherit from PrimaryModel. + """ + pass + + +class OrganizationalModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): + """ + Bulk import form for models which inherit from OrganizationalModel. + """ + slug = SlugField() + + +class NestedGroupModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): + """ + Bulk import form for models which inherit from NestedGroupModel. + """ + slug = SlugField() diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index 5861c976b07..81729dc7bf6 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -2,7 +2,10 @@ from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ -from netbox.forms import NetBoxModelImportForm +from netbox.forms import ( + NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, + PrimaryModelBulkImportForm, +) from tenancy.models import * from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField @@ -20,7 +23,7 @@ # Tenants # -class TenantGroupImportForm(NetBoxModelImportForm): +class TenantGroupImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=TenantGroup.objects.all(), @@ -28,14 +31,13 @@ class TenantGroupImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Parent group'), ) - slug = SlugField() class Meta: model = TenantGroup - fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') + fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class TenantImportForm(NetBoxModelImportForm): +class TenantImportForm(PrimaryModelBulkImportForm): slug = SlugField() group = CSVModelChoiceField( label=_('Group'), @@ -47,14 +49,14 @@ class TenantImportForm(NetBoxModelImportForm): class Meta: model = Tenant - fields = ('name', 'slug', 'group', 'description', 'comments', 'tags') + fields = ('name', 'slug', 'group', 'description', 'owner', 'comments', 'tags') # # Contacts # -class ContactGroupImportForm(NetBoxModelImportForm): +class ContactGroupImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=ContactGroup.objects.all(), @@ -62,22 +64,20 @@ class ContactGroupImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Parent group'), ) - slug = SlugField() class Meta: model = ContactGroup - fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') + fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class ContactRoleImportForm(NetBoxModelImportForm): - slug = SlugField() +class ContactRoleImportForm(OrganizationalModelBulkImportForm): class Meta: model = ContactRole - fields = ('name', 'slug', 'description') + fields = ('name', 'slug', 'description', 'owner', 'tags') -class ContactImportForm(NetBoxModelImportForm): +class ContactImportForm(PrimaryModelBulkImportForm): groups = CSVModelMultipleChoiceField( queryset=ContactGroup.objects.all(), required=False, @@ -92,7 +92,9 @@ class ContactImportForm(NetBoxModelImportForm): class Meta: model = Contact - fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'groups', 'description', 'comments', 'tags') + fields = ( + 'name', 'title', 'phone', 'email', 'address', 'link', 'groups', 'description', 'owner', 'comments', 'tags', + ) class ContactAssignmentImportForm(NetBoxModelImportForm): diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index 6b5b62d1103..c058454b5a2 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -5,9 +5,9 @@ from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate from ipam.models import VRF -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField from virtualization.choices import * from virtualization.models import * @@ -21,23 +21,21 @@ ) -class ClusterTypeImportForm(NetBoxModelImportForm): - slug = SlugField() +class ClusterTypeImportForm(OrganizationalModelBulkImportForm): class Meta: model = ClusterType - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') -class ClusterGroupImportForm(NetBoxModelImportForm): - slug = SlugField() +class ClusterGroupImportForm(OrganizationalModelBulkImportForm): class Meta: model = ClusterGroup - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') -class ClusterImportForm(ScopedImportForm, NetBoxModelImportForm): +class ClusterImportForm(ScopedImportForm, PrimaryModelBulkImportForm): type = CSVModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -74,14 +72,15 @@ class ClusterImportForm(ScopedImportForm, NetBoxModelImportForm): class Meta: model = Cluster fields = ( - 'name', 'type', 'group', 'status', 'scope_type', 'scope_id', 'tenant', 'description', 'comments', 'tags', + 'name', 'type', 'group', 'status', 'scope_type', 'scope_id', 'tenant', 'description', 'owner', 'comments', + 'tags', ) labels = { 'scope_id': _('Scope ID'), } -class VirtualMachineImportForm(NetBoxModelImportForm): +class VirtualMachineImportForm(PrimaryModelBulkImportForm): status = CSVChoiceField( label=_('Status'), choices=VirtualMachineStatusChoices, @@ -143,7 +142,7 @@ class Meta: model = VirtualMachine fields = ( 'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', - 'description', 'serial', 'config_template', 'comments', 'tags', + 'description', 'serial', 'config_template', 'comments', 'owner', 'tags', ) diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py index 96744887ea2..795431fda76 100644 --- a/netbox/vpn/forms/bulk_import.py +++ b/netbox/vpn/forms/bulk_import.py @@ -3,9 +3,9 @@ from dcim.models import Device, Interface from ipam.models import IPAddress, VLAN -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField from virtualization.models import VirtualMachine, VMInterface from vpn.choices import * from vpn.models import * @@ -24,15 +24,14 @@ ) -class TunnelGroupImportForm(NetBoxModelImportForm): - slug = SlugField() +class TunnelGroupImportForm(OrganizationalModelBulkImportForm): class Meta: model = TunnelGroup - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') -class TunnelImportForm(NetBoxModelImportForm): +class TunnelImportForm(PrimaryModelBulkImportForm): status = CSVChoiceField( label=_('Status'), choices=TunnelStatusChoices, @@ -67,7 +66,7 @@ class Meta: model = Tunnel fields = ( 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ) @@ -140,7 +139,7 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) -class IKEProposalImportForm(NetBoxModelImportForm): +class IKEProposalImportForm(PrimaryModelBulkImportForm): authentication_method = CSVChoiceField( label=_('Authentication method'), choices=AuthenticationMethodChoices @@ -163,11 +162,11 @@ class Meta: model = IKEProposal fields = ( 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', - 'group', 'sa_lifetime', 'comments', 'tags', + 'group', 'sa_lifetime', 'owner', 'comments', 'tags', ) -class IKEPolicyImportForm(NetBoxModelImportForm): +class IKEPolicyImportForm(PrimaryModelBulkImportForm): version = CSVChoiceField( label=_('Version'), choices=IKEVersionChoices @@ -186,11 +185,11 @@ class IKEPolicyImportForm(NetBoxModelImportForm): class Meta: model = IKEPolicy fields = ( - 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', 'tags', + 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'owner', 'comments', 'tags', ) -class IPSecProposalImportForm(NetBoxModelImportForm): +class IPSecProposalImportForm(PrimaryModelBulkImportForm): encryption_algorithm = CSVChoiceField( label=_('Encryption algorithm'), choices=EncryptionAlgorithmChoices, @@ -206,11 +205,11 @@ class Meta: model = IPSecProposal fields = ( 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', - 'sa_lifetime_data', 'comments', 'tags', + 'sa_lifetime_data', 'owner', 'comments', 'tags', ) -class IPSecPolicyImportForm(NetBoxModelImportForm): +class IPSecPolicyImportForm(PrimaryModelBulkImportForm): pfs_group = CSVChoiceField( label=_('Diffie-Hellman group for Perfect Forward Secrecy'), choices=DHGroupChoices, @@ -225,11 +224,11 @@ class IPSecPolicyImportForm(NetBoxModelImportForm): class Meta: model = IPSecPolicy fields = ( - 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', + 'name', 'description', 'proposals', 'pfs_group', 'owner', 'comments', 'tags', ) -class IPSecProfileImportForm(NetBoxModelImportForm): +class IPSecProfileImportForm(PrimaryModelBulkImportForm): mode = CSVChoiceField( label=_('Mode'), choices=IPSecModeChoices, @@ -249,11 +248,11 @@ class IPSecProfileImportForm(NetBoxModelImportForm): class Meta: model = IPSecProfile fields = ( - 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', + 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'owner', 'comments', 'tags', ) -class L2VPNImportForm(NetBoxModelImportForm): +class L2VPNImportForm(PrimaryModelBulkImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -273,8 +272,9 @@ class L2VPNImportForm(NetBoxModelImportForm): class Meta: model = L2VPN - fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description', - 'comments', 'tags') + fields = ( + 'identifier', 'name', 'slug', 'tenant', 'type', 'description', 'owner', 'comments', 'tags', + ) class L2VPNTerminationImportForm(NetBoxModelImportForm): diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 29395f81483..a76e12ef61d 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -5,9 +5,9 @@ from dcim.models import Device, Interface, Site from ipam.models import VLAN from netbox.choices import * -from netbox.forms import NetBoxModelImportForm +from netbox.forms import NestedGroupModelBulkImportForm, PrimaryModelBulkImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField from wireless.choices import * from wireless.models import * @@ -18,7 +18,7 @@ ) -class WirelessLANGroupImportForm(NetBoxModelImportForm): +class WirelessLANGroupImportForm(NestedGroupModelBulkImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=WirelessLANGroup.objects.all(), @@ -26,14 +26,13 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Parent group') ) - slug = SlugField() class Meta: model = WirelessLANGroup - fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') + fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm): +class WirelessLANImportForm(ScopedImportForm, PrimaryModelBulkImportForm): group = CSVModelChoiceField( label=_('Group'), queryset=WirelessLANGroup.objects.all(), @@ -77,14 +76,14 @@ class Meta: model = WirelessLAN fields = ( 'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type', - 'scope_id', 'description', 'comments', 'tags', + 'scope_id', 'description', 'owner', 'comments', 'tags', ) labels = { 'scope_id': _('Scope ID'), } -class WirelessLinkImportForm(NetBoxModelImportForm): +class WirelessLinkImportForm(PrimaryModelBulkImportForm): # Termination A site_a = CSVModelChoiceField( label=_('Site A'), @@ -163,7 +162,8 @@ class Meta: model = WirelessLink fields = ( 'site_a', 'device_a', 'interface_a', 'site_b', 'device_b', 'interface_b', 'status', 'ssid', 'tenant', - 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'comments', 'tags', + 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'owner', 'comments', + 'tags', ) def __init__(self, data=None, *args, **kwargs): From cd485a5c91309b95ae97d67591196cd2343ff25e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 16:45:35 -0400 Subject: [PATCH 15/40] Add owner field to all applicable filterset forms --- netbox/circuits/forms/filtersets.py | 18 ++++---- netbox/core/forms/filtersets.py | 4 +- netbox/dcim/forms/filtersets.py | 52 +++++++++++------------ netbox/extras/forms/filtersets.py | 26 ++++++------ netbox/ipam/forms/filtersets.py | 32 +++++++------- netbox/netbox/forms/filtersets.py | 33 ++++++++++---- netbox/tenancy/forms/filtersets.py | 15 ++++--- netbox/virtualization/forms/filtersets.py | 10 ++--- netbox/vpn/forms/filtersets.py | 18 ++++---- netbox/wireless/forms/filtersets.py | 8 ++-- 10 files changed, 118 insertions(+), 98 deletions(-) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 73adb26c5f0..c71f5c65c40 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -9,7 +9,7 @@ from dcim.models import Location, Region, Site, SiteGroup from ipam.models import ASN from netbox.choices import DistanceUnitChoices -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from utilities.forms import add_blank_choice from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField @@ -31,7 +31,7 @@ ) -class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = Provider fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -66,7 +66,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = ProviderAccount fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -85,7 +85,7 @@ class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm tag = TagFilterField(model) -class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): +class ProviderNetworkFilterForm(PrimaryModelFilterSetForm): model = ProviderNetwork fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -104,7 +104,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class CircuitTypeFilterForm(NetBoxModelFilterSetForm): +class CircuitTypeFilterForm(OrganizationalModelFilterSetForm): model = CircuitType fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -118,7 +118,7 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm): ) -class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): +class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = Circuit fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -271,7 +271,7 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): model = CircuitGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -309,7 +309,7 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm): +class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm): model = VirtualCircuitType fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -323,7 +323,7 @@ class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm): ) -class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): +class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = VirtualCircuit fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 40ee399b5bc..f403d838c98 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -3,7 +3,7 @@ from core.choices import * from core.models import * -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from netbox.utils import get_data_backend_choices from users.models import User @@ -23,7 +23,7 @@ ) -class DataSourceFilterForm(NetBoxModelFilterSetForm): +class DataSourceFilterForm(PrimaryModelFilterSetForm): model = DataSource fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 14a51ad78e9..50dff23b40d 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -8,7 +8,10 @@ from extras.models import ConfigTemplate from ipam.models import ASN, VRF, VLANTranslationPolicy from netbox.choices import * -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import ( + NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, + PrimaryModelFilterSetForm, +) from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice @@ -139,7 +142,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): ) -class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): model = Region fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -154,7 +157,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): model = SiteGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -169,7 +172,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): +class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = Site fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -201,7 +204,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte tag = TagFilterField(model) -class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): +class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm): model = Location fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -249,7 +252,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF tag = TagFilterField(model) -class RackRoleFilterForm(NetBoxModelFilterSetForm): +class RackRoleFilterForm(OrganizationalModelFilterSetForm): model = RackRole fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -257,7 +260,7 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class RackBaseFilterForm(NetBoxModelFilterSetForm): +class RackBaseFilterForm(PrimaryModelFilterSetForm): form_factor = forms.MultipleChoiceField( label=_('Form factor'), choices=RackFormFactorChoices, @@ -418,7 +421,7 @@ class RackElevationFilterForm(RackFilterForm): ) -class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = RackReservation fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -476,7 +479,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm): model = Manufacturer fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -485,7 +488,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class DeviceTypeFilterForm(NetBoxModelFilterSetForm): +class DeviceTypeFilterForm(PrimaryModelFilterSetForm): model = DeviceType fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -613,7 +616,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): ) -class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm): +class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm): model = ModuleTypeProfile fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -621,7 +624,7 @@ class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm): selector_fields = ('filter_id', 'q') -class ModuleTypeFilterForm(NetBoxModelFilterSetForm): +class ModuleTypeFilterForm(PrimaryModelFilterSetForm): model = ModuleType fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -706,7 +709,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): ) -class DeviceRoleFilterForm(NetBoxModelFilterSetForm): +class DeviceRoleFilterForm(NestedGroupModelFilterSetForm): model = DeviceRole fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -725,7 +728,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class PlatformFilterForm(NetBoxModelFilterSetForm): +class PlatformFilterForm(NestedGroupModelFilterSetForm): model = Platform fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -754,7 +757,7 @@ class DeviceFilterForm( LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, - NetBoxModelFilterSetForm + PrimaryModelFilterSetForm ): model = Device fieldsets = ( @@ -948,10 +951,7 @@ class DeviceFilterForm( tag = TagFilterField(model) -class VirtualDeviceContextFilterForm( - TenancyFilterForm, - NetBoxModelFilterSetForm -): +class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VirtualDeviceContext fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -978,7 +978,7 @@ class VirtualDeviceContextFilterForm( tag = TagFilterField(model) -class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Module fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1061,7 +1061,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo tag = TagFilterField(model) -class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VirtualChassis fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1090,7 +1090,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = Cable fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1174,7 +1174,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = PowerPanel fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1213,7 +1213,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = PowerFeed fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1676,7 +1676,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): # Device component roles # -class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): +class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm): model = InventoryItemRole fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -1688,7 +1688,7 @@ class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): # Addressing # -class MACAddressFilterForm(NetBoxModelFilterSetForm): +class MACAddressFilterForm(PrimaryModelFilterSetForm): model = MACAddress fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index c542b64042b..72b6587dc67 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -6,8 +6,8 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms import NetBoxModelFilterSetForm -from netbox.forms.mixins import SavedFiltersMixin +from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm +from netbox.forms.mixins import OwnerMixin, SavedFiltersMixin from tenancy.models import Tenant, TenantGroup from users.models import Group, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice @@ -38,7 +38,7 @@ ) -class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): +class CustomFieldFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = CustomField fieldsets = ( FieldSet('q', 'filter_id'), @@ -117,7 +117,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): ) -class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): +class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = CustomFieldChoiceSet fieldsets = ( FieldSet('q', 'filter_id'), @@ -132,7 +132,7 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): ) -class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): +class CustomLinkFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = CustomLink fieldsets = ( FieldSet('q', 'filter_id'), @@ -163,7 +163,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): ) -class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): +class ExportTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = ExportTemplate fieldsets = ( FieldSet('q', 'filter_id', 'object_type_id'), @@ -226,7 +226,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): ) -class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): +class SavedFilterFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = SavedFilter fieldsets = ( FieldSet('q', 'filter_id'), @@ -287,7 +287,7 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm): ) -class WebhookFilterForm(NetBoxModelFilterSetForm): +class WebhookFilterForm(OwnerMixin, NetBoxModelFilterSetForm): model = Webhook fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -309,7 +309,7 @@ class WebhookFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class EventRuleFilterForm(NetBoxModelFilterSetForm): +class EventRuleFilterForm(OwnerMixin, NetBoxModelFilterSetForm): model = EventRule fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -340,7 +340,7 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class TagFilterForm(SavedFiltersMixin, FilterForm): +class TagFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = Tag content_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('tags'), @@ -354,7 +354,7 @@ class TagFilterForm(SavedFiltersMixin, FilterForm): ) -class ConfigContextProfileFilterForm(SavedFiltersMixin, FilterForm): +class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): model = ConfigContextProfile fieldsets = ( FieldSet('q', 'filter_id'), @@ -375,7 +375,7 @@ class ConfigContextProfileFilterForm(SavedFiltersMixin, FilterForm): ) -class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): +class ConfigContextFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = ConfigContext fieldsets = ( FieldSet('q', 'filter_id', 'tag_id'), @@ -471,7 +471,7 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): ) -class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): +class ConfigTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): model = ConfigTemplate fieldsets = ( FieldSet('q', 'filter_id', 'tag'), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index cbded745048..85b0f825dd7 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -5,7 +5,7 @@ from ipam.choices import * from ipam.constants import * from ipam.models import * -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField @@ -42,7 +42,7 @@ ]) -class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VRF fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -62,7 +62,7 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = RouteTarget fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -82,7 +82,7 @@ class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class RIRFilterForm(NetBoxModelFilterSetForm): +class RIRFilterForm(OrganizationalModelFilterSetForm): model = RIR fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -98,7 +98,7 @@ class RIRFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Aggregate fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -119,7 +119,7 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel tag = TagFilterField(model) -class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): model = ASNRange fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -142,7 +142,7 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = ASN fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -167,7 +167,7 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class RoleFilterForm(NetBoxModelFilterSetForm): +class RoleFilterForm(OrganizationalModelFilterSetForm): model = Role fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -175,7 +175,7 @@ class RoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm, ): +class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Prefix fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -281,7 +281,7 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFil tag = TagFilterField(model) -class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = IPRange fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -328,7 +328,7 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFi tag = TagFilterField(model) -class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = IPAddress fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -406,7 +406,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel tag = TagFilterField(model) -class FHRPGroupFilterForm(NetBoxModelFilterSetForm): +class FHRPGroupFilterForm(PrimaryModelFilterSetForm): model = FHRPGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -439,7 +439,7 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')), @@ -492,7 +492,7 @@ class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm): +class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm): model = VLANTranslationPolicy fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -529,7 +529,7 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm): ) -class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VLAN fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -601,7 +601,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): +class ServiceTemplateFilterForm(PrimaryModelFilterSetForm): model = ServiceTemplate fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/netbox/forms/filtersets.py b/netbox/netbox/forms/filtersets.py index fb4e496c845..5d0a84b5323 100644 --- a/netbox/netbox/forms/filtersets.py +++ b/netbox/netbox/forms/filtersets.py @@ -3,12 +3,13 @@ from django.utils.translation import gettext_lazy as _ from extras.choices import * -from users.models import Owner -from utilities.forms.fields import DynamicModelMultipleChoiceField -from .mixins import CustomFieldsMixin, SavedFiltersMixin +from .mixins import CustomFieldsMixin, OwnerMixin, SavedFiltersMixin __all__ = ( + 'NestedGroupModelFilterSetForm', 'NetBoxModelFilterSetForm', + 'OrganizationalModelFilterSetForm', + 'PrimaryModelFilterSetForm', ) @@ -28,11 +29,6 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form) required=False, label=_('Search') ) - owner_id = DynamicModelMultipleChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) selector_fields = ('filter_id', 'q') @@ -44,3 +40,24 @@ def _get_custom_fields(self, content_type): def _get_form_field(self, customfield): return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False) + + +class PrimaryModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): + """ + FilterSet form for models which inherit from PrimaryModel. + """ + pass + + +class OrganizationalModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): + """ + FilterSet form for models which inherit from OrganizationalModel. + """ + pass + + +class NestedGroupModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): + """ + FilterSet form for models which inherit from NestedGroupModel. + """ + pass diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 90dca58484a..239a765c6e6 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -2,7 +2,10 @@ from django.utils.translation import gettext_lazy as _ from core.models import ObjectType -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import ( + NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, + PrimaryModelFilterSetForm, +) from tenancy.choices import * from tenancy.models import * from tenancy.forms import ContactModelFilterForm @@ -25,7 +28,7 @@ # Tenants # -class TenantGroupFilterForm(NetBoxModelFilterSetForm): +class TenantGroupFilterForm(NestedGroupModelFilterSetForm): model = TenantGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -39,7 +42,7 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class TenantFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = Tenant fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -59,7 +62,7 @@ class TenantFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): # Contacts # -class ContactGroupFilterForm(NetBoxModelFilterSetForm): +class ContactGroupFilterForm(NestedGroupModelFilterSetForm): model = ContactGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -73,7 +76,7 @@ class ContactGroupFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ContactRoleFilterForm(NetBoxModelFilterSetForm): +class ContactRoleFilterForm(OrganizationalModelFilterSetForm): model = ContactRole fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -81,7 +84,7 @@ class ContactRoleFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ContactFilterForm(NetBoxModelFilterSetForm): +class ContactFilterForm(PrimaryModelFilterSetForm): model = Contact fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 43f10b09e3a..6741499c081 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -6,7 +6,7 @@ from extras.forms import LocalConfigContextFilterForm from extras.models import ConfigTemplate from ipam.models import VRF, VLANTranslationPolicy -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField @@ -25,7 +25,7 @@ ) -class ClusterTypeFilterForm(NetBoxModelFilterSetForm): +class ClusterTypeFilterForm(OrganizationalModelFilterSetForm): model = ClusterType fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -33,7 +33,7 @@ class ClusterTypeFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class ClusterGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm): model = ClusterGroup tag = TagFilterField(model) fieldsets = ( @@ -42,7 +42,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): ) -class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm): +class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = Cluster fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -100,7 +100,7 @@ class VirtualMachineFilterForm( LocalConfigContextFilterForm, TenancyFilterForm, ContactModelFilterForm, - NetBoxModelFilterSetForm + PrimaryModelFilterSetForm ): model = VirtualMachine fieldsets = ( diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py index 6a4d91a0cae..4085d9ac5f1 100644 --- a/netbox/vpn/forms/filtersets.py +++ b/netbox/vpn/forms/filtersets.py @@ -4,7 +4,7 @@ from dcim.models import Device, Region, Site from ipam.models import RouteTarget, VLAN -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms.fields import ( ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, @@ -30,7 +30,7 @@ ) -class TunnelGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): +class TunnelGroupFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm): model = TunnelGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -39,7 +39,7 @@ class TunnelGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class TunnelFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Tunnel fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -94,7 +94,7 @@ class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class IKEProposalFilterForm(NetBoxModelFilterSetForm): +class IKEProposalFilterForm(PrimaryModelFilterSetForm): model = IKEProposal fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -125,7 +125,7 @@ class IKEProposalFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class IKEPolicyFilterForm(NetBoxModelFilterSetForm): +class IKEPolicyFilterForm(PrimaryModelFilterSetForm): model = IKEPolicy fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -149,7 +149,7 @@ class IKEPolicyFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class IPSecProposalFilterForm(NetBoxModelFilterSetForm): +class IPSecProposalFilterForm(PrimaryModelFilterSetForm): model = IPSecProposal fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -168,7 +168,7 @@ class IPSecProposalFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): +class IPSecPolicyFilterForm(PrimaryModelFilterSetForm): model = IPSecPolicy fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -187,7 +187,7 @@ class IPSecPolicyFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class IPSecProfileFilterForm(NetBoxModelFilterSetForm): +class IPSecProfileFilterForm(PrimaryModelFilterSetForm): model = IPSecProfile fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -211,7 +211,7 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): +class L2VPNFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = L2VPN fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 4311eb2f71a..171a7d8b6ed 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices from dcim.models import Location, Region, Site, SiteGroup from netbox.choices import * -from netbox.forms import NetBoxModelFilterSetForm +from netbox.forms import NestedGroupModelFilterSetForm, PrimaryModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import add_blank_choice from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField @@ -19,7 +19,7 @@ ) -class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): +class WirelessLANGroupFilterForm(NestedGroupModelFilterSetForm): model = WirelessLANGroup fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -33,7 +33,7 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class WirelessLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = WirelessLAN fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -99,7 +99,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) -class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): +class WirelessLinkFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = WirelessLink fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), From 4dda9686873ce4ad471f4481e21c3e5564fd5806 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Oct 2025 16:59:53 -0400 Subject: [PATCH 16/40] Update forms for device & VM components --- netbox/dcim/forms/bulk_edit.py | 4 +- netbox/dcim/forms/bulk_import.py | 43 ++++++++++++---------- netbox/dcim/forms/filtersets.py | 3 +- netbox/dcim/forms/model_forms.py | 4 +- netbox/virtualization/forms/bulk_edit.py | 5 ++- netbox/virtualization/forms/bulk_import.py | 12 +++--- netbox/virtualization/forms/filtersets.py | 5 ++- netbox/virtualization/forms/model_forms.py | 3 +- 8 files changed, 44 insertions(+), 35 deletions(-) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index cfc6140736a..eb3659fc2ea 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -13,7 +13,7 @@ from netbox.forms import ( NestedGroupModelBulkEditForm, NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm, ) -from netbox.forms.mixins import ChangelogMessageMixin +from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin from tenancy.models import Tenant from users.models import User from utilities.forms import BulkEditForm, add_blank_choice, form_from_model @@ -1246,7 +1246,7 @@ class InventoryItemTemplateBulkEditForm(ComponentTemplateBulkEditForm): # Device components # -class ComponentBulkEditForm(NetBoxModelBulkEditForm): +class ComponentBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): device = forms.ModelChoiceField( label=_('Device'), queryset=Device.objects.all(), diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 65abd9502c3..726c2551857 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -12,7 +12,7 @@ from ipam.models import VRF, IPAddress from netbox.choices import * from netbox.forms import ( - NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, + NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, OwnerCSVMixin, PrimaryModelBulkImportForm, ) from tenancy.models import Tenant @@ -779,7 +779,7 @@ def clean_replicate_components(self): # Device components # -class ConsolePortImportForm(NetBoxModelImportForm): +class ConsolePortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -802,10 +802,10 @@ class ConsolePortImportForm(NetBoxModelImportForm): class Meta: model = ConsolePort - fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags') + fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags') -class ConsoleServerPortImportForm(NetBoxModelImportForm): +class ConsoleServerPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -828,10 +828,10 @@ class ConsoleServerPortImportForm(NetBoxModelImportForm): class Meta: model = ConsoleServerPort - fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags') + fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags') -class PowerPortImportForm(NetBoxModelImportForm): +class PowerPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -847,11 +847,12 @@ class PowerPortImportForm(NetBoxModelImportForm): class Meta: model = PowerPort fields = ( - 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags' + 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', + 'owner', 'tags', ) -class PowerOutletImportForm(NetBoxModelImportForm): +class PowerOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -881,7 +882,7 @@ class Meta: model = PowerOutlet fields = ( 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description', - 'tags', + 'owner', 'tags', ) def __init__(self, *args, **kwargs): @@ -907,7 +908,7 @@ def __init__(self, *args, **kwargs): self.fields['power_port'].queryset = PowerPort.objects.none() -class InterfaceImportForm(NetBoxModelImportForm): +class InterfaceImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -990,7 +991,7 @@ class Meta: fields = ( 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode', - 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags' + 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'owner', 'tags' ) def __init__(self, data=None, *args, **kwargs): @@ -1025,7 +1026,7 @@ def clean_vdcs(self): return self.cleaned_data['vdcs'] -class FrontPortImportForm(NetBoxModelImportForm): +class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1047,7 +1048,7 @@ class Meta: model = FrontPort fields = ( 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position', - 'description', 'tags' + 'description', 'owner', 'tags' ) def __init__(self, *args, **kwargs): @@ -1073,7 +1074,7 @@ def __init__(self, *args, **kwargs): self.fields['rear_port'].queryset = RearPort.objects.none() -class RearPortImportForm(NetBoxModelImportForm): +class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1087,10 +1088,12 @@ class RearPortImportForm(NetBoxModelImportForm): class Meta: model = RearPort - fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags') + fields = ( + 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'owner', 'tags', + ) -class ModuleBayImportForm(NetBoxModelImportForm): +class ModuleBayImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1099,10 +1102,10 @@ class ModuleBayImportForm(NetBoxModelImportForm): class Meta: model = ModuleBay - fields = ('device', 'name', 'label', 'position', 'description', 'tags') + fields = ('device', 'name', 'label', 'position', 'description', 'owner', 'tags') -class DeviceBayImportForm(NetBoxModelImportForm): +class DeviceBayImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1121,7 +1124,7 @@ class DeviceBayImportForm(NetBoxModelImportForm): class Meta: model = DeviceBay - fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags') + fields = ('device', 'name', 'label', 'installed_device', 'description', 'owner', 'tags') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1150,7 +1153,7 @@ def __init__(self, *args, **kwargs): self.fields['installed_device'].queryset = Device.objects.none() -class InventoryItemImportForm(OrganizationalModelBulkImportForm): +class InventoryItemImportForm(OwnerCSVMixin, NetBoxModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 50dff23b40d..4d37460fd8e 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,6 +12,7 @@ NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm, ) +from netbox.forms.mixins import OwnerMixin from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice @@ -63,7 +64,7 @@ ) -class DeviceComponentFilterForm(NetBoxModelFilterSetForm): +class DeviceComponentFilterForm(OwnerMixin, NetBoxModelFilterSetForm): name = forms.CharField( label=_('Name'), required=False diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index a7af27e1d41..2d78d44e654 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -11,7 +11,7 @@ from ipam.choices import VLANQinQRoleChoices from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm -from netbox.forms.mixins import ChangelogMessageMixin +from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin from tenancy.forms import TenancyForm from users.models import User from utilities.forms import add_blank_choice, get_field_value @@ -1334,7 +1334,7 @@ def clean(self): # Device components # -class DeviceComponentForm(NetBoxModelForm): +class DeviceComponentForm(OwnerMixin, NetBoxModelForm): device = DynamicModelChoiceField( label=_('Device'), queryset=Device.objects.all(), diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index d72d7c2bed9..092bf576b0a 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -8,6 +8,7 @@ from extras.models import ConfigTemplate from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VRF from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm +from netbox.forms.mixins import OwnerMixin from tenancy.models import Tenant from utilities.forms import BulkRenameForm, add_blank_choice from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField @@ -153,7 +154,7 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): ) -class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): +class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): virtual_machine = forms.ModelChoiceField( label=_('Virtual machine'), queryset=VirtualMachine.objects.all(), @@ -287,7 +288,7 @@ class VMInterfaceBulkRenameForm(BulkRenameForm): ) -class VirtualDiskBulkEditForm(NetBoxModelBulkEditForm): +class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): virtual_machine = forms.ModelChoiceField( label=_('Virtual machine'), queryset=VirtualMachine.objects.all(), diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index c058454b5a2..e64ba4905a4 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -5,7 +5,9 @@ from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate from ipam.models import VRF -from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm +from netbox.forms import ( + NetBoxModelImportForm, OrganizationalModelBulkImportForm, OwnerCSVMixin, PrimaryModelBulkImportForm, +) from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField from virtualization.choices import * @@ -146,7 +148,7 @@ class Meta: ) -class VMInterfaceImportForm(NetBoxModelImportForm): +class VMInterfaceImportForm(OwnerCSVMixin, NetBoxModelImportForm): virtual_machine = CSVModelChoiceField( label=_('Virtual machine'), queryset=VirtualMachine.objects.all(), @@ -184,7 +186,7 @@ class Meta: model = VMInterface fields = ( 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode', - 'vrf', 'tags' + 'vrf', 'owner', 'tags' ) def __init__(self, data=None, *args, **kwargs): @@ -207,7 +209,7 @@ def clean_enabled(self): return self.cleaned_data['enabled'] -class VirtualDiskImportForm(NetBoxModelImportForm): +class VirtualDiskImportForm(OwnerCSVMixin, NetBoxModelImportForm): virtual_machine = CSVModelChoiceField( label=_('Virtual machine'), queryset=VirtualMachine.objects.all(), @@ -217,5 +219,5 @@ class VirtualDiskImportForm(NetBoxModelImportForm): class Meta: model = VirtualDisk fields = ( - 'virtual_machine', 'name', 'size', 'description', 'tags' + 'virtual_machine', 'name', 'size', 'description', 'owner', 'tags' ) diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 6741499c081..1b390307519 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -7,6 +7,7 @@ from extras.models import ConfigTemplate from ipam.models import VRF, VLANTranslationPolicy from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm +from netbox.forms.mixins import OwnerMixin from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField @@ -199,7 +200,7 @@ class VirtualMachineFilterForm( tag = TagFilterField(model) -class VMInterfaceFilterForm(NetBoxModelFilterSetForm): +class VMInterfaceFilterForm(OwnerMixin, NetBoxModelFilterSetForm): model = VMInterface fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -256,7 +257,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): tag = TagFilterField(model) -class VirtualDiskFilterForm(NetBoxModelFilterSetForm): +class VirtualDiskFilterForm(OwnerMixin, NetBoxModelFilterSetForm): model = VirtualDisk fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 9b4c21783e1..fa4966b2b3b 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -11,6 +11,7 @@ from ipam.choices import VLANQinQRoleChoices from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm +from netbox.forms.mixins import OwnerMixin from tenancy.forms import TenancyForm from utilities.forms import ConfirmationForm from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField @@ -280,7 +281,7 @@ def __init__(self, *args, **kwargs): # Virtual machine components # -class VMComponentForm(NetBoxModelForm): +class VMComponentForm(OwnerMixin, NetBoxModelForm): virtual_machine = DynamicModelChoiceField( label=_('Virtual machine'), queryset=VirtualMachine.objects.all(), From c3144ddb6a5cda8bcd6691f81dee42fcb81a60fc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 09:15:57 -0400 Subject: [PATCH 17/40] Fix base form classes --- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 726c2551857..1753ec34321 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1263,7 +1263,7 @@ def clean(self): # Device component roles # -class InventoryItemRoleImportForm(NetBoxModelImportForm): +class InventoryItemRoleImportForm(OrganizationalModelBulkImportForm): slug = SlugField() class Meta: diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 2d78d44e654..7baab4c7a7b 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -307,7 +307,7 @@ def __init__(self, *args, **kwargs): ) -class RackReservationForm(TenancyForm, NetBoxModelForm): +class RackReservationForm(TenancyForm, PrimaryModelForm): rack = DynamicModelChoiceField( label=_('Rack'), queryset=Rack.objects.all(), From dd6c9853007739e0c2c544c34d9fea61b499a5dd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 09:16:56 -0400 Subject: [PATCH 18/40] Rename bulk import form base classes --- netbox/circuits/forms/bulk_import.py | 18 ++++---- netbox/core/forms/bulk_import.py | 4 +- netbox/dcim/forms/bulk_import.py | 50 +++++++++++----------- netbox/extras/forms/bulk_import.py | 4 +- netbox/ipam/forms/bulk_import.py | 34 +++++++-------- netbox/netbox/forms/bulk_import.py | 12 +++--- netbox/tenancy/forms/bulk_import.py | 14 +++--- netbox/virtualization/forms/bulk_import.py | 10 ++--- netbox/vpn/forms/bulk_import.py | 18 ++++---- netbox/wireless/forms/bulk_import.py | 8 ++-- 10 files changed, 86 insertions(+), 86 deletions(-) diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 4f529598266..22b7a159c6c 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -7,7 +7,7 @@ from circuits.models import * from dcim.models import Interface from netbox.choices import DistanceUnitChoices -from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField @@ -28,7 +28,7 @@ ) -class ProviderImportForm(PrimaryModelBulkImportForm): +class ProviderImportForm(PrimaryModelImportForm): slug = SlugField() class Meta: @@ -38,7 +38,7 @@ class Meta: ) -class ProviderAccountImportForm(PrimaryModelBulkImportForm): +class ProviderAccountImportForm(PrimaryModelImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -53,7 +53,7 @@ class Meta: ) -class ProviderNetworkImportForm(PrimaryModelBulkImportForm): +class ProviderNetworkImportForm(PrimaryModelImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -68,7 +68,7 @@ class Meta: ] -class CircuitTypeImportForm(OrganizationalModelBulkImportForm): +class CircuitTypeImportForm(OrganizationalModelImportForm): slug = SlugField() class Meta: @@ -76,7 +76,7 @@ class Meta: fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class CircuitImportForm(PrimaryModelBulkImportForm): +class CircuitImportForm(PrimaryModelImportForm): provider = CSVModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), @@ -165,7 +165,7 @@ class Meta: } -class CircuitGroupImportForm(OrganizationalModelBulkImportForm): +class CircuitGroupImportForm(OrganizationalModelImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -195,14 +195,14 @@ class Meta: fields = ('member_type', 'member_id', 'group', 'priority') -class VirtualCircuitTypeImportForm(OrganizationalModelBulkImportForm): +class VirtualCircuitTypeImportForm(OrganizationalModelImportForm): class Meta: model = VirtualCircuitType fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class VirtualCircuitImportForm(PrimaryModelBulkImportForm): +class VirtualCircuitImportForm(PrimaryModelImportForm): provider_network = CSVModelChoiceField( label=_('Provider network'), queryset=ProviderNetwork.objects.all(), diff --git a/netbox/core/forms/bulk_import.py b/netbox/core/forms/bulk_import.py index c9311a61df6..b04e705e39a 100644 --- a/netbox/core/forms/bulk_import.py +++ b/netbox/core/forms/bulk_import.py @@ -1,12 +1,12 @@ from core.models import * -from netbox.forms import PrimaryModelBulkImportForm +from netbox.forms import PrimaryModelImportForm __all__ = ( 'DataSourceImportForm', ) -class DataSourceImportForm(PrimaryModelBulkImportForm): +class DataSourceImportForm(PrimaryModelImportForm): class Meta: model = DataSource diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 1753ec34321..68ff356b07e 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -12,8 +12,8 @@ from ipam.models import VRF, IPAddress from netbox.choices import * from netbox.forms import ( - NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, OwnerCSVMixin, - PrimaryModelBulkImportForm, + NestedGroupModelImportForm, NetBoxModelImportForm, OrganizationalModelImportForm, OwnerCSVMixin, + PrimaryModelImportForm, ) from tenancy.models import Tenant from utilities.forms.fields import ( @@ -61,7 +61,7 @@ ) -class RegionImportForm(NestedGroupModelBulkImportForm): +class RegionImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=Region.objects.all(), @@ -75,7 +75,7 @@ class Meta: fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class SiteGroupImportForm(NestedGroupModelBulkImportForm): +class SiteGroupImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=SiteGroup.objects.all(), @@ -89,7 +89,7 @@ class Meta: fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class SiteImportForm(PrimaryModelBulkImportForm): +class SiteImportForm(PrimaryModelImportForm): status = CSVChoiceField( label=_('Status'), choices=SiteStatusChoices, @@ -132,7 +132,7 @@ class Meta: } -class LocationImportForm(NestedGroupModelBulkImportForm): +class LocationImportForm(NestedGroupModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -178,14 +178,14 @@ def __init__(self, data=None, *args, **kwargs): self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params) -class RackRoleImportForm(OrganizationalModelBulkImportForm): +class RackRoleImportForm(OrganizationalModelImportForm): class Meta: model = RackRole fields = ('name', 'slug', 'color', 'description', 'owner', 'tags') -class RackTypeImportForm(PrimaryModelBulkImportForm): +class RackTypeImportForm(PrimaryModelImportForm): manufacturer = forms.ModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -233,7 +233,7 @@ def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) -class RackImportForm(PrimaryModelBulkImportForm): +class RackImportForm(PrimaryModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -335,7 +335,7 @@ def clean(self): raise forms.ValidationError(_("U height must be set if not specifying a rack type.")) -class RackReservationImportForm(PrimaryModelBulkImportForm): +class RackReservationImportForm(PrimaryModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -395,14 +395,14 @@ def __init__(self, data=None, *args, **kwargs): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class ManufacturerImportForm(OrganizationalModelBulkImportForm): +class ManufacturerImportForm(OrganizationalModelImportForm): class Meta: model = Manufacturer fields = ('name', 'slug', 'description', 'owner', 'tags') -class DeviceTypeImportForm(PrimaryModelBulkImportForm): +class DeviceTypeImportForm(PrimaryModelImportForm): manufacturer = CSVModelChoiceField( label=_('Manufacturer'), queryset=Manufacturer.objects.all(), @@ -437,7 +437,7 @@ class Meta: ] -class ModuleTypeProfileImportForm(PrimaryModelBulkImportForm): +class ModuleTypeProfileImportForm(PrimaryModelImportForm): class Meta: model = ModuleTypeProfile @@ -446,7 +446,7 @@ class Meta: ] -class ModuleTypeImportForm(PrimaryModelBulkImportForm): +class ModuleTypeImportForm(PrimaryModelImportForm): profile = forms.ModelChoiceField( label=_('Profile'), queryset=ModuleTypeProfile.objects.all(), @@ -484,7 +484,7 @@ class Meta: ] -class DeviceRoleImportForm(NestedGroupModelBulkImportForm): +class DeviceRoleImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=DeviceRole.objects.all(), @@ -510,7 +510,7 @@ class Meta: ) -class PlatformImportForm(NestedGroupModelBulkImportForm): +class PlatformImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=Platform.objects.all(), @@ -543,7 +543,7 @@ class Meta: ) -class BaseDeviceImportForm(PrimaryModelBulkImportForm): +class BaseDeviceImportForm(PrimaryModelImportForm): role = CSVModelChoiceField( label=_('Device role'), queryset=DeviceRole.objects.all(), @@ -717,7 +717,7 @@ def clean(self): self.instance.parent_bay = device_bay -class ModuleImportForm(ModuleCommonForm, PrimaryModelBulkImportForm): +class ModuleImportForm(ModuleCommonForm, PrimaryModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1263,7 +1263,7 @@ def clean(self): # Device component roles # -class InventoryItemRoleImportForm(OrganizationalModelBulkImportForm): +class InventoryItemRoleImportForm(OrganizationalModelImportForm): slug = SlugField() class Meta: @@ -1275,7 +1275,7 @@ class Meta: # Addressing # -class MACAddressImportForm(PrimaryModelBulkImportForm): +class MACAddressImportForm(PrimaryModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), @@ -1360,7 +1360,7 @@ def save(self, *args, **kwargs): # Cables # -class CableImportForm(PrimaryModelBulkImportForm): +class CableImportForm(PrimaryModelImportForm): # Termination A side_a_site = CSVModelChoiceField( label=_('Side A site'), @@ -1543,7 +1543,7 @@ def clean_color(self): # -class VirtualChassisImportForm(PrimaryModelBulkImportForm): +class VirtualChassisImportForm(PrimaryModelImportForm): master = CSVModelChoiceField( label=_('Master'), queryset=Device.objects.all(), @@ -1561,7 +1561,7 @@ class Meta: # Power # -class PowerPanelImportForm(PrimaryModelBulkImportForm): +class PowerPanelImportForm(PrimaryModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -1589,7 +1589,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) -class PowerFeedImportForm(PrimaryModelBulkImportForm): +class PowerFeedImportForm(PrimaryModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -1671,7 +1671,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class VirtualDeviceContextImportForm(PrimaryModelBulkImportForm): +class VirtualDeviceContextImportForm(PrimaryModelImportForm): device = CSVModelChoiceField( label=_('Device'), queryset=Device.objects.all(), diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 8b6420b4c6a..ff7f97caf60 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -9,7 +9,7 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices -from netbox.forms import NetBoxModelImportForm, OwnerCSVMixin, PrimaryModelBulkImportForm +from netbox.forms import NetBoxModelImportForm, OwnerCSVMixin, PrimaryModelImportForm from users.models import Group, User from utilities.forms import CSVModelForm from utilities.forms.fields import ( @@ -150,7 +150,7 @@ class Meta: ) -class ConfigContextProfileImportForm(PrimaryModelBulkImportForm): +class ConfigContextProfileImportForm(PrimaryModelImportForm): class Meta: model = ConfigContextProfile diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index f2db5a52b5f..9b212db79fc 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -7,7 +7,7 @@ from ipam.choices import * from ipam.constants import * from ipam.models import * -from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm from tenancy.models import Tenant from utilities.forms.fields import ( CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField, @@ -36,7 +36,7 @@ ) -class VRFImportForm(PrimaryModelBulkImportForm): +class VRFImportForm(PrimaryModelImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -65,7 +65,7 @@ class Meta: ) -class RouteTargetImportForm(PrimaryModelBulkImportForm): +class RouteTargetImportForm(PrimaryModelImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -79,7 +79,7 @@ class Meta: fields = ('name', 'tenant', 'description', 'owner', 'comments', 'tags') -class RIRImportForm(OrganizationalModelBulkImportForm): +class RIRImportForm(OrganizationalModelImportForm): slug = SlugField() class Meta: @@ -87,7 +87,7 @@ class Meta: fields = ('name', 'slug', 'is_private', 'description', 'owner', 'tags') -class AggregateImportForm(PrimaryModelBulkImportForm): +class AggregateImportForm(PrimaryModelImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -107,7 +107,7 @@ class Meta: fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'owner', 'comments', 'tags') -class ASNRangeImportForm(OrganizationalModelBulkImportForm): +class ASNRangeImportForm(OrganizationalModelImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -127,7 +127,7 @@ class Meta: fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'owner', 'tags') -class ASNImportForm(PrimaryModelBulkImportForm): +class ASNImportForm(PrimaryModelImportForm): rir = CSVModelChoiceField( label=_('RIR'), queryset=RIR.objects.all(), @@ -147,14 +147,14 @@ class Meta: fields = ('asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags') -class RoleImportForm(OrganizationalModelBulkImportForm): +class RoleImportForm(OrganizationalModelImportForm): class Meta: model = Role fields = ('name', 'slug', 'weight', 'description', 'owner', 'tags') -class PrefixImportForm(ScopedImportForm, PrimaryModelBulkImportForm): +class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -243,7 +243,7 @@ def __init__(self, data=None, *args, **kwargs): self.fields['vlan'].queryset = queryset -class IPRangeImportForm(PrimaryModelBulkImportForm): +class IPRangeImportForm(PrimaryModelImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -279,7 +279,7 @@ class Meta: ) -class IPAddressImportForm(PrimaryModelBulkImportForm): +class IPAddressImportForm(PrimaryModelImportForm): vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -427,7 +427,7 @@ def save(self, *args, **kwargs): return ipaddress -class FHRPGroupImportForm(PrimaryModelBulkImportForm): +class FHRPGroupImportForm(PrimaryModelImportForm): protocol = CSVChoiceField( label=_('Protocol'), choices=FHRPGroupProtocolChoices @@ -443,7 +443,7 @@ class Meta: fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'owner', 'comments', 'tags') -class VLANGroupImportForm(OrganizationalModelBulkImportForm): +class VLANGroupImportForm(OrganizationalModelImportForm): scope_type = CSVContentTypeField( queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), required=False, @@ -468,7 +468,7 @@ class Meta: } -class VLANImportForm(PrimaryModelBulkImportForm): +class VLANImportForm(PrimaryModelImportForm): site = CSVModelChoiceField( label=_('Site'), queryset=Site.objects.all(), @@ -524,7 +524,7 @@ class Meta: ) -class VLANTranslationPolicyImportForm(PrimaryModelBulkImportForm): +class VLANTranslationPolicyImportForm(PrimaryModelImportForm): class Meta: model = VLANTranslationPolicy @@ -544,7 +544,7 @@ class Meta: fields = ('policy', 'local_vid', 'remote_vid') -class ServiceTemplateImportForm(PrimaryModelBulkImportForm): +class ServiceTemplateImportForm(PrimaryModelImportForm): protocol = CSVChoiceField( label=_('Protocol'), choices=ServiceProtocolChoices, @@ -556,7 +556,7 @@ class Meta: fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags') -class ServiceImportForm(PrimaryModelBulkImportForm): +class ServiceImportForm(PrimaryModelImportForm): parent_object_type = CSVContentTypeField( queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS), required=True, diff --git a/netbox/netbox/forms/bulk_import.py b/netbox/netbox/forms/bulk_import.py index 062a8e7886f..b01429a906d 100644 --- a/netbox/netbox/forms/bulk_import.py +++ b/netbox/netbox/forms/bulk_import.py @@ -9,11 +9,11 @@ from .model_forms import NetBoxModelForm __all__ = ( - 'NestedGroupModelBulkImportForm', + 'NestedGroupModelImportForm', 'NetBoxModelImportForm', - 'OrganizationalModelBulkImportForm', + 'OrganizationalModelImportForm', 'OwnerCSVMixin', - 'PrimaryModelBulkImportForm' + 'PrimaryModelImportForm' ) @@ -48,21 +48,21 @@ class OwnerCSVMixin(forms.Form): ) -class PrimaryModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): +class PrimaryModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): """ Bulk import form for models which inherit from PrimaryModel. """ pass -class OrganizationalModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): +class OrganizationalModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): """ Bulk import form for models which inherit from OrganizationalModel. """ slug = SlugField() -class NestedGroupModelBulkImportForm(OwnerCSVMixin, NetBoxModelImportForm): +class NestedGroupModelImportForm(OwnerCSVMixin, NetBoxModelImportForm): """ Bulk import form for models which inherit from NestedGroupModel. """ diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index 81729dc7bf6..5f9df428e6c 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -3,8 +3,8 @@ from django.utils.translation import gettext_lazy as _ from netbox.forms import ( - NestedGroupModelBulkImportForm, NetBoxModelImportForm, OrganizationalModelBulkImportForm, - PrimaryModelBulkImportForm, + NestedGroupModelImportForm, NetBoxModelImportForm, OrganizationalModelImportForm, + PrimaryModelImportForm, ) from tenancy.models import * from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField @@ -23,7 +23,7 @@ # Tenants # -class TenantGroupImportForm(NestedGroupModelBulkImportForm): +class TenantGroupImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=TenantGroup.objects.all(), @@ -37,7 +37,7 @@ class Meta: fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class TenantImportForm(PrimaryModelBulkImportForm): +class TenantImportForm(PrimaryModelImportForm): slug = SlugField() group = CSVModelChoiceField( label=_('Group'), @@ -56,7 +56,7 @@ class Meta: # Contacts # -class ContactGroupImportForm(NestedGroupModelBulkImportForm): +class ContactGroupImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=ContactGroup.objects.all(), @@ -70,14 +70,14 @@ class Meta: fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class ContactRoleImportForm(OrganizationalModelBulkImportForm): +class ContactRoleImportForm(OrganizationalModelImportForm): class Meta: model = ContactRole fields = ('name', 'slug', 'description', 'owner', 'tags') -class ContactImportForm(PrimaryModelBulkImportForm): +class ContactImportForm(PrimaryModelImportForm): groups = CSVModelMultipleChoiceField( queryset=ContactGroup.objects.all(), required=False, diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index e64ba4905a4..67f39b6f531 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -6,7 +6,7 @@ from extras.models import ConfigTemplate from ipam.models import VRF from netbox.forms import ( - NetBoxModelImportForm, OrganizationalModelBulkImportForm, OwnerCSVMixin, PrimaryModelBulkImportForm, + NetBoxModelImportForm, OrganizationalModelImportForm, OwnerCSVMixin, PrimaryModelImportForm, ) from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField @@ -23,21 +23,21 @@ ) -class ClusterTypeImportForm(OrganizationalModelBulkImportForm): +class ClusterTypeImportForm(OrganizationalModelImportForm): class Meta: model = ClusterType fields = ('name', 'slug', 'description', 'owner', 'tags') -class ClusterGroupImportForm(OrganizationalModelBulkImportForm): +class ClusterGroupImportForm(OrganizationalModelImportForm): class Meta: model = ClusterGroup fields = ('name', 'slug', 'description', 'owner', 'tags') -class ClusterImportForm(ScopedImportForm, PrimaryModelBulkImportForm): +class ClusterImportForm(ScopedImportForm, PrimaryModelImportForm): type = CSVModelChoiceField( label=_('Type'), queryset=ClusterType.objects.all(), @@ -82,7 +82,7 @@ class Meta: } -class VirtualMachineImportForm(PrimaryModelBulkImportForm): +class VirtualMachineImportForm(PrimaryModelImportForm): status = CSVChoiceField( label=_('Status'), choices=VirtualMachineStatusChoices, diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py index 795431fda76..1b6769fad3d 100644 --- a/netbox/vpn/forms/bulk_import.py +++ b/netbox/vpn/forms/bulk_import.py @@ -3,7 +3,7 @@ from dcim.models import Device, Interface from ipam.models import IPAddress, VLAN -from netbox.forms import NetBoxModelImportForm, OrganizationalModelBulkImportForm, PrimaryModelBulkImportForm +from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField from virtualization.models import VirtualMachine, VMInterface @@ -24,14 +24,14 @@ ) -class TunnelGroupImportForm(OrganizationalModelBulkImportForm): +class TunnelGroupImportForm(OrganizationalModelImportForm): class Meta: model = TunnelGroup fields = ('name', 'slug', 'description', 'owner', 'tags') -class TunnelImportForm(PrimaryModelBulkImportForm): +class TunnelImportForm(PrimaryModelImportForm): status = CSVChoiceField( label=_('Status'), choices=TunnelStatusChoices, @@ -139,7 +139,7 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) -class IKEProposalImportForm(PrimaryModelBulkImportForm): +class IKEProposalImportForm(PrimaryModelImportForm): authentication_method = CSVChoiceField( label=_('Authentication method'), choices=AuthenticationMethodChoices @@ -166,7 +166,7 @@ class Meta: ) -class IKEPolicyImportForm(PrimaryModelBulkImportForm): +class IKEPolicyImportForm(PrimaryModelImportForm): version = CSVChoiceField( label=_('Version'), choices=IKEVersionChoices @@ -189,7 +189,7 @@ class Meta: ) -class IPSecProposalImportForm(PrimaryModelBulkImportForm): +class IPSecProposalImportForm(PrimaryModelImportForm): encryption_algorithm = CSVChoiceField( label=_('Encryption algorithm'), choices=EncryptionAlgorithmChoices, @@ -209,7 +209,7 @@ class Meta: ) -class IPSecPolicyImportForm(PrimaryModelBulkImportForm): +class IPSecPolicyImportForm(PrimaryModelImportForm): pfs_group = CSVChoiceField( label=_('Diffie-Hellman group for Perfect Forward Secrecy'), choices=DHGroupChoices, @@ -228,7 +228,7 @@ class Meta: ) -class IPSecProfileImportForm(PrimaryModelBulkImportForm): +class IPSecProfileImportForm(PrimaryModelImportForm): mode = CSVChoiceField( label=_('Mode'), choices=IPSecModeChoices, @@ -252,7 +252,7 @@ class Meta: ) -class L2VPNImportForm(PrimaryModelBulkImportForm): +class L2VPNImportForm(PrimaryModelImportForm): tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index a76e12ef61d..64f8c85633c 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -5,7 +5,7 @@ from dcim.models import Device, Interface, Site from ipam.models import VLAN from netbox.choices import * -from netbox.forms import NestedGroupModelBulkImportForm, PrimaryModelBulkImportForm +from netbox.forms import NestedGroupModelImportForm, PrimaryModelImportForm from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField from wireless.choices import * @@ -18,7 +18,7 @@ ) -class WirelessLANGroupImportForm(NestedGroupModelBulkImportForm): +class WirelessLANGroupImportForm(NestedGroupModelImportForm): parent = CSVModelChoiceField( label=_('Parent'), queryset=WirelessLANGroup.objects.all(), @@ -32,7 +32,7 @@ class Meta: fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags') -class WirelessLANImportForm(ScopedImportForm, PrimaryModelBulkImportForm): +class WirelessLANImportForm(ScopedImportForm, PrimaryModelImportForm): group = CSVModelChoiceField( label=_('Group'), queryset=WirelessLANGroup.objects.all(), @@ -83,7 +83,7 @@ class Meta: } -class WirelessLinkImportForm(PrimaryModelBulkImportForm): +class WirelessLinkImportForm(PrimaryModelImportForm): # Termination A site_a = CSVModelChoiceField( label=_('Site A'), From 165c3f59d8708e505f212d0d31dd326a8221da10 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 10:08:28 -0400 Subject: [PATCH 19/40] Fix device/VM component type definitions --- netbox/dcim/graphql/types.py | 2 +- netbox/virtualization/graphql/types.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 81aedcf1e5a..8b72807ddd8 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -87,7 +87,7 @@ @strawberry.type -class ComponentType(PrimaryObjectType): +class ComponentType(NetBoxObjectType): """ Base type for device/VM components """ diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 870fab9e288..e67c954896f 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -6,7 +6,7 @@ from extras.graphql.mixins import ConfigContextMixin, ContactsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt -from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType +from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType, NetBoxObjectType from virtualization import models from .filters import * @@ -36,7 +36,7 @@ @strawberry.type -class ComponentType(PrimaryObjectType): +class ComponentType(NetBoxObjectType): """ Base type for device/VM components """ From 912d2af0058eb58b12cc233713a2a29dd9a4fbc4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 10:14:02 -0400 Subject: [PATCH 20/40] Correct filterset definitions --- netbox/dcim/filtersets.py | 4 ++-- netbox/extras/filtersets.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index ba7aa30c1cf..8cd36acbeee 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -951,7 +951,7 @@ def search(self, queryset, name, value): return queryset.filter(qs_filter) -class DeviceRoleFilterSet(OrganizationalModelFilterSet): +class DeviceRoleFilterSet(NestedGroupModelFilterSet): config_template_id = django_filters.ModelMultipleChoiceFilter( queryset=ConfigTemplate.objects.all(), label=_('Config template (ID)'), @@ -985,7 +985,7 @@ class Meta: fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description') -class PlatformFilterSet(OrganizationalModelFilterSet): +class PlatformFilterSet(NestedGroupModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Platform.objects.all(), label=_('Immediate parent platform (ID)'), diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index fd2476f20b8..e2058d9ada0 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -5,7 +5,7 @@ from core.models import DataSource, ObjectType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup -from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet from tenancy.models import Tenant, TenantGroup from users.filterset_mixins import OwnerFilterMixin from users.models import Group, User @@ -590,7 +590,7 @@ def search(self, queryset, name, value): ) -class ConfigContextProfileFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): +class ConfigContextProfileFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), From dbce38482ab8059bea182b69c4ef8daa7f9f0e2d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 10:15:33 -0400 Subject: [PATCH 21/40] Add base class tests for forms, filtersets, serializers, and GraphQL types --- netbox/netbox/tests/test_base_classes.py | 279 +++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 netbox/netbox/tests/test_base_classes.py diff --git a/netbox/netbox/tests/test_base_classes.py b/netbox/netbox/tests/test_base_classes.py new file mode 100644 index 00000000000..0b471f796cb --- /dev/null +++ b/netbox/netbox/tests/test_base_classes.py @@ -0,0 +1,279 @@ +from django.apps import apps +from django.test import TestCase +from django.utils.module_loading import import_string + +from netbox.api.serializers import ( + NestedGroupModelSerializer, + NetBoxModelSerializer, + OrganizationalModelSerializer, + PrimaryModelSerializer, +) +from netbox.filtersets import ( + NestedGroupModelFilterSet, + NetBoxModelFilterSet, + OrganizationalModelFilterSet, + PrimaryModelFilterSet, +) +from netbox.forms.bulk_edit import ( + NestedGroupModelBulkEditForm, + NetBoxModelBulkEditForm, + OrganizationalModelBulkEditForm, + PrimaryModelBulkEditForm, +) +from netbox.forms.bulk_import import ( + NestedGroupModelImportForm, + NetBoxModelImportForm, + OrganizationalModelImportForm, + PrimaryModelImportForm, +) +from netbox.forms.filtersets import ( + NestedGroupModelFilterSetForm, + NetBoxModelFilterSetForm, + OrganizationalModelFilterSetForm, + PrimaryModelFilterSetForm, +) +from netbox.forms.model_forms import ( + NestedGroupModelForm, + NetBoxModelForm, + OrganizationalModelForm, + PrimaryModelForm, +) +from netbox.graphql.types import ( + NestedGroupObjectType, + NetBoxObjectType, + OrganizationalObjectType, + PrimaryObjectType, +) +from netbox.models import NestedGroupModel, NetBoxModel, OrganizationalModel, PrimaryModel + + +class FormClassesTestCase(TestCase): + + @staticmethod + def get_form_for_model(model, prefix=''): + """ + Import and return the form class for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.forms.{model_name}{prefix}Form') + + @staticmethod + def get_model_form_base_class(model): + """ + Return the base form class for creating/editing the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelForm + if issubclass(model, OrganizationalModel): + return OrganizationalModelForm + if issubclass(model, NestedGroupModel): + return NestedGroupModelForm + if issubclass(model, NetBoxModel): + return NetBoxModelForm + + @staticmethod + def get_bulk_edit_form_base_class(model): + """ + Return the base form class for bulk editing the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelBulkEditForm + if issubclass(model, OrganizationalModel): + return OrganizationalModelBulkEditForm + if issubclass(model, NestedGroupModel): + return NestedGroupModelBulkEditForm + if issubclass(model, NetBoxModel): + return NetBoxModelBulkEditForm + + @staticmethod + def get_import_form_base_class(model): + """ + Return the base form class for importing the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelImportForm + if issubclass(model, OrganizationalModel): + return OrganizationalModelImportForm + if issubclass(model, NestedGroupModel): + return NestedGroupModelImportForm + if issubclass(model, NetBoxModel): + return NetBoxModelImportForm + + @staticmethod + def get_filterset_form_base_class(model): + """ + Return the base form class for the given model's FilterSet. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelFilterSetForm + if issubclass(model, OrganizationalModel): + return OrganizationalModelFilterSetForm + if issubclass(model, NestedGroupModel): + return NestedGroupModelFilterSetForm + if issubclass(model, NetBoxModel): + return NetBoxModelFilterSetForm + + def test_model_form_base_classes(self): + """ + Check that each model form inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_model_form_base_class(model): + form_class = self.get_form_for_model(model) + self.assertTrue(issubclass(form_class, base_class), f"{form_class} does not inherit from {base_class}") + + def test_bulk_edit_form_base_classes(self): + """ + Check that each bulk edit form inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_bulk_edit_form_base_class(model): + form_class = self.get_form_for_model(model, prefix='BulkEdit') + self.assertTrue(issubclass(form_class, base_class), f"{form_class} does not inherit from {base_class}") + + def test_import_form_base_classes(self): + """ + Check that each bulk import form inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_import_form_base_class(model): + form_class = self.get_form_for_model(model, prefix='Import') + self.assertTrue(issubclass(form_class, base_class), f"{form_class} does not inherit from {base_class}") + + def test_filterset_form_base_classes(self): + """ + Check that each filterset form inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_filterset_form_base_class(model): + form_class = self.get_form_for_model(model, prefix='Filter') + self.assertTrue(issubclass(form_class, base_class), f"{form_class} does not inherit from {base_class}") + + +class FilterSetClassesTestCase(TestCase): + + @staticmethod + def get_filterset_for_model(model): + """ + Import and return the filterset class for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.filtersets.{model_name}FilterSet') + + @staticmethod + def get_model_filterset_base_class(model): + """ + Return the base FilterSet class for the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelFilterSet + if issubclass(model, OrganizationalModel): + return OrganizationalModelFilterSet + if issubclass(model, NestedGroupModel): + return NestedGroupModelFilterSet + if issubclass(model, NetBoxModel): + return NetBoxModelFilterSet + + def test_model_filterset_base_classes(self): + """ + Check that each FilterSet inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_model_filterset_base_class(model): + filterset = self.get_filterset_for_model(model) + self.assertTrue( + issubclass(filterset, base_class), + f"{filterset} does not inherit from {base_class}", + ) + + +class SerializerClassesTestCase(TestCase): + + @staticmethod + def get_serializer_for_model(model): + """ + Import and return the REST API serializer class for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.api.serializers.{model_name}Serializer') + + @staticmethod + def get_model_serializer_base_class(model): + """ + Return the base serializer class for the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelSerializer + if issubclass(model, OrganizationalModel): + return OrganizationalModelSerializer + if issubclass(model, NestedGroupModel): + return NestedGroupModelSerializer + if issubclass(model, NetBoxModel): + return NetBoxModelSerializer + + def test_model_serializer_base_classes(self): + """ + Check that each model serializer inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_model_serializer_base_class(model): + serializer = self.get_serializer_for_model(model) + self.assertTrue( + issubclass(serializer, base_class), + f"{serializer} does not inherit from {base_class}", + ) + + +class GraphQLTypeClassesTestCase(TestCase): + + @staticmethod + def get_type_for_model(model): + """ + Import and return the GraphQL type for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.graphql.types.{model_name}Type') + + @staticmethod + def get_model_type_base_class(model): + """ + Return the base GraphQL type for the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryObjectType + if issubclass(model, OrganizationalModel): + return OrganizationalObjectType + if issubclass(model, NestedGroupModel): + return NestedGroupObjectType + if issubclass(model, NetBoxModel): + return NetBoxObjectType + + def test_model_type_base_classes(self): + """ + Check that each GraphQL type inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_model_type_base_class(model): + graphql_type = self.get_type_for_model(model) + self.assertTrue( + issubclass(graphql_type, base_class), + f"{graphql_type} does not inherit from {base_class}", + ) From 2f23bdcebb2c49c5708e2bd65f198ac468aa422b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 10:23:11 -0400 Subject: [PATCH 22/40] Correct device/VM component filterset definitions --- netbox/dcim/filtersets.py | 5 +++-- netbox/virtualization/filtersets.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 8cd36acbeee..64f592ae5aa 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -12,10 +12,11 @@ from netbox.choices import ColorChoices from netbox.filtersets import ( AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, - OrganizationalModelFilterSet, PrimaryModelFilterSet, + OrganizationalModelFilterSet, PrimaryModelFilterSet, NetBoxModelFilterSet, ) from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.models import * +from users.filterset_mixins import OwnerFilterMixin from users.models import User from utilities.filters import ( ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, @@ -1516,7 +1517,7 @@ def search(self, queryset, name, value): ).distinct() -class DeviceComponentFilterSet(PrimaryModelFilterSet): +class DeviceComponentFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 567b20b6da6..e2ef8cb6a8a 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -9,8 +9,10 @@ from extras.filtersets import LocalConfigContextFilterSet from extras.models import ConfigTemplate from ipam.filtersets import PrimaryIPFilterSet -from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet + +from users.filterset_mixins import OwnerFilterMixin from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from .choices import * from .models import * @@ -235,7 +237,7 @@ def _has_primary_ip(self, queryset, name, value): return queryset.exclude(params) -class VMInterfaceFilterSet(PrimaryModelFilterSet, CommonInterfaceFilterSet): +class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxModelFilterSet): cluster_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_machine__cluster', queryset=Cluster.objects.all(), @@ -297,7 +299,7 @@ def search(self, queryset, name, value): ) -class VirtualDiskFilterSet(PrimaryModelFilterSet): +class VirtualDiskFilterSet(OwnerFilterMixin, NetBoxModelFilterSet): virtual_machine_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_machine', queryset=VirtualMachine.objects.all(), From 24775796201821776ccb8a8bca03a44c5c6a5917 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 12:03:47 -0400 Subject: [PATCH 23/40] Correct device/VM component GraphQL type definitions --- netbox/dcim/graphql/types.py | 3 ++- netbox/virtualization/graphql/types.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 8b72807ddd8..d092cc41754 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -11,6 +11,7 @@ from netbox.graphql.types import ( BaseObjectType, NestedGroupObjectType, NetBoxObjectType, OrganizationalObjectType, PrimaryObjectType, ) +from users.graphql.mixins import OwnerMixin from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -87,7 +88,7 @@ @strawberry.type -class ComponentType(NetBoxObjectType): +class ComponentType(OwnerMixin, NetBoxObjectType): """ Base type for device/VM components """ diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index e67c954896f..59323e7e533 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -7,6 +7,7 @@ from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType, NetBoxObjectType +from users.graphql.mixins import OwnerMixin from virtualization import models from .filters import * @@ -36,7 +37,7 @@ @strawberry.type -class ComponentType(NetBoxObjectType): +class ComponentType(OwnerMixin, NetBoxObjectType): """ Base type for device/VM components """ From a4b8862a7ba602a97adb34b465f38a11efab1c18 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 12:23:08 -0400 Subject: [PATCH 24/40] Add owner to base object template --- netbox/templates/generic/object.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 437690b3d15..e7ef9d97eec 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -55,6 +55,10 @@ {% block subtitle %}
+ {% if object.owner %} + {{ object.owner|linkify }} + · + {% endif %} {% trans "Created" %} {{ object.created|isodatetime:"minutes" }} {% if object.last_updated %} · From 77117a248775bc2f7a02b37173b1de949996af22 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 12:43:41 -0400 Subject: [PATCH 25/40] Show related objects under owner view --- netbox/circuits/migrations/0053_owner.py | 16 ++--- netbox/core/migrations/0020_owner.py | 7 +- netbox/dcim/migrations/0216_owner.py | 66 +++++++++---------- netbox/extras/migrations/0134_owner.py | 22 +++---- netbox/ipam/migrations/0083_owner.py | 32 ++++----- netbox/netbox/models/mixins.py | 1 - netbox/templates/users/owner.html | 5 +- netbox/tenancy/migrations/0021_owner.py | 10 +-- netbox/users/views.py | 13 +++- .../virtualization/migrations/0049_owner.py | 12 ++-- netbox/vpn/migrations/0010_owner.py | 16 ++--- netbox/wireless/migrations/0016_owner.py | 6 +- 12 files changed, 105 insertions(+), 101 deletions(-) diff --git a/netbox/circuits/migrations/0053_owner.py b/netbox/circuits/migrations/0053_owner.py index 04fe46c6172..04056bfce58 100644 --- a/netbox/circuits/migrations/0053_owner.py +++ b/netbox/circuits/migrations/0053_owner.py @@ -13,56 +13,56 @@ class Migration(migrations.Migration): model_name='circuit', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='circuitgroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='circuittype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='provider', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='provideraccount', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='providernetwork', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualcircuit', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualcircuittype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/core/migrations/0020_owner.py b/netbox/core/migrations/0020_owner.py index ecb30fa3e63..f9cdb15b0aa 100644 --- a/netbox/core/migrations/0020_owner.py +++ b/netbox/core/migrations/0020_owner.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ('core', '0019_configrevision_active'), ('users', '0015_owner'), @@ -14,11 +13,7 @@ class Migration(migrations.Migration): model_name='datasource', name='owner', field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name='+', - to='users.owner', + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/dcim/migrations/0216_owner.py b/netbox/dcim/migrations/0216_owner.py index a7c5aa899af..89b12128a1e 100644 --- a/netbox/dcim/migrations/0216_owner.py +++ b/netbox/dcim/migrations/0216_owner.py @@ -13,231 +13,231 @@ class Migration(migrations.Migration): model_name='cable', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='consoleport', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='consoleserverport', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='device', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='devicebay', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='devicerole', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='devicetype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='frontport', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='interface', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='inventoryitem', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='inventoryitemrole', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='location', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='macaddress', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='manufacturer', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='module', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='modulebay', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='moduletype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='moduletypeprofile', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='platform', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='powerfeed', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='poweroutlet', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='powerpanel', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='powerport', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='rack', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='rackreservation', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='rackrole', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='racktype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='rearport', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='region', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='site', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='sitegroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualchassis', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualdevicecontext', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/extras/migrations/0134_owner.py b/netbox/extras/migrations/0134_owner.py index 1a01fd95b12..2e47cc4e28a 100644 --- a/netbox/extras/migrations/0134_owner.py +++ b/netbox/extras/migrations/0134_owner.py @@ -13,77 +13,77 @@ class Migration(migrations.Migration): model_name='configcontext', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='configcontextprofile', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='configtemplate', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='customfield', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='customfieldchoiceset', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='customlink', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='eventrule', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='exporttemplate', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='savedfilter', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='tag', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='webhook', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/ipam/migrations/0083_owner.py b/netbox/ipam/migrations/0083_owner.py index 307963ba00b..abc5795fb89 100644 --- a/netbox/ipam/migrations/0083_owner.py +++ b/netbox/ipam/migrations/0083_owner.py @@ -13,112 +13,112 @@ class Migration(migrations.Migration): model_name='aggregate', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='asn', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='asnrange', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='fhrpgroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='ipaddress', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='iprange', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='prefix', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='rir', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='role', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='routetarget', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='service', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='servicetemplate', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='vlan', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='vlangroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='vlantranslationpolicy', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='vrf', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py index 10798796ddf..ef1cd9e9687 100644 --- a/netbox/netbox/models/mixins.py +++ b/netbox/netbox/models/mixins.py @@ -19,7 +19,6 @@ class OwnerMixin(models.Model): owner = models.ForeignKey( to='users.Owner', on_delete=models.PROTECT, - related_name='+', blank=True, null=True ) diff --git a/netbox/templates/users/owner.html b/netbox/templates/users/owner.html index b840c3b678b..7aa9b2edd73 100644 --- a/netbox/templates/users/owner.html +++ b/netbox/templates/users/owner.html @@ -19,8 +19,6 @@

{% trans "Owner" %}

-
-

{% trans "Groups" %}

@@ -42,5 +40,8 @@

{% trans "Users" %}

+
+ {% include 'inc/panels/related_objects.html' with filter_name='owner_id' %} +
{% endblock %} diff --git a/netbox/tenancy/migrations/0021_owner.py b/netbox/tenancy/migrations/0021_owner.py index b6fedda8830..4c1a52abb76 100644 --- a/netbox/tenancy/migrations/0021_owner.py +++ b/netbox/tenancy/migrations/0021_owner.py @@ -13,35 +13,35 @@ class Migration(migrations.Migration): model_name='contact', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='contactgroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='contactrole', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='tenant', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='tenantgroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/users/views.py b/netbox/users/views.py index aa0efdd72a0..7c833568fb4 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -4,7 +4,7 @@ from core.tables import ObjectChangeTable from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from netbox.views import generic -from utilities.views import register_model_view +from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import Group, User, ObjectPermission, Owner, Token @@ -246,10 +246,19 @@ class OwnerListView(generic.ObjectListView): @register_model_view(Owner) -class OwnerView(generic.ObjectView): +class OwnerView(GetRelatedModelsMixin, generic.ObjectView): queryset = Owner.objects.all() template_name = 'users/owner.html' + def get_extra_context(self, request, instance): + return { + 'related_models': self.get_related_models( + request, + instance, + omit=(Group, User), + ), + } + @register_model_view(Owner, 'add', detail=False) @register_model_view(Owner, 'edit') diff --git a/netbox/virtualization/migrations/0049_owner.py b/netbox/virtualization/migrations/0049_owner.py index 657d325ec08..630b7fc8020 100644 --- a/netbox/virtualization/migrations/0049_owner.py +++ b/netbox/virtualization/migrations/0049_owner.py @@ -13,42 +13,42 @@ class Migration(migrations.Migration): model_name='cluster', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='clustergroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='clustertype', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualdisk', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='virtualmachine', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='vminterface', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/vpn/migrations/0010_owner.py b/netbox/vpn/migrations/0010_owner.py index 135084f84f4..19749f2139d 100644 --- a/netbox/vpn/migrations/0010_owner.py +++ b/netbox/vpn/migrations/0010_owner.py @@ -13,56 +13,56 @@ class Migration(migrations.Migration): model_name='ikepolicy', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='ikeproposal', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='ipsecpolicy', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='ipsecprofile', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='ipsecproposal', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='l2vpn', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='tunnel', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='tunnelgroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] diff --git a/netbox/wireless/migrations/0016_owner.py b/netbox/wireless/migrations/0016_owner.py index 08167290c17..fa753ffc3e8 100644 --- a/netbox/wireless/migrations/0016_owner.py +++ b/netbox/wireless/migrations/0016_owner.py @@ -13,21 +13,21 @@ class Migration(migrations.Migration): model_name='wirelesslan', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='wirelesslangroup', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), migrations.AddField( model_name='wirelesslink', name='owner', field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner' ), ), ] From 57daa9f4a43b2460e93e0f30e2b406e77964f0ee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 12:48:25 -0400 Subject: [PATCH 26/40] Fix owner filter form field --- netbox/netbox/forms/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 4ee11b0bbf7..0f267cb0843 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -126,7 +126,7 @@ class OwnerMixin(forms.Form): """ Add an `owner` field to forms for models which support Owner assignment. """ - owner = DynamicModelChoiceField( + owner_id = DynamicModelChoiceField( queryset=Owner.objects.all(), required=False, label=_('Owner'), From 59082d03644979ed502c1ed62155e808d0f41e4d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 13:43:13 -0400 Subject: [PATCH 27/40] Introduce base table classes with an 'owner' column for primary, organizational, and nested group models --- netbox/circuits/tables/circuits.py | 21 ++---- netbox/circuits/tables/providers.py | 27 +++---- netbox/circuits/tables/virtual_circuits.py | 13 ++-- netbox/core/tables/data.py | 6 +- netbox/dcim/tables/cables.py | 11 ++- netbox/dcim/tables/devices.py | 53 ++++--------- netbox/dcim/tables/devicetypes.py | 13 ++-- netbox/dcim/tables/modules.py | 25 ++----- netbox/dcim/tables/power.py | 21 ++---- netbox/dcim/tables/racks.py | 45 +++-------- netbox/dcim/tables/sites.py | 75 +++---------------- netbox/extras/tables/tables.py | 46 +++++++++++- netbox/ipam/tables/asn.py | 13 ++-- netbox/ipam/tables/fhrp.py | 11 +-- netbox/ipam/tables/ip.py | 40 ++++------ netbox/ipam/tables/services.py | 18 ++--- netbox/ipam/tables/vlans.py | 17 ++--- netbox/ipam/tables/vrfs.py | 18 ++--- netbox/netbox/tables/tables.py | 38 ++++++++++ netbox/netbox/tests/test_base_classes.py | 50 +++++++++++++ netbox/tenancy/tables/contacts.py | 30 ++------ netbox/tenancy/tables/tenants.py | 28 ++----- netbox/users/tables.py | 2 +- netbox/virtualization/tables/clusters.py | 21 +++--- .../virtualization/tables/virtualmachines.py | 9 +-- netbox/vpn/tables/crypto.py | 37 +++------ netbox/vpn/tables/l2vpn.py | 9 +-- netbox/vpn/tables/tunnels.py | 13 ++-- netbox/wireless/tables/wirelesslan.py | 26 ++----- netbox/wireless/tables/wirelesslink.py | 8 +- 30 files changed, 316 insertions(+), 428 deletions(-) diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 901893a77eb..1c0e79d19ad 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -1,11 +1,9 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from circuits.models import * +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin - -from netbox.tables import NetBoxTable, columns - from .columns import CommitRateColumn __all__ = ( @@ -24,7 +22,7 @@ """ -class CircuitTypeTable(NetBoxTable): +class CircuitTypeTable(OrganizationalModelTable): name = tables.Column( linkify=True, verbose_name=_('Name'), @@ -39,7 +37,7 @@ class CircuitTypeTable(NetBoxTable): verbose_name=_('Circuits') ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = CircuitType fields = ( 'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', @@ -48,7 +46,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'circuit_count', 'color', 'description') -class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): cid = tables.Column( linkify=True, verbose_name=_('Circuit ID') @@ -79,9 +77,6 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): verbose_name=_('Commit Rate') ) distance = columns.DistanceColumn() - comments = columns.MarkdownColumn( - verbose_name=_('Comments') - ) tags = columns.TagColumn( url_name='circuits:circuit_list' ) @@ -90,7 +85,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): linkify_item=True ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Circuit fields = ( 'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group', @@ -163,7 +158,7 @@ class Meta(NetBoxTable.Meta): ) -class CircuitGroupTable(NetBoxTable): +class CircuitGroupTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -177,7 +172,7 @@ class CircuitGroupTable(NetBoxTable): url_name='circuits:circuitgroup_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = CircuitGroup fields = ( 'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags', diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index 54a5c2cc9b8..4fbdff8c703 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -1,10 +1,10 @@ import django_tables2 as tables from django.utils.translation import gettext_lazy as _ -from circuits.models import * from django_tables2.utils import Accessor -from tenancy.tables import ContactsColumnMixin -from netbox.tables import NetBoxTable, columns +from circuits.models import * +from netbox.tables import PrimaryModelTable, columns +from tenancy.tables import ContactsColumnMixin __all__ = ( 'ProviderTable', @@ -13,7 +13,7 @@ ) -class ProviderTable(ContactsColumnMixin, NetBoxTable): +class ProviderTable(ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -42,14 +42,11 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable): url_params={'provider_id': 'pk'}, verbose_name=_('Circuits') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='circuits:provider_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Provider fields = ( 'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description', @@ -58,7 +55,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'account_count', 'circuit_count') -class ProviderAccountTable(ContactsColumnMixin, NetBoxTable): +class ProviderAccountTable(ContactsColumnMixin, PrimaryModelTable): account = tables.Column( linkify=True, verbose_name=_('Account'), @@ -76,14 +73,11 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable): url_params={'provider_account_id': 'pk'}, verbose_name=_('Circuits') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='circuits:provideraccount_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ProviderAccount fields = ( 'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created', @@ -92,7 +86,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count') -class ProviderNetworkTable(NetBoxTable): +class ProviderNetworkTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -101,14 +95,11 @@ class ProviderNetworkTable(NetBoxTable): verbose_name=_('Provider'), linkify=True ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='circuits:providernetwork_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ProviderNetwork fields = ( 'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags', diff --git a/netbox/circuits/tables/virtual_circuits.py b/netbox/circuits/tables/virtual_circuits.py index ea3b6dc133d..c55dfd17848 100644 --- a/netbox/circuits/tables/virtual_circuits.py +++ b/netbox/circuits/tables/virtual_circuits.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from circuits.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin __all__ = ( @@ -12,7 +12,7 @@ ) -class VirtualCircuitTypeTable(NetBoxTable): +class VirtualCircuitTypeTable(OrganizationalModelTable): name = tables.Column( linkify=True, verbose_name=_('Name'), @@ -27,7 +27,7 @@ class VirtualCircuitTypeTable(NetBoxTable): verbose_name=_('Circuits') ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = VirtualCircuitType fields = ( 'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created', @@ -36,7 +36,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description') -class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): cid = tables.Column( linkify=True, verbose_name=_('Circuit ID') @@ -63,14 +63,11 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) url_params={'virtual_circuit_id': 'pk'}, verbose_name=_('Terminations') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments') - ) tags = columns.TagColumn( url_name='circuits:virtualcircuit_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = VirtualCircuit fields = ( 'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant', diff --git a/netbox/core/tables/data.py b/netbox/core/tables/data.py index 226a480810f..db688b22a05 100644 --- a/netbox/core/tables/data.py +++ b/netbox/core/tables/data.py @@ -2,7 +2,7 @@ import django_tables2 as tables from core.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, PrimaryModelTable, columns from .columns import BackendTypeColumn from .template_code import DATA_SOURCE_SYNC_BUTTON @@ -12,7 +12,7 @@ ) -class DataSourceTable(NetBoxTable): +class DataSourceTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True, @@ -42,7 +42,7 @@ class DataSourceTable(NetBoxTable): extra_buttons=DATA_SOURCE_SYNC_BUTTON, ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = DataSource fields = ( 'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments', diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py index 321eb79f52c..a4e3be26946 100644 --- a/netbox/dcim/tables/cables.py +++ b/netbox/dcim/tables/cables.py @@ -1,11 +1,11 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables -from django_tables2.utils import Accessor from django.utils.html import escape from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ +from django_tables2.utils import Accessor from dcim.models import Cable -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin from .template_code import CABLE_LENGTH @@ -48,7 +48,7 @@ def value(self, value): # Cables # -class CableTable(TenancyColumnsMixin, NetBoxTable): +class CableTable(TenancyColumnsMixin, PrimaryModelTable): a_terminations = CableTerminationsColumn( cable_end='A', orderable=False, @@ -117,12 +117,11 @@ class CableTable(TenancyColumnsMixin, NetBoxTable): verbose_name=_('Color Name'), orderable=False ) - comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='dcim:cable_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Cable fields = ( 'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b', diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index dbdfae11d98..e70738f0299 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -3,7 +3,7 @@ from django_tables2.utils import Accessor from dcim import models -from netbox.tables import NetBoxTable, columns +from netbox.tables import NestedGroupModelTable, NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from .template_code import * @@ -58,15 +58,7 @@ # Device roles # -class DeviceRoleTable(NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class DeviceRoleTable(NestedGroupModelTable): device_count = columns.LinkedCountColumn( viewname='dcim:device_list', url_params={'role_id': 'pk'}, @@ -89,7 +81,7 @@ class DeviceRoleTable(NetBoxTable): url_name='dcim:devicerole_list' ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = models.DeviceRole fields = ( 'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', @@ -102,15 +94,7 @@ class Meta(NetBoxTable.Meta): # Platforms # -class PlatformTable(NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class PlatformTable(NestedGroupModelTable): manufacturer = tables.Column( verbose_name=_('Manufacturer'), linkify=True @@ -133,7 +117,7 @@ class PlatformTable(NetBoxTable): url_name='dcim:platform_list' ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = models.Platform fields = ( 'pk', 'id', 'name', 'parent', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', @@ -148,7 +132,7 @@ class Meta(NetBoxTable.Meta): # Devices # -class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): name = tables.TemplateColumn( verbose_name=_('Name'), template_code=DEVICE_LINK, @@ -249,7 +233,6 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): accessor='parent_bay', linkify=True ) - comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='dcim:device_list' ) @@ -284,7 +267,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): verbose_name=_('Inventory items') ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = models.Device fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type', @@ -1050,7 +1033,7 @@ class Meta(NetBoxTable.Meta): ) -class InventoryItemRoleTable(NetBoxTable): +class InventoryItemRoleTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -1067,7 +1050,7 @@ class InventoryItemRoleTable(NetBoxTable): url_name='dcim:inventoryitemrole_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = models.InventoryItemRole fields = ( 'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions', @@ -1079,7 +1062,7 @@ class Meta(NetBoxTable.Meta): # Virtual chassis # -class VirtualChassisTable(NetBoxTable): +class VirtualChassisTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -1093,14 +1076,11 @@ class VirtualChassisTable(NetBoxTable): url_params={'virtual_chassis_id': 'pk'}, verbose_name=_('Members') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:virtualchassis_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = models.VirtualChassis fields = ( 'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created', @@ -1109,7 +1089,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'domain', 'master', 'member_count') -class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable): +class VirtualDeviceContextTable(TenancyColumnsMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -1140,14 +1120,11 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable): url_params={'vdc_id': 'pk'}, verbose_name=_('Interfaces') ) - - comments = columns.MarkdownColumn() - tags = columns.TagColumn( url_name='dcim:virtualdevicecontext_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = models.VirtualDeviceContext fields = ( 'pk', 'id', 'name', 'status', 'identifier', 'tenant', 'tenant_group', 'primary_ip', 'primary_ip4', @@ -1158,7 +1135,7 @@ class Meta(NetBoxTable.Meta): ) -class MACAddressTable(NetBoxTable): +class MACAddressTable(PrimaryModelTable): mac_address = tables.TemplateColumn( template_code=MACADDRESS_LINK, verbose_name=_('MAC Address') @@ -1181,7 +1158,7 @@ class MACAddressTable(NetBoxTable): extra_buttons=MACADDRESS_COPY_BUTTON ) - class Meta(DeviceComponentTable.Meta): + class Meta(PrimaryModelTable.Meta): model = models.MACAddress fields = ( 'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags', diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 91f9f3b479e..843211a6d70 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from dcim import models -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, WEIGHT @@ -26,7 +26,7 @@ # Manufacturers # -class ManufacturerTable(ContactsColumnMixin, NetBoxTable): +class ManufacturerTable(ContactsColumnMixin, OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -60,7 +60,7 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable): url_name='dcim:manufacturer_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = models.Manufacturer fields = ( 'pk', 'id', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count', @@ -76,7 +76,7 @@ class Meta(NetBoxTable.Meta): # Device types # -class DeviceTypeTable(NetBoxTable): +class DeviceTypeTable(PrimaryModelTable): model = tables.Column( linkify=True, verbose_name=_('Device Type') @@ -93,9 +93,6 @@ class DeviceTypeTable(NetBoxTable): verbose_name=_('Full Depth'), false_mark=None ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:devicetype_list' ) @@ -148,7 +145,7 @@ class DeviceTypeTable(NetBoxTable): verbose_name=_('Inventory Items') ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = models.DeviceType fields = ( 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index 52edea8b41e..78abfdd1922 100644 --- a/netbox/dcim/tables/modules.py +++ b/netbox/dcim/tables/modules.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from dcim.models import Module, ModuleType, ModuleTypeProfile -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns from .template_code import MODULETYPEPROFILE_ATTRIBUTES, WEIGHT __all__ = ( @@ -12,7 +12,7 @@ ) -class ModuleTypeProfileTable(NetBoxTable): +class ModuleTypeProfileTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -23,14 +23,11 @@ class ModuleTypeProfileTable(NetBoxTable): orderable=False, verbose_name=_('Attributes') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:moduletypeprofile_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ModuleTypeProfile fields = ( 'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated', @@ -40,7 +37,7 @@ class Meta(NetBoxTable.Meta): ) -class ModuleTypeTable(NetBoxTable): +class ModuleTypeTable(PrimaryModelTable): profile = tables.Column( verbose_name=_('Profile'), linkify=True @@ -64,14 +61,11 @@ class ModuleTypeTable(NetBoxTable): url_params={'module_type_id': 'pk'}, verbose_name=_('Instances') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:moduletype_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ModuleType fields = ( 'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', @@ -82,7 +76,7 @@ class Meta(NetBoxTable.Meta): ) -class ModuleTable(NetBoxTable): +class ModuleTable(PrimaryModelTable): device = tables.Column( verbose_name=_('Device'), linkify=True @@ -103,14 +97,11 @@ class ModuleTable(NetBoxTable): status = columns.ChoiceFieldColumn( verbose_name=_('Status'), ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:module_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Module fields = ( 'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag', diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index 40a58ad8161..d7d62ea1722 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -1,10 +1,9 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ + from dcim.models import PowerFeed, PowerPanel +from netbox.tables import PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin - -from netbox.tables import NetBoxTable, columns - from .devices import CableTerminationTable __all__ = ( @@ -17,7 +16,7 @@ # Power panels # -class PowerPanelTable(ContactsColumnMixin, NetBoxTable): +class PowerPanelTable(ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -35,14 +34,11 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable): url_params={'power_panel_id': 'pk'}, verbose_name=_('Power Feeds') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:powerpanel_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = PowerPanel fields = ( 'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags', @@ -57,7 +53,7 @@ class Meta(NetBoxTable.Meta): # We're not using PathEndpointTable for PowerFeed because power connections # cannot traverse pass-through ports. -class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable): +class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -92,14 +88,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable): linkify=True, verbose_name=_('Site'), ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:powerfeed_list' ) - class Meta(NetBoxTable.Meta): + class Meta(CableTerminationTable.Meta, PrimaryModelTable.Meta): model = PowerFeed fields = ( 'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index afb2c44c880..1cc774f2250 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -1,9 +1,9 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from django_tables2.utils import Accessor from dcim.models import Rack, RackReservation, RackRole, RackType -from netbox.tables import NetBoxTable, columns +from netbox.tables import OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from .template_code import OUTER_UNIT, WEIGHT @@ -15,11 +15,7 @@ ) -# -# Rack roles -# - -class RackRoleTable(NetBoxTable): +class RackRoleTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -36,7 +32,7 @@ class RackRoleTable(NetBoxTable): url_name='dcim:rackrole_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = RackRole fields = ( 'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', 'created', @@ -45,11 +41,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'rack_count', 'color', 'description') -# -# Rack Types -# - -class RackTypeTable(NetBoxTable): +class RackTypeTable(PrimaryModelTable): model = tables.Column( verbose_name=_('Model'), linkify=True @@ -84,9 +76,6 @@ class RackTypeTable(NetBoxTable): template_code=WEIGHT, order_by=('_abs_max_weight', 'weight_unit') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) instance_count = columns.LinkedCountColumn( viewname='dcim:rack_list', url_params={'rack_type_id': 'pk'}, @@ -96,7 +85,7 @@ class RackTypeTable(NetBoxTable): url_name='dcim:rack_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = RackType fields = ( 'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', @@ -108,11 +97,7 @@ class Meta(NetBoxTable.Meta): ) -# -# Racks -# - -class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class RackTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -144,9 +129,6 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): template_code="{{ value }}U", verbose_name=_('Height') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) device_count = columns.LinkedCountColumn( viewname='dcim:device_list', url_params={'rack_id': 'pk'}, @@ -186,7 +168,7 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): order_by=('_abs_max_weight', 'weight_unit') ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Rack fields = ( 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', @@ -201,11 +183,7 @@ class Meta(NetBoxTable.Meta): ) -# -# Rack reservations -# - -class RackReservationTable(TenancyColumnsMixin, NetBoxTable): +class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable): reservation = tables.Column( verbose_name=_('Reservation'), accessor='pk', @@ -232,14 +210,11 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable): status = columns.ChoiceFieldColumn( verbose_name=_('Status'), ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:rackreservation_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = RackReservation fields = ( 'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant', diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 0f8fb937298..544fb3cf87a 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -1,10 +1,9 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ + from dcim.models import Location, Region, Site, SiteGroup +from netbox.tables import NestedGroupModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin - -from netbox.tables import NetBoxTable, columns - from .template_code import LOCATION_BUTTONS __all__ = ( @@ -15,19 +14,7 @@ ) -# -# Regions -# - -class RegionTable(ContactsColumnMixin, NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class RegionTable(ContactsColumnMixin, NestedGroupModelTable): site_count = columns.LinkedCountColumn( viewname='dcim:site_list', url_params={'region_id': 'pk'}, @@ -36,11 +23,8 @@ class RegionTable(ContactsColumnMixin, NetBoxTable): tags = columns.TagColumn( url_name='dcim:region_list' ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = Region fields = ( 'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags', @@ -49,19 +33,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'site_count', 'description') -# -# Site groups -# - -class SiteGroupTable(ContactsColumnMixin, NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class SiteGroupTable(ContactsColumnMixin, NestedGroupModelTable): site_count = columns.LinkedCountColumn( viewname='dcim:site_list', url_params={'group_id': 'pk'}, @@ -70,11 +42,8 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable): tags = columns.TagColumn( url_name='dcim:sitegroup_list' ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = SiteGroup fields = ( 'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags', @@ -83,11 +52,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'site_count', 'description') -# -# Sites -# - -class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -117,14 +82,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): url_params={'site_id': 'pk'}, verbose_name=_('Devices') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='dcim:site_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Site fields = ( 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns', @@ -134,19 +96,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') -# -# Locations -# - -class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NestedGroupModelTable): site = tables.Column( verbose_name=_('Site'), linkify=True @@ -175,11 +125,8 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): actions = columns.ActionsColumn( extra_buttons=LOCATION_BUTTONS ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = Location fields = ( 'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index e89d06c4041..40f43b52705 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -10,7 +10,7 @@ from core.models import Job from netbox.constants import EMPTY_TABLE_TEXT from netbox.events import get_event_text -from netbox.tables import BaseTable, NetBoxTable, columns +from netbox.tables import BaseTable, NetBoxTable, PrimaryModelTable, columns from .columns import NotificationActionsColumn __all__ = ( @@ -109,6 +109,10 @@ class CustomFieldTable(NetBoxTable): validation_regex = tables.Column( verbose_name=_('Validation Regex'), ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) class Meta(NetBoxTable.Meta): model = CustomField @@ -146,6 +150,10 @@ class CustomFieldChoiceSetTable(NetBoxTable): verbose_name=_('Order Alphabetically'), false_mark=None ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) class Meta(NetBoxTable.Meta): model = CustomFieldChoiceSet @@ -171,6 +179,10 @@ class CustomLinkTable(NetBoxTable): verbose_name=_('New Window'), false_mark=None ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) class Meta(NetBoxTable.Meta): model = CustomLink @@ -214,6 +226,10 @@ class ExportTemplateTable(NetBoxTable): orderable=False, verbose_name=_('Synced') ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) class Meta(NetBoxTable.Meta): model = ExportTemplate @@ -294,6 +310,10 @@ class SavedFilterTable(NetBoxTable): verbose_name=_('Shared'), false_mark=None ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) def value_parameters(self, value): return json.dumps(value) @@ -450,6 +470,10 @@ class WebhookTable(NetBoxTable): ssl_validation = columns.BooleanColumn( verbose_name=_('SSL Validation') ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) tags = columns.TagColumn( url_name='extras:webhook_list' ) @@ -488,6 +512,10 @@ class EventRuleTable(NetBoxTable): func=get_event_text, orderable=False ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) tags = columns.TagColumn( url_name='extras:webhook_list' ) @@ -514,6 +542,10 @@ class TagTable(NetBoxTable): object_types = columns.ContentTypesColumn( verbose_name=_('Object Types'), ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) class Meta(NetBoxTable.Meta): model = Tag @@ -547,7 +579,7 @@ class Meta(NetBoxTable.Meta): fields = ('id', 'content_type', 'content_object') -class ConfigContextProfileTable(NetBoxTable): +class ConfigContextProfileTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -568,7 +600,7 @@ class ConfigContextProfileTable(NetBoxTable): url_name='extras:configcontextprofile_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ConfigContextProfile fields = ( 'pk', 'id', 'name', 'description', 'comments', 'data_source', 'data_file', 'is_synced', 'tags', 'created', @@ -601,6 +633,10 @@ class ConfigContextTable(NetBoxTable): orderable=False, verbose_name=_('Synced') ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) tags = columns.TagColumn( url_name='extras:configcontext_list' ) @@ -645,6 +681,10 @@ class ConfigTemplateTable(NetBoxTable): verbose_name=_('As Attachment'), false_mark=None ) + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) tags = columns.TagColumn( url_name='extras:configtemplate_list' ) diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py index bbe38dc1a33..0f9c357ad76 100644 --- a/netbox/ipam/tables/asn.py +++ b/netbox/ipam/tables/asn.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin __all__ = ( @@ -11,7 +11,7 @@ ) -class ASNRangeTable(TenancyColumnsMixin, NetBoxTable): +class ASNRangeTable(TenancyColumnsMixin, OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -27,7 +27,7 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable): verbose_name=_('ASNs') ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = ASNRange fields = ( 'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags', @@ -36,7 +36,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description') -class ASNTable(TenancyColumnsMixin, NetBoxTable): +class ASNTable(TenancyColumnsMixin, PrimaryModelTable): asn = tables.Column( verbose_name=_('ASN'), linkify=True @@ -65,14 +65,11 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable): linkify_item=True, verbose_name=_('Sites') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:asn_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ASN fields = ( 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py index 789845f25a7..2d77c62c7bf 100644 --- a/netbox/ipam/tables/fhrp.py +++ b/netbox/ipam/tables/fhrp.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, PrimaryModelTable, columns __all__ = ( 'FHRPGroupTable', @@ -17,7 +17,7 @@ """ -class FHRPGroupTable(NetBoxTable): +class FHRPGroupTable(PrimaryModelTable): group_id = tables.Column( verbose_name=_('Group ID'), linkify=True @@ -30,9 +30,6 @@ class FHRPGroupTable(NetBoxTable): member_count = tables.Column( verbose_name=_('Members') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:fhrpgroup_list' ) @@ -40,7 +37,7 @@ class FHRPGroupTable(NetBoxTable): def value_ip_addresses(self, value): return ",".join([str(obj.address) for obj in value.all()]) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = FHRPGroup fields = ( 'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'comments', 'ip_addresses', diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 03365a44292..11387185ae2 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -1,10 +1,10 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from django_tables2.utils import Accessor from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin, TenantColumn from .template_code import * @@ -27,7 +27,7 @@ # RIRs # -class RIRTable(NetBoxTable): +class RIRTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -45,7 +45,7 @@ class RIRTable(NetBoxTable): url_name='ipam:rir_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = RIR fields = ( 'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'created', @@ -58,7 +58,7 @@ class Meta(NetBoxTable.Meta): # Aggregates # -class AggregateTable(TenancyColumnsMixin, NetBoxTable): +class AggregateTable(TenancyColumnsMixin, PrimaryModelTable): prefix = tables.Column( linkify=True, verbose_name=_('Aggregate'), @@ -79,9 +79,6 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable): accessor='get_utilization', orderable=False ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:aggregate_list' ) @@ -89,7 +86,7 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable): extra_buttons=AGGREGATE_COPY_BUTTON ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Aggregate fields = ( 'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', @@ -102,7 +99,7 @@ class Meta(NetBoxTable.Meta): # Roles # -class RoleTable(NetBoxTable): +class RoleTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -126,7 +123,7 @@ class RoleTable(NetBoxTable): url_name='ipam:role_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = Role fields = ( 'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight', 'tags', @@ -154,7 +151,7 @@ class PrefixUtilizationColumn(columns.UtilizationColumn): """ -class PrefixTable(TenancyColumnsMixin, NetBoxTable): +class PrefixTable(TenancyColumnsMixin, PrimaryModelTable): prefix = columns.TemplateColumn( verbose_name=_('Prefix'), template_code=PREFIX_LINK_WITH_DEPTH, @@ -223,9 +220,6 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): accessor='get_utilization', orderable=False ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:prefix_list' ) @@ -233,7 +227,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): extra_buttons=PREFIX_COPY_BUTTON ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Prefix fields = ( 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', @@ -252,7 +246,7 @@ class Meta(NetBoxTable.Meta): # # IP ranges # -class IPRangeTable(TenancyColumnsMixin, NetBoxTable): +class IPRangeTable(TenancyColumnsMixin, PrimaryModelTable): start_address = tables.Column( verbose_name=_('Start address'), linkify=True @@ -282,14 +276,11 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable): accessor='utilization', orderable=False ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:iprange_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IPRange fields = ( 'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', @@ -308,7 +299,7 @@ class Meta(NetBoxTable.Meta): # IPAddresses # -class IPAddressTable(TenancyColumnsMixin, NetBoxTable): +class IPAddressTable(TenancyColumnsMixin, PrimaryModelTable): address = tables.TemplateColumn( template_code=IPADDRESS_LINK, verbose_name=_('IP Address') @@ -351,9 +342,6 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): verbose_name=_('Assigned'), false_mark=None ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:ipaddress_list' ) @@ -361,7 +349,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): extra_buttons=IPADDRESS_COPY_BUTTON ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IPAddress fields = ( 'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', diff --git a/netbox/ipam/tables/services.py b/netbox/ipam/tables/services.py index 392c3d3357a..f544ad24e3f 100644 --- a/netbox/ipam/tables/services.py +++ b/netbox/ipam/tables/services.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns __all__ = ( 'ServiceTable', @@ -10,7 +10,7 @@ ) -class ServiceTemplateTable(NetBoxTable): +class ServiceTemplateTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -20,14 +20,11 @@ class ServiceTemplateTable(NetBoxTable): accessor=tables.A('port_list'), order_by=tables.A('ports'), ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:servicetemplate_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = ServiceTemplate fields = ( 'pk', 'id', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'created', 'last_updated', @@ -35,7 +32,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'protocol', 'ports', 'description') -class ServiceTable(NetBoxTable): +class ServiceTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -50,14 +47,11 @@ class ServiceTable(NetBoxTable): accessor=tables.A('port_list'), order_by=tables.A('ports'), ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:service_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Service fields = ( 'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags', diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index c5e36abba5e..b97b13e0e19 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -5,7 +5,7 @@ from dcim.models import Interface from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin, TenantColumn from virtualization.models import VMInterface from .template_code import * @@ -28,7 +28,7 @@ # VLAN groups # -class VLANGroupTable(TenancyColumnsMixin, NetBoxTable): +class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -62,7 +62,7 @@ class VLANGroupTable(TenancyColumnsMixin, NetBoxTable): extra_buttons=VLANGROUP_BUTTONS ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = VLANGroup fields = ( 'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description', @@ -77,7 +77,7 @@ class Meta(NetBoxTable.Meta): # VLANs # -class VLANTable(TenancyColumnsMixin, NetBoxTable): +class VLANTable(TenancyColumnsMixin, PrimaryModelTable): vid = tables.TemplateColumn( template_code=VLAN_LINK, verbose_name=_('VID') @@ -120,14 +120,11 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name=_('Prefixes') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:vlan_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = VLAN fields = ( 'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', @@ -229,7 +226,7 @@ def __init__(self, interface, *args, **kwargs): # VLAN Translation # -class VLANTranslationPolicyTable(NetBoxTable): +class VLANTranslationPolicyTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -246,7 +243,7 @@ class VLANTranslationPolicyTable(NetBoxTable): url_name='ipam:vlantranslationpolicy_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = VLANTranslationPolicy fields = ( 'pk', 'id', 'name', 'rule_count', 'description', 'tags', 'created', 'last_updated', diff --git a/netbox/ipam/tables/vrfs.py b/netbox/ipam/tables/vrfs.py index 5fd9cbfb610..d9aa47bc5a9 100644 --- a/netbox/ipam/tables/vrfs.py +++ b/netbox/ipam/tables/vrfs.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from ipam.models import * -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin __all__ = ( @@ -21,7 +21,7 @@ # VRFs # -class VRFTable(TenancyColumnsMixin, NetBoxTable): +class VRFTable(TenancyColumnsMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -43,14 +43,11 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable): template_code=VRF_TARGETS, orderable=False ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:vrf_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = VRF fields = ( 'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'import_targets', 'export_targets', @@ -63,19 +60,16 @@ class Meta(NetBoxTable.Meta): # Route targets # -class RouteTargetTable(TenancyColumnsMixin, NetBoxTable): +class RouteTargetTable(TenancyColumnsMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='ipam:routetarget_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = RouteTarget fields = ( 'pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated', diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 89a9c1ac296..9376c7d64cc 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -27,7 +27,10 @@ __all__ = ( 'BaseTable', + 'NestedGroupModelTable', 'NetBoxTable', + 'OrganizationalModelTable', + 'PrimaryModelTable', 'SearchTable', ) @@ -267,6 +270,41 @@ def htmx_url(self): return '' +class PrimaryModelTable(NetBoxTable): + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) + + +class OrganizationalModelTable(NetBoxTable): + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) + + +class NestedGroupModelTable(NetBoxTable): + owner = tables.Column( + linkify=True, + verbose_name=_('Owner') + ) + name = columns.MPTTColumn( + verbose_name=_('Name'), + linkify=True + ) + parent = tables.Column( + verbose_name=_('Parent'), + linkify=True, + ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) + + class SearchTable(tables.Table): object_type = columns.ContentTypeColumn( verbose_name=_('Type'), diff --git a/netbox/netbox/tests/test_base_classes.py b/netbox/netbox/tests/test_base_classes.py index 0b471f796cb..0a8085a5e4f 100644 --- a/netbox/netbox/tests/test_base_classes.py +++ b/netbox/netbox/tests/test_base_classes.py @@ -45,6 +45,12 @@ PrimaryObjectType, ) from netbox.models import NestedGroupModel, NetBoxModel, OrganizationalModel, PrimaryModel +from netbox.tables import ( + NestedGroupModelTable, + NetBoxTable, + OrganizationalModelTable, + PrimaryModelTable, +) class FormClassesTestCase(TestCase): @@ -199,6 +205,50 @@ def test_model_filterset_base_classes(self): ) +class TableClassesTestCase(TestCase): + + @staticmethod + def get_table_for_model(model): + """ + Import and return the table class for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.tables.{model_name}Table') + + @staticmethod + def get_model_table_base_class(model): + """ + Return the base table class for the given model. + """ + if model._meta.app_label == 'dummy_plugin': + return + if issubclass(model, PrimaryModel): + return PrimaryModelTable + if issubclass(model, OrganizationalModel): + return OrganizationalModelTable + if issubclass(model, NestedGroupModel): + return NestedGroupModelTable + if issubclass(model, NetBoxModel): + return NetBoxTable + + def test_model_table_base_classes(self): + """ + Check that each table inherits from the appropriate base class. + """ + for model in apps.get_models(): + if base_class := self.get_model_table_base_class(model): + table = self.get_table_for_model(model) + self.assertTrue( + issubclass(table, base_class), + f"{table} does not inherit from {base_class}", + ) + self.assertTrue( + issubclass(table.Meta, base_class.Meta), + f"{table}.Meta does not inherit from {base_class}.Meta", + ) + + class SerializerClassesTestCase(TestCase): @staticmethod diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index fb997914070..8dfb4fb38bc 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from django_tables2.utils import Accessor -from netbox.tables import NetBoxTable, columns +from netbox.tables import NestedGroupModelTable, NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.models import * from utilities.tables import linkify_phone @@ -14,15 +14,7 @@ ) -class ContactGroupTable(NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class ContactGroupTable(NestedGroupModelTable): contact_count = columns.LinkedCountColumn( viewname='tenancy:contact_list', url_params={'group_id': 'pk'}, @@ -31,11 +23,8 @@ class ContactGroupTable(NetBoxTable): tags = columns.TagColumn( url_name='tenancy:contactgroup_list' ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = ContactGroup fields = ( 'pk', 'name', 'parent', 'contact_count', 'description', 'comments', 'slug', 'tags', 'created', @@ -44,7 +33,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'contact_count', 'description') -class ContactRoleTable(NetBoxTable): +class ContactRoleTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -53,13 +42,13 @@ class ContactRoleTable(NetBoxTable): url_name='tenancy:contactrole_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = ContactRole fields = ('pk', 'name', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions') default_columns = ('pk', 'name', 'description') -class ContactTable(NetBoxTable): +class ContactTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -72,9 +61,6 @@ class ContactTable(NetBoxTable): verbose_name=_('Phone'), linkify=linkify_phone, ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) assignment_count = columns.LinkedCountColumn( viewname='tenancy:contactassignment_list', url_params={'contact_id': 'pk'}, @@ -84,7 +70,7 @@ class ContactTable(NetBoxTable): url_name='tenancy:contact_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Contact fields = ( 'pk', 'name', 'groups', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', diff --git a/netbox/tenancy/tables/tenants.py b/netbox/tenancy/tables/tenants.py index b7e7f40dfc2..d36ae1386a4 100644 --- a/netbox/tenancy/tables/tenants.py +++ b/netbox/tenancy/tables/tenants.py @@ -1,25 +1,17 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ + +from netbox.tables import NestedGroupModelTable, PrimaryModelTable, columns from tenancy.models import * from tenancy.tables import ContactsColumnMixin -from netbox.tables import NetBoxTable, columns - __all__ = ( 'TenantGroupTable', 'TenantTable', ) -class TenantGroupTable(NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class TenantGroupTable(NestedGroupModelTable): tenant_count = columns.LinkedCountColumn( viewname='tenancy:tenant_list', url_params={'group_id': 'pk'}, @@ -28,11 +20,8 @@ class TenantGroupTable(NetBoxTable): tags = columns.TagColumn( url_name='tenancy:tenantgroup_list' ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = TenantGroup fields = ( 'pk', 'id', 'name', 'parent', 'tenant_count', 'description', 'comments', 'slug', 'tags', 'created', @@ -41,7 +30,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'tenant_count', 'description') -class TenantTable(ContactsColumnMixin, NetBoxTable): +class TenantTable(ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -50,14 +39,11 @@ class TenantTable(ContactsColumnMixin, NetBoxTable): verbose_name=_('Group'), linkify=True ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='tenancy:tenant_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Tenant fields = ( 'pk', 'id', 'name', 'slug', 'group', 'description', 'comments', 'contacts', 'tags', 'created', diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 233db59db98..1e6fcec03dd 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -156,7 +156,7 @@ class OwnerTable(NetBoxTable): linkify_item=('users:group', {'pk': tables.A('pk')}) ) users = columns.ManyToManyColumn( - verbose_name=_('Groups'), + verbose_name=_('Users'), linkify_item=('users:user', {'pk': tables.A('pk')}) ) actions = columns.ActionsColumn( diff --git a/netbox/virtualization/tables/clusters.py b/netbox/virtualization/tables/clusters.py index 665f8fa8b96..6053701b142 100644 --- a/netbox/virtualization/tables/clusters.py +++ b/netbox/virtualization/tables/clusters.py @@ -1,10 +1,10 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ + +from netbox.tables import OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from virtualization.models import Cluster, ClusterGroup, ClusterType -from netbox.tables import NetBoxTable, columns - __all__ = ( 'ClusterTable', 'ClusterGroupTable', @@ -12,7 +12,7 @@ ) -class ClusterTypeTable(NetBoxTable): +class ClusterTypeTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -26,7 +26,7 @@ class ClusterTypeTable(NetBoxTable): url_name='virtualization:clustertype_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = ClusterType fields = ( 'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'created', 'last_updated', 'tags', 'actions', @@ -34,7 +34,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'cluster_count', 'description') -class ClusterGroupTable(ContactsColumnMixin, NetBoxTable): +class ClusterGroupTable(ContactsColumnMixin, OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -48,7 +48,7 @@ class ClusterGroupTable(ContactsColumnMixin, NetBoxTable): url_name='virtualization:clustergroup_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = ClusterGroup fields = ( 'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'contacts', 'tags', 'created', 'last_updated', @@ -57,7 +57,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'cluster_count', 'description') -class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -91,14 +91,11 @@ class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): url_params={'cluster_id': 'pk'}, verbose_name=_('VMs') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='virtualization:cluster_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Cluster fields = ( 'pk', 'id', 'name', 'type', 'group', 'status', 'tenant', 'tenant_group', 'scope', 'scope_type', diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index d56fe668af5..fcb9017df50 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.tables.devices import BaseInterfaceTable -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, PrimaryModelTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from utilities.templatetags.helpers import humanize_disk_megabytes from virtualization.models import VirtualDisk, VirtualMachine, VMInterface @@ -21,7 +21,7 @@ # Virtual machines # -class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): +class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -48,9 +48,6 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) linkify=True, verbose_name=_('Platform') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) primary_ip4 = tables.Column( linkify=True, verbose_name=_('IPv4 Address') @@ -81,7 +78,7 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) verbose_name=_('Disk'), ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = VirtualMachine fields = ( 'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'vcpus', diff --git a/netbox/vpn/tables/crypto.py b/netbox/vpn/tables/crypto.py index 474062b3972..49a40255906 100644 --- a/netbox/vpn/tables/crypto.py +++ b/netbox/vpn/tables/crypto.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django.utils.translation import gettext_lazy as _ -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns from vpn.models import * __all__ = ( @@ -13,7 +13,7 @@ ) -class IKEProposalTable(NetBoxTable): +class IKEProposalTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -33,14 +33,11 @@ class IKEProposalTable(NetBoxTable): sa_lifetime = tables.Column( verbose_name=_('SA Lifetime') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:ikeproposal_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IKEProposal fields = ( 'pk', 'id', 'name', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', @@ -52,7 +49,7 @@ class Meta(NetBoxTable.Meta): ) -class IKEPolicyTable(NetBoxTable): +class IKEPolicyTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -70,14 +67,11 @@ class IKEPolicyTable(NetBoxTable): preshared_key = tables.Column( verbose_name=_('Pre-shared Key') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:ikepolicy_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IKEPolicy fields = ( 'pk', 'id', 'name', 'version', 'mode', 'proposals', 'preshared_key', 'description', 'comments', 'tags', @@ -88,7 +82,7 @@ class Meta(NetBoxTable.Meta): ) -class IPSecProposalTable(NetBoxTable): +class IPSecProposalTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -105,14 +99,11 @@ class IPSecProposalTable(NetBoxTable): sa_lifetime_data = tables.Column( verbose_name=_('SA Lifetime (KB)') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:ipsecproposal_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IPSecProposal fields = ( 'pk', 'id', 'name', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', @@ -124,7 +115,7 @@ class Meta(NetBoxTable.Meta): ) -class IPSecPolicyTable(NetBoxTable): +class IPSecPolicyTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -136,14 +127,11 @@ class IPSecPolicyTable(NetBoxTable): pfs_group = tables.Column( verbose_name=_('PFS Group') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:ipsecpolicy_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IPSecPolicy fields = ( 'pk', 'id', 'name', 'proposals', 'pfs_group', 'description', 'comments', 'tags', 'created', 'last_updated', @@ -153,7 +141,7 @@ class Meta(NetBoxTable.Meta): ) -class IPSecProfileTable(NetBoxTable): +class IPSecProfileTable(PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -169,14 +157,11 @@ class IPSecProfileTable(NetBoxTable): linkify=True, verbose_name=_('IPSec Policy') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:ipsecprofile_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = IPSecProfile fields = ( 'pk', 'id', 'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', 'created', diff --git a/netbox/vpn/tables/l2vpn.py b/netbox/vpn/tables/l2vpn.py index 95586461e48..72f4201c4f4 100644 --- a/netbox/vpn/tables/l2vpn.py +++ b/netbox/vpn/tables/l2vpn.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django.utils.translation import gettext_lazy as _ -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin from vpn.models import L2VPN, L2VPNTermination @@ -17,7 +17,7 @@ """ -class L2VPNTable(TenancyColumnsMixin, NetBoxTable): +class L2VPNTable(TenancyColumnsMixin, PrimaryModelTable): pk = columns.ToggleColumn() name = tables.Column( verbose_name=_('Name'), @@ -36,14 +36,11 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable): template_code=L2VPN_TARGETS, orderable=False ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:l2vpn_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = L2VPN fields = ( 'pk', 'name', 'slug', 'status', 'identifier', 'type', 'import_targets', 'export_targets', 'tenant', diff --git a/netbox/vpn/tables/tunnels.py b/netbox/vpn/tables/tunnels.py index fc8dec5e47d..4cbe440f6a0 100644 --- a/netbox/vpn/tables/tunnels.py +++ b/netbox/vpn/tables/tunnels.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from django_tables2.utils import Accessor -from netbox.tables import NetBoxTable, columns +from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin from vpn.models import * @@ -13,7 +13,7 @@ ) -class TunnelGroupTable(NetBoxTable): +class TunnelGroupTable(OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -27,7 +27,7 @@ class TunnelGroupTable(NetBoxTable): url_name='vpn:tunnelgroup_list' ) - class Meta(NetBoxTable.Meta): + class Meta(OrganizationalModelTable.Meta): model = TunnelGroup fields = ( 'pk', 'id', 'name', 'tunnel_count', 'description', 'slug', 'tags', 'actions', 'created', 'last_updated', @@ -35,7 +35,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'tunnel_count', 'description') -class TunnelTable(TenancyColumnsMixin, NetBoxTable): +class TunnelTable(TenancyColumnsMixin, PrimaryModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -57,14 +57,11 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable): url_params={'tunnel_id': 'pk'}, verbose_name=_('Terminations') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='vpn:tunnel_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = Tunnel fields = ( 'pk', 'id', 'name', 'group', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', diff --git a/netbox/wireless/tables/wirelesslan.py b/netbox/wireless/tables/wirelesslan.py index 24ad6434531..bc4926e82b8 100644 --- a/netbox/wireless/tables/wirelesslan.py +++ b/netbox/wireless/tables/wirelesslan.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from dcim.models import Interface -from netbox.tables import NetBoxTable, columns +from netbox.tables import NestedGroupModelTable, NetBoxTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin from wireless.models import * @@ -13,28 +13,17 @@ ) -class WirelessLANGroupTable(NetBoxTable): - name = columns.MPTTColumn( - verbose_name=_('Name'), - linkify=True - ) - parent = tables.Column( - verbose_name=_('Parent'), - linkify=True, - ) +class WirelessLANGroupTable(NestedGroupModelTable): wirelesslan_count = columns.LinkedCountColumn( viewname='wireless:wirelesslan_list', url_params={'group_id': 'pk'}, verbose_name=_('Wireless LANs') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='wireless:wirelesslangroup_list' ) - class Meta(NetBoxTable.Meta): + class Meta(NestedGroupModelTable.Meta): model = WirelessLANGroup fields = ( 'pk', 'name', 'parent', 'slug', 'description', 'comments', 'tags', 'wirelesslan_count', 'created', @@ -43,7 +32,7 @@ class Meta(NetBoxTable.Meta): default_columns = ('pk', 'name', 'wirelesslan_count', 'description') -class WirelessLANTable(TenancyColumnsMixin, NetBoxTable): +class WirelessLANTable(TenancyColumnsMixin, PrimaryModelTable): ssid = tables.Column( verbose_name=_('SSID'), linkify=True @@ -66,14 +55,11 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable): interface_count = tables.Column( verbose_name=_('Interfaces') ) - comments = columns.MarkdownColumn( - verbose_name=_('Comments'), - ) tags = columns.TagColumn( url_name='wireless:wirelesslan_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = WirelessLAN fields = ( 'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type', diff --git a/netbox/wireless/tables/wirelesslink.py b/netbox/wireless/tables/wirelesslink.py index dc8fb66db13..465ab08db28 100644 --- a/netbox/wireless/tables/wirelesslink.py +++ b/netbox/wireless/tables/wirelesslink.py @@ -1,7 +1,7 @@ -from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ -from netbox.tables import NetBoxTable, columns +from netbox.tables import PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin from wireless.models import * @@ -10,7 +10,7 @@ ) -class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable): +class WirelessLinkTable(TenancyColumnsMixin, PrimaryModelTable): id = tables.Column( linkify=True, verbose_name=_('ID') @@ -41,7 +41,7 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable): url_name='wireless:wirelesslink_list' ) - class Meta(NetBoxTable.Meta): + class Meta(PrimaryModelTable.Meta): model = WirelessLink fields = ( 'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'tenant', From 9cbda4d5b0834c2dbc4f383d381b7ff8a3111535 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 13:56:13 -0400 Subject: [PATCH 28/40] Fix owner filter --- netbox/dcim/forms/filtersets.py | 12 ++-- netbox/extras/forms/filtersets.py | 77 +++++++++++++++++++---- netbox/netbox/forms/filtersets.py | 28 ++++++--- netbox/netbox/forms/mixins.py | 2 +- netbox/virtualization/forms/filtersets.py | 18 ++++-- 5 files changed, 108 insertions(+), 29 deletions(-) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4d37460fd8e..30c46cb5a3b 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,11 +12,10 @@ NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm, ) -from netbox.forms.mixins import OwnerMixin from tenancy.forms import ContactModelFilterForm, TenancyFilterForm -from users.models import User +from users.models import Owner, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice -from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import NumberWithOptions from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -64,7 +63,7 @@ ) -class DeviceComponentFilterForm(OwnerMixin, NetBoxModelFilterSetForm): +class DeviceComponentFilterForm(NetBoxModelFilterSetForm): name = forms.CharField( label=_('Name'), required=False @@ -141,6 +140,11 @@ class DeviceComponentFilterForm(OwnerMixin, NetBoxModelFilterSetForm): required=False, label=_('Device Status'), ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 72b6587dc67..f8cd9119998 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -7,12 +7,13 @@ from extras.models import * from netbox.events import get_event_type_choices from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm -from netbox.forms.mixins import OwnerMixin, SavedFiltersMixin +from netbox.forms.mixins import SavedFiltersMixin from tenancy.models import Tenant, TenantGroup -from users.models import Group, User +from users.models import Group, Owner, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ( - ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, + ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, + TagFilterField, ) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker @@ -38,7 +39,7 @@ ) -class CustomFieldFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): model = CustomField fieldsets = ( FieldSet('q', 'filter_id'), @@ -115,9 +116,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): label=_('Validation regex'), required=False ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): model = CustomFieldChoiceSet fieldsets = ( FieldSet('q', 'filter_id'), @@ -130,9 +136,14 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): choice = forms.CharField( required=False ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class CustomLinkFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): model = CustomLink fieldsets = ( FieldSet('q', 'filter_id'), @@ -161,9 +172,14 @@ class CustomLinkFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): label=_('Weight'), required=False ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class ExportTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): model = ExportTemplate fieldsets = ( FieldSet('q', 'filter_id', 'object_type_id'), @@ -207,6 +223,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): @@ -226,7 +247,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): ) -class SavedFilterFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): model = SavedFilter fieldsets = ( FieldSet('q', 'filter_id'), @@ -255,6 +276,11 @@ class SavedFilterFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): label=_('Weight'), required=False ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) class TableConfigFilterForm(SavedFiltersMixin, FilterForm): @@ -287,7 +313,7 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm): ) -class WebhookFilterForm(OwnerMixin, NetBoxModelFilterSetForm): +class WebhookFilterForm(NetBoxModelFilterSetForm): model = Webhook fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -306,10 +332,15 @@ class WebhookFilterForm(OwnerMixin, NetBoxModelFilterSetForm): required=False, label=_('HTTP method') ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) tag = TagFilterField(model) -class EventRuleFilterForm(OwnerMixin, NetBoxModelFilterSetForm): +class EventRuleFilterForm(NetBoxModelFilterSetForm): model = EventRule fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -337,10 +368,15 @@ class EventRuleFilterForm(OwnerMixin, NetBoxModelFilterSetForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) tag = TagFilterField(model) -class TagFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class TagFilterForm(SavedFiltersMixin, FilterForm): model = Tag content_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('tags'), @@ -352,6 +388,11 @@ class TagFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): required=False, label=_('Allowed object type') ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): @@ -375,7 +416,7 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): ) -class ConfigContextFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): model = ConfigContext fieldsets = ( FieldSet('q', 'filter_id', 'tag_id'), @@ -469,9 +510,14 @@ class ConfigContextFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): required=False, label=_('Tags') ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class ConfigTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): +class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): model = ConfigTemplate fieldsets = ( FieldSet('q', 'filter_id', 'tag'), @@ -511,6 +557,11 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, OwnerMixin, FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) class LocalConfigContextFilterForm(forms.Form): diff --git a/netbox/netbox/forms/filtersets.py b/netbox/netbox/forms/filtersets.py index 5d0a84b5323..d5967c24bbc 100644 --- a/netbox/netbox/forms/filtersets.py +++ b/netbox/netbox/forms/filtersets.py @@ -3,7 +3,9 @@ from django.utils.translation import gettext_lazy as _ from extras.choices import * -from .mixins import CustomFieldsMixin, OwnerMixin, SavedFiltersMixin +from users.models import Owner +from utilities.forms.fields import DynamicModelChoiceField +from .mixins import CustomFieldsMixin, SavedFiltersMixin __all__ = ( 'NestedGroupModelFilterSetForm', @@ -42,22 +44,34 @@ def _get_form_field(self, customfield): return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False) -class PrimaryModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): +class PrimaryModelFilterSetForm(NetBoxModelFilterSetForm): """ FilterSet form for models which inherit from PrimaryModel. """ - pass + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class OrganizationalModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): +class OrganizationalModelFilterSetForm(NetBoxModelFilterSetForm): """ FilterSet form for models which inherit from OrganizationalModel. """ - pass + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) -class NestedGroupModelFilterSetForm(OwnerMixin, NetBoxModelFilterSetForm): +class NestedGroupModelFilterSetForm(NetBoxModelFilterSetForm): """ FilterSet form for models which inherit from NestedGroupModel. """ - pass + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 0f267cb0843..4ee11b0bbf7 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -126,7 +126,7 @@ class OwnerMixin(forms.Form): """ Add an `owner` field to forms for models which support Owner assignment. """ - owner_id = DynamicModelChoiceField( + owner = DynamicModelChoiceField( queryset=Owner.objects.all(), required=False, label=_('Owner'), diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 1b390307519..3e0db175e4f 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -7,10 +7,10 @@ from extras.models import ConfigTemplate from ipam.models import VRF, VLANTranslationPolicy from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm -from netbox.forms.mixins import OwnerMixin from tenancy.forms import ContactModelFilterForm, TenancyFilterForm +from users.models import Owner from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES -from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet from virtualization.choices import * from virtualization.models import * @@ -200,7 +200,7 @@ class VirtualMachineFilterForm( tag = TagFilterField(model) -class VMInterfaceFilterForm(OwnerMixin, NetBoxModelFilterSetForm): +class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -254,10 +254,15 @@ class VMInterfaceFilterForm(OwnerMixin, NetBoxModelFilterSetForm): required=False, label=_('VLAN Translation Policy') ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) tag = TagFilterField(model) -class VirtualDiskFilterForm(OwnerMixin, NetBoxModelFilterSetForm): +class VirtualDiskFilterForm(NetBoxModelFilterSetForm): model = VirtualDisk fieldsets = ( FieldSet('q', 'filter_id', 'tag', 'owner_id'), @@ -274,4 +279,9 @@ class VirtualDiskFilterForm(OwnerMixin, NetBoxModelFilterSetForm): required=False, min_value=1 ) + owner_id = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) tag = TagFilterField(model) From d154bac705ece3051d7a2a6121e0f4df2304a4d2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 14:40:49 -0400 Subject: [PATCH 29/40] Move owners under admin in nav menu --- netbox/netbox/navigation/menu.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 0c7fc22c149..dbe3ceac859 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -36,12 +36,6 @@ get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=['bulk_import']), ), ), - MenuGroup( - label=_('Ownership'), - items=( - get_model_item('users', 'owner', _('Owners')), - ), - ), ), ) @@ -471,6 +465,12 @@ ), ), ), + MenuGroup( + label=_('Ownership'), + items=( + get_model_item('users', 'owner', _('Owners')), + ), + ), MenuGroup( label=_('System'), items=( From b9cc93ad3fa0ac642232e583b6780fe01be8ae5d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 15:10:19 -0400 Subject: [PATCH 30/40] Misc cleanup --- netbox/templates/generic/object.html | 26 +++++++++++++++++--------- netbox/templates/users/group.html | 10 ++++++++++ netbox/templates/users/user.html | 10 ++++++++++ netbox/users/tables.py | 1 + 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index e7ef9d97eec..df95a4a42b4 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -55,15 +55,23 @@ {% block subtitle %}
- {% if object.owner %} - {{ object.owner|linkify }} - · - {% endif %} - {% trans "Created" %} {{ object.created|isodatetime:"minutes" }} - {% if object.last_updated %} - · - {% trans "Updated" %} {{ object.last_updated|isodatetime:"minutes" }} - {% endif %} +
    + {% if object.owner %} +
  • + {{ object.owner|linkify }} +
  • + {% endif %} +
  • + + {{ object.created|isodatetime:"minutes" }} +
  • + {% if object.last_updated %} +
  • + + {{ object.last_updated|isodatetime:"minutes" }} +
  • + {% endif %} +
{% endblock subtitle %} diff --git a/netbox/templates/users/group.html b/netbox/templates/users/group.html index 0a89f151ae0..9d4561c653d 100644 --- a/netbox/templates/users/group.html +++ b/netbox/templates/users/group.html @@ -45,6 +45,16 @@

{% trans "Assigned Permissions" %}

{% endfor %} +
+

{% trans "Owner Membership" %}

+
+ {% for owner in object.owners.all %} + {{ owner }} + {% empty %} +
{% trans "None" %}
+ {% endfor %} +
+
{% endblock %} diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 90a0bb084bd..efcc743f576 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -63,6 +63,16 @@

{% trans "Assigned Permissions" %}

{% endfor %} +
+

{% trans "Owner Membership" %}

+
+ {% for owner in object.owners.all %} + {{ owner }} + {% empty %} +
{% trans "None" %}
+ {% endfor %} +
+
{% if perms.core.view_objectchange %} diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 1e6fcec03dd..277b176fe59 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -168,3 +168,4 @@ class Meta(NetBoxTable.Meta): fields = ( 'pk', 'id', 'name', 'description', 'groups', 'users', ) + default_columns = ('pk', 'name', 'description', 'groups', 'users') From 3ca2a18a3fb99e9256ea9044c2411fbda4a7add2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 15:29:26 -0400 Subject: [PATCH 31/40] Introduce AdminModel base class to provide enhanced UI functionality for Owner --- netbox/netbox/models/__init__.py | 27 +++++++++++++++++++++++++-- netbox/users/models/owners.py | 9 +++------ netbox/utilities/querydict.py | 2 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index e6f1ecfd406..1e284674860 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -11,10 +11,9 @@ from netbox.models.mixins import OwnerMixin from utilities.mptt import TreeManager from utilities.querysets import RestrictedQuerySet -from utilities.views import get_viewname - __all__ = ( + 'AdminModel', 'ChangeLoggedModel', 'NestedGroupModel', 'NetBoxModel', @@ -44,6 +43,7 @@ def docs_url(self): return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/' def get_absolute_url(self): + from utilities.views import get_viewname return reverse(get_viewname(self), args=[self.pk]) @@ -222,3 +222,26 @@ class Meta: def __str__(self): return self.name + + +class AdminModel( + BookmarksMixin, + CloningMixin, + CustomLinksMixin, + CustomValidationMixin, + EventRulesMixin, + ExportTemplatesMixin, + NotificationsMixin, + BaseModel, +): + """ + A model which represents an administrative resource. + """ + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True + ) + + class Meta: + abstract = True diff --git a/netbox/users/models/owners.py b/netbox/users/models/owners.py index 6765d3034ef..bf24e43f867 100644 --- a/netbox/users/models/owners.py +++ b/netbox/users/models/owners.py @@ -2,6 +2,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from netbox.models import AdminModel from utilities.querysets import RestrictedQuerySet __all__ = ( @@ -9,17 +10,12 @@ ) -class Owner(models.Model): +class Owner(AdminModel): name = models.CharField( verbose_name=_('name'), max_length=150, unique=True, ) - description = models.CharField( - verbose_name=_('description'), - max_length=200, - blank=True - ) groups = models.ManyToManyField( to='users.Group', verbose_name=_('groups'), @@ -36,6 +32,7 @@ class Owner(models.Model): ) objects = RestrictedQuerySet.as_manager() + clone_fields = ('groups', 'users') class Meta: ordering = ('name',) diff --git a/netbox/utilities/querydict.py b/netbox/utilities/querydict.py index 73d40bfc4e8..17a0c8c2b80 100644 --- a/netbox/utilities/querydict.py +++ b/netbox/utilities/querydict.py @@ -2,7 +2,7 @@ from django.http import QueryDict from django.utils.datastructures import MultiValueDict -from netbox.models import CloningMixin +from netbox.models.features import CloningMixin __all__ = ( 'dict_to_querydict', From 1a6ea31538d1b8fbb9e7f466713feb5a1ba78596 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Oct 2025 16:34:27 -0400 Subject: [PATCH 32/40] Introduce OwnerGroup model --- contrib/openapi.json | 7340 +++++++++++++++++++++-- netbox/netbox/navigation/menu.py | 1 + netbox/templates/users/owner.html | 15 +- netbox/templates/users/ownergroup.html | 38 + netbox/users/api/serializers_/owners.py | 23 +- netbox/users/api/urls.py | 1 + netbox/users/api/views.py | 8 +- netbox/users/filtersets.py | 40 +- netbox/users/forms/bulk_edit.py | 29 +- netbox/users/forms/bulk_import.py | 21 +- netbox/users/forms/filtersets.py | 22 +- netbox/users/forms/model_forms.py | 32 +- netbox/users/graphql/filters.py | 14 +- netbox/users/graphql/schema.py | 3 + netbox/users/graphql/types.py | 17 +- netbox/users/migrations/0015_owner.py | 33 +- netbox/users/models/owners.py | 37 +- netbox/users/tables.py | 30 +- netbox/users/urls.py | 3 + netbox/users/views.py | 63 +- 20 files changed, 7102 insertions(+), 668 deletions(-) create mode 100644 netbox/templates/users/ownergroup.html diff --git a/contrib/openapi.json b/contrib/openapi.json index d0a486f9768..34263e6d299 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -1633,6 +1633,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -5010,6 +5064,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -6602,6 +6710,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "provider", @@ -8635,6 +8797,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "provider", @@ -9755,6 +9971,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "provider", @@ -11160,6 +11430,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -13982,6 +14306,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -15201,6 +15579,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "provider", @@ -18071,6 +18503,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -22821,6 +23307,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "powerfeed_id", @@ -26407,6 +26947,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -29838,6 +30432,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -33182,6 +33830,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -34729,6 +35431,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -36804,6 +37560,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "part_number", @@ -40219,6 +41029,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_bay_id", @@ -45184,6 +46048,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -50129,6 +51047,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_id", @@ -53459,6 +54431,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -57108,6 +58134,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_id", @@ -59250,6 +60330,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -61105,6 +62239,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -62326,6 +63514,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -65501,6 +66743,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_id", @@ -67000,6 +68296,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -68162,6 +69512,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "part_number", @@ -69794,6 +71198,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -71511,6 +72969,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -73109,6 +74621,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "phase", @@ -77232,6 +78798,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "power_port_id", @@ -79070,6 +80690,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -82499,6 +84173,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -83943,6 +85671,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -85702,6 +87484,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -87582,6 +89418,60 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -90353,6 +92243,60 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -95073,6 +97017,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "positions", @@ -96805,6 +98803,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -98202,6 +100254,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -99978,6 +102084,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -101946,6 +104106,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -103385,6 +105599,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "primary_ip4", @@ -105724,6 +107992,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -107262,6 +109584,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "platform", @@ -109448,6 +111824,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -110676,6 +113106,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -112217,6 +114701,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -114299,6 +116837,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -115930,6 +118522,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -117714,6 +120360,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -122076,6 +124776,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -126301,6 +129055,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -128022,6 +130830,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "payload_url", @@ -129277,6 +132139,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "prefix", @@ -130592,6 +133508,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -132087,6 +135057,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "provider", @@ -134854,6 +137878,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "protocol", @@ -136463,6 +139541,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -138148,6 +141280,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -140028,6 +143214,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "prefix", @@ -142123,6 +145363,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -143342,6 +146636,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -144878,6 +148226,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -146048,6 +149450,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "port", @@ -147357,6 +150813,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_object_id", @@ -148640,6 +152150,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -150191,6 +153755,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -152540,6 +156158,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -154610,6 +158282,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -157142,6 +160868,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -158415,6 +162195,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -160135,6 +163969,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "phone", @@ -161553,6 +165441,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -162950,6 +166892,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -164027,6 +168023,58 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "permission_id", @@ -164474,10 +168522,10 @@ } } }, - "/api/users/owners/": { + "/api/users/owner-groups/": { "get": { - "operationId": "users_owners_list", - "description": "Get a list of owner objects.", + "operationId": "users_owner_groups_list", + "description": "Get a list of owner group objects.", "parameters": [ { "in": "query", @@ -164630,58 +168678,6 @@ "explode": true, "style": "form" }, - { - "in": "query", - "name": "group", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "Group (name)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "group__n", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "Group (name)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "group_id", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "description": "Group (ID)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "group_id__n", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "description": "Group (ID)", - "explode": true, - "style": "form" - }, { "in": "query", "name": "id", @@ -164952,58 +168948,6 @@ "type": "string" }, "description": "Search" - }, - { - "in": "query", - "name": "user", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "User (username)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "user__n", - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "User (username)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "user_id", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "description": "User (ID)", - "explode": true, - "style": "form" - }, - { - "in": "query", - "name": "user_id__n", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "description": "User (ID)", - "explode": true, - "style": "form" } ], "tags": [ @@ -165022,7 +168966,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedOwnerList" + "$ref": "#/components/schemas/PaginatedOwnerGroupList" } } }, @@ -165031,8 +168975,8 @@ } }, "post": { - "operationId": "users_owners_create", - "description": "Post a list of owner objects.", + "operationId": "users_owner_groups_create", + "description": "Post a list of owner group objects.", "tags": [ "users" ], @@ -165040,12 +168984,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } }, @@ -165064,7 +169008,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } }, @@ -165073,8 +169017,8 @@ } }, "put": { - "operationId": "users_owners_bulk_update", - "description": "Put a list of owner objects.", + "operationId": "users_owner_groups_bulk_update", + "description": "Put a list of owner group objects.", "tags": [ "users" ], @@ -165084,7 +169028,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } }, @@ -165092,7 +169036,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } } @@ -165114,7 +169058,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } } @@ -165124,8 +169068,8 @@ } }, "patch": { - "operationId": "users_owners_bulk_partial_update", - "description": "Patch a list of owner objects.", + "operationId": "users_owner_groups_bulk_partial_update", + "description": "Patch a list of owner group objects.", "tags": [ "users" ], @@ -165135,7 +169079,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } }, @@ -165143,7 +169087,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } } @@ -165165,7 +169109,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } } @@ -165175,8 +169119,8 @@ } }, "delete": { - "operationId": "users_owners_bulk_destroy", - "description": "Delete a list of owner objects.", + "operationId": "users_owner_groups_bulk_destroy", + "description": "Delete a list of owner group objects.", "tags": [ "users" ], @@ -165186,7 +169130,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } }, @@ -165194,7 +169138,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } } @@ -165216,10 +169160,10 @@ } } }, - "/api/users/owners/{id}/": { + "/api/users/owner-groups/{id}/": { "get": { - "operationId": "users_owners_retrieve", - "description": "Get a owner object.", + "operationId": "users_owner_groups_retrieve", + "description": "Get a owner group object.", "parameters": [ { "in": "path", @@ -165227,7 +169171,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this owner.", + "description": "A unique integer value identifying this owner group.", "required": true } ], @@ -165247,7 +169191,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } }, @@ -165256,8 +169200,8 @@ } }, "put": { - "operationId": "users_owners_update", - "description": "Put a owner object.", + "operationId": "users_owner_groups_update", + "description": "Put a owner group object.", "parameters": [ { "in": "path", @@ -165265,7 +169209,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this owner.", + "description": "A unique integer value identifying this owner group.", "required": true } ], @@ -165276,12 +169220,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/OwnerRequest" + "$ref": "#/components/schemas/OwnerGroupRequest" } } }, @@ -165300,7 +169244,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } }, @@ -165309,8 +169253,8 @@ } }, "patch": { - "operationId": "users_owners_partial_update", - "description": "Patch a owner object.", + "operationId": "users_owner_groups_partial_update", + "description": "Patch a owner group object.", "parameters": [ { "in": "path", @@ -165318,7 +169262,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this owner.", + "description": "A unique integer value identifying this owner group.", "required": true } ], @@ -165329,12 +169273,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PatchedOwnerRequest" + "$ref": "#/components/schemas/PatchedOwnerGroupRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/PatchedOwnerRequest" + "$ref": "#/components/schemas/PatchedOwnerGroupRequest" } } } @@ -165352,7 +169296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Owner" + "$ref": "#/components/schemas/OwnerGroup" } } }, @@ -165361,8 +169305,8 @@ } }, "delete": { - "operationId": "users_owners_destroy", - "description": "Delete a owner object.", + "operationId": "users_owner_groups_destroy", + "description": "Delete a owner group object.", "parameters": [ { "in": "path", @@ -165370,7 +169314,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this owner.", + "description": "A unique integer value identifying this owner group.", "required": true } ], @@ -165392,39 +169336,11 @@ } } }, - "/api/users/permissions/": { + "/api/users/owners/": { "get": { - "operationId": "users_permissions_list", - "description": "Get a list of permission objects.", + "operationId": "users_owners_list", + "description": "Get a list of owner objects.", "parameters": [ - { - "in": "query", - "name": "can_add", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_change", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_delete", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "can_view", - "schema": { - "type": "boolean" - } - }, { "in": "query", "name": "description", @@ -165576,13 +169492,6 @@ "explode": true, "style": "form" }, - { - "in": "query", - "name": "enabled", - "schema": { - "type": "boolean" - } - }, { "in": "query", "name": "group", @@ -165615,10 +169524,11 @@ "schema": { "type": "array", "items": { - "type": "integer" + "type": "integer", + "nullable": true } }, - "description": "Group", + "description": "Group (ID)", "explode": true, "style": "form" }, @@ -165628,10 +169538,11 @@ "schema": { "type": "array", "items": { - "type": "integer" + "type": "integer", + "nullable": true } }, - "description": "Group", + "description": "Group (ID)", "explode": true, "style": "form" }, @@ -165881,186 +169792,106 @@ "style": "form" }, { + "name": "offset", + "required": false, "in": "query", - "name": "object_type", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__ic", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__ie", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__iew", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__iregex", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__isw", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__n", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__nic", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__nie", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "object_type__niew", + "description": "The initial index from which to return the results.", "schema": { - "type": "string" + "type": "integer" } }, { + "name": "ordering", + "required": false, "in": "query", - "name": "object_type__nisw", + "description": "Which field to use when ordering the results.", "schema": { "type": "string" } }, { "in": "query", - "name": "object_type__regex", + "name": "q", "schema": { "type": "string" - } + }, + "description": "Search" }, { "in": "query", - "name": "object_type_id", + "name": "user", "schema": { "type": "array", "items": { - "type": "integer" + "type": "string" } }, + "description": "User (username)", "explode": true, "style": "form" }, { "in": "query", - "name": "object_type_id__n", + "name": "user__n", "schema": { "type": "array", "items": { - "type": "integer" + "type": "string" } }, + "description": "User (username)", "explode": true, "style": "form" }, { "in": "query", - "name": "object_types", + "name": "user_group", "schema": { "type": "array", "items": { - "type": "integer" + "type": "string" } }, + "description": "User group (name)", "explode": true, "style": "form" }, { "in": "query", - "name": "object_types__n", + "name": "user_group__n", "schema": { "type": "array", "items": { - "type": "integer" + "type": "string" } }, + "description": "User group (name)", "explode": true, "style": "form" }, - { - "name": "offset", - "required": false, - "in": "query", - "description": "The initial index from which to return the results.", - "schema": { - "type": "integer" - } - }, - { - "name": "ordering", - "required": false, - "in": "query", - "description": "Which field to use when ordering the results.", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "q", - "schema": { - "type": "string" - }, - "description": "Search" - }, { "in": "query", - "name": "user", + "name": "user_group_id", "schema": { "type": "array", "items": { - "type": "string" + "type": "integer" } }, - "description": "User (name)", + "description": "User group (ID)", "explode": true, "style": "form" }, { "in": "query", - "name": "user__n", + "name": "user_group_id__n", "schema": { "type": "array", "items": { - "type": "string" + "type": "integer" } }, - "description": "User (name)", + "description": "User group (ID)", "explode": true, "style": "form" }, @@ -166073,7 +169904,7 @@ "type": "integer" } }, - "description": "User", + "description": "User (ID)", "explode": true, "style": "form" }, @@ -166086,7 +169917,7 @@ "type": "integer" } }, - "description": "User", + "description": "User (ID)", "explode": true, "style": "form" } @@ -166107,7 +169938,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedObjectPermissionList" + "$ref": "#/components/schemas/PaginatedOwnerList" } } }, @@ -166116,8 +169947,8 @@ } }, "post": { - "operationId": "users_permissions_create", - "description": "Post a list of permission objects.", + "operationId": "users_owners_create", + "description": "Post a list of owner objects.", "tags": [ "users" ], @@ -166125,12 +169956,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -166149,7 +169980,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -166158,8 +169989,8 @@ } }, "put": { - "operationId": "users_permissions_bulk_update", - "description": "Put a list of permission objects.", + "operationId": "users_owners_bulk_update", + "description": "Put a list of owner objects.", "tags": [ "users" ], @@ -166169,7 +170000,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -166177,7 +170008,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -166199,7 +170030,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } } @@ -166209,8 +170040,8 @@ } }, "patch": { - "operationId": "users_permissions_bulk_partial_update", - "description": "Patch a list of permission objects.", + "operationId": "users_owners_bulk_partial_update", + "description": "Patch a list of owner objects.", "tags": [ "users" ], @@ -166220,7 +170051,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -166228,7 +170059,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -166250,7 +170081,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } } @@ -166260,8 +170091,8 @@ } }, "delete": { - "operationId": "users_permissions_bulk_destroy", - "description": "Delete a list of permission objects.", + "operationId": "users_owners_bulk_destroy", + "description": "Delete a list of owner objects.", "tags": [ "users" ], @@ -166271,7 +170102,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -166279,7 +170110,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } } @@ -166301,10 +170132,10 @@ } } }, - "/api/users/permissions/{id}/": { + "/api/users/owners/{id}/": { "get": { - "operationId": "users_permissions_retrieve", - "description": "Get a permission object.", + "operationId": "users_owners_retrieve", + "description": "Get a owner object.", "parameters": [ { "in": "path", @@ -166312,7 +170143,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -166332,7 +170163,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -166341,8 +170172,8 @@ } }, "put": { - "operationId": "users_permissions_update", - "description": "Put a permission object.", + "operationId": "users_owners_update", + "description": "Put a owner object.", "parameters": [ { "in": "path", @@ -166350,7 +170181,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -166361,12 +170192,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/ObjectPermissionRequest" + "$ref": "#/components/schemas/OwnerRequest" } } }, @@ -166385,7 +170216,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -166394,8 +170225,8 @@ } }, "patch": { - "operationId": "users_permissions_partial_update", - "description": "Patch a permission object.", + "operationId": "users_owners_partial_update", + "description": "Patch a owner object.", "parameters": [ { "in": "path", @@ -166403,7 +170234,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -166414,12 +170245,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + "$ref": "#/components/schemas/PatchedOwnerRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + "$ref": "#/components/schemas/PatchedOwnerRequest" } } } @@ -166437,7 +170268,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ObjectPermission" + "$ref": "#/components/schemas/Owner" } } }, @@ -166446,8 +170277,8 @@ } }, "delete": { - "operationId": "users_permissions_destroy", - "description": "Delete a permission object.", + "operationId": "users_owners_destroy", + "description": "Delete a owner object.", "parameters": [ { "in": "path", @@ -166455,7 +170286,7 @@ "schema": { "type": "integer" }, - "description": "A unique integer value identifying this permission.", + "description": "A unique integer value identifying this owner.", "required": true } ], @@ -166477,33 +170308,37 @@ } } }, - "/api/users/tokens/": { + "/api/users/permissions/": { "get": { - "operationId": "users_tokens_list", - "description": "Get a list of token objects.", + "operationId": "users_permissions_list", + "description": "Get a list of permission objects.", "parameters": [ { "in": "query", - "name": "created", + "name": "can_add", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "created__gte", + "name": "can_change", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "created__lte", + "name": "can_delete", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" + } + }, + { + "in": "query", + "name": "can_view", + "schema": { + "type": "boolean" } }, { @@ -166659,27 +170494,62 @@ }, { "in": "query", - "name": "expires", + "name": "enabled", "schema": { - "type": "string", - "format": "date-time" + "type": "boolean" } }, { "in": "query", - "name": "expires__gte", + "name": "group", "schema": { - "type": "string", - "format": "date-time" - } + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Group (name)", + "explode": true, + "style": "form" }, { "in": "query", - "name": "expires__lte", + "name": "group__n", "schema": { - "type": "string", - "format": "date-time" - } + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Group", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Group", + "explode": true, + "style": "form" }, { "in": "query", @@ -166767,8 +170637,17 @@ "style": "form" }, { + "name": "limit", + "required": false, "in": "query", - "name": "key", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "name", "schema": { "type": "array", "items": { @@ -166780,14 +170659,14 @@ }, { "in": "query", - "name": "key__empty", + "name": "name__empty", "schema": { "type": "boolean" } }, { "in": "query", - "name": "key__ic", + "name": "name__ic", "schema": { "type": "array", "items": { @@ -166799,7 +170678,7 @@ }, { "in": "query", - "name": "key__ie", + "name": "name__ie", "schema": { "type": "array", "items": { @@ -166811,7 +170690,7 @@ }, { "in": "query", - "name": "key__iew", + "name": "name__iew", "schema": { "type": "array", "items": { @@ -166823,7 +170702,7 @@ }, { "in": "query", - "name": "key__iregex", + "name": "name__iregex", "schema": { "type": "array", "items": { @@ -166835,7 +170714,7 @@ }, { "in": "query", - "name": "key__isw", + "name": "name__isw", "schema": { "type": "array", "items": { @@ -166847,7 +170726,7 @@ }, { "in": "query", - "name": "key__n", + "name": "name__n", "schema": { "type": "array", "items": { @@ -166859,7 +170738,7 @@ }, { "in": "query", - "name": "key__nic", + "name": "name__nic", "schema": { "type": "array", "items": { @@ -166871,7 +170750,7 @@ }, { "in": "query", - "name": "key__nie", + "name": "name__nie", "schema": { "type": "array", "items": { @@ -166883,7 +170762,7 @@ }, { "in": "query", - "name": "key__niew", + "name": "name__niew", "schema": { "type": "array", "items": { @@ -166895,7 +170774,7 @@ }, { "in": "query", - "name": "key__nisw", + "name": "name__nisw", "schema": { "type": "array", "items": { @@ -166907,7 +170786,7 @@ }, { "in": "query", - "name": "key__regex", + "name": "name__regex", "schema": { "type": "array", "items": { @@ -166919,96 +170798,95 @@ }, { "in": "query", - "name": "last_used", + "name": "object_type", "schema": { - "type": "string", - "format": "date-time" + "type": "string" } }, { "in": "query", - "name": "last_used__gte", + "name": "object_type__ic", "schema": { - "type": "string", - "format": "date-time" + "type": "string" } }, { "in": "query", - "name": "last_used__lte", + "name": "object_type__ie", "schema": { - "type": "string", - "format": "date-time" + "type": "string" } }, { - "name": "limit", - "required": false, "in": "query", - "description": "Number of results to return per page.", + "name": "object_type__iew", "schema": { - "type": "integer" + "type": "string" } }, { - "name": "offset", - "required": false, "in": "query", - "description": "The initial index from which to return the results.", + "name": "object_type__iregex", "schema": { - "type": "integer" + "type": "string" } }, { - "name": "ordering", - "required": false, "in": "query", - "description": "Which field to use when ordering the results.", + "name": "object_type__isw", "schema": { "type": "string" } }, { "in": "query", - "name": "pepper_id", + "name": "object_type__n", "schema": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - } - }, - "explode": true, - "style": "form" + "type": "string" + } }, { "in": "query", - "name": "pepper_id__empty", + "name": "object_type__nic", "schema": { - "type": "boolean" + "type": "string" } }, { "in": "query", - "name": "pepper_id__gt", + "name": "object_type__nie", "schema": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - } - }, - "explode": true, - "style": "form" + "type": "string" + } }, { "in": "query", - "name": "pepper_id__gte", + "name": "object_type__niew", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__nisw", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type__regex", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "object_type_id", "schema": { "type": "array", "items": { - "type": "integer", - "format": "int32" + "type": "integer" } }, "explode": true, @@ -167016,12 +170894,11 @@ }, { "in": "query", - "name": "pepper_id__lt", + "name": "object_type_id__n", "schema": { "type": "array", "items": { - "type": "integer", - "format": "int32" + "type": "integer" } }, "explode": true, @@ -167029,12 +170906,11 @@ }, { "in": "query", - "name": "pepper_id__lte", + "name": "object_types", "schema": { "type": "array", "items": { - "type": "integer", - "format": "int32" + "type": "integer" } }, "explode": true, @@ -167042,17 +170918,34 @@ }, { "in": "query", - "name": "pepper_id__n", + "name": "object_types__n", "schema": { "type": "array", "items": { - "type": "integer", - "format": "int32" + "type": "integer" } }, "explode": true, "style": "form" }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "q", @@ -167112,26 +171005,6 @@ "description": "User", "explode": true, "style": "form" - }, - { - "in": "query", - "name": "version", - "schema": { - "type": "integer", - "x-spec-enum-id": "b5df70f0bffd12cb", - "enum": [ - 1, - 2 - ] - }, - "description": "* `1` - v1\n* `2` - v2" - }, - { - "in": "query", - "name": "write_enabled", - "schema": { - "type": "boolean" - } } ], "tags": [ @@ -167150,7 +171023,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaginatedTokenList" + "$ref": "#/components/schemas/PaginatedObjectPermissionList" } } }, @@ -167159,8 +171032,8 @@ } }, "post": { - "operationId": "users_tokens_create", - "description": "Post a list of token objects.", + "operationId": "users_permissions_create", + "description": "Post a list of permission objects.", "tags": [ "users" ], @@ -167168,12 +171041,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokenRequest" + "$ref": "#/components/schemas/ObjectPermissionRequest" } }, "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/TokenRequest" + "$ref": "#/components/schemas/ObjectPermissionRequest" } } }, @@ -167192,7 +171065,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Token" + "$ref": "#/components/schemas/ObjectPermission" } } }, @@ -167201,8 +171074,8 @@ } }, "put": { - "operationId": "users_tokens_bulk_update", - "description": "Put a list of token objects.", + "operationId": "users_permissions_bulk_update", + "description": "Put a list of permission objects.", "tags": [ "users" ], @@ -167212,7 +171085,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/TokenRequest" + "$ref": "#/components/schemas/ObjectPermissionRequest" } } }, @@ -167220,7 +171093,1050 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/TokenRequest" + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "users_permissions_bulk_partial_update", + "description": "Patch a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "users_permissions_bulk_destroy", + "description": "Delete a list of permission objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, + "/api/users/permissions/{id}/": { + "get": { + "operationId": "users_permissions_retrieve", + "description": "Get a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "users_permissions_update", + "description": "Put a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/ObjectPermissionRequest" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "users_permissions_partial_update", + "description": "Patch a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedObjectPermissionRequest" + } + } + } + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObjectPermission" + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "users_permissions_destroy", + "description": "Delete a permission object.", + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "integer" + }, + "description": "A unique integer value identifying this permission.", + "required": true + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, + "/api/users/tokens/": { + "get": { + "operationId": "users_tokens_list", + "description": "Get a list of token objects.", + "parameters": [ + { + "in": "query", + "name": "created", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "created__gte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "created__lte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "description", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "description__ic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__ie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__iew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__iregex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__isw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__niew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__nisw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "description__regex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "expires", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "expires__gte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "expires__lte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "id__gt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__gte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__lt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__lte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "key__ic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__ie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__iew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__iregex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__isw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__nic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__nie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__niew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__nisw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "key__regex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "last_used", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "last_used__gte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "last_used__lte", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pepper_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "pepper_id__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "pepper_id__gt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "pepper_id__gte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "pepper_id__lt", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "pepper_id__lte", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "pepper_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "q", + "schema": { + "type": "string" + }, + "description": "Search" + }, + { + "in": "query", + "name": "user", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "User (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "User (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "User", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "user_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "User", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "version", + "schema": { + "type": "integer", + "x-spec-enum-id": "b5df70f0bffd12cb", + "enum": [ + 1, + 2 + ] + }, + "description": "* `1` - v1\n* `2` - v2" + }, + { + "in": "query", + "name": "write_enabled", + "schema": { + "type": "boolean" + } + } + ], + "tags": [ + "users" + ], + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedTokenList" + } + } + }, + "description": "" + } + } + }, + "post": { + "operationId": "users_tokens_create", + "description": "Post a list of token objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TokenRequest" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + }, + { + "tokenAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + }, + "description": "" + } + } + }, + "put": { + "operationId": "users_tokens_bulk_update", + "description": "Put a list of token objects.", + "tags": [ + "users" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TokenRequest" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TokenRequest" } } } @@ -168407,6 +173323,58 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "permission_id", @@ -169672,6 +174640,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -170891,6 +175913,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -172288,6 +177364,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -174482,6 +179612,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent_id", @@ -175804,6 +180988,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -177742,6 +182980,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "platform", @@ -180040,6 +185332,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "preshared_key", @@ -182069,6 +187415,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -183270,6 +188670,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "pfs_group", @@ -184785,6 +190239,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -186279,6 +191787,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -189067,6 +194629,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -190790,6 +196406,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -193566,6 +199236,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -195032,6 +200756,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "parent", @@ -196795,6 +202573,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -199075,6 +204907,60 @@ "type": "string" } }, + { + "in": "query", + "name": "owner", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer", + "nullable": true + } + }, + "description": "Owner (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "q", @@ -203149,7 +209035,7 @@ }, "name": { "type": "string", - "maxLength": 150 + "maxLength": 100 }, "description": { "type": "string", @@ -203163,6 +209049,57 @@ "url" ] }, + "BriefOwnerGroup": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "display", + "id", + "name", + "url" + ] + }, + "BriefOwnerGroupRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": [ + "name" + ] + }, "BriefOwnerRequest": { "type": "object", "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", @@ -203170,7 +209107,7 @@ "name": { "type": "string", "minLength": 1, - "maxLength": 150 + "maxLength": 100 }, "description": { "type": "string", @@ -223756,106 +229693,245 @@ "type": "string", "maxLength": 200 }, - "enabled": { - "type": "boolean" - }, - "object_types": { - "type": "array", - "items": { - "type": "string" - } - }, - "actions": { - "type": "array", - "items": { - "type": "string", - "maxLength": 30 - }, - "description": "The list of actions granted by this permission" - }, - "constraints": { - "nullable": true, - "description": "Queryset filter matching the applicable objects of the selected type(s)" - }, - "groups": { + "enabled": { + "type": "boolean" + }, + "object_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "actions": { + "type": "array", + "items": { + "type": "string", + "maxLength": 30 + }, + "description": "The list of actions granted by this permission" + }, + "constraints": { + "nullable": true, + "description": "Queryset filter matching the applicable objects of the selected type(s)" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedGroup" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedUser" + } + } + }, + "required": [ + "actions", + "display", + "display_url", + "id", + "name", + "object_types", + "url" + ] + }, + "ObjectPermissionRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "object_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "actions": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 30 + }, + "description": "The list of actions granted by this permission" + }, + "constraints": { + "nullable": true, + "description": "Queryset filter matching the applicable objects of the selected type(s)" + }, + "groups": { + "type": "array", + "items": { + "type": "integer" + } + }, + "users": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "actions", + "name", + "object_types" + ] + }, + "ObjectType": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "app_label": { + "type": "string", + "maxLength": 100 + }, + "app_name": { + "type": "string", + "readOnly": true + }, + "model": { + "type": "string", + "title": "Python model class name", + "maxLength": 100 + }, + "model_name": { + "type": "string", + "readOnly": true + }, + "model_name_plural": { + "type": "string", + "readOnly": true + }, + "public": { + "type": "boolean", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": "string", + "maxLength": 50 + }, + "readOnly": true + }, + "is_plugin_model": { + "type": "boolean", + "readOnly": true + }, + "rest_api_endpoint": { + "type": "string", + "readOnly": true + }, + "description": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "app_label", + "app_name", + "description", + "display", + "features", + "id", + "is_plugin_model", + "model", + "model_name", + "model_name_plural", + "public", + "rest_api_endpoint", + "url" + ] + }, + "Owner": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "id": { + "type": "integer", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display_url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "display": { + "type": "string", + "readOnly": true + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "group": { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerGroup" + } + ], + "nullable": true + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "user_groups": { "type": "array", "items": { - "$ref": "#/components/schemas/NestedGroup" + "$ref": "#/components/schemas/Group" } }, "users": { "type": "array", "items": { - "$ref": "#/components/schemas/NestedUser" + "$ref": "#/components/schemas/User" } } }, "required": [ - "actions", "display", "display_url", + "group", "id", "name", - "object_types", "url" ] }, - "ObjectPermissionRequest": { + "OwnerGroup": { "type": "object", "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 200 - }, - "enabled": { - "type": "boolean" - }, - "object_types": { - "type": "array", - "items": { - "type": "string" - } - }, - "actions": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 30 - }, - "description": "The list of actions granted by this permission" - }, - "constraints": { - "nullable": true, - "description": "Queryset filter matching the applicable objects of the selected type(s)" - }, - "groups": { - "type": "array", - "items": { - "type": "integer" - } - }, - "users": { - "type": "array", - "items": { - "type": "integer" - } - } - }, - "required": [ - "actions", - "name", - "object_types" - ] - }, - "ObjectType": { - "type": "object", "properties": { "id": { "type": "integer", @@ -223866,121 +229942,54 @@ "format": "uri", "readOnly": true }, - "display": { + "display_url": { "type": "string", + "format": "uri", "readOnly": true }, - "app_label": { - "type": "string", - "maxLength": 100 - }, - "app_name": { + "display": { "type": "string", "readOnly": true }, - "model": { + "name": { "type": "string", - "title": "Python model class name", "maxLength": 100 }, - "model_name": { - "type": "string", - "readOnly": true - }, - "model_name_plural": { - "type": "string", - "readOnly": true - }, - "public": { - "type": "boolean", - "readOnly": true - }, - "features": { - "type": "array", - "items": { - "type": "string", - "maxLength": 50 - }, - "readOnly": true - }, - "is_plugin_model": { - "type": "boolean", - "readOnly": true - }, - "rest_api_endpoint": { - "type": "string", - "readOnly": true - }, "description": { "type": "string", + "maxLength": 200 + }, + "member_count": { + "type": "integer", + "format": "int64", "readOnly": true } }, "required": [ - "app_label", - "app_name", - "description", "display", - "features", + "display_url", "id", - "is_plugin_model", - "model", - "model_name", - "model_name_plural", - "public", - "rest_api_endpoint", + "member_count", + "name", "url" ] }, - "Owner": { + "OwnerGroupRequest": { "type": "object", "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", "properties": { - "id": { - "type": "integer", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display_url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "display": { - "type": "string", - "readOnly": true - }, "name": { "type": "string", - "maxLength": 150 + "minLength": 1, + "maxLength": 100 }, "description": { "type": "string", "maxLength": 200 - }, - "groups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Group" - } - }, - "users": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } } }, "required": [ - "display", - "display_url", - "id", - "name", - "url" + "name" ] }, "OwnerRequest": { @@ -223990,13 +229999,29 @@ "name": { "type": "string", "minLength": 1, - "maxLength": 150 + "maxLength": 100 + }, + "group": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerGroupRequest" + } + ], + "nullable": true + } + ], + "nullable": true }, "description": { "type": "string", "maxLength": 200 }, - "groups": { + "user_groups": { "type": "array", "items": { "type": "integer" @@ -224010,6 +230035,7 @@ } }, "required": [ + "group", "name" ] }, @@ -226245,6 +232271,37 @@ } } }, + "PaginatedOwnerGroupList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 + }, + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" + }, + "previous": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OwnerGroup" + } + } + } + }, "PaginatedOwnerList": { "type": "object", "required": [ @@ -229868,6 +235925,21 @@ } } }, + "PatchedOwnerGroupRequest": { + "type": "object", + "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 200 + } + } + }, "PatchedOwnerRequest": { "type": "object", "description": "Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during\nvalidation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)", @@ -229875,13 +235947,29 @@ "name": { "type": "string", "minLength": 1, - "maxLength": 150 + "maxLength": 100 + }, + "group": { + "oneOf": [ + { + "type": "integer" + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/BriefOwnerGroupRequest" + } + ], + "nullable": true + } + ], + "nullable": true }, "description": { "type": "string", "maxLength": 200 }, - "groups": { + "user_groups": { "type": "array", "items": { "type": "integer" diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index dbe3ceac859..34b66ada0b0 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -468,6 +468,7 @@ MenuGroup( label=_('Ownership'), items=( + get_model_item('users', 'ownergroup', _('Owner Groups')), get_model_item('users', 'owner', _('Owners')), ), ), diff --git a/netbox/templates/users/owner.html b/netbox/templates/users/owner.html index 7aa9b2edd73..3e9d1c1255b 100644 --- a/netbox/templates/users/owner.html +++ b/netbox/templates/users/owner.html @@ -1,6 +1,15 @@ {% extends 'generic/object.html' %} {% load i18n %} +{% block breadcrumbs %} + {{ block.super }} + {% if object.group %} + + {% endif %} +{% endblock %} + {% block subtitle %}{% endblock %} {% block content %} @@ -13,6 +22,10 @@

{% trans "Owner" %}

{% trans "Name" %} {{ object.name }} + + {% trans "Group" %} + {{ object.group|linkify|placeholder }} + {% trans "Description" %} {{ object.description|placeholder }} @@ -22,7 +35,7 @@

{% trans "Owner" %}

{% trans "Groups" %}

- {% for group in object.groups.all %} + {% for group in object.user_groups.all %} {{ group }} {% empty %}
{% trans "None" %}
diff --git a/netbox/templates/users/ownergroup.html b/netbox/templates/users/ownergroup.html new file mode 100644 index 00000000000..bbd8e46c999 --- /dev/null +++ b/netbox/templates/users/ownergroup.html @@ -0,0 +1,38 @@ +{% extends 'generic/object.html' %} +{% load i18n %} +{% load helpers %} +{% load render_table from django_tables2 %} + +{% block subtitle %}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Group" %}

+ + + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
+
+
+
+
+

{% trans "Members" %}

+
+ {% for owner in object.members.all %} + {{ owner }} + {% empty %} +
{% trans "None" %}
+ {% endfor %} +
+
+
+
+{% endblock %} diff --git a/netbox/users/api/serializers_/owners.py b/netbox/users/api/serializers_/owners.py index b67d5b6c8c0..2d704d591f3 100644 --- a/netbox/users/api/serializers_/owners.py +++ b/netbox/users/api/serializers_/owners.py @@ -1,15 +1,30 @@ -from netbox.api.fields import SerializedPKRelatedField +from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from users.models import Group, Owner, User +from users.models import Group, Owner, OwnerGroup, User from .users import GroupSerializer, UserSerializer __all__ = ( + 'OwnerGroupSerializer', 'OwnerSerializer', ) +class OwnerGroupSerializer(ValidatedModelSerializer): + # Related object counts + member_count = RelatedObjectCountField('members') + + class Meta: + model = OwnerGroup + fields = ('id', 'url', 'display_url', 'display', 'name', 'description', 'member_count') + brief_fields = ('id', 'url', 'display', 'name', 'description') + + class OwnerSerializer(ValidatedModelSerializer): - groups = SerializedPKRelatedField( + group = OwnerGroupSerializer( + nested=True, + allow_null=True, + ) + user_groups = SerializedPKRelatedField( queryset=Group.objects.all(), serializer=GroupSerializer, nested=True, @@ -26,5 +41,5 @@ class OwnerSerializer(ValidatedModelSerializer): class Meta: model = Owner - fields = ('id', 'url', 'display_url', 'display', 'name', 'description', 'groups', 'users') + fields = ('id', 'url', 'display_url', 'display', 'name', 'group', 'description', 'user_groups', 'users') brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/users/api/urls.py b/netbox/users/api/urls.py index 87a5fde090f..8ee9edd5bc8 100644 --- a/netbox/users/api/urls.py +++ b/netbox/users/api/urls.py @@ -11,6 +11,7 @@ router.register('groups', views.GroupViewSet) router.register('tokens', views.TokenViewSet) router.register('permissions', views.ObjectPermissionViewSet) +router.register('owner-groups', views.OwnerGroupViewSet) router.register('owners', views.OwnerViewSet) router.register('config', views.UserConfigViewSet, basename='userconfig') diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index 651c2c8a756..de48dc17bc3 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -12,7 +12,7 @@ from netbox.api.viewsets import NetBoxModelViewSet from users import filtersets -from users.models import Group, ObjectPermission, Owner, Token, User, UserConfig +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User, UserConfig from utilities.data import deepmerge from utilities.querysets import RestrictedQuerySet from . import serializers @@ -92,6 +92,12 @@ class ObjectPermissionViewSet(NetBoxModelViewSet): # Owners # +class OwnerGroupViewSet(NetBoxModelViewSet): + queryset = OwnerGroup.objects.all() + serializer_class = serializers.OwnerGroupSerializer + filterset_class = filtersets.OwnerGroupFilterSet + + class OwnerViewSet(NetBoxModelViewSet): queryset = Owner.objects.all() serializer_class = serializers.OwnerSerializer diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index f94681443e7..c53166b5d3c 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -6,13 +6,14 @@ from core.models import ObjectType from extras.models import NotificationGroup from netbox.filtersets import BaseFilterSet -from users.models import Group, ObjectPermission, Owner, Token, User +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User from utilities.filters import ContentTypeFilter __all__ = ( 'GroupFilterSet', 'ObjectPermissionFilterSet', 'OwnerFilterSet', + 'OwnerGroupFilterSet', 'TokenFilterSet', 'UserFilterSet', ) @@ -246,22 +247,51 @@ def _check_action(self, queryset, name, value): return queryset.exclude(actions__contains=[action]) +class OwnerGroupFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + + class Meta: + model = OwnerGroup + fields = ('id', 'name', 'description') + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) + + class OwnerFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', label=_('Search'), ) group_id = django_filters.ModelMultipleChoiceFilter( - field_name='groups', - queryset=Group.objects.all(), + queryset=OwnerGroup.objects.all(), label=_('Group (ID)'), ) group = django_filters.ModelMultipleChoiceFilter( - field_name='groups__name', - queryset=Group.objects.all(), + field_name='group__name', + queryset=OwnerGroup.objects.all(), to_field_name='name', label=_('Group (name)'), ) + user_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='user_groups', + queryset=Group.objects.all(), + label=_('User group (ID)'), + ) + user_group = django_filters.ModelMultipleChoiceFilter( + field_name='user_groups__name', + queryset=Group.objects.all(), + to_field_name='name', + label=_('User group (name)'), + ) user_id = django_filters.ModelMultipleChoiceFilter( field_name='users', queryset=User.objects.all(), diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index a31593e734e..227711d9bc5 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -6,6 +6,7 @@ from ipam.validators import prefix_validator from users.models import * from utilities.forms import BulkEditForm +from utilities.forms.fields import DynamicModelChoiceField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker @@ -13,6 +14,7 @@ 'GroupBulkEditForm', 'ObjectPermissionBulkEditForm', 'OwnerBulkEditForm', + 'OwnerGroupBulkEditForm', 'UserBulkEditForm', 'TokenBulkEditForm', ) @@ -127,11 +129,34 @@ class TokenBulkEditForm(BulkEditForm): ) +class OwnerGroupBulkEditForm(BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=OwnerGroup.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + + model = OwnerGroup + fieldsets = ( + FieldSet('description',), + ) + nullable_fields = ('description',) + + class OwnerBulkEditForm(BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Owner.objects.all(), widget=forms.MultipleHiddenInput ) + group = DynamicModelChoiceField( + label=_('Group'), + queryset=OwnerGroup.objects.all(), + required=False + ) description = forms.CharField( label=_('Description'), max_length=200, @@ -140,6 +165,6 @@ class OwnerBulkEditForm(BulkEditForm): model = Owner fieldsets = ( - FieldSet('description',), + FieldSet('group', 'description'), ) - nullable_fields = ('description',) + nullable_fields = ('group', 'description',) diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index 045461239ab..776333c7bfa 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -3,11 +3,12 @@ from users.models import * from users.choices import TokenVersionChoices from utilities.forms import CSVModelForm -from utilities.forms.fields import CSVModelMultipleChoiceField +from utilities.forms.fields import CSVModelChoiceField, CSVModelMultipleChoiceField __all__ = ( 'GroupImportForm', + 'OwnerGroupImportForm', 'OwnerImportForm', 'UserImportForm', 'TokenImportForm', @@ -54,8 +55,22 @@ class Meta: fields = ('user', 'version', 'token', 'write_enabled', 'expires', 'description',) +class OwnerGroupImportForm(CSVModelForm): + + class Meta: + model = OwnerGroup + fields = ( + 'name', 'description', + ) + + class OwnerImportForm(CSVModelForm): - groups = CSVModelMultipleChoiceField( + group = CSVModelChoiceField( + queryset=OwnerGroup.objects.all(), + required=False, + to_field_name='name', + ) + user_groups = CSVModelMultipleChoiceField( queryset=Group.objects.all(), required=False, to_field_name='name', @@ -69,5 +84,5 @@ class OwnerImportForm(CSVModelForm): class Meta: model = Owner fields = ( - 'name', 'description', 'groups', 'users', + 'group', 'name', 'description', 'user_groups', 'users', ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 96a7eb317cd..df5bc4da1ba 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -4,7 +4,7 @@ from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin from users.choices import TokenVersionChoices -from users.models import Group, ObjectPermission, Owner, Token, User +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.rendering import FieldSet @@ -15,6 +15,7 @@ 'GroupFilterForm', 'ObjectPermissionFilterForm', 'OwnerFilterForm', + 'OwnerGroupFilterForm', 'TokenFilterForm', 'UserFilterForm', ) @@ -143,19 +144,32 @@ class TokenFilterForm(SavedFiltersMixin, FilterForm): ) +class OwnerGroupFilterForm(NetBoxModelFilterSetForm): + model = OwnerGroup + fieldsets = ( + FieldSet('q', 'filter_id',), + ) + + class OwnerFilterForm(NetBoxModelFilterSetForm): model = Owner fieldsets = ( FieldSet('q', 'filter_id',), - FieldSet('group_id', 'user_id', name=_('Members')), + FieldSet('group_id', name=_('Group')), + FieldSet('user_group_id', 'user_id', name=_('Membership')), ) group_id = DynamicModelMultipleChoiceField( - queryset=Group.objects.all(), + queryset=OwnerGroup.objects.all(), required=False, label=_('Group') ) + user_group_id = DynamicModelMultipleChoiceField( + queryset=Group.objects.all(), + required=False, + label=_('Groups') + ) user_id = DynamicModelMultipleChoiceField( queryset=User.objects.all(), required=False, - label=_('User') + label=_('Users') ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 4656129b5f8..cb3bd8594cb 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -15,7 +15,9 @@ from users.constants import * from users.models import * from utilities.data import flatten_dict -from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField +from utilities.forms.fields import ( + ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, +) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget from utilities.permissions import qs_filter_from_constraints @@ -24,6 +26,7 @@ 'GroupForm', 'ObjectPermissionForm', 'OwnerForm', + 'OwnerGroupForm', 'TokenForm', 'UserConfigForm', 'UserForm', @@ -433,16 +436,35 @@ def save(self, *args, **kwargs): return instance -class OwnerForm(forms.ModelForm): +class OwnerGroupForm(forms.ModelForm): fieldsets = ( - FieldSet('name', 'description', name=_('Owner')), - FieldSet('groups', name=_('Groups')), + FieldSet('name', 'description', name=_('Owner Group')), + ) + + class Meta: + model = OwnerGroup + fields = [ + 'name', 'description', + ] + + +class OwnerForm(forms.ModelForm): + fieldsets = ( + FieldSet('name', 'group', 'description', name=_('Owner')), + FieldSet('user_groups', name=_('Groups')), FieldSet('users', name=_('Users')), ) + group = DynamicModelChoiceField( + label=_('Group'), + queryset=OwnerGroup.objects.all(), + required=False, + selector=True, + quick_add=True + ) class Meta: model = Owner fields = [ - 'name', 'description', 'groups', 'users', + 'name', 'group', 'description', 'user_groups', 'users', ] diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index bfec7d5fc4d..52a768b8541 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -11,6 +11,7 @@ __all__ = ( 'GroupFilter', 'OwnerFilter', + 'OwnerGroupFilter', 'UserFilter', ) @@ -38,5 +39,16 @@ class UserFilter(BaseObjectTypeFilterMixin): class OwnerFilter(BaseObjectTypeFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() - groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + user_groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + + +@strawberry_django.filter_type(models.OwnerGroup, lookups=True) +class OwnerGroupFilter(BaseObjectTypeFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index cb35f9284fc..5b6341d54df 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -14,5 +14,8 @@ class UsersQuery: user: UserType = strawberry_django.field() user_list: List[UserType] = strawberry_django.field() + owner_group: OwnerGroupType = strawberry_django.field() + owner_group_list: List[OwnerGroupType] = strawberry_django.field() + owner: OwnerType = strawberry_django.field() owner_list: List[OwnerType] = strawberry_django.field() diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index d8edfcb44b2..53c1a5e1136 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -3,11 +3,12 @@ import strawberry_django from netbox.graphql.types import BaseObjectType -from users.models import Group, Owner, User +from users.models import Group, Owner, OwnerGroup, User from .filters import * __all__ = ( 'GroupType', + 'OwnerGroupType', 'OwnerType', 'UserType', ) @@ -35,11 +36,21 @@ class UserType(BaseObjectType): groups: List[GroupType] +@strawberry_django.type( + OwnerGroup, + fields=['id', 'name', 'description'], + filters=OwnerGroupFilter, + pagination=True +) +class OwnerGroupType(BaseObjectType): + pass + + @strawberry_django.type( Owner, - fields=['id', 'name', 'description', 'groups', 'users'], + fields=['id', 'group', 'name', 'description', 'user_groups', 'users'], filters=OwnerFilter, pagination=True ) class OwnerType(BaseObjectType): - pass + group: OwnerGroupType diff --git a/netbox/users/migrations/0015_owner.py b/netbox/users/migrations/0015_owner.py index cec3034e2f2..1ccaf48807f 100644 --- a/netbox/users/migrations/0015_owner.py +++ b/netbox/users/migrations/0015_owner.py @@ -1,28 +1,51 @@ +import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('users', '0014_users_token_v2'), ] operations = [ + migrations.CreateModel( + name='OwnerGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('description', models.CharField(blank=True, max_length=200)), + ('name', models.CharField(max_length=100, unique=True)), + ], + options={ + 'verbose_name': 'owner group', + 'verbose_name_plural': 'owner groups', + 'ordering': ['name'], + }, + ), migrations.CreateModel( name='Owner', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, unique=True)), + ('name', models.CharField(max_length=100, unique=True)), ('description', models.CharField(blank=True, max_length=200)), ( - 'groups', + 'group', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='members', + to='users.ownergroup', + ), + ), + ( + 'user_groups', models.ManyToManyField( blank=True, related_name='owners', related_query_name='owner', to='users.group', - ) + ), ), ( 'users', @@ -31,7 +54,7 @@ class Migration(migrations.Migration): related_name='owners', related_query_name='owner', to=settings.AUTH_USER_MODEL, - ) + ), ), ], options={ diff --git a/netbox/users/models/owners.py b/netbox/users/models/owners.py index bf24e43f867..2ddb1a03068 100644 --- a/netbox/users/models/owners.py +++ b/netbox/users/models/owners.py @@ -7,16 +7,47 @@ __all__ = ( 'Owner', + 'OwnerGroup', ) +class OwnerGroup(AdminModel): + """ + An arbitrary grouping of Owners. + """ + name = models.CharField( + verbose_name=_('name'), + max_length=100, + unique=True, + ) + + class Meta: + ordering = ['name'] + verbose_name = _('owner group') + verbose_name_plural = _('owner groups') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('users:ownergroup', args=[self.pk]) + + class Owner(AdminModel): name = models.CharField( verbose_name=_('name'), - max_length=150, + max_length=100, unique=True, ) - groups = models.ManyToManyField( + group = models.ForeignKey( + to='users.OwnerGroup', + on_delete=models.PROTECT, + related_name='members', + verbose_name=_('group'), + blank=True, + null=True, + ) + user_groups = models.ManyToManyField( to='users.Group', verbose_name=_('groups'), blank=True, @@ -32,7 +63,7 @@ class Owner(AdminModel): ) objects = RestrictedQuerySet.as_manager() - clone_fields = ('groups', 'users') + clone_fields = ('user_groups', 'users') class Meta: ordering = ('name',) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 277b176fe59..542a665b2d2 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -2,11 +2,12 @@ from django.utils.translation import gettext as _ from netbox.tables import NetBoxTable, columns -from users.models import Group, ObjectPermission, Owner, Token, User +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User __all__ = ( 'GroupTable', 'ObjectPermissionTable', + 'OwnerGroupTable', 'OwnerTable', 'TokenTable', 'UserTable', @@ -146,12 +147,33 @@ class Meta(NetBoxTable.Meta): ) +class OwnerGroupTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + actions = columns.ActionsColumn( + actions=('edit', 'delete'), + ) + + class Meta(NetBoxTable.Meta): + model = OwnerGroup + fields = ( + 'pk', 'id', 'name', 'description', + ) + default_columns = ('pk', 'name', 'description') + + class OwnerTable(NetBoxTable): name = tables.Column( verbose_name=_('Name'), linkify=True ) - groups = columns.ManyToManyColumn( + group = tables.Column( + verbose_name=_('Group'), + linkify=True, + ) + user_groups = columns.ManyToManyColumn( verbose_name=_('Groups'), linkify_item=('users:group', {'pk': tables.A('pk')}) ) @@ -166,6 +188,6 @@ class OwnerTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Owner fields = ( - 'pk', 'id', 'name', 'description', 'groups', 'users', + 'pk', 'id', 'name', 'group', 'description', 'user_groups', 'users', ) - default_columns = ('pk', 'name', 'description', 'groups', 'users') + default_columns = ('pk', 'name', 'group', 'description', 'user_groups', 'users') diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 9fa24bc7ecc..d820295b0ea 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -18,6 +18,9 @@ path('permissions/', include(get_model_urls('users', 'objectpermission', detail=False))), path('permissions//', include(get_model_urls('users', 'objectpermission'))), + path('owner-groups/', include(get_model_urls('users', 'ownergroup', detail=False))), + path('owner-groups//', include(get_model_urls('users', 'ownergroup'))), + path('owners/', include(get_model_urls('users', 'owner', detail=False))), path('owners//', include(get_model_urls('users', 'owner'))), diff --git a/netbox/users/views.py b/netbox/users/views.py index 7c833568fb4..a9c74eb409c 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -6,7 +6,7 @@ from netbox.views import generic from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables -from .models import Group, User, ObjectPermission, Owner, Token +from .models import Group, User, ObjectPermission, Owner, OwnerGroup, Token # @@ -233,6 +233,67 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView): table = tables.ObjectPermissionTable +# +# Owner groups +# + +@register_model_view(OwnerGroup, 'list', path='', detail=False) +class OwnerGroupListView(generic.ObjectListView): + queryset = OwnerGroup.objects.all() + filterset = filtersets.OwnerGroupFilterSet + filterset_form = forms.OwnerGroupFilterForm + table = tables.OwnerGroupTable + + +@register_model_view(OwnerGroup) +class OwnerGroupView(GetRelatedModelsMixin, generic.ObjectView): + queryset = OwnerGroup.objects.all() + template_name = 'users/ownergroup.html' + + def get_extra_context(self, request, instance): + return { + 'related_models': self.get_related_models(request, instance), + } + + +@register_model_view(OwnerGroup, 'add', detail=False) +@register_model_view(OwnerGroup, 'edit') +class OwnerGroupEditView(generic.ObjectEditView): + queryset = OwnerGroup.objects.all() + form = forms.OwnerGroupForm + + +@register_model_view(OwnerGroup, 'delete') +class OwnerGroupDeleteView(generic.ObjectDeleteView): + queryset = OwnerGroup.objects.all() + + +@register_model_view(OwnerGroup, 'bulk_import', path='import', detail=False) +class OwnerGroupBulkImportView(generic.BulkImportView): + queryset = OwnerGroup.objects.all() + model_form = forms.OwnerGroupImportForm + + +@register_model_view(OwnerGroup, 'bulk_edit', path='edit', detail=False) +class OwnerGroupBulkEditView(generic.BulkEditView): + queryset = OwnerGroup.objects.all() + filterset = filtersets.OwnerGroupFilterSet + table = tables.OwnerGroupTable + form = forms.OwnerGroupBulkEditForm + + +@register_model_view(OwnerGroup, 'bulk_rename', path='rename', detail=False) +class OwnerGroupBulkRenameView(generic.BulkRenameView): + queryset = OwnerGroup.objects.all() + + +@register_model_view(OwnerGroup, 'bulk_delete', path='delete', detail=False) +class OwnerGroupBulkDeleteView(generic.BulkDeleteView): + queryset = OwnerGroup.objects.all() + filterset = filtersets.OwnerGroupFilterSet + table = tables.OwnerGroupTable + + # # Owners # From 3a7b4ac7fc50541e15d7f56eed4216888657ec59 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 08:51:12 -0400 Subject: [PATCH 33/40] Add documentation for owners & owner groups --- docs/features/resource-ownership.md | 10 ++++++++++ docs/features/tenancy.md | 30 ++++++++++++++++++++++------- docs/models/users/owner.md | 23 ++++++++++++++++++++++ docs/models/users/ownergroup.md | 9 +++++++++ mkdocs.yml | 4 ++++ 5 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 docs/features/resource-ownership.md create mode 100644 docs/models/users/owner.md create mode 100644 docs/models/users/ownergroup.md diff --git a/docs/features/resource-ownership.md b/docs/features/resource-ownership.md new file mode 100644 index 00000000000..a50984f5aa1 --- /dev/null +++ b/docs/features/resource-ownership.md @@ -0,0 +1,10 @@ +# Resource Ownership + +!!! info "This feature was introduced in NetBox v4.5." + +Most objects in NetBox can be assigned an owner. An owner is a set of users and/or groups who are responsible for the administration of associated objects. For example, you might designate the operations team at a site as the owner for all prefixes and VLANs deployed at that site. The users and groups assigned to an owner are referred to as its members. + +!!! note + Ownership of an object should not be confused with the concept of [tenancy](./tenancy.md), which indicates the dedication of an object to a specific tenant. For instance, a tenant might represent a customer served by the object, whereas an owner typically represents a set of internal users responsible for the management of the object. + +Owners can be organized into groups for easier management. diff --git a/docs/features/tenancy.md b/docs/features/tenancy.md index 470905f2013..3f76ff80549 100644 --- a/docs/features/tenancy.md +++ b/docs/features/tenancy.md @@ -1,6 +1,6 @@ # Tenancy -Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey ownership or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers. +Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey assignment or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers. ```mermaid flowchart TD @@ -19,20 +19,36 @@ Tenants can be grouped by any logic that your use case demands, and groups can b Typically, the tenant model is used to represent a customer or internal organization, however it can be used for whatever purpose meets your needs. -Most core objects within NetBox can be assigned to particular tenant, so this model provides a very convenient way to correlate ownership across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment. +Most core objects within NetBox can be assigned to a particular tenant, so this model provides a very convenient way to correlate resource allocation across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment. The following objects can be assigned to tenants: -* Sites +* Circuits +* Circuit groups +* Virtual circuits +* Cables +* Devices +* Virtual device contexts +* Power feeds * Racks * Rack reservations -* Devices -* VRFs +* Sites +* Locations +* ASNs +* ASN ranges +* Aggregates * Prefixes +* IP ranges * IP addresses * VLANs -* Circuits +* VLAN groups +* VRFs +* Route targets * Clusters * Virtual machines +* L2VPNs +* Tunnels +* Wireless LANs +* Wireless links -Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate. +Tenancy represents the dedication of an object to a specific tenant. As such, each object may only be assigned to a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so the assignment of a tenant would not be appropriate. diff --git a/docs/models/users/owner.md b/docs/models/users/owner.md new file mode 100644 index 00000000000..70c9a93e733 --- /dev/null +++ b/docs/models/users/owner.md @@ -0,0 +1,23 @@ +# Owner + +An owner is a set of users and/or groups who are responsible for the administration of certain resources within NetBox. The users and groups assigned to an owner are referred to as its members. Owner assignments are useful for indicating which parties are responsible for the administration of a particular object. + +Most objects within NetBox can be assigned an owner, although this is not required. + +## Fields + +### Name + +The owner's name. + +### Group + +The [group](./ownergroup.md) to which the owner is assigned. The assignment of an owner to a group is optional. + +### User Groups + +Groups of users that are members of the owner. + +### Users + +Individual users that are members of the owner. diff --git a/docs/models/users/ownergroup.md b/docs/models/users/ownergroup.md new file mode 100644 index 00000000000..61c27943810 --- /dev/null +++ b/docs/models/users/ownergroup.md @@ -0,0 +1,9 @@ +# Owner Groups + +Groups are used to correlate and organize [owners](./owner.md). The assignment of an owner to a group has no bearing on the relationship of owned objects to their owners. + +## Fields + +### Name + +The name of the group. diff --git a/mkdocs.yml b/mkdocs.yml index fd6d7e4f222..078fc5e50cb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,7 @@ nav: - Wireless: 'features/wireless.md' - Virtualization: 'features/virtualization.md' - VPN Tunnels: 'features/vpn-tunnels.md' + - Resource Ownership: 'features/resource-ownership.md' - Tenancy: 'features/tenancy.md' - Contacts: 'features/contacts.md' - Search: 'features/search.md' @@ -273,6 +274,9 @@ nav: - ContactRole: 'models/tenancy/contactrole.md' - Tenant: 'models/tenancy/tenant.md' - TenantGroup: 'models/tenancy/tenantgroup.md' + - Users: + - Owner: 'models/users/owner.md' + - OwnerGroup: 'models/users/ownergroup.md' - Virtualization: - Cluster: 'models/virtualization/cluster.md' - ClusterGroup: 'models/virtualization/clustergroup.md' From c858d2a17697c95fa9ea5095a8415a3e845061ab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 09:47:29 -0400 Subject: [PATCH 34/40] Add tests for Owner & OwnerGroup --- netbox/users/graphql/types.py | 2 +- netbox/users/tests/test_api.py | 111 +++++++++++++++++++++++++- netbox/users/tests/test_filtersets.py | 105 +++++++++++++++++++++++- netbox/users/tests/test_views.py | 105 ++++++++++++++++++++++++ netbox/utilities/testing/views.py | 15 ++++ 5 files changed, 335 insertions(+), 3 deletions(-) diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 53c1a5e1136..e04fc866873 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -53,4 +53,4 @@ class OwnerGroupType(BaseObjectType): pagination=True ) class OwnerType(BaseObjectType): - group: OwnerGroupType + group: OwnerGroupType | None diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 597ce77de25..0e1ccebf857 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -3,7 +3,7 @@ from core.models import ObjectType from users.constants import TOKEN_DEFAULT_LENGTH -from users.models import Group, ObjectPermission, Token, User +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User from utilities.data import deepmerge from utilities.testing import APIViewTestCases, APITestCase, create_test_user @@ -448,3 +448,112 @@ def test_patch(self): self.assertDictEqual(response.data, new_data) userconfig.refresh_from_db() self.assertDictEqual(userconfig.data, new_data) + + +class OwnerGroupTest(APIViewTestCases.APIViewTestCase): + model = OwnerGroup + brief_fields = ['description', 'display', 'id', 'name', 'url'] + bulk_update_data = { + 'description': 'New description', + } + + @classmethod + def setUpTestData(cls): + owner_groups = ( + OwnerGroup(name='Owner Group 1'), + OwnerGroup(name='Owner Group 2'), + OwnerGroup(name='Owner Group 3'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + cls.create_data = [ + { + 'name': 'Owner Group 4', + 'description': 'Fourth owner group', + }, + { + 'name': 'Owner Group 5', + 'description': 'Fifth owner group', + }, + { + 'name': 'Owner Group 6', + 'description': 'Sixth owner group', + }, + ] + + +class OwnerTest(APIViewTestCases.APIViewTestCase): + model = Owner + brief_fields = ['description', 'display', 'id', 'name', 'url'] + + @classmethod + def setUpTestData(cls): + owner_groups = ( + OwnerGroup(name='Owner Group 1'), + OwnerGroup(name='Owner Group 2'), + OwnerGroup(name='Owner Group 3'), + OwnerGroup(name='Owner Group 4'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + groups = ( + Group(name='Group 1'), + Group(name='Group 2'), + Group(name='Group 3'), + Group(name='Group 4'), + ) + Group.objects.bulk_create(groups) + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + User(username='User 4'), + ) + User.objects.bulk_create(users) + + owners = ( + Owner(name='Owner 1'), + Owner(name='Owner 2'), + Owner(name='Owner 3'), + ) + Owner.objects.bulk_create(owners) + + # Assign users and groups to owners + owners[0].user_groups.add(groups[0]) + owners[1].user_groups.add(groups[1]) + owners[2].user_groups.add(groups[2]) + owners[0].users.add(users[0]) + owners[1].users.add(users[1]) + owners[2].users.add(users[2]) + + cls.create_data = [ + { + 'name': 'Owner 4', + 'description': 'Fourth owner', + 'group': owner_groups[3].pk, + 'user_groups': [groups[3].pk], + 'users': [users[3].pk], + }, + { + 'name': 'Owner 5', + 'description': 'Fifth owner', + 'group': owner_groups[3].pk, + 'user_groups': [groups[3].pk], + 'users': [users[3].pk], + }, + { + 'name': 'Owner 6', + 'description': 'Sixth owner', + 'group': owner_groups[3].pk, + 'user_groups': [groups[3].pk], + 'users': [users[3].pk], + }, + ] + + cls.bulk_update_data = { + 'group': owner_groups[3].pk, + 'user_groups': [groups[3].pk], + 'users': [users[3].pk], + 'description': 'New description', + } diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 1f7336cc3c1..745b001262c 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -5,7 +5,7 @@ from core.models import ObjectType from users import filtersets -from users.models import Group, ObjectPermission, Token, User +from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User from utilities.testing import BaseFilterSetTests @@ -348,3 +348,106 @@ def test_write_enabled(self): def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class OwnerGroupTestCase(TestCase, BaseFilterSetTests): + queryset = OwnerGroup.objects.all() + filterset = filtersets.OwnerGroupFilterSet + + @classmethod + def setUpTestData(cls): + + owner_groups = ( + OwnerGroup(name='Owner Group 1', description='Foo'), + OwnerGroup(name='Owner Group 2', description='Bar'), + OwnerGroup(name='Owner Group 3', description='Baz'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + def test_q(self): + params = {'q': 'foo'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_name(self): + params = {'name': ['Owner Group 1', 'Owner Group 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['Foo', 'Bar']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class OwnerTestCase(TestCase, BaseFilterSetTests): + queryset = Owner.objects.all() + filterset = filtersets.OwnerFilterSet + + @classmethod + def setUpTestData(cls): + owner_groups = ( + OwnerGroup(name='Owner Group 1'), + OwnerGroup(name='Owner Group 2'), + OwnerGroup(name='Owner Group 3'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + groups = ( + Group(name='Group 1'), + Group(name='Group 2'), + Group(name='Group 3'), + ) + Group.objects.bulk_create(groups) + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + owners = ( + Owner(name='Owner 1', group=owner_groups[0], description='Foo'), + Owner(name='Owner 2', group=owner_groups[1], description='Bar'), + Owner(name='Owner 3', group=owner_groups[2], description='Baz'), + ) + Owner.objects.bulk_create(owners) + + # Assign users and groups to owners + owners[0].user_groups.add(groups[0]) + owners[1].user_groups.add(groups[1]) + owners[2].user_groups.add(groups[2]) + owners[0].users.add(users[0]) + owners[1].users.add(users[1]) + owners[2].users.add(users[2]) + + def test_q(self): + params = {'q': 'foo'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_name(self): + params = {'name': ['Owner 1', 'Owner 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_description(self): + params = {'description': ['Foo', 'Bar']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_group(self): + owner_groups = OwnerGroup.objects.order_by('id')[:2] + params = {'group_id': [owner_groups[0].pk, owner_groups[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'group': [owner_groups[0].name, owner_groups[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_user_group(self): + group = Group.objects.order_by('id')[:2] + params = {'user_group_id': [group[0].pk, group[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'user_group': [group[0].name, group[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_user(self): + users = User.objects.order_by('id')[:2] + params = {'user_id': [users[0].pk, users[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'user': [users[0].username, users[1].username]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 24aec6941f6..1980299fdd8 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -255,3 +255,108 @@ def setUpTestData(cls): cls.bulk_edit_data = { 'description': 'New description', } + + +class OwnerGroupTestCase(ViewTestCases.AdminModelViewTestCase): + model = OwnerGroup + + @classmethod + def setUpTestData(cls): + owner_groups = ( + OwnerGroup(name='Owner Group 1'), + OwnerGroup(name='Owner Group 2'), + OwnerGroup(name='Owner Group 3'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + cls.form_data = { + 'name': 'Owner Group X', + 'description': 'A new owner group', + } + + cls.csv_data = ( + "name,description", + "Owner Group 4,Foo", + "Owner Group 5,Bar", + "Owner Group 6,Baz", + ) + + cls.csv_update_data = ( + "id,description", + f"{owner_groups[0].pk},Foo", + f"{owner_groups[1].pk},Bar", + f"{owner_groups[2].pk},Baz", + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } + + +class OwnerTestCase(ViewTestCases.AdminModelViewTestCase): + model = Owner + + @classmethod + def setUpTestData(cls): + groups = ( + Group(name='Group 1'), + Group(name='Group 2'), + Group(name='Group 3'), + ) + Group.objects.bulk_create(groups) + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + owner_groups = ( + OwnerGroup(name='Owner Group 1'), + OwnerGroup(name='Owner Group 2'), + OwnerGroup(name='Owner Group 3'), + OwnerGroup(name='Owner Group 4'), + ) + OwnerGroup.objects.bulk_create(owner_groups) + + owners = ( + Owner(name='Owner 1'), + Owner(name='Owner 2'), + Owner(name='Owner 3'), + ) + Owner.objects.bulk_create(owners) + + # Assign users and groups to owners + owners[0].user_groups.add(groups[0]) + owners[1].user_groups.add(groups[1]) + owners[2].user_groups.add(groups[2]) + owners[0].users.add(users[0]) + owners[1].users.add(users[1]) + owners[2].users.add(users[2]) + + cls.form_data = { + 'name': 'Owner X', + 'group': owner_groups[3].pk, + 'user_groups': [groups[0].pk, groups[1].pk], + 'users': [users[0].pk, users[1].pk], + 'description': 'A new owner', + } + + cls.csv_data = ( + "name,group,description", + "Owner 4,Owner Group 4,Foo", + "Owner 5,Owner Group 4,Bar", + "Owner 6,Owner Group 4,Baz", + ) + + cls.csv_update_data = ( + "id,description", + f"{owners[0].pk},Foo", + f"{owners[1].pk},Bar", + f"{owners[2].pk},Baz", + ) + + cls.bulk_edit_data = { + 'description': 'New description', + } diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index f00b21d0835..c4ef28e2628 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -1113,6 +1113,21 @@ class OrganizationalObjectViewTestCase( """ maxDiff = None + class AdminModelViewTestCase( + GetObjectViewTestCase, + CreateObjectViewTestCase, + EditObjectViewTestCase, + DeleteObjectViewTestCase, + ListObjectsViewTestCase, + BulkImportObjectsViewTestCase, + BulkEditObjectsViewTestCase, + BulkDeleteObjectsViewTestCase, + ): + """ + TestCase suitable for testing all standard View functions for objects which inherit from AdminModel. + """ + maxDiff = None + class DeviceComponentTemplateViewTestCase( EditObjectViewTestCase, DeleteObjectViewTestCase, From ef2225113ad5f43b1edf52272b188b3a3eb93077 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 15:52:43 -0400 Subject: [PATCH 35/40] Add "add owner" button to owner group detail view --- netbox/templates/users/ownergroup.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/templates/users/ownergroup.html b/netbox/templates/users/ownergroup.html index bbd8e46c999..1a74c3d4858 100644 --- a/netbox/templates/users/ownergroup.html +++ b/netbox/templates/users/ownergroup.html @@ -5,6 +5,14 @@ {% block subtitle %}{% endblock %} +{% block extra_controls %} + {% if perms.users.add_owner %} + + {% trans "Add Owner" %} + + {% endif %} +{% endblock extra_controls %} + {% block content %}
From f5da3629c73d155b7f981d2a6c5cf7499d25cda0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 15:59:19 -0400 Subject: [PATCH 36/40] Add owners count to OwnerGroup list --- netbox/users/tables.py | 7 ++++++- netbox/users/views.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 542a665b2d2..ff28545b13e 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -152,6 +152,11 @@ class OwnerGroupTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) + owner_count = columns.LinkedCountColumn( + viewname='users:owner_list', + url_params={'group_id': 'pk'}, + verbose_name=_('Owners') + ) actions = columns.ActionsColumn( actions=('edit', 'delete'), ) @@ -161,7 +166,7 @@ class Meta(NetBoxTable.Meta): fields = ( 'pk', 'id', 'name', 'description', ) - default_columns = ('pk', 'name', 'description') + default_columns = ('pk', 'name', 'owner_count', 'description') class OwnerTable(NetBoxTable): diff --git a/netbox/users/views.py b/netbox/users/views.py index a9c74eb409c..ffb1ab8c5c5 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -4,6 +4,7 @@ from core.tables import ObjectChangeTable from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from netbox.views import generic +from utilities.query import count_related from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import Group, User, ObjectPermission, Owner, OwnerGroup, Token @@ -239,7 +240,9 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView): @register_model_view(OwnerGroup, 'list', path='', detail=False) class OwnerGroupListView(generic.ObjectListView): - queryset = OwnerGroup.objects.all() + queryset = OwnerGroup.objects.annotate( + owner_count=count_related(Owner, 'group') + ) filterset = filtersets.OwnerGroupFilterSet filterset_form = forms.OwnerGroupFilterForm table = tables.OwnerGroupTable From c42e3824a0e8cdda88f58938fb9dc77cf285e372 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 16:05:41 -0400 Subject: [PATCH 37/40] Make user_groups and users DynamicModelMultipleChoiceFields on OwnerForm --- netbox/users/forms/model_forms.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index cb3bd8594cb..f316e0cd499 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -462,6 +462,16 @@ class OwnerForm(forms.ModelForm): selector=True, quick_add=True ) + user_groups = DynamicModelMultipleChoiceField( + label=_('User groups'), + queryset=Group.objects.all(), + required=False + ) + users = DynamicModelMultipleChoiceField( + label=_('Users'), + queryset=User.objects.all(), + required=False + ) class Meta: model = Owner From b9576ed38cbc7801b03c110e4635345befc5adbc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 23 Oct 2025 19:57:29 -0400 Subject: [PATCH 38/40] Misc cleanup --- netbox/templates/dcim/device_edit.html | 7 +++++++ netbox/templates/dcim/htmx/cable_edit.html | 7 +++++++ netbox/templates/dcim/virtualchassis_edit.html | 11 +++++++++-- netbox/templates/ipam/vlan_edit.html | 7 +++++++ netbox/templates/users/ownergroup.html | 2 +- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 7b010cb63f3..454faf8fbaa 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -99,6 +99,13 @@

{% trans "Virtual Chassis" %}

{% render_field form.vc_priority %}
+
+
+

{% trans "Owner" %}

+
+ {% render_field form.owner %} +
+ {% if form.custom_fields %}
diff --git a/netbox/templates/dcim/htmx/cable_edit.html b/netbox/templates/dcim/htmx/cable_edit.html index b0b2ce75067..6de42fc4955 100644 --- a/netbox/templates/dcim/htmx/cable_edit.html +++ b/netbox/templates/dcim/htmx/cable_edit.html @@ -77,6 +77,13 @@

{% trans "Tenancy" %}

{% render_field form.tenant %}
+
+
+

{% trans "Owner" %}

+
+ {% render_field form.owner %} +
+ {% if form.custom_fields %}
diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index 768fb766665..8744947fab1 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -34,8 +34,11 @@

{% trans "Virtual Chassis" %}

{% render_field vc_form.tags %}
-
- {% render_field vc_form.comments %} +
+
+

{% trans "Owner" %}

+
+ {% render_field vc_form.owner %}
{% if vc_form.custom_fields %} @@ -47,6 +50,10 @@

{% trans "Custom Fields" %}

{% endif %} +
+ {% render_field vc_form.comments %} +
+

{% trans "Members" %}

diff --git a/netbox/templates/ipam/vlan_edit.html b/netbox/templates/ipam/vlan_edit.html index b6c21f97291..623468af2b0 100644 --- a/netbox/templates/ipam/vlan_edit.html +++ b/netbox/templates/ipam/vlan_edit.html @@ -65,6 +65,13 @@

{% trans "Assignment" %}

{% endwith %} +
+
+

{% trans "Owner" %}

+
+ {% render_field form.owner %} +
+ {% if form.custom_fields %}
diff --git a/netbox/templates/users/ownergroup.html b/netbox/templates/users/ownergroup.html index 1a74c3d4858..c45da792a6f 100644 --- a/netbox/templates/users/ownergroup.html +++ b/netbox/templates/users/ownergroup.html @@ -35,7 +35,7 @@

{% trans "Group" %}

{% trans "Members" %}

{% for owner in object.members.all %} - {{ owner }} + {{ owner }} {% empty %}
{% trans "None" %}
{% endfor %} From fc9a863cd1a2d06108af1b89d45e59fda40f3bc0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Oct 2025 15:33:58 -0400 Subject: [PATCH 39/40] Add owner field to VirtualChassisCreateForm --- netbox/dcim/forms/object_create.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 5c9599eeb12..072850c523a 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -434,8 +434,8 @@ class VirtualChassisCreateForm(NetBoxModelForm): class Meta: model = VirtualChassis fields = [ - 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', - 'tags', + 'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'owner', 'members', + 'initial_position', 'tags', ] def clean(self): From ee192e2a8e16220539f6b4d36f7ec01b8ece39df Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 24 Oct 2025 15:41:02 -0400 Subject: [PATCH 40/40] Reindex migrations --- netbox/dcim/migrations/{0216_owner.py => 0217_owner.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename netbox/dcim/migrations/{0216_owner.py => 0217_owner.py} (99%) diff --git a/netbox/dcim/migrations/0216_owner.py b/netbox/dcim/migrations/0217_owner.py similarity index 99% rename from netbox/dcim/migrations/0216_owner.py rename to netbox/dcim/migrations/0217_owner.py index 89b12128a1e..f6c662f6820 100644 --- a/netbox/dcim/migrations/0216_owner.py +++ b/netbox/dcim/migrations/0217_owner.py @@ -4,7 +4,7 @@ class Migration(migrations.Migration): dependencies = [ - ('dcim', '0215_rackreservation_status'), + ('dcim', '0216_poweroutlettemplate_color'), ('users', '0015_owner'), ]