Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
78994a6
NPL-374 Add Mixin Support and make CustomObject class ABC (#110)
arthanson Jul 22, 2025
b9cb8d8
First batch of tests (test_models.py) running
bctiemann Jul 23, 2025
6fff0b6
#77 Fix rename of MultiObject field (#117)
arthanson Jul 23, 2025
11ce9f5
NPL-427 fix custom object detail view header (#116)
arthanson Jul 23, 2025
5d2485c
Uncomment disabled tests
bctiemann Jul 23, 2025
d0b7876
Remove validation test
bctiemann Jul 23, 2025
94f10db
Add all field types to COT model objects
bctiemann Jul 23, 2025
3d353d2
Add CI linting/tests
bctiemann Jul 24, 2025
69803b2
Remove mkdocs step
bctiemann Jul 24, 2025
1ca04d1
Add ruff config
bctiemann Jul 24, 2025
2fc611a
Ruff fixes
bctiemann Jul 24, 2025
77d4967
Fix tests
bctiemann Jul 24, 2025
48aa2eb
Fix tests
bctiemann Jul 24, 2025
bef64ff
First batch of tests (test_models.py) running
bctiemann Jul 23, 2025
e30e733
Uncomment disabled tests
bctiemann Jul 23, 2025
5241826
Remove validation test
bctiemann Jul 23, 2025
8281b99
Add all field types to COT model objects
bctiemann Jul 23, 2025
8413258
Add CI linting/tests
bctiemann Jul 24, 2025
ac05d59
Remove mkdocs step
bctiemann Jul 24, 2025
563d990
Add ruff config
bctiemann Jul 24, 2025
4a50ca1
Ruff fixes
bctiemann Jul 24, 2025
f805537
Fix tests
bctiemann Jul 24, 2025
8379757
Fix tests
bctiemann Jul 24, 2025
67fc8a1
Merge remote-tracking branch 'origin/unit-test-suite' into unit-test-…
bctiemann Jul 24, 2025
9c29ebe
Merge branch 'feature' into unit-test-suite
bctiemann Jul 24, 2025
81a1c7f
#86 add bulk edit, import to CustomObjectType
arthanson Jul 24, 2025
a7dc444
Rebase on feature
bctiemann Jul 24, 2025
9119dda
Fix base fields count
bctiemann Jul 24, 2025
dc75fca
Skip test_custom_object_type_delete_removes_table
bctiemann Jul 24, 2025
573cc7f
Ruff fixes
bctiemann Jul 24, 2025
c0ee2ad
Merge pull request #118 from netboxlabs/unit-test-suite
bctiemann Jul 24, 2025
5a7cefc
#86 add filter form
arthanson Jul 24, 2025
cf74ccb
#86 fix merge conflicts
arthanson Jul 24, 2025
c3bf8a1
Merge pull request #119 from netboxlabs/86-bulk-edit
bctiemann Jul 24, 2025
a49ee81
105 fix bulk edit for object and multi-object fields
arthanson Jul 24, 2025
7f5b106
35 fix group name on edit form
arthanson Jul 24, 2025
0eaca97
take into account weight
arthanson Jul 24, 2025
1c32d0e
take into account weight
arthanson Jul 24, 2025
1d54924
Merge pull request #120 from netboxlabs/105-bulk-edit
bctiemann Jul 25, 2025
446240d
Merge pull request #121 from netboxlabs/35-group-name
bctiemann Jul 25, 2025
663f08c
Add test_field_types.py
bctiemann Jul 27, 2025
a341db3
Silence ruff errors
bctiemann Jul 27, 2025
6b6664d
Populate test instance with all fields
bctiemann Jul 27, 2025
5e16cb7
Fix "custom_fields" and "custom_field_groups" attr names on CO edit f…
bctiemann Jul 28, 2025
4c60678
Merge pull request #123 from netboxlabs/unit-test-suite
bctiemann Jul 28, 2025
c0cb294
Add test_views.py
bctiemann Jul 29, 2025
814d091
Ruff fixes
bctiemann Jul 29, 2025
ba0a566
cleanup urls (#128)
arthanson Jul 31, 2025
27fa949
NPL-432 Add changes for ObjectType (#125)
arthanson Jul 31, 2025
f65f3ed
Remove Custom Object table on COT detail page and add Edit label on C…
bctiemann Aug 3, 2025
0282693
Merge branch 'refs/heads/feature' into unit-test-suite
bctiemann Aug 3, 2025
bd51b6a
Target feature branch for tests
bctiemann Aug 3, 2025
177d141
Comment out get_models
bctiemann Aug 3, 2025
c820dc4
Remove unused import
bctiemann Aug 4, 2025
3c74f71
Change ContentType refs to ObjectType
bctiemann Aug 4, 2025
fd73073
Change ContentType refs to ObjectType
bctiemann Aug 4, 2025
d982d6e
Fix get_models for testing
arthanson Aug 4, 2025
46fa739
ignore warnings
arthanson Aug 4, 2025
320b8c7
cleanup
arthanson Aug 4, 2025
5bd261e
Merge pull request #126 from netboxlabs/unit-test-suite
bctiemann Aug 4, 2025
6d1004a
Merge branch 'feature' into fix-testing
bctiemann Aug 4, 2025
70fb4c1
Merge pull request #134 from netboxlabs/fix-testing
bctiemann Aug 4, 2025
eef7696
Merge branch 'refs/heads/feature' into remove-inline-object-table
bctiemann Aug 4, 2025
7f79cce
Allow custom fields to point to custom objects (#131)
arthanson Aug 4, 2025
9c83a56
Merge pull request #133 from netboxlabs/remove-inline-object-table
bctiemann Aug 4, 2025
e7d5778
Synthetic SearchIndex classes for CustomObjectTypes
bctiemann Aug 5, 2025
5afcaa5
Revert "Synthetic SearchIndex classes for CustomObjectTypes"
bctiemann Aug 5, 2025
4a665a1
NPL-440 fix buttons on custom object list view
arthanson Aug 6, 2025
ad0472d
Merge pull request #139 from netboxlabs/NPL-440-action-buttons
bctiemann Aug 6, 2025
2cb9ad0
NPL-286 add get_action_url override
arthanson Aug 7, 2025
518b33a
NPL-386 add journaling tab to CustomObjectDetail
arthanson Aug 4, 2025
effc1ab
NPL-286 merge other branch changes
arthanson Aug 4, 2025
6d4f0f4
NPL-286 merge other branch changes
arthanson Aug 4, 2025
975c810
NPL-286 update journal url
arthanson Aug 7, 2025
0a9721d
NPL-286 update journal url
arthanson Aug 8, 2025
39e6941
Fix CustomObjectObjectType inheritance
bctiemann Aug 8, 2025
fd30299
NPL-286 remove _get_viewname
arthanson Aug 8, 2025
3a5a21f
NPL-286 _get_viewname
arthanson Aug 8, 2025
7f36b87
Merge pull request #142 from netboxlabs/NPL-386-journal-2
bctiemann Aug 11, 2025
2e26e72
NPL-415 fix detail page when custom object links is null
arthanson Aug 11, 2025
9c77792
NPL-415 fix detail page when custom object links is null
arthanson Aug 11, 2025
0872201
NPL-364 Add grouping of fields on CO detail view
arthanson Aug 12, 2025
31349fd
NPL-364 Add grouping of fields on CO detail view
arthanson Aug 12, 2025
e00c22f
Handle deletion of all linked ObjectChanges for CustomObjects when de…
bctiemann Aug 12, 2025
d743839
Merge pull request #143 from netboxlabs/NPL-415-location
bctiemann Aug 12, 2025
30bd7d2
Merge pull request #144 from netboxlabs/NPL-364-group-name
bctiemann Aug 12, 2025
e102c99
NPL-373 fix changelog registration
arthanson Aug 12, 2025
d029321
NPL-373 fix changelog registration
arthanson Aug 12, 2025
8507290
Handle deletion of ObjectType cleanly by disconnecting pre_delete han…
bctiemann Aug 13, 2025
2b04436
fix recursion
arthanson Aug 13, 2025
bdbd76a
fix recursion
arthanson Aug 13, 2025
78fb3f3
NPL-247 fix custom fields referencing a custom object
arthanson Aug 14, 2025
8b9ffa2
add todo to comment
arthanson Aug 14, 2025
b32ed25
Merge pull request #153 from netboxlabs/NPL-247-custom-fields2
bctiemann Aug 15, 2025
7231f9f
Merge pull request #146 from netboxlabs/NPL-373-changelog
bctiemann Aug 15, 2025
d7105fc
Revert "NPL-373 fix changelog registration"
bctiemann Aug 15, 2025
2baf8bf
Merge pull request #154 from netboxlabs/revert-146-NPL-373-changelog
bctiemann Aug 15, 2025
4f8e771
Change prints to logger.debug
bctiemann Aug 15, 2025
6d4a198
Return "Custom Object" as view name (label) for DRF browseable API br…
bctiemann Aug 15, 2025
1582796
Cleanup warning messages
arthanson Aug 15, 2025
0992a51
Cleanup warning messages
arthanson Aug 15, 2025
d680218
Cleanup warning messages
arthanson Aug 15, 2025
f675d30
Cleanup warning messages
arthanson Aug 15, 2025
29f8b10
Cleanup warning messages
arthanson Aug 15, 2025
bdad2c9
Add TODO to clean up disconnect/reconnect of signal handler
bctiemann Aug 15, 2025
04988ae
Merge pull request #145 from netboxlabs/delete-custom_object_type-obj…
bctiemann Aug 15, 2025
3391f6d
Merge pull request #155 from netboxlabs/107-remove-warnings
bctiemann Aug 15, 2025
2b9f2b7
Synthetic SearchIndex classes for custom objects (#137)
bctiemann Aug 15, 2025
af477b0
Remove import button from CO list view (#157)
arthanson Aug 18, 2025
c65d985
Squash migrations (#158)
bctiemann Aug 18, 2025
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
91 changes: 91 additions & 0 deletions .github/workflows/lint-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Lint and tests
on:
workflow_dispatch:
pull_request:
push:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

permissions:
contents: write
checks: write
pull-requests: write

jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .
pip install .[dev]
pip install .[test]
- name: Run ruff
run: ruff check
tests:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: [ "3.10", "3.11", "3.12" ]
services:
redis:
image: redis
ports:
- 6379:6379
postgres:
image: postgres
env:
POSTGRES_USER: netbox
POSTGRES_PASSWORD: netbox
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout netbox-custom-objects
uses: actions/checkout@v4
with:
path: netbox-custom-objects
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Checkout netbox
uses: actions/checkout@v4
with:
repository: "netbox-community/netbox"
path: netbox
ref: feature
- name: Install netbox-custom-objects
working-directory: netbox-custom-objects
run: |
# Include tests directory for test
sed -i 's/exclude-package-data/#exclude-package-data/g' pyproject.toml
python -m pip install --upgrade pip
pip install .
pip install .[test]
- name: Install dependencies & configure plugin
working-directory: netbox
run: |
ln -s $(pwd)/../netbox-custom-objects/testing/configuration.py netbox/netbox/configuration.py

python -m pip install --upgrade pip
pip install -r requirements.txt -U
- name: Run tests
working-directory: netbox
run: |
python netbox/manage.py test netbox_custom_objects.tests --keepdb
169 changes: 143 additions & 26 deletions netbox_custom_objects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,159 @@
import sys
import warnings

from django.core.exceptions import AppRegistryNotReady
from django.db import transaction
from django.db.utils import DatabaseError, OperationalError, ProgrammingError
from netbox.plugins import PluginConfig


def is_running_migration():
"""
Check if the code is currently running during a Django migration.
"""
# Check if 'makemigrations' or 'migrate' command is in sys.argv
if any(cmd in sys.argv for cmd in ["makemigrations", "migrate"]):
return True

return False


def is_in_clear_cache():
"""
Check if the code is currently being called from Django's clear_cache() method.

TODO: This is fairly ugly, but in models.CustomObjectType.get_model() we call
meta = type() which calls clear_cache on the model which causes a call to
get_models() which in-turn calls get_model and therefore recurses.

This catches the specific case of a recursive call to get_models() from
clear_cache() which is the only case we care about, so should be relatively
safe. An alternative should be found for this.
"""
import inspect

frame = inspect.currentframe()
try:
# Walk up the call stack to see if we're being called from clear_cache
while frame:
if (
frame.f_code.co_name == "clear_cache"
and "django/apps/registry.py" in frame.f_code.co_filename
):
return True
frame = frame.f_back
return False
finally:
# Clean up the frame reference
del frame


def check_custom_object_type_table_exists():
"""
Check if the CustomObjectType table exists in the database.
Returns True if the table exists, False otherwise.
"""
from .models import CustomObjectType

try:
# Try to query the model - if the table doesn't exist, this will raise an exception
# this check and the transaction.atomic() is only required when running tests as the
# migration check doesn't work correctly in the test environment
with transaction.atomic():
# Force immediate execution by using first()
CustomObjectType.objects.first()
return True
except (OperationalError, ProgrammingError, DatabaseError):
# Catch database-specific errors (table doesn't exist, permission issues, etc.)
return False


# Plugin Configuration
class CustomObjectsPluginConfig(PluginConfig):
name = "netbox_custom_objects"
verbose_name = "Custom Objects"
description = "A plugin to manage custom objects in NetBox"
version = "0.1.0"
version = "0.2.0"
base_url = "custom-objects"
min_version = "4.2.0"
min_version = "4.4.0"
default_settings = {}
required_settings = []
template_extensions = "template_content.template_extensions"
_in_get_models = False # Recursion guard

def get_model(self, model_name, require_ready=True):
try:
# if the model is already loaded, return it
return super().get_model(model_name, require_ready)
except LookupError:
try:
self.apps.check_apps_ready()
except AppRegistryNotReady:
raise

# only do database calls if we are sure the app is ready to avoid
# Django warnings
if "table" not in model_name.lower() or "model" not in model_name.lower():
raise LookupError(
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
)

from .models import CustomObjectType

custom_object_type_id = int(
model_name.replace("table", "").replace("model", "")
)

try:
obj = CustomObjectType.objects.get(pk=custom_object_type_id)
except CustomObjectType.DoesNotExist:
raise LookupError(
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
)

return obj.get_model()

def get_models(self, include_auto_created=False, include_swapped=False):
"""Return all models for this plugin, including custom object type models."""

# Get the regular Django models first
for model in super().get_models(include_auto_created, include_swapped):
yield model

# Prevent recursion
if self._in_get_models and is_in_clear_cache():
# Skip dynamic model creation if we're in a recursive get_models call
return

self._in_get_models = True
try:
# Suppress warnings about database calls during model loading
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=RuntimeWarning, message=".*database.*"
)
warnings.filterwarnings(
"ignore", category=UserWarning, message=".*database.*"
)

# Skip custom object type model loading if running during migration
if (
is_running_migration()
or not check_custom_object_type_table_exists()
):
return

# Add custom object type models
from .models import CustomObjectType

# def get_model(self, model_name, require_ready=True):
# if require_ready:
# self.apps.check_models_ready()
# else:
# self.apps.check_apps_ready()
#
# if model_name.lower() in self.models:
# return self.models[model_name.lower()]
#
# from .models import CustomObjectType
# if "table" not in model_name.lower() or "model" not in model_name.lower():
# raise LookupError(
# "App '%s' doesn't have a '%s' model." % (self.label, model_name)
# )
#
# custom_object_type_id = int(model_name.replace("table", "").replace("model", ""))
#
# try:
# obj = CustomObjectType.objects.get(pk=custom_object_type_id)
# except CustomObjectType.DoesNotExist:
# raise LookupError(
# "App '%s' doesn't have a '%s' model." % (self.label, model_name)
# )
# return obj.get_model()
custom_object_types = CustomObjectType.objects.all()
for custom_type in custom_object_types:
model = custom_type.get_model()
if model:
yield model
finally:
# Clean up the recursion guard
self._in_get_models = False


config = CustomObjectsPluginConfig
47 changes: 38 additions & 9 deletions netbox_custom_objects/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import logging
import sys

from core.models import ObjectType
from django.contrib.contenttypes.models import ContentType
from extras.choices import CustomFieldTypeChoices
from netbox.api.serializers import NetBoxModelSerializer
Expand All @@ -6,7 +10,11 @@
from rest_framework.reverse import reverse

from netbox_custom_objects import field_types
from netbox_custom_objects.models import CustomObject, CustomObjectType, CustomObjectTypeField
from netbox_custom_objects.models import (CustomObject, CustomObjectType,
CustomObjectTypeField)

logger = logging.getLogger('netbox_custom_objects.api.serializers')


__all__ = (
"CustomObjectTypeSerializer",
Expand Down Expand Up @@ -59,10 +67,10 @@ def validate(self, attrs):
CustomFieldTypeChoices.TYPE_MULTIOBJECT,
]:
try:
attrs["related_object_type"] = ContentType.objects.get(
attrs["related_object_type"] = ObjectType.objects.get(
app_label=app_label, model=model
)
except ContentType.DoesNotExist:
except ObjectType.DoesNotExist:
raise ValidationError(
"Must provide valid app_label and model for object field type."
)
Expand Down Expand Up @@ -197,17 +205,24 @@ def get_field_data(self, obj):

def get_serializer_class(model):
model_fields = model.custom_object_type.fields.all()

# Create field list including all necessary fields
base_fields = ["id", "url", "display", "created", "last_updated", "tags"]
custom_field_names = [field.name for field in model_fields]
all_fields = base_fields + custom_field_names

meta = type(
"Meta",
(),
{
"model": model,
"fields": "__all__",
"fields": all_fields,
"brief_fields": ("id", "url", "display"),
},
)

def get_url(self, obj):
# Unsaved objects will not yet have a valid URL.
"""Generate the API URL for this object"""
if hasattr(obj, "pk") and obj.pk in (None, ""):
return None

Expand All @@ -221,24 +236,38 @@ def get_url(self, obj):
format = self.context.get("format")
return reverse(view_name, kwargs=kwargs, request=request, format=format)

def get_display(self, obj):
"""Get display representation of the object"""
return str(obj)

# Create basic attributes for the serializer
attrs = {
"Meta": meta,
"__module__": "database.serializers",
"__module__": "netbox_custom_objects.api.serializers",
"url": serializers.SerializerMethodField(),
"get_url": get_url,
"display": serializers.SerializerMethodField(),
"get_display": get_display,
}

for field in model_fields:
field_type = field_types.FIELD_TYPE_CLASS[field.type]()
try:
attrs[field.name] = field_type.get_serializer_field(field)
except NotImplementedError:
print(f"serializer: {field.name} field is not implemented; using a default serializer field")
logger.debug(
"serializer: {} field is not implemented; using a default serializer field".format(field.name)
)

serializer_name = f"{model._meta.object_name}Serializer"
serializer = type(
f"{model._meta.object_name}Serializer",
(serializers.ModelSerializer,),
serializer_name,
(NetBoxModelSerializer,),
attrs,
)

# Register the serializer in the current module so NetBox can find it
current_module = sys.modules[__name__]
setattr(current_module, serializer_name, serializer)

return serializer
Loading