Compare commits

..

63 Commits

Author SHA1 Message Date
Jeremy Stretch
1a404f5c0f Merge branch 'main' into feature 2026-02-25 17:07:26 -05:00
Kartik
3adcdc34c3 clarify E501 enforcement 2026-02-25 15:33:25 -05:00
Martin Hauser
f33109e485 fix(dcim): Rename facility to facility_id in panel attrs (#21482)
Corrects field mismatch by aligning the attribute name with the
data model. This change ensures consistency in attribute mappings
and improves clarity in the codebase.

Fixes #21481
2026-02-25 12:20:51 -08:00
github-actions
d10453883f Update source translation strings 2026-02-21 05:16:36 +00:00
bctiemann
6dbd8f6170 Merge pull request #21507 from netbox-community/21497-pin-ruff-in-ci-to-avoid-surprise-breakages
Fixes #21497: Pin Ruff 0.15.2 and run CI via ruff-action
2026-02-20 16:59:46 -05:00
Jason Novinger
715f9d150c Closes #21385: Add contact assignment support to virtual circuits
Adds ContactsMixin to VirtualCircuit model and GraphQL type, and includes
'contacts' in table fields. Verified: UI Contacts tab, REST API POST (201),
GraphQL contacts query.
2026-02-20 16:59:37 -05:00
Martin Hauser
f4567ba099 chore(ci): Pin Ruff 0.15.2 and run via ruff-action
Pin Ruff to v0.15.2 in CI and pre-commit to avoid breakages from
upstream releases. Run Ruff via astral-sh/ruff-action (pinned by SHA)
instead of installing Ruff via pip.
Document where Ruff is pinned and keep the release checklist/style guide
in sync.

Fixes #21472
Fixes #21497
2026-02-20 20:38:11 +01:00
bctiemann
3320e07b70 Closes #21284: Add deprecation note to webhooks documentation (#21491)
* Add searchable deprecation comments on request_id and username fields in EventContext

* Add deprecation note in webhooks documentation

* Expand deprecation note/warning

* Add version number to deprecation warning

* Add deprecation warning to two other places
2026-02-20 19:52:42 +01:00
Jeremy Stretch
d5e8f7dafa Closes #21459: Avoid prefetching data for hidden table columns (#21460) 2026-02-20 10:36:46 -08:00
Jeremy Stretch
32e2a17c88 Merge pull request #21503 from netbox-community/21500-enable-linting-for-all-ordering-ruf022
Closes #21500: Enable RUF022 to enforce sorted `__all__` lists
2026-02-20 11:52:00 -05:00
Martin Hauser
3beef34355 chore(ruff): Sort __all__ definitions across modules
Apply consistent alphabetical ordering to `__all__` lists in the
circuits module. Enhances readability and alignment with established
linting guidelines.
2026-02-20 15:36:01 +01:00
Martin Hauser
85d6242962 chore(ruff): Add RUF022 and tweak formatting in ruff.toml
Enable `RUF022` to enforce sorted `__all__` lists. Adjust comment
alignment and whitespace for improved readability and consistency
in ruff.toml configuration.
2026-02-20 15:34:58 +01:00
bctiemann
bb1a44d35b Merge pull request #21479 from netbox-community/21478-graphql-union-type-resolution-for-connected-endpoints
Fixes #21478: Fix GraphQL connected endpoint type resolution for Console Ports
2026-02-20 09:27:36 -05:00
bctiemann
ae6f1f9ae3 Merge pull request #21496 from netbox-community/20923-convert-virtualization-views-to-new-ui-layout
Closes #20923: Migrate Virtualization object views to declarative layouts
2026-02-20 09:26:56 -05:00
Arthur Hanson
915ac90119 20911 Fix sorting in dropdown (#21101)
* Fix TomSelect dropdown ordering

* cleanup

* cleanup

* cleanup

* use correct node version

* change ordering field, remove front-end changes

* rebuild tree after rename

* add migration

* fix migration

* fix migration

* fix migration

* fix migration

* fix migration

* cleanup

* use bulk_update and rebuild

* use bulk_update and rebuild

* cleanup

* fix csv import

* Review feedback

* Review feedback

* fix dropdown sorting

* fix ordering

* review feedback

* review feedback
2026-02-20 09:03:47 -05:00
Martin Hauser
cc47afc401 refactor(virtualization): Port to declarative layout
Add declarative layout panels for Cluster, Cluster Group, Cluster Type,
Virtual Disk, and VM Interface, including addressing, VLAN assignment,
and FHRP group handling.

Expand the declarative layout primitives:
- add GFK attribute rendering support
- add panel for rendering context-provided tables
- update templates to support new panels/attrs

Closes #20923
2026-02-20 14:58:20 +01:00
Martin Hauser
20fee95a9a Closes #21499: Restore deterministic Ruff linting (match Ruff 0.15.1 preview defaults)
Explicitly set `select` rules to maintain compatibility with
Ruff 0.15.1. Ensures deterministic linting behavior despite changes in
Ruff 0.15.2 defaults.
See https://github.com/astral-sh/ruff/releases/tag/0.15.2 for more
details.
2026-02-20 14:40:45 +01:00
github-actions
d2002c64b4 Update source translation strings 2026-02-20 05:23:09 +00:00
Martin Hauser
1b295f1d69 Closes #21473: Enable UP rules and modernize string formatting (#21488) 2026-02-19 10:25:08 -06:00
Martin Hauser
2c200a4fd3 Closes #21369: Add lazy loading and decoding options for ImageAttr (#21444)
Introduces `load_lazy` and `decoding` parameters to `ImageAttr` for
enhanced image handling. Lazy loading improves page performance, while
configurable decoding options provide greater flexibility. Updates the
template to conditionally include these attributes in rendered HTML.

Fixes #21369
2026-02-19 09:22:16 -06:00
bctiemann
fb71cafb51 Closes: #21284: Mark request_id and username fields in EventContext as deprecated (#21485)
Add searchable deprecation comments on request_id and username fields
in EventContext.
2026-02-19 14:03:47 +01:00
github-actions
f373adb636 Update source translation strings 2026-02-19 05:26:47 +00:00
Martin Hauser
e84b062393 fix(dcim): Correct type check for ConsolePort in GraphQL mixin
Fixes a typo in the `resolve_type` method where `ConsolePortType` was
mistakenly referenced instead of `ConsolePort`. Ensures the correct
GraphQL type is returned for ConsolePort instances.

Fixes #21478
2026-02-18 23:19:36 +01:00
Martin Hauser
ef52ac4203 chore(ruff): Enable RET rules and add explicit fallbacks
Adopt Ruff `RET` to improve return-flow consistency across the codebase.
Simplify control flow by removing redundant `else` blocks after
`return`, and add explicit `return None` (or equivalent) fallbacks
where appropriate to preserve existing behavior.

Fixes #21411
2026-02-18 16:49:36 -05:00
bctiemann
b22e490847 Fixes: #20490 - Add filtering of Script objects based on object permissions with custom constraints (#21212) 2026-02-18 15:37:40 -05:00
Martin Hauser
945e7ade0a Fixes #21407: Enable I (isort) and stabilize import ordering (#21458)
- Adopt Ruff `I` (isort) rules for consistent import sorting
- Add two `# isort: split` boundaries to keep required imports pinned
  in `__init__.py` modules
2026-02-18 10:41:51 -06:00
github-actions
7300104cea Update source translation strings 2026-02-18 05:28:02 +00:00
bctiemann
2900429769 Merge pull request #21441 from netbox-community/21410-tighten-up-ruff-configuration-defaults
Fixes #21410: Expand Ruff exclusions and standardize formatting settings
2026-02-17 13:14:11 -05:00
Martin Hauser
278c82dd88 chore(ruff): Expand configuration for linting and formatting
Update `ruff.toml` with additional exclusions, linting rules, and
formatting preferences. Includes support for respecting `.gitignore`
and a consistent coding style.

Fixes #21410
2026-02-17 18:31:15 +01:00
Jeremy Stretch
c029782cf5 Release v4.5.3 2026-02-17 10:37:44 -05:00
Martin Hauser
bdd23f3d17 fix(extras): Handle username fallback for job events
Fallback to the associated user when username is missing from job
lifecycle event contexts. Add a regression test to ensure JOB_COMPLETED
webhooks are enqueued without a request context.

Fixes #21371
2026-02-17 08:15:58 -05:00
github-actions
af6e18b7d4 Update source translation strings 2026-02-17 05:26:34 +00:00
Jeremy Stretch
816c5d4bea Fixes #21412: Defer monkey-patching until after settings have been loaded (#21415) 2026-02-16 18:17:50 +01:00
Martin Hauser
f4c3c90bab perf(filters): Avoid ContentType join in ContentTypeFilter
Resolve the ContentType via get_by_natural_key() and filter by the
FK value to prevent an unnecessary join to django_content_type.

Fixes #21420
2026-02-16 12:06:31 -05:00
Martin Hauser
862593f2dd fix(circuits): Persist CircuitType owner field
CircuitTypeForm rendered `owner` twice and did not persist ownership
because the displayed fields didn't match the fields processed by the
form. Remove `owner` from the fieldset and include it in `Meta.fields`
to keep rendering and form processing in sync.

Fixes #21397
2026-02-16 08:54:34 -05:00
Martin Hauser
f4c27fd494 fix(ipam): Use bulk_update in VLANGroup VID range migration
Replace per-row `save()` calls with `bulk_update` when populating
VLANGroup VLAN ID ranges during migration.

This avoids triggering post_save handlers (e.g. search cache/indexing)
on existing VLANGroup records and updates only the relevant fields,
improving both reliability and performance on larger databases.

Fixes #21375
2026-02-16 08:53:16 -05:00
Martin Hauser
ae736ef407 fix(dcim): Render device height as rack units via floatformat
Use `TemplatedAttr` for device height and render using Django's
`floatformat` filter so 0.0 is displayed as `0U` (and whole-U values
omit the decimal).

Fixes #21267
2026-02-16 08:37:50 -05:00
github-actions
d95b1186fb Update source translation strings 2026-02-14 05:18:04 +00:00
Jason Novinger
d6b9d30086 Fixes #20442: Mark template-accessible methods with alters_data=True (#21431)
Add alters_data=True to methods that modify database or filesystem state
and are accessible from Jinja2 sandbox template contexts:

- UserConfig.set(), clear(): Persist preference changes when commit=True
- ManagedFile.sync_data(): Writes files to scripts/reports storage
- ScriptModule.sync_classes(), sync_data(): Creates/deletes Script objects
- Job.start(), terminate(): Updates job status, creates notifications

Methods intentionally not protected:
- DataFile.refresh_from_disk(): Only modifies instance attributes in memory
- Overridden save()/delete(): Django's AltersData mixin auto-propagates
- Properties like Script.python_class: Not callable in template context

Ref: #20356 for exploit details demonstrating the vulnerability
2026-02-13 10:44:18 -08:00
Martin Hauser
9be5aa188c chore(ruff): Update target Python version to 3.12 (#21405)
Set the `target-version` in `ruff.toml` to Python 3.12. Ensures the
linter aligns with the version used in the project's environment.

Fixes #21404
2026-02-13 10:39:09 -08:00
Jason Novinger
f113557e81 Fixes #21127: Clear _path on interfaces when removed from cable
When editing a cable to remove an interface from the B side, the _path
field on the removed interface was not being cleared. This caused the
interface table to display stale connection info via _path.destinations.

Two changes:
- Signal handler now clears _path when termination removed from origins
- CablePath.delete() clears _path on origins (mirrors save() behavior)
2026-02-13 13:36:09 -05:00
Arthur
de812a5a85 21390 skip m2m processing for internal models to avoid extraneous ObjectChange records 2026-02-13 13:27:25 -05:00
Jason Novinger
0b7375136d Closes #21016: Add missing MPTT tree indexes (#21432)
Upgrade django-mptt to 0.18.0 and add empty indexes tuple to MPTT model
Meta classes. The empty tuple triggers Django's migration detection for
indexes that django-mptt adds dynamically (see
django-mptt/django-mptt#682). We cannot define the indexes explicitly
because the MPTT fields don't exist when the Meta class is evaluated.

Affected models: Region, SiteGroup, Location, DeviceRole, Platform,
ModuleBay, InventoryItem, InventoryItemTemplate, TenantGroup,
ContactGroup, WirelessLANGroup
2026-02-13 17:00:04 +01:00
Jeremy Stretch
1190adde2b Closes #21419: Improve query efficiency for MultipleChoiceFilter (#21421)
* Pass distinct=False to all ModelMultipleChoiceFilters associated with a ForeignKey field

* Pass distinct=False to all MultipleChoiceFilters associated with a concrete model
2026-02-13 12:31:36 +01:00
Arthur Hanson
2330874a8c Fixes #21277: Record pre-change snapshot when adding devices to cluster in UI (#21424) 2026-02-13 04:41:41 -06:00
Jeremy Stretch
dc738c7102 Closes #21257: Introduce & adopt MultiValueContentTypeFilter (#21417) 2026-02-13 04:24:36 -06:00
Jeremy Stretch
76fd3e3c61 Fixes #21196: q filter should match on primary IP only for IP address values (#21401) 2026-02-13 04:08:01 -06:00
github-actions
4ee64a7731 Update source translation strings 2026-02-13 05:27:16 +00:00
Arthur Hanson
0bb22dee0c Allow REDIS KWARGS to be set in configuration.py (#21377)
* Allow REDIS KWARGS to be set in configuration.py

* cleanup

* cleanup

* cleanup

* Update netbox/netbox/settings.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/netbox/settings.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* document in REDIS config section

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-02-12 08:35:20 -05:00
Jason Novinger
6c383f293c Fixes #20435: Fix navigation margin issue when scrollbar appears (#21403)
Override Tabler's problematic margin-left: calc(100vw - 100%) rule that
causes a gap between the sidebar and main content when vertical scrollbar
is present on Windows/Linux browsers.

Uses scrollbar-gutter: stable to match the upstream fix in Tabler PR #2548.
2026-02-12 11:30:33 +01:00
github-actions
5bf516c63d Update source translation strings 2026-02-12 05:28:54 +00:00
Aditya Sharma
7df062d590 Fixes #21358: Prevent exception when sorting by Token column (#21391)
Mark the `token` TemplateColumn as non-orderable since it maps to a
Python property rather than a database field, causing a FieldError
when django-tables2 attempts to sort by it.

Add a regression test for TokenTable following the existing pattern
in circuits and vpn test suites.
2026-02-12 00:21:49 +01:00
Aditya Sharma
4b22be03a0 Fixes #21354: Fix Swagger-UI generating wrong URLs when BASE_PATH is set (#21392) 2026-02-11 11:35:13 -08:00
Dylan Lucci
24769ce127 Closes #21266: Add installed device table columns to DeviceBay table (#21348)
Expose additional properties of the device installed in each bay as
configurable table columns.

- Rename `role` → `installed_role`
- Rename `device_type` → `installed_device_type`
- Add `installed_description`, `installed_serial`, and
  `installed_asset_tag` columns to `DeviceBayTable`

---------

Co-authored-by: Martin Hauser <mhauser@netboxlabs.com>
2026-02-11 13:55:37 +01:00
github-actions
164e9db98d Update source translation strings 2026-02-11 05:29:43 +00:00
Martin Hauser
23f1c86e9c Closes #20211: Use thumbnails for ImageAttachment hover previews to improve page load performance (#21386) 2026-02-10 11:01:33 -06:00
Martin Hauser
02ffdd9d5d Closes #21268: Add Device Type details panel to Device view (#21368) 2026-02-10 10:37:35 -06:00
Martin Hauser
5013297326 feat(virtualization): Refactor VirtualMachine view to UI layout
Migrate the VirtualMachine detail view to SimpleLayout with standardized
panels for attributes, clusters, and resources. Modularize templates
to improve maintainability and reuse.

Fixes #21337
2026-02-10 10:22:18 -05:00
github-actions
584e0a9b8c Update source translation strings 2026-02-10 05:29:34 +00:00
Martin Hauser
3ac9d0b8bf Closes #20981: Enhance JSON rendering for Custom Validators and Protection Rules in Config Revision View (#21376)
* feat(config): Add extra context to ConfigRevisionView

Introduces `get_extra_context` method for `ConfigRevisionView` to
format JSON-based attributes like `CUSTOM_VALIDATORS`,
`DEFAULT_USER_PREFERENCES`, and `PROTECTION_RULES`.
This ensures clearer rendering of configuration data in the UI.

Fixes #20981

* Reduce padding on JSON blocks

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-02-09 09:48:39 -05:00
github-actions
b387ea5f58 Update source translation strings 2026-02-06 05:22:42 +00:00
bctiemann
ba9f6bf359 Fixes: #19129 - Richer display of MAC addresses in InterfaceTable when multiple MACs are present (#21270)
* Richer display of MAC addresses in InterfaceTable when multiple MACs are present

* Fix docstring

* Fix docstring

* Use mac_address_display in interface detail page

* Ensure "-" null placeholder still shows up on detail page

* Also include vminterface.html

* Simplify Multiple MAC addresses with additional selectable column for tables in list view and detail view

* Use ManyToManyColumn
2026-02-05 11:16:31 -05:00
Martin Hauser
ee6cbdcefe Fixes #21320: Prevent Rack validation errors when site or optional fields are missing during import (#21321) 2026-02-03 09:32:07 -06:00
620 changed files with 21997 additions and 19900 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.3
validations:
required: true
- type: dropdown

View File

@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.3
validations:
required: true
- type: dropdown

View File

@@ -8,7 +8,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.3
validations:
required: true
- type: dropdown

View File

@@ -55,6 +55,13 @@ jobs:
- name: Check out repo
uses: actions/checkout@v4
- name: Check Python linting & PEP8 compliance
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1
with:
version: "0.15.2"
args: "check --output-format=github"
src: "netbox/"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -82,7 +89,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff coverage tblib
pip install coverage tblib
- name: Build documentation
run: mkdocs build
@@ -93,9 +100,6 @@ jobs:
- name: Check for missing migrations
run: python netbox/manage.py makemigrations --check
- name: Check PEP8 compliance
run: ruff check netbox/
- name: Check UI ESLint, TypeScript, and Prettier Compliance
run: yarn --cwd netbox/project-static validate

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
rev: v0.15.2
hooks:
- id: ruff
name: "Ruff linter"

View File

@@ -27,9 +27,7 @@ django-graphiql-debug-toolbar
django-htmx
# Modified Preorder Tree Traversal (recursive nesting of objects)
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
# v0.18.0 introduces errant migrations which need to be resolved
django-mptt==0.17.0
django-mptt
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
@@ -159,7 +157,8 @@ strawberry-graphql
# Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/releases
strawberry-graphql-django
# Blocked by #21450
strawberry-graphql-django==0.75.0
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,48 @@ REDIS = {
!!! note
It is permissible to use Sentinel for only one database and not the other.
### SSL Configuration
If you need to configure SSL/TLS for Redis beyond the basic `SSL`, `CA_CERT_PATH`, and `INSECURE_SKIP_TLS_VERIFY` options (for example, client certificates, a specific TLS version, or custom ciphers), you can pass additional parameters via the `KWARGS` key in either the `tasks` or `caching` subsection.
NetBox already maps `CA_CERT_PATH` to `ssl_ca_certs` and (for caching) `INSECURE_SKIP_TLS_VERIFY` to `ssl_cert_reqs`; only add `KWARGS` when you need to override or extend those settings (for example, to supply client certificates or restrict TLS version or ciphers).
* `KWARGS` - Optional dictionary of additional SSL/TLS (or other) parameters passed to the Redis client. These are passed directly to the underlying Redis client: for `tasks` to [redis-py](https://redis-py.readthedocs.io/en/stable/connections.html), and for `caching` to the [django-redis](https://github.com/jazzband/django-redis#configure-as-cache-backend) connection pool.
Example:
```python
REDIS = {
'tasks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'SSL': True,
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
'KWARGS': {
'ssl_certfile': '/path/to/client-cert.pem',
'ssl_keyfile': '/path/to/client-key.pem',
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
'ssl_ciphers': 'HIGH:!aNULL',
},
},
'caching': {
'HOST': 'redis.example.com',
'PORT': 1234,
'SSL': True,
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
'KWARGS': {
'ssl_certfile': '/path/to/client-cert.pem',
'ssl_keyfile': '/path/to/client-key.pem',
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
'ssl_ciphers': 'HIGH:!aNULL',
},
}
}
```
!!! note
If you use `ssl.TLSVersion` in your configuration (e.g. `ssl_min_version`), add `import ssl` at the top of your configuration file.
---
## SECRET_KEY

View File

@@ -18,7 +18,8 @@ They can also be used as a mechanism for validating the integrity of data within
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
!!! danger "Only install trusted scripts"
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
## Writing Custom Scripts

View File

@@ -168,6 +168,14 @@ Update the static OpenAPI schema definition at `contrib/openapi.json` with the m
./manage.py spectacular --format openapi-json > ../contrib/openapi.json
```
### Update Development Dependencies
Keep development tooling versions consistent across the project. If you upgrade a dev-only dependency, update all places where its pinned so local tooling and CI run the same versions.
* Ruff:
* `.pre-commit-config.yaml`
* `.github/workflows/ci.yml`
### Submit a Pull Request
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.

View File

@@ -34,7 +34,8 @@ The following rules are ignored when linting.
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
NetBox enforces a maximum line length of 120 characters for Python code using Ruff (E501).
The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
@@ -47,6 +48,14 @@ Wildcard imports (for example, `from .constants import *`) are acceptable under
The justification for ignoring this rule is the same as F403 above.
##### [RET504](https://docs.astral.sh/ruff/rules/unnecessary-assign/): Unnecessary assign
There are multiple instances where it is more readable and clearer to first assign to a variable and then return it.
##### [UP032](https://docs.astral.sh/ruff/rules/f-string/): f-string
For localizable strings, it is necessary to not use the `f-string` syntax, as Django's translation functions (e.g. `gettext_lazy`) require plain string literals.
### Introducing New Dependencies
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.

View File

@@ -31,6 +31,11 @@ The following data is available as context for Jinja2 templates:
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
!!! warning "Deprecation of legacy fields"
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
### Default Request Body
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:

View File

@@ -88,3 +88,8 @@ The following context variables are available in to the text and link templates.
| `request_id` | The unique request ID |
| `data` | A complete serialized representation of the object |
| `snapshots` | Pre- and post-change snapshots of the object |
!!! warning "Deprecation of legacy fields"
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.

View File

@@ -43,6 +43,11 @@ The resulting webhook payload will look like the following:
}
```
!!! warning "Deprecation of legacy fields"
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
!!! note "Consider namespacing webhook data"
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:

View File

@@ -1,5 +1,42 @@
# NetBox v4.5
## v4.5.3 (2026-02-17)
### Enhancements
* [#19129](https://github.com/netbox-community/netbox/issues/19129) - Improve display of multiple MAC addresses within interfaces table
* [#20981](https://github.com/netbox-community/netbox/issues/20981) - Enhance JSON rendering for custom validators and protection rules in config revision view
* [#21240](https://github.com/netbox-community/netbox/issues/21240) - Add support for configuring Redis `KWARGS` parameters
* [#21257](https://github.com/netbox-community/netbox/issues/21257) - `ContentTypeFilter` now accepts multiple values
* [#21266](https://github.com/netbox-community/netbox/issues/21266) - Add table columns representing installed devices to the device bays table
* [#21267](https://github.com/netbox-community/netbox/issues/21267) - Normalize device height formatting in rack units (display "0U")
* [#21268](https://github.com/netbox-community/netbox/issues/21268) - Add device type details panel to device view
* [#21337](https://github.com/netbox-community/netbox/issues/21337) - Show the assigned platform's parent on the virtual machine UI view
### Performance Improvements
* [#20211](https://github.com/netbox-community/netbox/issues/20211) - Use thumbnails for image attachment hover previews to improve page load performance
* [#21016](https://github.com/netbox-community/netbox/issues/21016) - Restore missing SQL indexes for MPTT fields
* [#21196](https://github.com/netbox-community/netbox/issues/21196) - `q` filter should match on primary IP only for IP address values when filtering devices/VMs
* [#21420](https://github.com/netbox-community/netbox/issues/21420) - Improve query performance of `ContentTypeFilter`
* [#21421](https://github.com/netbox-community/netbox/issues/21421) - Eliminate extraneous application of `DISTINCT` to queries for `MultipleChoiceFilter`
### Bug Fixes
* [#20435](https://github.com/netbox-community/netbox/issues/20435) - Fix navigation menu margin issue when scrollbar appears
* [#21127](https://github.com/netbox-community/netbox/issues/21127) - Ensure assigned cable paths are cleared when removing terminations from a cable
* [#21277](https://github.com/netbox-community/netbox/issues/21277) - Record pre-change snapshot when adding cluster members via UI
* [#21320](https://github.com/netbox-community/netbox/issues/21320) - Avoid validation failures when site or optional fields are missing during rack import
* [#21354](https://github.com/netbox-community/netbox/issues/21354) - Fix base URL in Swagger when `BASE_PATH` is set
* [#21358](https://github.com/netbox-community/netbox/issues/21358) - Token list in UI cannot be ordered by token value
* [#21371](https://github.com/netbox-community/netbox/issues/21371) - Fix `KeyError` exception when triggering a webhook from an event rule
* [#21375](https://github.com/netbox-community/netbox/issues/21375) - Address failure condition in `ipam.0070_vlangroup_vlan_id_ranges` migration
* [#21390](https://github.com/netbox-community/netbox/issues/21390) - Avoid creating "empty" changelog records for related objects when processing manyo-to-many relations
* [#21397](https://github.com/netbox-community/netbox/issues/21397) - Correct rendering of owner field in CircuitType edit form
* [#21412](https://github.com/netbox-community/netbox/issues/21412) - Avoid `AttributeError` exception on initialization when a plugin has local imports in `__init__.py`
---
## v4.5.2 (2026-02-03)
### Enhancements

View File

@@ -1,6 +1,7 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'account'

View File

@@ -2,14 +2,15 @@ import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
@@ -35,11 +36,11 @@ from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
@@ -139,9 +140,8 @@ class LoginView(View):
return response
else:
username = form['username'].value()
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
username = form['username'].value()
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
return render(request, self.template_name, {
'form': form,

View File

@@ -1,2 +1,2 @@
from .serializers_.providers import *
from .serializers_.circuits import *
from .serializers_.providers import *

View File

@@ -4,24 +4,34 @@ from rest_framework import serializers
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import (
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
VirtualCircuitTermination, VirtualCircuitType,
Circuit,
CircuitGroup,
CircuitGroupAssignment,
CircuitTermination,
CircuitType,
VirtualCircuit,
VirtualCircuitTermination,
VirtualCircuitType,
)
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.device_components import InterfaceSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import (
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
NetBoxModelSerializer,
OrganizationalModelSerializer,
PrimaryModelSerializer,
WritableNestedSerializer,
)
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
'CircuitSerializer',
'CircuitGroupAssignmentSerializer',
'CircuitGroupSerializer',
'CircuitSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
'VirtualCircuitSerializer',

View File

@@ -5,6 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import PrimaryModelSerializer
from .nested import NestedProviderAccountSerializer
__all__ = (

View File

@@ -1,6 +1,6 @@
from netbox.api.routers import NetBoxRouter
from . import views
from . import views
router = NetBoxRouter()
router.APIRootView = views.CircuitsRootView

View File

@@ -4,6 +4,7 @@ from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet
from . import serializers

View File

@@ -9,7 +9,8 @@ class CircuitsConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
from . import signals, search # noqa: F401
from . import search, signals # noqa: F401
from .models import CircuitTermination
# Register models

View File

@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
#
# Circuits
#
class CircuitStatusChoices(ChoiceSet):
key = 'Circuit.status'

View File

@@ -1,6 +1,5 @@
from django.db.models import Q
# models values for ContentTypes which may be CircuitTermination termination types
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
'region', 'sitegroup', 'site', 'location', 'providernetwork',

View File

@@ -9,9 +9,13 @@ from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
MultiValueCharFilter,
MultiValueContentTypeFilter,
MultiValueNumberFilter,
TreeNodeMultipleChoiceFilter,
)
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -21,9 +25,9 @@ __all__ = (
'CircuitGroupFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet',
'ProviderNetworkFilterSet',
'VirtualCircuitFilterSet',
'VirtualCircuitTerminationFilterSet',
'VirtualCircuitTypeFilterSet',
@@ -99,11 +103,13 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -127,11 +133,13 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -163,22 +171,26 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
@@ -189,16 +201,19 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(),
distinct=False,
label=_('Circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=CircuitType.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
distinct=False,
null_value=None
)
region_id = TreeNodeMultipleChoiceFilter(
@@ -245,10 +260,12 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
)
termination_a_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
distinct=False,
label=_('Termination A (ID)'),
)
termination_z_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
distinct=False,
label=_('Termination A (ID)'),
)
@@ -279,9 +296,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=Circuit.objects.all(),
distinct=False,
label=_('Circuit'),
)
termination_type = ContentTypeFilter()
termination_type = MultiValueContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
@@ -310,12 +328,14 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
distinct=False,
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Site (slug)'),
)
@@ -334,17 +354,20 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
field_name='_provider_network',
label=_('ProviderNetwork (ID)'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider_id',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -381,7 +404,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
member_type = ContentTypeFilter()
member_type = MultiValueContentTypeFilter()
circuit = MultiValueCharFilter(
method='filter_circuit',
field_name='cid',
@@ -414,11 +437,13 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
)
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitGroup.objects.all(),
distinct=False,
label=_('Circuit group (ID)'),
)
group = django_filters.ModelMultipleChoiceFilter(
field_name='group__slug',
queryset=CircuitGroup.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Circuit group (slug)'),
)
@@ -488,41 +513,49 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
label=_('Provider network (ID)'),
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuitType.objects.all(),
distinct=False,
label=_('Virtual circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=VirtualCircuitType.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Virtual circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
distinct=False,
null_value=None
)
@@ -548,41 +581,49 @@ class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
)
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuit.objects.all(),
distinct=False,
label=_('Virtual circuit'),
)
role = django_filters.MultipleChoiceFilter(
choices=VirtualCircuitTerminationRoleChoices,
distinct=False,
null_value=None
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account__account',
queryset=ProviderAccount.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
field_name='virtual_circuit__provider_network',
label=_('Provider network (ID)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(),
distinct=False,
field_name='interface',
label=_('Interface (ID)'),
)

View File

@@ -4,7 +4,10 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
CircuitCommitRateChoices,
CircuitPriorityChoices,
CircuitStatusChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import *
@@ -15,7 +18,10 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
from tenancy.models import Tenant
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ColorField,
ContentTypeChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
@@ -27,8 +33,8 @@ __all__ = (
'CircuitGroupBulkEditForm',
'CircuitTerminationBulkEditForm',
'CircuitTypeBulkEditForm',
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderBulkEditForm',
'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm',

View File

@@ -12,14 +12,14 @@ from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
__all__ = (
'CircuitImportForm',
'CircuitGroupAssignmentImportForm',
'CircuitGroupImportForm',
'CircuitImportForm',
'CircuitTerminationImportForm',
'CircuitTerminationImportRelatedForm',
'CircuitTypeImportForm',
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderImportForm',
'ProviderNetworkImportForm',
'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm',

View File

@@ -2,7 +2,10 @@ from django import forms
from django.utils.translation import gettext as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
CircuitCommitRateChoices,
CircuitPriorityChoices,
CircuitStatusChoices,
CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.models import *
@@ -10,7 +13,7 @@ from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
@@ -22,8 +25,8 @@ __all__ = (
'CircuitGroupFilterForm',
'CircuitTerminationFilterForm',
'CircuitTypeFilterForm',
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderFilterForm',
'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm',

View File

@@ -4,7 +4,9 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
CircuitCommitRateChoices,
CircuitTerminationPortSpeedChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
@@ -14,7 +16,10 @@ from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelF
from tenancy.forms import TenancyForm
from utilities.forms import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
ContentTypeChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
SlugField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
@@ -27,8 +32,8 @@ __all__ = (
'CircuitGroupForm',
'CircuitTerminationForm',
'CircuitTypeForm',
'ProviderForm',
'ProviderAccountForm',
'ProviderForm',
'ProviderNetworkForm',
'VirtualCircuitForm',
'VirtualCircuitTerminationForm',
@@ -91,13 +96,13 @@ class ProviderNetworkForm(PrimaryModelForm):
class CircuitTypeForm(OrganizationalModelForm):
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = CircuitType
fields = [
'name', 'slug', 'color', 'description', 'comments', 'tags',
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
]

View File

@@ -3,9 +3,9 @@ import strawberry
from circuits.choices import *
__all__ = (
'CircuitPriorityEnum',
'CircuitStatusEnum',
'CircuitTerminationSideEnum',
'CircuitPriorityEnum',
'VirtualCircuitTerminationRoleEnum',
)

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django

View File

@@ -1,10 +1,10 @@
from datetime import date
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup
from circuits import models
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from ipam.graphql.filters import ASNFilter
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
__all__ = (
@@ -27,8 +28,8 @@ __all__ = (
'CircuitGroupFilter',
'CircuitTerminationFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
'ProviderFilter',
'ProviderNetworkFilter',
'VirtualCircuitFilter',
'VirtualCircuitTerminationFilter',

View File

@@ -1,5 +1,3 @@
from typing import List
import strawberry
import strawberry_django
@@ -9,34 +7,34 @@ from .types import *
@strawberry.type(name="Query")
class CircuitsQuery:
circuit: CircuitType = strawberry_django.field()
circuit_list: List[CircuitType] = strawberry_django.field()
circuit_list: list[CircuitType] = strawberry_django.field()
circuit_termination: CircuitTerminationType = strawberry_django.field()
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
circuit_termination_list: list[CircuitTerminationType] = strawberry_django.field()
circuit_type: CircuitTypeType = strawberry_django.field()
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
circuit_type_list: list[CircuitTypeType] = strawberry_django.field()
circuit_group: CircuitGroupType = strawberry_django.field()
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
circuit_group_list: list[CircuitGroupType] = strawberry_django.field()
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
circuit_group_assignment_list: list[CircuitGroupAssignmentType] = strawberry_django.field()
provider: ProviderType = strawberry_django.field()
provider_list: List[ProviderType] = strawberry_django.field()
provider_list: list[ProviderType] = strawberry_django.field()
provider_account: ProviderAccountType = strawberry_django.field()
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
provider_account_list: list[ProviderAccountType] = strawberry_django.field()
provider_network: ProviderNetworkType = strawberry_django.field()
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
provider_network_list: list[ProviderNetworkType] = strawberry_django.field()
virtual_circuit: VirtualCircuitType = strawberry_django.field()
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
virtual_circuit_list: list[VirtualCircuitType] = strawberry_django.field()
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_termination_list: list[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
virtual_circuit_type_list: list[VirtualCircuitTypeType] = strawberry_django.field()

View File

@@ -1,4 +1,4 @@
from typing import Annotated, List, TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
@@ -8,6 +8,7 @@ from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
from tenancy.graphql.types import TenantType
from .filters import *
if TYPE_CHECKING:
@@ -20,9 +21,9 @@ __all__ = (
'CircuitTerminationType',
'CircuitType',
'CircuitTypeType',
'ProviderType',
'ProviderAccountType',
'ProviderNetworkType',
'ProviderType',
'VirtualCircuitTerminationType',
'VirtualCircuitType',
'VirtualCircuitTypeType',
@@ -36,10 +37,10 @@ __all__ = (
pagination=True
)
class ProviderType(ContactsMixin, PrimaryObjectType):
networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
accounts: List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
networks: list[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
asns: list[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
accounts: list[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -50,7 +51,7 @@ class ProviderType(ContactsMixin, PrimaryObjectType):
)
class ProviderAccountType(ContactsMixin, PrimaryObjectType):
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')]]
@strawberry_django.type(
@@ -61,7 +62,7 @@ class ProviderAccountType(ContactsMixin, PrimaryObjectType):
)
class ProviderNetworkType(PrimaryObjectType):
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')]]
@strawberry_django.type(
@@ -71,16 +72,17 @@ class ProviderNetworkType(PrimaryObjectType):
pagination=True
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
circuit: Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
@strawberry_django.field
def termination(self) -> Annotated[Union[
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitTerminationTerminationType")] | None:
def termination(self) -> Annotated[
Annotated['LocationType', strawberry.lazy('dcim.graphql.types')]
| Annotated['RegionType', strawberry.lazy('dcim.graphql.types')]
| Annotated['SiteGroupType', strawberry.lazy('dcim.graphql.types')]
| Annotated['SiteType', strawberry.lazy('dcim.graphql.types')]
| Annotated['ProviderNetworkType', strawberry.lazy('circuits.graphql.types')],
strawberry.union('CircuitTerminationTerminationType'),
] | None:
return self.termination
@@ -93,7 +95,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
class CircuitTypeType(OrganizationalObjectType):
color: str
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -109,7 +111,7 @@ class CircuitType(PrimaryObjectType, ContactsMixin):
termination_z: CircuitTerminationType | None
type: CircuitTypeType
tenant: TenantType | None
terminations: List[CircuitTerminationType]
terminations: list[CircuitTerminationType]
@strawberry_django.type(
@@ -129,13 +131,14 @@ class CircuitGroupType(OrganizationalObjectType):
pagination=True
)
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
group: Annotated['CircuitGroupType', strawberry.lazy('circuits.graphql.types')]
@strawberry_django.field
def member(self) -> Annotated[Union[
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
def member(self) -> Annotated[
Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
| Annotated['VirtualCircuitType', strawberry.lazy('circuits.graphql.types')],
strawberry.union('CircuitGroupAssignmentMemberType'),
] | None:
return self.member
@@ -148,7 +151,7 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
class VirtualCircuitTypeType(OrganizationalObjectType):
color: str
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
virtual_circuits: list[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -174,11 +177,11 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
filters=VirtualCircuitFilter,
pagination=True
)
class VirtualCircuitType(PrimaryObjectType):
class VirtualCircuitType(ContactsMixin, PrimaryObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
provider_account: ProviderAccountType | None
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
select_related=["type"]
)
tenant: TenantType | None
terminations: List[VirtualCircuitTerminationType]
terminations: list[VirtualCircuitTerminationType]

View File

@@ -1,7 +1,8 @@
import django.db.models.deletion
from django.db import migrations, models
import ipam.fields
from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@@ -1,6 +1,6 @@
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -1,6 +1,7 @@
# Generated by Django 4.2.5 on 2023-10-20 21:25
from django.db import migrations
import utilities.fields

View File

@@ -1,8 +1,9 @@
import django.db.models.deletion
import taggit.managers
import utilities.json
from django.db import migrations, models
import utilities.json
class Migration(migrations.Migration):
dependencies = [

View File

@@ -8,10 +8,16 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.mixins import DistanceMixin
from netbox.models.features import (
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
ContactsMixin,
CustomFieldsMixin,
CustomLinksMixin,
ExportTemplatesMixin,
ImageAttachmentsMixin,
TagsMixin,
)
from netbox.models.mixins import DistanceMixin
from .base import BaseCircuitType
__all__ = (

View File

@@ -6,9 +6,9 @@ from netbox.models import PrimaryModel
from netbox.models.features import ContactsMixin
__all__ = (
'ProviderNetwork',
'Provider',
'ProviderAccount',
'ProviderNetwork',
)

View File

@@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
from .base import BaseCircuitType
__all__ = (
@@ -29,7 +30,7 @@ class VirtualCircuitType(BaseCircuitType):
verbose_name_plural = _('virtual circuit types')
class VirtualCircuit(PrimaryModel):
class VirtualCircuit(ContactsMixin, PrimaryModel):
"""
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
"""
@@ -184,6 +185,8 @@ class VirtualCircuitTermination(
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
)
# Fallback for unexpected roles
return self.virtual_circuit.terminations.none()
def clean(self):
super().clean()

View File

@@ -1,4 +1,5 @@
from netbox.search import SearchIndex, register_search
from . import models

View File

@@ -2,6 +2,7 @@ from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from dcim.signals import rebuild_paths
from .models import CircuitTermination

View File

@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from circuits.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .columns import CommitRateColumn
__all__ = (

View File

@@ -7,9 +7,9 @@ from netbox.tables import PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin
__all__ = (
'ProviderTable',
'ProviderAccountTable',
'ProviderNetworkTable',
'ProviderTable',
)

View File

@@ -71,7 +71,7 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
model = VirtualCircuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
'tenant_group', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',

View File

@@ -5,7 +5,16 @@ from circuits.filtersets import *
from circuits.models import *
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
Cable,
Device,
DeviceRole,
DeviceType,
Interface,
Location,
Manufacturer,
Region,
Site,
SiteGroup,
)
from ipam.models import ASN, RIR
from netbox.choices import DistanceUnitChoices

View File

@@ -1,4 +1,4 @@
from django.test import RequestFactory, tag, TestCase
from django.test import RequestFactory, TestCase, tag
from circuits.models import CircuitTermination
from circuits.tables import CircuitTerminationTable

View File

@@ -1,6 +1,7 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'circuits'

View File

@@ -5,14 +5,15 @@ from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, B
from netbox.views import generic
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
#
# Providers
#
@register_model_view(Provider, 'list', path='', detail=False)
class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate(

View File

@@ -2,10 +2,14 @@ import re
import typing
from collections import OrderedDict
from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
build_basic_type,
build_choice_field,
build_media_type_object,
build_object_type,
get_doc,
)
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import Direction
@@ -35,7 +39,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
if direction == 'request':
return build_cf
elif direction == "response":
if direction == "response":
value = build_cf
label = {
**build_basic_type(OpenApiTypes.STR),
@@ -49,6 +53,10 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
}
)
# TODO: This function should never implicitly/explicitly return `None`
# The fallback should be well-defined (drf-spectacular expects request/response naming).
return None
def viewset_handles_bulk_create(view):
"""Check if view automatically provides list-based bulk create"""
@@ -71,8 +79,7 @@ class NetBoxAutoSchema(AutoSchema):
def is_bulk_action(self):
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
return True
else:
return False
return False
def get_operation_id(self):
"""
@@ -312,8 +319,7 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
if direction == "response":
component = auto_schema.resolve_serializer(self.target.serializer, direction)
return component.ref if component else None
else:
return build_basic_type(OpenApiTypes.INT)
return build_basic_type(OpenApiTypes.INT)
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):

View File

@@ -34,14 +34,14 @@ class ObjectTypeSerializer(BaseModelSerializer):
@extend_schema_field(OpenApiTypes.STR)
def get_rest_api_endpoint(self, obj):
if not (model := obj.model_class()):
return
return None
try:
return get_action_url(model, action='list', rest_api=True)
except NoReverseMatch:
return
return None
@extend_schema_field(OpenApiTypes.STR)
def get_description(self, obj):
if not (model := obj.model_class()):
return
return None
return inspect.getdoc(model)

View File

@@ -2,8 +2,8 @@ from rest_framework import serializers
from rest_framework.reverse import reverse
__all__ = (
'BackgroundTaskSerializer',
'BackgroundQueueSerializer',
'BackgroundTaskSerializer',
'BackgroundWorkerSerializer',
)

View File

@@ -1,4 +1,5 @@
from netbox.api.routers import NetBoxRouter
from . import views
app_name = 'core-api'

View File

@@ -23,6 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.api import IsSuperuser
from . import serializers
@@ -284,5 +285,4 @@ class BackgroundTaskViewSet(BaseRQViewSet):
stopped_jobs = stop_rq_job(id)
if len(stopped_jobs) == 1:
return HttpResponse(status=200)
else:
return HttpResponse(status=204)
return HttpResponse(status=204)

View File

@@ -6,7 +6,7 @@ from django.db.migrations.operations import AlterModelOptions
from django.utils.translation import gettext as _
from core.events import *
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
from netbox.events import EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING, EventType
from utilities.migration import custom_deconstruct
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
@@ -23,9 +23,10 @@ class CoreConfig(AppConfig):
def ready(self):
from core.api import schema # noqa: F401
from core.checks import check_duplicate_indexes # noqa: F401
from netbox.models.features import register_models
from . import data_backends, events, search # noqa: F401
from netbox import context_managers # noqa: F401
from netbox.models.features import register_models
from . import data_backends, events, search # noqa: F401
# Register models
register_models(*self.get_models())

View File

@@ -1,6 +1,6 @@
from django.core.checks import Error, register, Tags
from django.db.models import Index, UniqueConstraint
from django.apps import apps
from django.core.checks import Error, Tags, register
from django.db.models import Index, UniqueConstraint
__all__ = (
'check_duplicate_indexes',

View File

@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
#
# Data sources
#
class DataSourceStatusChoices(ChoiceSet):
NEW = 'new'
QUEUED = 'queued'

View File

@@ -15,6 +15,7 @@ from netbox.utils import register_data_backend
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
from utilities.proxy import resolve_proxies
from utilities.socks import ProxyPoolManager
from .exceptions import SyncError
__all__ = (

View File

@@ -1,5 +1,4 @@
import logging
from dataclasses import dataclass, field
from datetime import datetime

View File

@@ -6,8 +6,9 @@ from django.utils.translation import gettext as _
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.filters import ContentTypeFilter
from utilities.filters import MultiValueContentTypeFilter
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -25,14 +26,17 @@ __all__ = (
class DataSourceFilterSet(PrimaryModelFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=get_data_backend_choices,
distinct=False,
null_value=None
)
status = django_filters.MultipleChoiceFilter(
choices=DataSourceStatusChoices,
distinct=False,
null_value=None
)
sync_interval = django_filters.MultipleChoiceFilter(
choices=JobIntervalChoices,
distinct=False,
null_value=None
)
@@ -57,11 +61,13 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
)
source_id = django_filters.ModelMultipleChoiceFilter(
queryset=DataSource.objects.all(),
distinct=False,
label=_('Data source (ID)'),
)
source = django_filters.ModelMultipleChoiceFilter(
field_name='source__name',
queryset=DataSource.objects.all(),
distinct=False,
to_field_name='name',
label=_('Data source (name)'),
)
@@ -86,9 +92,10 @@ class JobFilterSet(BaseFilterSet):
)
object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.with_feature('jobs'),
distinct=False,
field_name='object_type_id',
)
object_type = ContentTypeFilter()
object_type = MultiValueContentTypeFilter()
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
field_name='created',
@@ -127,6 +134,7 @@ class JobFilterSet(BaseFilterSet):
)
status = django_filters.MultipleChoiceFilter(
choices=JobStatusChoices,
distinct=False,
null_value=None
)
queue_name = django_filters.CharFilter()
@@ -180,18 +188,21 @@ class ObjectChangeFilterSet(BaseFilterSet):
label=_('Search'),
)
time = django_filters.DateTimeFromToRangeFilter()
changed_object_type = ContentTypeFilter()
changed_object_type = MultiValueContentTypeFilter()
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
queryset=ContentType.objects.all(),
distinct=False,
)
related_object_type = ContentTypeFilter()
related_object_type = MultiValueContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
distinct=False,
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
queryset=User.objects.all(),
distinct=False,
to_field_name='username',
label=_('User name'),
)

View File

@@ -9,7 +9,10 @@ from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
ContentTypeChoiceField,
ContentTypeMultipleChoiceField,
DynamicModelMultipleChoiceField,
TagFilterField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker

View File

@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
from netbox.config import get_config, PARAMS
from netbox.config import PARAMS, get_config
from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices

View File

@@ -1,6 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
@@ -9,6 +9,7 @@ from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLook
from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
from .enums import *
if TYPE_CHECKING:
@@ -16,10 +17,10 @@ if TYPE_CHECKING:
from users.graphql.filters import UserFilter
__all__ = (
'ContentTypeFilter',
'DataFileFilter',
'DataSourceFilter',
'ObjectChangeFilter',
'ContentTypeFilter',
)

View File

@@ -1,4 +1,4 @@
from typing import Annotated, List, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
@@ -20,7 +20,7 @@ __all__ = (
class ChangelogMixin:
@strawberry_django.field
def changelog(self, info: Info) -> List[Annotated['ObjectChangeType', strawberry.lazy('.types')]]: # noqa: F821
def changelog(self, info: Info) -> list[Annotated['ObjectChangeType', strawberry.lazy('.types')]]: # noqa: F821
content_type = ContentType.objects.get_for_model(self)
object_changes = ObjectChange.objects.filter(
changed_object_type=content_type,

View File

@@ -1,5 +1,3 @@
from typing import List
import strawberry
import strawberry_django
@@ -9,7 +7,7 @@ from .types import *
@strawberry.type(name="Query")
class CoreQuery:
data_file: DataFileType = strawberry_django.field()
data_file_list: List[DataFileType] = strawberry_django.field()
data_file_list: list[DataFileType] = strawberry_django.field()
data_source: DataSourceType = strawberry_django.field()
data_source_list: List[DataSourceType] = strawberry_django.field()
data_source_list: list[DataSourceType] = strawberry_django.field()

View File

@@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated
import strawberry
import strawberry_django
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType as DjangoContentType
from core import models
from netbox.graphql.types import BaseObjectType, PrimaryObjectType
from .filters import *
__all__ = (
@@ -33,7 +34,7 @@ class DataFileType(BaseObjectType):
pagination=True
)
class DataSourceType(PrimaryObjectType):
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
datafiles: list[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
@strawberry_django.type(

View File

@@ -13,6 +13,7 @@ from netbox.config import Config
from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend
from utilities.proxy import resolve_proxies
from .choices import DataSourceStatusChoices, JobIntervalChoices
from .models import DataSource

View File

@@ -144,7 +144,7 @@ class Command(BaseCommand):
# If Python code has been passed, execute it and exit.
if options['command']:
exec(options['command'], namespace)
return
return None
# Try to enable tab-complete
try:

View File

@@ -4,7 +4,6 @@ from django_rq.management.commands.rqworker import Command as _Command
from netbox.registry import registry
DEFAULT_QUEUES = ('high', 'default', 'low')
logger = logging.getLogger('netbox.rqworker')

View File

@@ -1,6 +1,7 @@
import core.models.object_types
from django.db import migrations
import core.models.object_types
class Migration(migrations.Migration):
dependencies = [

View File

@@ -1,4 +1,5 @@
from .object_types import *
from .object_types import * # isort: split
from .change_logging import *
from .config import *
from .data import *

View File

@@ -10,8 +10,7 @@ from mptt.models import MPTTModel
from core.choices import ObjectChangeActionChoices
from core.querysets import ObjectChangeQuerySet
from netbox.models.features import ChangeLoggingMixin
from netbox.models.features import has_feature
from netbox.models.features import ChangeLoggingMixin, has_feature
from utilities.data import shallow_compare_dict
__all__ = (

View File

@@ -1,7 +1,8 @@
from django.core.cache import cache
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from utilities.querysets import RestrictedQuerySet

View File

@@ -19,6 +19,7 @@ from netbox.models import PrimaryModel
from netbox.models.features import JobsMixin
from netbox.registry import registry
from utilities.querysets import RestrictedQuerySet
from ..choices import *
from ..exceptions import SyncError
@@ -97,6 +98,7 @@ class DataSource(JobsMixin, PrimaryModel):
def get_type_display(self):
if backend := registry['data_backends'].get(self.type):
return backend.label
return None
def get_status_color(self):
return DataSourceStatusChoices.colors.get(self.status)

View File

@@ -4,15 +4,16 @@ from functools import cached_property
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.core.files.storage import storages
from django.db import models
from django.utils.translation import gettext as _
from ..choices import ManagedFileRootPathChoices
from extras.storage import ScriptFileSystemStorage
from netbox.models.features import SyncedDataMixin
from utilities.querysets import RestrictedQuerySet
from ..choices import ManagedFileRootPathChoices
__all__ = (
'ManagedFile',
)
@@ -78,8 +79,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
'scripts': settings.SCRIPTS_ROOT,
'reports': settings.REPORTS_ROOT,
}[self.file_root]
else:
return ""
return ""
def sync_data(self):
if self.data_file:
@@ -89,6 +89,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
with storage.open(self.full_path, 'wb+') as new_file:
new_file.write(self.data_file.data)
sync_data.alters_data = True
@cached_property
def storage(self):

View File

@@ -146,7 +146,7 @@ class Job(models.Model):
if self.object_type:
if self.object_type.model == 'reportmodule':
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
elif self.object_type.model == 'scriptmodule':
if self.object_type.model == 'scriptmodule':
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
return reverse('core:job', args=[self.pk])
@@ -216,6 +216,7 @@ class Job(models.Model):
# Send signal
job_start.send(self)
start.alters_data = True
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
"""
@@ -245,6 +246,7 @@ class Job(models.Model):
# Send signal
job_end.send(self)
terminate.alters_data = True
def log(self, record: logging.LogRecord):
"""

View File

@@ -218,19 +218,22 @@ class ObjectType(ContentType):
def app_verbose_name(self):
if model := self.model_class():
return model._meta.app_config.verbose_name
return None
@property
def model_verbose_name(self):
if model := self.model_class():
return model._meta.verbose_name
return None
@property
def model_verbose_name_plural(self):
if model := self.model_class():
return model._meta.verbose_name_plural
return None
@property
def is_plugin_model(self):
if not (model := self.model_class()):
return # Return null if model class is invalid
return None # Return null if model class is invalid
return isinstance(model._meta.app_config, PluginConfig)

View File

@@ -1,7 +1,6 @@
import datetime
import importlib
from dataclasses import dataclass, field
from typing import Optional
import requests
from django.conf import settings
@@ -55,7 +54,7 @@ class Plugin:
tag_line: str = ''
description_short: str = ''
slug: str = ''
author: Optional[PluginAuthor] = None
author: PluginAuthor | None = None
created_at: datetime.datetime = None
updated_at: datetime.datetime = None
license_type: str = ''

View File

@@ -1,4 +1,5 @@
from netbox.search import SearchIndex, register_search
from . import models

View File

@@ -3,11 +3,11 @@ from threading import local
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.signals import request_finished
from django.db.models import CASCADE, RESTRICT
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
from django.dispatch import receiver, Signal
from django.core.signals import request_finished
from django.dispatch import Signal, receiver
from django.utils.translation import gettext_lazy as _
from django_prometheus.models import model_deletes, model_inserts, model_updates
@@ -18,10 +18,11 @@ from extras.events import enqueue_event
from extras.models import Tag
from extras.utils import run_validators
from netbox.config import get_config
from utilities.data import get_config_value_ci
from netbox.context import current_request, events_queue
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
from utilities.data import get_config_value_ci
from utilities.exceptions import AbortRequest
from .models import ConfigRevision, DataSource, ObjectChange
__all__ = (
@@ -209,22 +210,28 @@ def handle_deleted_object(sender, instance, **kwargs):
# for the forward direction of the relationship, ensuring that the change is recorded.
# Similarly, for many-to-one relationships, we set the value on the related object to None
# and save it to trigger a change record on that object.
for relation in instance._meta.related_objects:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
if not issubclass(related_model, ChangeLoggingMixin):
# We only care about triggering the m2m_changed signal for models which support
# change logging
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
setattr(obj, related_field_name, None)
obj.save()
#
# Skip this for private models (e.g. CablePath) whose lifecycle is an internal
# implementation detail. Django's on_delete handlers (e.g. SET_NULL) already take
# care of the database integrity; recording changelog entries for the related
# objects would be spurious. (Ref: #21390)
if not getattr(instance, '_netbox_private', False):
for relation in instance._meta.related_objects:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
if not issubclass(related_model, ChangeLoggingMixin):
# We only care about triggering the m2m_changed signal for models which support
# change logging
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
setattr(obj, related_field_name, None)
obj.save()
# Enqueue the object for event processing
queue = events_queue.get()

View File

@@ -2,5 +2,5 @@ from .change_logging import *
from .config import *
from .data import *
from .jobs import *
from .tasks import *
from .plugins import *
from .tasks import *

View File

@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from core.models import ObjectChange
from netbox.tables import NetBoxTable, columns
from .template_code import *
__all__ = (

View File

@@ -1,8 +1,9 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from core.models import *
from netbox.tables import NetBoxTable, PrimaryModelTable, columns
from .columns import BackendTypeColumn
from .template_code import DATA_SOURCE_SYNC_BUTTON

View File

@@ -1,10 +1,10 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, NetBoxTable, columns
from core.constants import JOB_LOG_ENTRY_LEVELS
from core.models import Job
from core.tables.columns import BadgeColumn
from netbox.tables import BaseTable, NetBoxTable, columns
class JobTable(NetBoxTable):

View File

@@ -2,6 +2,7 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, columns
from .template_code import PLUGIN_IS_INSTALLED, PLUGIN_NAME_TEMPLATE
__all__ = (

View File

@@ -1,17 +1,19 @@
import uuid
from django_rq import get_queue
from django_rq.workers import get_worker
from django.urls import reverse
from django.utils import timezone
from rq.job import Job as RQ_Job, JobStatus
from django_rq import get_queue
from django_rq.workers import get_worker
from rest_framework import status
from rq.job import Job as RQ_Job
from rq.job import JobStatus
from rq.registry import FailedJobRegistry, StartedJobRegistry
from rest_framework import status
from users.constants import TOKEN_PREFIX
from users.models import Token, User
from utilities.testing import APITestCase, APIViewTestCases, TestCase
from utilities.testing.utils import disable_logging
from ..models import *

View File

@@ -7,7 +7,16 @@ from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.choices import InterfaceTypeChoices, ModuleStatusChoices, SiteStatusChoices
from dcim.models import (
Cable, CableTermination, Device, DeviceRole, DeviceType, Manufacturer, Module, ModuleBay, ModuleType, Interface,
Cable,
CableTermination,
Device,
DeviceRole,
DeviceType,
Interface,
Manufacturer,
Module,
ModuleBay,
ModuleType,
Site,
)
from extras.choices import *

View File

@@ -1,5 +1,5 @@
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
@@ -8,6 +8,7 @@ from dcim.models import Site
from ipam.models import IPAddress
from users.models import User
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
from ..choices import *
from ..filtersets import *
from ..models import *
@@ -99,21 +100,21 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
DataFile(
source=data_sources[0],
path='dir1/file1.txt',
last_updated=datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 1, 0, 0, 0, tzinfo=UTC),
size=1000,
hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1'
),
DataFile(
source=data_sources[1],
path='dir1/file2.txt',
last_updated=datetime(2023, 1, 2, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 2, 0, 0, 0, tzinfo=UTC),
size=2000,
hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2'
),
DataFile(
source=data_sources[2],
path='dir1/file3.txt',
last_updated=datetime(2023, 1, 3, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 3, 0, 0, 0, tzinfo=UTC),
size=3000,
hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a'
),
@@ -237,9 +238,9 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_changed_object_type(self):
params = {'changed_object_type': 'dcim.site'}
params = {'changed_object_type': ['dcim.site']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
params = {'changed_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

View File

@@ -1,12 +1,12 @@
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase
from core.models import DataSource, Job, ObjectType
from core.choices import ObjectChangeActionChoices
from dcim.models import Site, Location, Device
from core.models import DataSource, Job, ObjectType
from dcim.models import Device, Location, Site
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED

View File

@@ -4,6 +4,7 @@ Unit tests for OpenAPI schema generation.
Refs: #20638
"""
import json
from django.test import TestCase

View File

@@ -8,7 +8,8 @@ from django.utils import timezone
from django_rq import get_queue
from django_rq.settings import QUEUES_MAP
from django_rq.workers import get_worker
from rq.job import Job as RQ_Job, JobStatus
from rq.job import Job as RQ_Job
from rq.job import JobStatus
from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
from core.choices import ObjectChangeActionChoices

View File

@@ -1,6 +1,7 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'core'

View File

@@ -1,11 +1,12 @@
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django_rq.queues import get_queue, get_queue_by_index, get_redis_connection
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
from django_rq.settings import QUEUES_LIST, QUEUES_MAP
from django_rq.utils import get_jobs, stop_jobs
from rq import requeue_job
from rq.exceptions import NoSuchJobError
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
from rq.job import Job as RQ_Job
from rq.job import JobStatus as RQJobStatus
from rq.registry import (
DeferredJobRegistry,
FailedJobRegistry,

View File

@@ -1,27 +1,29 @@
import json
import platform
from copy import deepcopy
from django import __version__ as django_version
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.cache import cache
from django.db import connection, ProgrammingError
from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.db import ProgrammingError, connection
from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
from django_rq.settings import QUEUES_LIST, QUEUES_MAP
from django_rq.utils import get_statistics
from rq.exceptions import NoSuchJobError
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
from rq.job import Job as RQ_Job
from rq.job import JobStatus as RQJobStatus
from rq.worker import Worker
from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from netbox.config import get_config, PARAMS
from netbox.config import PARAMS, get_config
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.plugins.utils import get_installed_plugins
from netbox.views import generic
@@ -40,17 +42,18 @@ from utilities.views import (
ViewTab,
register_model_view,
)
from . import filtersets, forms, tables
from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
#
# Data sources
#
@register_model_view(DataSource, 'list', path='', detail=False)
class DataSourceListView(generic.ObjectListView):
queryset = DataSource.objects.annotate(
@@ -310,6 +313,22 @@ class ConfigRevisionListView(generic.ObjectListView):
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
def get_extra_context(self, request, instance):
"""
Retrieve additional context for a given request and instance.
"""
# Copy the revision data to avoid modifying the original
config = deepcopy(instance.data or {})
# Serialize any JSON-based classes
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
if attr in config:
config[attr] = json.dumps(config[attr], cls=ConfigJSONEncoder, indent=4)
return {
'config': config,
}
@register_model_view(ConfigRevision, 'add', detail=False)
class ConfigRevisionEditView(generic.ObjectEditView):
@@ -617,8 +636,8 @@ class SystemView(UserPassesTestMixin, View):
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
return response
# Serialize any CustomValidator classes
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
# Serialize any JSON-based classes
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
if hasattr(config, attr) and getattr(config, attr, None):
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
@@ -665,7 +684,7 @@ class PluginListView(BasePluginView):
plugins = [plugin for plugin in plugins if not plugin.hidden]
table = CatalogPluginTable(plugins, user=request.user)
table = CatalogPluginTable(plugins)
table.configure(request)
# If this is an HTMX request, return only the rendered table HTML
@@ -688,7 +707,7 @@ class PluginView(BasePluginView):
raise Http404(_("Plugin {name} not found").format(name=name))
plugin = plugins[name]
table = PluginVersionTable(plugin.release_recent_history, user=request.user)
table = PluginVersionTable(plugin.release_recent_history)
table.configure(request)
return render(request, 'core/plugin.html', {

View File

@@ -1,13 +1,13 @@
from .serializers_.cables import *
from .serializers_.sites import *
from .serializers_.racks import *
from .serializers_.device_components import *
from .serializers_.devices import *
from .serializers_.devicetype_components import *
from .serializers_.devicetypes import *
from .serializers_.manufacturers import *
from .serializers_.platforms import *
from .serializers_.roles import *
from .serializers_.devicetypes import *
from .serializers_.devicetype_components import *
from .serializers_.virtualchassis import *
from .serializers_.devices import *
from .serializers_.device_components import *
from .serializers_.power import *
from .serializers_.racks import *
from .serializers_.rackunits import *
from .serializers_.roles import *
from .serializers_.sites import *
from .serializers_.virtualchassis import *

View File

@@ -23,6 +23,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
def get_connected_endpoints_type(self, obj):
if endpoints := obj.connected_endpoints:
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
return None
@extend_schema_field(serializers.ListField(allow_null=True))
def get_connected_endpoints(self, obj):
@@ -33,6 +34,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
serializer = get_serializer_for_model(endpoints[0])
context = {'request': self.context['request']}
return serializer(endpoints, nested=True, many=True, context=context).data
return None
@extend_schema_field(serializers.BooleanField)
def get_connected_endpoints_reachable(self, obj):

View File

@@ -7,7 +7,10 @@ from dcim.models import Cable, CablePath, CableTermination
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import (
BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer, PrimaryModelSerializer,
BaseModelSerializer,
GenericObjectSerializer,
NetBoxModelSerializer,
PrimaryModelSerializer,
)
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model

Some files were not shown because too many files have changed in this diff Show More