diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d4a9bab72ef..8fc9bc205b9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,7 +1,7 @@ --- name: 🐛 Bug Report description: Report a reproducible bug in the current release of NetBox -labels: ["type: bug", "needs triage"] +labels: ["type: bug", "status: needs triage"] body: - type: markdown attributes: @@ -26,7 +26,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v3.7.5 + placeholder: v3.7.6 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 0f80f1716d3..b5a97078219 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -1,7 +1,7 @@ --- name: 📖 Documentation Change description: Suggest an addition or modification to the NetBox documentation -labels: ["type: documentation", "needs triage"] +labels: ["type: documentation", "status: needs triage"] body: - type: dropdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 2cee040f8b5..3e7372484d1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,7 +1,7 @@ --- name: ✨ Feature Request description: Propose a new NetBox feature or enhancement -labels: ["type: feature", "needs triage"] +labels: ["type: feature", "status: needs triage"] body: - type: markdown attributes: @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.7.5 + placeholder: v3.7.6 validations: required: true - type: dropdown diff --git a/.github/workflows/auto-assign-issue.yml b/.github/workflows/auto-assign-issue.yml index 9abbc0cce3d..e32e23c84e6 100644 --- a/.github/workflows/auto-assign-issue.yml +++ b/.github/workflows/auto-assign-issue.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: pozil/auto-assign-issue@v1 - if: "contains(github.event.issue.labels.*.name, 'needs triage')" + if: "contains(github.event.issue.labels.*.name, 'status: needs triage')" with: # Weighted assignments assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, abhi1693, DanSheps diff --git a/base_requirements.txt b/base_requirements.txt index 642450cf85c..8971ebe1e1b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -61,7 +61,8 @@ django-timezone-field # A REST API framework for Django projects # https://www.django-rest-framework.org/community/release-notes/ -djangorestframework +# Pinned to 3.14 for NetBox v3.7 +djangorestframework<3.15 # Sane and flexible OpenAPI 3 schema generation for Django REST framework. # https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst diff --git a/docs/_theme/main.html b/docs/_theme/main.html index 3ff44b9cbf9..4dfc4e14e8a 100644 --- a/docs/_theme/main.html +++ b/docs/_theme/main.html @@ -2,8 +2,8 @@ {% block site_meta %} {{ super() }} - {# Disable search indexing unless we're building for ReadTheDocs (see #10496) #} - {% if page.canonical_url != 'https://docs.netbox.dev/' %} + {# Disable search indexing unless we're building for ReadTheDocs #} + {% if not config.extra.readthedocs %} {% endif %} {% endblock %} diff --git a/docs/configuration/security.md b/docs/configuration/security.md index 2ae92285f1d..9de09cedad0 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -183,6 +183,30 @@ The view name or URL to which a user is redirected after logging out. --- +## SECURE_HSTS_INCLUDE_SUBDOMAINS + +Default: False + +If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain. + +--- + +## SECURE_HSTS_PRELOAD + +Default: False + +If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar. + +--- + +## SECURE_HSTS_SECONDS + +Default: 0 + +If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security (HSTS) header on all responses that do not already have it. This will instruct the browser that the website must be accessed via HTTPS, blocking any HTTP request. + +--- + ## SECURE_SSL_REDIRECT Default: False diff --git a/docs/configuration/system.md b/docs/configuration/system.md index 806839778f9..28c09444bd6 100644 --- a/docs/configuration/system.md +++ b/docs/configuration/system.md @@ -16,10 +16,7 @@ BASE_PATH = 'netbox/' Default: `en-us` (US English) -Defines the default preferred language/locale for requests that do not specify one. This is used to alter e.g. the display of dates and numbers to fit the user's locale. See [this list](http://www.i18nguy.com/unicode/language-identifiers.html) of standard language codes. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.) - -!!! note - Altering this parameter will *not* change the language used in NetBox. We hope to provide translation support in a future NetBox release. +Defines the default preferred language/locale for requests that do not specify one. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.) --- diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index 31751855e50..1f844dc1b12 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -62,7 +62,7 @@ class MyModelImportForm(NetBoxModelImportForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', - help_text='Assigned site' + help_text=_('Assigned site') ) class Meta: diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md index 4db1d5ef6ad..4d026cacd4f 100644 --- a/docs/plugins/development/index.md +++ b/docs/plugins/development/index.md @@ -3,6 +3,9 @@ !!! tip "Plugins Development Tutorial" Just getting started with plugins? Check out our [**NetBox Plugin Tutorial**](https://github.com/netbox-community/netbox-plugin-tutorial) on GitHub! This in-depth guide will walk you through the process of creating an entire plugin from scratch. It even includes a companion [demo plugin repo](https://github.com/netbox-community/netbox-plugin-demo) to ensure you can jump in at any step along the way. This will get you up and running with plugins in no time! +!!! tip "Plugin Certification Program" + NetBox Labs offers a [**Plugin Certification Program**](https://github.com/netbox-community/netbox/wiki/Plugin-Certification-Program) for plugin developers interested in establishing a co-maintainer relationship. The program aims to assure ongoing compatibility, maintainability, and commercial supportability of key plugins. + NetBox can be extended to support additional data models and functionality through the use of plugins. A plugin is essentially a self-contained [Django app](https://docs.djangoproject.com/en/stable/) which gets installed alongside NetBox to provide custom functionality. Multiple plugins can be installed in a single NetBox instance, and each plugin can be enabled and configured independently. !!! info "Django Development" diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 1730b0ebde3..3c13a6fcbf8 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -157,7 +157,7 @@ These views are provided to enable or enhance certain NetBox model features, suc ### Additional Tabs -Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`: +Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict: ```python from dcim.models import Site @@ -173,6 +173,16 @@ class MyView(generic.ObjectView): badge=lambda obj: Stuff.objects.filter(site=obj).count(), permission='myplugin.view_stuff' ) + + def get(self, request, pk): + ... + return render( + request, + "myplugin/mytabview.html", + context={ + "tab": self.tab, + }, + ) ``` ::: utilities.views.register_model_view diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 39314187b48..0f502c5d82f 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -2,6 +2,8 @@ Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own. +Please see the documented instructions for [installing a plugin](./installation.md) to get started. + ## Capabilities The NetBox plugin architecture allows for the following: @@ -23,122 +25,3 @@ Either by policy or by technical limitation, the interaction of plugins with Net * **Override core templates.** Plugins can inject additional content where supported, but may not manipulate or remove core content. * **Modify core settings.** A configuration registry is provided for plugins, however they cannot alter or delete the core configuration. * **Disable core components.** Plugins are not permitted to disable or hide core NetBox components. - -## Installing Plugins - -The instructions below detail the process for installing and enabling a NetBox plugin. - -### Install Package - -Download and install the plugin package per its installation instructions. Plugins published via PyPI are typically installed using pip. Be sure to install the plugin within NetBox's virtual environment. - -```no-highlight -$ source /opt/netbox/venv/bin/activate -(venv) $ pip install -``` - -Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead. - -### Enable the Plugin - -In `configuration.py`, add the plugin's name to the `PLUGINS` list: - -```python -PLUGINS = [ - 'plugin_name', -] -``` - -### Configure Plugin - -If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's README file. - -```no-highlight -PLUGINS_CONFIG = { - 'plugin_name': { - 'foo': 'bar', - 'buzz': 'bazz' - } -} -``` - -### Run Database Migrations - -If the plugin introduces new database models, run the provided schema migrations: - -```no-highlight -(venv) $ cd /opt/netbox/netbox/ -(venv) $ python3 manage.py migrate -``` - -### Collect Static Files - -Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command: - -```no-highlight -(venv) $ cd /opt/netbox/netbox/ -(venv) $ python3 manage.py collectstatic -``` - -### Restart WSGI Service - -Restart the WSGI service and RQ workers to load the new plugin: - -```no-highlight -# sudo systemctl restart netbox netbox-rq -``` - -## Removing Plugins - -Follow these steps to completely remove a plugin. - -### Update Configuration - -Remove the plugin from the `PLUGINS` list in `configuration.py`. Also remove any relevant configuration parameters from `PLUGINS_CONFIG`. - -### Remove the Python Package - -Use `pip` to remove the installed plugin: - -```no-highlight -$ source /opt/netbox/venv/bin/activate -(venv) $ pip uninstall -``` - -### Restart WSGI Service - -Restart the WSGI service: - -```no-highlight -# sudo systemctl restart netbox -``` - -### Drop Database Tables - -!!! note - This step is necessary only for plugin which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure. - -Enter the PostgreSQL database shell to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.) - -```no-highlight -netbox=> \dt pluginname_* - List of relations - List of relations - Schema | Name | Type | Owner ---------+----------------+-------+-------- - public | pluginname_foo | table | netbox - public | pluginname_bar | table | netbox -(2 rows) -``` - -!!! warning - Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions. - -Drop each of the listed tables to remove it from the database: - -```no-highlight -netbox=> DROP TABLE pluginname_foo; -DROP TABLE -netbox=> DROP TABLE pluginname_bar; -DROP TABLE -``` diff --git a/docs/plugins/installation.md b/docs/plugins/installation.md new file mode 100644 index 00000000000..ffea5d42deb --- /dev/null +++ b/docs/plugins/installation.md @@ -0,0 +1,68 @@ +# Installing a Plugin + +!!! warning + The instructions below detail the general process for installing and configuring a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to install it. + +## Install the Python Package + +Download and install the plugin's Python package per its installation instructions. Plugins published via PyPI are typically installed using the [`pip`](https://packaging.python.org/en/latest/tutorials/installing-packages/) command line utility. Be sure to install the plugin within NetBox's virtual environment. + +```no-highlight +$ source /opt/netbox/venv/bin/activate +(venv) $ pip install +``` + +Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead. + +## Enable the Plugin + +In `configuration.py`, add the plugin's name to the `PLUGINS` list: + +```python +PLUGINS = [ + # ... + 'plugin_name', +] +``` + +## Configure the Plugin + +If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's `README` file or other documentation. + +```no-highlight +PLUGINS_CONFIG = { + 'plugin_name': { + 'foo': 'bar', + 'buzz': 'bazz' + } +} +``` + +## Run Database Migrations + +If the plugin introduces new database models, run the provided schema migrations: + +```no-highlight +(venv) $ cd /opt/netbox/netbox/ +(venv) $ python3 manage.py migrate +``` + +!!! tip + It's okay to run the `migrate` management command even if the plugin does not include any migration files. + +## Collect Static Files + +Plugins may package static resources like images or scripts to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command: + +```no-highlight +(venv) $ cd /opt/netbox/netbox/ +(venv) $ python3 manage.py collectstatic +``` + +### Restart WSGI Service + +Finally, restart the WSGI service and RQ workers to load the new plugin: + +```no-highlight +# sudo systemctl restart netbox netbox-rq +``` diff --git a/docs/plugins/removal.md b/docs/plugins/removal.md new file mode 100644 index 00000000000..f5e81bdc083 --- /dev/null +++ b/docs/plugins/removal.md @@ -0,0 +1,72 @@ +# Removing a Plugin + +!!! warning + The instructions below detail the general process for removing a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to remove it. + +## Disable the Plugin + +Disable the plugin by removing it from the `PLUGINS` list in `configuration.py`. + +## Remove its Configuration + +Delete the plugin's entry (if any) in the `PLUGINS_CONFIG` dictionary in `configuration.py`. + +!!! tip + If there's a chance you may reinstall the plugin, consider commenting out any configuration parameters instead of deleting them. + +## Re-index Search Entries + +Run the `reindex` management command to reindex the global search engine. This will remove any stale entries pertaining to objects provided by the plugin. + +```no-highlight +$ cd /opt/netbox/netbox/ +$ source /opt/netbox/venv/bin/activate +(venv) $ python3 manage.py reindex +``` + +## Uninstall its Python Package + +Use `pip` to remove the installed plugin: + +```no-highlight +$ source /opt/netbox/venv/bin/activate +(venv) $ pip uninstall +``` + +## Restart WSGI Service + +Restart the WSGI service: + +```no-highlight +# sudo systemctl restart netbox +``` + +## Drop Database Tables + +!!! note + This step is necessary only for plugins which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure. + +Enter the PostgreSQL database shell (`manage.py dbshell`) to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.) + +```no-highlight +netbox=> \dt pluginname_* + List of relations + List of relations + Schema | Name | Type | Owner +--------+----------------+-------+-------- + public | pluginname_foo | table | netbox + public | pluginname_bar | table | netbox +(2 rows) +``` + +!!! warning + Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions. + +Drop each of the listed tables to remove it from the database: + +```no-highlight +netbox=> DROP TABLE pluginname_foo; +DROP TABLE +netbox=> DROP TABLE pluginname_bar; +DROP TABLE +``` diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md index ddbeb4bca38..062dc3fe734 100644 --- a/docs/release-notes/version-3.7.md +++ b/docs/release-notes/version-3.7.md @@ -1,5 +1,29 @@ # NetBox v3.7 +## v3.7.6 (2024-04-22) + +### Enhancements + +* [#14690](https://github.com/netbox-community/netbox/issues/14690) - Improve rendering of JSON data in configuration form +* [#15427](https://github.com/netbox-community/netbox/issues/15427) - Enable compatibility with non-Amazon S3 providers for remote data sources +* [#15640](https://github.com/netbox-community/netbox/issues/15640) - Add global search support for L2VPN identifiers +* [#15644](https://github.com/netbox-community/netbox/issues/15644) - Introduce new configuration parameters for enabling HTTP Strict Transport Security (HSTS) + +### Bug Fixes + +* [#15541](https://github.com/netbox-community/netbox/issues/15541) - Restore ability to modify assigned component template when adding/modifying an inventory item template +* [#15582](https://github.com/netbox-community/netbox/issues/15582) - Fix permission constraints for synchronization of remote data sources +* [#15588](https://github.com/netbox-community/netbox/issues/15588) - Correct OpenAPI schema definitions for read-only fields which may return null values +* [#15635](https://github.com/netbox-community/netbox/issues/15635) - Extend plugin removal instruction to include reindexing the global search cache +* [#15654](https://github.com/netbox-community/netbox/issues/15654) - Fix `AttributeError` exception when attempting to save an incomplete tunnel termination +* [#15668](https://github.com/netbox-community/netbox/issues/15668) - Fix permission required to display virtual disks tab on virtual machine UI view +* [#15685](https://github.com/netbox-community/netbox/issues/15685) - Allow filtering cables by decimal values using UI filter form +* [#15761](https://github.com/netbox-community/netbox/issues/15761) - Add missing `ike_policy` & `ike_policy_id` filters for IKE proposals +* [#15771](https://github.com/netbox-community/netbox/issues/15771) - Include `id` in list of supported fields for all bulk import forms +* [#15790](https://github.com/netbox-community/netbox/issues/15790) - Fix live preview support for EventRule comments + +--- + ## v3.7.5 (2024-04-04) ### Enhancements diff --git a/mkdocs.yml b/mkdocs.yml index e1128578acd..5aa6572302b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ plugins: show_root_toc_entry: false show_source: false extra: + readthedocs: !ENV READTHEDOCS social: - icon: fontawesome/brands/github link: https://github.com/netbox-community/netbox @@ -127,7 +128,9 @@ nav: - Synchronized Data: 'integrations/synchronized-data.md' - Prometheus Metrics: 'integrations/prometheus-metrics.md' - Plugins: - - Using Plugins: 'plugins/index.md' + - About Plugins: 'plugins/index.md' + - Installing a Plugin: 'plugins/installation.md' + - Removing a Plugin: 'plugins/removal.md' - Developing Plugins: - Getting Started: 'plugins/development/index.md' - Models: 'plugins/development/models.md' diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py index 7bf2f87a666..39c922eb641 100644 --- a/netbox/core/api/views.py +++ b/netbox/core/api/views.py @@ -1,5 +1,5 @@ from django.shortcuts import get_object_or_404 - +from django.utils.translation import gettext_lazy as _ from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response @@ -33,10 +33,11 @@ def sync(self, request, pk): """ Enqueue a job to synchronize the DataSource. """ - if not request.user.has_perm('core.sync_datasource'): - raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.") - datasource = get_object_or_404(DataSource, pk=pk) + + if not request.user.has_perm('core.sync_datasource', obj=datasource): + raise PermissionDenied(_("This user does not have permission to synchronize this data source.")) + datasource.enqueue_sync_job(request) serializer = serializers.DataSourceSerializer(datasource, context={'request': request}) diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 15891a6f54e..2d3a7d8c8f2 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -149,7 +149,8 @@ def fetch(self): region_name=self._region_name, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, - config=self.config + config=self.config, + endpoint_url=self._endpoint_url ) bucket = s3.Bucket(self._bucket_name) @@ -176,6 +177,11 @@ def _bucket_name(self): url_path = urlparse(self.url).path.lstrip('/') return url_path.split('/')[0] + @property + def _endpoint_url(self): + url_path = urlparse(self.url) + return url_path._replace(params="", fragment="", query="", path="").geturl() + @property def _remote_path(self): url_path = urlparse(self.url).path.lstrip('/') diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index ae891dd59e1..0f4f971dca2 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -3,6 +3,7 @@ from django import forms from django.conf import settings +from django.forms.fields import JSONField as _JSONField from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin @@ -12,7 +13,7 @@ from netbox.registry import registry from netbox.utils import get_data_backend_choices from utilities.forms import BootstrapMixin, get_field_value -from utilities.forms.fields import CommentField +from utilities.forms.fields import CommentField, JSONField from utilities.forms.widgets import HTMXSelect __all__ = ( @@ -132,6 +133,9 @@ def __new__(mcs, name, bases, attrs): 'help_text': param.description, } field_kwargs.update(**param.field_kwargs) + if param.field is _JSONField: + # Replace with our own JSONField to get pretty JSON in config editor + param.field = JSONField param_fields[param.name] = param.field(**field_kwargs) attrs.update(param_fields) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 053b3e9eacf..e07d17acaf9 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -612,7 +612,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True) + component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: @@ -668,7 +668,7 @@ class DeviceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = NestedDeviceTypeSerializer() role = NestedDeviceRoleSerializer() - device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.') + device_role = NestedDeviceRoleSerializer(read_only=True, help_text=_('Deprecated in v3.6 in favor of `role`.')) tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() @@ -685,7 +685,7 @@ class DeviceSerializer(NetBoxModelSerializer): ) status = ChoiceField(choices=DeviceStatusChoices, required=False) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) - primary_ip = NestedIPAddressSerializer(read_only=True) + primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) @@ -735,7 +735,7 @@ def get_device_role(self, obj): class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) + config_context = serializers.SerializerMethodField(read_only=True, allow_null=True) class Meta(DeviceSerializer.Meta): fields = [ @@ -1067,7 +1067,7 @@ class InventoryItemSerializer(NetBoxModelSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True) + component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 47974096fad..95e6c409849 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -1373,14 +1373,14 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm): label=_('Device'), queryset=Device.objects.all(), to_field_name='name', - help_text='Assigned role' + help_text=_('Assigned role') ) tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), required=False, to_field_name='name', - help_text='Assigned tenant' + help_text=_('Assigned tenant') ) status = CSVChoiceField( label=_('Status'), diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 95c44138135..d8d32627170 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -977,9 +977,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): label=_('Color'), required=False ) - length = forms.IntegerField( + length = forms.DecimalField( label=_('Length'), - required=False + required=False, ) length_unit = forms.ChoiceField( label=_('Length unit'), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 6773bc55f81..cee8fcfba87 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -976,21 +976,67 @@ class InventoryItemTemplateForm(ComponentTemplateForm): queryset=Manufacturer.objects.all(), required=False ) - component_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS, + # Assigned component selectors + consoleporttemplate = DynamicModelChoiceField( + queryset=ConsolePortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Console port template') + ) + consoleserverporttemplate = DynamicModelChoiceField( + queryset=ConsoleServerPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Console server port template') + ) + frontporttemplate = DynamicModelChoiceField( + queryset=FrontPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Front port template') + ) + interfacetemplate = DynamicModelChoiceField( + queryset=InterfaceTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Interface template') + ) + poweroutlettemplate = DynamicModelChoiceField( + queryset=PowerOutletTemplate.objects.all(), required=False, - widget=forms.HiddenInput + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Power outlet template') ) - component_id = forms.IntegerField( + powerporttemplate = DynamicModelChoiceField( + queryset=PowerPortTemplate.objects.all(), required=False, - widget=forms.HiddenInput + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Power port template') + ) + rearporttemplate = DynamicModelChoiceField( + queryset=RearPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Rear port template') ) fieldsets = ( (None, ( 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', - 'component_type', 'component_id', )), ) @@ -998,9 +1044,52 @@ class Meta: model = InventoryItemTemplate fields = [ 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', - 'component_type', 'component_id', ] + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}).copy() + component_type = initial.get('component_type') + component_id = initial.get('component_id') + + # Used for picking the default active tab for component selection + self.no_component = True + + if instance: + # When editing set the initial value for component selection + for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS): + if type(instance.component) is component_model.model_class(): + initial[component_model.model] = instance.component + self.no_component = False + break + elif component_type and component_id: + # When adding the InventoryItem from a component page + if content_type := ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS).filter(pk=component_type).first(): + if component := content_type.model_class().objects.filter(pk=component_id).first(): + initial[content_type.model] = component + self.no_component = False + + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + def clean(self): + super().clean() + + # Handle object assignment + selected_objects = [ + field for field in ( + 'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', + 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate' + ) if self.cleaned_data[field] + ] + if len(selected_objects) > 1: + raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component.")) + elif selected_objects: + self.instance.component = self.cleaned_data[selected_objects[0]] + else: + self.instance.component = None + # # Device components diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d0e92ff56c4..ce4bb5750d8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1656,6 +1656,7 @@ class InventoryItemTemplateCreateView(generic.ComponentCreateView): queryset = InventoryItemTemplate.objects.all() form = forms.InventoryItemTemplateCreateForm model_form = forms.InventoryItemTemplateForm + template_name = 'dcim/inventoryitemtemplate_edit.html' def alter_object(self, instance, request): # Set component (if any) @@ -1673,6 +1674,7 @@ def alter_object(self, instance, request): class InventoryItemTemplateEditView(generic.ObjectEditView): queryset = InventoryItemTemplate.objects.all() form = forms.InventoryItemTemplateForm + template_name = 'dcim/inventoryitemtemplate_edit.html' @register_model_view(InventoryItemTemplate, 'delete') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 8f9face4128..70b7a78a4c6 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -265,6 +265,7 @@ class EventRuleForm(NetBoxModelForm): required=False, help_text=_('Enter parameters to pass to the action in JSON format.') ) + comments = CommentField() fieldsets = ( (_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')), diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 33aa55a93ed..8dca73d9420 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -262,7 +262,7 @@ class AvailableVLANSerializer(serializers.Serializer): Representation of a VLAN which does not exist in the database. """ vid = serializers.IntegerField(read_only=True) - group = NestedVLANGroupSerializer(read_only=True) + group = NestedVLANGroupSerializer(read_only=True, allow_null=True) def to_representation(self, instance): return { @@ -348,9 +348,9 @@ class AvailablePrefixSerializer(serializers.Serializer): """ Representation of a prefix which does not exist in the database. """ - family = serializers.IntegerField(read_only=True) + family = serializers.IntegerField(read_only=True, allow_null=True) prefix = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = NestedVRFSerializer(read_only=True, allow_null=True) def to_representation(self, instance): if self.context.get('vrf'): @@ -429,9 +429,9 @@ class AvailableIPSerializer(serializers.Serializer): """ Representation of an IP address which does not exist in the database. """ - family = serializers.IntegerField(read_only=True) + family = serializers.IntegerField(read_only=True, allow_null=True) address = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = NestedVRFSerializer(read_only=True, allow_null=True) description = serializers.CharField(required=False) def to_representation(self, instance): diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 0b0e2036e1f..1a4155aaba8 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -73,17 +73,12 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): """ Base form for creating a NetBox objects from CSV data. Used for bulk importing. """ - id = forms.IntegerField( - label=_('Id'), - required=False, - help_text='Numeric ID of an existing object to update (if not creating a new object)' - ) tags = CSVModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), required=False, to_field_name='slug', - help_text='Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")' + help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")') ) def _get_custom_fields(self, content_type): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0ae43da668f..764aa049a67 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -28,7 +28,7 @@ # Environment setup # -VERSION = '3.7.5' +VERSION = '3.7.6' # Hostname HOSTNAME = platform.node() @@ -160,6 +160,9 @@ RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0) SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend') +SECURE_HSTS_INCLUDE_SUBDOMAINS = getattr(configuration, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', False) +SECURE_HSTS_PRELOAD = getattr(configuration, 'SECURE_HSTS_PRELOAD', False) +SECURE_HSTS_SECONDS = getattr(configuration, 'SECURE_HSTS_SECONDS', 0) SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False) SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None) SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False) diff --git a/netbox/templates/dcim/inventoryitemtemplate_edit.html b/netbox/templates/dcim/inventoryitemtemplate_edit.html new file mode 100644 index 00000000000..d3ac58e255f --- /dev/null +++ b/netbox/templates/dcim/inventoryitemtemplate_edit.html @@ -0,0 +1,104 @@ +{% extends 'generic/object_edit.html' %} +{% load static %} +{% load form_helpers %} +{% load helpers %} +{% load i18n %} + +{% block form %} +
+
+
{% trans "Inventory Item" %}
+
+ {% render_field form.device_type %} + {% render_field form.parent %} + {% render_field form.name %} + {% render_field form.label %} + {% render_field form.role %} + {% render_field form.description %} +
+ +
+
+
{% trans "Hardware" %}
+
+ {% render_field form.manufacturer %} + {% render_field form.part_id %} +
+ +
+
+
{% trans "Component Assignment" %}
+
+
+ +
+
+
+ {% render_field form.consoleporttemplate %} +
+
+ {% render_field form.consoleserverporttemplate %} +
+
+ {% render_field form.frontporttemplate %} +
+
+ {% render_field form.interfacetemplate %} +
+
+ {% render_field form.poweroutlettemplate %} +
+
+ {% render_field form.powerporttemplate %} +
+
+ {% render_field form.rearporttemplate %} +
+
+
+ + {% if form.custom_fields %} +
+
+
{% trans "Custom Fields" %}
+
+ {% render_custom_fields form %} +
+ {% endif %} +{% endblock %} diff --git a/netbox/translations/fr/LC_MESSAGES/django.mo b/netbox/translations/fr/LC_MESSAGES/django.mo index b5dac2ef5cd..4bb44f73d38 100644 Binary files a/netbox/translations/fr/LC_MESSAGES/django.mo and b/netbox/translations/fr/LC_MESSAGES/django.mo differ diff --git a/netbox/translations/fr/LC_MESSAGES/django.po b/netbox/translations/fr/LC_MESSAGES/django.po index 8c8a362ff3a..cc489f360ff 100644 --- a/netbox/translations/fr/LC_MESSAGES/django.po +++ b/netbox/translations/fr/LC_MESSAGES/django.po @@ -6,6 +6,7 @@ # Translators: # Jonathan Senecal, 2024 # Jeremy Stretch, 2024 +# Quentin Laurent, 2024 # #, fuzzy msgid "" @@ -14,7 +15,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-04-04 19:11+0000\n" "PO-Revision-Date: 2023-10-30 17:48+0000\n" -"Last-Translator: Jeremy Stretch, 2024\n" +"Last-Translator: Quentin Laurent, 2024\n" "Language-Team: French (https://app.transifex.com/netbox-community/teams/178115/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -3716,7 +3717,7 @@ msgstr "Réservation" #: dcim/forms/model_forms.py:301 dcim/forms/model_forms.py:384 #: utilities/forms/fields/fields.py:47 msgid "Slug" -msgstr "limace" +msgstr "Identifiant" #: dcim/forms/model_forms.py:308 templates/dcim/devicetype.html:12 msgid "Chassis" @@ -5813,7 +5814,7 @@ msgstr "Poids maximum" #: ipam/tables/asn.py:66 netbox/navigation/menu.py:16 #: netbox/navigation/menu.py:18 msgid "Sites" -msgstr "Des sites" +msgstr "Sites" #: dcim/tests/test_api.py:49 msgid "Test case must set peer_termination_type" @@ -13355,7 +13356,7 @@ msgstr "" #: utilities/forms/fields/fields.py:48 msgid "URL-friendly unique shorthand" -msgstr "Raccourci unique et convivial pour les URL" +msgstr "Identifiant unique utilisable dans les URL" #: utilities/forms/fields/fields.py:101 msgid "Enter context data in JSON format." diff --git a/netbox/translations/ja/LC_MESSAGES/django.mo b/netbox/translations/ja/LC_MESSAGES/django.mo index 2962616319d..da77c423a93 100644 Binary files a/netbox/translations/ja/LC_MESSAGES/django.mo and b/netbox/translations/ja/LC_MESSAGES/django.mo differ diff --git a/netbox/translations/ja/LC_MESSAGES/django.po b/netbox/translations/ja/LC_MESSAGES/django.po index 5b79cf0dd6e..34193822c20 100644 --- a/netbox/translations/ja/LC_MESSAGES/django.po +++ b/netbox/translations/ja/LC_MESSAGES/django.po @@ -5,8 +5,8 @@ # # Translators: # Tatsuya Ueda , 2024 -# teapot, 2024 # Jeremy Stretch, 2024 +# teapot, 2024 # #, fuzzy msgid "" @@ -15,7 +15,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-04-04 19:11+0000\n" "PO-Revision-Date: 2023-10-30 17:48+0000\n" -"Last-Translator: Jeremy Stretch, 2024\n" +"Last-Translator: teapot, 2024\n" "Language-Team: Japanese (https://app.transifex.com/netbox-community/teams/178115/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -7681,7 +7681,7 @@ msgstr "プレフィックス内およびプレフィックスを含む" #: ipam/filtersets.py:259 msgid "Prefixes which contain this prefix or IP" -msgstr "このプレフィックスまたは IP を含むプレフィックス" +msgstr "このプレフィックス / IP を含むプレフィックス" #: ipam/filtersets.py:270 ipam/filtersets.py:538 ipam/forms/bulk_edit.py:326 #: ipam/forms/filtersets.py:191 ipam/forms/filtersets.py:317 @@ -7700,11 +7700,11 @@ msgstr "VLAN 番号 (1-4094)" #: ipam/forms/model_forms.py:430 templates/tenancy/contact.html:54 #: tenancy/forms/bulk_edit.py:112 msgid "Address" -msgstr "住所" +msgstr "アドレス" #: ipam/filtersets.py:445 msgid "Ranges which contain this prefix or IP" -msgstr "このプレフィックスまたは IP を含む範囲" +msgstr "このプレフィックス / IP を含む範囲" #: ipam/filtersets.py:473 ipam/filtersets.py:529 msgid "Parent prefix" @@ -7743,11 +7743,11 @@ msgstr "FHRP グループ (ID)" #: ipam/filtersets.py:618 msgid "Is assigned to an interface" -msgstr "インタフェースに割り当てられている" +msgstr "インタフェースに割り当てられているか" #: ipam/filtersets.py:622 msgid "Is assigned" -msgstr "割り当てられている" +msgstr "割当済みか" #: ipam/filtersets.py:1047 msgid "IP address (ID)" @@ -7881,7 +7881,7 @@ msgstr "子 VLAN VID の最小値" #: ipam/forms/bulk_edit.py:420 msgid "Maximum child VLAN VID" -msgstr "子 VLAN VID の最大数" +msgstr "子 VLAN VID の最大値" #: ipam/forms/bulk_edit.py:428 ipam/forms/model_forms.py:531 msgid "Scope type" @@ -7905,11 +7905,11 @@ msgstr "ポート" #: ipam/forms/bulk_import.py:47 msgid "Import route targets" -msgstr "ルートターゲットをインポート" +msgstr "インポートルートターゲット" #: ipam/forms/bulk_import.py:53 msgid "Export route targets" -msgstr "ルートターゲットをエクスポートする" +msgstr "エクスポートルートターゲット" #: ipam/forms/bulk_import.py:91 ipam/forms/bulk_import.py:111 #: ipam/forms/bulk_import.py:131 diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 54c9e41cbaa..93227b1d057 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -70,6 +70,12 @@ class CSVModelForm(forms.ModelForm): """ ModelForm used for the import of objects in CSV format. """ + id = forms.IntegerField( + label=_('ID'), + required=False, + help_text=_('Numeric ID of an existing object to update (if not creating a new object)') + ) + def __init__(self, *args, headers=None, **kwargs): self.headers = headers or {} super().__init__(*args, **kwargs) diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index aa2093a9a54..52e5d66ca8b 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -10,10 +10,11 @@ from netaddr import IPNetwork from taggit.managers import TaggableManager +from netbox.models.features import CustomFieldsMixin from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct from utilities.utils import content_type_identifier -from .utils import extract_form_failures +from .utils import DUMMY_CF_DATA, extract_form_failures __all__ = ( 'ModelTestCase', @@ -166,8 +167,12 @@ def assertInstanceEqual(self, instance, data, exclude=None, api=False): model_dict = self.model_to_dict(instance, fields=fields, api=api) # Omit any dictionary keys which are not instance attributes or have been excluded - relevant_data = { + model_data = { k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude } - self.assertDictEqual(model_dict, relevant_data) + self.assertDictEqual(model_dict, model_data) + + # Validate any custom field data, if present + if getattr(instance, 'custom_field_data', None): + self.assertDictEqual(instance.custom_field_data, DUMMY_CF_DATA) diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py index fd6d72e2718..238f8170e21 100644 --- a/netbox/utilities/testing/utils.py +++ b/netbox/utilities/testing/utils.py @@ -1,13 +1,16 @@ +import json import logging import re from contextlib import contextmanager from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType from django.utils.text import slugify from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site -from extras.models import Tag +from extras.choices import CustomFieldTypeChoices +from extras.models import CustomField, Tag from virtualization.models import Cluster, ClusterType, VirtualMachine @@ -102,3 +105,42 @@ def disable_warnings(logger_name): logger.setLevel(logging.ERROR) yield logger.setLevel(current_level) + + +# +# Custom field testing +# + +DUMMY_CF_DATA = { + 'text_field': 'foo123', + 'integer_field': 456, + 'decimal_field': 456.12, + 'boolean_field': True, + 'json_field': {'abc': 123}, +} + + +def add_custom_field_data(form_data, model): + """ + Create some custom fields for the model and add a value for each to the form data. + + Args: + form_data: The dictionary of form data to be updated + model: The model of the object the form seeks to create or modify + """ + content_type = ContentType.objects.get_for_model(model) + custom_fields = ( + CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo'), + CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='integer_field', default=123), + CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45), + CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False), + CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}'), + ) + CustomField.objects.bulk_create(custom_fields) + for cf in custom_fields: + cf.content_types.set([content_type]) + + form_data.update({ + f'cf_{k}': v if type(v) is str else json.dumps(v) + for k, v in DUMMY_CF_DATA.items() + }) diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index daa44b905af..22371db3cf6 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -10,11 +10,11 @@ from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange -from netbox.models.features import ChangeLoggingMixin +from netbox.models.features import ChangeLoggingMixin, CustomFieldsMixin from users.models import ObjectPermission from utilities.choices import CSVDelimiterChoices, ImportFormatChoices from .base import ModelTestCase -from .utils import disable_warnings, post_data +from .utils import add_custom_field_data, disable_warnings, post_data __all__ = ( 'ModelViewTestCase', @@ -26,7 +26,6 @@ # UI Tests # - class ModelViewTestCase(ModelTestCase): """ Base TestCase for model views. Subclass to test individual views. @@ -166,6 +165,10 @@ def test_create_object_with_permission(self): # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('add')), 200) + # Add custom field data if the model supports it + if issubclass(self.model, CustomFieldsMixin): + add_custom_field_data(self.form_data, self.model) + # Try POST with model-level permission initial_count = self._get_queryset().count() request = { @@ -265,6 +268,10 @@ def test_edit_object_with_permission(self): # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 200) + # Add custom field data if the model supports it + if issubclass(self.model, CustomFieldsMixin): + add_custom_field_data(self.form_data, self.model) + # Try POST with model-level permission request = { 'path': self._get_url('edit', instance), diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 34e4037e9fe..a54643e6215 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -76,7 +76,7 @@ class VirtualMachineSerializer(NetBoxModelSerializer): role = NestedDeviceRoleSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True) - primary_ip = NestedIPAddressSerializer(read_only=True) + primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6019fc22705..ec19a1d22f9 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -388,7 +388,7 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView): tab = ViewTab( label=_('Virtual Disks'), badge=lambda obj: obj.virtual_disk_count, - permission='virtualization.view_virtual_disk', + permission='virtualization.view_virtualdisk', weight=500 ) actions = { diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index 5f6fcd5f771..36ccf28de8b 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -98,6 +98,9 @@ class Meta: @extend_schema_field(serializers.JSONField(allow_null=True)) def get_termination(self, obj): + if not obj.termination: + return None + serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(obj.termination, context=context).data diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py index 0647838a8ec..10f0834fb78 100644 --- a/netbox/vpn/filtersets.py +++ b/netbox/vpn/filtersets.py @@ -136,6 +136,17 @@ class IKEProposalFilterSet(NetBoxModelFilterSet): group = django_filters.MultipleChoiceFilter( choices=DHGroupChoices ) + ike_policy_id = django_filters.ModelMultipleChoiceFilter( + field_name='ike_policies', + queryset=IKEPolicy.objects.all(), + label=_('IKE policy (ID)'), + ) + ike_policy = django_filters.ModelMultipleChoiceFilter( + field_name='ike_policies__name', + queryset=IKEPolicy.objects.all(), + to_field_name='name', + label=_('IKE policy (name)'), + ) class Meta: model = IKEProposal diff --git a/netbox/vpn/search.py b/netbox/vpn/search.py index 066bc68bb12..c1914dc222d 100644 --- a/netbox/vpn/search.py +++ b/netbox/vpn/search.py @@ -75,6 +75,7 @@ class L2VPNIndex(SearchIndex): fields = ( ('name', 100), ('slug', 110), + ('identifier', 200), ('description', 500), ('comments', 5000), ) diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py index d4e80750d02..f11e63f1045 100644 --- a/netbox/vpn/tests/test_filtersets.py +++ b/netbox/vpn/tests/test_filtersets.py @@ -331,6 +331,16 @@ def setUpTestData(cls): ) IKEProposal.objects.bulk_create(ike_proposals) + ike_policies = ( + IKEPolicy(name='IKE Policy 1'), + IKEPolicy(name='IKE Policy 2'), + IKEPolicy(name='IKE Policy 3'), + ) + IKEPolicy.objects.bulk_create(ike_policies) + ike_policies[0].proposals.add(ike_proposals[0]) + ike_policies[1].proposals.add(ike_proposals[1]) + ike_policies[2].proposals.add(ike_proposals[2]) + def test_q(self): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -369,6 +379,13 @@ def test_sa_lifetime(self): params = {'sa_lifetime': [1000, 2000]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_ike_policy(self): + ike_policies = IKEPolicy.objects.all()[:2] + params = {'ike_policy_id': [ike_policies[0].pk, ike_policies[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'ike_policy': [ike_policies[0].name, ike_policies[1].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class IKEPolicyTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IKEPolicy.objects.all() diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index c0e2dfb54cb..38bc37360af 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -42,7 +42,7 @@ class WirelessLANImportForm(NetBoxModelImportForm): status = CSVChoiceField( label=_('Status'), choices=WirelessLANStatusChoices, - help_text='Operational status' + help_text=_('Operational status') ) vlan = CSVModelChoiceField( label=_('VLAN'), diff --git a/requirements.txt b/requirements.txt index 2fbc25b219c..78b423692ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,11 +18,11 @@ drf-spectacular==0.27.2 drf-spectacular-sidecar==2024.4.1 feedparser==6.0.11 graphene-django==3.0.0 -gunicorn==21.2.0 +gunicorn==22.0.0 Jinja2==3.1.3 Markdown==3.6 -mkdocs-material==9.5.17 -mkdocstrings[python-legacy]==0.24.2 +mkdocs-material==9.5.18 +mkdocstrings[python-legacy]==0.24.3 netaddr==1.2.1 Pillow==10.3.0 psycopg[binary,pool]==3.1.18