Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b33075f
Merge pull request #608 from netboxlabs/feature
bctiemann Jul 6, 2026
015051d
Closes #268: Add display_expression for composite custom object names…
bctiemann Jul 7, 2026
cb2f121
Fixes #595: SET CONSTRAINTS before poly column removal on revert (#596)
bctiemann Jul 8, 2026
5bc4f11
Fixes #614: try netboxlabs-netbox-branching dist name in version chec…
bctiemann Jul 8, 2026
c7a6965
Closes #583: Add branching CI matrix job and test_branching.py (#591)
bctiemann Jul 9, 2026
d26e324
avoid loading entire table into memory when opening bulk import/edit/…
arthanson Jul 14, 2026
8b2b568
add tests
arthanson Jul 14, 2026
2161180
Merge pull request #622 from netboxlabs/630-perf
bctiemann Jul 15, 2026
1c198f1
Closes #566: Add missing collectstatic step and CustomObjectType mode…
bctiemann Jul 15, 2026
65fa232
Closes #549: Surface Custom Objects in Jinja config templates (#624)
bctiemann Jul 18, 2026
6e76e96
Fix bulk import failing on required Hidden/Read-only fields (#630)
bctiemann Jul 21, 2026
b9a8989
Fixes #628: Restore `?id=` filtering on the Custom Object REST API (#…
bctiemann Jul 22, 2026
e85421f
Fixes #625: schema_id (and related bookkeeping fields) nulled by unre…
bctiemann Jul 22, 2026
fa8c373
Closes: #621 - Add "Set null" to bulk edit for all real, nullable fie…
bctiemann Jul 22, 2026
7292407
Fixes #640: Serialize polymorphic multiobject through-model creation …
bctiemann Aug 7, 2026
43c9134
Move #640 concurrency tests to test_schema_operations.py and trim com…
bctiemann Aug 7, 2026
78c7f37
Fixes #649: Refresh query_counts.json baseline for current NetBox main
bctiemann Aug 7, 2026
ad7956a
Merge pull request #650 from netboxlabs/649-refresh-query-count-basel…
bctiemann Aug 11, 2026
8d27f7e
Merge branch 'main' into 640-fix-polymorphic-multiobject-delete-race
bctiemann Aug 11, 2026
fc492cf
Reduce forced-timeout duration in the #640 concurrency test
bctiemann Aug 11, 2026
5d2456c
Fixes #639: Respect filter_logic in generated filtersets (#644)
bctiemann Aug 12, 2026
1c9e6b2
Fixes #647: heal mixin fields unmasked by renaming/deleting a collidi…
bctiemann Aug 12, 2026
145feb3
Address round-2 review comments from Martin on PR #648
bctiemann Aug 14, 2026
2092cf7
Merge remote-tracking branch 'origin/main' into 640-fix-polymorphic-m…
bctiemann Aug 14, 2026
77fa2ee
Narrow CustomObjectType._global_lock to avoid a real deadlock (PR #648)
bctiemann Aug 14, 2026
afce6bf
Confirm compatibility with NetBox v4.7
jnovinger Aug 14, 2026
7a47878
Update remaining #640 references to #658; shorten a now-inaccurate te…
bctiemann Aug 14, 2026
725e866
Merge pull request #648 from netboxlabs/640-fix-polymorphic-multiobje…
bctiemann Aug 14, 2026
d8ae8fa
Merge pull request #659 from netboxlabs/netbox-v4.7-compatibility
bctiemann Aug 17, 2026
c637e15
Closes #661: Fix CustomObjectTypeField.from_db() for Django 6.1 compa…
bctiemann Aug 17, 2026
06acdd4
Closes #645: Omit hidden fields from custom object create/edit and bu…
bctiemann Aug 18, 2026
141ad95
Merge remote-tracking branch 'origin/main' into 666-merge-main-into-f…
bctiemann Aug 19, 2026
d3bc3af
Fix issues surfaced by the main merge: circular import, one behaviora…
bctiemann Aug 19, 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
45 changes: 41 additions & 4 deletions .github/workflows/lint-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,25 @@ jobs:
pip install .[test]
- name: Run ruff
run: ruff check
- name: Build in-app model documentation
run: |
mkdocs build -f mkdocs.models.yml
test -f netbox_custom_objects/static/docs/models/netbox_custom_objects/customobjecttype/index.html
tests:
name: tests (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
netbox-ref: [ "main", "feature" ]
include:
- netbox-ref: "main"
name: "main"
- netbox-ref: "feature"
name: "feature"
- netbox-ref: "main"
name: "main, branching"
with-branching: true
services:
redis:
image: redis
Expand Down Expand Up @@ -80,13 +92,38 @@ jobs:
pip install .
pip install .[test]
- name: Install dependencies & configure plugin
if: ${{ !matrix.with-branching }}
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
pip install -r requirements.txt
- name: Install dependencies & configure plugin (with branching)
if: ${{ matrix.with-branching == true }}
working-directory: netbox
run: |
ln -s $(pwd)/../netbox-custom-objects/testing/configuration_branching.py netbox/netbox/configuration.py
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install 'netboxlabs-netbox-branching>=1.0.4,<2.0.0'
- name: Tune PostgreSQL for branching test performance
if: ${{ matrix.with-branching == true }}
run: |
sudo apt-get install -y -q postgresql-client
PGPASSWORD=netbox psql -h localhost -U netbox \
-c "ALTER SYSTEM SET checkpoint_timeout = '30min'" \
-c "ALTER SYSTEM SET max_wal_size = '2GB'" \
-c "ALTER SYSTEM SET synchronous_commit = off" \
-c "ALTER SYSTEM SET lock_timeout = '60s'" \
-c "SELECT pg_reload_conf()"
- name: Run tests
if: ${{ matrix.with-branching != true }}
working-directory: netbox
run: |
python netbox/manage.py test netbox_custom_objects.tests --keepdb --verbosity=2

- name: Run tests (with branching)
if: ${{ matrix.with-branching == true }}
working-directory: netbox
run: |
python netbox/manage.py test netbox_custom_objects.tests --keepdb
python netbox/manage.py test netbox_custom_objects.tests.test_branching --keepdb --verbosity=2
7 changes: 7 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ jobs:
- name: Install pypa/build
run: |
python3 -m pip install build
- name: Install mkdocs
run: |
python3 -m pip install 'mkdocs>=1.6,<2' 'mkdocs-material>=9.7,<10'
- name: Build in-app model documentation
run: |
mkdocs build -f mkdocs.models.yml
test -f netbox_custom_objects/static/docs/models/netbox_custom_objects/customobjecttype/index.html
- name: Build distribution package
run: |
python3 -m build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.claude/settings.local.json
.idea/
.DS_Store
netbox_custom_objects/static/
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ There is no Justfile/Makefile in this repo; commands are raw. Run tests inside a
| `python netbox/manage.py makemigrations netbox_custom_objects` | Generate Django migrations after model changes |
| `python netbox/manage.py migrate` | Apply migrations |
| `python netbox/manage.py runserver` | Start NetBox locally with the plugin loaded |
| `mkdocs build -f mkdocs.models.yml` | Build the in-app "Help" model documentation pages into `netbox_custom_objects/static/docs/models/` (run before `collectstatic`; the release workflow does this automatically before packaging) |

## Development

Expand Down Expand Up @@ -218,6 +219,7 @@ GitHub Actions workflows in `.github/workflows/`:
3. Wire up the rest of the surface area: `filtersets.py`, `forms.py`, `tables.py`, `api/serializers.py`, `api/urls.py`, `urls.py`, `navigation.py`, and a template under `templates/netbox_custom_objects/`.
4. Register a `SearchIndex` in `search.py` if the model should appear in NetBox's global search.
5. Add tests covering model logic, API, filtersets, and views.
6. If the model is a `NetBoxModel` (it has a "Help" link on its edit page via `docs_url`), add `docs_models/netbox_custom_objects/<model_name>.md` and a nav entry in `mkdocs.models.yml`, or the Help link will 404.

### Add a REST API endpoint

Expand Down
1 change: 1 addition & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Release | Minimum NetBox Version | Maximum NetBox Version |
|---------|------------------------|------------------------|
| unreleased | 4.5.2 | 4.7.x |
| 0.6.x | 4.5.2 | 4.6.x |
| 0.5.2 | 4.5.2 | 4.6.x |
| 0.5.1 | 4.5.2 | 4.6.x |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ PLUGINS = [
]
```

3. Run NetBox migrations:
3. Run NetBox migrations and collect static files:

```
$ ./manage.py migrate
$ ./manage.py collectstatic
```

4. Restart NetBox
Expand Down
36 changes: 36 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,42 @@ Notes:
- A field must be an object field with both the **name and target model** above; a mis-named or mis-pointed field is simply ignored.
- If a type defines **none** of these fields, aggregation is skipped entirely and its rendered context is just its Local Context Data. This is a deliberate difference from Devices/VMs: **global (unassigned) ConfigContexts are not applied** to such a type — enabling config context support alone never silently pulls in every global context. Add at least one convention field (e.g. `site`) to opt the type into source aggregation; global contexts then apply too (as they do for any object with a dimension).

### Jinja Config Templates

!!! note
Requires NetBox 4.7 or later. On earlier NetBox versions, `custom_objects` is simply unavailable in config templates and nothing else changes — no error, no crash. A `DEBUG`-level log message noting this is emitted at plugin startup; enable debug logging if you need to confirm why `custom_objects` isn't resolving.

Custom Objects can be referenced directly from NetBox [config templates](https://netboxlabs.com/docs/netbox/models/extras/configtemplate/), so device configuration can pull in data modelled with Custom Object Types (e.g. OSPF interface parameters, BGP peer groups, MPLS label ranges) alongside built-in NetBox models.

Two equivalent access patterns are available, both resolving the Custom Object Type by its **internal name** at render time (so templates keep working even if the type's slug or internal table ID changes):

Attribute-style, via the `custom_objects` context variable:

```jinja2
{% for iface in custom_objects.ospf_interface.filter(device=device) %}
interface {{ iface.name }}
ip ospf area {{ iface.area }}
{% endfor %}
```

Filter syntax, via the `custom_objects` Jinja filter:

```jinja2
{% for iface in 'ospf_interface' | custom_objects %}
interface {{ iface.name }}
{% endfor %}
```

Notes:

- The attribute-style form returns the model's manager (`.filter(...)`, `.all()`, etc.); the filter form returns a queryset of all instances of that type.
- An unknown type name is handled the same way by both forms: a warning is logged, and the reference resolves to an empty, chainable stand-in — further calls like `.filter(...)` or `.all()` continue to render no rows rather than raising. Check the type's internal name (shown on its detail page) if a template renders no data.
- A type's internal name may begin with a digit (e.g. `123foo`), which isn't valid Jinja dot-notation. Use bracket notation with the attribute-style form instead:

```jinja2
{% for obj in custom_objects['123foo'].filter(device=device) %}
```

### Deletions

#### Deleting Custom Object Types
Expand Down
5 changes: 3 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ PLUGINS = [
]
```

### 3. Run Database Migrations
### 3. Run Database Migrations and Collect Static Files

Apply the plugin's database migrations:
Apply the plugin's database migrations and collect its static files:

```
./manage.py migrate
./manage.py collectstatic
```

### 4. Restart NetBox
Expand Down
49 changes: 49 additions & 0 deletions docs_models/netbox_custom_objects/customobjecttype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Custom Object Types

A Custom Object Type defines a new object type in NetBox — the equivalent of a model in NetBox plugin terminology. Each Custom Object Type generates its own database table, list and detail views, REST API endpoints, and an entry in the left navigation pane. See the [Custom Objects documentation](https://github.com/netboxlabs/netbox-custom-objects/blob/main/docs/index.md) for a full walkthrough, including how Custom Object Type Fields are added to a type.

## Fields

### Internal Name

A unique, lowercased, URL-friendly internal name, e.g. `vendor_policy`. Only lowercase alphanumeric characters and underscores are permitted; names may not start or end with an underscore, and double underscores are not allowed.

### Display Name (Singular)

The human-friendly singular name shown throughout the UI, e.g. `Vendor Policy`. Defaults to the internal name if left blank.

### Display Name (Plural)

The human-friendly plural name shown throughout the UI, e.g. `Vendor Policies`. Defaults to the internal name if left blank.

### URL Path/Slug

A unique, plural, URL-friendly identifier used as a URL component for this type's list and detail views, e.g. `vendor-policies`.

### Display Expression

An optional Jinja2 template used to render the display name of individual objects of this type, e.g. `{{ name }} - {{ manufacturer }}`. Reference field values by name; undefined fields resolve to an empty string. If left blank, the field marked as the type's primary field is used instead.

### Group Name

An optional label used to group similar Custom Object Types together in the navigation menu.

### Version

An optional [PEP 440](https://peps.python.org/pep-0440/) version string, e.g. `1.0.0`. Used when managing schemas across environments with the [portable schema](https://github.com/netboxlabs/netbox-custom-objects/blob/main/docs/portable-schema.md) feature.

### Description

A short, optional description of this Custom Object Type.

### Config Context Support

Whether objects of this type support NetBox's [config context](https://netboxlabs.com/docs/netbox/models/extras/configcontext/) feature, gaining a Local Context Data field and a Config Context tab. This can only be set when the type is created — it adds a column to the type's table, so it cannot be toggled afterward. See the [Config Context](https://github.com/netboxlabs/netbox-custom-objects/blob/main/docs/index.md#config-context) section of the documentation for details.

### Comments

Free-form text for any additional notes about this Custom Object Type.

### Tags

NetBox tags applied to this Custom Object Type.
9 changes: 9 additions & 0 deletions mkdocs.models.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
site_name: NetBox Custom Objects Model Documentation
docs_dir: docs_models
site_dir: netbox_custom_objects/static/docs/models
theme:
name: mkdocs
plugins: []
nav:
- netbox_custom_objects:
- Custom Object Type: netbox_custom_objects/customobjecttype.md
26 changes: 25 additions & 1 deletion netbox_custom_objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ class CustomObjectsPluginConfig(PluginConfig):
base_url = "custom-objects"
# Remember to update COMPATIBILITY.md when modifying the minimum/maximum supported NetBox versions.
min_version = "4.5.2"
max_version = "4.6.99"
max_version = "4.7.99"
default_settings = {
# The maximum number of Custom Object Types that may be created
'max_custom_object_types': 50,
Expand All @@ -312,6 +312,10 @@ class CustomObjectsPluginConfig(PluginConfig):
# so the swallowed exception isn't invisible outside the logs.
_register_tabs_error = None
template_extensions = "template_content.template_extensions"
# Registers the custom_objects Jinja filter (jinja_env.filters). Requires NetBox
# 4.7+; on older NetBox this attribute is simply never read by core (see ready()
# for the startup log message covering that case).
jinja_filters = "jinja_env.filters"
# Resolves dynamic CO models (table{n}model) to on-the-fly serializers —
# they have no importable path at the conventional location.
serializer_resolver = "api.serializers.serializer_resolver"
Expand Down Expand Up @@ -427,6 +431,26 @@ def _call_super_ready_once(self):
super().ready()
_super_ready_called = True

# On NetBox < 4.7 the jinja_filters resource and get_jinja_context() hook
# don't exist, so super().ready() never calls _load_resource('jinja_filters')
# and get_jinja_context() is never invoked by RenderTemplateMixin.get_context().
# This is every currently-supported NetBox version (4.7 isn't released yet), so
# log at DEBUG rather than INFO: it's an explanation to reach for when actively
# troubleshooting why 'custom_objects' isn't resolving, not a startup notice
# every install should see by default.
from netbox.registry import registry
if 'custom_objects' not in registry.get('plugins', {}).get('jinja_filters', {}):
logger.debug(
"NetBox Jinja config template hooks (jinja_filters / get_jinja_context) "
"are not available in this version of NetBox. The 'custom_objects' filter "
"and context variable will not be active in config templates. Upgrade to "
"NetBox 4.7+ to enable this feature."
)

def get_jinja_context(self):
from netbox_custom_objects.jinja_env import CustomObjectsNamespace
return {'custom_objects': CustomObjectsNamespace()}

def ready(self):
# Install the thread-safe apps.clear_cache wrapper before any dynamic
# model is registered (must happen exactly once, before get_model() runs).
Expand Down
1 change: 1 addition & 0 deletions netbox_custom_objects/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ class Meta:
"slug",
"version",
"group_name",
"display_expression",
"description",
"config_context_enabled",
"tags",
Expand Down
Loading
Loading