Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
53ee617
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
a5c8d7c
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
162ab54
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
f63eb57
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
89ff967
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
099e6ce
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
f4e5c58
#21488 - Replace MPTT wtih PostgreSQL Ltree
arthanson May 27, 2026
8926313
backwards compatability for NestedGroupModel
arthanson May 30, 2026
4b80b59
backwards compatability for NestedGroupModel
arthanson May 30, 2026
a4140cd
merge feature
arthanson Jun 2, 2026
0208621
fix natural sort for modules
arthanson Jun 2, 2026
4a722d1
fixes
arthanson Jun 2, 2026
62afbcf
fixes
arthanson Jun 2, 2026
ef0b044
fixes
arthanson Jun 2, 2026
40a1d54
fixes
arthanson Jun 2, 2026
c6ef4ac
fixes
arthanson Jun 2, 2026
dca2dea
cleanup
arthanson Jun 2, 2026
90888ef
fix concurrency issue
arthanson Jun 2, 2026
87c88d8
fixes
arthanson Jun 2, 2026
33038a6
fixes
arthanson Jun 3, 2026
4f2a2b2
fixes
arthanson Jun 3, 2026
f8cd61c
fixes
arthanson Jun 3, 2026
a0d1d59
fixes
arthanson Jun 3, 2026
4a15446
cleanup
arthanson Jun 3, 2026
056da80
cleanup
arthanson Jun 3, 2026
d87052e
cleanup
arthanson Jun 3, 2026
3e4ac0f
Merge branch 'feature' into 21488-ltree
arthanson Jun 3, 2026
a82c8dc
merge feature
arthanson Jun 3, 2026
1757d96
fix ordering on module bays
arthanson Jun 4, 2026
d1461b0
update ordering
arthanson Jun 4, 2026
38c27d9
fix inventory item ordering
arthanson Jun 4, 2026
36ffd8d
cleanup
arthanson Jun 5, 2026
79ea4e0
review changes and refactor
arthanson Jun 8, 2026
8e319a5
refactor
arthanson Jun 8, 2026
d89a712
review feedback
arthanson Jun 10, 2026
493f571
review feedback
arthanson Jun 10, 2026
1ca3da1
review feedback
arthanson Jun 10, 2026
0c6aa43
review feedback
arthanson Jun 10, 2026
c618e07
review feedback
arthanson Jun 10, 2026
449b5f2
review feedback
arthanson Jun 10, 2026
eb47b3f
put back MPTT into generic view
arthanson Jun 10, 2026
bba6a22
fix circular import
arthanson Jun 10, 2026
b36d830
fix circular import
arthanson Jun 10, 2026
f5949ca
update locks
arthanson Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions base_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ django-graphiql-debug-toolbar
django-htmx

# Modified Preorder Tree Traversal (recursive nesting of objects)
# Retained primarily for plugin backward compatibility: the deprecated
# NestedGroupModel base remains MPTT-backed for plugins still using it. Also
# required by historical migrations that pre-date the switch to PostgreSQL ltree.
# NetBox core runtime uses netbox.models.ltree.LtreeModel instead.
django-mptt

# Context managers for PostgreSQL advisory locks
Expand Down
18 changes: 15 additions & 3 deletions netbox/core/models/change_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel

from core.choices import ObjectChangeActionChoices
from core.querysets import ObjectChangeQuerySet
Expand Down Expand Up @@ -160,13 +159,26 @@ def diff_exclude_fields(self):
model = self.changed_object_type.model_class()
attrs = set()

# model_class() returns None when the model's app is no longer installed
# (e.g. a removed plugin); there are no fields to exclude, and the
# issubclass() checks below would raise TypeError on None.
if model is None:
return attrs

# Exclude auto-populated change tracking fields
if issubclass(model, ChangeLoggingMixin):
attrs.update({'created', 'last_updated'})

# Exclude MPTT-internal fields
# Exclude trigger-maintained ltree columns (path and the optional sort_path)
from netbox.models.ltree import LtreeModel
if issubclass(model, LtreeModel):
attrs.update({'path', 'sort_path'})

# Exclude MPTT bookkeeping columns for the deprecated MPTT-backed
# NestedGroupModel still shipped for plugin compatibility.
from mptt.models import MPTTModel
if issubclass(model, MPTTModel):
attrs.update({'level', 'lft', 'rght', 'tree_id'})
attrs.update({'lft', 'rght', 'tree_id', 'level'})

return attrs

Expand Down
14 changes: 7 additions & 7 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import MPTTLockedMixin, NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
from utilities.api import get_serializer_for_model
from utilities.query import count_related
Expand Down Expand Up @@ -95,7 +95,7 @@ def paths(self, request, pk):
# Regions
#

class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class RegionViewSet(NetBoxModelViewSet):
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
Expand All @@ -111,7 +111,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# Site groups
#

class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class SiteGroupViewSet(NetBoxModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
Expand All @@ -137,7 +137,7 @@ class SiteViewSet(NetBoxModelViewSet):
# Locations
#

class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class LocationViewSet(NetBoxModelViewSet):
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Expand Down Expand Up @@ -356,7 +356,7 @@ class DeviceBayTemplateViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceBayTemplateFilterSet


class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class InventoryItemTemplateViewSet(NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.all()
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
Expand Down Expand Up @@ -388,7 +388,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
# Platforms
#

class PlatformViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class PlatformViewSet(NetBoxModelViewSet):
queryset = Platform.objects.add_related_count(
Platform.objects.add_related_count(
Platform.objects.all(),
Expand Down Expand Up @@ -543,7 +543,7 @@ class DeviceBayViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceBayFilterSet


class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class InventoryItemViewSet(NetBoxModelViewSet):
queryset = InventoryItem.objects.all()
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
Expand Down
35 changes: 18 additions & 17 deletions netbox/dcim/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import (
BaseObjectType,
NestedGroupObjectType,
LtreeNodeMixin,
NestedLtreeGroupObjectType,
NetBoxObjectType,
OrganizationalObjectType,
PrimaryObjectType,
Expand Down Expand Up @@ -322,11 +323,11 @@ class DeviceBayTemplateType(ComponentTemplateType):

@strawberry_django.type(
models.InventoryItemTemplate,
exclude=['component_type', 'component_id', 'parent'],
exclude=['component_type', 'component_id', 'parent', 'path'],
filters=InventoryItemTemplateFilter,
pagination=True
)
class InventoryItemTemplateType(ComponentTemplateType):
class InventoryItemTemplateType(LtreeNodeMixin, ComponentTemplateType):
role: Annotated['InventoryItemRoleType', strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated['ManufacturerType', strawberry.lazy('dcim.graphql.types')]

Expand All @@ -350,11 +351,11 @@ def parent(self) -> Annotated['InventoryItemTemplateType', strawberry.lazy('dcim

@strawberry_django.type(
models.DeviceRole,
fields='__all__',
exclude=['path', 'sort_path'],
filters=DeviceRoleFilter,
pagination=True
)
class DeviceRoleType(NestedGroupObjectType):
class DeviceRoleType(NestedLtreeGroupObjectType):
parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None
children: list[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]]
color: str
Expand Down Expand Up @@ -487,11 +488,11 @@ class InterfaceTemplateType(ModularComponentTemplateType):

@strawberry_django.type(
models.InventoryItem,
exclude=['component_type', 'component_id', 'parent'],
exclude=['component_type', 'component_id', 'parent', 'path'],
filters=InventoryItemFilter,
pagination=True
)
class InventoryItemType(ComponentType):
class InventoryItemType(LtreeNodeMixin, ComponentType):
role: Annotated['InventoryItemRoleType', strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated['ManufacturerType', strawberry.lazy('dcim.graphql.types')] | None

Expand Down Expand Up @@ -529,11 +530,11 @@ class InventoryItemRoleType(OrganizationalObjectType):
@strawberry_django.type(
models.Location,
# fields='__all__',
exclude=['parent'], # bug - temp
exclude=['parent', 'path', 'sort_path'], # bug - temp
filters=LocationFilter,
pagination=True
)
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NestedGroupObjectType):
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NestedLtreeGroupObjectType):
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
parent: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
Expand Down Expand Up @@ -602,11 +603,11 @@ class ModuleType(PrimaryObjectType):
@strawberry_django.type(
models.ModuleBay,
# fields='__all__',
exclude=['parent'],
exclude=['parent', 'path', 'sort_path'],
filters=ModuleBayFilter,
pagination=True
)
class ModuleBayType(ModularComponentType):
class ModuleBayType(LtreeNodeMixin, ModularComponentType):

installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None
children: list[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]
Expand Down Expand Up @@ -667,11 +668,11 @@ class ModuleTypeType(PrimaryObjectType):

@strawberry_django.type(
models.Platform,
fields='__all__',
exclude=['path', 'sort_path'],
filters=PlatformFilter,
pagination=True
)
class PlatformType(NestedGroupObjectType):
class PlatformType(NestedLtreeGroupObjectType):
parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None
children: list[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]]
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
Expand Down Expand Up @@ -875,11 +876,11 @@ class RearPortTemplateType(ModularComponentTemplateType):

@strawberry_django.type(
models.Region,
exclude=['parent'],
exclude=['parent', 'path', 'sort_path'],
filters=RegionFilter,
pagination=True
)
class RegionType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType):
class RegionType(VLANGroupsMixin, ContactsMixin, NestedLtreeGroupObjectType):

sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
children: list[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]]
Expand Down Expand Up @@ -952,11 +953,11 @@ def circuit_terminations(self) -> list[

@strawberry_django.type(
models.SiteGroup,
exclude=['parent'], # bug - temp
exclude=['parent', 'path', 'sort_path'], # bug - temp
filters=SiteGroupFilter,
pagination=True
)
class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType):
class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedLtreeGroupObjectType):

sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]]
children: list[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]]
Expand Down
Loading