diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml index f7cf89ee6..89f550298 100644 --- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml @@ -15,7 +15,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.2.5 + placeholder: v4.2.6 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml index 7a4910dcc..8b1f56914 100644 --- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml @@ -27,7 +27,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.2.5 + placeholder: v4.2.6 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index 75ee4bbfd..3490f3874 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -8,7 +8,10 @@ django-cors-headers # Runtime UI tool for debugging Django # https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst -django-debug-toolbar +# See: https://django-debug-toolbar.readthedocs.io/en/latest/changes.html#id1 +# "Wrap SHOW_TOOLBAR_CALLBACK function with sync_to_async or async_to_sync to allow sync/async +# compatibility." breaks stawberry-graphql-django at version 0.52.0 (current) +django-debug-toolbar==5.0.1 # Library for writing reusable URL query filters # https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst @@ -88,8 +91,7 @@ mkdocs-material # Introspection for embedded code # https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md -# See #18568 -mkdocstrings[python-legacy]==0.27.0 +mkdocstrings[python] # Library for manipulating IP prefixes and addresses # https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json index 639f0df8d..5a0616ac2 100644 --- a/contrib/generated_schema.json +++ b/contrib/generated_schema.json @@ -500,6 +500,9 @@ "n", "mrj21", "fc", + "fc-pc", + "fc-upc", + "fc-apc", "lc", "lc-pc", "lc-upc", @@ -565,6 +568,9 @@ "n", "mrj21", "fc", + "fc-pc", + "fc-upc", + "fc-apc", "lc", "lc-pc", "lc-upc", diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 5b31a6391..a58670e61 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -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 diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index e6d05738f..993d3987d 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -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 diff --git a/docs/plugins/development/filtersets.md b/docs/plugins/development/filtersets.md index d803ce2f4..b0059029a 100644 --- a/docs/plugins/development/filtersets.md +++ b/docs/plugins/development/filtersets.md @@ -61,6 +61,11 @@ class MyModelViewSet(...): The `TagFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class. +This class filters `tags` using the `slug` field. For example: + +`GET /api/dcim/sites/?tag=alpha&tag=bravo` + + ```python from django_filters import FilterSet from extras.filters import TagFilter @@ -68,3 +73,19 @@ from extras.filters import TagFilter class MyModelFilterSet(FilterSet): tag = TagFilter() ``` + +### TagIDFilter + +The `TagIDFilter` class is available for all models which support tag assignment (those which inherit from `NetBoxModel` or `TagsMixin`). This filter subclasses django-filter's `ModelMultipleChoiceFilter` to work with NetBox's `TaggedItem` class. + +This class filters `tags` using the `id` field. For example: + +`GET /api/dcim/sites/?tag_id=100&tag_id=200` + +```python +from django_filters import FilterSet +from extras.filters import TagIDFilter + +class MyModelFilterSet(FilterSet): + tag_id = TagIDFilter() +``` diff --git a/docs/release-notes/version-4.2.md b/docs/release-notes/version-4.2.md index 67493806b..7681a5218 100644 --- a/docs/release-notes/version-4.2.md +++ b/docs/release-notes/version-4.2.md @@ -1,5 +1,40 @@ # NetBox v4.2 +## v4.2.6 (2025-03-21) + +### Enhancements + +* [#17503](https://github.com/netbox-community/netbox/issues/17503) - Add rack title above rack on rack detail view +* [#17686](https://github.com/netbox-community/netbox/issues/17686) - Add config option for disk space divisor +* [#18579](https://github.com/netbox-community/netbox/issues/18579) - Update filtersets and filter forms to include contact filters where missing +* [#18744](https://github.com/netbox-community/netbox/issues/18744) - Ensure contact link in tables is hyperlinked +* [#18816](https://github.com/netbox-community/netbox/issues/18816) - Add FC/UPC, FC/APC and FC/PC port types +* [#18880](https://github.com/netbox-community/netbox/issues/18880) - Delay enqueuing background tasks until DB transaction is committed to avoid race condition +* [#18939](https://github.com/netbox-community/netbox/issues/18939) - Support site group search for ASNs + +### Bug Fixes + +* [#18409](https://github.com/netbox-community/netbox/issues/18409) - Eliminate N+1 issue by adding generic prefetch operation to Interface API endpoint +* [#18557](https://github.com/netbox-community/netbox/issues/18557) - Update JSONField to enclose bare string values in quotes +* [#18582](https://github.com/netbox-community/netbox/issues/18582) - Fix prefix bulk import with associated VLAN and conflicting VLAN IDs +* [#18742](https://github.com/netbox-community/netbox/issues/18742) - Ensure location list and detail views show related VLAN group information +* [#18782](https://github.com/netbox-community/netbox/issues/18782) - Ensure misconfigured object list widgets on the dashboard now degrade gracefully +* [#18833](https://github.com/netbox-community/netbox/issues/18833) - Fix inventory item bulk edit to ensure that component name and type are both validated Ensure +* [#18838](https://github.com/netbox-community/netbox/issues/18838) - Ensure that local context data correctly rejects falsy values +* [#18845](https://github.com/netbox-community/netbox/issues/18845) - Restore default sort behavior of name column on devices list view +* [#18863](https://github.com/netbox-community/netbox/issues/18863) - Exempt MPTT-based models from ordering fix introduced in #18279 +* [#18869](https://github.com/netbox-community/netbox/issues/18869) - Ensure numeric conversion helper always return a clean decimal value +* [#18872](https://github.com/netbox-community/netbox/issues/18872) - Ensure that `kind` is a required field when making journal entries +* [#18884](https://github.com/netbox-community/netbox/issues/18884) - Ensure tag deserialization is handled correctly +* [#18887](https://github.com/netbox-community/netbox/issues/18887) - Allow VM interface objects to be set on prefix object-type custom field +* [#18926](https://github.com/netbox-community/netbox/issues/18926) - Fix icon displayed for GitHub authentication on login page +* [#18928](https://github.com/netbox-community/netbox/issues/18928) - Support cascading deletions when cleaning up expired changelog records +* [#18933](https://github.com/netbox-community/netbox/issues/18933) - Allow filtering VLAN groups by associated site groups +* [#18944](https://github.com/netbox-community/netbox/issues/18944) - Ensure clearing "Widget type" field when adding widgets to dashboard does not cause a "ValueError: Unregistered widget class" error +* [#18949](https://github.com/netbox-community/netbox/issues/18949) - Add missing contacts property to GraphQL types where the associated model has a connection to a contact + +--- + ## v4.2.5 (2025-03-06) ### Enhancements diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 564b5ed6f..96fcaa144 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -43,7 +43,7 @@ class ProviderType(NetBoxObjectType, ContactsMixin): fields='__all__', filters=ProviderAccountFilter ) -class ProviderAccountType(NetBoxObjectType): +class ProviderAccountType(ContactsMixin, NetBoxObjectType): provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')] circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]] diff --git a/netbox/core/apps.py b/netbox/core/apps.py index b1337c7ed..283eed448 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -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: diff --git a/netbox/core/events.py b/netbox/core/events.py index 384b61fd4..06af75df7 100644 --- a/netbox/core/events.py +++ b/netbox/core/events.py @@ -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() diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 0f32c53a6..bf405707c 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -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'), ) ), ( diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 6f6cd8f7c..b279aec9b 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -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']) diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 2e5ab7ea7..de8691889 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -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 diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 8d992176a..b644cc50c 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -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')]] diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 08f93f6ea..b1ed4aca3 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -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 diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 89b7508f3..73d5dbd98 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -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 diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index de739aa59..d05800c22 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -4,6 +4,7 @@ from .models import Tag __all__ = ( 'TagFilter', + 'TagIDFilter', ) @@ -20,3 +21,18 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter): kwargs.setdefault('queryset', Tag.objects.all()) super().__init__(*args, **kwargs) + + +class TagIDFilter(django_filters.ModelMultipleChoiceFilter): + """ + Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=1&tag=2), the queryset is filtered + to objects matching all tags. + """ + def __init__(self, *args, **kwargs): + + kwargs.setdefault('field_name', 'tags__id') + kwargs.setdefault('to_field_name', 'id') + kwargs.setdefault('conjoined', True) + kwargs.setdefault('queryset', Tag.objects.all()) + + super().__init__(*args, **kwargs) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 4f40ce500..f4ef5b453 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -11,7 +11,7 @@ from users.models import Group, User from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter from virtualization.models import Cluster, ClusterGroup, ClusterType from .choices import * -from .filters import TagFilter +from .filters import TagFilter, TagIDFilter from .models import * __all__ = ( @@ -665,6 +665,7 @@ class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet): label=_('Data file (ID)'), ) tag = TagFilter() + tag_id = TagIDFilter() class Meta: model = ConfigTemplate diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 79b01b6ab..aeeb15728 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -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) diff --git a/netbox/extras/models/scripts.py b/netbox/extras/models/scripts.py index 98d79c53c..9cf2c2f9a 100644 --- a/netbox/extras/models/scripts.py +++ b/netbox/extras/models/scripts.py @@ -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): diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index e538c488e..cb9538cd2 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -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') diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index a672fd641..0fbcd414b 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -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) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index e6ecca984..54ce2fc74 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -5,6 +5,7 @@ import strawberry_django from circuits.graphql.types import ProviderType from dcim.graphql.types import SiteType +from extras.graphql.mixins import ContactsMixin from ipam import models from netbox.graphql.scalars import BigInt from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType @@ -83,7 +84,7 @@ class ASNRangeType(NetBoxObjectType): fields='__all__', filters=AggregateFilter ) -class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): +class AggregateType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): prefix: str rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -120,7 +121,7 @@ class FHRPGroupAssignmentType(BaseObjectType): exclude=('assigned_object_type', 'assigned_object_id', 'address'), filters=IPAddressFilter ) -class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): +class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -144,7 +145,7 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): fields='__all__', filters=IPRangeFilter ) -class IPRangeType(NetBoxObjectType): +class IPRangeType(NetBoxObjectType, ContactsMixin): start_address: str end_address: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None @@ -157,7 +158,7 @@ class IPRangeType(NetBoxObjectType): exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), filters=PrefixFilter ) -class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): +class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType): prefix: str vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None @@ -217,7 +218,7 @@ class RouteTargetType(NetBoxObjectType): fields='__all__', filters=ServiceFilter ) -class ServiceType(NetBoxObjectType): +class ServiceType(NetBoxObjectType, ContactsMixin): ports: List[int] device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')] | None virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')] | None diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index d7d367bb7..345f39a51 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -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 = ( diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index b8fbe7ad5..eaead5e74 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext as _ from core.choices import ObjectChangeActionChoices from core.models import ObjectChange from extras.choices import CustomFieldFilterLogicChoices -from extras.filters import TagFilter +from extras.filters import TagFilter, TagIDFilter from extras.models import CustomField, SavedFilter from utilities.constants import ( FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, @@ -286,6 +286,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): label=_('Search'), ) tag = TagFilter() + tag_id = TagIDFilter() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 48371ebee..002acf8b0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -700,6 +700,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView): else: logger.debug("Form validation failed") + else: + form = self.form(initial=initial_data) + restrict_form_fields(form, request.user) + # Retrieve objects being edited table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) if not table.rows: diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 2cb549a0d..da802c1b6 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 01c14cf5c..068df3056 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 0eda793ea..c7a7c7e54 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index bc8f3cee4..6ca59318d 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -27,10 +27,10 @@ "bootstrap": "5.3.3", "clipboard": "2.0.11", "flatpickr": "4.6.13", - "gridstack": "11.3.0", + "gridstack": "11.5.0", "htmx.org": "1.9.12", "query-string": "9.1.1", - "sass": "1.85.0", + "sass": "1.86.0", "tom-select": "2.4.3", "typeface-inter": "3.18.1", "typeface-roboto-mono": "1.1.13" diff --git a/netbox/project-static/styles/custom/_misc.scss b/netbox/project-static/styles/custom/_misc.scss index 4ef2f1337..f0c229e52 100644 --- a/netbox/project-static/styles/custom/_misc.scss +++ b/netbox/project-static/styles/custom/_misc.scss @@ -38,7 +38,7 @@ span.color-label { .btn-float-group { position: sticky; bottom: 10px; - z-index: 2; + z-index: 4; } .btn-float-group-left { diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 816779bce..8cc19d255 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -769,9 +769,9 @@ bootstrap "5.3.3" "@tabler/icons@^3.14.0": - version "3.16.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.16.0.tgz#d618670b80163925a31a6c2290e8775f6058d81a" - integrity sha512-GU7MSx4uQEr55BmyON6hD/QYTl6k1v0YlRhM91gBWDoKAbyCt6QIYw7rpJ/ecdh5zrHaTOJKPenZ4+luoutwFA== + version "3.31.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.31.0.tgz#92d39dc336f2e3e312170420b00ffe9ca474925e" + integrity sha512-dblAdeKY3+GA1U+Q9eziZ0ooVlZMHsE8dqP0RkwvRtEsAULoKOYaCUOcJ4oW1DjWegdxk++UAt2SlQVnmeHv+g== "@tanstack/react-virtual@^3.0.0-beta.60": version "3.5.0" @@ -1911,10 +1911,10 @@ graphql@16.10.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== -gridstack@11.3.0: - version "11.3.0" - resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.3.0.tgz#b110c66bafc64c920fc54933e2c9df4f7b2cfffe" - integrity sha512-Z0eRovKcZTRTs3zetJwjO6CNwrgIy845WfOeZGk8ybpeMCE8fMA8tScyKU72Y2M6uGHkjgwnjflglvPiv+RcBQ== +gridstack@11.5.0: + version "11.5.0" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.5.0.tgz#ecd507776db857f3308d37a8fd67d6a24c7fdd74" + integrity sha512-SE1a/aC2K8VKQr5cqV7gSJ+r/xIYghijIjHzkZ3Xo3aS1/4dvwIgPYT7QqgV1z+d7XjKYUPEizcgVQ5HhdFTng== has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" @@ -2673,10 +2673,10 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -sass@1.85.0: - version "1.85.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.85.0.tgz#0127ef697d83144496401553f0a0e87be83df45d" - integrity sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww== +sass@1.86.0: + version "1.86.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.86.0.tgz#f49464fb6237a903a93f4e8760ef6e37a5030114" + integrity sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA== dependencies: chokidar "^4.0.0" immutable "^5.0.2" diff --git a/netbox/release.yaml b/netbox/release.yaml index 4653aeec2..058304a27 100644 --- a/netbox/release.yaml +++ b/netbox/release.yaml @@ -1,3 +1,3 @@ -version: "4.2.5" +version: "4.2.6" edition: "Community" -published: "2025-03-06" +published: "2025-03-21" diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index cbffbf8de..57269aa53 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -37,7 +37,7 @@ {% endif %} - {% with scripts=module.scripts.all %} + {% with scripts=module.ordered_scripts %} {% if scripts %}
{{ script.python_class.Meta.description|markdown|placeholder }} | +{{ script.python_class.description|markdown|placeholder }} | {% if last_job %}
{{ last_job.created|isodatetime }}
@@ -79,6 +79,9 @@
{% if request.user|can_run:script and script.is_executable %}
|