فهرست منبع

Closes #20304: Object owners (#20634)

Jeremy Stretch 3 ماه پیش
والد
کامیت
be74436884
100فایلهای تغییر یافته به همراه2669 افزوده شده و 1726 حذف شده
  1. 918 0
      contrib/openapi.json
  2. 10 0
      docs/features/resource-ownership.md
  3. 23 7
      docs/features/tenancy.md
  4. 23 0
      docs/models/users/owner.md
  5. 9 0
      docs/models/users/ownergroup.md
  6. 4 0
      mkdocs.yml
  7. 16 14
      netbox/circuits/api/serializers_/circuits.py
  8. 9 9
      netbox/circuits/api/serializers_/providers.py
  9. 6 6
      netbox/circuits/filtersets.py
  10. 10 55
      netbox/circuits/forms/bulk_edit.py
  11. 18 19
      netbox/circuits/forms/bulk_import.py
  12. 17 17
      netbox/circuits/forms/filtersets.py
  13. 18 29
      netbox/circuits/forms/model_forms.py
  14. 6 11
      netbox/circuits/graphql/types.py
  15. 68 0
      netbox/circuits/migrations/0053_owner.py
  16. 8 13
      netbox/circuits/tables/circuits.py
  17. 9 18
      netbox/circuits/tables/providers.py
  18. 5 8
      netbox/circuits/tables/virtual_circuits.py
  19. 4 4
      netbox/core/api/serializers_/data.py
  20. 2 2
      netbox/core/filtersets.py
  21. 2 9
      netbox/core/forms/bulk_edit.py
  22. 3 3
      netbox/core/forms/bulk_import.py
  23. 5 4
      netbox/core/forms/filtersets.py
  24. 5 5
      netbox/core/forms/model_forms.py
  25. 2 3
      netbox/core/graphql/types.py
  26. 19 0
      netbox/core/migrations/0020_owner.py
  27. 3 3
      netbox/core/tables/data.py
  28. 6 4
      netbox/dcim/api/serializers_/cables.py
  29. 11 11
      netbox/dcim/api/serializers_/devices.py
  30. 9 9
      netbox/dcim/api/serializers_/devicetypes.py
  31. 3 3
      netbox/dcim/api/serializers_/manufacturers.py
  32. 1 1
      netbox/dcim/api/serializers_/platforms.py
  33. 7 6
      netbox/dcim/api/serializers_/power.py
  34. 11 11
      netbox/dcim/api/serializers_/racks.py
  35. 5 5
      netbox/dcim/api/serializers_/roles.py
  36. 6 6
      netbox/dcim/api/serializers_/sites.py
  37. 4 4
      netbox/dcim/api/serializers_/virtualchassis.py
  38. 30 58
      netbox/dcim/filtersets.py
  39. 30 160
      netbox/dcim/forms/bulk_edit.py
  40. 77 72
      netbox/dcim/forms/bulk_import.py
  41. 78 57
      netbox/dcim/forms/filtersets.py
  42. 62 92
      netbox/dcim/forms/model_forms.py
  43. 2 2
      netbox/dcim/forms/object_create.py
  44. 26 34
      netbox/dcim/graphql/types.py
  45. 243 0
      netbox/dcim/migrations/0217_owner.py
  46. 2 1
      netbox/dcim/models/device_components.py
  47. 5 6
      netbox/dcim/tables/cables.py
  48. 15 38
      netbox/dcim/tables/devices.py
  49. 5 8
      netbox/dcim/tables/devicetypes.py
  50. 8 17
      netbox/dcim/tables/modules.py
  51. 7 14
      netbox/dcim/tables/power.py
  52. 10 35
      netbox/dcim/tables/racks.py
  53. 11 64
      netbox/dcim/tables/sites.py
  54. 8 13
      netbox/extras/api/serializers_/configcontexts.py
  55. 8 2
      netbox/extras/api/serializers_/configtemplates.py
  56. 6 5
      netbox/extras/api/serializers_/customfields.py
  57. 3 2
      netbox/extras/api/serializers_/customlinks.py
  58. 5 4
      netbox/extras/api/serializers_/events.py
  59. 3 2
      netbox/extras/api/serializers_/exporttemplates.py
  60. 3 2
      netbox/extras/api/serializers_/savedfilters.py
  61. 2 1
      netbox/extras/api/serializers_/tags.py
  62. 13 12
      netbox/extras/filtersets.py
  63. 13 19
      netbox/extras/forms/bulk_edit.py
  64. 21 21
      netbox/extras/forms/bulk_import.py
  65. 58 8
      netbox/extras/forms/filtersets.py
  66. 19 18
      netbox/extras/forms/model_forms.py
  67. 13 12
      netbox/extras/graphql/types.py
  68. 89 0
      netbox/extras/migrations/0134_owner.py
  69. 9 2
      netbox/extras/models/configs.py
  70. 3 2
      netbox/extras/models/customfields.py
  71. 13 5
      netbox/extras/models/models.py
  72. 2 1
      netbox/extras/models/tags.py
  73. 43 3
      netbox/extras/tables/tables.py
  74. 7 7
      netbox/ipam/api/serializers_/asns.py
  75. 3 3
      netbox/ipam/api/serializers_/fhrpgroups.py
  76. 9 9
      netbox/ipam/api/serializers_/ip.py
  77. 4 4
      netbox/ipam/api/serializers_/roles.py
  78. 6 6
      netbox/ipam/api/serializers_/services.py
  79. 8 7
      netbox/ipam/api/serializers_/vlans.py
  80. 7 7
      netbox/ipam/api/serializers_/vrfs.py
  81. 15 13
      netbox/ipam/filtersets.py
  82. 17 103
      netbox/ipam/forms/bulk_edit.py
  83. 34 36
      netbox/ipam/forms/bulk_import.py
  84. 37 30
      netbox/ipam/forms/filtersets.py
  85. 35 52
      netbox/ipam/forms/model_forms.py
  86. 14 15
      netbox/ipam/graphql/types.py
  87. 124 0
      netbox/ipam/migrations/0083_owner.py
  88. 5 8
      netbox/ipam/tables/asn.py
  89. 4 7
      netbox/ipam/tables/fhrp.py
  90. 14 26
      netbox/ipam/tables/ip.py
  91. 6 12
      netbox/ipam/tables/services.py
  92. 7 10
      netbox/ipam/tables/vlans.py
  93. 6 12
      netbox/ipam/tables/vrfs.py
  94. 2 29
      netbox/netbox/api/serializers/__init__.py
  95. 11 0
      netbox/netbox/api/serializers/bulk.py
  96. 14 0
      netbox/netbox/api/serializers/features.py
  97. 31 0
      netbox/netbox/api/serializers/models.py
  98. 14 4
      netbox/netbox/filtersets.py
  99. 5 57
      netbox/netbox/forms/__init__.py
  100. 0 178
      netbox/netbox/forms/base.py

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 918 - 0
contrib/openapi.json


+ 10 - 0
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.

+ 23 - 7
docs/features/tenancy.md

@@ -1,6 +1,6 @@
 # Tenancy
 # 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
 ```mermaid
 flowchart TD
 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.
 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:
 The following objects can be assigned to tenants:
 
 
-* Sites
+* Circuits
+* Circuit groups
+* Virtual circuits
+* Cables
+* Devices
+* Virtual device contexts
+* Power feeds
 * Racks
 * Racks
 * Rack reservations
 * Rack reservations
-* Devices
-* VRFs
+* Sites
+* Locations
+* ASNs
+* ASN ranges
+* Aggregates
 * Prefixes
 * Prefixes
+* IP ranges
 * IP addresses
 * IP addresses
 * VLANs
 * VLANs
-* Circuits
+* VLAN groups
+* VRFs
+* Route targets
 * Clusters
 * Clusters
 * Virtual machines
 * 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.

+ 23 - 0
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.

+ 9 - 0
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.

+ 4 - 0
mkdocs.yml

@@ -77,6 +77,7 @@ nav:
         - Wireless: 'features/wireless.md'
         - Wireless: 'features/wireless.md'
         - Virtualization: 'features/virtualization.md'
         - Virtualization: 'features/virtualization.md'
         - VPN Tunnels: 'features/vpn-tunnels.md'
         - VPN Tunnels: 'features/vpn-tunnels.md'
+        - Resource Ownership: 'features/resource-ownership.md'
         - Tenancy: 'features/tenancy.md'
         - Tenancy: 'features/tenancy.md'
         - Contacts: 'features/contacts.md'
         - Contacts: 'features/contacts.md'
         - Search: 'features/search.md'
         - Search: 'features/search.md'
@@ -273,6 +274,9 @@ nav:
             - ContactRole: 'models/tenancy/contactrole.md'
             - ContactRole: 'models/tenancy/contactrole.md'
             - Tenant: 'models/tenancy/tenant.md'
             - Tenant: 'models/tenancy/tenant.md'
             - TenantGroup: 'models/tenancy/tenantgroup.md'
             - TenantGroup: 'models/tenancy/tenantgroup.md'
+        - Users:
+            - Owner: 'models/users/owner.md'
+            - OwnerGroup: 'models/users/ownergroup.md'
         - Virtualization:
         - Virtualization:
             - Cluster: 'models/virtualization/cluster.md'
             - Cluster: 'models/virtualization/cluster.md'
             - ClusterGroup: 'models/virtualization/clustergroup.md'
             - ClusterGroup: 'models/virtualization/clustergroup.md'

+ 16 - 14
netbox/circuits/api/serializers_/circuits.py

@@ -11,7 +11,9 @@ from circuits.models import (
 from dcim.api.serializers_.device_components import InterfaceSerializer
 from dcim.api.serializers_.device_components import InterfaceSerializer
 from dcim.api.serializers_.cables import CabledObjectSerializer
 from dcim.api.serializers_.cables import CabledObjectSerializer
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
 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 netbox.choices import DistanceUnitChoices
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
@@ -29,7 +31,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CircuitTypeSerializer(NetBoxModelSerializer):
+class CircuitTypeSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     circuit_count = RelatedObjectCountField('circuits')
     circuit_count = RelatedObjectCountField('circuits')
@@ -37,8 +39,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = CircuitType
         model = CircuitType
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
 
 
@@ -71,15 +73,15 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
         return serializer(obj.termination, nested=True, context=context).data
         return serializer(obj.termination, nested=True, context=context).data
 
 
 
 
-class CircuitGroupSerializer(NetBoxModelSerializer):
+class CircuitGroupSerializer(OrganizationalModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     circuit_count = RelatedObjectCountField('assignments')
     circuit_count = RelatedObjectCountField('assignments')
 
 
     class Meta:
     class Meta:
         model = CircuitGroup
         model = CircuitGroup
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name')
 
 
@@ -99,7 +101,7 @@ class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'group', 'priority')
         brief_fields = ('id', 'url', 'display', 'group', 'priority')
 
 
 
 
-class CircuitSerializer(NetBoxModelSerializer):
+class CircuitSerializer(PrimaryModelSerializer):
     provider = ProviderSerializer(nested=True)
     provider = ProviderSerializer(nested=True)
     provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
     provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
@@ -115,7 +117,7 @@ class CircuitSerializer(NetBoxModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
             'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
             'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
             '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',
             'assignments',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
         brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
@@ -176,7 +178,7 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
         return serializer(obj.member, nested=True, context=context).data
         return serializer(obj.member, nested=True, context=context).data
 
 
 
 
-class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
+class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
     virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
@@ -184,13 +186,13 @@ class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = VirtualCircuitType
         model = VirtualCircuitType
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
 
 
 
 
-class VirtualCircuitSerializer(NetBoxModelSerializer):
+class VirtualCircuitSerializer(PrimaryModelSerializer):
     provider_network = ProviderNetworkSerializer(nested=True)
     provider_network = ProviderNetworkSerializer(nested=True)
     provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
     provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
     type = VirtualCircuitTypeSerializer(nested=True)
     type = VirtualCircuitTypeSerializer(nested=True)
@@ -201,7 +203,7 @@ class VirtualCircuitSerializer(NetBoxModelSerializer):
         model = VirtualCircuit
         model = VirtualCircuit
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
             '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')
         brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
 
 

+ 9 - 9
netbox/circuits/api/serializers_/providers.py

@@ -4,7 +4,7 @@ from circuits.models import Provider, ProviderAccount, ProviderNetwork
 from ipam.api.serializers_.asns import ASNSerializer
 from ipam.api.serializers_.asns import ASNSerializer
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import NetBoxModelSerializer
+from netbox.api.serializers import PrimaryModelSerializer
 from .nested import NestedProviderAccountSerializer
 from .nested import NestedProviderAccountSerializer
 
 
 __all__ = (
 __all__ = (
@@ -14,7 +14,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderSerializer(NetBoxModelSerializer):
+class ProviderSerializer(PrimaryModelSerializer):
     accounts = SerializedPKRelatedField(
     accounts = SerializedPKRelatedField(
         queryset=ProviderAccount.objects.all(),
         queryset=ProviderAccount.objects.all(),
         serializer=NestedProviderAccountSerializer,
         serializer=NestedProviderAccountSerializer,
@@ -35,32 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = [
         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',
             'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
 
 
 
 
-class ProviderAccountSerializer(NetBoxModelSerializer):
+class ProviderAccountSerializer(PrimaryModelSerializer):
     provider = ProviderSerializer(nested=True)
     provider = ProviderSerializer(nested=True)
     name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
     name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
 
 
     class Meta:
     class Meta:
         model = ProviderAccount
         model = ProviderAccount
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
 
 
 
 
-class ProviderNetworkSerializer(NetBoxModelSerializer):
+class ProviderNetworkSerializer(PrimaryModelSerializer):
     provider = ProviderSerializer(nested=True)
     provider = ProviderSerializer(nested=True)
 
 
     class Meta:
     class Meta:
         model = ProviderNetwork
         model = ProviderNetwork
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 6 - 6
netbox/circuits/filtersets.py

@@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
 from dcim.filtersets import CabledObjectFilterSet
 from dcim.filtersets import CabledObjectFilterSet
 from dcim.models import Interface, Location, Region, Site, SiteGroup
 from dcim.models import Interface, Location, Region, Site, SiteGroup
 from ipam.models import ASN
 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 tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
@@ -29,7 +29,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
+class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='circuits__terminations___region',
         field_name='circuits__terminations___region',
@@ -95,7 +95,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         )
         )
 
 
 
 
-class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
+class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
     provider_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         label=_('Provider (ID)'),
         label=_('Provider (ID)'),
@@ -122,7 +122,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         ).distinct()
         ).distinct()
 
 
 
 
-class ProviderNetworkFilterSet(NetBoxModelFilterSet):
+class ProviderNetworkFilterSet(PrimaryModelFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
     provider_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         label=_('Provider (ID)'),
         label=_('Provider (ID)'),
@@ -156,7 +156,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'color', 'description')
         fields = ('id', 'name', 'slug', 'color', 'description')
 
 
 
 
-class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
     provider_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         label=_('Provider (ID)'),
         label=_('Provider (ID)'),
@@ -475,7 +475,7 @@ class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'color', 'description')
         fields = ('id', 'name', 'slug', 'color', 'description')
 
 
 
 
-class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
     provider_id = django_filters.ModelMultipleChoiceFilter(
         field_name='provider_network__provider',
         field_name='provider_network__provider',
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),

+ 10 - 55
netbox/circuits/forms/bulk_edit.py

@@ -11,11 +11,11 @@ from circuits.models import *
 from dcim.models import Site
 from dcim.models import Site
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.choices import DistanceUnitChoices
 from netbox.choices import DistanceUnitChoices
-from netbox.forms import NetBoxModelBulkEditForm
+from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms.fields import (
 from utilities.forms.fields import (
-    ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
 )
 )
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
 from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
@@ -36,18 +36,12 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderBulkEditForm(NetBoxModelBulkEditForm):
+class ProviderBulkEditForm(PrimaryModelBulkEditForm):
     asns = DynamicModelMultipleChoiceField(
     asns = DynamicModelMultipleChoiceField(
         queryset=ASN.objects.all(),
         queryset=ASN.objects.all(),
         label=_('ASNs'),
         label=_('ASNs'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Provider
     model = Provider
     fieldsets = (
     fieldsets = (
@@ -58,18 +52,12 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
+class ProviderAccountBulkEditForm(PrimaryModelBulkEditForm):
     provider = DynamicModelChoiceField(
     provider = DynamicModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ProviderAccount
     model = ProviderAccount
     fieldsets = (
     fieldsets = (
@@ -80,7 +68,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
+class ProviderNetworkBulkEditForm(PrimaryModelBulkEditForm):
     provider = DynamicModelChoiceField(
     provider = DynamicModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -91,12 +79,6 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         label=_('Service ID')
         label=_('Service ID')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ProviderNetwork
     model = ProviderNetwork
     fieldsets = (
     fieldsets = (
@@ -107,16 +89,11 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
+class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
     color = ColorField(
     color = ColorField(
         label=_('Color'),
         label=_('Color'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = CircuitType
     model = CircuitType
     fieldsets = (
     fieldsets = (
@@ -125,7 +102,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
 
 
-class CircuitBulkEditForm(NetBoxModelBulkEditForm):
+class CircuitBulkEditForm(PrimaryModelBulkEditForm):
     type = DynamicModelChoiceField(
     type = DynamicModelChoiceField(
         label=_('Type'),
         label=_('Type'),
         queryset=CircuitType.objects.all(),
         queryset=CircuitType.objects.all(),
@@ -183,12 +160,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=100,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Circuit
     model = Circuit
     fieldsets = (
     fieldsets = (
@@ -261,12 +232,7 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
                 pass
                 pass
 
 
 
 
-class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
+class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -298,16 +264,11 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('priority',)
     nullable_fields = ('priority',)
 
 
 
 
-class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
+class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
     color = ColorField(
     color = ColorField(
         label=_('Color'),
         label=_('Color'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = VirtualCircuitType
     model = VirtualCircuitType
     fieldsets = (
     fieldsets = (
@@ -316,7 +277,7 @@ class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
 
 
-class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
+class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
     provider_network = DynamicModelChoiceField(
     provider_network = DynamicModelChoiceField(
         label=_('Provider network'),
         label=_('Provider network'),
         queryset=ProviderNetwork.objects.all(),
         queryset=ProviderNetwork.objects.all(),
@@ -343,12 +304,6 @@ class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=100,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = VirtualCircuit
     model = VirtualCircuit
     fieldsets = (
     fieldsets = (

+ 18 - 19
netbox/circuits/forms/bulk_import.py

@@ -7,7 +7,7 @@ from circuits.constants import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Interface
 from dcim.models import Interface
 from netbox.choices import DistanceUnitChoices
 from netbox.choices import DistanceUnitChoices
-from netbox.forms import NetBoxModelImportForm
+from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
 from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
 
 
@@ -28,17 +28,17 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderImportForm(NetBoxModelImportForm):
+class ProviderImportForm(PrimaryModelImportForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = (
         fields = (
-            'name', 'slug', 'description', 'comments', 'tags',
+            'name', 'slug', 'description', 'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class ProviderAccountImportForm(NetBoxModelImportForm):
+class ProviderAccountImportForm(PrimaryModelImportForm):
     provider = CSVModelChoiceField(
     provider = CSVModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -49,11 +49,11 @@ class ProviderAccountImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = ProviderAccount
         model = ProviderAccount
         fields = (
         fields = (
-            'provider', 'name', 'account', 'description', 'comments', 'tags',
+            'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class ProviderNetworkImportForm(NetBoxModelImportForm):
+class ProviderNetworkImportForm(PrimaryModelImportForm):
     provider = CSVModelChoiceField(
     provider = CSVModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -64,19 +64,19 @@ class ProviderNetworkImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = ProviderNetwork
         model = ProviderNetwork
         fields = [
         fields = [
-            'provider', 'name', 'service_id', 'description', 'comments', 'tags'
+            'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags'
         ]
         ]
 
 
 
 
-class CircuitTypeImportForm(NetBoxModelImportForm):
+class CircuitTypeImportForm(OrganizationalModelImportForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
         model = CircuitType
         model = CircuitType
-        fields = ('name', 'slug', 'color', 'description', 'tags')
+        fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
 
 
 
 
-class CircuitImportForm(NetBoxModelImportForm):
+class CircuitImportForm(PrimaryModelImportForm):
     provider = CSVModelChoiceField(
     provider = CSVModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -119,7 +119,7 @@ class CircuitImportForm(NetBoxModelImportForm):
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
             '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 CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
         }
         }
 
 
 
 
-class CircuitGroupImportForm(NetBoxModelImportForm):
+class CircuitGroupImportForm(OrganizationalModelImportForm):
     tenant = CSVModelChoiceField(
     tenant = CSVModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -176,7 +176,7 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = CircuitGroup
         model = CircuitGroup
-        fields = ('name', 'slug', 'description', 'tenant', 'tags')
+        fields = ('name', 'slug', 'description', 'tenant', 'owner', 'tags')
 
 
 
 
 class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
 class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
@@ -195,15 +195,14 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
         fields = ('member_type', 'member_id', 'group', 'priority')
         fields = ('member_type', 'member_id', 'group', 'priority')
 
 
 
 
-class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
-    slug = SlugField()
+class VirtualCircuitTypeImportForm(OrganizationalModelImportForm):
 
 
     class Meta:
     class Meta:
         model = VirtualCircuitType
         model = VirtualCircuitType
-        fields = ('name', 'slug', 'color', 'description', 'tags')
+        fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
 
 
 
 
-class VirtualCircuitImportForm(NetBoxModelImportForm):
+class VirtualCircuitImportForm(PrimaryModelImportForm):
     provider_network = CSVModelChoiceField(
     provider_network = CSVModelChoiceField(
         label=_('Provider network'),
         label=_('Provider network'),
         queryset=ProviderNetwork.objects.all(),
         queryset=ProviderNetwork.objects.all(),
@@ -239,8 +238,8 @@ class VirtualCircuitImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = VirtualCircuit
         model = VirtualCircuit
         fields = [
         fields = [
-            'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
-            'tags',
+            'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'owner',
+            'comments', 'tags',
         ]
         ]
 
 
 
 

+ 17 - 17
netbox/circuits/forms/filtersets.py

@@ -9,7 +9,7 @@ from circuits.models import *
 from dcim.models import Location, Region, Site, SiteGroup
 from dcim.models import Location, Region, Site, SiteGroup
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.choices import DistanceUnitChoices
 from netbox.choices import DistanceUnitChoices
-from netbox.forms import NetBoxModelFilterSetForm
+from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
@@ -31,10 +31,10 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = Provider
     model = Provider
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('asn_id', name=_('ASN')),
         FieldSet('asn_id', name=_('ASN')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -66,10 +66,10 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = ProviderAccount
     model = ProviderAccount
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('provider_id', 'account', name=_('Attributes')),
         FieldSet('provider_id', 'account', name=_('Attributes')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
@@ -85,10 +85,10 @@ class ProviderAccountFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
+class ProviderNetworkFilterForm(PrimaryModelFilterSetForm):
     model = ProviderNetwork
     model = ProviderNetwork
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('provider_id', 'service_id', name=_('Attributes')),
         FieldSet('provider_id', 'service_id', name=_('Attributes')),
     )
     )
     provider_id = DynamicModelMultipleChoiceField(
     provider_id = DynamicModelMultipleChoiceField(
@@ -104,10 +104,10 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
+class CircuitTypeFilterForm(OrganizationalModelFilterSetForm):
     model = CircuitType
     model = CircuitType
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('color', name=_('Attributes')),
         FieldSet('color', name=_('Attributes')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
@@ -118,10 +118,10 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
     )
     )
 
 
 
 
-class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
+class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = Circuit
     model = Circuit
     fieldsets = (
     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('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
         FieldSet(
         FieldSet(
             'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
             'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
@@ -271,10 +271,10 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
     model = CircuitGroup
     model = CircuitGroup
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
@@ -309,10 +309,10 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
+class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm):
     model = VirtualCircuitType
     model = VirtualCircuitType
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('color', name=_('Attributes')),
         FieldSet('color', name=_('Attributes')),
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
@@ -323,10 +323,10 @@ class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
     )
     )
 
 
 
 
-class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
+class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = VirtualCircuit
     model = VirtualCircuit
     fieldsets = (
     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('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
         FieldSet('type_id', 'status', name=_('Attributes')),
         FieldSet('type_id', 'status', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),

+ 18 - 29
netbox/circuits/forms/model_forms.py

@@ -10,11 +10,11 @@ from circuits.constants import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Interface, Site
 from dcim.models import Interface, Site
 from ipam.models import ASN
 from ipam.models import ASN
-from netbox.forms import NetBoxModelForm
+from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms import get_field_value
 from utilities.forms import get_field_value
 from utilities.forms.fields import (
 from utilities.forms.fields import (
-    CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
+    ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
 )
 )
 from utilities.forms.mixins import DistanceValidationMixin
 from utilities.forms.mixins import DistanceValidationMixin
 from utilities.forms.rendering import FieldSet, InlineFields
 from utilities.forms.rendering import FieldSet, InlineFields
@@ -36,14 +36,13 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderForm(NetBoxModelForm):
+class ProviderForm(PrimaryModelForm):
     slug = SlugField()
     slug = SlugField()
     asns = DynamicModelMultipleChoiceField(
     asns = DynamicModelMultipleChoiceField(
         queryset=ASN.objects.all(),
         queryset=ASN.objects.all(),
         label=_('ASNs'),
         label=_('ASNs'),
         required=False
         required=False
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'asns', 'description', 'tags'),
         FieldSet('name', 'slug', 'asns', 'description', 'tags'),
@@ -52,34 +51,32 @@ class ProviderForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = [
         fields = [
-            'name', 'slug', 'asns', 'description', 'comments', 'tags',
+            'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ProviderAccountForm(NetBoxModelForm):
+class ProviderAccountForm(PrimaryModelForm):
     provider = DynamicModelChoiceField(
     provider = DynamicModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         selector=True,
         selector=True,
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = ProviderAccount
         model = ProviderAccount
         fields = [
         fields = [
-            'provider', 'name', 'account', 'description', 'comments', 'tags',
+            'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ProviderNetworkForm(NetBoxModelForm):
+class ProviderNetworkForm(PrimaryModelForm):
     provider = DynamicModelChoiceField(
     provider = DynamicModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
         selector=True,
         selector=True,
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
         FieldSet('provider', 'name', 'service_id', 'description', 'tags'),
@@ -88,15 +85,13 @@ class ProviderNetworkForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = ProviderNetwork
         model = ProviderNetwork
         fields = [
         fields = [
-            'provider', 'name', 'service_id', 'description', 'comments', 'tags',
+            'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class CircuitTypeForm(NetBoxModelForm):
-    slug = SlugField()
-
+class CircuitTypeForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
-        FieldSet('name', 'slug', 'color', 'description', 'tags'),
+        FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
     )
     )
 
 
     class Meta:
     class Meta:
@@ -106,7 +101,7 @@ class CircuitTypeForm(NetBoxModelForm):
         ]
         ]
 
 
 
 
-class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
+class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
     provider = DynamicModelChoiceField(
     provider = DynamicModelChoiceField(
         label=_('Provider'),
         label=_('Provider'),
         queryset=Provider.objects.all(),
         queryset=Provider.objects.all(),
@@ -125,7 +120,6 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
         queryset=CircuitType.objects.all(),
         queryset=CircuitType.objects.all(),
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -147,7 +141,7 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
             '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 = {
         widgets = {
             'install_date': DatePicker(),
             'install_date': DatePicker(),
@@ -233,9 +227,7 @@ class CircuitTerminationForm(NetBoxModelForm):
         self.instance.termination = self.cleaned_data.get('termination')
         self.instance.termination = self.cleaned_data.get('termination')
 
 
 
 
-class CircuitGroupForm(TenancyForm, NetBoxModelForm):
-    slug = SlugField()
-
+class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
         FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
@@ -244,7 +236,7 @@ class CircuitGroupForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = CircuitGroup
         model = CircuitGroup
         fields = [
         fields = [
-            'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
+            'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -307,9 +299,7 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
         self.instance.member = self.cleaned_data.get('member')
         self.instance.member = self.cleaned_data.get('member')
 
 
 
 
-class VirtualCircuitTypeForm(NetBoxModelForm):
-    slug = SlugField()
-
+class VirtualCircuitTypeForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'color', 'description', 'tags'),
         FieldSet('name', 'slug', 'color', 'description', 'tags'),
     )
     )
@@ -317,11 +307,11 @@ class VirtualCircuitTypeForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = VirtualCircuitType
         model = VirtualCircuitType
         fields = [
         fields = [
-            'name', 'slug', 'color', 'description', 'tags',
+            'name', 'slug', 'color', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
+class VirtualCircuitForm(TenancyForm, PrimaryModelForm):
     provider_network = DynamicModelChoiceField(
     provider_network = DynamicModelChoiceField(
         label=_('Provider network'),
         label=_('Provider network'),
         queryset=ProviderNetwork.objects.all(),
         queryset=ProviderNetwork.objects.all(),
@@ -336,7 +326,6 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
         queryset=VirtualCircuitType.objects.all(),
         queryset=VirtualCircuitType.objects.all(),
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -350,7 +339,7 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
         model = VirtualCircuit
         model = VirtualCircuit
         fields = [
         fields = [
             'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
             'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
-            'comments', 'tags',
+            'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 

+ 6 - 11
netbox/circuits/graphql/types.py

@@ -6,7 +6,7 @@ import strawberry_django
 from circuits import models
 from circuits import models
 from dcim.graphql.mixins import CabledObjectMixin
 from dcim.graphql.mixins import CabledObjectMixin
 from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
 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 tenancy.graphql.types import TenantType
 from .filters import *
 from .filters import *
 
 
@@ -35,8 +35,7 @@ __all__ = (
     filters=ProviderFilter,
     filters=ProviderFilter,
     pagination=True
     pagination=True
 )
 )
-class ProviderType(NetBoxObjectType, ContactsMixin):
-
+class ProviderType(ContactsMixin, PrimaryObjectType):
     networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
     networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
     circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
     circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
     asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
     asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
@@ -49,9 +48,8 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
     filters=ProviderAccountFilter,
     filters=ProviderAccountFilter,
     pagination=True
     pagination=True
 )
 )
-class ProviderAccountType(ContactsMixin, NetBoxObjectType):
+class ProviderAccountType(ContactsMixin, PrimaryObjectType):
     provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
     provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
-
     circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
     circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
 
 
 
 
@@ -61,9 +59,8 @@ class ProviderAccountType(ContactsMixin, NetBoxObjectType):
     filters=ProviderNetworkFilter,
     filters=ProviderNetworkFilter,
     pagination=True
     pagination=True
 )
 )
-class ProviderNetworkType(NetBoxObjectType):
+class ProviderNetworkType(PrimaryObjectType):
     provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
     provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
-
     circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
     circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
 
 
 
 
@@ -105,14 +102,13 @@ class CircuitTypeType(OrganizationalObjectType):
     filters=CircuitFilter,
     filters=CircuitFilter,
     pagination=True
     pagination=True
 )
 )
-class CircuitType(NetBoxObjectType, ContactsMixin):
+class CircuitType(PrimaryObjectType, ContactsMixin):
     provider: ProviderType
     provider: ProviderType
     provider_account: ProviderAccountType | None
     provider_account: ProviderAccountType | None
     termination_a: CircuitTerminationType | None
     termination_a: CircuitTerminationType | None
     termination_z: CircuitTerminationType | None
     termination_z: CircuitTerminationType | None
     type: CircuitTypeType
     type: CircuitTypeType
     tenant: TenantType | None
     tenant: TenantType | None
-
     terminations: List[CircuitTerminationType]
     terminations: List[CircuitTerminationType]
 
 
 
 
@@ -178,12 +174,11 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
     filters=VirtualCircuitFilter,
     filters=VirtualCircuitFilter,
     pagination=True
     pagination=True
 )
 )
-class VirtualCircuitType(NetBoxObjectType):
+class VirtualCircuitType(PrimaryObjectType):
     provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
     provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
     provider_account: ProviderAccountType | None
     provider_account: ProviderAccountType | None
     type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
     type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
         select_related=["type"]
         select_related=["type"]
     )
     )
     tenant: TenantType | None
     tenant: TenantType | None
-
     terminations: List[VirtualCircuitTerminationType]
     terminations: List[VirtualCircuitTerminationType]

+ 68 - 0
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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='circuitgroup',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='provider',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='providernetwork',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='virtualcircuittype',
+            name='owner',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
+            ),
+        ),
+    ]

+ 8 - 13
netbox/circuits/tables/circuits.py

@@ -1,11 +1,9 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 
 
 from circuits.models import *
 from circuits.models import *
+from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
-
-from netbox.tables import NetBoxTable, columns
-
 from .columns import CommitRateColumn
 from .columns import CommitRateColumn
 
 
 __all__ = (
 __all__ = (
@@ -24,7 +22,7 @@ CIRCUITTERMINATION_LINK = """
 """
 """
 
 
 
 
-class CircuitTypeTable(NetBoxTable):
+class CircuitTypeTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Name'),
         verbose_name=_('Name'),
@@ -39,7 +37,7 @@ class CircuitTypeTable(NetBoxTable):
         verbose_name=_('Circuits')
         verbose_name=_('Circuits')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = CircuitType
         model = CircuitType
         fields = (
         fields = (
             'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
             'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
@@ -48,7 +46,7 @@ class CircuitTypeTable(NetBoxTable):
         default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
         default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
 
 
 
 
-class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
+class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
     cid = tables.Column(
     cid = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Circuit ID')
         verbose_name=_('Circuit ID')
@@ -79,9 +77,6 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         verbose_name=_('Commit Rate')
         verbose_name=_('Commit Rate')
     )
     )
     distance = columns.DistanceColumn()
     distance = columns.DistanceColumn()
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments')
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='circuits:circuit_list'
         url_name='circuits:circuit_list'
     )
     )
@@ -90,7 +85,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         linkify_item=True
         linkify_item=True
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Circuit
         model = Circuit
         fields = (
         fields = (
             'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
             'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
@@ -163,7 +158,7 @@ class CircuitTerminationTable(NetBoxTable):
         )
         )
 
 
 
 
-class CircuitGroupTable(NetBoxTable):
+class CircuitGroupTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -177,7 +172,7 @@ class CircuitGroupTable(NetBoxTable):
         url_name='circuits:circuitgroup_list'
         url_name='circuits:circuitgroup_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = CircuitGroup
         model = CircuitGroup
         fields = (
         fields = (
             'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
             'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',

+ 9 - 18
netbox/circuits/tables/providers.py

@@ -1,10 +1,10 @@
 import django_tables2 as tables
 import django_tables2 as tables
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
-from circuits.models import *
 from django_tables2.utils import Accessor
 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__ = (
 __all__ = (
     'ProviderTable',
     'ProviderTable',
@@ -13,7 +13,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderTable(ContactsColumnMixin, NetBoxTable):
+class ProviderTable(ContactsColumnMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -42,14 +42,11 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
         url_params={'provider_id': 'pk'},
         url_params={'provider_id': 'pk'},
         verbose_name=_('Circuits')
         verbose_name=_('Circuits')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='circuits:provider_list'
         url_name='circuits:provider_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Provider
         model = Provider
         fields = (
         fields = (
             'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
             'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
@@ -58,7 +55,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
         default_columns = ('pk', 'name', 'account_count', 'circuit_count')
         default_columns = ('pk', 'name', 'account_count', 'circuit_count')
 
 
 
 
-class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
+class ProviderAccountTable(ContactsColumnMixin, PrimaryModelTable):
     account = tables.Column(
     account = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Account'),
         verbose_name=_('Account'),
@@ -76,14 +73,11 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
         url_params={'provider_account_id': 'pk'},
         url_params={'provider_account_id': 'pk'},
         verbose_name=_('Circuits')
         verbose_name=_('Circuits')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='circuits:provideraccount_list'
         url_name='circuits:provideraccount_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ProviderAccount
         model = ProviderAccount
         fields = (
         fields = (
             'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
             'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
@@ -92,7 +86,7 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
         default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
         default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
 
 
 
 
-class ProviderNetworkTable(NetBoxTable):
+class ProviderNetworkTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -101,14 +95,11 @@ class ProviderNetworkTable(NetBoxTable):
         verbose_name=_('Provider'),
         verbose_name=_('Provider'),
         linkify=True
         linkify=True
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='circuits:providernetwork_list'
         url_name='circuits:providernetwork_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ProviderNetwork
         model = ProviderNetwork
         fields = (
         fields = (
             'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags',
             'pk', 'id', 'name', 'provider', 'service_id', 'description', 'comments', 'created', 'last_updated', 'tags',

+ 5 - 8
netbox/circuits/tables/virtual_circuits.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from circuits.models import *
 from circuits.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
@@ -12,7 +12,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VirtualCircuitTypeTable(NetBoxTable):
+class VirtualCircuitTypeTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Name'),
         verbose_name=_('Name'),
@@ -27,7 +27,7 @@ class VirtualCircuitTypeTable(NetBoxTable):
         verbose_name=_('Circuits')
         verbose_name=_('Circuits')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = VirtualCircuitType
         model = VirtualCircuitType
         fields = (
         fields = (
             'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
             'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
@@ -36,7 +36,7 @@ class VirtualCircuitTypeTable(NetBoxTable):
         default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
         default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
 
 
 
 
-class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
+class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
     cid = tables.Column(
     cid = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Circuit ID')
         verbose_name=_('Circuit ID')
@@ -63,14 +63,11 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
         url_params={'virtual_circuit_id': 'pk'},
         url_params={'virtual_circuit_id': 'pk'},
         verbose_name=_('Terminations')
         verbose_name=_('Terminations')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments')
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='circuits:virtualcircuit_list'
         url_name='circuits:virtualcircuit_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = VirtualCircuit
         model = VirtualCircuit
         fields = (
         fields = (
             'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
             'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',

+ 4 - 4
netbox/core/api/serializers_/data.py

@@ -1,7 +1,7 @@
 from core.choices import *
 from core.choices import *
 from core.models import DataFile, DataSource
 from core.models import DataFile, DataSource
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 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
 from netbox.utils import get_data_backend_choices
 
 
 __all__ = (
 __all__ = (
@@ -10,7 +10,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DataSourceSerializer(NetBoxModelSerializer):
+class DataSourceSerializer(PrimaryModelSerializer):
     type = ChoiceField(
     type = ChoiceField(
         choices=get_data_backend_choices()
         choices=get_data_backend_choices()
     )
     )
@@ -26,8 +26,8 @@ class DataSourceSerializer(NetBoxModelSerializer):
         model = DataSource
         model = DataSource
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 

+ 2 - 2
netbox/core/filtersets.py

@@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.db.models import Q
 from django.utils.translation import gettext as _
 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 netbox.utils import get_data_backend_choices
 from users.models import User
 from users.models import User
 from utilities.filters import ContentTypeFilter
 from utilities.filters import ContentTypeFilter
@@ -20,7 +20,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DataSourceFilterSet(NetBoxModelFilterSet):
+class DataSourceFilterSet(PrimaryModelFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=get_data_backend_choices,
         choices=get_data_backend_choices,
         null_value=None
         null_value=None

+ 2 - 9
netbox/core/forms/bulk_edit.py

@@ -3,9 +3,8 @@ from django.utils.translation import gettext_lazy as _
 
 
 from core.choices import JobIntervalChoices
 from core.choices import JobIntervalChoices
 from core.models import *
 from core.models import *
-from netbox.forms import NetBoxModelBulkEditForm
+from netbox.forms import PrimaryModelBulkEditForm
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
-from utilities.forms.fields import CommentField
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 
 
@@ -14,7 +13,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
+class DataSourceBulkEditForm(PrimaryModelBulkEditForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         label=_('Type'),
         label=_('Type'),
         choices=get_data_backend_choices,
         choices=get_data_backend_choices,
@@ -25,17 +24,11 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
         widget=BulkEditNullBooleanSelect(),
         widget=BulkEditNullBooleanSelect(),
         label=_('Enabled')
         label=_('Enabled')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
     sync_interval = forms.ChoiceField(
     sync_interval = forms.ChoiceField(
         choices=JobIntervalChoices,
         choices=JobIntervalChoices,
         required=False,
         required=False,
         label=_('Sync interval')
         label=_('Sync interval')
     )
     )
-    comments = CommentField()
     parameters = forms.JSONField(
     parameters = forms.JSONField(
         label=_('Parameters'),
         label=_('Parameters'),
         required=False
         required=False

+ 3 - 3
netbox/core/forms/bulk_import.py

@@ -1,16 +1,16 @@
 from core.models import *
 from core.models import *
-from netbox.forms import NetBoxModelImportForm
+from netbox.forms import PrimaryModelImportForm
 
 
 __all__ = (
 __all__ = (
     'DataSourceImportForm',
     'DataSourceImportForm',
 )
 )
 
 
 
 
-class DataSourceImportForm(NetBoxModelImportForm):
+class DataSourceImportForm(PrimaryModelImportForm):
 
 
     class Meta:
     class Meta:
         model = DataSource
         model = DataSource
         fields = (
         fields = (
             'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules',
             'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'parameters', 'ignore_rules',
-            'comments',
+            'owner', 'comments',
         )
         )

+ 5 - 4
netbox/core/forms/filtersets.py

@@ -3,13 +3,13 @@ from django.utils.translation import gettext_lazy as _
 
 
 from core.choices import *
 from core.choices import *
 from core.models import *
 from core.models import *
-from netbox.forms import NetBoxModelFilterSetForm
+from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm
 from netbox.forms.mixins import SavedFiltersMixin
 from netbox.forms.mixins import SavedFiltersMixin
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from users.models import User
 from users.models import User
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
 from utilities.forms.fields import (
 from utilities.forms.fields import (
-    ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
+    ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
 )
 )
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DateTimePicker
 from utilities.forms.widgets import DateTimePicker
@@ -23,10 +23,10 @@ __all__ = (
 )
 )
 
 
 
 
-class DataSourceFilterForm(NetBoxModelFilterSetForm):
+class DataSourceFilterForm(PrimaryModelFilterSetForm):
     model = DataSource
     model = DataSource
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
         FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
@@ -51,6 +51,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
         choices=JobIntervalChoices,
         choices=JobIntervalChoices,
         required=False
         required=False
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 class DataFileFilterForm(NetBoxModelFilterSetForm):
 class DataFileFilterForm(NetBoxModelFilterSetForm):

+ 5 - 5
netbox/core/forms/model_forms.py

@@ -9,11 +9,11 @@ from django.utils.translation import gettext_lazy as _
 from core.forms.mixins import SyncedDataMixin
 from core.forms.mixins import SyncedDataMixin
 from core.models import *
 from core.models import *
 from netbox.config import get_config, PARAMS
 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.registry import registry
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from utilities.forms import get_field_value
 from utilities.forms import get_field_value
-from utilities.forms.fields import CommentField, JSONField
+from utilities.forms.fields import JSONField
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import HTMXSelect
 from utilities.forms.widgets import HTMXSelect
 
 
@@ -26,17 +26,17 @@ __all__ = (
 EMPTY_VALUES = ('', None, [], ())
 EMPTY_VALUES = ('', None, [], ())
 
 
 
 
-class DataSourceForm(NetBoxModelForm):
+class DataSourceForm(PrimaryModelForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         choices=get_data_backend_choices,
         choices=get_data_backend_choices,
         widget=HTMXSelect()
         widget=HTMXSelect()
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = DataSource
         model = DataSource
         fields = [
         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 = {
         widgets = {
             'ignore_rules': forms.Textarea(
             'ignore_rules': forms.Textarea(

+ 2 - 3
netbox/core/graphql/types.py

@@ -5,7 +5,7 @@ import strawberry_django
 from django.contrib.contenttypes.models import ContentType as DjangoContentType
 from django.contrib.contenttypes.models import ContentType as DjangoContentType
 
 
 from core import models
 from core import models
-from netbox.graphql.types import BaseObjectType, NetBoxObjectType
+from netbox.graphql.types import BaseObjectType, PrimaryObjectType
 from .filters import *
 from .filters import *
 
 
 __all__ = (
 __all__ = (
@@ -32,8 +32,7 @@ class DataFileType(BaseObjectType):
     filters=DataSourceFilter,
     filters=DataSourceFilter,
     pagination=True
     pagination=True
 )
 )
-class DataSourceType(NetBoxObjectType):
-
+class DataSourceType(PrimaryObjectType):
     datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
     datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
 
 
 
 

+ 19 - 0
netbox/core/migrations/0020_owner.py

@@ -0,0 +1,19 @@
+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, to='users.owner'
+            ),
+        ),
+    ]

+ 3 - 3
netbox/core/tables/data.py

@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
 
 
 from core.models import *
 from core.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import NetBoxTable, PrimaryModelTable, columns
 from .columns import BackendTypeColumn
 from .columns import BackendTypeColumn
 from .template_code import DATA_SOURCE_SYNC_BUTTON
 from .template_code import DATA_SOURCE_SYNC_BUTTON
 
 
@@ -12,7 +12,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DataSourceTable(NetBoxTable):
+class DataSourceTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True,
         linkify=True,
@@ -42,7 +42,7 @@ class DataSourceTable(NetBoxTable):
         extra_buttons=DATA_SOURCE_SYNC_BUTTON,
         extra_buttons=DATA_SOURCE_SYNC_BUTTON,
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = DataSource
         model = DataSource
         fields = (
         fields = (
             'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments',
             'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'sync_interval', 'comments',

+ 6 - 4
netbox/dcim/api/serializers_/cables.py

@@ -5,7 +5,9 @@ from rest_framework import serializers
 from dcim.choices import *
 from dcim.choices import *
 from dcim.models import Cable, CablePath, CableTermination
 from dcim.models import Cable, CablePath, CableTermination
 from netbox.api.fields import ChoiceField, ContentTypeField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 
 
@@ -18,7 +20,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CableSerializer(NetBoxModelSerializer):
+class CableSerializer(PrimaryModelSerializer):
     a_terminations = GenericObjectSerializer(many=True, required=False)
     a_terminations = GenericObjectSerializer(many=True, required=False)
     b_terminations = GenericObjectSerializer(many=True, required=False)
     b_terminations = GenericObjectSerializer(many=True, required=False)
     status = ChoiceField(choices=LinkStatusChoices, required=False)
     status = ChoiceField(choices=LinkStatusChoices, required=False)
@@ -29,8 +31,8 @@ class CableSerializer(NetBoxModelSerializer):
         model = Cable
         model = Cable
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant',
             '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')
         brief_fields = ('id', 'url', 'display', 'label', 'description')
 
 

+ 11 - 11
netbox/dcim/api/serializers_/devices.py

@@ -11,15 +11,15 @@ from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceCont
 from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
 from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
 from ipam.api.serializers_.ip import IPAddressSerializer
 from ipam.api.serializers_.ip import IPAddressSerializer
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from virtualization.api.serializers_.clusters import ClusterSerializer
 from virtualization.api.serializers_.clusters import ClusterSerializer
 from .devicetypes import *
 from .devicetypes import *
+from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
 from .platforms import PlatformSerializer
 from .platforms import PlatformSerializer
 from .racks import RackSerializer
 from .racks import RackSerializer
 from .roles import DeviceRoleSerializer
 from .roles import DeviceRoleSerializer
-from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
 from .sites import LocationSerializer, SiteSerializer
 from .sites import LocationSerializer, SiteSerializer
 from .virtualchassis import VirtualChassisSerializer
 from .virtualchassis import VirtualChassisSerializer
 
 
@@ -32,7 +32,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DeviceSerializer(NetBoxModelSerializer):
+class DeviceSerializer(PrimaryModelSerializer):
     device_type = DeviceTypeSerializer(nested=True)
     device_type = DeviceTypeSerializer(nested=True)
     role = DeviceRoleSerializer(nested=True)
     role = DeviceRoleSerializer(nested=True)
     tenant = TenantSerializer(
     tenant = TenantSerializer(
@@ -84,8 +84,8 @@ class DeviceSerializer(NetBoxModelSerializer):
             'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
             'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
             'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
             'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
             'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
             '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',
             'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
             'device_bay_count', 'module_bay_count', 'inventory_item_count',
             'device_bay_count', 'module_bay_count', 'inventory_item_count',
         ]
         ]
@@ -122,7 +122,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
         return obj.get_config_context()
         return obj.get_config_context()
 
 
 
 
-class VirtualDeviceContextSerializer(NetBoxModelSerializer):
+class VirtualDeviceContextSerializer(PrimaryModelSerializer):
     device = DeviceSerializer(nested=True)
     device = DeviceSerializer(nested=True)
     identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
     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)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
@@ -138,13 +138,13 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
         model = VirtualDeviceContext
         model = VirtualDeviceContext
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip',
             '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',
             'created', 'last_updated', 'interface_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
 
 
 
 
-class ModuleSerializer(NetBoxModelSerializer):
+class ModuleSerializer(PrimaryModelSerializer):
     device = DeviceSerializer(nested=True)
     device = DeviceSerializer(nested=True)
     module_bay = NestedModuleBaySerializer()
     module_bay = NestedModuleBaySerializer()
     module_type = ModuleTypeSerializer(nested=True)
     module_type = ModuleTypeSerializer(nested=True)
@@ -154,12 +154,12 @@ class ModuleSerializer(NetBoxModelSerializer):
         model = Module
         model = Module
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
             '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')
         brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
 
 
 
 
-class MACAddressSerializer(NetBoxModelSerializer):
+class MACAddressSerializer(PrimaryModelSerializer):
     assigned_object_type = ContentTypeField(
     assigned_object_type = ContentTypeField(
         queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
         queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
         required=False,
         required=False,
@@ -171,7 +171,7 @@ class MACAddressSerializer(NetBoxModelSerializer):
         model = MACAddress
         model = MACAddress
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id',
             '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')
         brief_fields = ('id', 'url', 'display', 'mac_address', 'description')
 
 

+ 9 - 9
netbox/dcim/api/serializers_/devicetypes.py

@@ -6,7 +6,7 @@ from rest_framework import serializers
 from dcim.choices import *
 from dcim.choices import *
 from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
 from dcim.models import DeviceType, ModuleType, ModuleTypeProfile
 from netbox.api.fields import AttributesField, ChoiceField, RelatedObjectCountField
 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 netbox.choices import *
 from .manufacturers import ManufacturerSerializer
 from .manufacturers import ManufacturerSerializer
 from .platforms import PlatformSerializer
 from .platforms import PlatformSerializer
@@ -18,7 +18,7 @@ __all__ = (
 )
 )
 
 
 
 
-class DeviceTypeSerializer(NetBoxModelSerializer):
+class DeviceTypeSerializer(PrimaryModelSerializer):
     manufacturer = ManufacturerSerializer(nested=True)
     manufacturer = ManufacturerSerializer(nested=True)
     default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
     default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
     u_height = serializers.DecimalField(
     u_height = serializers.DecimalField(
@@ -54,7 +54,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number',
             'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number',
             'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
             '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',
             'created', 'last_updated', 'device_count', 'console_port_template_count',
             'console_server_port_template_count', 'power_port_template_count', 'power_outlet_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',
             'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
@@ -63,18 +63,18 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
         brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
 
 
 
 
-class ModuleTypeProfileSerializer(NetBoxModelSerializer):
+class ModuleTypeProfileSerializer(PrimaryModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ModuleTypeProfile
         model = ModuleTypeProfile
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
 
 
-class ModuleTypeSerializer(NetBoxModelSerializer):
+class ModuleTypeSerializer(PrimaryModelSerializer):
     profile = ModuleTypeProfileSerializer(
     profile = ModuleTypeProfileSerializer(
         nested=True,
         nested=True,
         required=False,
         required=False,
@@ -105,7 +105,7 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
         model = ModuleType
         model = ModuleType
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
             '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')
         brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description')

+ 3 - 3
netbox/dcim/api/serializers_/manufacturers.py

@@ -1,13 +1,13 @@
 from dcim.models import Manufacturer
 from dcim.models import Manufacturer
 from netbox.api.fields import RelatedObjectCountField
 from netbox.api.fields import RelatedObjectCountField
-from netbox.api.serializers import NetBoxModelSerializer
+from netbox.api.serializers import OrganizationalModelSerializer
 
 
 __all__ = (
 __all__ = (
     'ManufacturerSerializer',
     'ManufacturerSerializer',
 )
 )
 
 
 
 
-class ManufacturerSerializer(NetBoxModelSerializer):
+class ManufacturerSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     devicetype_count = RelatedObjectCountField('device_types')
     devicetype_count = RelatedObjectCountField('device_types')
@@ -17,7 +17,7 @@ class ManufacturerSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
         fields = [
         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',
             'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')

+ 1 - 1
netbox/dcim/api/serializers_/platforms.py

@@ -24,7 +24,7 @@ class PlatformSerializer(NestedGroupModelSerializer):
         model = Platform
         model = Platform
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'parent', 'name', 'slug', 'manufacturer', 'config_template',
             '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',
             'virtualmachine_count', '_depth',
         ]
         ]
         brief_fields = (
         brief_fields = (

+ 7 - 6
netbox/dcim/api/serializers_/power.py

@@ -1,7 +1,7 @@
 from dcim.choices import *
 from dcim.choices import *
 from dcim.models import PowerFeed, PowerPanel
 from dcim.models import PowerFeed, PowerPanel
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from .base import ConnectedEndpointsSerializer
 from .base import ConnectedEndpointsSerializer
 from .cables import CabledObjectSerializer
 from .cables import CabledObjectSerializer
@@ -14,7 +14,7 @@ __all__ = (
 )
 )
 
 
 
 
-class PowerPanelSerializer(NetBoxModelSerializer):
+class PowerPanelSerializer(PrimaryModelSerializer):
     site = SiteSerializer(nested=True)
     site = SiteSerializer(nested=True)
     location = LocationSerializer(
     location = LocationSerializer(
         nested=True,
         nested=True,
@@ -29,13 +29,13 @@ class PowerPanelSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count')
 
 
 
 
-class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
+class PowerFeedSerializer(PrimaryModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
     power_panel = PowerPanelSerializer(nested=True)
     power_panel = PowerPanelSerializer(nested=True)
     rack = RackSerializer(
     rack = RackSerializer(
         nested=True,
         nested=True,
@@ -71,6 +71,7 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
             'id', 'url', 'display_url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply',
             'id', 'url', 'display_url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply',
             'phase', 'voltage', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers',
             'phase', 'voltage', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers',
             'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied')

+ 11 - 11
netbox/dcim/api/serializers_/racks.py

@@ -5,7 +5,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import Rack, RackReservation, RackRole, RackType
 from dcim.models import Rack, RackReservation, RackRole, RackType
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 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.choices import *
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
@@ -22,7 +22,7 @@ __all__ = (
 )
 )
 
 
 
 
-class RackRoleSerializer(NetBoxModelSerializer):
+class RackRoleSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     rack_count = RelatedObjectCountField('racks')
     rack_count = RelatedObjectCountField('racks')
@@ -30,13 +30,13 @@ class RackRoleSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
 
 
 
 
-class RackBaseSerializer(NetBoxModelSerializer):
+class RackBaseSerializer(PrimaryModelSerializer):
     form_factor = ChoiceField(
     form_factor = ChoiceField(
         choices=RackFormFactorChoices,
         choices=RackFormFactorChoices,
         allow_blank=True,
         allow_blank=True,
@@ -71,8 +71,8 @@ class RackTypeSerializer(RackBaseSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
             'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
             'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
             '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')
         brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
 
 
@@ -130,13 +130,13 @@ class RackSerializer(RackBaseSerializer):
             'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
             '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',
             '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',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
 
 
 
 
-class RackReservationSerializer(NetBoxModelSerializer):
+class RackReservationSerializer(PrimaryModelSerializer):
     rack = RackSerializer(
     rack = RackSerializer(
         nested=True,
         nested=True,
     )
     )
@@ -157,7 +157,7 @@ class RackReservationSerializer(NetBoxModelSerializer):
         model = RackReservation
         model = RackReservation
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'rack', 'units', 'status', 'created', 'last_updated', 'user',
             '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')
         brief_fields = ('id', 'url', 'display', 'status', 'user', 'description', 'units')
 
 

+ 5 - 5
netbox/dcim/api/serializers_/roles.py

@@ -3,7 +3,7 @@ from rest_framework import serializers
 from dcim.models import DeviceRole, InventoryItemRole
 from dcim.models import DeviceRole, InventoryItemRole
 from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
 from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
 from netbox.api.fields import RelatedObjectCountField
 from netbox.api.fields import RelatedObjectCountField
-from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
+from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer
 from .nested import NestedDeviceRoleSerializer
 from .nested import NestedDeviceRoleSerializer
 
 
 __all__ = (
 __all__ = (
@@ -23,14 +23,14 @@ class DeviceRoleSerializer(NestedGroupModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'parent',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'parent',
             'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
             'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
-            'comments', '_depth',
+            'owner', 'comments', '_depth',
         ]
         ]
         brief_fields = (
         brief_fields = (
             'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth'
             'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth'
         )
         )
 
 
 
 
-class InventoryItemRoleSerializer(NetBoxModelSerializer):
+class InventoryItemRoleSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     inventoryitem_count = RelatedObjectCountField('inventory_items')
     inventoryitem_count = RelatedObjectCountField('inventory_items')
@@ -38,7 +38,7 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = InventoryItemRole
         model = InventoryItemRole
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')

+ 6 - 6
netbox/dcim/api/serializers_/sites.py

@@ -6,7 +6,7 @@ from dcim.models import Location, Region, Site, SiteGroup
 from ipam.api.serializers_.asns import ASNSerializer
 from ipam.api.serializers_.asns import ASNSerializer
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer
 from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer
 
 
@@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer):
         model = Region
         model = Region
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
 
 
@@ -41,12 +41,12 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
         model = SiteGroup
         model = SiteGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
 
 
 
 
-class SiteSerializer(NetBoxModelSerializer):
+class SiteSerializer(PrimaryModelSerializer):
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     region = RegionSerializer(nested=True, required=False, allow_null=True)
     region = RegionSerializer(nested=True, required=False, allow_null=True)
     group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
     group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
@@ -72,7 +72,7 @@ class SiteSerializer(NetBoxModelSerializer):
         model = Site
         model = Site
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility',
             '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',
             'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count',
             'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
             'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
         ]
         ]
@@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
             'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

+ 4 - 4
netbox/dcim/api/serializers_/virtualchassis.py

@@ -1,7 +1,7 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.models import VirtualChassis
 from dcim.models import VirtualChassis
-from netbox.api.serializers import NetBoxModelSerializer
+from netbox.api.serializers import PrimaryModelSerializer
 from .nested import NestedDeviceSerializer
 from .nested import NestedDeviceSerializer
 
 
 __all__ = (
 __all__ = (
@@ -9,7 +9,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VirtualChassisSerializer(NetBoxModelSerializer):
+class VirtualChassisSerializer(PrimaryModelSerializer):
     master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
     master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
     members = NestedDeviceSerializer(many=True, read_only=True)
     members = NestedDeviceSerializer(many=True, read_only=True)
 
 
@@ -19,7 +19,7 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')

+ 30 - 58
netbox/dcim/filtersets.py

@@ -11,11 +11,12 @@ from ipam.filtersets import PrimaryIPFilterSet
 from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
 from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.filtersets import (
 from netbox.filtersets import (
-    AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
-    OrganizationalModelFilterSet,
+    AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet,
+    OrganizationalModelFilterSet, PrimaryModelFilterSet, NetBoxModelFilterSet,
 )
 )
 from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
 from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
 from tenancy.models import *
 from tenancy.models import *
+from users.filterset_mixins import OwnerFilterMixin
 from users.models import User
 from users.models import User
 from utilities.filters import (
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
     ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
@@ -143,7 +144,7 @@ class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
         fields = ('id', 'name', 'slug', 'description')
         fields = ('id', 'name', 'slug', 'description')
 
 
 
 
-class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
         choices=SiteStatusChoices,
         choices=SiteStatusChoices,
         null_value=None
         null_value=None
@@ -293,7 +294,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'color', 'description')
         fields = ('id', 'name', 'slug', 'color', 'description')
 
 
 
 
-class RackTypeFilterSet(NetBoxModelFilterSet):
+class RackTypeFilterSet(PrimaryModelFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         label=_('Manufacturer (ID)'),
         label=_('Manufacturer (ID)'),
@@ -328,7 +329,7 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
         )
         )
 
 
 
 
-class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region',
         field_name='site__region',
@@ -444,7 +445,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
         )
         )
 
 
 
 
-class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     rack_id = django_filters.ModelMultipleChoiceFilter(
     rack_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
         label=_('Rack (ID)'),
         label=_('Rack (ID)'),
@@ -540,7 +541,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
         fields = ('id', 'name', 'slug', 'description')
         fields = ('id', 'name', 'slug', 'description')
 
 
 
 
-class DeviceTypeFilterSet(NetBoxModelFilterSet):
+class DeviceTypeFilterSet(PrimaryModelFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         label=_('Manufacturer (ID)'),
         label=_('Manufacturer (ID)'),
@@ -682,7 +683,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
         return queryset.exclude(inventoryitemtemplates__isnull=value)
         return queryset.exclude(inventoryitemtemplates__isnull=value)
 
 
 
 
-class ModuleTypeProfileFilterSet(NetBoxModelFilterSet):
+class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = ModuleTypeProfile
         model = ModuleTypeProfile
@@ -698,7 +699,7 @@ class ModuleTypeProfileFilterSet(NetBoxModelFilterSet):
         )
         )
 
 
 
 
-class ModuleTypeFilterSet(AttributeFiltersMixin, NetBoxModelFilterSet):
+class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
     profile_id = django_filters.ModelMultipleChoiceFilter(
     profile_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ModuleTypeProfile.objects.all(),
         queryset=ModuleTypeProfile.objects.all(),
         label=_('Profile (ID)'),
         label=_('Profile (ID)'),
@@ -951,7 +952,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class DeviceRoleFilterSet(OrganizationalModelFilterSet):
+class DeviceRoleFilterSet(NestedGroupModelFilterSet):
     config_template_id = django_filters.ModelMultipleChoiceFilter(
     config_template_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         label=_('Config template (ID)'),
         label=_('Config template (ID)'),
@@ -985,7 +986,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
         fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
 
 
 
 
-class PlatformFilterSet(OrganizationalModelFilterSet):
+class PlatformFilterSet(NestedGroupModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         label=_('Immediate parent platform (ID)'),
         label=_('Immediate parent platform (ID)'),
@@ -1043,7 +1044,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
 
 
 
 
 class DeviceFilterSet(
 class DeviceFilterSet(
-    NetBoxModelFilterSet,
+    PrimaryModelFilterSet,
     TenancyFilterSet,
     TenancyFilterSet,
     ContactModelFilterSet,
     ContactModelFilterSet,
     LocalConfigContextFilterSet,
     LocalConfigContextFilterSet,
@@ -1345,7 +1346,7 @@ class DeviceFilterSet(
         return queryset.exclude(params)
         return queryset.exclude(params)
 
 
 
 
-class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
+class VirtualDeviceContextFilterSet(PrimaryModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
     device_id = django_filters.ModelMultipleChoiceFilter(
     device_id = django_filters.ModelMultipleChoiceFilter(
         field_name='device',
         field_name='device',
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1394,7 +1395,7 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
         return queryset.exclude(params)
         return queryset.exclude(params)
 
 
 
 
-class ModuleFilterSet(NetBoxModelFilterSet):
+class ModuleFilterSet(PrimaryModelFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         field_name='module_type__manufacturer',
         field_name='module_type__manufacturer',
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -1516,7 +1517,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
         ).distinct()
         ).distinct()
 
 
 
 
-class DeviceComponentFilterSet(django_filters.FilterSet):
+class DeviceComponentFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -1682,12 +1683,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
             return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
             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(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -1698,12 +1694,7 @@ class ConsolePortFilterSet(
         fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
         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(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -1714,12 +1705,7 @@ class ConsoleServerPortFilterSet(
         fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
         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(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerPortTypeChoices,
         choices=PowerPortTypeChoices,
         null_value=None
         null_value=None
@@ -1732,12 +1718,7 @@ class PowerPortFilterSet(
         )
         )
 
 
 
 
-class PowerOutletFilterSet(
-    ModularDeviceComponentFilterSet,
-    NetBoxModelFilterSet,
-    CabledObjectFilterSet,
-    PathEndpointFilterSet
-):
+class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerOutletTypeChoices,
         choices=PowerOutletTypeChoices,
         null_value=None
         null_value=None
@@ -1762,7 +1743,7 @@ class PowerOutletFilterSet(
         )
         )
 
 
 
 
-class MACAddressFilterSet(NetBoxModelFilterSet):
+class MACAddressFilterSet(PrimaryModelFilterSet):
     mac_address = MultiValueMACAddressFilter()
     mac_address = MultiValueMACAddressFilter()
     assigned_object_type = ContentTypeFilter()
     assigned_object_type = ContentTypeFilter()
     device = MultiValueCharFilter(
     device = MultiValueCharFilter(
@@ -1914,7 +1895,6 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
 
 
 class InterfaceFilterSet(
 class InterfaceFilterSet(
     ModularDeviceComponentFilterSet,
     ModularDeviceComponentFilterSet,
-    NetBoxModelFilterSet,
     CabledObjectFilterSet,
     CabledObjectFilterSet,
     PathEndpointFilterSet,
     PathEndpointFilterSet,
     CommonInterfaceFilterSet
     CommonInterfaceFilterSet
@@ -2075,11 +2055,7 @@ class InterfaceFilterSet(
             )
             )
 
 
 
 
-class FrontPortFilterSet(
-    ModularDeviceComponentFilterSet,
-    NetBoxModelFilterSet,
-    CabledObjectFilterSet
-):
+class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -2095,11 +2071,7 @@ class FrontPortFilterSet(
         )
         )
 
 
 
 
-class RearPortFilterSet(
-    ModularDeviceComponentFilterSet,
-    NetBoxModelFilterSet,
-    CabledObjectFilterSet
-):
+class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -2112,7 +2084,7 @@ class RearPortFilterSet(
         )
         )
 
 
 
 
-class ModuleBayFilterSet(ModularDeviceComponentFilterSet, NetBoxModelFilterSet):
+class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ModuleBay.objects.all(),
         queryset=ModuleBay.objects.all(),
         label=_('Parent module bay (ID)'),
         label=_('Parent module bay (ID)'),
@@ -2128,7 +2100,7 @@ class ModuleBayFilterSet(ModularDeviceComponentFilterSet, NetBoxModelFilterSet):
         fields = ('id', 'name', 'label', 'position', 'description')
         fields = ('id', 'name', 'label', 'position', 'description')
 
 
 
 
-class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
+class DeviceBayFilterSet(DeviceComponentFilterSet):
     installed_device_id = django_filters.ModelMultipleChoiceFilter(
     installed_device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         label=_('Installed device (ID)'),
         label=_('Installed device (ID)'),
@@ -2145,7 +2117,7 @@ class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
         fields = ('id', 'name', 'label', 'description')
         fields = ('id', 'name', 'label', 'description')
 
 
 
 
-class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
+class InventoryItemFilterSet(DeviceComponentFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=InventoryItem.objects.all(),
         queryset=InventoryItem.objects.all(),
         label=_('Parent inventory item (ID)'),
         label=_('Parent inventory item (ID)'),
@@ -2204,7 +2176,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'color', 'description')
         fields = ('id', 'name', 'slug', 'color', 'description')
 
 
 
 
-class VirtualChassisFilterSet(NetBoxModelFilterSet):
+class VirtualChassisFilterSet(PrimaryModelFilterSet):
     master_id = django_filters.ModelMultipleChoiceFilter(
     master_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         label=_('Master (ID)'),
         label=_('Master (ID)'),
@@ -2280,7 +2252,7 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
         return queryset.filter(qs_filter).distinct()
         return queryset.filter(qs_filter).distinct()
 
 
 
 
-class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
+class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
     termination_a_type = ContentTypeFilter(
     termination_a_type = ContentTypeFilter(
         field_name='terminations__termination_type'
         field_name='terminations__termination_type'
     )
     )
@@ -2457,7 +2429,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
         fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
         fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
 
 
 
 
-class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
+class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region',
         field_name='site__region',
@@ -2515,7 +2487,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
+class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='power_panel__site__region',
         field_name='power_panel__site__region',

+ 30 - 160
netbox/dcim/forms/bulk_edit.py

@@ -10,14 +10,14 @@ from extras.models import ConfigTemplate
 from ipam.choices import VLANQinQRoleChoices
 from ipam.choices import VLANQinQRoleChoices
 from ipam.models import ASN, VLAN, VLANGroup, VRF
 from ipam.models import ASN, VLAN, VLANGroup, VRF
 from netbox.choices import *
 from netbox.choices import *
-from netbox.forms import NetBoxModelBulkEditForm
-from netbox.forms.mixins import ChangelogMessageMixin
+from netbox.forms import (
+    NestedGroupModelBulkEditForm, NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm,
+)
+from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from users.models import User
 from users.models import User
 from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
 from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
-from utilities.forms.fields import (
-    ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
-)
+from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField
 from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
 from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
 from virtualization.models import Cluster
 from virtualization.models import Cluster
@@ -71,18 +71,12 @@ __all__ = (
 )
 )
 
 
 
 
-class RegionBulkEditForm(NetBoxModelBulkEditForm):
+class RegionBulkEditForm(NestedGroupModelBulkEditForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Region
     model = Region
     fieldsets = (
     fieldsets = (
@@ -91,18 +85,12 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'description', 'comments')
     nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
-class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
+class SiteGroupBulkEditForm(NestedGroupModelBulkEditForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = SiteGroup
     model = SiteGroup
     fieldsets = (
     fieldsets = (
@@ -111,7 +99,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'description', 'comments')
     nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
-class SiteBulkEditForm(NetBoxModelBulkEditForm):
+class SiteBulkEditForm(PrimaryModelBulkEditForm):
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         label=_('Status'),
         label=_('Status'),
         choices=add_blank_choice(SiteStatusChoices),
         choices=add_blank_choice(SiteStatusChoices),
@@ -162,12 +150,6 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
         choices=add_blank_choice(TimeZoneFormField().choices),
         choices=add_blank_choice(TimeZoneFormField().choices),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Site
     model = Site
     fieldsets = (
     fieldsets = (
@@ -178,7 +160,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class LocationBulkEditForm(NetBoxModelBulkEditForm):
+class LocationBulkEditForm(NestedGroupModelBulkEditForm):
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -208,12 +190,6 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
         max_length=50,
         max_length=50,
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Location
     model = Location
     fieldsets = (
     fieldsets = (
@@ -222,16 +198,11 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'tenant', 'facility', 'description', 'comments')
     nullable_fields = ('parent', 'tenant', 'facility', 'description', 'comments')
 
 
 
 
-class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
+class RackRoleBulkEditForm(OrganizationalModelBulkEditForm):
     color = ColorField(
     color = ColorField(
         label=_('Color'),
         label=_('Color'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = RackRole
     model = RackRole
     fieldsets = (
     fieldsets = (
@@ -240,7 +211,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
 
 
-class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
+class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -310,12 +281,6 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = RackType
     model = RackType
     fieldsets = (
     fieldsets = (
@@ -334,7 +299,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class RackBulkEditForm(NetBoxModelBulkEditForm):
+class RackBulkEditForm(PrimaryModelBulkEditForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         label=_('Region'),
         label=_('Region'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -464,12 +429,6 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Rack
     model = Rack
     fieldsets = (
     fieldsets = (
@@ -485,7 +444,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
+class RackReservationBulkEditForm(PrimaryModelBulkEditForm):
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         label=_('Status'),
         label=_('Status'),
         choices=add_blank_choice(RackReservationStatusChoices),
         choices=add_blank_choice(RackReservationStatusChoices),
@@ -502,12 +461,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = RackReservation
     model = RackReservation
     fieldsets = (
     fieldsets = (
@@ -516,13 +469,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('comments',)
     nullable_fields = ('comments',)
 
 
 
 
-class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-
+class ManufacturerBulkEditForm(OrganizationalModelBulkEditForm):
     model = Manufacturer
     model = Manufacturer
     fieldsets = (
     fieldsets = (
         FieldSet('description'),
         FieldSet('description'),
@@ -530,7 +477,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
+class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -576,12 +523,6 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = DeviceType
     model = DeviceType
     fieldsets = (
     fieldsets = (
@@ -594,17 +535,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
     nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
 
 
 
 
-class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm):
+class ModuleTypeProfileBulkEditForm(PrimaryModelBulkEditForm):
     schema = JSONField(
     schema = JSONField(
         label=_('Schema'),
         label=_('Schema'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ModuleTypeProfile
     model = ModuleTypeProfile
     fieldsets = (
     fieldsets = (
@@ -613,7 +548,7 @@ class ModuleTypeProfileBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description', 'comments')
     nullable_fields = ('description', 'comments')
 
 
 
 
-class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
+class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm):
     profile = DynamicModelChoiceField(
     profile = DynamicModelChoiceField(
         label=_('Profile'),
         label=_('Profile'),
         queryset=ModuleTypeProfile.objects.all(),
         queryset=ModuleTypeProfile.objects.all(),
@@ -644,12 +579,6 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ModuleType
     model = ModuleType
     fieldsets = (
     fieldsets = (
@@ -663,7 +592,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'description', 'comments')
     nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'description', 'comments')
 
 
 
 
-class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
+class DeviceRoleBulkEditForm(NestedGroupModelBulkEditForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
@@ -683,12 +612,6 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = DeviceRole
     model = DeviceRole
     fieldsets = (
     fieldsets = (
@@ -697,7 +620,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'color', 'config_template', 'description', 'comments')
     nullable_fields = ('parent', 'color', 'config_template', 'description', 'comments')
 
 
 
 
-class PlatformBulkEditForm(NetBoxModelBulkEditForm):
+class PlatformBulkEditForm(NestedGroupModelBulkEditForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
@@ -713,12 +636,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Platform
     model = Platform
     fieldsets = (
     fieldsets = (
@@ -727,7 +644,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'manufacturer', 'config_template', 'description', 'comments')
     nullable_fields = ('parent', 'manufacturer', 'config_template', 'description', 'comments')
 
 
 
 
-class DeviceBulkEditForm(NetBoxModelBulkEditForm):
+class DeviceBulkEditForm(PrimaryModelBulkEditForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -787,11 +704,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         label=_('Serial Number')
         label=_('Serial Number')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
     config_template = DynamicModelChoiceField(
     config_template = DynamicModelChoiceField(
         label=_('Config template'),
         label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
@@ -805,7 +717,6 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
             'site_id': ['$site', 'null']
             'site_id': ['$site', 'null']
         },
         },
     )
     )
-    comments = CommentField()
 
 
     model = Device
     model = Device
     fieldsets = (
     fieldsets = (
@@ -820,7 +731,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class ModuleBulkEditForm(NetBoxModelBulkEditForm):
+class ModuleBulkEditForm(PrimaryModelBulkEditForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -848,12 +759,6 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         label=_('Serial Number')
         label=_('Serial Number')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Module
     model = Module
     fieldsets = (
     fieldsets = (
@@ -862,7 +767,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('serial', 'description', 'comments')
     nullable_fields = ('serial', 'description', 'comments')
 
 
 
 
-class CableBulkEditForm(NetBoxModelBulkEditForm):
+class CableBulkEditForm(PrimaryModelBulkEditForm):
     type = forms.ChoiceField(
     type = forms.ChoiceField(
         label=_('Type'),
         label=_('Type'),
         choices=add_blank_choice(CableTypeChoices),
         choices=add_blank_choice(CableTypeChoices),
@@ -900,12 +805,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Cable
     model = Cable
     fieldsets = (
     fieldsets = (
@@ -917,18 +816,12 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
+class VirtualChassisBulkEditForm(PrimaryModelBulkEditForm):
     domain = forms.CharField(
     domain = forms.CharField(
         label=_('Domain'),
         label=_('Domain'),
         max_length=30,
         max_length=30,
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = VirtualChassis
     model = VirtualChassis
     fieldsets = (
     fieldsets = (
@@ -937,7 +830,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('domain', 'description', 'comments')
     nullable_fields = ('domain', 'description', 'comments')
 
 
 
 
-class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
+class PowerPanelBulkEditForm(PrimaryModelBulkEditForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         label=_('Region'),
         label=_('Region'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -971,12 +864,6 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
             'site_id': '$site'
             'site_id': '$site'
         }
         }
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = PowerPanel
     model = PowerPanel
     fieldsets = (
     fieldsets = (
@@ -985,7 +872,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('location', 'description', 'comments')
     nullable_fields = ('location', 'description', 'comments')
 
 
 
 
-class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
+class PowerFeedBulkEditForm(PrimaryModelBulkEditForm):
     power_panel = DynamicModelChoiceField(
     power_panel = DynamicModelChoiceField(
         label=_('Power panel'),
         label=_('Power panel'),
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
@@ -1041,12 +928,6 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     fieldsets = (
@@ -1369,7 +1250,7 @@ class InventoryItemTemplateBulkEditForm(ComponentTemplateBulkEditForm):
 # Device components
 # Device components
 #
 #
 
 
-class ComponentBulkEditForm(NetBoxModelBulkEditForm):
+class ComponentBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     device = forms.ModelChoiceField(
     device = forms.ModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1822,16 +1703,11 @@ class InventoryItemBulkEditForm(
 # Device component roles
 # Device component roles
 #
 #
 
 
-class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
+class InventoryItemRoleBulkEditForm(OrganizationalModelBulkEditForm):
     color = ColorField(
     color = ColorField(
         label=_('Color'),
         label=_('Color'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = InventoryItemRole
     model = InventoryItemRole
     fieldsets = (
     fieldsets = (
@@ -1840,7 +1716,7 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('color', 'description')
     nullable_fields = ('color', 'description')
 
 
 
 
-class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
+class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1856,6 +1732,7 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
+
     model = VirtualDeviceContext
     model = VirtualDeviceContext
     fieldsets = (
     fieldsets = (
         FieldSet('device', 'status', 'tenant'),
         FieldSet('device', 'status', 'tenant'),
@@ -1867,14 +1744,7 @@ class VirtualDeviceContextBulkEditForm(NetBoxModelBulkEditForm):
 # Addressing
 # Addressing
 #
 #
 
 
-class MACAddressBulkEditForm(NetBoxModelBulkEditForm):
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
-
+class MACAddressBulkEditForm(PrimaryModelBulkEditForm):
     model = MACAddress
     model = MACAddress
     fieldsets = (
     fieldsets = (
         FieldSet('description'),
         FieldSet('description'),

+ 77 - 72
netbox/dcim/forms/bulk_import.py

@@ -11,7 +11,10 @@ from dcim.models import *
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import VRF, IPAddress
 from ipam.models import VRF, IPAddress
 from netbox.choices import *
 from netbox.choices import *
-from netbox.forms import NetBoxModelImportForm
+from netbox.forms import (
+    NestedGroupModelImportForm, NetBoxModelImportForm, OrganizationalModelImportForm, OwnerCSVMixin,
+    PrimaryModelImportForm,
+)
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
     CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField,
@@ -58,7 +61,7 @@ __all__ = (
 )
 )
 
 
 
 
-class RegionImportForm(NetBoxModelImportForm):
+class RegionImportForm(NestedGroupModelImportForm):
     parent = CSVModelChoiceField(
     parent = CSVModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -69,10 +72,10 @@ class RegionImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Region
         model = Region
-        fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
+        fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class SiteGroupImportForm(NetBoxModelImportForm):
+class SiteGroupImportForm(NestedGroupModelImportForm):
     parent = CSVModelChoiceField(
     parent = CSVModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
@@ -83,10 +86,10 @@ class SiteGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = SiteGroup
         model = SiteGroup
-        fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
+        fields = ('name', 'slug', 'parent', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class SiteImportForm(NetBoxModelImportForm):
+class SiteImportForm(PrimaryModelImportForm):
     status = CSVChoiceField(
     status = CSVChoiceField(
         label=_('Status'),
         label=_('Status'),
         choices=SiteStatusChoices,
         choices=SiteStatusChoices,
@@ -118,7 +121,7 @@ class SiteImportForm(NetBoxModelImportForm):
         model = Site
         model = Site
         fields = (
         fields = (
             'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
             '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 = {
         help_texts = {
             'time_zone': mark_safe(
             'time_zone': mark_safe(
@@ -129,7 +132,7 @@ class SiteImportForm(NetBoxModelImportForm):
         }
         }
 
 
 
 
-class LocationImportForm(NetBoxModelImportForm):
+class LocationImportForm(NestedGroupModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -162,8 +165,8 @@ class LocationImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = Location
         model = Location
         fields = (
         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):
     def __init__(self, data=None, *args, **kwargs):
@@ -175,15 +178,14 @@ class LocationImportForm(NetBoxModelImportForm):
             self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
             self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
 
 
 
 
-class RackRoleImportForm(NetBoxModelImportForm):
-    slug = SlugField()
+class RackRoleImportForm(OrganizationalModelImportForm):
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
-        fields = ('name', 'slug', 'color', 'description', 'tags')
+        fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
 
 
 
 
-class RackTypeImportForm(NetBoxModelImportForm):
+class RackTypeImportForm(PrimaryModelImportForm):
     manufacturer = forms.ModelChoiceField(
     manufacturer = forms.ModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -224,14 +226,14 @@ class RackTypeImportForm(NetBoxModelImportForm):
         fields = (
         fields = (
             'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
             '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',
             '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):
     def __init__(self, data=None, *args, **kwargs):
         super().__init__(data, *args, **kwargs)
         super().__init__(data, *args, **kwargs)
 
 
 
 
-class RackImportForm(NetBoxModelImportForm):
+class RackImportForm(PrimaryModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -309,7 +311,8 @@ class RackImportForm(NetBoxModelImportForm):
         fields = (
         fields = (
             'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
             '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',
             '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):
     def __init__(self, data=None, *args, **kwargs):
@@ -332,7 +335,7 @@ class RackImportForm(NetBoxModelImportForm):
                 raise forms.ValidationError(_("U height must be set if not specifying a rack type."))
                 raise forms.ValidationError(_("U height must be set if not specifying a rack type."))
 
 
 
 
-class RackReservationImportForm(NetBoxModelImportForm):
+class RackReservationImportForm(PrimaryModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -373,7 +376,7 @@ class RackReservationImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = RackReservation
         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):
     def __init__(self, data=None, *args, **kwargs):
         super().__init__(data, *args, **kwargs)
         super().__init__(data, *args, **kwargs)
@@ -392,14 +395,14 @@ class RackReservationImportForm(NetBoxModelImportForm):
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
 
 
 
 
-class ManufacturerImportForm(NetBoxModelImportForm):
+class ManufacturerImportForm(OrganizationalModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
-        fields = ('name', 'slug', 'description', 'tags')
+        fields = ('name', 'slug', 'description', 'owner', 'tags')
 
 
 
 
-class DeviceTypeImportForm(NetBoxModelImportForm):
+class DeviceTypeImportForm(PrimaryModelImportForm):
     manufacturer = CSVModelChoiceField(
     manufacturer = CSVModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -429,20 +432,21 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
         model = DeviceType
         model = DeviceType
         fields = [
         fields = [
             'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
             '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(PrimaryModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ModuleTypeProfile
         model = ModuleTypeProfile
         fields = [
         fields = [
-            'name', 'description', 'schema', 'comments', 'tags',
+            'name', 'description', 'schema', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ModuleTypeImportForm(NetBoxModelImportForm):
+class ModuleTypeImportForm(PrimaryModelImportForm):
     profile = forms.ModelChoiceField(
     profile = forms.ModelChoiceField(
         label=_('Profile'),
         label=_('Profile'),
         queryset=ModuleTypeProfile.objects.all(),
         queryset=ModuleTypeProfile.objects.all(),
@@ -476,11 +480,11 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
         model = ModuleType
         model = ModuleType
         fields = [
         fields = [
             'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
             'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
-            'comments', 'tags'
+            'owner', 'comments', 'tags'
         ]
         ]
 
 
 
 
-class DeviceRoleImportForm(NetBoxModelImportForm):
+class DeviceRoleImportForm(NestedGroupModelImportForm):
     parent = CSVModelChoiceField(
     parent = CSVModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
@@ -498,17 +502,15 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
         required=False,
         required=False,
         help_text=_('Config template')
         help_text=_('Config template')
     )
     )
-    slug = SlugField()
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
         fields = (
         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(NestedGroupModelImportForm):
     parent = CSVModelChoiceField(
     parent = CSVModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
@@ -537,11 +539,11 @@ class PlatformImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = Platform
         model = Platform
         fields = (
         fields = (
-            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags',
+            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class BaseDeviceImportForm(NetBoxModelImportForm):
+class BaseDeviceImportForm(PrimaryModelImportForm):
     role = CSVModelChoiceField(
     role = CSVModelChoiceField(
         label=_('Device role'),
         label=_('Device role'),
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
@@ -667,8 +669,8 @@ class DeviceImportForm(BaseDeviceImportForm):
         fields = [
         fields = [
             'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
             'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
             'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow',
             '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):
     def __init__(self, data=None, *args, **kwargs):
@@ -715,7 +717,7 @@ class DeviceImportForm(BaseDeviceImportForm):
             self.instance.parent_bay = device_bay
             self.instance.parent_bay = device_bay
 
 
 
 
-class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
+class ModuleImportForm(ModuleCommonForm, PrimaryModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -753,7 +755,7 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = Module
         model = Module
         fields = (
         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',
             'replicate_components', 'adopt_components', 'tags',
         )
         )
 
 
@@ -777,7 +779,7 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
 # Device components
 # Device components
 #
 #
 
 
-class ConsolePortImportForm(NetBoxModelImportForm):
+class ConsolePortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -800,10 +802,10 @@ class ConsolePortImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ConsolePort
         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(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -826,10 +828,10 @@ class ConsoleServerPortImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPort
         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(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -845,11 +847,12 @@ class PowerPortImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = PowerPort
         model = PowerPort
         fields = (
         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(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -879,7 +882,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
         model = PowerOutlet
         model = PowerOutlet
         fields = (
         fields = (
             'device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description',
             'device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description',
-            'tags',
+            'owner', 'tags',
         )
         )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -905,7 +908,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
             self.fields['power_port'].queryset = PowerPort.objects.none()
             self.fields['power_port'].queryset = PowerPort.objects.none()
 
 
 
 
-class InterfaceImportForm(NetBoxModelImportForm):
+class InterfaceImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -988,7 +991,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
         fields = (
         fields = (
             'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
             'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
             'mark_connected', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
             '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):
     def __init__(self, data=None, *args, **kwargs):
@@ -1023,7 +1026,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
         return self.cleaned_data['vdcs']
         return self.cleaned_data['vdcs']
 
 
 
 
-class FrontPortImportForm(NetBoxModelImportForm):
+class FrontPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1045,7 +1048,7 @@ class FrontPortImportForm(NetBoxModelImportForm):
         model = FrontPort
         model = FrontPort
         fields = (
         fields = (
             'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
             'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
-            'description', 'tags'
+            'description', 'owner', 'tags'
         )
         )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -1071,7 +1074,7 @@ class FrontPortImportForm(NetBoxModelImportForm):
             self.fields['rear_port'].queryset = RearPort.objects.none()
             self.fields['rear_port'].queryset = RearPort.objects.none()
 
 
 
 
-class RearPortImportForm(NetBoxModelImportForm):
+class RearPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1085,10 +1088,12 @@ class RearPortImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = RearPort
         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(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1097,10 +1102,10 @@ class ModuleBayImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ModuleBay
         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(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1119,7 +1124,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
-        fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
+        fields = ('device', 'name', 'label', 'installed_device', 'description', 'owner', 'tags')
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -1148,7 +1153,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
             self.fields['installed_device'].queryset = Device.objects.none()
             self.fields['installed_device'].queryset = Device.objects.none()
 
 
 
 
-class InventoryItemImportForm(NetBoxModelImportForm):
+class InventoryItemImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1195,7 +1200,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
         model = InventoryItem
         model = InventoryItem
         fields = (
         fields = (
             'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag',
             '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):
     def __init__(self, *args, **kwargs):
@@ -1258,7 +1263,7 @@ class InventoryItemImportForm(NetBoxModelImportForm):
 # Device component roles
 # Device component roles
 #
 #
 
 
-class InventoryItemRoleImportForm(NetBoxModelImportForm):
+class InventoryItemRoleImportForm(OrganizationalModelImportForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
@@ -1270,7 +1275,7 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
 # Addressing
 # Addressing
 #
 #
 
 
-class MACAddressImportForm(NetBoxModelImportForm):
+class MACAddressImportForm(PrimaryModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1301,7 +1306,8 @@ class MACAddressImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = MACAddress
         model = MACAddress
         fields = [
         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):
     def __init__(self, data=None, *args, **kwargs):
@@ -1354,7 +1360,7 @@ class MACAddressImportForm(NetBoxModelImportForm):
 # Cables
 # Cables
 #
 #
 
 
-class CableImportForm(NetBoxModelImportForm):
+class CableImportForm(PrimaryModelImportForm):
     # Termination A
     # Termination A
     side_a_site = CSVModelChoiceField(
     side_a_site = CSVModelChoiceField(
         label=_('Side A site'),
         label=_('Side A site'),
@@ -1443,7 +1449,7 @@ class CableImportForm(NetBoxModelImportForm):
         fields = [
         fields = [
             'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
             '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',
             'side_b_name', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
-            'comments', 'tags',
+            'owner', 'comments', 'tags',
         ]
         ]
 
 
     def __init__(self, data=None, *args, **kwargs):
     def __init__(self, data=None, *args, **kwargs):
@@ -1537,7 +1543,7 @@ class CableImportForm(NetBoxModelImportForm):
 #
 #
 
 
 
 
-class VirtualChassisImportForm(NetBoxModelImportForm):
+class VirtualChassisImportForm(PrimaryModelImportForm):
     master = CSVModelChoiceField(
     master = CSVModelChoiceField(
         label=_('Master'),
         label=_('Master'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1548,14 +1554,14 @@ class VirtualChassisImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
-        fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
+        fields = ('name', 'domain', 'master', 'description', 'owner', 'comments', 'tags')
 
 
 
 
 #
 #
 # Power
 # Power
 #
 #
 
 
-class PowerPanelImportForm(NetBoxModelImportForm):
+class PowerPanelImportForm(PrimaryModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -1571,7 +1577,7 @@ class PowerPanelImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
-        fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
+        fields = ('site', 'location', 'name', 'description', 'owner', 'comments', 'tags')
 
 
     def __init__(self, data=None, *args, **kwargs):
     def __init__(self, data=None, *args, **kwargs):
         super().__init__(data, *args, **kwargs)
         super().__init__(data, *args, **kwargs)
@@ -1583,7 +1589,7 @@ class PowerPanelImportForm(NetBoxModelImportForm):
             self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
             self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
 
 
 
-class PowerFeedImportForm(NetBoxModelImportForm):
+class PowerFeedImportForm(PrimaryModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -1641,7 +1647,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
         model = PowerFeed
         model = PowerFeed
         fields = (
         fields = (
             'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
             '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):
     def __init__(self, data=None, *args, **kwargs):
@@ -1665,8 +1671,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
 
 
 
 
-class VirtualDeviceContextImportForm(NetBoxModelImportForm):
-
+class VirtualDeviceContextImportForm(PrimaryModelImportForm):
     device = CSVModelChoiceField(
     device = CSVModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1701,7 +1706,7 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         fields = [
         fields = [
-            'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
+            'name', 'device', 'status', 'tenant', 'identifier', 'owner', 'comments', 'primary_ip4', 'primary_ip6',
         ]
         ]
         model = VirtualDeviceContext
         model = VirtualDeviceContext
 
 

+ 78 - 57
netbox/dcim/forms/filtersets.py

@@ -8,11 +8,14 @@ from extras.forms import LocalConfigContextFilterForm
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import ASN, VRF, VLANTranslationPolicy
 from ipam.models import ASN, VRF, VLANTranslationPolicy
 from netbox.choices import *
 from netbox.choices import *
-from netbox.forms import NetBoxModelFilterSetForm
+from netbox.forms import (
+    NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm,
+    PrimaryModelFilterSetForm,
+)
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 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 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.rendering import FieldSet
 from utilities.forms.widgets import NumberWithOptions
 from utilities.forms.widgets import NumberWithOptions
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine
@@ -137,12 +140,18 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
         required=False,
         required=False,
         label=_('Device Status'),
         label=_('Device Status'),
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
-class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
     model = Region
     model = Region
     fieldsets = (
     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'))
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     parent_id = DynamicModelMultipleChoiceField(
     parent_id = DynamicModelMultipleChoiceField(
@@ -153,10 +162,11 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm):
     model = SiteGroup
     model = SiteGroup
     fieldsets = (
     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'))
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     parent_id = DynamicModelMultipleChoiceField(
     parent_id = DynamicModelMultipleChoiceField(
@@ -167,10 +177,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
+class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = Site
     model = Site
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
         FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -199,10 +209,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
+class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm):
     model = Location
     model = Location
     fieldsets = (
     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('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -247,12 +257,15 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class RackRoleFilterForm(NetBoxModelFilterSetForm):
+class RackRoleFilterForm(OrganizationalModelFilterSetForm):
     model = RackRole
     model = RackRole
+    fieldsets = (
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class RackBaseFilterForm(NetBoxModelFilterSetForm):
+class RackBaseFilterForm(PrimaryModelFilterSetForm):
     form_factor = forms.MultipleChoiceField(
     form_factor = forms.MultipleChoiceField(
         label=_('Form factor'),
         label=_('Form factor'),
         choices=RackFormFactorChoices,
         choices=RackFormFactorChoices,
@@ -303,7 +316,7 @@ class RackBaseFilterForm(NetBoxModelFilterSetForm):
 class RackTypeFilterForm(RackBaseFilterForm):
 class RackTypeFilterForm(RackBaseFilterForm):
     model = RackType
     model = RackType
     fieldsets = (
     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('manufacturer_id', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
         FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
         FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
         FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
         FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
@@ -320,7 +333,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
 class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
 class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
     model = Rack
     model = Rack
     fieldsets = (
     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('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
         FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
@@ -413,10 +426,10 @@ class RackElevationFilterForm(RackFilterForm):
     )
     )
 
 
 
 
-class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = RackReservation
     model = RackReservation
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('status', 'user_id', name=_('Reservation')),
         FieldSet('status', 'user_id', name=_('Reservation')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
@@ -471,19 +484,19 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm):
     model = Manufacturer
     model = Manufacturer
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts'))
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
+class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
     model = DeviceType
     model = DeviceType
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet(
         FieldSet(
             'manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow', name=_('Hardware')
             'manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow', name=_('Hardware')
         ),
         ),
@@ -608,18 +621,18 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
     )
     )
 
 
 
 
-class ModuleTypeProfileFilterForm(NetBoxModelFilterSetForm):
+class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm):
     model = ModuleTypeProfile
     model = ModuleTypeProfile
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
     )
     )
     selector_fields = ('filter_id', 'q')
     selector_fields = ('filter_id', 'q')
 
 
 
 
-class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
+class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
     model = ModuleType
     model = ModuleType
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
         FieldSet('profile_id', 'manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
         FieldSet(
         FieldSet(
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
@@ -701,8 +714,12 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
     )
     )
 
 
 
 
-class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
+class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
     model = DeviceRole
     model = DeviceRole
+    fieldsets = (
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
+        FieldSet('parent_id', 'config_template_id', name=_('Device Role'))
+    )
     config_template_id = DynamicModelMultipleChoiceField(
     config_template_id = DynamicModelMultipleChoiceField(
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         required=False,
         required=False,
@@ -716,8 +733,12 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class PlatformFilterForm(NetBoxModelFilterSetForm):
+class PlatformFilterForm(NestedGroupModelFilterSetForm):
     model = Platform
     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')
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
     parent_id = DynamicModelMultipleChoiceField(
     parent_id = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
@@ -741,11 +762,11 @@ class DeviceFilterForm(
     LocalConfigContextFilterForm,
     LocalConfigContextFilterForm,
     TenancyFilterForm,
     TenancyFilterForm,
     ContactModelFilterForm,
     ContactModelFilterForm,
-    NetBoxModelFilterSetForm
+    PrimaryModelFilterSetForm
 ):
 ):
     model = Device
     model = Device
     fieldsets = (
     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('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('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
         FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
         FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
@@ -935,13 +956,10 @@ class DeviceFilterForm(
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VirtualDeviceContextFilterForm(
-    TenancyFilterForm,
-    NetBoxModelFilterSetForm
-):
+class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = VirtualDeviceContext
     model = VirtualDeviceContext
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
         FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -965,10 +983,10 @@ class VirtualDeviceContextFilterForm(
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
+class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
     model = Module
     model = Module
     fieldsets = (
     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('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')),
         FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
     )
     )
@@ -1048,10 +1066,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = VirtualChassis
     model = VirtualChassis
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -1077,10 +1095,10 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = Cable
     model = Cable
     fieldsets = (
     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('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
         FieldSet('type', 'status', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
         FieldSet('type', 'status', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
@@ -1161,10 +1179,10 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
+class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
     model = PowerPanel
     model = PowerPanel
     fieldsets = (
     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('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
     )
     )
@@ -1200,10 +1218,10 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = PowerFeed
     model = PowerFeed
     fieldsets = (
     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('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
         FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')),
@@ -1313,7 +1331,7 @@ class PathEndpointFilterForm(CabledFilterForm):
 class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = ConsolePort
     model = ConsolePort
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
         FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1337,7 +1355,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = ConsoleServerPort
     model = ConsoleServerPort
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
         FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1362,7 +1380,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
 class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerPort
     model = PowerPort
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', name=_('Attributes')),
         FieldSet('name', 'label', 'type', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1381,7 +1399,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerOutlet
     model = PowerOutlet
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
         FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1410,7 +1428,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = Interface
     model = Interface
     fieldsets = (
     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('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')),
         FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
         FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')),
         FieldSet('poe_mode', 'poe_type', name=_('PoE')),
         FieldSet('poe_mode', 'poe_type', name=_('PoE')),
@@ -1535,7 +1553,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 
 
 class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
         FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1559,7 +1577,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     model = RearPort
     model = RearPort
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
         FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1583,7 +1601,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
 class ModuleBayFilterForm(DeviceComponentFilterForm):
 class ModuleBayFilterForm(DeviceComponentFilterForm):
     model = ModuleBay
     model = ModuleBay
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', 'position', name=_('Attributes')),
         FieldSet('name', 'label', 'position', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1601,7 +1619,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
     model = DeviceBay
     model = DeviceBay
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'label', name=_('Attributes')),
         FieldSet('name', 'label', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
@@ -1615,7 +1633,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
 class InventoryItemFilterForm(DeviceComponentFilterForm):
 class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
     model = InventoryItem
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet(
         FieldSet(
             'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
             'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered',
             name=_('Attributes')
             name=_('Attributes')
@@ -1663,8 +1681,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
 # Device component roles
 # Device component roles
 #
 #
 
 
-class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm):
+class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm):
     model = InventoryItemRole
     model = InventoryItemRole
+    fieldsets = (
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
@@ -1672,10 +1693,10 @@ class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm):
 # Addressing
 # Addressing
 #
 #
 
 
-class MACAddressFilterForm(NetBoxModelFilterSetForm):
+class MACAddressFilterForm(PrimaryModelFilterSetForm):
     model = MACAddress
     model = MACAddress
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
         FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
     )
     )
     selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
     selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')

+ 62 - 92
netbox/dcim/forms/model_forms.py

@@ -10,13 +10,13 @@ from dcim.models import *
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.choices import VLANQinQRoleChoices
 from ipam.choices import VLANQinQRoleChoices
 from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
 from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
-from netbox.forms import NetBoxModelForm
-from netbox.forms.mixins import ChangelogMessageMixin
+from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
+from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from users.models import User
 from users.models import User
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms.fields import (
 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.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
 from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
@@ -75,14 +75,12 @@ __all__ = (
 )
 )
 
 
 
 
-class RegionForm(NetBoxModelForm):
+class RegionForm(NestedGroupModelForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         required=False
         required=False
     )
     )
-    slug = SlugField()
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -91,18 +89,16 @@ class RegionForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Region
         model = Region
         fields = (
         fields = (
-            'parent', 'name', 'slug', 'description', 'tags', 'comments',
+            'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments',
         )
         )
 
 
 
 
-class SiteGroupForm(NetBoxModelForm):
+class SiteGroupForm(NestedGroupModelForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
         required=False
         required=False
     )
     )
-    slug = SlugField()
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -111,11 +107,11 @@ class SiteGroupForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = SiteGroup
         model = SiteGroup
         fields = (
         fields = (
-            'parent', 'name', 'slug', 'description', 'comments', 'tags',
+            'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class SiteForm(TenancyForm, NetBoxModelForm):
+class SiteForm(TenancyForm, PrimaryModelForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         label=_('Region'),
         label=_('Region'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -139,7 +135,6 @@ class SiteForm(TenancyForm, NetBoxModelForm):
         choices=add_blank_choice(TimeZoneFormField().choices),
         choices=add_blank_choice(TimeZoneFormField().choices),
         required=False
         required=False
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -154,7 +149,7 @@ class SiteForm(TenancyForm, NetBoxModelForm):
         model = Site
         model = Site
         fields = (
         fields = (
             'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
             '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 = {
         widgets = {
             'physical_address': forms.Textarea(
             'physical_address': forms.Textarea(
@@ -170,7 +165,7 @@ class SiteForm(TenancyForm, NetBoxModelForm):
         }
         }
 
 
 
 
-class LocationForm(TenancyForm, NetBoxModelForm):
+class LocationForm(TenancyForm, NestedGroupModelForm):
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -184,8 +179,6 @@ class LocationForm(TenancyForm, NetBoxModelForm):
             'site_id': '$site'
             'site_id': '$site'
         }
         }
     )
     )
-    slug = SlugField()
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
         FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
@@ -195,14 +188,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Location
         model = Location
         fields = (
         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',
         )
         )
 
 
 
 
-class RackRoleForm(NetBoxModelForm):
-    slug = SlugField()
-
+class RackRoleForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
         FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Rack Role')),
     )
     )
@@ -210,17 +201,16 @@ class RackRoleForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
         fields = [
         fields = [
-            'name', 'slug', 'color', 'description', 'tags',
+            'name', 'slug', 'color', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class RackTypeForm(NetBoxModelForm):
+class RackTypeForm(PrimaryModelForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
     slug = SlugField(
     slug = SlugField(
         label=_('Slug'),
         label=_('Slug'),
         slug_source='model'
         slug_source='model'
@@ -242,11 +232,11 @@ class RackTypeForm(NetBoxModelForm):
         fields = [
         fields = [
             'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
             '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',
             'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
-            'weight_unit', 'description', 'comments', 'tags',
+            'weight_unit', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class RackForm(TenancyForm, NetBoxModelForm):
+class RackForm(TenancyForm, PrimaryModelForm):
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -271,7 +261,6 @@ class RackForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         help_text=_("Select a pre-defined rack type, or set physical characteristics below.")
         help_text=_("Select a pre-defined rack type, or set physical characteristics below.")
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -288,7 +277,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
             'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
             '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',
             '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',
             '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):
     def __init__(self, *args, **kwargs):
@@ -318,7 +307,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
             )
             )
 
 
 
 
-class RackReservationForm(TenancyForm, NetBoxModelForm):
+class RackReservationForm(TenancyForm, PrimaryModelForm):
     rack = DynamicModelChoiceField(
     rack = DynamicModelChoiceField(
         label=_('Rack'),
         label=_('Rack'),
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
@@ -333,7 +322,6 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
         label=_('User'),
         label=_('User'),
         queryset=User.objects.order_by('username')
         queryset=User.objects.order_by('username')
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
         FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
@@ -343,13 +331,11 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = RackReservation
         model = RackReservation
         fields = [
         fields = [
-            'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
+            'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ManufacturerForm(NetBoxModelForm):
-    slug = SlugField()
-
+class ManufacturerForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
         FieldSet('name', 'slug', 'description', 'tags', name=_('Manufacturer')),
     )
     )
@@ -357,11 +343,11 @@ class ManufacturerForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
         fields = [
         fields = [
-            'name', 'slug', 'description', 'tags',
+            'name', 'slug', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class DeviceTypeForm(NetBoxModelForm):
+class DeviceTypeForm(PrimaryModelForm):
     manufacturer = DynamicModelChoiceField(
     manufacturer = DynamicModelChoiceField(
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -380,7 +366,6 @@ class DeviceTypeForm(NetBoxModelForm):
         label=_('Slug'),
         label=_('Slug'),
         slug_source='model'
         slug_source='model'
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
         FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
@@ -396,7 +381,7 @@ class DeviceTypeForm(NetBoxModelForm):
         fields = [
         fields = [
             'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
             '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',
             'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
-            'description', 'comments', 'tags',
+            'description', 'owner', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'front_image': ClearableFileInput(attrs={
             'front_image': ClearableFileInput(attrs={
@@ -408,13 +393,12 @@ class DeviceTypeForm(NetBoxModelForm):
         }
         }
 
 
 
 
-class ModuleTypeProfileForm(NetBoxModelForm):
+class ModuleTypeProfileForm(PrimaryModelForm):
     schema = JSONField(
     schema = JSONField(
         label=_('Schema'),
         label=_('Schema'),
         required=False,
         required=False,
         help_text=_("Enter a valid JSON schema to define supported attributes.")
         help_text=_("Enter a valid JSON schema to define supported attributes.")
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
         FieldSet('name', 'description', 'schema', 'tags', name=_('Profile')),
@@ -423,11 +407,11 @@ class ModuleTypeProfileForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = ModuleTypeProfile
         model = ModuleTypeProfile
         fields = [
         fields = [
-            'name', 'description', 'schema', 'comments', 'tags',
+            'name', 'description', 'schema', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ModuleTypeForm(NetBoxModelForm):
+class ModuleTypeForm(PrimaryModelForm):
     profile = forms.ModelChoiceField(
     profile = forms.ModelChoiceField(
         queryset=ModuleTypeProfile.objects.all(),
         queryset=ModuleTypeProfile.objects.all(),
         label=_('Profile'),
         label=_('Profile'),
@@ -438,7 +422,6 @@ class ModuleTypeForm(NetBoxModelForm):
         label=_('Manufacturer'),
         label=_('Manufacturer'),
         queryset=Manufacturer.objects.all()
         queryset=Manufacturer.objects.all()
     )
     )
-    comments = CommentField()
 
 
     @property
     @property
     def fieldsets(self):
     def fieldsets(self):
@@ -452,7 +435,7 @@ class ModuleTypeForm(NetBoxModelForm):
         model = ModuleType
         model = ModuleType
         fields = [
         fields = [
             'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
             'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
-            'comments', 'tags',
+            'owner', 'comments', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -507,19 +490,17 @@ class ModuleTypeForm(NetBoxModelForm):
         return super()._post_clean()
         return super()._post_clean()
 
 
 
 
-class DeviceRoleForm(NetBoxModelForm):
+class DeviceRoleForm(NestedGroupModelForm):
     config_template = DynamicModelChoiceField(
     config_template = DynamicModelChoiceField(
         label=_('Config template'),
         label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         required=False
         required=False
     )
     )
-    slug = SlugField()
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
         required=False,
         required=False,
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -531,11 +512,11 @@ class DeviceRoleForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
         fields = [
         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 PlatformForm(NetBoxModelForm):
+class PlatformForm(NestedGroupModelForm):
     parent = DynamicModelChoiceField(
     parent = DynamicModelChoiceField(
         label=_('Parent'),
         label=_('Parent'),
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
@@ -556,7 +537,6 @@ class PlatformForm(NetBoxModelForm):
         label=_('Slug'),
         label=_('Slug'),
         max_length=64
         max_length=64
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -567,11 +547,11 @@ class PlatformForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Platform
         model = Platform
         fields = [
         fields = [
-            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'comments', 'tags',
+            'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class DeviceForm(TenancyForm, NetBoxModelForm):
+class DeviceForm(TenancyForm, PrimaryModelForm):
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -641,7 +621,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             'site_id': ['$site', 'null']
             'site_id': ['$site', 'null']
         },
         },
     )
     )
-    comments = CommentField()
     local_context_data = JSONField(
     local_context_data = JSONField(
         required=False,
         required=False,
         label=''
         label=''
@@ -677,7 +656,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
             'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
             'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
             'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
             'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
             '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):
     def __init__(self, *args, **kwargs):
@@ -742,7 +721,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             self.fields['position'].widget.choices = [(position, f'U{position}')]
             self.fields['position'].widget.choices = [(position, f'U{position}')]
 
 
 
 
-class ModuleForm(ModuleCommonForm, NetBoxModelForm):
+class ModuleForm(ModuleCommonForm, PrimaryModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -765,7 +744,6 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
         },
         },
         selector=True
         selector=True
     )
     )
-    comments = CommentField()
     replicate_components = forms.BooleanField(
     replicate_components = forms.BooleanField(
         label=_('Replicate components'),
         label=_('Replicate components'),
         required=False,
         required=False,
@@ -788,7 +766,7 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
         model = Module
         model = Module
         fields = [
         fields = [
             'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
             '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):
     def __init__(self, *args, **kwargs):
@@ -809,7 +787,7 @@ def get_termination_type_choices():
     ])
     ])
 
 
 
 
-class CableForm(TenancyForm, NetBoxModelForm):
+class CableForm(TenancyForm, PrimaryModelForm):
     a_terminations_type = forms.ChoiceField(
     a_terminations_type = forms.ChoiceField(
         choices=get_termination_type_choices,
         choices=get_termination_type_choices,
         required=False,
         required=False,
@@ -822,17 +800,16 @@ class CableForm(TenancyForm, NetBoxModelForm):
         widget=HTMXSelect(),
         widget=HTMXSelect(),
         label=_('Type')
         label=_('Type')
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = Cable
         model = Cable
         fields = [
         fields = [
             'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
             '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',
         ]
         ]
 
 
 
 
-class PowerPanelForm(NetBoxModelForm):
+class PowerPanelForm(PrimaryModelForm):
     site = DynamicModelChoiceField(
     site = DynamicModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -846,7 +823,6 @@ class PowerPanelForm(NetBoxModelForm):
             'site_id': '$site'
             'site_id': '$site'
         }
         }
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
         FieldSet('site', 'location', 'name', 'description', 'tags', name=_('Power Panel')),
@@ -855,11 +831,11 @@ class PowerPanelForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel
         fields = [
         fields = [
-            'site', 'location', 'name', 'description', 'comments', 'tags',
+            'site', 'location', 'name', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class PowerFeedForm(TenancyForm, NetBoxModelForm):
+class PowerFeedForm(TenancyForm, PrimaryModelForm):
     power_panel = DynamicModelChoiceField(
     power_panel = DynamicModelChoiceField(
         label=_('Power panel'),
         label=_('Power panel'),
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
@@ -872,7 +848,6 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         selector=True
         selector=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -887,7 +862,7 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
         model = PowerFeed
         model = PowerFeed
         fields = [
         fields = [
             'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
             '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'
         ]
         ]
 
 
 
 
@@ -895,18 +870,17 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisForm(NetBoxModelForm):
+class VirtualChassisForm(PrimaryModelForm):
     master = forms.ModelChoiceField(
     master = forms.ModelChoiceField(
         label=_('Master'),
         label=_('Master'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         required=False,
         required=False,
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
         fields = [
         fields = [
-            'name', 'domain', 'master', 'description', 'comments', 'tags',
+            'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'master': SelectWithPK(),
             'master': SelectWithPK(),
@@ -1360,7 +1334,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
 # Device components
 # Device components
 #
 #
 
 
-class DeviceComponentForm(NetBoxModelForm):
+class DeviceComponentForm(OwnerMixin, NetBoxModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1396,7 +1370,7 @@ class ConsolePortForm(ModularDeviceComponentForm):
     class Meta:
     class Meta:
         model = ConsolePort
         model = ConsolePort
         fields = [
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1410,7 +1384,7 @@ class ConsoleServerPortForm(ModularDeviceComponentForm):
     class Meta:
     class Meta:
         model = ConsoleServerPort
         model = ConsoleServerPort
         fields = [
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1426,7 +1400,7 @@ class PowerPortForm(ModularDeviceComponentForm):
         model = PowerPort
         model = PowerPort
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
-            'description', 'tags',
+            'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1443,7 +1417,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
             'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected',
-            'description', 'tags',
+            'description', 'owner', 'tags',
         ),
         ),
     )
     )
 
 
@@ -1587,7 +1561,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
             'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
             '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',
             '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',
             'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address',
-            'tags',
+            'owner', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'speed': NumberWithOptions(
             'speed': NumberWithOptions(
@@ -1619,7 +1593,7 @@ class FrontPortForm(ModularDeviceComponentForm):
         model = FrontPort
         model = FrontPort
         fields = [
         fields = [
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
             'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
-            'description', 'tags',
+            'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1633,7 +1607,8 @@ class RearPortForm(ModularDeviceComponentForm):
     class Meta:
     class Meta:
         model = RearPort
         model = RearPort
         fields = [
         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 +1620,7 @@ class ModuleBayForm(ModularDeviceComponentForm):
     class Meta:
     class Meta:
         model = ModuleBay
         model = ModuleBay
         fields = [
         fields = [
-            'device', 'module', 'name', 'label', 'position', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'position', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1657,7 +1632,7 @@ class DeviceBayForm(DeviceComponentForm):
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = [
         fields = [
-            'device', 'name', 'label', 'description', 'tags',
+            'device', 'name', 'label', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1782,7 +1757,7 @@ class InventoryItemForm(DeviceComponentForm):
         model = InventoryItem
         model = InventoryItem
         fields = [
         fields = [
             'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
             'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
-            'status', 'description', 'tags',
+            'status', 'description', 'owner', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -1828,12 +1803,7 @@ class InventoryItemForm(DeviceComponentForm):
             self.instance.component = None
             self.instance.component = None
 
 
 
 
-# Device component roles
-#
-
-class InventoryItemRoleForm(NetBoxModelForm):
-    slug = SlugField()
-
+class InventoryItemRoleForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
         FieldSet('name', 'slug', 'color', 'description', 'tags', name=_('Inventory Item Role')),
     )
     )
@@ -1841,11 +1811,11 @@ class InventoryItemRoleForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = InventoryItemRole
         model = InventoryItemRole
         fields = [
         fields = [
-            'name', 'slug', 'color', 'description', 'tags',
+            'name', 'slug', 'color', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
+class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm):
     device = DynamicModelChoiceField(
     device = DynamicModelChoiceField(
         label=_('Device'),
         label=_('Device'),
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -1881,7 +1851,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = VirtualDeviceContext
         model = VirtualDeviceContext
         fields = [
         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'
             'comments', 'tags'
         ]
         ]
 
 
@@ -1890,7 +1860,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
 # Addressing
 # Addressing
 #
 #
 
 
-class MACAddressForm(NetBoxModelForm):
+class MACAddressForm(PrimaryModelForm):
     mac_address = forms.CharField(
     mac_address = forms.CharField(
         required=True,
         required=True,
         label=_('MAC address')
         label=_('MAC address')
@@ -1929,7 +1899,7 @@ class MACAddressForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = MACAddress
         model = MACAddress
         fields = [
         fields = [
-            'mac_address', 'interface', 'vminterface', 'description', 'tags',
+            'mac_address', 'interface', 'vminterface', 'description', 'owner', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 2 - 2
netbox/dcim/forms/object_create.py

@@ -434,8 +434,8 @@ class VirtualChassisCreateForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
         fields = [
         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):
     def clean(self):

+ 26 - 34
netbox/dcim/graphql/types.py

@@ -5,16 +5,13 @@ import strawberry_django
 
 
 from core.graphql.mixins import ChangelogMixin
 from core.graphql.mixins import ChangelogMixin
 from dcim import models
 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 ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
 from netbox.graphql.scalars import BigInt
 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 .filters import *
 from .mixins import CabledObjectMixin, PathEndpointMixin
 from .mixins import CabledObjectMixin, PathEndpointMixin
 
 
@@ -91,12 +88,7 @@ __all__ = (
 
 
 
 
 @strawberry.type
 @strawberry.type
-class ComponentType(
-    ChangelogMixin,
-    CustomFieldsMixin,
-    TagsMixin,
-    BaseObjectType
-):
+class ComponentType(OwnerMixin, NetBoxObjectType):
     """
     """
     Base type for device/VM components
     Base type for device/VM components
     """
     """
@@ -159,7 +151,7 @@ class CableTerminationType(NetBoxObjectType):
     filters=CableFilter,
     filters=CableFilter,
     pagination=True
     pagination=True
 )
 )
-class CableType(NetBoxObjectType):
+class CableType(PrimaryObjectType):
     color: str
     color: str
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
 
 
@@ -236,7 +228,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
     filters=DeviceFilter,
     filters=DeviceFilter,
     pagination=True
     pagination=True
 )
 )
-class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
+class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType):
     console_port_count: BigInt
     console_port_count: BigInt
     console_server_port_count: BigInt
     console_server_port_count: BigInt
     power_port_count: BigInt
     power_port_count: BigInt
@@ -339,7 +331,7 @@ class InventoryItemTemplateType(ComponentTemplateType):
     filters=DeviceRoleFilter,
     filters=DeviceRoleFilter,
     pagination=True
     pagination=True
 )
 )
-class DeviceRoleType(OrganizationalObjectType):
+class DeviceRoleType(NestedGroupObjectType):
     parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None
     parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None
     children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]]
     color: str
     color: str
@@ -355,7 +347,7 @@ class DeviceRoleType(OrganizationalObjectType):
     filters=DeviceTypeFilter,
     filters=DeviceTypeFilter,
     pagination=True
     pagination=True
 )
 )
-class DeviceTypeType(NetBoxObjectType):
+class DeviceTypeType(PrimaryObjectType):
     console_port_template_count: BigInt
     console_port_template_count: BigInt
     console_server_port_template_count: BigInt
     console_server_port_template_count: BigInt
     power_port_template_count: BigInt
     power_port_template_count: BigInt
@@ -412,7 +404,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
     filters=MACAddressFilter,
     filters=MACAddressFilter,
     pagination=True
     pagination=True
 )
 )
-class MACAddressType(NetBoxObjectType):
+class MACAddressType(PrimaryObjectType):
     mac_address: str
     mac_address: str
 
 
     @strawberry_django.field
     @strawberry_django.field
@@ -512,7 +504,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
     filters=LocationFilter,
     filters=LocationFilter,
     pagination=True
     pagination=True
 )
 )
-class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
+class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NestedGroupObjectType):
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     parent: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     parent: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
@@ -555,7 +547,7 @@ class ManufacturerType(OrganizationalObjectType, ContactsMixin):
     filters=ModuleFilter,
     filters=ModuleFilter,
     pagination=True
     pagination=True
 )
 )
-class ModuleType(NetBoxObjectType):
+class ModuleType(PrimaryObjectType):
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
     module_bay: Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]
     module_bay: Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]
     module_type: Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]
     module_type: Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]
@@ -602,7 +594,7 @@ class ModuleBayTemplateType(ModularComponentTemplateType):
     filters=ModuleTypeProfileFilter,
     filters=ModuleTypeProfileFilter,
     pagination=True
     pagination=True
 )
 )
-class ModuleTypeProfileType(NetBoxObjectType):
+class ModuleTypeProfileType(PrimaryObjectType):
     module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
     module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
 
 
 
 
@@ -612,7 +604,7 @@ class ModuleTypeProfileType(NetBoxObjectType):
     filters=ModuleTypeFilter,
     filters=ModuleTypeFilter,
     pagination=True
     pagination=True
 )
 )
-class ModuleTypeType(NetBoxObjectType):
+class ModuleTypeType(PrimaryObjectType):
     profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
     profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
 
 
@@ -632,7 +624,7 @@ class ModuleTypeType(NetBoxObjectType):
     filters=PlatformFilter,
     filters=PlatformFilter,
     pagination=True
     pagination=True
 )
 )
-class PlatformType(OrganizationalObjectType):
+class PlatformType(NestedGroupObjectType):
     parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None
     parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None
     children: List[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]]
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
@@ -648,7 +640,7 @@ class PlatformType(OrganizationalObjectType):
     filters=PowerFeedFilter,
     filters=PowerFeedFilter,
     pagination=True
     pagination=True
 )
 )
-class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
+class PowerFeedType(CabledObjectMixin, PathEndpointMixin, PrimaryObjectType):
     power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]
     power_panel: Annotated["PowerPanelType", strawberry.lazy('dcim.graphql.types')]
     rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] | None
     rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -682,7 +674,7 @@ class PowerOutletTemplateType(ModularComponentTemplateType):
     filters=PowerPanelFilter,
     filters=PowerPanelFilter,
     pagination=True
     pagination=True
 )
 )
-class PowerPanelType(NetBoxObjectType, ContactsMixin):
+class PowerPanelType(ContactsMixin, PrimaryObjectType):
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
 
 
@@ -716,7 +708,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
     filters=RackTypeFilter,
     filters=RackTypeFilter,
     pagination=True
     pagination=True
 )
 )
-class RackTypeType(NetBoxObjectType):
+class RackTypeType(PrimaryObjectType):
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
     manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
 
 
 
 
@@ -726,7 +718,7 @@ class RackTypeType(NetBoxObjectType):
     filters=RackFilter,
     filters=RackFilter,
     pagination=True
     pagination=True
 )
 )
-class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
+class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType):
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -745,7 +737,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
     filters=RackReservationFilter,
     filters=RackReservationFilter,
     pagination=True
     pagination=True
 )
 )
-class RackReservationType(NetBoxObjectType):
+class RackReservationType(PrimaryObjectType):
     units: List[int]
     units: List[int]
     rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')]
     rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')]
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -794,7 +786,7 @@ class RearPortTemplateType(ModularComponentTemplateType):
     filters=RegionFilter,
     filters=RegionFilter,
     pagination=True
     pagination=True
 )
 )
-class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
+class RegionType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType):
 
 
     sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
     sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]
@@ -820,7 +812,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
     filters=SiteFilter,
     filters=SiteFilter,
     pagination=True
     pagination=True
 )
 )
-class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
+class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObjectType):
     time_zone: str | None
     time_zone: str | None
     region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
     region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
     group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
     group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
@@ -855,7 +847,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
     filters=SiteGroupFilter,
     filters=SiteGroupFilter,
     pagination=True
     pagination=True
 )
 )
-class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
+class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType):
 
 
     sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
     sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]
     children: List[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]
@@ -881,7 +873,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
     filters=VirtualChassisFilter,
     filters=VirtualChassisFilter,
     pagination=True
     pagination=True
 )
 )
-class VirtualChassisType(NetBoxObjectType):
+class VirtualChassisType(PrimaryObjectType):
     member_count: BigInt
     member_count: BigInt
     master: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
     master: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
 
 
@@ -894,7 +886,7 @@ class VirtualChassisType(NetBoxObjectType):
     filters=VirtualDeviceContextFilter,
     filters=VirtualDeviceContextFilter,
     pagination=True
     pagination=True
 )
 )
-class VirtualDeviceContextType(NetBoxObjectType):
+class VirtualDeviceContextType(PrimaryObjectType):
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
     device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None
     primary_ip4: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
     primary_ip4: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
     primary_ip6: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
     primary_ip6: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None

+ 243 - 0
netbox/dcim/migrations/0217_owner.py

@@ -0,0 +1,243 @@
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0216_poweroutlettemplate_color'),
+        ('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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleport',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='device',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='devicerole',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='location',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='manufacturer',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='modulebay',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='moduletypeprofile',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='powerfeed',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='powerpanel',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rack',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rackrole',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rearport',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='site',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='virtualchassis',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+    ]

+ 2 - 1
netbox/dcim/models/device_components.py

@@ -14,6 +14,7 @@ from dcim.fields import WWNField
 from dcim.models.mixins import InterfaceValidationMixin
 from dcim.models.mixins import InterfaceValidationMixin
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import OrganizationalModel, NetBoxModel
 from netbox.models import OrganizationalModel, NetBoxModel
+from netbox.models.mixins import OwnerMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.mptt import TreeManager
 from utilities.mptt import TreeManager
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
@@ -40,7 +41,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ComponentModel(NetBoxModel):
+class ComponentModel(OwnerMixin, NetBoxModel):
     """
     """
     An abstract model inherited by any model which has a parent Device.
     An abstract model inherited by any model which has a parent Device.
     """
     """

+ 5 - 6
netbox/dcim/tables/cables.py

@@ -1,11 +1,11 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
-from django_tables2.utils import Accessor
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 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 dcim.models import Cable
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import PrimaryModelTable, columns
 from tenancy.tables import TenancyColumnsMixin
 from tenancy.tables import TenancyColumnsMixin
 from .template_code import CABLE_LENGTH
 from .template_code import CABLE_LENGTH
 
 
@@ -48,7 +48,7 @@ class CableTerminationsColumn(tables.Column):
 # Cables
 # Cables
 #
 #
 
 
-class CableTable(TenancyColumnsMixin, NetBoxTable):
+class CableTable(TenancyColumnsMixin, PrimaryModelTable):
     a_terminations = CableTerminationsColumn(
     a_terminations = CableTerminationsColumn(
         cable_end='A',
         cable_end='A',
         orderable=False,
         orderable=False,
@@ -117,12 +117,11 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Color Name'),
         verbose_name=_('Color Name'),
         orderable=False
         orderable=False
     )
     )
-    comments = columns.MarkdownColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:cable_list'
         url_name='dcim:cable_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Cable
         model = Cable
         fields = (
         fields = (
             'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
             'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',

+ 15 - 38
netbox/dcim/tables/devices.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from dcim import models
 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 tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from .template_code import *
 from .template_code import *
 
 
@@ -58,15 +58,7 @@ MACADDRESS_COPY_BUTTON = """
 # Device roles
 # 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(
     device_count = columns.LinkedCountColumn(
         viewname='dcim:device_list',
         viewname='dcim:device_list',
         url_params={'role_id': 'pk'},
         url_params={'role_id': 'pk'},
@@ -89,7 +81,7 @@ class DeviceRoleTable(NetBoxTable):
         url_name='dcim:devicerole_list'
         url_name='dcim:devicerole_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(NestedGroupModelTable.Meta):
         model = models.DeviceRole
         model = models.DeviceRole
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template',
             'pk', 'id', 'name', 'parent', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template',
@@ -102,15 +94,7 @@ class DeviceRoleTable(NetBoxTable):
 # Platforms
 # 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(
     manufacturer = tables.Column(
         verbose_name=_('Manufacturer'),
         verbose_name=_('Manufacturer'),
         linkify=True
         linkify=True
@@ -133,7 +117,7 @@ class PlatformTable(NetBoxTable):
         url_name='dcim:platform_list'
         url_name='dcim:platform_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(NestedGroupModelTable.Meta):
         model = models.Platform
         model = models.Platform
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template',
             'pk', 'id', 'name', 'parent', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template',
@@ -148,7 +132,7 @@ class PlatformTable(NetBoxTable):
 # Devices
 # Devices
 #
 #
 
 
-class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
+class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         template_code=DEVICE_LINK,
         template_code=DEVICE_LINK,
@@ -249,7 +233,6 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         accessor='parent_bay',
         accessor='parent_bay',
         linkify=True
         linkify=True
     )
     )
-    comments = columns.MarkdownColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:device_list'
         url_name='dcim:device_list'
     )
     )
@@ -284,7 +267,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         verbose_name=_('Inventory items')
         verbose_name=_('Inventory items')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = models.Device
         model = models.Device
         fields = (
         fields = (
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
@@ -1050,7 +1033,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
         )
         )
 
 
 
 
-class InventoryItemRoleTable(NetBoxTable):
+class InventoryItemRoleTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -1067,7 +1050,7 @@ class InventoryItemRoleTable(NetBoxTable):
         url_name='dcim:inventoryitemrole_list'
         url_name='dcim:inventoryitemrole_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = models.InventoryItemRole
         model = models.InventoryItemRole
         fields = (
         fields = (
             'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
             'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
@@ -1079,7 +1062,7 @@ class InventoryItemRoleTable(NetBoxTable):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisTable(NetBoxTable):
+class VirtualChassisTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -1093,14 +1076,11 @@ class VirtualChassisTable(NetBoxTable):
         url_params={'virtual_chassis_id': 'pk'},
         url_params={'virtual_chassis_id': 'pk'},
         verbose_name=_('Members')
         verbose_name=_('Members')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:virtualchassis_list'
         url_name='dcim:virtualchassis_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = models.VirtualChassis
         model = models.VirtualChassis
         fields = (
         fields = (
             'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created',
             'pk', 'id', 'name', 'domain', 'master', 'member_count', 'description', 'comments', 'tags', 'created',
@@ -1109,7 +1089,7 @@ class VirtualChassisTable(NetBoxTable):
         default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
         default_columns = ('pk', 'name', 'domain', 'master', 'member_count')
 
 
 
 
-class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
+class VirtualDeviceContextTable(TenancyColumnsMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -1140,14 +1120,11 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
         url_params={'vdc_id': 'pk'},
         url_params={'vdc_id': 'pk'},
         verbose_name=_('Interfaces')
         verbose_name=_('Interfaces')
     )
     )
-
-    comments = columns.MarkdownColumn()
-
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:virtualdevicecontext_list'
         url_name='dcim:virtualdevicecontext_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = models.VirtualDeviceContext
         model = models.VirtualDeviceContext
         fields = (
         fields = (
             'pk', 'id', 'name', 'status', 'identifier', 'tenant', 'tenant_group', 'primary_ip', 'primary_ip4',
             'pk', 'id', 'name', 'status', 'identifier', 'tenant', 'tenant_group', 'primary_ip', 'primary_ip4',
@@ -1158,7 +1135,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
         )
         )
 
 
 
 
-class MACAddressTable(NetBoxTable):
+class MACAddressTable(PrimaryModelTable):
     mac_address = tables.TemplateColumn(
     mac_address = tables.TemplateColumn(
         template_code=MACADDRESS_LINK,
         template_code=MACADDRESS_LINK,
         verbose_name=_('MAC Address')
         verbose_name=_('MAC Address')
@@ -1181,7 +1158,7 @@ class MACAddressTable(NetBoxTable):
         extra_buttons=MACADDRESS_COPY_BUTTON
         extra_buttons=MACADDRESS_COPY_BUTTON
     )
     )
 
 
-    class Meta(DeviceComponentTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = models.MACAddress
         model = models.MACAddress
         fields = (
         fields = (
             'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags',
             'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags',

+ 5 - 8
netbox/dcim/tables/devicetypes.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from dcim import models
 from dcim import models
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin
 from tenancy.tables import ContactsColumnMixin
 from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, WEIGHT
 from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, WEIGHT
 
 
@@ -26,7 +26,7 @@ __all__ = (
 # Manufacturers
 # Manufacturers
 #
 #
 
 
-class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
+class ManufacturerTable(ContactsColumnMixin, OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -60,7 +60,7 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
         url_name='dcim:manufacturer_list'
         url_name='dcim:manufacturer_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = models.Manufacturer
         model = models.Manufacturer
         fields = (
         fields = (
             'pk', 'id', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
             'pk', 'id', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
@@ -76,7 +76,7 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
 # Device types
 # Device types
 #
 #
 
 
-class DeviceTypeTable(NetBoxTable):
+class DeviceTypeTable(PrimaryModelTable):
     model = tables.Column(
     model = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Device Type')
         verbose_name=_('Device Type')
@@ -93,9 +93,6 @@ class DeviceTypeTable(NetBoxTable):
         verbose_name=_('Full Depth'),
         verbose_name=_('Full Depth'),
         false_mark=None
         false_mark=None
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:devicetype_list'
         url_name='dcim:devicetype_list'
     )
     )
@@ -148,7 +145,7 @@ class DeviceTypeTable(NetBoxTable):
         verbose_name=_('Inventory Items')
         verbose_name=_('Inventory Items')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = models.DeviceType
         model = models.DeviceType
         fields = (
         fields = (
             'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',
             'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',

+ 8 - 17
netbox/dcim/tables/modules.py

@@ -1,8 +1,8 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 
 
 from dcim.models import Module, ModuleType, ModuleTypeProfile
 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
 from .template_code import MODULETYPEPROFILE_ATTRIBUTES, WEIGHT
 
 
 __all__ = (
 __all__ = (
@@ -12,7 +12,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ModuleTypeProfileTable(NetBoxTable):
+class ModuleTypeProfileTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -23,14 +23,11 @@ class ModuleTypeProfileTable(NetBoxTable):
         orderable=False,
         orderable=False,
         verbose_name=_('Attributes')
         verbose_name=_('Attributes')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:moduletypeprofile_list'
         url_name='dcim:moduletypeprofile_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ModuleTypeProfile
         model = ModuleTypeProfile
         fields = (
         fields = (
             'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated',
             'pk', 'id', 'name', 'description', 'comments', 'tags', 'created', 'last_updated',
@@ -40,7 +37,7 @@ class ModuleTypeProfileTable(NetBoxTable):
         )
         )
 
 
 
 
-class ModuleTypeTable(NetBoxTable):
+class ModuleTypeTable(PrimaryModelTable):
     profile = tables.Column(
     profile = tables.Column(
         verbose_name=_('Profile'),
         verbose_name=_('Profile'),
         linkify=True
         linkify=True
@@ -64,14 +61,11 @@ class ModuleTypeTable(NetBoxTable):
         url_params={'module_type_id': 'pk'},
         url_params={'module_type_id': 'pk'},
         verbose_name=_('Instances')
         verbose_name=_('Instances')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:moduletype_list'
         url_name='dcim:moduletype_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ModuleType
         model = ModuleType
         fields = (
         fields = (
             'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description',
             'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description',
@@ -82,7 +76,7 @@ class ModuleTypeTable(NetBoxTable):
         )
         )
 
 
 
 
-class ModuleTable(NetBoxTable):
+class ModuleTable(PrimaryModelTable):
     device = tables.Column(
     device = tables.Column(
         verbose_name=_('Device'),
         verbose_name=_('Device'),
         linkify=True
         linkify=True
@@ -103,14 +97,11 @@ class ModuleTable(NetBoxTable):
     status = columns.ChoiceFieldColumn(
     status = columns.ChoiceFieldColumn(
         verbose_name=_('Status'),
         verbose_name=_('Status'),
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:module_list'
         url_name='dcim:module_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Module
         model = Module
         fields = (
         fields = (
             'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
             'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',

+ 7 - 14
netbox/dcim/tables/power.py

@@ -1,10 +1,9 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
+
 from dcim.models import PowerFeed, PowerPanel
 from dcim.models import PowerFeed, PowerPanel
+from netbox.tables import PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
-
-from netbox.tables import NetBoxTable, columns
-
 from .devices import CableTerminationTable
 from .devices import CableTerminationTable
 
 
 __all__ = (
 __all__ = (
@@ -17,7 +16,7 @@ __all__ = (
 # Power panels
 # Power panels
 #
 #
 
 
-class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
+class PowerPanelTable(ContactsColumnMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -35,14 +34,11 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
         url_params={'power_panel_id': 'pk'},
         url_params={'power_panel_id': 'pk'},
         verbose_name=_('Power Feeds')
         verbose_name=_('Power Feeds')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:powerpanel_list'
         url_name='dcim:powerpanel_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = PowerPanel
         model = PowerPanel
         fields = (
         fields = (
             'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags',
             'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'description', 'comments', 'tags',
@@ -57,7 +53,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
 
 
 # We're not using PathEndpointTable for PowerFeed because power connections
 # We're not using PathEndpointTable for PowerFeed because power connections
 # cannot traverse pass-through ports.
 # cannot traverse pass-through ports.
-class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
+class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -92,14 +88,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
         linkify=True,
         linkify=True,
         verbose_name=_('Site'),
         verbose_name=_('Site'),
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:powerfeed_list'
         url_name='dcim:powerfeed_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(CableTerminationTable.Meta, PrimaryModelTable.Meta):
         model = PowerFeed
         model = PowerFeed
         fields = (
         fields = (
             'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',
             'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',

+ 10 - 35
netbox/dcim/tables/racks.py

@@ -1,9 +1,9 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from dcim.models import Rack, RackReservation, RackRole, RackType
 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 tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from .template_code import OUTER_UNIT, WEIGHT
 from .template_code import OUTER_UNIT, WEIGHT
 
 
@@ -15,11 +15,7 @@ __all__ = (
 )
 )
 
 
 
 
-#
-# Rack roles
-#
-
-class RackRoleTable(NetBoxTable):
+class RackRoleTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -36,7 +32,7 @@ class RackRoleTable(NetBoxTable):
         url_name='dcim:rackrole_list'
         url_name='dcim:rackrole_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = RackRole
         model = RackRole
         fields = (
         fields = (
             'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', 'created',
             'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', 'created',
@@ -45,11 +41,7 @@ class RackRoleTable(NetBoxTable):
         default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
         default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
 
 
 
 
-#
-# Rack Types
-#
-
-class RackTypeTable(NetBoxTable):
+class RackTypeTable(PrimaryModelTable):
     model = tables.Column(
     model = tables.Column(
         verbose_name=_('Model'),
         verbose_name=_('Model'),
         linkify=True
         linkify=True
@@ -84,9 +76,6 @@ class RackTypeTable(NetBoxTable):
         template_code=WEIGHT,
         template_code=WEIGHT,
         order_by=('_abs_max_weight', 'weight_unit')
         order_by=('_abs_max_weight', 'weight_unit')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     instance_count = columns.LinkedCountColumn(
     instance_count = columns.LinkedCountColumn(
         viewname='dcim:rack_list',
         viewname='dcim:rack_list',
         url_params={'rack_type_id': 'pk'},
         url_params={'rack_type_id': 'pk'},
@@ -96,7 +85,7 @@ class RackTypeTable(NetBoxTable):
         url_name='dcim:rack_list'
         url_name='dcim:rack_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = RackType
         model = RackType
         fields = (
         fields = (
             'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
             'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
@@ -108,11 +97,7 @@ class RackTypeTable(NetBoxTable):
         )
         )
 
 
 
 
-#
-# Racks
-#
-
-class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
+class RackTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -144,9 +129,6 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         template_code="{{ value }}U",
         template_code="{{ value }}U",
         verbose_name=_('Height')
         verbose_name=_('Height')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     device_count = columns.LinkedCountColumn(
     device_count = columns.LinkedCountColumn(
         viewname='dcim:device_list',
         viewname='dcim:device_list',
         url_params={'rack_id': 'pk'},
         url_params={'rack_id': 'pk'},
@@ -186,7 +168,7 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         order_by=('_abs_max_weight', 'weight_unit')
         order_by=('_abs_max_weight', 'weight_unit')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Rack
         model = Rack
         fields = (
         fields = (
             'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
             'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
@@ -201,11 +183,7 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         )
         )
 
 
 
 
-#
-# Rack reservations
-#
-
-class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
+class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
     reservation = tables.Column(
     reservation = tables.Column(
         verbose_name=_('Reservation'),
         verbose_name=_('Reservation'),
         accessor='pk',
         accessor='pk',
@@ -232,14 +210,11 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
     status = columns.ChoiceFieldColumn(
     status = columns.ChoiceFieldColumn(
         verbose_name=_('Status'),
         verbose_name=_('Status'),
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:rackreservation_list'
         url_name='dcim:rackreservation_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = RackReservation
         model = RackReservation
         fields = (
         fields = (
             'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',
             'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',

+ 11 - 64
netbox/dcim/tables/sites.py

@@ -1,10 +1,9 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
+
 from dcim.models import Location, Region, Site, SiteGroup
 from dcim.models import Location, Region, Site, SiteGroup
+from netbox.tables import NestedGroupModelTable, PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
-
-from netbox.tables import NetBoxTable, columns
-
 from .template_code import LOCATION_BUTTONS
 from .template_code import LOCATION_BUTTONS
 
 
 __all__ = (
 __all__ = (
@@ -15,19 +14,7 @@ __all__ = (
 )
 )
 
 
 
 
-#
-# 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(
     site_count = columns.LinkedCountColumn(
         viewname='dcim:site_list',
         viewname='dcim:site_list',
         url_params={'region_id': 'pk'},
         url_params={'region_id': 'pk'},
@@ -36,11 +23,8 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:region_list'
         url_name='dcim:region_list'
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(NestedGroupModelTable.Meta):
         model = Region
         model = Region
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
             'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
@@ -49,19 +33,7 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
         default_columns = ('pk', 'name', 'site_count', 'description')
         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(
     site_count = columns.LinkedCountColumn(
         viewname='dcim:site_list',
         viewname='dcim:site_list',
         url_params={'group_id': 'pk'},
         url_params={'group_id': 'pk'},
@@ -70,11 +42,8 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:sitegroup_list'
         url_name='dcim:sitegroup_list'
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(NestedGroupModelTable.Meta):
         model = SiteGroup
         model = SiteGroup
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
             'pk', 'id', 'name', 'parent', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
@@ -83,11 +52,7 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
         default_columns = ('pk', 'name', 'site_count', 'description')
         default_columns = ('pk', 'name', 'site_count', 'description')
 
 
 
 
-#
-# Sites
-#
-
-class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
+class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -117,14 +82,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         url_params={'site_id': 'pk'},
         url_params={'site_id': 'pk'},
         verbose_name=_('Devices')
         verbose_name=_('Devices')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:site_list'
         url_name='dcim:site_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Site
         model = Site
         fields = (
         fields = (
             'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns',
             'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns',
@@ -134,19 +96,7 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
         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(
     site = tables.Column(
         verbose_name=_('Site'),
         verbose_name=_('Site'),
         linkify=True
         linkify=True
@@ -175,11 +125,8 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
         extra_buttons=LOCATION_BUTTONS
         extra_buttons=LOCATION_BUTTONS
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(NestedGroupModelTable.Meta):
         model = Location
         model = Location
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count',
             'pk', 'id', 'name', 'parent', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count',

+ 8 - 13
netbox/extras/api/serializers_/configcontexts.py

@@ -8,7 +8,8 @@ from dcim.api.serializers_.sites import LocationSerializer, RegionSerializer, Si
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from extras.models import ConfigContext, ConfigContextProfile, Tag
 from extras.models import ConfigContext, ConfigContextProfile, Tag
 from netbox.api.fields import SerializedPKRelatedField
 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.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer
 from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer
@@ -20,13 +21,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ConfigContextProfileSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
-    tags = serializers.SlugRelatedField(
-        queryset=Tag.objects.all(),
-        slug_field='slug',
-        required=False,
-        many=True
-    )
+class ConfigContextProfileSerializer(PrimaryModelSerializer):
     data_source = DataSourceSerializer(
     data_source = DataSourceSerializer(
         nested=True,
         nested=True,
         required=False
         required=False
@@ -39,13 +34,13 @@ class ConfigContextProfileSerializer(ChangeLogMessageSerializer, ValidatedModelS
     class Meta:
     class Meta:
         model = ConfigContextProfile
         model = ConfigContextProfile
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
 
 
-class ConfigContextSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class ConfigContextSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     profile = ConfigContextProfileSerializer(
     profile = ConfigContextProfileSerializer(
         nested=True,
         nested=True,
         required=False,
         required=False,
@@ -156,7 +151,7 @@ class ConfigContextSerializer(ChangeLogMessageSerializer, ValidatedModelSerializ
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'weight', 'profile', 'description', 'is_active', 'regions',
             'id', 'url', 'display_url', 'display', 'name', 'weight', 'profile', 'description', 'is_active', 'regions',
             'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 8 - 2
netbox/extras/api/serializers_/configtemplates.py

@@ -2,13 +2,19 @@ from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers.features import TaggableModelSerializer
 from netbox.api.serializers.features import TaggableModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 
 
 __all__ = (
 __all__ = (
     'ConfigTemplateSerializer',
     'ConfigTemplateSerializer',
 )
 )
 
 
 
 
-class ConfigTemplateSerializer(ChangeLogMessageSerializer, TaggableModelSerializer, ValidatedModelSerializer):
+class ConfigTemplateSerializer(
+    OwnerMixin,
+    ChangeLogMessageSerializer,
+    TaggableModelSerializer,
+    ValidatedModelSerializer
+):
     data_source = DataSourceSerializer(
     data_source = DataSourceSerializer(
         nested=True,
         nested=True,
         required=False
         required=False
@@ -23,6 +29,6 @@ class ConfigTemplateSerializer(ChangeLogMessageSerializer, TaggableModelSerializ
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code',
             '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',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 6 - 5
netbox/extras/api/serializers_/customfields.py

@@ -8,6 +8,7 @@ from extras.choices import *
 from extras.models import CustomField, CustomFieldChoiceSet
 from extras.models import CustomField, CustomFieldChoiceSet
 from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 
 
 __all__ = (
 __all__ = (
     'CustomFieldChoiceSetSerializer',
     'CustomFieldChoiceSetSerializer',
@@ -15,7 +16,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CustomFieldChoiceSetSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class CustomFieldChoiceSetSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     base_choices = ChoiceField(
     base_choices = ChoiceField(
         choices=CustomFieldChoiceSetBaseChoices,
         choices=CustomFieldChoiceSetBaseChoices,
         required=False
         required=False
@@ -32,12 +33,12 @@ class CustomFieldChoiceSetSerializer(ChangeLogMessageSerializer, ValidatedModelS
         model = CustomFieldChoiceSet
         model = CustomFieldChoiceSet
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'description', 'base_choices', 'extra_choices',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
 
 
 
 
-class CustomFieldSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class CustomFieldSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.with_feature('custom_fields'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
         many=True
         many=True
@@ -64,8 +65,8 @@ class CustomFieldSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer
             'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
             'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
             'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic',
             'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic',
             'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 

+ 3 - 2
netbox/extras/api/serializers_/customlinks.py

@@ -2,13 +2,14 @@ from core.models import ObjectType
 from extras.models import CustomLink
 from extras.models import CustomLink
 from netbox.api.fields import ContentTypeField
 from netbox.api.fields import ContentTypeField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 
 
 __all__ = (
 __all__ = (
     'CustomLinkSerializer',
     'CustomLinkSerializer',
 )
 )
 
 
 
 
-class CustomLinkSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class CustomLinkSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.with_feature('custom_links'),
         queryset=ObjectType.objects.with_feature('custom_links'),
         many=True
         many=True
@@ -18,6 +19,6 @@ class CustomLinkSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer)
         model = CustomLink
         model = CustomLink
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url',
             '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')
         brief_fields = ('id', 'url', 'display', 'name')

+ 5 - 4
netbox/extras/api/serializers_/events.py

@@ -7,6 +7,7 @@ from extras.choices import *
 from extras.models import EventRule, Webhook
 from extras.models import EventRule, Webhook
 from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from .scripts import ScriptSerializer
 from .scripts import ScriptSerializer
 
 
@@ -20,7 +21,7 @@ __all__ = (
 # Event Rules
 # Event Rules
 #
 #
 
 
-class EventRuleSerializer(NetBoxModelSerializer):
+class EventRuleSerializer(OwnerMixin, NetBoxModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.with_feature('event_rules'),
         queryset=ObjectType.objects.with_feature('event_rules'),
         many=True
         many=True
@@ -36,7 +37,7 @@ class EventRuleSerializer(NetBoxModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'event_types', 'conditions',
             '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',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
@@ -56,13 +57,13 @@ class EventRuleSerializer(NetBoxModelSerializer):
 # Webhooks
 # Webhooks
 #
 #
 
 
-class WebhookSerializer(NetBoxModelSerializer):
+class WebhookSerializer(OwnerMixin, NetBoxModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Webhook
         model = Webhook
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'description', 'payload_url', 'http_method',
             'id', 'url', 'display_url', 'display', 'name', 'description', 'payload_url', 'http_method',
             'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 3 - 2
netbox/extras/api/serializers_/exporttemplates.py

@@ -3,13 +3,14 @@ from core.models import ObjectType
 from extras.models import ExportTemplate
 from extras.models import ExportTemplate
 from netbox.api.fields import ContentTypeField
 from netbox.api.fields import ContentTypeField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 
 
 __all__ = (
 __all__ = (
     'ExportTemplateSerializer',
     'ExportTemplateSerializer',
 )
 )
 
 
 
 
-class ExportTemplateSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class ExportTemplateSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.with_feature('export_templates'),
         queryset=ObjectType.objects.with_feature('export_templates'),
         many=True
         many=True
@@ -28,6 +29,6 @@ class ExportTemplateSerializer(ChangeLogMessageSerializer, ValidatedModelSeriali
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'environment_params',
             'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'environment_params',
             'template_code', 'mime_type', 'file_name', 'file_extension', 'as_attachment', 'data_source',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 3 - 2
netbox/extras/api/serializers_/savedfilters.py

@@ -2,13 +2,14 @@ from core.models import ObjectType
 from extras.models import SavedFilter
 from extras.models import SavedFilter
 from netbox.api.fields import ContentTypeField
 from netbox.api.fields import ContentTypeField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 
 
 __all__ = (
 __all__ = (
     'SavedFilterSerializer',
     'SavedFilterSerializer',
 )
 )
 
 
 
 
-class SavedFilterSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class SavedFilterSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.all(),
         queryset=ObjectType.objects.all(),
         many=True
         many=True
@@ -18,6 +19,6 @@ class SavedFilterSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer
         model = SavedFilter
         model = SavedFilter
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

+ 2 - 1
netbox/extras/api/serializers_/tags.py

@@ -6,6 +6,7 @@ from extras.models import Tag, TaggedItem
 from netbox.api.exceptions import SerializerNotFound
 from netbox.api.exceptions import SerializerNotFound
 from netbox.api.fields import ContentTypeField, RelatedObjectCountField
 from netbox.api.fields import ContentTypeField, RelatedObjectCountField
 from netbox.api.serializers import BaseModelSerializer, ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import BaseModelSerializer, ChangeLogMessageSerializer, ValidatedModelSerializer
+from users.api.serializers_.mixins import OwnerMixin
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 
 
 __all__ = (
 __all__ = (
@@ -14,7 +15,7 @@ __all__ = (
 )
 )
 
 
 
 
-class TagSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
+class TagSerializer(OwnerMixin, ChangeLogMessageSerializer, ValidatedModelSerializer):
     object_types = ContentTypeField(
     object_types = ContentTypeField(
         queryset=ObjectType.objects.with_feature('tags'),
         queryset=ObjectType.objects.with_feature('tags'),
         many=True,
         many=True,

+ 13 - 12
netbox/extras/filtersets.py

@@ -5,8 +5,9 @@ from django.utils.translation import gettext as _
 
 
 from core.models import DataSource, ObjectType
 from core.models import DataSource, ObjectType
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 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 tenancy.models import Tenant, TenantGroup
+from users.filterset_mixins import OwnerFilterMixin
 from users.models import Group, User
 from users.models import Group, User
 from utilities.filters import (
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
@@ -61,7 +62,7 @@ class ScriptFilterSet(BaseFilterSet):
         )
         )
 
 
 
 
-class WebhookFilterSet(NetBoxModelFilterSet):
+class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -90,7 +91,7 @@ class WebhookFilterSet(NetBoxModelFilterSet):
         )
         )
 
 
 
 
-class EventRuleFilterSet(NetBoxModelFilterSet):
+class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -130,7 +131,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
         return queryset.filter(event_types__overlap=value)
         return queryset.filter(event_types__overlap=value)
 
 
 
 
-class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
+class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -179,7 +180,7 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
         )
         )
 
 
 
 
-class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet):
+class CustomFieldChoiceSetFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -207,7 +208,7 @@ class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet):
         return queryset.filter(extra_choices__overlap=value)
         return queryset.filter(extra_choices__overlap=value)
 
 
 
 
-class CustomLinkFilterSet(ChangeLoggedModelFilterSet):
+class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -237,7 +238,7 @@ class CustomLinkFilterSet(ChangeLoggedModelFilterSet):
         )
         )
 
 
 
 
-class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
+class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -275,7 +276,7 @@ class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
         )
         )
 
 
 
 
-class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
+class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -494,7 +495,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
         return queryset.filter(comments__icontains=value)
         return queryset.filter(comments__icontains=value)
 
 
 
 
-class TagFilterSet(ChangeLoggedModelFilterSet):
+class TagFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -589,7 +590,7 @@ class TaggedItemFilterSet(BaseFilterSet):
         )
         )
 
 
 
 
-class ConfigContextProfileFilterSet(NetBoxModelFilterSet):
+class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -619,7 +620,7 @@ class ConfigContextProfileFilterSet(NetBoxModelFilterSet):
         )
         )
 
 
 
 
-class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
+class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
@@ -788,7 +789,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
         )
         )
 
 
 
 
-class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet):
+class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),

+ 13 - 19
netbox/extras/forms/bulk_edit.py

@@ -4,8 +4,8 @@ from django.utils.translation import gettext_lazy as _
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from netbox.events import get_event_type_choices
 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 import BulkEditForm, add_blank_choice
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
 from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
@@ -30,7 +30,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CustomFieldBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class CustomFieldBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=CustomField.objects.all(),
         queryset=CustomField.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -98,7 +98,7 @@ class CustomFieldBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     nullable_fields = ('group_name', 'description', 'choice_set')
     nullable_fields = ('group_name', 'description', 'choice_set')
 
 
 
 
-class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=CustomFieldChoiceSet.objects.all(),
         queryset=CustomFieldChoiceSet.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -118,7 +118,7 @@ class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     nullable_fields = ('base_choices', 'description')
     nullable_fields = ('base_choices', 'description')
 
 
 
 
-class CustomLinkBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class CustomLinkBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=CustomLink.objects.all(),
         queryset=CustomLink.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -144,7 +144,7 @@ class CustomLinkBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     )
     )
 
 
 
 
-class ExportTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class ExportTemplateBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=ExportTemplate.objects.all(),
         queryset=ExportTemplate.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -177,7 +177,7 @@ class ExportTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension')
     nullable_fields = ('description', 'mime_type', 'file_name', 'file_extension')
 
 
 
 
-class SavedFilterBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class SavedFilterBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=SavedFilter.objects.all(),
         queryset=SavedFilter.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -233,7 +233,7 @@ class TableConfigBulkEditForm(BulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class WebhookBulkEditForm(NetBoxModelBulkEditForm):
+class WebhookBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     model = Webhook
     model = Webhook
 
 
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
@@ -271,7 +271,7 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('secret', 'ca_file_path')
     nullable_fields = ('secret', 'ca_file_path')
 
 
 
 
-class EventRuleBulkEditForm(NetBoxModelBulkEditForm):
+class EventRuleBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     model = EventRule
     model = EventRule
 
 
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
@@ -297,7 +297,7 @@ class EventRuleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description', 'conditions')
     nullable_fields = ('description', 'conditions')
 
 
 
 
-class TagBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class TagBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -319,17 +319,11 @@ class TagBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class ConfigContextProfileBulkEditForm(NetBoxModelBulkEditForm):
+class ConfigContextProfileBulkEditForm(PrimaryModelBulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=ConfigContextProfile.objects.all(),
         queryset=ConfigContextProfile.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        required=False,
-        max_length=100
-    )
-    comments = CommentField()
 
 
     model = ConfigContextProfile
     model = ConfigContextProfile
     fieldsets = (
     fieldsets = (
@@ -338,7 +332,7 @@ class ConfigContextProfileBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class ConfigContextBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class ConfigContextBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=ConfigContext.objects.all(),
         queryset=ConfigContext.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
@@ -369,7 +363,7 @@ class ConfigContextBulkEditForm(ChangelogMessageMixin, BulkEditForm):
     nullable_fields = ('profile', 'description')
     nullable_fields = ('profile', 'description')
 
 
 
 
-class ConfigTemplateBulkEditForm(ChangelogMessageMixin, BulkEditForm):
+class ConfigTemplateBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
     pk = forms.ModelMultipleChoiceField(
         queryset=ConfigTemplate.objects.all(),
         queryset=ConfigTemplate.objects.all(),
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput

+ 21 - 21
netbox/extras/forms/bulk_import.py

@@ -9,7 +9,7 @@ from core.models import ObjectType
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from netbox.events import get_event_type_choices
 from netbox.events import get_event_type_choices
-from netbox.forms import NetBoxModelImportForm
+from netbox.forms import NetBoxModelImportForm, OwnerCSVMixin, PrimaryModelImportForm
 from users.models import Group, User
 from users.models import Group, User
 from utilities.forms import CSVModelForm
 from utilities.forms import CSVModelForm
 from utilities.forms.fields import (
 from utilities.forms.fields import (
@@ -33,7 +33,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CustomFieldImportForm(CSVModelForm):
+class CustomFieldImportForm(OwnerCSVMixin, CSVModelForm):
     object_types = CSVMultipleContentTypeField(
     object_types = CSVMultipleContentTypeField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
@@ -75,11 +75,11 @@ class CustomFieldImportForm(CSVModelForm):
         fields = (
         fields = (
             'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique',
             'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique',
             'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
             '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(
     base_choices = CSVChoiceField(
         choices=CustomFieldChoiceSetBaseChoices,
         choices=CustomFieldChoiceSetBaseChoices,
         required=False,
         required=False,
@@ -97,7 +97,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
     class Meta:
     class Meta:
         model = CustomFieldChoiceSet
         model = CustomFieldChoiceSet
         fields = (
         fields = (
-            'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
+            'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner',
         )
         )
 
 
     def clean_extra_choices(self):
     def clean_extra_choices(self):
@@ -114,7 +114,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
             return data
             return data
 
 
 
 
-class CustomLinkImportForm(CSVModelForm):
+class CustomLinkImportForm(OwnerCSVMixin, CSVModelForm):
     object_types = CSVMultipleContentTypeField(
     object_types = CSVMultipleContentTypeField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('custom_links'),
         queryset=ObjectType.objects.with_feature('custom_links'),
@@ -131,11 +131,11 @@ class CustomLinkImportForm(CSVModelForm):
         model = CustomLink
         model = CustomLink
         fields = (
         fields = (
             'name', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
             '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(
     object_types = CSVMultipleContentTypeField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('export_templates'),
         queryset=ObjectType.objects.with_feature('export_templates'),
@@ -146,30 +146,30 @@ class ExportTemplateImportForm(CSVModelForm):
         model = ExportTemplate
         model = ExportTemplate
         fields = (
         fields = (
             'name', 'object_types', 'description', 'environment_params', 'mime_type', 'file_name', 'file_extension',
             '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(PrimaryModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ConfigContextProfile
         model = ConfigContextProfile
         fields = [
         fields = [
-            'name', 'description', 'schema', 'comments', 'tags',
+            'name', 'description', 'schema', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class ConfigTemplateImportForm(CSVModelForm):
+class ConfigTemplateImportForm(OwnerCSVMixin, CSVModelForm):
 
 
     class Meta:
     class Meta:
         model = ConfigTemplate
         model = ConfigTemplate
         fields = (
         fields = (
             'name', 'description', 'template_code', 'environment_params', 'mime_type', 'file_name', 'file_extension',
             '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(
     object_types = CSVMultipleContentTypeField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.all(),
         queryset=ObjectType.objects.all(),
@@ -179,21 +179,21 @@ class SavedFilterImportForm(CSVModelForm):
     class Meta:
     class Meta:
         model = SavedFilter
         model = SavedFilter
         fields = (
         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:
     class Meta:
         model = Webhook
         model = Webhook
         fields = (
         fields = (
             'name', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template',
             '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(
     object_types = CSVMultipleContentTypeField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('event_rules'),
         queryset=ObjectType.objects.with_feature('event_rules'),
@@ -214,7 +214,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
         model = EventRule
         model = EventRule
         fields = (
         fields = (
             'name', 'description', 'enabled', 'conditions', 'object_types', 'event_types', 'action_type',
             'name', 'description', 'enabled', 'conditions', 'object_types', 'event_types', 'action_type',
-            'comments', 'tags'
+            'owner', 'comments', 'tags'
         )
         )
 
 
     def clean(self):
     def clean(self):
@@ -242,7 +242,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
                 self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False)
                 self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False)
 
 
 
 
-class TagImportForm(CSVModelForm):
+class TagImportForm(OwnerCSVMixin, CSVModelForm):
     slug = SlugField()
     slug = SlugField()
     weight = forms.IntegerField(
     weight = forms.IntegerField(
         label=_('Weight'),
         label=_('Weight'),
@@ -258,7 +258,7 @@ class TagImportForm(CSVModelForm):
     class Meta:
     class Meta:
         model = Tag
         model = Tag
         fields = (
         fields = (
-            'name', 'slug', 'color', 'weight', 'description', 'object_types',
+            'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner',
         )
         )
 
 
 
 

+ 58 - 8
netbox/extras/forms/filtersets.py

@@ -6,13 +6,14 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from netbox.events import get_event_type_choices
 from netbox.events import get_event_type_choices
-from netbox.forms.base import NetBoxModelFilterSetForm
+from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm
 from netbox.forms.mixins import SavedFiltersMixin
 from netbox.forms.mixins import SavedFiltersMixin
 from tenancy.models import Tenant, TenantGroup
 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 import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
 from utilities.forms.fields import (
 from utilities.forms.fields import (
-    ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
+    ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    TagFilterField,
 )
 )
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DateTimePicker
 from utilities.forms.widgets import DateTimePicker
@@ -115,6 +116,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
         label=_('Validation regex'),
         label=_('Validation regex'),
         required=False
         required=False
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
 class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
@@ -130,6 +136,11 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
     choice = forms.CharField(
     choice = forms.CharField(
         required=False
         required=False
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
 class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
@@ -161,6 +172,11 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
         label=_('Weight'),
         label=_('Weight'),
         required=False
         required=False
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
 class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
@@ -207,6 +223,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
 class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
@@ -255,6 +276,11 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
         label=_('Weight'),
         label=_('Weight'),
         required=False
         required=False
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
 class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
@@ -290,7 +316,7 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm):
 class WebhookFilterForm(NetBoxModelFilterSetForm):
 class WebhookFilterForm(NetBoxModelFilterSetForm):
     model = Webhook
     model = Webhook
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
         FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')),
     )
     )
     http_content_type = forms.CharField(
     http_content_type = forms.CharField(
@@ -306,15 +332,18 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
         required=False,
         required=False,
         label=_('HTTP method')
         label=_('HTTP method')
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
 class EventRuleFilterForm(NetBoxModelFilterSetForm):
 class EventRuleFilterForm(NetBoxModelFilterSetForm):
     model = EventRule
     model = EventRule
-    tag = TagFilterField(model)
-
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
         FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
     )
     )
     object_type_id = ContentTypeMultipleChoiceField(
     object_type_id = ContentTypeMultipleChoiceField(
@@ -339,6 +368,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
+    tag = TagFilterField(model)
 
 
 
 
 class TagFilterForm(SavedFiltersMixin, FilterForm):
 class TagFilterForm(SavedFiltersMixin, FilterForm):
@@ -353,9 +388,14 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
         required=False,
         required=False,
         label=_('Allowed object type')
         label=_('Allowed object type')
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
-class ConfigContextProfileFilterForm(SavedFiltersMixin, FilterForm):
+class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm):
     model = ConfigContextProfile
     model = ConfigContextProfile
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id'),
         FieldSet('q', 'filter_id'),
@@ -470,6 +510,11 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
         required=False,
         required=False,
         label=_('Tags')
         label=_('Tags')
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
@@ -512,6 +557,11 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    owner_id = DynamicModelChoiceField(
+        queryset=Owner.objects.all(),
+        required=False,
+        label=_('Owner'),
+    )
 
 
 
 
 class LocalConfigContextFilterForm(forms.Form):
 class LocalConfigContextFilterForm(forms.Form):

+ 19 - 18
netbox/extras/forms/model_forms.py

@@ -12,8 +12,8 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from netbox.events import get_event_type_choices
 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 tenancy.models import Tenant, TenantGroup
 from users.models import Group, User
 from users.models import Group, User
 from utilities.forms import get_field_value
 from utilities.forms import get_field_value
@@ -47,7 +47,7 @@ __all__ = (
 )
 )
 
 
 
 
-class CustomFieldForm(ChangelogMessageMixin, forms.ModelForm):
+class CustomFieldForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
         queryset=ObjectType.objects.with_feature('custom_fields'),
@@ -166,7 +166,7 @@ class CustomFieldForm(ChangelogMessageMixin, forms.ModelForm):
             del self.fields['choice_set']
             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
     # TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model
     extra_choices = forms.CharField(
     extra_choices = forms.CharField(
         widget=ChoicesWidget(),
         widget=ChoicesWidget(),
@@ -179,7 +179,7 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = CustomFieldChoiceSet
         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):
     def __init__(self, *args, initial=None, **kwargs):
         super().__init__(*args, initial=initial, **kwargs)
         super().__init__(*args, initial=initial, **kwargs)
@@ -219,7 +219,7 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, forms.ModelForm):
         return data
         return data
 
 
 
 
-class CustomLinkForm(ChangelogMessageMixin, forms.ModelForm):
+class CustomLinkForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('custom_links')
         queryset=ObjectType.objects.with_feature('custom_links')
@@ -251,7 +251,7 @@ class CustomLinkForm(ChangelogMessageMixin, forms.ModelForm):
         }
         }
 
 
 
 
-class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm):
+class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('export_templates')
         queryset=ObjectType.objects.with_feature('export_templates')
@@ -293,7 +293,7 @@ class ExportTemplateForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm
         return self.cleaned_data
         return self.cleaned_data
 
 
 
 
-class SavedFilterForm(ChangelogMessageMixin, forms.ModelForm):
+class SavedFilterForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
     slug = SlugField()
     slug = SlugField()
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
@@ -427,7 +427,7 @@ class SubscriptionForm(forms.ModelForm):
         fields = ('object_type', 'object_id')
         fields = ('object_type', 'object_id')
 
 
 
 
-class WebhookForm(NetBoxModelForm):
+class WebhookForm(OwnerMixin, NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'description', 'tags', name=_('Webhook')),
         FieldSet('name', 'description', 'tags', name=_('Webhook')),
@@ -447,7 +447,7 @@ class WebhookForm(NetBoxModelForm):
         }
         }
 
 
 
 
-class EventRuleForm(NetBoxModelForm):
+class EventRuleForm(OwnerMixin, NetBoxModelForm):
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
         queryset=ObjectType.objects.with_feature('event_rules'),
         queryset=ObjectType.objects.with_feature('event_rules'),
@@ -480,7 +480,7 @@ class EventRuleForm(NetBoxModelForm):
         model = EventRule
         model = EventRule
         fields = (
         fields = (
             'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
             '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 = {
         widgets = {
             'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
             'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
@@ -563,7 +563,7 @@ class EventRuleForm(NetBoxModelForm):
         return self.cleaned_data
         return self.cleaned_data
 
 
 
 
-class TagForm(ChangelogMessageMixin, forms.ModelForm):
+class TagForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
     slug = SlugField()
     slug = SlugField()
     object_types = ContentTypeMultipleChoiceField(
     object_types = ContentTypeMultipleChoiceField(
         label=_('Object types'),
         label=_('Object types'),
@@ -582,11 +582,11 @@ class TagForm(ChangelogMessageMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = Tag
         model = Tag
         fields = [
         fields = [
-            'name', 'slug', 'color', 'weight', 'description', 'object_types',
+            'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner',
         ]
         ]
 
 
 
 
-class ConfigContextProfileForm(SyncedDataMixin, NetBoxModelForm):
+class ConfigContextProfileForm(SyncedDataMixin, PrimaryModelForm):
     schema = JSONField(
     schema = JSONField(
         label=_('Schema'),
         label=_('Schema'),
         required=False,
         required=False,
@@ -606,11 +606,12 @@ class ConfigContextProfileForm(SyncedDataMixin, NetBoxModelForm):
     class Meta:
     class Meta:
         model = ConfigContextProfile
         model = ConfigContextProfile
         fields = (
         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',
         )
         )
 
 
 
 
-class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm):
+class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
     profile = DynamicModelChoiceField(
     profile = DynamicModelChoiceField(
         label=_('Profile'),
         label=_('Profile'),
         queryset=ConfigContextProfile.objects.all(),
         queryset=ConfigContextProfile.objects.all(),
@@ -701,7 +702,7 @@ class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm)
         fields = (
         fields = (
             'name', 'weight', 'profile', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites',
             'name', 'weight', 'profile', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites',
             'locations', 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
             '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):
     def __init__(self, *args, initial=None, **kwargs):
@@ -727,7 +728,7 @@ class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm)
         return self.cleaned_data
         return self.cleaned_data
 
 
 
 
-class ConfigTemplateForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm):
+class ConfigTemplateForm(ChangelogMessageMixin, SyncedDataMixin, OwnerMixin, forms.ModelForm):
     tags = DynamicModelMultipleChoiceField(
     tags = DynamicModelMultipleChoiceField(
         label=_('Tags'),
         label=_('Tags'),
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),

+ 13 - 12
netbox/extras/graphql/types.py

@@ -6,7 +6,8 @@ import strawberry_django
 from core.graphql.mixins import SyncedDataMixin
 from core.graphql.mixins import SyncedDataMixin
 from extras import models
 from extras import models
 from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
 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 *
 from .filters import *
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -51,7 +52,7 @@ __all__ = (
     filters=ConfigContextProfileFilter,
     filters=ConfigContextProfileFilter,
     pagination=True
     pagination=True
 )
 )
-class ConfigContextProfileType(SyncedDataMixin, NetBoxObjectType):
+class ConfigContextProfileType(SyncedDataMixin, PrimaryObjectType):
     pass
     pass
 
 
 
 
@@ -61,7 +62,7 @@ class ConfigContextProfileType(SyncedDataMixin, NetBoxObjectType):
     filters=ConfigContextFilter,
     filters=ConfigContextFilter,
     pagination=True
     pagination=True
 )
 )
-class ConfigContextType(SyncedDataMixin, ObjectType):
+class ConfigContextType(SyncedDataMixin, OwnerMixin, ObjectType):
     profile: ConfigContextProfileType | None
     profile: ConfigContextProfileType | None
     roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]
     roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]]
     device_types: List[Annotated["DeviceTypeType", 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,
     filters=ConfigTemplateFilter,
     pagination=True
     pagination=True
 )
 )
-class ConfigTemplateType(SyncedDataMixin, TagsMixin, ObjectType):
+class ConfigTemplateType(SyncedDataMixin, OwnerMixin, TagsMixin, ObjectType):
     virtualmachines: List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]
     virtualmachines: List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]]
     devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
     devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
     platforms: List[Annotated["PlatformType", 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,
     filters=CustomFieldFilter,
     pagination=True
     pagination=True
 )
 )
-class CustomFieldType(ObjectType):
+class CustomFieldType(OwnerMixin, ObjectType):
     related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
     related_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
     choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None
     choice_set: Annotated["CustomFieldChoiceSetType", strawberry.lazy('extras.graphql.types')] | None
 
 
@@ -108,7 +109,7 @@ class CustomFieldType(ObjectType):
     filters=CustomFieldChoiceSetFilter,
     filters=CustomFieldChoiceSetFilter,
     pagination=True
     pagination=True
 )
 )
-class CustomFieldChoiceSetType(ObjectType):
+class CustomFieldChoiceSetType(OwnerMixin, ObjectType):
 
 
     choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]
     choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]
     extra_choices: List[List[str]] | None
     extra_choices: List[List[str]] | None
@@ -120,7 +121,7 @@ class CustomFieldChoiceSetType(ObjectType):
     filters=CustomLinkFilter,
     filters=CustomLinkFilter,
     pagination=True
     pagination=True
 )
 )
-class CustomLinkType(ObjectType):
+class CustomLinkType(OwnerMixin, ObjectType):
     pass
     pass
 
 
 
 
@@ -130,7 +131,7 @@ class CustomLinkType(ObjectType):
     filters=ExportTemplateFilter,
     filters=ExportTemplateFilter,
     pagination=True
     pagination=True
 )
 )
-class ExportTemplateType(SyncedDataMixin, ObjectType):
+class ExportTemplateType(SyncedDataMixin, OwnerMixin, ObjectType):
     pass
     pass
 
 
 
 
@@ -180,7 +181,7 @@ class NotificationGroupType(ObjectType):
     filters=SavedFilterFilter,
     filters=SavedFilterFilter,
     pagination=True
     pagination=True
 )
 )
-class SavedFilterType(ObjectType):
+class SavedFilterType(OwnerMixin, ObjectType):
     user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
     user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
 
 
 
 
@@ -209,7 +210,7 @@ class TableConfigType(ObjectType):
     filters=TagFilter,
     filters=TagFilter,
     pagination=True
     pagination=True
 )
 )
-class TagType(ObjectType):
+class TagType(OwnerMixin, ObjectType):
     color: str
     color: str
 
 
     object_types: List[ContentTypeType]
     object_types: List[ContentTypeType]
@@ -221,7 +222,7 @@ class TagType(ObjectType):
     filters=WebhookFilter,
     filters=WebhookFilter,
     pagination=True
     pagination=True
 )
 )
-class WebhookType(OrganizationalObjectType):
+class WebhookType(OwnerMixin, CustomFieldsMixin, TagsMixin, ObjectType):
     pass
     pass
 
 
 
 
@@ -231,5 +232,5 @@ class WebhookType(OrganizationalObjectType):
     filters=EventRuleFilter,
     filters=EventRuleFilter,
     pagination=True
     pagination=True
 )
 )
-class EventRuleType(OrganizationalObjectType):
+class EventRuleType(OwnerMixin, CustomFieldsMixin, TagsMixin, ObjectType):
     action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None
     action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None

+ 89 - 0
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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='configcontextprofile',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='customfield',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='customlink',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='exporttemplate',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+    ]

+ 9 - 2
netbox/extras/models/configs.py

@@ -13,6 +13,7 @@ from extras.models.mixins import RenderTemplateMixin
 from extras.querysets import ConfigContextQuerySet
 from extras.querysets import ConfigContextQuerySet
 from netbox.models import ChangeLoggedModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, PrimaryModel
 from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
 from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
+from netbox.models.mixins import OwnerMixin
 from utilities.data import deepmerge
 from utilities.data import deepmerge
 from utilities.jsonschema import validate_schema
 from utilities.jsonschema import validate_schema
 
 
@@ -68,7 +69,7 @@ class ConfigContextProfile(SyncedDataMixin, PrimaryModel):
     sync_data.alters_data = True
     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
     A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
     qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
     qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@@ -266,7 +267,13 @@ class ConfigContextModel(models.Model):
 #
 #
 
 
 class ConfigTemplate(
 class ConfigTemplate(
-    RenderTemplateMixin, SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel
+    RenderTemplateMixin,
+    SyncedDataMixin,
+    CustomLinksMixin,
+    ExportTemplatesMixin,
+    OwnerMixin,
+    TagsMixin,
+    ChangeLoggedModel,
 ):
 ):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),

+ 3 - 2
netbox/extras/models/customfields.py

@@ -21,6 +21,7 @@ from extras.choices import *
 from extras.data import CHOICE_SETS
 from extras.data import CHOICE_SETS
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
 from netbox.models.features import CloningMixin, ExportTemplatesMixin
 from netbox.models.features import CloningMixin, ExportTemplatesMixin
+from netbox.models.mixins import OwnerMixin
 from netbox.search import FieldTypes
 from netbox.search import FieldTypes
 from utilities import filters
 from utilities import filters
 from utilities.datetime import datetime_from_timestamp
 from utilities.datetime import datetime_from_timestamp
@@ -70,7 +71,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
         }
         }
 
 
 
 
-class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
+class CustomField(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel):
     object_types = models.ManyToManyField(
     object_types = models.ManyToManyField(
         to='contenttypes.ContentType',
         to='contenttypes.ContentType',
         related_name='custom_fields',
         related_name='custom_fields',
@@ -773,7 +774,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
             raise ValidationError(_("Required field cannot be empty."))
             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.
     Represents a set of choices available for choice and multi-choice custom fields.
     """
     """

+ 13 - 5
netbox/extras/models/models.py

@@ -25,6 +25,7 @@ from netbox.models import ChangeLoggedModel
 from netbox.models.features import (
 from netbox.models.features import (
     CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, has_feature
     CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, has_feature
 )
 )
+from netbox.models.mixins import OwnerMixin
 from utilities.html import clean_html
 from utilities.html import clean_html
 from utilities.jinja2 import render_jinja2
 from utilities.jinja2 import render_jinja2
 from utilities.querydict import dict_to_querydict
 from utilities.querydict import dict_to_querydict
@@ -44,7 +45,7 @@ __all__ = (
 )
 )
 
 
 
 
-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
     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
     specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a
@@ -155,7 +156,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
             return False
             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
     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.
     delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
@@ -294,7 +295,7 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
         return render_jinja2(self.payload_url, 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
     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.
     code to be rendered with an object as context.
@@ -394,7 +395,14 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
         }
         }
 
 
 
 
-class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderTemplateMixin):
+class ExportTemplate(
+    SyncedDataMixin,
+    CloningMixin,
+    ExportTemplatesMixin,
+    OwnerMixin,
+    ChangeLoggedModel,
+    RenderTemplateMixin,
+):
     object_types = models.ManyToManyField(
     object_types = models.ManyToManyField(
         to='contenttypes.ContentType',
         to='contenttypes.ContentType',
         related_name='export_templates',
         related_name='export_templates',
@@ -456,7 +464,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
         return _context
         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.
     A set of predefined keyword parameters that can be reused to filter for specific objects.
     """
     """

+ 2 - 1
netbox/extras/models/tags.py

@@ -8,6 +8,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
 from netbox.models.features import CloningMixin, ExportTemplatesMixin
 from netbox.models.features import CloningMixin, ExportTemplatesMixin
+from netbox.models.mixins import OwnerMixin
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 
 
@@ -21,7 +22,7 @@ __all__ = (
 # Tags
 # Tags
 #
 #
 
 
-class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
+class Tag(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel, TagBase):
     id = models.BigAutoField(
     id = models.BigAutoField(
         primary_key=True
         primary_key=True
     )
     )

+ 43 - 3
netbox/extras/tables/tables.py

@@ -10,7 +10,7 @@ from core.tables import JobTable
 from core.models import Job
 from core.models import Job
 from netbox.constants import EMPTY_TABLE_TEXT
 from netbox.constants import EMPTY_TABLE_TEXT
 from netbox.events import get_event_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
 from .columns import NotificationActionsColumn
 
 
 __all__ = (
 __all__ = (
@@ -109,6 +109,10 @@ class CustomFieldTable(NetBoxTable):
     validation_regex = tables.Column(
     validation_regex = tables.Column(
         verbose_name=_('Validation Regex'),
         verbose_name=_('Validation Regex'),
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = CustomField
         model = CustomField
@@ -146,6 +150,10 @@ class CustomFieldChoiceSetTable(NetBoxTable):
         verbose_name=_('Order Alphabetically'),
         verbose_name=_('Order Alphabetically'),
         false_mark=None
         false_mark=None
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = CustomFieldChoiceSet
         model = CustomFieldChoiceSet
@@ -171,6 +179,10 @@ class CustomLinkTable(NetBoxTable):
         verbose_name=_('New Window'),
         verbose_name=_('New Window'),
         false_mark=None
         false_mark=None
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = CustomLink
         model = CustomLink
@@ -214,6 +226,10 @@ class ExportTemplateTable(NetBoxTable):
         orderable=False,
         orderable=False,
         verbose_name=_('Synced')
         verbose_name=_('Synced')
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ExportTemplate
         model = ExportTemplate
@@ -294,6 +310,10 @@ class SavedFilterTable(NetBoxTable):
         verbose_name=_('Shared'),
         verbose_name=_('Shared'),
         false_mark=None
         false_mark=None
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     def value_parameters(self, value):
     def value_parameters(self, value):
         return json.dumps(value)
         return json.dumps(value)
@@ -450,6 +470,10 @@ class WebhookTable(NetBoxTable):
     ssl_validation = columns.BooleanColumn(
     ssl_validation = columns.BooleanColumn(
         verbose_name=_('SSL Validation')
         verbose_name=_('SSL Validation')
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='extras:webhook_list'
         url_name='extras:webhook_list'
     )
     )
@@ -488,6 +512,10 @@ class EventRuleTable(NetBoxTable):
         func=get_event_text,
         func=get_event_text,
         orderable=False
         orderable=False
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='extras:webhook_list'
         url_name='extras:webhook_list'
     )
     )
@@ -514,6 +542,10 @@ class TagTable(NetBoxTable):
     object_types = columns.ContentTypesColumn(
     object_types = columns.ContentTypesColumn(
         verbose_name=_('Object Types'),
         verbose_name=_('Object Types'),
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Tag
         model = Tag
@@ -547,7 +579,7 @@ class TaggedItemTable(NetBoxTable):
         fields = ('id', 'content_type', 'content_object')
         fields = ('id', 'content_type', 'content_object')
 
 
 
 
-class ConfigContextProfileTable(NetBoxTable):
+class ConfigContextProfileTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -568,7 +600,7 @@ class ConfigContextProfileTable(NetBoxTable):
         url_name='extras:configcontextprofile_list'
         url_name='extras:configcontextprofile_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ConfigContextProfile
         model = ConfigContextProfile
         fields = (
         fields = (
             'pk', 'id', 'name', 'description', 'comments', 'data_source', 'data_file', 'is_synced', 'tags', 'created',
             'pk', 'id', 'name', 'description', 'comments', 'data_source', 'data_file', 'is_synced', 'tags', 'created',
@@ -601,6 +633,10 @@ class ConfigContextTable(NetBoxTable):
         orderable=False,
         orderable=False,
         verbose_name=_('Synced')
         verbose_name=_('Synced')
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='extras:configcontext_list'
         url_name='extras:configcontext_list'
     )
     )
@@ -645,6 +681,10 @@ class ConfigTemplateTable(NetBoxTable):
         verbose_name=_('As Attachment'),
         verbose_name=_('As Attachment'),
         false_mark=None
         false_mark=None
     )
     )
+    owner = tables.Column(
+        linkify=True,
+        verbose_name=_('Owner')
+    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='extras:configtemplate_list'
         url_name='extras:configtemplate_list'
     )
     )

+ 7 - 7
netbox/ipam/api/serializers_/asns.py

@@ -2,7 +2,7 @@ from rest_framework import serializers
 
 
 from ipam.models import ASN, ASNRange, RIR
 from ipam.models import ASN, ASNRange, RIR
 from netbox.api.fields import RelatedObjectCountField
 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
 from tenancy.api.serializers_.tenants import TenantSerializer
 
 
 __all__ = (
 __all__ = (
@@ -13,7 +13,7 @@ __all__ = (
 )
 )
 
 
 
 
-class RIRSerializer(NetBoxModelSerializer):
+class RIRSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     aggregate_count = RelatedObjectCountField('aggregates')
     aggregate_count = RelatedObjectCountField('aggregates')
@@ -21,13 +21,13 @@ class RIRSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = RIR
         model = RIR
         fields = [
         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',
             'custom_fields', 'created', 'last_updated', 'aggregate_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
 
 
 
 
-class ASNRangeSerializer(NetBoxModelSerializer):
+class ASNRangeSerializer(OrganizationalModelSerializer):
     rir = RIRSerializer(nested=True)
     rir = RIRSerializer(nested=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     asn_count = serializers.IntegerField(read_only=True)
     asn_count = serializers.IntegerField(read_only=True)
@@ -36,12 +36,12 @@ class ASNRangeSerializer(NetBoxModelSerializer):
         model = ASNRange
         model = ASNRange
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
 
 
-class ASNSerializer(NetBoxModelSerializer):
+class ASNSerializer(PrimaryModelSerializer):
     rir = RIRSerializer(nested=True, required=False, allow_null=True)
     rir = RIRSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(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:
     class Meta:
         model = ASN
         model = ASN
         fields = [
         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',
             'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'asn', 'description')
         brief_fields = ('id', 'url', 'display', 'asn', 'description')

+ 3 - 3
netbox/ipam/api/serializers_/fhrpgroups.py

@@ -4,7 +4,7 @@ from rest_framework import serializers
 
 
 from ipam.models import FHRPGroup, FHRPGroupAssignment
 from ipam.models import FHRPGroup, FHRPGroupAssignment
 from netbox.api.fields import ContentTypeField
 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 utilities.api import get_serializer_for_model
 from .ip import IPAddressSerializer
 from .ip import IPAddressSerializer
 
 
@@ -14,14 +14,14 @@ __all__ = (
 )
 )
 
 
 
 
-class FHRPGroupSerializer(NetBoxModelSerializer):
+class FHRPGroupSerializer(PrimaryModelSerializer):
     ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True)
     ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True)
 
 
     class Meta:
     class Meta:
         model = FHRPGroup
         model = FHRPGroup
         fields = [
         fields = [
             'id', 'name', 'url', 'display_url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key',
             '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')
         brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description')
 
 

+ 9 - 9
netbox/ipam/api/serializers_/ip.py

@@ -7,7 +7,7 @@ from ipam.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
 from ipam.models import Aggregate, IPAddress, IPRange, Prefix
 from ipam.models import Aggregate, IPAddress, IPRange, Prefix
 from netbox.api.fields import ChoiceField, ContentTypeField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from .asns import RIRSerializer
 from .asns import RIRSerializer
@@ -28,7 +28,7 @@ __all__ = (
 )
 )
 
 
 
 
-class AggregateSerializer(NetBoxModelSerializer):
+class AggregateSerializer(PrimaryModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     rir = RIRSerializer(nested=True)
     rir = RIRSerializer(nested=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@@ -38,12 +38,12 @@ class AggregateSerializer(NetBoxModelSerializer):
         model = Aggregate
         model = Aggregate
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description',
             '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')
         brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description')
 
 
 
 
-class PrefixSerializer(NetBoxModelSerializer):
+class PrefixSerializer(PrimaryModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     vrf = VRFSerializer(nested=True, required=False, allow_null=True)
     vrf = VRFSerializer(nested=True, required=False, allow_null=True)
     scope_type = ContentTypeField(
     scope_type = ContentTypeField(
@@ -68,7 +68,7 @@ class PrefixSerializer(NetBoxModelSerializer):
         model = Prefix
         model = Prefix
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
             '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',
             'custom_fields', 'created', 'last_updated', 'children', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
         brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
@@ -133,7 +133,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
 # IP ranges
 # IP ranges
 #
 #
 
 
-class IPRangeSerializer(NetBoxModelSerializer):
+class IPRangeSerializer(PrimaryModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     start_address = IPAddressField()
     start_address = IPAddressField()
     end_address = IPAddressField()
     end_address = IPAddressField()
@@ -146,7 +146,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
         model = IPRange
         model = IPRange
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
             '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',
             'mark_populated', 'mark_utilized',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
         brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
@@ -156,7 +156,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressSerializer(NetBoxModelSerializer):
+class IPAddressSerializer(PrimaryModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     address = IPAddressField()
     address = IPAddressField()
     vrf = VRFSerializer(nested=True, required=False, allow_null=True)
     vrf = VRFSerializer(nested=True, required=False, allow_null=True)
@@ -177,7 +177,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
             'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
             'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
             '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')
         brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
 
 

+ 4 - 4
netbox/ipam/api/serializers_/roles.py

@@ -1,13 +1,13 @@
 from ipam.models import Role
 from ipam.models import Role
 from netbox.api.fields import RelatedObjectCountField
 from netbox.api.fields import RelatedObjectCountField
-from netbox.api.serializers import NetBoxModelSerializer
+from netbox.api.serializers import OrganizationalModelSerializer
 
 
 __all__ = (
 __all__ = (
     'RoleSerializer',
     'RoleSerializer',
 )
 )
 
 
 
 
-class RoleSerializer(NetBoxModelSerializer):
+class RoleSerializer(OrganizationalModelSerializer):
 
 
     # Related object counts
     # Related object counts
     prefix_count = RelatedObjectCountField('prefixes')
     prefix_count = RelatedObjectCountField('prefixes')
@@ -16,7 +16,7 @@ class RoleSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = Role
         model = Role
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count')

+ 6 - 6
netbox/ipam/api/serializers_/services.py

@@ -6,7 +6,7 @@ from ipam.choices import *
 from ipam.constants import SERVICE_ASSIGNMENT_MODELS
 from ipam.constants import SERVICE_ASSIGNMENT_MODELS
 from ipam.models import IPAddress, Service, ServiceTemplate
 from ipam.models import IPAddress, Service, ServiceTemplate
 from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
 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 utilities.api import get_serializer_for_model
 from .ip import IPAddressSerializer
 from .ip import IPAddressSerializer
 
 
@@ -16,19 +16,19 @@ __all__ = (
 )
 )
 
 
 
 
-class ServiceTemplateSerializer(NetBoxModelSerializer):
+class ServiceTemplateSerializer(PrimaryModelSerializer):
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
 
 
     class Meta:
     class Meta:
         model = ServiceTemplate
         model = ServiceTemplate
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
 
 
 
 
-class ServiceSerializer(NetBoxModelSerializer):
+class ServiceSerializer(PrimaryModelSerializer):
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     ipaddresses = SerializedPKRelatedField(
     ipaddresses = SerializedPKRelatedField(
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
@@ -46,7 +46,7 @@ class ServiceSerializer(NetBoxModelSerializer):
         model = Service
         model = Service
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'parent_object_type', 'parent_object_id', 'parent', 'name',
             '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',
             'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')

+ 8 - 7
netbox/ipam/api/serializers_/vlans.py

@@ -7,7 +7,7 @@ from ipam.choices import *
 from ipam.constants import VLANGROUP_SCOPE_TYPES
 from ipam.constants import VLANGROUP_SCOPE_TYPES
 from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule
 from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule
 from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
 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 tenancy.api.serializers_.tenants import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
@@ -24,7 +24,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VLANGroupSerializer(NetBoxModelSerializer):
+class VLANGroupSerializer(OrganizationalModelSerializer):
     scope_type = ContentTypeField(
     scope_type = ContentTypeField(
         queryset=ContentType.objects.filter(
         queryset=ContentType.objects.filter(
             model__in=VLANGROUP_SCOPE_TYPES
             model__in=VLANGROUP_SCOPE_TYPES
@@ -46,7 +46,8 @@ class VLANGroupSerializer(NetBoxModelSerializer):
         model = VLANGroup
         model = VLANGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
             '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')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
         validators = []
         validators = []
@@ -60,7 +61,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
         return serializer(obj.scope, nested=True, context=context).data
         return serializer(obj.scope, nested=True, context=context).data
 
 
 
 
-class VLANSerializer(NetBoxModelSerializer):
+class VLANSerializer(PrimaryModelSerializer):
     site = SiteSerializer(nested=True, required=False, allow_null=True)
     site = SiteSerializer(nested=True, required=False, allow_null=True)
     group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None)
     group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@@ -77,7 +78,7 @@ class VLANSerializer(NetBoxModelSerializer):
         model = VLAN
         model = VLAN
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role',
             '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',
             'created', 'last_updated', 'prefix_count',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')
         brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')
@@ -125,10 +126,10 @@ class VLANTranslationRuleSerializer(NetBoxModelSerializer):
         fields = ['id', 'url', 'display', 'policy', 'local_vid', 'remote_vid', 'description']
         fields = ['id', 'url', 'display', 'policy', 'local_vid', 'remote_vid', 'description']
 
 
 
 
-class VLANTranslationPolicySerializer(NetBoxModelSerializer):
+class VLANTranslationPolicySerializer(PrimaryModelSerializer):
     rules = VLANTranslationRuleSerializer(many=True, read_only=True)
     rules = VLANTranslationRuleSerializer(many=True, read_only=True)
 
 
     class Meta:
     class Meta:
         model = VLANTranslationPolicy
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 7 - 7
netbox/ipam/api/serializers_/vrfs.py

@@ -1,6 +1,6 @@
 from ipam.models import RouteTarget, VRF
 from ipam.models import RouteTarget, VRF
 from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
 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
 from tenancy.api.serializers_.tenants import TenantSerializer
 
 
 __all__ = (
 __all__ = (
@@ -9,19 +9,19 @@ __all__ = (
 )
 )
 
 
 
 
-class RouteTargetSerializer(NetBoxModelSerializer):
+class RouteTargetSerializer(PrimaryModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = RouteTarget
         model = RouteTarget
         fields = [
         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',
             'custom_fields', 'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
 
 
-class VRFSerializer(NetBoxModelSerializer):
+class VRFSerializer(PrimaryModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     import_targets = SerializedPKRelatedField(
     import_targets = SerializedPKRelatedField(
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
@@ -43,8 +43,8 @@ class VRFSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = VRF
         model = VRF
         fields = [
         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')
         brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count')

+ 15 - 13
netbox/ipam/filtersets.py

@@ -11,7 +11,9 @@ from netaddr.core import AddrFormatError
 
 
 from circuits.models import Provider
 from circuits.models import Provider
 from dcim.models import Device, Interface, Region, Site, SiteGroup
 from dcim.models import Device, Interface, Region, Site, SiteGroup
-from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
+from netbox.filtersets import (
+    ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, PrimaryModelFilterSet,
+)
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 
 
 from utilities.filters import (
 from utilities.filters import (
@@ -45,7 +47,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     import_target_id = django_filters.ModelMultipleChoiceFilter(
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
@@ -83,7 +85,7 @@ class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
         fields = ('id', 'name', 'rd', 'enforce_unique', 'description')
 
 
 
 
-class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
     importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
         field_name='importing_vrfs',
         field_name='importing_vrfs',
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -149,7 +151,7 @@ class RIRFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'is_private', 'description')
         fields = ('id', 'name', 'slug', 'is_private', 'description')
 
 
 
 
-class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     family = django_filters.NumberFilter(
     family = django_filters.NumberFilter(
         field_name='prefix',
         field_name='prefix',
         lookup_expr='family'
         lookup_expr='family'
@@ -221,7 +223,7 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
         )
         )
 
 
 
 
-class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
+class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     rir_id = django_filters.ModelMultipleChoiceFilter(
     rir_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         label=_('RIR (ID)'),
         label=_('RIR (ID)'),
@@ -290,7 +292,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'description', 'weight')
         fields = ('id', 'name', 'slug', 'description', 'weight')
 
 
 
 
-class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet):
     family = django_filters.NumberFilter(
     family = django_filters.NumberFilter(
         field_name='prefix',
         field_name='prefix',
         lookup_expr='family'
         lookup_expr='family'
@@ -456,7 +458,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
         ).distinct()
         ).distinct()
 
 
 
 
-class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilterSet):
+class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     family = django_filters.NumberFilter(
     family = django_filters.NumberFilter(
         field_name='start_address',
         field_name='start_address',
         lookup_expr='family'
         lookup_expr='family'
@@ -548,7 +550,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilte
         return queryset.filter(q)
         return queryset.filter(q)
 
 
 
 
-class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
+class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     family = django_filters.NumberFilter(
     family = django_filters.NumberFilter(
         field_name='address',
         field_name='address',
         lookup_expr='family'
         lookup_expr='family'
@@ -784,7 +786,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
             )
             )
 
 
 
 
-class FHRPGroupFilterSet(NetBoxModelFilterSet):
+class FHRPGroupFilterSet(PrimaryModelFilterSet):
     protocol = django_filters.MultipleChoiceFilter(
     protocol = django_filters.MultipleChoiceFilter(
         choices=FHRPGroupProtocolChoices
         choices=FHRPGroupProtocolChoices
     )
     )
@@ -934,7 +936,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
         )
         )
 
 
 
 
-class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region',
         field_name='site__region',
@@ -1085,7 +1087,7 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         ).distinct()
         ).distinct()
 
 
 
 
-class VLANTranslationPolicyFilterSet(NetBoxModelFilterSet):
+class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = VLANTranslationPolicy
         model = VLANTranslationPolicy
@@ -1132,7 +1134,7 @@ class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class ServiceTemplateFilterSet(NetBoxModelFilterSet):
+class ServiceTemplateFilterSet(PrimaryModelFilterSet):
     port = NumericArrayFilter(
     port = NumericArrayFilter(
         field_name='ports',
         field_name='ports',
         lookup_expr='contains'
         lookup_expr='contains'
@@ -1152,7 +1154,7 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class ServiceFilterSet(ContactModelFilterSet, NetBoxModelFilterSet):
+class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet):
     parent_object_type = ContentTypeFilter()
     parent_object_type = ContentTypeFilter()
     device = MultiValueCharFilter(
     device = MultiValueCharFilter(
         method='filter_device',
         method='filter_device',

+ 17 - 103
netbox/ipam/forms/bulk_edit.py

@@ -9,11 +9,11 @@ from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.models import *
 from ipam.models import *
 from ipam.models import ASN
 from ipam.models import ASN
-from netbox.forms import NetBoxModelBulkEditForm
+from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms import add_blank_choice, get_field_value
 from utilities.forms.fields import (
 from utilities.forms.fields import (
-    CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
+    ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
     NumericRangeArrayField,
     NumericRangeArrayField,
 )
 )
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
@@ -41,7 +41,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFBulkEditForm(NetBoxModelBulkEditForm):
+class VRFBulkEditForm(PrimaryModelBulkEditForm):
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -52,12 +52,6 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
         widget=BulkEditNullBooleanSelect(),
         widget=BulkEditNullBooleanSelect(),
         label=_('Enforce unique space')
         label=_('Enforce unique space')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = VRF
     model = VRF
     fieldsets = (
     fieldsets = (
@@ -66,18 +60,12 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
 
 
-class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
+class RouteTargetBulkEditForm(PrimaryModelBulkEditForm):
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = RouteTarget
     model = RouteTarget
     fieldsets = (
     fieldsets = (
@@ -86,17 +74,12 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
 
 
-class RIRBulkEditForm(NetBoxModelBulkEditForm):
+class RIRBulkEditForm(OrganizationalModelBulkEditForm):
     is_private = forms.NullBooleanField(
     is_private = forms.NullBooleanField(
         label=_('Is private'),
         label=_('Is private'),
         required=False,
         required=False,
         widget=BulkEditNullBooleanSelect
         widget=BulkEditNullBooleanSelect
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = RIR
     model = RIR
     fieldsets = (
     fieldsets = (
@@ -105,7 +88,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('is_private', 'description')
     nullable_fields = ('is_private', 'description')
 
 
 
 
-class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
+class ASNRangeBulkEditForm(OrganizationalModelBulkEditForm):
     rir = DynamicModelChoiceField(
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         required=False,
         required=False,
@@ -116,11 +99,6 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = ASNRange
     model = ASNRange
     fieldsets = (
     fieldsets = (
@@ -129,7 +107,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class ASNBulkEditForm(NetBoxModelBulkEditForm):
+class ASNBulkEditForm(PrimaryModelBulkEditForm):
     sites = DynamicModelMultipleChoiceField(
     sites = DynamicModelMultipleChoiceField(
         label=_('Sites'),
         label=_('Sites'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -145,12 +123,6 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ASN
     model = ASN
     fieldsets = (
     fieldsets = (
@@ -159,7 +131,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('tenant', 'description', 'comments')
     nullable_fields = ('tenant', 'description', 'comments')
 
 
 
 
-class AggregateBulkEditForm(NetBoxModelBulkEditForm):
+class AggregateBulkEditForm(PrimaryModelBulkEditForm):
     rir = DynamicModelChoiceField(
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         required=False,
         required=False,
@@ -174,12 +146,6 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Date added'),
         label=_('Date added'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Aggregate
     model = Aggregate
     fieldsets = (
     fieldsets = (
@@ -188,16 +154,11 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('date_added', 'description', 'comments')
     nullable_fields = ('date_added', 'description', 'comments')
 
 
 
 
-class RoleBulkEditForm(NetBoxModelBulkEditForm):
+class RoleBulkEditForm(OrganizationalModelBulkEditForm):
     weight = forms.IntegerField(
     weight = forms.IntegerField(
         label=_('Weight'),
         label=_('Weight'),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
 
 
     model = Role
     model = Role
     fieldsets = (
     fieldsets = (
@@ -206,7 +167,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('description',)
     nullable_fields = ('description',)
 
 
 
 
-class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
+class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
     vlan_group = DynamicModelChoiceField(
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         required=False,
         required=False,
@@ -256,12 +217,6 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
         widget=BulkEditNullBooleanSelect(),
         widget=BulkEditNullBooleanSelect(),
         label=_('Treat as fully utilized')
         label=_('Treat as fully utilized')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = Prefix
     model = Prefix
     fieldsets = (
     fieldsets = (
@@ -275,7 +230,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
+class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
@@ -306,12 +261,6 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
         widget=BulkEditNullBooleanSelect(),
         widget=BulkEditNullBooleanSelect(),
         label=_('Treat as fully utilized')
         label=_('Treat as fully utilized')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = IPRange
     model = IPRange
     fieldsets = (
     fieldsets = (
@@ -322,7 +271,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
+class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
@@ -354,12 +303,6 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         required=False,
         label=_('DNS name')
         label=_('DNS name')
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = IPAddress
     model = IPAddress
     fieldsets = (
     fieldsets = (
@@ -371,7 +314,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
 
 
 
 
-class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
+class FHRPGroupBulkEditForm(PrimaryModelBulkEditForm):
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
         label=_('Protocol'),
         label=_('Protocol'),
         choices=add_blank_choice(FHRPGroupProtocolChoices),
         choices=add_blank_choice(FHRPGroupProtocolChoices),
@@ -397,12 +340,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
         max_length=100,
         max_length=100,
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = FHRPGroup
     model = FHRPGroup
     fieldsets = (
     fieldsets = (
@@ -412,12 +349,7 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
     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(
     scope_type = ContentTypeChoiceField(
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
         widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
@@ -464,7 +396,7 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
                 pass
                 pass
 
 
 
 
-class VLANBulkEditForm(NetBoxModelBulkEditForm):
+class VLANBulkEditForm(PrimaryModelBulkEditForm):
     region = DynamicModelChoiceField(
     region = DynamicModelChoiceField(
         label=_('Region'),
         label=_('Region'),
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -507,11 +439,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
     qinq_role = forms.ChoiceField(
     qinq_role = forms.ChoiceField(
         label=_('Q-in-Q role'),
         label=_('Q-in-Q role'),
         choices=add_blank_choice(VLANQinQRoleChoices),
         choices=add_blank_choice(VLANQinQRoleChoices),
@@ -525,7 +452,6 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
             'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
             'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
         }
         }
     )
     )
-    comments = CommentField()
 
 
     model = VLAN
     model = VLAN
     fieldsets = (
     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
     model = VLANTranslationPolicy
     fieldsets = (
     fieldsets = (
         FieldSet('description'),
         FieldSet('description'),
@@ -568,7 +488,7 @@ class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm):
     fields = ('policy', 'local_vid', 'remote_vid')
     fields = ('policy', 'local_vid', 'remote_vid')
 
 
 
 
-class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
+class ServiceTemplateBulkEditForm(PrimaryModelBulkEditForm):
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
         label=_('Protocol'),
         label=_('Protocol'),
         choices=add_blank_choice(ServiceProtocolChoices),
         choices=add_blank_choice(ServiceProtocolChoices),
@@ -582,12 +502,6 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
         ),
         ),
         required=False
         required=False
     )
     )
-    description = forms.CharField(
-        label=_('Description'),
-        max_length=200,
-        required=False
-    )
-    comments = CommentField()
 
 
     model = ServiceTemplate
     model = ServiceTemplate
     fieldsets = (
     fieldsets = (

+ 34 - 36
netbox/ipam/forms/bulk_import.py

@@ -7,7 +7,7 @@ from dcim.forms.mixins import ScopedImportForm
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.models import *
 from ipam.models import *
-from netbox.forms import NetBoxModelImportForm
+from netbox.forms import NetBoxModelImportForm, OrganizationalModelImportForm, PrimaryModelImportForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import (
 from utilities.forms.fields import (
     CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField,
     CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField,
@@ -36,7 +36,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFImportForm(NetBoxModelImportForm):
+class VRFImportForm(PrimaryModelImportForm):
     tenant = CSVModelChoiceField(
     tenant = CSVModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -60,12 +60,12 @@ class VRFImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = VRF
         model = VRF
         fields = (
         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(PrimaryModelImportForm):
     tenant = CSVModelChoiceField(
     tenant = CSVModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -76,18 +76,18 @@ class RouteTargetImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = RouteTarget
         model = RouteTarget
-        fields = ('name', 'tenant', 'description', 'comments', 'tags')
+        fields = ('name', 'tenant', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class RIRImportForm(NetBoxModelImportForm):
+class RIRImportForm(OrganizationalModelImportForm):
     slug = SlugField()
     slug = SlugField()
 
 
     class Meta:
     class Meta:
         model = RIR
         model = RIR
-        fields = ('name', 'slug', 'is_private', 'description', 'tags')
+        fields = ('name', 'slug', 'is_private', 'description', 'owner', 'tags')
 
 
 
 
-class AggregateImportForm(NetBoxModelImportForm):
+class AggregateImportForm(PrimaryModelImportForm):
     rir = CSVModelChoiceField(
     rir = CSVModelChoiceField(
         label=_('RIR'),
         label=_('RIR'),
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -104,10 +104,10 @@ class AggregateImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Aggregate
         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(OrganizationalModelImportForm):
     rir = CSVModelChoiceField(
     rir = CSVModelChoiceField(
         label=_('RIR'),
         label=_('RIR'),
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -124,10 +124,10 @@ class ASNRangeImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ASNRange
         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(PrimaryModelImportForm):
     rir = CSVModelChoiceField(
     rir = CSVModelChoiceField(
         label=_('RIR'),
         label=_('RIR'),
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -144,18 +144,17 @@ class ASNImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ASN
         model = ASN
-        fields = ('asn', 'rir', 'tenant', 'description', 'comments', 'tags')
+        fields = ('asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class RoleImportForm(NetBoxModelImportForm):
-    slug = SlugField()
+class RoleImportForm(OrganizationalModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Role
         model = Role
-        fields = ('name', 'slug', 'weight', 'description', 'tags')
+        fields = ('name', 'slug', 'weight', 'description', 'owner', 'tags')
 
 
 
 
-class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
+class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
     vrf = CSVModelChoiceField(
     vrf = CSVModelChoiceField(
         label=_('VRF'),
         label=_('VRF'),
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -208,7 +207,7 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
         model = Prefix
         model = Prefix
         fields = (
         fields = (
             'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_id',
             '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 = {
         labels = {
             'scope_id': _('Scope ID'),
             'scope_id': _('Scope ID'),
@@ -244,7 +243,7 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
         self.fields['vlan'].queryset = queryset
         self.fields['vlan'].queryset = queryset
 
 
 
 
-class IPRangeImportForm(NetBoxModelImportForm):
+class IPRangeImportForm(PrimaryModelImportForm):
     vrf = CSVModelChoiceField(
     vrf = CSVModelChoiceField(
         label=_('VRF'),
         label=_('VRF'),
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -276,11 +275,11 @@ class IPRangeImportForm(NetBoxModelImportForm):
         model = IPRange
         model = IPRange
         fields = (
         fields = (
             'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_populated', 'mark_utilized',
             'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'mark_populated', 'mark_utilized',
-            'description', 'comments', 'tags',
+            'description', 'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class IPAddressImportForm(NetBoxModelImportForm):
+class IPAddressImportForm(PrimaryModelImportForm):
     vrf = CSVModelChoiceField(
     vrf = CSVModelChoiceField(
         label=_('VRF'),
         label=_('VRF'),
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
@@ -349,7 +348,7 @@ class IPAddressImportForm(NetBoxModelImportForm):
         model = IPAddress
         model = IPAddress
         fields = [
         fields = [
             'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
             '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):
     def __init__(self, data=None, *args, **kwargs):
@@ -428,7 +427,7 @@ class IPAddressImportForm(NetBoxModelImportForm):
         return ipaddress
         return ipaddress
 
 
 
 
-class FHRPGroupImportForm(NetBoxModelImportForm):
+class FHRPGroupImportForm(PrimaryModelImportForm):
     protocol = CSVChoiceField(
     protocol = CSVChoiceField(
         label=_('Protocol'),
         label=_('Protocol'),
         choices=FHRPGroupProtocolChoices
         choices=FHRPGroupProtocolChoices
@@ -441,11 +440,10 @@ class FHRPGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = FHRPGroup
         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(OrganizationalModelImportForm):
     scope_type = CSVContentTypeField(
     scope_type = CSVContentTypeField(
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         required=False,
         required=False,
@@ -464,13 +462,13 @@ class VLANGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = VLANGroup
         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 = {
         labels = {
             'scope_id': 'Scope ID',
             'scope_id': 'Scope ID',
         }
         }
 
 
 
 
-class VLANImportForm(NetBoxModelImportForm):
+class VLANImportForm(PrimaryModelImportForm):
     site = CSVModelChoiceField(
     site = CSVModelChoiceField(
         label=_('Site'),
         label=_('Site'),
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -522,15 +520,15 @@ class VLANImportForm(NetBoxModelImportForm):
         model = VLAN
         model = VLAN
         fields = (
         fields = (
             'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan',
             'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'qinq_role', 'qinq_svlan',
-            'comments', 'tags',
+            'owner', 'comments', 'tags',
         )
         )
 
 
 
 
-class VLANTranslationPolicyImportForm(NetBoxModelImportForm):
+class VLANTranslationPolicyImportForm(PrimaryModelImportForm):
 
 
     class Meta:
     class Meta:
         model = VLANTranslationPolicy
         model = VLANTranslationPolicy
-        fields = ('name', 'description', 'tags')
+        fields = ('name', 'description', 'owner', 'comments', 'tags')
 
 
 
 
 class VLANTranslationRuleImportForm(NetBoxModelImportForm):
 class VLANTranslationRuleImportForm(NetBoxModelImportForm):
@@ -546,7 +544,7 @@ class VLANTranslationRuleImportForm(NetBoxModelImportForm):
         fields = ('policy', 'local_vid', 'remote_vid')
         fields = ('policy', 'local_vid', 'remote_vid')
 
 
 
 
-class ServiceTemplateImportForm(NetBoxModelImportForm):
+class ServiceTemplateImportForm(PrimaryModelImportForm):
     protocol = CSVChoiceField(
     protocol = CSVChoiceField(
         label=_('Protocol'),
         label=_('Protocol'),
         choices=ServiceProtocolChoices,
         choices=ServiceProtocolChoices,
@@ -555,10 +553,10 @@ class ServiceTemplateImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ServiceTemplate
         model = ServiceTemplate
-        fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
+        fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class ServiceImportForm(NetBoxModelImportForm):
+class ServiceImportForm(PrimaryModelImportForm):
     parent_object_type = CSVContentTypeField(
     parent_object_type = CSVContentTypeField(
         queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
         queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
         required=True,
         required=True,
@@ -590,7 +588,7 @@ class ServiceImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = Service
         model = Service
         fields = (
         fields = (
-            'ipaddresses', 'name', 'protocol', 'ports', 'description', 'comments', 'tags',
+            'ipaddresses', 'name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags',
         )
         )
 
 
     def __init__(self, data=None, *args, **kwargs):
     def __init__(self, data=None, *args, **kwargs):

+ 37 - 30
netbox/ipam/forms/filtersets.py

@@ -5,7 +5,7 @@ from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.models import *
 from ipam.models import *
-from netbox.forms import NetBoxModelFilterSetForm
+from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
@@ -42,10 +42,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
 ])
 ])
 
 
 
 
-class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = VRF
     model = VRF
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
         FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -62,10 +62,10 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = RouteTarget
     model = RouteTarget
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
         FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -82,8 +82,12 @@ class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class RIRFilterForm(NetBoxModelFilterSetForm):
+class RIRFilterForm(OrganizationalModelFilterSetForm):
     model = RIR
     model = RIR
+    fieldsets = (
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
+        FieldSet('is_private', name=_('RIR')),
+    )
     is_private = forms.NullBooleanField(
     is_private = forms.NullBooleanField(
         required=False,
         required=False,
         label=_('Private'),
         label=_('Private'),
@@ -94,10 +98,10 @@ class RIRFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
+class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
     model = Aggregate
     model = Aggregate
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('family', 'rir_id', name=_('Attributes')),
         FieldSet('family', 'rir_id', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -115,10 +119,10 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
     model = ASNRange
     model = ASNRange
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('rir_id', 'start', 'end', name=_('Range')),
         FieldSet('rir_id', 'start', 'end', name=_('Range')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -138,10 +142,10 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = ASN
     model = ASN
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
         FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
@@ -163,15 +167,18 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class RoleFilterForm(NetBoxModelFilterSetForm):
+class RoleFilterForm(OrganizationalModelFilterSetForm):
     model = Role
     model = Role
+    fieldsets = (
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm, ):
+class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
     model = Prefix
     model = Prefix
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet(
         FieldSet(
             'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
             'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized',
             name=_('Addressing')
             name=_('Addressing')
@@ -274,10 +281,10 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFil
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
+class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
     model = IPRange
     model = IPRange
     fieldsets = (
     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('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -321,10 +328,10 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFi
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm):
+class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
     model = IPAddress
     model = IPAddress
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet(
         FieldSet(
             'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
             'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
             name=_('Attributes')
             name=_('Attributes')
@@ -399,10 +406,10 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
+class FHRPGroupFilterForm(PrimaryModelFilterSetForm):
     model = FHRPGroup
     model = FHRPGroup
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
         FieldSet('name', 'protocol', 'group_id', name=_('Attributes')),
         FieldSet('auth_type', 'auth_key', name=_('Authentication')),
         FieldSet('auth_type', 'auth_key', name=_('Authentication')),
     )
     )
@@ -432,9 +439,9 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm):
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
         FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')),
         FieldSet('cluster_group', 'cluster', name=_('Cluster')),
         FieldSet('cluster_group', 'cluster', name=_('Cluster')),
         FieldSet('contains_vid', name=_('VLANs')),
         FieldSet('contains_vid', name=_('VLANs')),
@@ -485,10 +492,10 @@ class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VLANTranslationPolicyFilterForm(NetBoxModelFilterSetForm):
+class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm):
     model = VLANTranslationPolicy
     model = VLANTranslationPolicy
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('name', name=_('Attributes')),
         FieldSet('name', name=_('Attributes')),
     )
     )
     name = forms.CharField(
     name = forms.CharField(
@@ -522,10 +529,10 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm):
     )
     )
 
 
 
 
-class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
     model = VLAN
     model = VLAN
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
         FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')),
         FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
         FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')),
@@ -594,10 +601,10 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
+class ServiceTemplateFilterForm(PrimaryModelFilterSetForm):
     model = ServiceTemplate
     model = ServiceTemplate
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('protocol', 'port', name=_('Attributes')),
         FieldSet('protocol', 'port', name=_('Attributes')),
     )
     )
     protocol = forms.ChoiceField(
     protocol = forms.ChoiceField(
@@ -615,7 +622,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
 class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
 class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm):
     model = Service
     model = Service
     fieldsets = (
     fieldsets = (
-        FieldSet('q', 'filter_id', 'tag'),
+        FieldSet('q', 'filter_id', 'tag', 'owner_id'),
         FieldSet('protocol', 'port', name=_('Attributes')),
         FieldSet('protocol', 'port', name=_('Attributes')),
         FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')),
         FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),

+ 35 - 52
netbox/ipam/forms/model_forms.py

@@ -9,13 +9,13 @@ from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.formfields import IPNetworkFormField
 from ipam.formfields import IPNetworkFormField
 from ipam.models import *
 from ipam.models import *
-from netbox.forms import NetBoxModelForm
+from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.exceptions import PermissionsViolation
 from utilities.exceptions import PermissionsViolation
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
 from utilities.forms.fields import (
 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.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
 from utilities.forms.utils import get_field_value
 from utilities.forms.utils import get_field_value
@@ -49,7 +49,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFForm(TenancyForm, NetBoxModelForm):
+class VRFForm(TenancyForm, PrimaryModelForm):
     import_targets = DynamicModelMultipleChoiceField(
     import_targets = DynamicModelMultipleChoiceField(
         label=_('Import targets'),
         label=_('Import targets'),
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
@@ -60,7 +60,6 @@ class VRFForm(TenancyForm, NetBoxModelForm):
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
         required=False
         required=False
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
         FieldSet('name', 'rd', 'enforce_unique', 'description', 'tags', name=_('VRF')),
@@ -72,30 +71,27 @@ class VRFForm(TenancyForm, NetBoxModelForm):
         model = VRF
         model = VRF
         fields = [
         fields = [
             'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
             'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description',
-            'comments', 'tags',
+            'owner', 'comments', 'tags',
         ]
         ]
         labels = {
         labels = {
             'rd': "RD",
             'rd': "RD",
         }
         }
 
 
 
 
-class RouteTargetForm(TenancyForm, NetBoxModelForm):
+class RouteTargetForm(TenancyForm, PrimaryModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'description', 'tags', name=_('Route Target')),
         FieldSet('name', 'description', 'tags', name=_('Route Target')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = RouteTarget
         model = RouteTarget
         fields = [
         fields = [
-            'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
+            'name', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class RIRForm(NetBoxModelForm):
-    slug = SlugField()
-
+class RIRForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
         FieldSet('name', 'slug', 'is_private', 'description', 'tags', name=_('RIR')),
     )
     )
@@ -103,17 +99,16 @@ class RIRForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = RIR
         model = RIR
         fields = [
         fields = [
-            'name', 'slug', 'is_private', 'description', 'tags',
+            'name', 'slug', 'is_private', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class AggregateForm(TenancyForm, NetBoxModelForm):
+class AggregateForm(TenancyForm, PrimaryModelForm):
     rir = DynamicModelChoiceField(
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         label=_('RIR'),
         label=_('RIR'),
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
         FieldSet('prefix', 'rir', 'date_added', 'description', 'tags', name=_('Aggregate')),
@@ -123,20 +118,19 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Aggregate
         model = Aggregate
         fields = [
         fields = [
-            'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
+            'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'date_added': DatePicker(),
             'date_added': DatePicker(),
         }
         }
 
 
 
 
-class ASNRangeForm(TenancyForm, NetBoxModelForm):
+class ASNRangeForm(TenancyForm, OrganizationalModelForm):
     rir = DynamicModelChoiceField(
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         label=_('RIR'),
         label=_('RIR'),
         quick_add=True
         quick_add=True
     )
     )
-    slug = SlugField()
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
         FieldSet('name', 'slug', 'rir', 'start', 'end', 'description', 'tags', name=_('ASN Range')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
@@ -145,11 +139,11 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = ASNRange
         model = ASNRange
         fields = [
         fields = [
-            'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags'
+            'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'tags'
         ]
         ]
 
 
 
 
-class ASNForm(TenancyForm, NetBoxModelForm):
+class ASNForm(TenancyForm, PrimaryModelForm):
     rir = DynamicModelChoiceField(
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
         label=_('RIR'),
         label=_('RIR'),
@@ -160,7 +154,6 @@ class ASNForm(TenancyForm, NetBoxModelForm):
         label=_('Sites'),
         label=_('Sites'),
         required=False
         required=False
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
         FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
@@ -170,7 +163,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = ASN
         model = ASN
         fields = [
         fields = [
-            'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
+            'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
         ]
         ]
         widgets = {
         widgets = {
             'date_added': DatePicker(),
             'date_added': DatePicker(),
@@ -188,9 +181,7 @@ class ASNForm(TenancyForm, NetBoxModelForm):
         return instance
         return instance
 
 
 
 
-class RoleForm(NetBoxModelForm):
-    slug = SlugField()
-
+class RoleForm(OrganizationalModelForm):
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
         FieldSet('name', 'slug', 'weight', 'description', 'tags', name=_('Role')),
     )
     )
@@ -198,11 +189,11 @@ class RoleForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Role
         model = Role
         fields = [
         fields = [
-            'name', 'slug', 'weight', 'description', 'tags',
+            'name', 'slug', 'weight', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
-class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
+class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
@@ -223,7 +214,6 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
         required=False,
         required=False,
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -238,7 +228,7 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
         model = Prefix
         model = Prefix
         fields = [
         fields = [
             'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'scope_type', 'tenant_group',
             '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):
     def __init__(self, *args, **kwargs):
@@ -250,7 +240,7 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
                 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
                 self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
 
 
 
 
-class IPRangeForm(TenancyForm, NetBoxModelForm):
+class IPRangeForm(TenancyForm, PrimaryModelForm):
     vrf = DynamicModelChoiceField(
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         queryset=VRF.objects.all(),
         required=False,
         required=False,
@@ -262,7 +252,6 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         quick_add=True
         quick_add=True
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -276,11 +265,11 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
         model = IPRange
         model = IPRange
         fields = [
         fields = [
             'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
             'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
-            'mark_utilized', 'description', 'comments', 'tags',
+            'mark_utilized', 'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class IPAddressForm(TenancyForm, NetBoxModelForm):
+class IPAddressForm(TenancyForm, PrimaryModelForm):
     interface = DynamicModelChoiceField(
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         required=False,
         required=False,
@@ -324,7 +313,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         label=_('Make this the out-of-band IP for the device')
         label=_('Make this the out-of-band IP for the device')
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
         FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
@@ -344,7 +332,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         model = IPAddress
         model = IPAddress
         fields = [
         fields = [
             'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
             '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):
     def __init__(self, *args, **kwargs):
@@ -494,7 +482,7 @@ class IPAddressAssignForm(forms.Form):
     )
     )
 
 
 
 
-class FHRPGroupForm(NetBoxModelForm):
+class FHRPGroupForm(PrimaryModelForm):
 
 
     # Optionally create a new IPAddress along with the FHRPGroup
     # Optionally create a new IPAddress along with the FHRPGroup
     ip_vrf = DynamicModelChoiceField(
     ip_vrf = DynamicModelChoiceField(
@@ -511,7 +499,6 @@ class FHRPGroupForm(NetBoxModelForm):
         required=False,
         required=False,
         label=_('Status')
         label=_('Status')
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
         FieldSet('protocol', 'group_id', 'name', 'description', 'tags', name=_('FHRP Group')),
@@ -523,7 +510,7 @@ class FHRPGroupForm(NetBoxModelForm):
         model = FHRPGroup
         model = FHRPGroup
         fields = (
         fields = (
             'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
             '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):
     def save(self, *args, **kwargs):
@@ -599,8 +586,7 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
         return group
         return group
 
 
 
 
-class VLANGroupForm(TenancyForm, NetBoxModelForm):
-    slug = SlugField()
+class VLANGroupForm(TenancyForm, OrganizationalModelForm):
     vid_ranges = NumericRangeArrayField(
     vid_ranges = NumericRangeArrayField(
         label=_('VLAN IDs')
         label=_('VLAN IDs')
     )
     )
@@ -628,7 +614,7 @@ class VLANGroupForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = VLANGroup
         model = VLANGroup
         fields = [
         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):
     def __init__(self, *args, **kwargs):
@@ -662,7 +648,7 @@ class VLANGroupForm(TenancyForm, NetBoxModelForm):
         self.instance.scope = self.cleaned_data.get('scope')
         self.instance.scope = self.cleaned_data.get('scope')
 
 
 
 
-class VLANForm(TenancyForm, NetBoxModelForm):
+class VLANForm(TenancyForm, PrimaryModelForm):
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         required=False,
         required=False,
@@ -698,17 +684,16 @@ class VLANForm(TenancyForm, NetBoxModelForm):
             'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
             'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
         }
         }
     )
     )
-    comments = CommentField()
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
         fields = [
         fields = [
             'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan',
             'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan',
-            'description', 'comments', 'tags',
+            'description', 'owner', 'comments', 'tags',
         ]
         ]
 
 
 
 
-class VLANTranslationPolicyForm(NetBoxModelForm):
+class VLANTranslationPolicyForm(PrimaryModelForm):
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
         FieldSet('name', 'description', 'tags', name=_('VLAN Translation Policy')),
@@ -717,7 +702,7 @@ class VLANTranslationPolicyForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = VLANTranslationPolicy
         model = VLANTranslationPolicy
         fields = [
         fields = [
-            'name', 'description', 'tags',
+            'name', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -739,7 +724,7 @@ class VLANTranslationRuleForm(NetBoxModelForm):
         ]
         ]
 
 
 
 
-class ServiceTemplateForm(NetBoxModelForm):
+class ServiceTemplateForm(PrimaryModelForm):
     ports = NumericArrayField(
     ports = NumericArrayField(
         label=_('Ports'),
         label=_('Ports'),
         base_field=forms.IntegerField(
         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.")
         help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Application Service Template')),
         FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Application Service Template')),
@@ -756,10 +740,10 @@ class ServiceTemplateForm(NetBoxModelForm):
 
 
     class Meta:
     class Meta:
         model = ServiceTemplate
         model = ServiceTemplate
-        fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
+        fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags')
 
 
 
 
-class ServiceForm(NetBoxModelForm):
+class ServiceForm(PrimaryModelForm):
     parent_object_type = ContentTypeChoiceField(
     parent_object_type = ContentTypeChoiceField(
         queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
         queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),
         widget=HTMXSelect(),
         widget=HTMXSelect(),
@@ -786,7 +770,6 @@ class ServiceForm(NetBoxModelForm):
         required=False,
         required=False,
         label=_('IP Addresses'),
         label=_('IP Addresses'),
     )
     )
-    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
@@ -799,7 +782,7 @@ class ServiceForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Service
         model = Service
         fields = [
         fields = [
-            'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
+            'name', 'protocol', 'ports', 'ipaddresses', 'description', 'owner', 'comments', 'tags',
             'parent_object_type',
             'parent_object_type',
         ]
         ]
 
 

+ 14 - 15
netbox/ipam/graphql/types.py

@@ -8,7 +8,7 @@ from dcim.graphql.types import SiteType
 from extras.graphql.mixins import ContactsMixin
 from extras.graphql.mixins import ContactsMixin
 from ipam import models
 from ipam import models
 from netbox.graphql.scalars import BigInt
 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 .filters import *
 from .mixins import IPAddressesMixin
 from .mixins import IPAddressesMixin
 
 
@@ -74,7 +74,7 @@ class BaseIPAddressFamilyType:
     filters=ASNFilter,
     filters=ASNFilter,
     pagination=True
     pagination=True
 )
 )
-class ASNType(NetBoxObjectType, ContactsMixin):
+class ASNType(ContactsMixin, PrimaryObjectType):
     asn: BigInt
     asn: BigInt
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -89,7 +89,7 @@ class ASNType(NetBoxObjectType, ContactsMixin):
     filters=ASNRangeFilter,
     filters=ASNRangeFilter,
     pagination=True
     pagination=True
 )
 )
-class ASNRangeType(NetBoxObjectType):
+class ASNRangeType(OrganizationalObjectType):
     start: BigInt
     start: BigInt
     end: BigInt
     end: BigInt
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
@@ -102,7 +102,7 @@ class ASNRangeType(NetBoxObjectType):
     filters=AggregateFilter,
     filters=AggregateFilter,
     pagination=True
     pagination=True
 )
 )
-class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
+class AggregateType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
     prefix: str
     prefix: str
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
     rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -114,8 +114,7 @@ class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
     filters=FHRPGroupFilter,
     filters=FHRPGroupFilter,
     pagination=True
     pagination=True
 )
 )
-class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
-
+class FHRPGroupType(IPAddressesMixin, PrimaryObjectType):
     fhrpgroupassignment_set: List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]
     fhrpgroupassignment_set: List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]
 
 
 
 
@@ -142,7 +141,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
     filters=IPAddressFilter,
     filters=IPAddressFilter,
     pagination=True
     pagination=True
 )
 )
-class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
+class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
     address: str
     address: str
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -167,7 +166,7 @@ class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
     filters=IPRangeFilter,
     filters=IPRangeFilter,
     pagination=True
     pagination=True
 )
 )
-class IPRangeType(NetBoxObjectType, ContactsMixin):
+class IPRangeType(ContactsMixin, PrimaryObjectType):
     start_address: str
     start_address: str
     end_address: str
     end_address: str
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
@@ -181,7 +180,7 @@ class IPRangeType(NetBoxObjectType, ContactsMixin):
     filters=PrefixFilter,
     filters=PrefixFilter,
     pagination=True
     pagination=True
 )
 )
-class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
+class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
     prefix: str
     prefix: str
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
     vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -230,7 +229,7 @@ class RoleType(OrganizationalObjectType):
     filters=RouteTargetFilter,
     filters=RouteTargetFilter,
     pagination=True
     pagination=True
 )
 )
-class RouteTargetType(NetBoxObjectType):
+class RouteTargetType(PrimaryObjectType):
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
 
 
     importing_l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]
     importing_l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]]
@@ -245,7 +244,7 @@ class RouteTargetType(NetBoxObjectType):
     filters=ServiceFilter,
     filters=ServiceFilter,
     pagination=True
     pagination=True
 )
 )
-class ServiceType(NetBoxObjectType, ContactsMixin):
+class ServiceType(ContactsMixin, PrimaryObjectType):
     ports: List[int]
     ports: List[int]
     ipaddresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
     ipaddresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
 
 
@@ -264,7 +263,7 @@ class ServiceType(NetBoxObjectType, ContactsMixin):
     filters=ServiceTemplateFilter,
     filters=ServiceTemplateFilter,
     pagination=True
     pagination=True
 )
 )
-class ServiceTemplateType(NetBoxObjectType):
+class ServiceTemplateType(PrimaryObjectType):
     ports: List[int]
     ports: List[int]
 
 
 
 
@@ -274,7 +273,7 @@ class ServiceTemplateType(NetBoxObjectType):
     filters=VLANFilter,
     filters=VLANFilter,
     pagination=True
     pagination=True
 )
 )
-class VLANType(NetBoxObjectType):
+class VLANType(PrimaryObjectType):
     site: Annotated["SiteType", strawberry.lazy('ipam.graphql.types')] | None
     site: Annotated["SiteType", strawberry.lazy('ipam.graphql.types')] | None
     group: Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')] | None
     group: Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -323,7 +322,7 @@ class VLANGroupType(OrganizationalObjectType):
     filters=VLANTranslationPolicyFilter,
     filters=VLANTranslationPolicyFilter,
     pagination=True
     pagination=True
 )
 )
-class VLANTranslationPolicyType(NetBoxObjectType):
+class VLANTranslationPolicyType(PrimaryObjectType):
     rules: List[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]]
     rules: List[Annotated["VLANTranslationRuleType", strawberry.lazy('ipam.graphql.types')]]
 
 
 
 
@@ -346,7 +345,7 @@ class VLANTranslationRuleType(NetBoxObjectType):
     filters=VRFFilter,
     filters=VRFFilter,
     pagination=True
     pagination=True
 )
 )
-class VRFType(NetBoxObjectType):
+class VRFType(PrimaryObjectType):
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
     tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
 
 
     interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
     interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]

+ 124 - 0
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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='asn',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='fhrpgroup',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='iprange',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rir',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='routetarget',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='servicetemplate',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='vlangroup',
+            name='owner',
+            field=models.ForeignKey(
+                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, to='users.owner'
+            ),
+        ),
+        migrations.AddField(
+            model_name='vrf',
+            name='owner',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='users.owner'
+            ),
+        ),
+    ]

+ 5 - 8
netbox/ipam/tables/asn.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from ipam.models import *
 from ipam.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import OrganizationalModelTable, PrimaryModelTable, columns
 from tenancy.tables import TenancyColumnsMixin
 from tenancy.tables import TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
@@ -11,7 +11,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
+class ASNRangeTable(TenancyColumnsMixin, OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -27,7 +27,7 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('ASNs')
         verbose_name=_('ASNs')
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = ASNRange
         model = ASNRange
         fields = (
         fields = (
             'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
             'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
@@ -36,7 +36,7 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
         default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description')
         default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description')
 
 
 
 
-class ASNTable(TenancyColumnsMixin, NetBoxTable):
+class ASNTable(TenancyColumnsMixin, PrimaryModelTable):
     asn = tables.Column(
     asn = tables.Column(
         verbose_name=_('ASN'),
         verbose_name=_('ASN'),
         linkify=True
         linkify=True
@@ -65,14 +65,11 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
         linkify_item=True,
         linkify_item=True,
         verbose_name=_('Sites')
         verbose_name=_('Sites')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:asn_list'
         url_name='ipam:asn_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ASN
         model = ASN
         fields = (
         fields = (
             'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
             'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',

+ 4 - 7
netbox/ipam/tables/fhrp.py

@@ -1,8 +1,8 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 
 
 from ipam.models import *
 from ipam.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import NetBoxTable, PrimaryModelTable, columns
 
 
 __all__ = (
 __all__ = (
     'FHRPGroupTable',
     'FHRPGroupTable',
@@ -17,7 +17,7 @@ IPADDRESSES = """
 """
 """
 
 
 
 
-class FHRPGroupTable(NetBoxTable):
+class FHRPGroupTable(PrimaryModelTable):
     group_id = tables.Column(
     group_id = tables.Column(
         verbose_name=_('Group ID'),
         verbose_name=_('Group ID'),
         linkify=True
         linkify=True
@@ -30,9 +30,6 @@ class FHRPGroupTable(NetBoxTable):
     member_count = tables.Column(
     member_count = tables.Column(
         verbose_name=_('Members')
         verbose_name=_('Members')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:fhrpgroup_list'
         url_name='ipam:fhrpgroup_list'
     )
     )
@@ -40,7 +37,7 @@ class FHRPGroupTable(NetBoxTable):
     def value_ip_addresses(self, value):
     def value_ip_addresses(self, value):
         return ",".join([str(obj.address) for obj in value.all()])
         return ",".join([str(obj.address) for obj in value.all()])
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = FHRPGroup
         model = FHRPGroup
         fields = (
         fields = (
             'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'comments', 'ip_addresses',
             'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'comments', 'ip_addresses',

+ 14 - 26
netbox/ipam/tables/ip.py

@@ -1,10 +1,10 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from ipam.models import *
 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 tenancy.tables import TenancyColumnsMixin, TenantColumn
 from .template_code import *
 from .template_code import *
 
 
@@ -27,7 +27,7 @@ AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span
 # RIRs
 # RIRs
 #
 #
 
 
-class RIRTable(NetBoxTable):
+class RIRTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -45,7 +45,7 @@ class RIRTable(NetBoxTable):
         url_name='ipam:rir_list'
         url_name='ipam:rir_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = RIR
         model = RIR
         fields = (
         fields = (
             'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'created',
             'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'created',
@@ -58,7 +58,7 @@ class RIRTable(NetBoxTable):
 # Aggregates
 # Aggregates
 #
 #
 
 
-class AggregateTable(TenancyColumnsMixin, NetBoxTable):
+class AggregateTable(TenancyColumnsMixin, PrimaryModelTable):
     prefix = tables.Column(
     prefix = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name=_('Aggregate'),
         verbose_name=_('Aggregate'),
@@ -79,9 +79,6 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
         accessor='get_utilization',
         accessor='get_utilization',
         orderable=False
         orderable=False
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:aggregate_list'
         url_name='ipam:aggregate_list'
     )
     )
@@ -89,7 +86,7 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
         extra_buttons=AGGREGATE_COPY_BUTTON
         extra_buttons=AGGREGATE_COPY_BUTTON
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Aggregate
         model = Aggregate
         fields = (
         fields = (
             'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added',
             'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added',
@@ -102,7 +99,7 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
 # Roles
 # Roles
 #
 #
 
 
-class RoleTable(NetBoxTable):
+class RoleTable(OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -126,7 +123,7 @@ class RoleTable(NetBoxTable):
         url_name='ipam:role_list'
         url_name='ipam:role_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = Role
         model = Role
         fields = (
         fields = (
             'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight', 'tags',
             '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(
     prefix = columns.TemplateColumn(
         verbose_name=_('Prefix'),
         verbose_name=_('Prefix'),
         template_code=PREFIX_LINK_WITH_DEPTH,
         template_code=PREFIX_LINK_WITH_DEPTH,
@@ -223,9 +220,6 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
         accessor='get_utilization',
         accessor='get_utilization',
         orderable=False
         orderable=False
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:prefix_list'
         url_name='ipam:prefix_list'
     )
     )
@@ -233,7 +227,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
         extra_buttons=PREFIX_COPY_BUTTON
         extra_buttons=PREFIX_COPY_BUTTON
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Prefix
         model = Prefix
         fields = (
         fields = (
             'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
             'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
@@ -252,7 +246,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
 #
 #
 # IP ranges
 # IP ranges
 #
 #
-class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
+class IPRangeTable(TenancyColumnsMixin, PrimaryModelTable):
     start_address = tables.Column(
     start_address = tables.Column(
         verbose_name=_('Start address'),
         verbose_name=_('Start address'),
         linkify=True
         linkify=True
@@ -282,14 +276,11 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
         accessor='utilization',
         accessor='utilization',
         orderable=False
         orderable=False
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:iprange_list'
         url_name='ipam:iprange_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = IPRange
         model = IPRange
         fields = (
         fields = (
             'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
             'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
@@ -308,7 +299,7 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
 # IPAddresses
 # IPAddresses
 #
 #
 
 
-class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
+class IPAddressTable(TenancyColumnsMixin, PrimaryModelTable):
     address = tables.TemplateColumn(
     address = tables.TemplateColumn(
         template_code=IPADDRESS_LINK,
         template_code=IPADDRESS_LINK,
         verbose_name=_('IP Address')
         verbose_name=_('IP Address')
@@ -351,9 +342,6 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Assigned'),
         verbose_name=_('Assigned'),
         false_mark=None
         false_mark=None
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:ipaddress_list'
         url_name='ipam:ipaddress_list'
     )
     )
@@ -361,7 +349,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
         extra_buttons=IPADDRESS_COPY_BUTTON
         extra_buttons=IPADDRESS_COPY_BUTTON
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = IPAddress
         model = IPAddress
         fields = (
         fields = (
             'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
             'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',

+ 6 - 12
netbox/ipam/tables/services.py

@@ -1,8 +1,8 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 
 
 from ipam.models import *
 from ipam.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import PrimaryModelTable, columns
 
 
 __all__ = (
 __all__ = (
     'ServiceTable',
     'ServiceTable',
@@ -10,7 +10,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ServiceTemplateTable(NetBoxTable):
+class ServiceTemplateTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -20,14 +20,11 @@ class ServiceTemplateTable(NetBoxTable):
         accessor=tables.A('port_list'),
         accessor=tables.A('port_list'),
         order_by=tables.A('ports'),
         order_by=tables.A('ports'),
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:servicetemplate_list'
         url_name='ipam:servicetemplate_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = ServiceTemplate
         model = ServiceTemplate
         fields = (
         fields = (
             'pk', 'id', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'created', 'last_updated',
             'pk', 'id', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'created', 'last_updated',
@@ -35,7 +32,7 @@ class ServiceTemplateTable(NetBoxTable):
         default_columns = ('pk', 'name', 'protocol', 'ports', 'description')
         default_columns = ('pk', 'name', 'protocol', 'ports', 'description')
 
 
 
 
-class ServiceTable(NetBoxTable):
+class ServiceTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -50,14 +47,11 @@ class ServiceTable(NetBoxTable):
         accessor=tables.A('port_list'),
         accessor=tables.A('port_list'),
         order_by=tables.A('ports'),
         order_by=tables.A('ports'),
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:service_list'
         url_name='ipam:service_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = Service
         model = Service
         fields = (
         fields = (
             'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
             'pk', 'id', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',

+ 7 - 10
netbox/ipam/tables/vlans.py

@@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import *
 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 tenancy.tables import TenancyColumnsMixin, TenantColumn
 from virtualization.models import VMInterface
 from virtualization.models import VMInterface
 from .template_code import *
 from .template_code import *
@@ -28,7 +28,7 @@ AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span
 # VLAN groups
 # VLAN groups
 #
 #
 
 
-class VLANGroupTable(TenancyColumnsMixin, NetBoxTable):
+class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -62,7 +62,7 @@ class VLANGroupTable(TenancyColumnsMixin, NetBoxTable):
         extra_buttons=VLANGROUP_BUTTONS
         extra_buttons=VLANGROUP_BUTTONS
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(OrganizationalModelTable.Meta):
         model = VLANGroup
         model = VLANGroup
         fields = (
         fields = (
             'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
             'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
@@ -77,7 +77,7 @@ class VLANGroupTable(TenancyColumnsMixin, NetBoxTable):
 # VLANs
 # VLANs
 #
 #
 
 
-class VLANTable(TenancyColumnsMixin, NetBoxTable):
+class VLANTable(TenancyColumnsMixin, PrimaryModelTable):
     vid = tables.TemplateColumn(
     vid = tables.TemplateColumn(
         template_code=VLAN_LINK,
         template_code=VLAN_LINK,
         verbose_name=_('VID')
         verbose_name=_('VID')
@@ -120,14 +120,11 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
         orderable=False,
         orderable=False,
         verbose_name=_('Prefixes')
         verbose_name=_('Prefixes')
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:vlan_list'
         url_name='ipam:vlan_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = VLAN
         model = VLAN
         fields = (
         fields = (
             'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
             'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
@@ -229,7 +226,7 @@ class InterfaceVLANTable(NetBoxTable):
 # VLAN Translation
 # VLAN Translation
 #
 #
 
 
-class VLANTranslationPolicyTable(NetBoxTable):
+class VLANTranslationPolicyTable(PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -246,7 +243,7 @@ class VLANTranslationPolicyTable(NetBoxTable):
         url_name='ipam:vlantranslationpolicy_list'
         url_name='ipam:vlantranslationpolicy_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = VLANTranslationPolicy
         model = VLANTranslationPolicy
         fields = (
         fields = (
             'pk', 'id', 'name', 'rule_count', 'description', 'tags', 'created', 'last_updated',
             'pk', 'id', 'name', 'rule_count', 'description', 'tags', 'created', 'last_updated',

+ 6 - 12
netbox/ipam/tables/vrfs.py

@@ -1,8 +1,8 @@
-from django.utils.translation import gettext_lazy as _
 import django_tables2 as tables
 import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
 
 
 from ipam.models import *
 from ipam.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import PrimaryModelTable, columns
 from tenancy.tables import TenancyColumnsMixin
 from tenancy.tables import TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
@@ -21,7 +21,7 @@ VRF_TARGETS = """
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFTable(TenancyColumnsMixin, NetBoxTable):
+class VRFTable(TenancyColumnsMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -43,14 +43,11 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
         template_code=VRF_TARGETS,
         template_code=VRF_TARGETS,
         orderable=False
         orderable=False
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:vrf_list'
         url_name='ipam:vrf_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = VRF
         model = VRF
         fields = (
         fields = (
             'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'import_targets', 'export_targets',
             'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'import_targets', 'export_targets',
@@ -63,19 +60,16 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
 # Route targets
 # Route targets
 #
 #
 
 
-class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
+class RouteTargetTable(TenancyColumnsMixin, PrimaryModelTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
     )
     )
-    comments = columns.MarkdownColumn(
-        verbose_name=_('Comments'),
-    )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:routetarget_list'
         url_name='ipam:routetarget_list'
     )
     )
 
 
-    class Meta(NetBoxTable.Meta):
+    class Meta(PrimaryModelTable.Meta):
         model = RouteTarget
         model = RouteTarget
         fields = (
         fields = (
             'pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
             'pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',

+ 2 - 29
netbox/netbox/api/serializers/__init__.py

@@ -1,33 +1,6 @@
-from rest_framework import serializers
-
 from .base import *
 from .base import *
 from .features import *
 from .features import *
 from .generic import *
 from .generic import *
 from .nested 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 *

+ 11 - 0
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()

+ 14 - 0
netbox/netbox/api/serializers/features.py

@@ -2,11 +2,13 @@ from rest_framework import serializers
 from rest_framework.fields import CreateOnlyDefault
 from rest_framework.fields import CreateOnlyDefault
 
 
 from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
 from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
+from .base import ValidatedModelSerializer
 from .nested import NestedTagSerializer
 from .nested import NestedTagSerializer
 
 
 __all__ = (
 __all__ = (
     'ChangeLogMessageSerializer',
     'ChangeLogMessageSerializer',
     'CustomFieldModelSerializer',
     'CustomFieldModelSerializer',
+    'NetBoxModelSerializer',
     'TaggableModelSerializer',
     'TaggableModelSerializer',
 )
 )
 
 
@@ -76,3 +78,15 @@ class ChangeLogMessageSerializer(serializers.Serializer):
         if self.instance is not None:
         if self.instance is not None:
             self.instance._changelog_message = self.validated_data.get('changelog_message')
             self.instance._changelog_message = self.validated_data.get('changelog_message')
         return super().save(**kwargs)
         return super().save(**kwargs)
+
+
+class NetBoxModelSerializer(
+    ChangeLogMessageSerializer,
+    TaggableModelSerializer,
+    CustomFieldModelSerializer,
+    ValidatedModelSerializer
+):
+    """
+    Adds support for custom fields and tags.
+    """
+    pass

+ 31 - 0
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

+ 14 - 4
netbox/netbox/filtersets.py

@@ -14,6 +14,7 @@ from core.models import ObjectChange
 from extras.choices import CustomFieldFilterLogicChoices
 from extras.choices import CustomFieldFilterLogicChoices
 from extras.filters import TagFilter, TagIDFilter
 from extras.filters import TagFilter, TagIDFilter
 from extras.models import CustomField, SavedFilter
 from extras.models import CustomField, SavedFilter
+from users.filterset_mixins import OwnerFilterMixin
 from utilities.constants import (
 from utilities.constants import (
     FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
     FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
     FILTER_NUMERIC_BASED_LOOKUP_MAP
     FILTER_NUMERIC_BASED_LOOKUP_MAP
@@ -25,8 +26,10 @@ __all__ = (
     'AttributeFiltersMixin',
     'AttributeFiltersMixin',
     'BaseFilterSet',
     'BaseFilterSet',
     'ChangeLoggedModelFilterSet',
     'ChangeLoggedModelFilterSet',
+    'NestedGroupModelFilterSet',
     'NetBoxModelFilterSet',
     'NetBoxModelFilterSet',
     'OrganizationalModelFilterSet',
     'OrganizationalModelFilterSet',
+    'PrimaryModelFilterSet',
 )
 )
 
 
 STANDARD_LOOKUPS = (
 STANDARD_LOOKUPS = (
@@ -328,9 +331,16 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
         return queryset
         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):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():
@@ -342,9 +352,9 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
         )
         )
 
 
 
 
-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):
     def search(self, queryset, name, value):
         if value.strip():
         if value.strip():

+ 5 - 57
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 *

+ 0 - 178
netbox/netbox/forms/base.py

@@ -1,178 +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 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
-
-__all__ = (
-    'NetBoxModelForm',
-    'NetBoxModelImportForm',
-    'NetBoxModelBulkEditForm',
-    'NetBoxModelFilterSetForm',
-)
-
-
-class NetBoxModelForm(ChangelogMessageMixin, CheckLastUpdatedMixin, CustomFieldsMixin, 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 a NetBox objects from CSV data. Used for bulk importing.
-    """
-    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, 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_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)
-
-
-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')
-    )
-
-    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 نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است