mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-28 08:07:45 -06:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03f3f5c957 | ||
|
|
fe7fb94e44 | ||
|
|
82b9e4ca26 | ||
|
|
457fb977a7 | ||
|
|
13c20957a6 | ||
|
|
30208549ba | ||
|
|
bf286df670 | ||
|
|
2be257db48 | ||
|
|
2207ea1a32 | ||
|
|
10e1ae8292 | ||
|
|
f8f5ab8d61 | ||
|
|
92317248a3 | ||
|
|
426e6439e3 | ||
|
|
621b29cd71 | ||
|
|
8f5d273f08 | ||
|
|
45779a24a4 | ||
|
|
f17bbe610e | ||
|
|
bad820001d | ||
|
|
a5106b858d | ||
|
|
bbd5e9cab9 | ||
|
|
12231ad71a | ||
|
|
88ef9ecfa3 | ||
|
|
6f78b3d0cd | ||
|
|
d3f42deb32 | ||
|
|
db4fb8f406 | ||
|
|
5b8eaced1a | ||
|
|
ada0c7f687 | ||
|
|
b750d0dff2 | ||
|
|
e1e514251e | ||
|
|
7d80a45bf8 | ||
|
|
09854a3d54 | ||
|
|
39a96ddf3a | ||
|
|
be26f86b62 | ||
|
|
fd2bcda8b8 | ||
|
|
817d7efee3 | ||
|
|
9a1d9365cd | ||
|
|
ada4a4b93c | ||
|
|
64a98fd87f | ||
|
|
bd8e00a935 | ||
|
|
af5a600583 | ||
|
|
8ab73501d1 | ||
|
|
447e108d97 |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.6
|
||||
placeholder: v4.2.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.2.6
|
||||
placeholder: v4.2.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -12,6 +12,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
1
.github/workflows/close-stale-issues.yml
vendored
1
.github/workflows/close-stale-issues.yml
vendored
@@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
|
||||
1
.github/workflows/lock-threads.yml
vendored
1
.github/workflows/lock-threads.yml
vendored
@@ -13,6 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
|
||||
@@ -13,6 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
makemessages:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NETBOX_CONFIGURATION: netbox.configuration_testing
|
||||
|
||||
@@ -427,6 +427,7 @@
|
||||
"e3",
|
||||
"xdsl",
|
||||
"docsis",
|
||||
"moca",
|
||||
"bpon",
|
||||
"epon",
|
||||
"10g-epon",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Release Checklist
|
||||
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
|
||||
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of releases:
|
||||
|
||||
* Major release (e.g. v3.7.8 to v4.0.0)
|
||||
* Minor release (e.g. v4.0.10 to v4.1.0)
|
||||
* Patch release (e.g. v4.1.0 to v4.1.1)
|
||||
|
||||
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
While major releases generally introduce some very substantial changes to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
|
||||
|
||||
For patch releases (e.g. upgrading from v4.2.2 to v4.2.3), begin at the [patch releases](#patch-releases) heading below. For minor or major releases, complete the entire checklist.
|
||||
|
||||
@@ -31,6 +31,29 @@ Close the [release milestone](https://github.com/netbox-community/netbox/milesto
|
||||
|
||||
Check that a link to the release notes for the new version is present in the navigation menu (defined in `mkdocs.yml`), and that a summary of all major new features has been added to `docs/index.md`.
|
||||
|
||||
### Update the Dependency Requirements Matrix
|
||||
|
||||
For every minor release, update the dependency requirements matrix in `docs/installation/upgrading.md` ("All versions") to reflect the supported versions of Python, PostgreSQL, and Redis:
|
||||
|
||||
1. Add a new row with the supported dependency versions.
|
||||
2. Include a documentation link using the release tag format: `https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md`
|
||||
3. Bold any version changes for clarity.
|
||||
|
||||
**Example Update:**
|
||||
|
||||
```markdown
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
```
|
||||
|
||||
### Update System Requirements
|
||||
|
||||
If a new Django release is adopted or other major dependencies (Python, PostgreSQL, Redis) change:
|
||||
|
||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
Start the documentation server and navigate to the current version of the installation docs:
|
||||
@@ -39,15 +62,25 @@ Start the documentation server and navigate to the current version of the instal
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation and ensure that it is kept up to date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||
|
||||
### Test Upgrade Paths
|
||||
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
|
||||
Upgrading from a previous version typically involves database migrations, which must work without errors.
|
||||
Test the following supported upgrade paths:
|
||||
|
||||
- From one minor version to another within the same major version (e.g. 4.0 to 4.1).
|
||||
- From the latest patch version of the previous minor version (e.g. 3.7 to 4.0 or 4.1).
|
||||
|
||||
Prior to release, test all these supported paths by loading demo data from the source version and performing:
|
||||
|
||||
```no-highlight
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
### Merge the `feature` Branch
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for the patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
@@ -59,7 +92,7 @@ After the release of a new minor version, generate a new demo data snapshot comp
|
||||
|
||||
### Create a Release Branch
|
||||
|
||||
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
|
||||
Begin by creating a new branch (based on `main`) to effect the release. This will comprise the changes listed below.
|
||||
|
||||
```
|
||||
git checkout main
|
||||
@@ -136,7 +169,7 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
!!! tip
|
||||
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include key words or phrases (such as exception names) that can be easily searched.
|
||||
Put yourself in the shoes of the user when recording change notes. Focus on the effect that each change has for the end user, rather than the specific bits of code that were modified in a PR. Ensure that each message conveys meaning absent context of the initial feature request or bug report. Remember to include keywords or phrases (such as exception names) that can be easily searched.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
|
||||
@@ -17,11 +17,52 @@ Prior to upgrading your NetBox instance, be sure to carefully review all [releas
|
||||
|
||||
NetBox requires the following dependencies:
|
||||
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
=== "Current Version"
|
||||
|
||||
| Dependency | Supported Versions |
|
||||
|------------|--------------------|
|
||||
| Python | 3.10, 3.11, 3.12 |
|
||||
| PostgreSQL | 13+ |
|
||||
| Redis | 4.0+ |
|
||||
|
||||
=== "All Versions"
|
||||
|
||||
| NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation |
|
||||
|:--------------:|:----------:|:----------:|:--------------:|:---------:|:-------------------------------------------------------------------------------------------------:|
|
||||
| 4.2 | 3.10 | 3.12 | **13** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.2.0/docs/installation/index.md) |
|
||||
| 4.1 | 3.10 | 3.12 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.1.0/docs/installation/index.md) |
|
||||
| 4.0 | **3.10** | **3.12** | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.0.0/docs/installation/index.md) |
|
||||
| 3.7 | 3.8 | 3.11 | 12 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.7.0/docs/installation/index.md) |
|
||||
| 3.6 | 3.8 | **3.11** | **12** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.6.0/docs/installation/index.md) |
|
||||
| 3.5 | 3.8 | 3.10 | 11 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.5.0/docs/installation/index.md) |
|
||||
| 3.4 | 3.8 | 3.10 | **11** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.4.0/docs/installation/index.md) |
|
||||
| 3.3 | 3.8 | 3.10 | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.3.0/docs/installation/index.md) |
|
||||
| 3.2 | **3.8** | **3.10** | 10 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.2.0/docs/installation/index.md) |
|
||||
| 3.1 | 3.7 | 3.9 | **10** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.1.0/docs/installation/index.md) |
|
||||
| 3.0 | **3.7** | 3.9 | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v3.0.0/docs/installation/index.md) |
|
||||
| 2.11 | 3.6 | **3.9** | 9.6 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.11.0/docs/installation/index.md) |
|
||||
| 2.10 | 3.6 | 3.8 | **9.6** | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v2.10.0/docs/installation/index.md) |
|
||||
| 2.9 | 3.6 | 3.8 | 9.5 | **4.0** | [Link](https://github.com/netbox-community/netbox/blob/v2.9.0/docs/installation/index.md) |
|
||||
| 2.8 | **3.6** | **3.8** | **9.5** | **3.4** | [Link](https://github.com/netbox-community/netbox/blob/v2.8.0/docs/installation/index.md) |
|
||||
| 2.7 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.7.0/docs/installation/index.md) |
|
||||
| 2.6 | 3.5 | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.6.0/docs/installation/index.md) |
|
||||
| 2.5 | **3.5** | 3.7 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.5.0/docs/installation/index.md) |
|
||||
| 2.4 | **3.4** | **3.7** | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.4.0/docs/installation/index.md) |
|
||||
| 2.3 | 2.7 | 3.6 | 9.4 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.3.0/docs/installation/postgresql.md) |
|
||||
| 2.2 | 2.7 | 3.6 | **9.4** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.2.0/docs/installation/postgresql.md) |
|
||||
| 2.1 | 2.7 | 3.6 | 9.3 | - | [Link](https://github.com/netbox-community/netbox/blob/v2.1.0/docs/installation/postgresql.md) |
|
||||
| 2.0 | 2.7 | **3.6** | **9.3** | - | [Link](https://github.com/netbox-community/netbox/blob/v2.0.0/docs/installation/postgresql.md) |
|
||||
| 1.9 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.9.0-r1/docs/installation/postgresql.md) |
|
||||
| 1.8 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.8.0/docs/installation/postgresql.md) |
|
||||
| 1.7 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.7.0/docs/installation/postgresql.md) |
|
||||
| 1.6 | 2.7 | 3.5 | 9.2 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.6.0/docs/installation/postgresql.md) |
|
||||
| 1.5 | 2.7 | 3.5 | **9.2** | - | [Link](https://github.com/netbox-community/netbox/blob/v1.5.0/docs/installation/postgresql.md) |
|
||||
| 1.4 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.4.0/docs/installation/postgresql.md) |
|
||||
| 1.3 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.3.0/docs/installation/postgresql.md) |
|
||||
| 1.2 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.2.0/docs/installation/postgresql.md) |
|
||||
| 1.1 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/v1.1.0/docs/getting-started.md) |
|
||||
| 1.0 | 2.7 | 3.5 | 9.1 | - | [Link](https://github.com/netbox-community/netbox/blob/1.0.0/docs/getting-started.md) |
|
||||
|
||||
|
||||
## 3. Install the Latest Release
|
||||
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# NetBox v4.2
|
||||
|
||||
## v4.2.7 (2025-04-10)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16144](https://github.com/netbox-community/netbox/issues/16144) - Add support for plugin models to GetReturnURLMixin
|
||||
* [#18138](https://github.com/netbox-community/netbox/issues/18138) - Enable filtering of ObjectVar and MultiObjectVar input selections for custom fields
|
||||
* [#18656](https://github.com/netbox-community/netbox/issues/18656) - Enable FHRP group assignment when bulk importing IP addresses
|
||||
* [#18980](https://github.com/netbox-community/netbox/issues/18980) - Optimize bulk updates of custom field values when custom fields are added/removed
|
||||
* [#19018](https://github.com/netbox-community/netbox/issues/19018) - Add MoCA interface type
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#18553](https://github.com/netbox-community/netbox/issues/18553) - Avoid clearing site of assigned virtual machines when editing a cluster
|
||||
* [#18738](https://github.com/netbox-community/netbox/issues/18738) - Respect declared ordering of custom scripts within a module
|
||||
* [#18895](https://github.com/netbox-community/netbox/issues/18895) - Fix GraphQL support for interfaces which terminate virtual circuits
|
||||
* [#18904](https://github.com/netbox-community/netbox/issues/18904) - Add missing tags column to config contexts table
|
||||
* [#18964](https://github.com/netbox-community/netbox/issues/18964) - Fix "select all" behavior on object lists
|
||||
* [#18965](https://github.com/netbox-community/netbox/issues/18965) - "Run script" button should respect default commit toggle for custom scripts
|
||||
* [#18991](https://github.com/netbox-community/netbox/issues/18991) - Fix cable path tracing for pass-through ports in REST API
|
||||
* [#18999](https://github.com/netbox-community/netbox/issues/18999) - Fix filtering of inventory items with no manufacturer in GraphQL API
|
||||
* [#19021](https://github.com/netbox-community/netbox/issues/19021) - Preserve JSONField stylign when `help_text` is passed
|
||||
* [#19023](https://github.com/netbox-community/netbox/issues/19023) - `get_field_value()` should honor null values on bound form fields
|
||||
* [#19030](https://github.com/netbox-community/netbox/issues/19030) - Prevent pagination buttons from overlapping bulk action buttons on object lists
|
||||
* [#19041](https://github.com/netbox-community/netbox/issues/19041) - Fix `IndexError` exception when creating multiple front ports with a label
|
||||
* [#19092](https://github.com/netbox-community/netbox/issues/19092) - Fix clearing of scope field when bulk editing prefixes
|
||||
* [#19122](https://github.com/netbox-community/netbox/issues/19122) - Fix styling of server error page
|
||||
|
||||
---
|
||||
|
||||
## v4.2.6 (2025-03-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -3,7 +3,10 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
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 utilities.migration import custom_deconstruct
|
||||
|
||||
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
|
||||
@@ -26,6 +29,15 @@ class CoreConfig(AppConfig):
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
||||
# Clear Redis cache on startup in development mode
|
||||
if settings.DEBUG:
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
|
||||
__all__ = (
|
||||
'JOB_COMPLETED',
|
||||
'JOB_ERRORED',
|
||||
@@ -22,12 +18,3 @@ JOB_STARTED = 'job_started'
|
||||
JOB_COMPLETED = 'job_completed'
|
||||
JOB_FAILED = 'job_failed'
|
||||
JOB_ERRORED = 'job_errored'
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
|
||||
@@ -986,6 +986,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
|
||||
# Coaxial
|
||||
TYPE_DOCSIS = 'docsis'
|
||||
TYPE_MOCA = 'moca'
|
||||
|
||||
# PON
|
||||
TYPE_BPON = 'bpon'
|
||||
@@ -1182,6 +1183,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
_('Coaxial'),
|
||||
(
|
||||
(TYPE_DOCSIS, 'DOCSIS'),
|
||||
(TYPE_MOCA, 'MoCA'),
|
||||
)
|
||||
),
|
||||
(
|
||||
|
||||
@@ -153,6 +153,7 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
@@ -302,6 +303,7 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
|
||||
self.fields['rear_port'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that the number of FrontPorts to be created matches the selected number of RearPort positions
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
|
||||
@@ -30,6 +30,7 @@ class PathEndpointMixin:
|
||||
|
||||
connected_endpoints: List[Annotated[Union[
|
||||
Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["VirtualCircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
|
||||
Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
|
||||
|
||||
@@ -429,7 +429,7 @@ class InterfaceTemplateType(ModularComponentTemplateType):
|
||||
)
|
||||
class InventoryItemType(ComponentType):
|
||||
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
child_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
from django.test import override_settings, tag
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import status
|
||||
@@ -1904,6 +1904,27 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_front_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
rear_port = RearPort.objects.create(
|
||||
device=device, name='Rear Port 10', type=PortTypeChoices.TYPE_8P8C
|
||||
)
|
||||
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||
front_port = FrontPort.objects.create(
|
||||
device=device,
|
||||
name='Rear Port 10',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
rear_port=rear_port,
|
||||
)
|
||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[front_port])
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': front_port.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RearPort
|
||||
@@ -1947,6 +1968,23 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
@tag('regression') # Issue #18991
|
||||
def test_rear_port_paths(self):
|
||||
device = Device.objects.first()
|
||||
interface1 = Interface.objects.create(device=device, name='Interface 1')
|
||||
rear_port = RearPort.objects.create(
|
||||
device=device,
|
||||
name='Rear Port 10',
|
||||
type=PortTypeChoices.TYPE_8P8C,
|
||||
)
|
||||
Cable.objects.create(a_terminations=[interface1], b_terminations=[rear_port])
|
||||
|
||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||
url = reverse(f'dcim-api:{self.model._meta.model_name}-paths', kwargs={'pk': rear_port.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ModuleBay
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices
|
||||
from dcim.choices import (
|
||||
DeviceFaceChoices,
|
||||
DeviceStatusChoices,
|
||||
InterfaceModeChoices,
|
||||
InterfaceTypeChoices,
|
||||
PortTypeChoices,
|
||||
)
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
@@ -118,6 +124,51 @@ class DeviceTestCase(TestCase):
|
||||
self.assertIn('position', form.errors)
|
||||
|
||||
|
||||
class FrontPortTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.device = create_test_device('Panel Device 1')
|
||||
cls.rear_ports = (
|
||||
RearPort(name='RearPort1', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPort(name='RearPort2', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPort(name='RearPort3', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
|
||||
RearPort(name='RearPort4', device=cls.device, type=PortTypeChoices.TYPE_8P8C),
|
||||
)
|
||||
RearPort.objects.bulk_create(cls.rear_ports)
|
||||
|
||||
def test_front_port_label_count_valid(self):
|
||||
"""
|
||||
Test that generating an equal number of names and labels passes form validation.
|
||||
"""
|
||||
front_port_data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'FrontPort[1-4]',
|
||||
'label': 'Port[1-4]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
}
|
||||
form = FrontPortCreateForm(front_port_data)
|
||||
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_front_port_label_count_mismatch(self):
|
||||
"""
|
||||
Check that attempting to generate a differing number of names and labels results in a validation error.
|
||||
"""
|
||||
bad_front_port_data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'FrontPort[1-4]',
|
||||
'label': 'Port[1-2]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': [f'{rear_port.pk}:1' for rear_port in self.rear_ports],
|
||||
}
|
||||
form = FrontPortCreateForm(bad_front_port_data)
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('label', form.errors)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -9,6 +9,8 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Func, Value
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -281,12 +283,20 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
Populate initial custom field data upon either a) the creation of a new CustomField, or
|
||||
b) the assignment of an existing CustomField to new object types.
|
||||
"""
|
||||
if self.default is None:
|
||||
# We have to convert None to a JSON null for jsonb_set()
|
||||
value = RawSQL("'null'::jsonb", [])
|
||||
else:
|
||||
value = Value(self.default, models.JSONField())
|
||||
for ct in content_types:
|
||||
model = ct.model_class()
|
||||
instances = model.objects.exclude(**{'custom_field_data__contains': self.name})
|
||||
for instance in instances:
|
||||
instance.custom_field_data[self.name] = self.default
|
||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||
ct.model_class().objects.update(
|
||||
custom_field_data=Func(
|
||||
F('custom_field_data'),
|
||||
Value([self.name]),
|
||||
value,
|
||||
function='jsonb_set'
|
||||
)
|
||||
)
|
||||
|
||||
def remove_stale_data(self, content_types):
|
||||
"""
|
||||
@@ -295,22 +305,27 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
"""
|
||||
for ct in content_types:
|
||||
if model := ct.model_class():
|
||||
instances = model.objects.filter(custom_field_data__has_key=self.name)
|
||||
for instance in instances:
|
||||
del instance.custom_field_data[self.name]
|
||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||
model.objects.update(
|
||||
custom_field_data=F('custom_field_data') - self.name
|
||||
)
|
||||
|
||||
def rename_object_data(self, old_name, new_name):
|
||||
"""
|
||||
Called when a CustomField has been renamed. Updates all assigned object data.
|
||||
Called when a CustomField has been renamed. Removes the original key and inserts the new
|
||||
one, copying the value of the old key.
|
||||
"""
|
||||
for ct in self.object_types.all():
|
||||
model = ct.model_class()
|
||||
params = {f'custom_field_data__{old_name}__isnull': False}
|
||||
instances = model.objects.filter(**params)
|
||||
for instance in instances:
|
||||
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
|
||||
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
|
||||
ct.model_class().objects.update(
|
||||
custom_field_data=Func(
|
||||
F('custom_field_data') - old_name,
|
||||
Value([new_name]),
|
||||
Func(
|
||||
F('custom_field_data'),
|
||||
function='jsonb_extract_path_text',
|
||||
template=f"to_jsonb(%(expressions)s -> '{old_name}')"
|
||||
),
|
||||
function='jsonb_set')
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
@@ -532,6 +547,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
}
|
||||
if not for_csv_import:
|
||||
kwargs['query_params'] = self.related_object_filter
|
||||
kwargs['selector'] = True
|
||||
|
||||
field = field_class(**kwargs)
|
||||
|
||||
@@ -546,6 +562,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
}
|
||||
if not for_csv_import:
|
||||
kwargs['query_params'] = self.related_object_filter
|
||||
kwargs['selector'] = True
|
||||
|
||||
field = field_class(**kwargs)
|
||||
|
||||
|
||||
@@ -117,6 +117,15 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
def __str__(self):
|
||||
return self.python_name
|
||||
|
||||
@property
|
||||
def ordered_scripts(self):
|
||||
script_objects = {s.name: s for s in self.scripts.all()}
|
||||
ordered = [
|
||||
script_objects.pop(sc) for sc in self.module_scripts.keys() if sc in script_objects
|
||||
]
|
||||
ordered.extend(script_objects.items())
|
||||
return ordered
|
||||
|
||||
@property
|
||||
def module_scripts(self):
|
||||
|
||||
|
||||
@@ -498,13 +498,16 @@ class ConfigContextTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name=_('Synced')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='extras:configcontext_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'weight', 'is_active', 'is_synced', 'description', 'regions', 'sites', 'locations',
|
||||
'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants',
|
||||
'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
|
||||
'data_source', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'weight', 'is_active', 'is_synced', 'description')
|
||||
|
||||
|
||||
@@ -327,6 +327,13 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned interface')
|
||||
)
|
||||
fhrp_group = CSVModelChoiceField(
|
||||
label=_('FHRP Group'),
|
||||
queryset=FHRPGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned FHRP Group name')
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
label=_('Is primary'),
|
||||
help_text=_('Make this the primary IP for the assigned device'),
|
||||
@@ -341,8 +348,8 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
'is_oob', 'dns_name', 'description', 'comments', 'tags',
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
|
||||
'is_primary', 'is_oob', 'dns_name', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
@@ -398,6 +405,8 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
# Set interface assignment
|
||||
if self.cleaned_data.get('interface'):
|
||||
self.instance.assigned_object = self.cleaned_data['interface']
|
||||
if self.cleaned_data.get('fhrp_group'):
|
||||
self.instance.assigned_object = self.cleaned_data['fhrp_group']
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -666,6 +666,24 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
fhrp_groups = (
|
||||
FHRPGroup(
|
||||
name='FHRP Group 1',
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
|
||||
group_id=10
|
||||
),
|
||||
FHRPGroup(
|
||||
name='FHRP Group 2',
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
|
||||
group_id=20
|
||||
),
|
||||
FHRPGroup(
|
||||
name='FHRP Group 3',
|
||||
protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
|
||||
group_id=30
|
||||
),
|
||||
)
|
||||
FHRPGroup.objects.bulk_create(fhrp_groups)
|
||||
cls.form_data = {
|
||||
'vrf': vrfs[1].pk,
|
||||
'address': IPNetwork('192.0.2.99/24'),
|
||||
@@ -679,10 +697,10 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"vrf,address,status",
|
||||
"VRF 1,192.0.2.4/24,active",
|
||||
"VRF 1,192.0.2.5/24,active",
|
||||
"VRF 1,192.0.2.6/24,active",
|
||||
"vrf,address,status,fhrp_group",
|
||||
"VRF 1,192.0.2.4/24,active,FHRP Group 1",
|
||||
"VRF 1,192.0.2.5/24,active,FHRP Group 2",
|
||||
"VRF 1,192.0.2.6/24,active,FHRP Group 3",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
||||
@@ -666,7 +666,9 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
elif 'virtual_machine' in request.GET:
|
||||
initial_data['virtual_machine'] = request.GET.get('virtual_machine')
|
||||
|
||||
form = self.form(request.POST, initial=initial_data)
|
||||
post_data = request.POST.copy()
|
||||
post_data.setlist('pk', pk_list)
|
||||
form = self.form(post_data, initial=initial_data)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
if '_apply' in request.POST:
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@ span.color-label {
|
||||
.btn-float-group {
|
||||
position: sticky;
|
||||
bottom: 10px;
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.btn-float-group-left {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.2.6"
|
||||
version: "4.2.7"
|
||||
edition: "Community"
|
||||
published: "2025-03-21"
|
||||
published: "2025-04-10"
|
||||
|
||||
@@ -4,30 +4,30 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{% trans "Server Error" %}</title>
|
||||
<link rel="stylesheet" href="{% static 'netbox-light.css'%}" />
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans "Server Error" %}</title>
|
||||
<link rel="stylesheet" href="{% static 'netbox.css'%}" />
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col col-md-6 offset-md-3">
|
||||
<div class="card border-danger mt-5">
|
||||
<h2 class="card-header">
|
||||
<i class="mdi mdi-alert"></i> {% trans "Server Error" %}
|
||||
</h2>
|
||||
<div class="card-body">
|
||||
{% block message %}
|
||||
<p>
|
||||
{% trans "There was a problem with your request. Please contact an administrator" %}.
|
||||
</p>
|
||||
{% endblock %}
|
||||
<hr />
|
||||
<p>
|
||||
{% trans "The complete exception is provided below" %}:
|
||||
</p>
|
||||
<pre class="block"><strong>{{ exception }}</strong><br />
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col col-md-6 offset-md-3">
|
||||
<div class="card border-danger mt-5">
|
||||
<h2 class="card-header text-bg-danger">
|
||||
<i class="mdi mdi-alert"></i> {% trans "Server Error" %}
|
||||
</h2>
|
||||
<div class="card-body">
|
||||
{% block message %}
|
||||
<p>
|
||||
{% trans "There was a problem with your request. Please contact an administrator" %}.
|
||||
</p>
|
||||
{% endblock %}
|
||||
<hr />
|
||||
<p>
|
||||
{% trans "The complete exception is provided below" %}:
|
||||
</p>
|
||||
<pre class="block"><strong>{{ exception }}</strong><br />
|
||||
{{ error }}
|
||||
|
||||
{% trans "Python version" %}: {{ python_version }}
|
||||
@@ -35,17 +35,17 @@
|
||||
{% trans "Plugins" %}: {% for plugin, version in plugins.items %}
|
||||
{{ plugin }}: {{ version }}{% empty %}{% trans "None installed" %}{% endfor %}
|
||||
</pre>
|
||||
<p>
|
||||
{% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
|
||||
</p>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">{% trans "Home Page" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
{% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
|
||||
</p>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">{% trans "Home Page" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</h2>
|
||||
{% with scripts=module.scripts.all %}
|
||||
{% with scripts=module.ordered_scripts %}
|
||||
{% if scripts %}
|
||||
<table class="table table-hover scripts">
|
||||
<thead>
|
||||
@@ -63,7 +63,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
|
||||
<td>{{ script.python_class.description|markdown|placeholder }}</td>
|
||||
{% if last_job %}
|
||||
<td>
|
||||
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
|
||||
@@ -79,6 +79,9 @@
|
||||
{% if request.user|can_run:script and script.is_executable %}
|
||||
<div class="float-end d-print-none">
|
||||
<form action="{% url 'extras:script' script.pk %}" method="post">
|
||||
{% if script.python_class.commit_default %}
|
||||
<input type="checkbox" name="_commit" hidden checked>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="_run" class="btn btn-primary btn-sm">
|
||||
{% if last_job %}
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -198,9 +198,9 @@ class GenericArrayForeignKey(FieldCacheMixin, models.Field):
|
||||
Provide a generic many-to-many relation through an 2d array field
|
||||
"""
|
||||
|
||||
many_to_many = True
|
||||
many_to_many = False
|
||||
many_to_one = False
|
||||
one_to_many = False
|
||||
one_to_many = True
|
||||
one_to_one = False
|
||||
|
||||
def __init__(self, field, for_concrete_model=True):
|
||||
|
||||
@@ -97,10 +97,11 @@ class JSONField(_JSONField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.widget.attrs['placeholder'] = ''
|
||||
self.widget.attrs['class'] = 'font-monospace'
|
||||
if not self.help_text:
|
||||
self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.')
|
||||
self.widget.attrs['placeholder'] = ''
|
||||
self.widget.attrs['class'] = 'font-monospace'
|
||||
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, InvalidJSONInput):
|
||||
|
||||
@@ -136,9 +136,11 @@ def get_field_value(form, field_name):
|
||||
"""
|
||||
field = form.fields[field_name]
|
||||
|
||||
if form.is_bound and (data := form.data.get(field_name)):
|
||||
if hasattr(field, 'valid_value') and field.valid_value(data):
|
||||
return data
|
||||
if form.is_bound and field_name in form.data:
|
||||
if (value := form.data[field_name]) is None:
|
||||
return
|
||||
if hasattr(field, 'valid_value') and field.valid_value(value):
|
||||
return value
|
||||
|
||||
return form.get_initial_for_field(field, field_name)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Site
|
||||
from netbox.choices import ImportFormatChoices
|
||||
from utilities.forms.bulk_import import BulkImportForm
|
||||
from utilities.forms.forms import BulkRenameForm
|
||||
from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
|
||||
from utilities.forms.utils import get_field_value, expand_alphanumeric_pattern, expand_ipaddress_pattern
|
||||
|
||||
|
||||
class ExpandIPAddress(TestCase):
|
||||
@@ -387,3 +388,63 @@ class BulkRenameFormTest(TestCase):
|
||||
self.assertTrue(form.is_valid())
|
||||
self.assertEqual(form.cleaned_data["find"], " hello ")
|
||||
self.assertEqual(form.cleaned_data["replace"], " world ")
|
||||
|
||||
|
||||
class GetFieldValueTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
class TestForm(forms.Form):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
cls.form_class = TestForm
|
||||
|
||||
cls.sites = (
|
||||
Site(name='Test Site 1', slug='test-site-1'),
|
||||
Site(name='Test Site 2', slug='test-site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(cls.sites)
|
||||
|
||||
def test_unbound_without_initial(self):
|
||||
form = self.form_class()
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
None
|
||||
)
|
||||
|
||||
def test_unbound_with_initial(self):
|
||||
form = self.form_class(initial={'site': self.sites[0].pk})
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
self.sites[0].pk
|
||||
)
|
||||
|
||||
def test_bound_value_without_initial(self):
|
||||
form = self.form_class({'site': self.sites[0].pk})
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
self.sites[0].pk
|
||||
)
|
||||
|
||||
def test_bound_value_with_initial(self):
|
||||
form = self.form_class({'site': self.sites[0].pk}, initial={'site': self.sites[1].pk})
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
self.sites[0].pk
|
||||
)
|
||||
|
||||
def test_bound_null_without_initial(self):
|
||||
form = self.form_class({'site': None})
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
None
|
||||
)
|
||||
|
||||
def test_bound_null_with_initial(self):
|
||||
form = self.form_class({'site': None}, initial={'site': self.sites[1].pk})
|
||||
self.assertEqual(
|
||||
get_field_value(form, 'site'),
|
||||
None
|
||||
)
|
||||
|
||||
@@ -149,9 +149,8 @@ class GetReturnURLMixin:
|
||||
|
||||
# Attempt to dynamically resolve the list view for the object
|
||||
if hasattr(self, 'queryset'):
|
||||
model_opts = self.queryset.model._meta
|
||||
try:
|
||||
return reverse(f'{model_opts.app_label}:{model_opts.model_name}_list')
|
||||
return reverse(get_viewname(self.queryset.model, 'list'))
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from netbox import denormalized
|
||||
|
||||
|
||||
class VirtualizationConfig(AppConfig):
|
||||
name = 'virtualization'
|
||||
@@ -15,10 +13,5 @@ class VirtualizationConfig(AppConfig):
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register denormalized fields
|
||||
denormalized.register(VirtualMachine, 'cluster', {
|
||||
'site': '_site',
|
||||
})
|
||||
|
||||
# Register counters
|
||||
connect_counters(VirtualMachine)
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.db.models import Sum
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import VirtualDisk, VirtualMachine
|
||||
from .models import Cluster, VirtualDisk, VirtualMachine
|
||||
|
||||
|
||||
@receiver((post_delete, post_save), sender=VirtualDisk)
|
||||
@@ -14,3 +14,12 @@ def update_virtualmachine_disk(instance, **kwargs):
|
||||
VirtualMachine.objects.filter(pk=vm.pk).update(
|
||||
disk=vm.virtualdisks.aggregate(Sum('size'))['size__sum']
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Cluster)
|
||||
def update_virtualmachine_site(instance, **kwargs):
|
||||
"""
|
||||
Update the assigned site for all VMs to match that of the Cluster (if any).
|
||||
"""
|
||||
if instance._site:
|
||||
VirtualMachine.objects.filter(cluster=instance).update(site=instance._site)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Django==5.1.7
|
||||
Django==5.1.8
|
||||
django-cors-headers==4.7.0
|
||||
django-debug-toolbar==5.0.1
|
||||
django-filter==25.1
|
||||
django-htmx==1.23.0
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-mptt==0.16.0
|
||||
django-mptt==0.17.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.3.1
|
||||
django-redis==5.4.0
|
||||
@@ -13,15 +13,15 @@ django-rq==3.0
|
||||
django-taggit==6.1.0
|
||||
django-tables2==2.7.5
|
||||
django-timezone-field==7.1
|
||||
djangorestframework==3.15.2
|
||||
djangorestframework==3.16.0
|
||||
drf-spectacular==0.28.0
|
||||
drf-spectacular-sidecar==2025.3.1
|
||||
drf-spectacular-sidecar==2025.4.1
|
||||
feedparser==6.0.11
|
||||
gunicorn==23.0.0
|
||||
Jinja2==3.1.6
|
||||
Markdown==3.7
|
||||
mkdocs-material==9.6.9
|
||||
mkdocstrings[python]==0.29.0
|
||||
mkdocs-material==9.6.11
|
||||
mkdocstrings[python]==0.29.1
|
||||
netaddr==1.3.0
|
||||
nh3==0.2.21
|
||||
Pillow==11.1.0
|
||||
@@ -31,8 +31,8 @@ requests==2.32.3
|
||||
rq==2.1.0
|
||||
social-auth-app-django==5.4.3
|
||||
social-auth-core==4.5.6
|
||||
strawberry-graphql==0.262.5
|
||||
strawberry-graphql==0.263.2
|
||||
strawberry-graphql-django==0.52.0
|
||||
svgwrite==1.4.3
|
||||
tablib==3.8.0
|
||||
tzdata==2025.1
|
||||
tzdata==2025.2
|
||||
|
||||
Reference in New Issue
Block a user