Sfoglia il codice sorgente

backwards compatability for NestedGroupModel

Arthur 4 ore fa
parent
commit
8926313c43

+ 3 - 3
netbox/dcim/models/devices.py

@@ -24,7 +24,7 @@ from extras.models import ConfigContextModel, CustomField
 from extras.querysets import ConfigContextModelQuerySet
 from netbox.choices import ColorChoices
 from netbox.config import ConfigItem
-from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
+from netbox.models import NestedLtreeGroupModel, OrganizationalModel, PrimaryModel
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.mixins import WeightMixin
 from utilities.exceptions import AbortRequest
@@ -386,7 +386,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
 # Devices
 #
 
-class DeviceRole(NestedGroupModel):
+class DeviceRole(NestedLtreeGroupModel):
     """
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
     color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
@@ -443,7 +443,7 @@ class DeviceRole(NestedGroupModel):
         verbose_name_plural = _('device roles')
 
 
-class Platform(NestedGroupModel):
+class Platform(NestedLtreeGroupModel):
     """
     Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". A
     Platform may optionally be associated with a particular Manufacturer.

+ 4 - 4
netbox/dcim/models/sites.py

@@ -10,7 +10,7 @@ from timezone_field import TimeZoneField
 
 from dcim.choices import *
 from dcim.constants import *
-from netbox.models import NestedGroupModel, PrimaryModel
+from netbox.models import NestedLtreeGroupModel, PrimaryModel
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 
 __all__ = (
@@ -25,7 +25,7 @@ __all__ = (
 # Regions
 #
 
-class Region(ContactsMixin, NestedGroupModel):
+class Region(ContactsMixin, NestedLtreeGroupModel):
     """
     A region represents a geographic collection of sites. For example, you might create regions representing countries,
     states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
@@ -86,7 +86,7 @@ class Region(ContactsMixin, NestedGroupModel):
 # Site groups
 #
 
-class SiteGroup(ContactsMixin, NestedGroupModel):
+class SiteGroup(ContactsMixin, NestedLtreeGroupModel):
     """
     A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
     within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
@@ -278,7 +278,7 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
 # Locations
 #
 
-class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
+class Location(ContactsMixin, ImageAttachmentsMixin, NestedLtreeGroupModel):
     """
     A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
     site, or a room within a building, for example.

+ 65 - 26
netbox/netbox/models/__init__.py

@@ -5,16 +5,20 @@ from django.core.validators import ValidationError
 from django.db import models
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
+from mptt.models import MPTTModel, TreeForeignKey
 
 from netbox.models.features import *
 from netbox.models.ltree import LtreeManager, LtreeModel
 from netbox.models.mixins import OwnerMixin
+from utilities.mptt import TreeManager
 from utilities.querysets import RestrictedQuerySet
 
 __all__ = (
     'AdminModel',
     'ChangeLoggedModel',
     'NestedGroupModel',
+    'NestedGroupModelMixin',
+    'NestedLtreeGroupModel',
     'NetBoxModel',
     'OrganizationalModel',
     'PrimaryModel',
@@ -158,24 +162,11 @@ class PrimaryModel(OwnerMixin, NetBoxModel):
         abstract = True
 
 
-class NestedGroupModel(OwnerMixin, NetBoxModel, LtreeModel):
+class NestedGroupModelMixin(OwnerMixin, NetBoxModel):
     """
-    Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
-    recursively using PostgreSQL ltree. Within each parent, each child instance must have a unique name.
-
-    `sort_path` is a trigger-maintained text column that mirrors MPTT's `order_insertion_by=('name',)`
-    semantics: at insert and reparent time it is set to a chr(1)-separated chain of ancestor names.
-    Ordering by `sort_path` yields tree-flatten output with siblings in their column's collation order.
-    Renaming a node does NOT update `sort_path` (matching MPTT behavior).
+    Shared field set and behavior for hierarchical group models. Concrete bases supply the
+    tree backend (MPTT or ltree) and the corresponding `parent` ForeignKey / manager.
     """
-    parent = models.ForeignKey(
-        to='self',
-        on_delete=models.CASCADE,
-        related_name='children',
-        blank=True,
-        null=True,
-        db_index=True
-    )
     name = models.CharField(
         verbose_name=_('name'),
         max_length=100
@@ -193,19 +184,9 @@ class NestedGroupModel(OwnerMixin, NetBoxModel, LtreeModel):
         verbose_name=_('comments'),
         blank=True
     )
-    sort_path = models.TextField(
-        editable=False,
-        blank=True,
-        default='',
-    )
-
-    # Re-declare so the LtreeManager wins over BaseModel's RestrictedQuerySet
-    # default manager via MRO resolution.
-    objects = LtreeManager()
 
     class Meta:
         abstract = True
-        ordering = ('sort_path',)
 
     def __str__(self):
         return self.name
@@ -220,6 +201,64 @@ class NestedGroupModel(OwnerMixin, NetBoxModel, LtreeModel):
             })
 
 
+class NestedGroupModel(NestedGroupModelMixin, MPTTModel):
+    """
+    Deprecated MPTT-backed nested group base, retained for backwards compatibility with plugins.
+
+    New code (in NetBox core and in plugins) should use `NestedLtreeGroupModel` instead. This
+    class will be removed in a future release once the deprecation period has elapsed.
+    """
+    parent = TreeForeignKey(
+        to='self',
+        on_delete=models.CASCADE,
+        related_name='children',
+        blank=True,
+        null=True,
+        db_index=True
+    )
+
+    objects = TreeManager()
+
+    class Meta:
+        abstract = True
+
+    class MPTTMeta:
+        order_insertion_by = ('name',)
+
+
+class NestedLtreeGroupModel(NestedGroupModelMixin, LtreeModel):
+    """
+    Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
+    recursively using PostgreSQL ltree. Within each parent, each child instance must have a unique name.
+
+    `sort_path` is a trigger-maintained text column that mirrors MPTT's `order_insertion_by=('name',)`
+    semantics: at insert and reparent time it is set to a chr(1)-separated chain of ancestor names.
+    Ordering by `sort_path` yields tree-flatten output with siblings in their column's collation order.
+    Renaming a node does NOT update `sort_path` (matching MPTT behavior).
+    """
+    parent = models.ForeignKey(
+        to='self',
+        on_delete=models.CASCADE,
+        related_name='children',
+        blank=True,
+        null=True,
+        db_index=True
+    )
+    sort_path = models.TextField(
+        editable=False,
+        blank=True,
+        default='',
+    )
+
+    # Re-declare so the LtreeManager wins over BaseModel's RestrictedQuerySet
+    # default manager via MRO resolution.
+    objects = LtreeManager()
+
+    class Meta:
+        abstract = True
+        ordering = ('sort_path',)
+
+
 class OrganizationalModel(OwnerMixin, NetBoxModel):
     """
     Organizational models are those which are used solely to categorize and qualify other objects, and do not convey

+ 5 - 2
netbox/netbox/models/features.py

@@ -408,13 +408,16 @@ class ContactsMixin(models.Model):
         """
         from tenancy.models import ContactAssignment
 
-        from . import NestedGroupModel
+        # TODO: Once the deprecated MPTT-backed NestedGroupModel is removed, narrow this
+        #  check to NestedLtreeGroupModel (or drop the mixin alias). NestedGroupModelMixin
+        #  exists only so both legacy MPTT and new ltree bases satisfy the inheritance check.
+        from . import NestedGroupModelMixin
 
         filter = Q(
             object_type=ObjectType.objects.get_for_model(self),
             object_id__in=(
                 self.get_ancestors(include_self=True)
-                if (isinstance(self, NestedGroupModel) and inherited)
+                if (isinstance(self, NestedGroupModelMixin) and inherited)
                 else [self.pk]
             ),
         )

+ 9 - 9
netbox/netbox/tests/test_base_classes.py

@@ -44,7 +44,7 @@ from netbox.graphql.types import (
     OrganizationalObjectType,
     PrimaryObjectType,
 )
-from netbox.models import NestedGroupModel, NetBoxModel, OrganizationalModel, PrimaryModel
+from netbox.models import NestedGroupModelMixin, NetBoxModel, OrganizationalModel, PrimaryModel
 from netbox.registry import registry
 from netbox.tables import (
     NestedGroupModelTable,
@@ -76,7 +76,7 @@ class FormClassesTestCase(TestCase):
             return PrimaryModelForm
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelForm
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelForm
         if issubclass(model, NetBoxModel):
             return NetBoxModelForm
@@ -93,7 +93,7 @@ class FormClassesTestCase(TestCase):
             return PrimaryModelBulkEditForm
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelBulkEditForm
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelBulkEditForm
         if issubclass(model, NetBoxModel):
             return NetBoxModelBulkEditForm
@@ -110,7 +110,7 @@ class FormClassesTestCase(TestCase):
             return PrimaryModelImportForm
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelImportForm
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelImportForm
         if issubclass(model, NetBoxModel):
             return NetBoxModelImportForm
@@ -127,7 +127,7 @@ class FormClassesTestCase(TestCase):
             return PrimaryModelFilterSetForm
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelFilterSetForm
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelFilterSetForm
         if issubclass(model, NetBoxModel):
             return NetBoxModelFilterSetForm
@@ -191,7 +191,7 @@ class FilterSetClassesTestCase(TestCase):
             return PrimaryModelFilterSet
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelFilterSet
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelFilterSet
         if issubclass(model, NetBoxModel):
             return NetBoxModelFilterSet
@@ -233,7 +233,7 @@ class TableClassesTestCase(TestCase):
             return PrimaryModelTable
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelTable
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelTable
         if issubclass(model, NetBoxModel):
             return NetBoxTable
@@ -278,7 +278,7 @@ class SerializerClassesTestCase(TestCase):
             return PrimaryModelSerializer
         if issubclass(model, OrganizationalModel):
             return OrganizationalModelSerializer
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupModelSerializer
         if issubclass(model, NetBoxModel):
             return NetBoxModelSerializer
@@ -319,7 +319,7 @@ class GraphQLTypeClassesTestCase(TestCase):
             return PrimaryObjectType
         if issubclass(model, OrganizationalModel):
             return OrganizationalObjectType
-        if issubclass(model, NestedGroupModel):
+        if issubclass(model, NestedGroupModelMixin):
             return NestedGroupObjectType
         if issubclass(model, NetBoxModel):
             return NetBoxObjectType

+ 2 - 2
netbox/tenancy/models/contacts.py

@@ -6,7 +6,7 @@ from django.db.models.expressions import RawSQL
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
-from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
+from netbox.models import ChangeLoggedModel, NestedLtreeGroupModel, OrganizationalModel, PrimaryModel
 from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, has_feature
 from netbox.models.ltree import LtreeManager
 from tenancy.choices import *
@@ -40,7 +40,7 @@ class ContactGroupManager(LtreeManager):
         )
 
 
-class ContactGroup(NestedGroupModel):
+class ContactGroup(NestedLtreeGroupModel):
     """
     An arbitrary collection of Contacts.
     """

+ 2 - 2
netbox/tenancy/models/tenants.py

@@ -3,7 +3,7 @@ from django.db import models
 from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 
-from netbox.models import NestedGroupModel, PrimaryModel
+from netbox.models import NestedLtreeGroupModel, PrimaryModel
 from netbox.models.features import ContactsMixin
 
 __all__ = (
@@ -12,7 +12,7 @@ __all__ = (
 )
 
 
-class TenantGroup(NestedGroupModel):
+class TenantGroup(NestedLtreeGroupModel):
     """
     An arbitrary collection of Tenants.
     """

+ 24 - 0
netbox/utilities/mptt.py

@@ -0,0 +1,24 @@
+from mptt.managers import TreeManager as TreeManager_
+from mptt.querysets import TreeQuerySet as TreeQuerySet_
+
+from django.db.models import Manager
+from .querysets import RestrictedQuerySet
+
+__all__ = (
+    'TreeManager',
+    'TreeQuerySet',
+)
+
+
+class TreeQuerySet(TreeQuerySet_, RestrictedQuerySet):
+    """
+    Mate django-mptt's TreeQuerySet with our RestrictedQuerySet for permissions enforcement.
+    """
+    pass
+
+
+class TreeManager(Manager.from_queryset(TreeQuerySet), TreeManager_):
+    """
+    Extend django-mptt's TreeManager to incorporate RestrictedQuerySet().
+    """
+    pass

+ 2 - 2
netbox/wireless/models.py

@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from dcim.models.mixins import CachedScopeMixin
-from netbox.models import NestedGroupModel, PrimaryModel
+from netbox.models import NestedLtreeGroupModel, PrimaryModel
 from netbox.models.mixins import DistanceMixin
 
 from .choices import *
@@ -47,7 +47,7 @@ class WirelessAuthenticationBase(models.Model):
         abstract = True
 
 
-class WirelessLANGroup(NestedGroupModel):
+class WirelessLANGroup(NestedLtreeGroupModel):
     """
     A nested grouping of WirelessLANs
     """