diff --git a/docs/administration/error-reporting.md b/docs/administration/error-reporting.md index e04372338..162998774 100644 --- a/docs/administration/error-reporting.md +++ b/docs/administration/error-reporting.md @@ -4,7 +4,7 @@ ### Enabling Error Reporting -NetBox v3.2.3 and later support native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis. +NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis. ```python SENTRY_ENABLED = True diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index bcfbf0ba4..4b8556616 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -1,6 +1,6 @@ # Object-Based Permissions -NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range. +NetBox employs a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range. A permission in NetBox represents a relationship shared by several components: diff --git a/docs/customization/custom-links.md b/docs/customization/custom-links.md index 16ba9d2af..5d1cd4556 100644 --- a/docs/customization/custom-links.md +++ b/docs/customization/custom-links.md @@ -27,7 +27,6 @@ The following context data is available within the template when rendering a cus | Variable | Description | |-----------|-------------------------------------------------------------------------------------------------------------------| | `object` | The NetBox object being displayed | -| `obj` | Same as `object`; maintained for backward compatability until NetBox v3.5 | | `debug` | A boolean indicating whether debugging is enabled | | `request` | The current WSGI request | | `user` | The current user (if authenticated) | diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 3b56a8459..074da34cd 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -35,12 +35,9 @@ class MyScript(Script): The `run()` method should accept two arguments: -* `data` - A dictionary containing all of the variable data passed via the web form. +* `data` - A dictionary containing all the variable data passed via the web form. * `commit` - A boolean indicating whether database changes will be committed. -!!! note - The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.) - Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed. Any output generated by the script during its execution will be displayed under the "output" tab in the UI. diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md index 9d212a34e..caa441771 100644 --- a/docs/features/configuration-rendering.md +++ b/docs/features/configuration-rendering.md @@ -1,5 +1,7 @@ # Configuration Rendering +!!! info "This feature was introduced in NetBox v3.5." + One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network. ```mermaid diff --git a/docs/installation/2-redis.md b/docs/installation/2-redis.md index fcdfa9ceb..7c364947e 100644 --- a/docs/installation/2-redis.md +++ b/docs/installation/2-redis.md @@ -4,9 +4,6 @@ [Redis](https://redis.io/) is an in-memory key-value store which NetBox employs for caching and queuing. This section entails the installation and configuration of a local Redis instance. If you already have a Redis service in place, skip to [the next section](3-netbox.md). -!!! warning "Redis v4.0 or later required" - NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details. - === "Ubuntu" ```no-highlight diff --git a/docs/models/circuits/provideraccount.md b/docs/models/circuits/provideraccount.md index f906c657e..a1748a6d6 100644 --- a/docs/models/circuits/provideraccount.md +++ b/docs/models/circuits/provideraccount.md @@ -1,17 +1,19 @@ -# Provider Accounts - -This model can be used to represent individual accounts associated with a provider. - -## Fields - -### Provider - -The [provider](./provider.md) the account belongs to. - -### Name - -A human-friendly name, unique to the provider. - -### Account Number - -The administrative account identifier tied to this provider for your organization. \ No newline at end of file +# Provider Accounts + +!!! info "This model was introduced in NetBox v3.5." + +This model can be used to represent individual accounts associated with a provider. + +## Fields + +### Provider + +The [provider](./provider.md) the account belongs to. + +### Name + +A human-friendly name, unique to the provider. + +### Account Number + +The administrative account identifier tied to this provider for your organization. diff --git a/docs/models/ipam/asnrange.md b/docs/models/ipam/asnrange.md index 30d2f49c3..4dcb25624 100644 --- a/docs/models/ipam/asnrange.md +++ b/docs/models/ipam/asnrange.md @@ -1,5 +1,7 @@ # ASN Ranges +!!! info "This model was introduced in NetBox v3.5." + Ranges can be defined to group [AS numbers](./asn.md) numerically and to facilitate their automatic provisioning. Each range must be assigned to a [RIR](./rir.md). ## Fields diff --git a/docs/plugins/development/dashboard-widgets.md b/docs/plugins/development/dashboard-widgets.md index 85f2a57dd..1842fb61f 100644 --- a/docs/plugins/development/dashboard-widgets.md +++ b/docs/plugins/development/dashboard-widgets.md @@ -1,7 +1,6 @@ # Dashboard Widgets -!!! note "Introduced in v3.5" - Support for custom dashboard widgets was introduced in NetBox v3.5. +!!! info "This feature was introduced in NetBox v3.5." Each NetBox user can customize his or her personal dashboard by adding and removing widgets and by manipulating the size and position of each. Plugins can register their own dashboard widgets to complement those already available natively. diff --git a/docs/plugins/index.md b/docs/plugins/index.md index c2d62330f..0658ed402 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -2,8 +2,6 @@ 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. -Plugins are supported on NetBox v2.8 and later. - ## Capabilities The NetBox plugin architecture allows for the following: diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index ee9b04628..169946a4d 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -8,6 +8,7 @@ * The JobResult model has been moved from the `extras` app to `core` and renamed to Job. Accordingly, its REST API endpoint has been moved from `/api/extras/job-results/` to `/api/core/jobs/`. * The `obj_type` field on the Job model (previously JobResult) has been renamed to `object_type` for consistency with other models. * The `JOBRESULT_RETENTION` configuration parameter has been renamed to `JOB_RETENTION`. +* The `obj` context variable is no longer passed when rendering custom links: Use `object` instead. * The REST API schema is now generated using the OpenAPI 3.0 spec * The URLs for the REST API schema documentation have changed: * `/api/docs/` is now `/api/schema/swagger-ui/` diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py index ddb00c64b..518baea9f 100644 --- a/netbox/circuits/choices.py +++ b/netbox/circuits/choices.py @@ -25,6 +25,22 @@ class CircuitStatusChoices(ChoiceSet): ] +class CircuitCommitRateChoices(ChoiceSet): + key = 'Circuit.commit_rate' + + CHOICES = [ + (10000, '10 Mbps'), + (100000, '100 Mbps'), + (1000000, '1 Gbps'), + (10000000, '10 Gbps'), + (25000000, '25 Gbps'), + (40000000, '40 Gbps'), + (100000000, '100 Gbps'), + (1544, 'T1 (1.544 Mbps)'), + (2048, 'E1 (2.048 Mbps)'), + ] + + # # CircuitTerminations # @@ -38,3 +54,19 @@ class CircuitTerminationSideChoices(ChoiceSet): (SIDE_A, 'A'), (SIDE_Z, 'Z') ) + + +class CircuitTerminationPortSpeedChoices(ChoiceSet): + key = 'CircuitTermination.port_speed' + + CHOICES = [ + (10000, '10 Mbps'), + (100000, '100 Mbps'), + (1000000, '1 Gbps'), + (10000000, '10 Gbps'), + (25000000, '25 Gbps'), + (40000000, '40 Gbps'), + (100000000, '100 Gbps'), + (1544, 'T1 (1.544 Mbps)'), + (2048, 'E1 (2.048 Mbps)'), + ] diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index efc9b5f3a..9dba87e47 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -1,14 +1,14 @@ from django import forms from django.utils.translation import gettext as _ -from circuits.choices import CircuitStatusChoices +from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices from circuits.models import * from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField -from utilities.forms.widgets import DatePicker +from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( 'CircuitBulkEditForm', @@ -139,7 +139,10 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): ) commit_rate = forms.IntegerField( required=False, - label=_('Commit rate (Kbps)') + label=_('Commit rate (Kbps)'), + widget=NumberWithOptions( + options=CircuitCommitRateChoices + ) ) description = forms.CharField( max_length=100, diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 075855f3b..83da0d50a 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -1,14 +1,14 @@ from django import forms from django.utils.translation import gettext as _ -from circuits.choices import CircuitStatusChoices +from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices from circuits.models import * from dcim.models import Region, Site, SiteGroup from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField -from utilities.forms.widgets import DatePicker +from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( 'CircuitFilterForm', @@ -168,6 +168,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi commit_rate = forms.IntegerField( required=False, min_value=0, - label=_('Commit rate (Kbps)') + label=_('Commit rate (Kbps)'), + widget=NumberWithOptions( + options=CircuitCommitRateChoices + ) ) tag = TagFilterField(model) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 2925efec1..d3929c08a 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -1,12 +1,13 @@ from django.utils.translation import gettext as _ +from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices from circuits.models import * from dcim.models import Site from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.widgets import DatePicker, SelectSpeedWidget +from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( 'CircuitForm', @@ -116,7 +117,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm): widgets = { 'install_date': DatePicker(), 'termination_date': DatePicker(), - 'commit_rate': SelectSpeedWidget(), + 'commit_rate': NumberWithOptions( + options=CircuitCommitRateChoices + ), } @@ -143,6 +146,10 @@ class CircuitTerminationForm(NetBoxModelForm): 'xconnect_id', 'pp_info', 'description', 'tags', ] widgets = { - 'port_speed': SelectSpeedWidget(), - 'upstream_speed': SelectSpeedWidget(), + 'port_speed': NumberWithOptions( + options=CircuitTerminationPortSpeedChoices + ), + 'upstream_speed': NumberWithOptions( + options=CircuitTerminationPortSpeedChoices + ), } diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 0b44a3d52..1502592b0 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -10,6 +10,7 @@ from drf_spectacular.plumbing import ( ComponentRegistry, ResolvedComponent, build_basic_type, + build_choice_field, build_media_type_object, build_object_type, is_serializer, @@ -38,7 +39,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension): def map_serializer_field(self, auto_schema, direction): if direction == 'request': - return build_basic_type(OpenApiTypes.STR) + return build_choice_field(self.target) elif direction == "response": return build_object_type( @@ -150,8 +151,12 @@ class NetBoxAutoSchema(AutoSchema): def get_writable_class(self, serializer): properties = {} fields = {} if hasattr(serializer, 'child') else serializer.fields + remove_fields = [] for child_name, child in fields.items(): + # read_only fields don't need to be in writable (write only) serializers + if 'read_only' in dir(child) and child.read_only: + remove_fields.append(child_name) if isinstance(child, (ChoiceField, WritableNestedSerializer)): properties[child_name] = None elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField): @@ -165,7 +170,12 @@ class NetBoxAutoSchema(AutoSchema): meta_class = getattr(type(serializer), 'Meta', None) if meta_class: ref_name = 'Writable' + self.get_serializer_ref_name(serializer) - writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name}) + # remove read_only fields from write-only serializers + fields = list(meta_class.fields) + for field in remove_fields: + fields.remove(field) + writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name, 'fields': fields}) + properties['Meta'] = writable_meta self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties) diff --git a/netbox/core/forms/mixins.py b/netbox/core/forms/mixins.py new file mode 100644 index 000000000..3e76f67a2 --- /dev/null +++ b/netbox/core/forms/mixins.py @@ -0,0 +1,25 @@ +from django import forms +from django.utils.translation import gettext as _ + +from core.models import DataFile, DataSource +from utilities.forms.fields import DynamicModelChoiceField + +__all__ = ( + 'SyncedDataMixin', +) + + +class SyncedDataMixin(forms.Form): + data_source = DynamicModelChoiceField( + queryset=DataSource.objects.all(), + required=False, + label=_('Data source') + ) + data_file = DynamicModelChoiceField( + queryset=DataFile.objects.all(), + required=False, + label=_('File'), + query_params={ + 'source_id': '$data_source', + } + ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 405d70437..666a19e85 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -2,8 +2,8 @@ import copy from django import forms +from core.forms.mixins import SyncedDataMixin from core.models import * -from extras.forms.mixins import SyncedDataMixin from netbox.forms import NetBoxModelForm from netbox.registry import registry from utilities.forms import get_field_value diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index ad196a69e..8ad8a0eb7 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1096,6 +1096,20 @@ class InterfaceTypeChoices(ChoiceSet): ) +class InterfaceSpeedChoices(ChoiceSet): + key = 'Interface.speed' + + CHOICES = [ + (10000, '10 Mbps'), + (100000, '100 Mbps'), + (1000000, '1 Gbps'), + (10000000, '10 Gbps'), + (25000000, '25 Gbps'), + (40000000, '40 Gbps'), + (100000000, '100 Gbps'), + ] + + class InterfaceDuplexChoices(ChoiceSet): DUPLEX_HALF = 'half' diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 5966588fa..0762c0a32 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField -from utilities.forms.widgets import BulkEditNullBooleanSelect, SelectSpeedWidget +from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions __all__ = ( 'CableBulkEditForm', @@ -1169,8 +1169,9 @@ class InterfaceBulkEditForm( ) speed = forms.IntegerField( required=False, - widget=SelectSpeedWidget(), - label=_('Speed') + widget=NumberWithOptions( + options=InterfaceSpeedChoices + ) ) mgmt_only = forms.NullBooleanField( required=False, diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 727064e8f..a00c7fe26 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField -from utilities.forms.widgets import APISelectMultiple, SelectSpeedWidget +from utilities.forms.widgets import APISelectMultiple, NumberWithOptions from wireless.choices import * __all__ = ( @@ -1154,8 +1154,9 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): ) speed = forms.IntegerField( required=False, - label='Speed', - widget=SelectSpeedWidget() + widget=NumberWithOptions( + options=InterfaceSpeedChoices + ) ) duplex = forms.MultipleChoiceField( choices=InterfaceDuplexChoices, diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index f899c31e1..ee1d57781 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) -from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, SelectSpeedWidget, SelectWithPK +from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup from .common import InterfaceCommonForm, ModuleCommonForm @@ -1136,7 +1136,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] widgets = { - 'speed': SelectSpeedWidget(), + 'speed': NumberWithOptions( + options=InterfaceSpeedChoices + ), 'mode': HTMXSelect(), } labels = { diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 878d9df6a..e10516c4c 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -116,7 +116,7 @@ class JournalEntryKindChoices(ChoiceSet): # -# Log Levels for Reports and Scripts +# Reports and Scripts # class LogLevelChoices(ChoiceSet): @@ -136,6 +136,17 @@ class LogLevelChoices(ChoiceSet): ) +class DurationChoices(ChoiceSet): + + CHOICES = ( + (60, 'Hourly'), + (720, '12 hours'), + (1440, 'Daily'), + (10080, 'Weekly'), + (43200, '30 days'), + ) + + # # Job results # diff --git a/netbox/extras/forms/mixins.py b/netbox/extras/forms/mixins.py index 4e05e3a1e..be45f5211 100644 --- a/netbox/extras/forms/mixins.py +++ b/netbox/extras/forms/mixins.py @@ -1,16 +1,14 @@ -from django.contrib.contenttypes.models import ContentType from django import forms +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ -from core.models import DataFile, DataSource -from extras.models import * from extras.choices import CustomFieldVisibilityChoices -from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from extras.models import * +from utilities.forms.fields import DynamicModelMultipleChoiceField __all__ = ( 'CustomFieldsMixin', 'SavedFiltersMixin', - 'SyncedDataMixin', ) @@ -74,19 +72,3 @@ class SavedFiltersMixin(forms.Form): 'usable': True, } ) - - -class SyncedDataMixin(forms.Form): - data_source = DynamicModelChoiceField( - queryset=DataSource.objects.all(), - required=False, - label=_('Data source') - ) - data_file = DynamicModelChoiceField( - queryset=DataFile.objects.all(), - required=False, - label=_('File'), - query_params={ - 'source_id': '$data_source', - } - ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index bc79d8fe5..2f617b682 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -5,9 +5,9 @@ from django.db.models import Q from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from core.forms.mixins import SyncedDataMixin from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * -from extras.forms.mixins import SyncedDataMixin from extras.models import * from extras.utils import FeatureQuery from netbox.forms import NetBoxModelForm diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py index f3a9927f8..4000c01e6 100644 --- a/netbox/extras/forms/reports.py +++ b/netbox/extras/forms/reports.py @@ -1,8 +1,9 @@ from django import forms from django.utils.translation import gettext as _ +from extras.choices import DurationChoices from utilities.forms import BootstrapMixin -from utilities.forms.widgets import DateTimePicker, SelectDurationWidget +from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.utils import local_now __all__ = ( @@ -21,7 +22,9 @@ class ReportForm(BootstrapMixin, forms.Form): required=False, min_value=1, label=_("Recurs every"), - widget=SelectDurationWidget(), + widget=NumberWithOptions( + options=DurationChoices + ), help_text=_("Interval at which this report is re-run (in minutes)") ) diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index b19faba8f..05febaa6f 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -1,8 +1,9 @@ from django import forms from django.utils.translation import gettext as _ +from extras.choices import DurationChoices from utilities.forms import BootstrapMixin -from utilities.forms.widgets import DateTimePicker, SelectDurationWidget +from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.utils import local_now __all__ = ( @@ -27,7 +28,9 @@ class ScriptForm(BootstrapMixin, forms.Form): required=False, min_value=1, label=_("Recurs every"), - widget=SelectDurationWidget(), + widget=NumberWithOptions( + options=DurationChoices + ), help_text=_("Interval at which this script is re-run (in minutes)") ) diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index b7d8d1448..5de95b607 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -40,7 +40,6 @@ def custom_links(context, obj): # Pass select context data when rendering the CustomLink link_context = { 'object': obj, - 'obj': obj, # TODO: Remove in NetBox v3.5 'debug': context.get('debug', False), # django.template.context_processors.debug 'request': context['request'], # django.template.context_processors.request 'user': context['user'], # django.contrib.auth.context_processors.auth diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index e1cbacd97..ef8e87489 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -479,8 +479,8 @@ class CustomLinkTest(TestCase): def test_view_object_with_custom_link(self): customlink = CustomLink( name='Test', - link_text='FOO {{ obj.name }} BAR', - link_url='http://example.com/?site={{ obj.slug }}', + link_text='FOO {{ object.name }} BAR', + link_url='http://example.com/?site={{ object.slug }}', new_window=False ) customlink.save() diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index df9160621..2745a22b8 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -581,14 +581,16 @@ REST_FRAMEWORK = { # SPECTACULAR_SETTINGS = { - "TITLE": "NetBox API", - "DESCRIPTION": "API to access NetBox", - "LICENSE": {"name": "Apache v2 License"}, - "VERSION": VERSION, + 'TITLE': 'NetBox API', + 'DESCRIPTION': 'API to access NetBox', + 'LICENSE': {'name': 'Apache v2 License'}, + 'VERSION': VERSION, 'COMPONENT_SPLIT_REQUEST': True, + 'REDOC_DIST': 'SIDECAR', + 'SERVERS': [{'url': f'/{BASE_PATH}api'}], 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', - 'REDOC_DIST': 'SIDECAR', + 'POSTPROCESSING_HOOKS': [], } # diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8ca77ce55..e66e79a7a 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -20,7 +20,7 @@ from extras.signals import clear_webhooks from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields -from utilities.forms.bulk_import import ImportForm +from utilities.forms.bulk_import import BulkImportForm from utilities.htmx import is_embedded, is_htmx from utilities.permissions import get_permission_for_model from utilities.utils import get_viewname @@ -425,7 +425,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): # def get(self, request): - form = ImportForm() + form = BulkImportForm() return render(request, self.template_name, { 'model': self.model_form._meta.model, @@ -438,7 +438,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): def post(self, request): logger = logging.getLogger('netbox.views.BulkImportView') model = self.model_form._meta.model - form = ImportForm(request.POST, request.FILES) + form = BulkImportForm(request.POST, request.FILES) if form.is_valid(): logger.debug("Import form validation was successful") diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 7f20abbd6..e9509a52b 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 1cb775229..d71e22312 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/src/forms/speedSelector.ts b/netbox/project-static/src/forms/speedSelector.ts index 9195afce3..ac7f570de 100644 --- a/netbox/project-static/src/forms/speedSelector.ts +++ b/netbox/project-static/src/forms/speedSelector.ts @@ -4,7 +4,7 @@ import { getElements } from '../util'; * Set the value of the number input field based on the selection of the dropdown. */ export function initSpeedSelector(): void { - for (const element of getElements('a.set_speed')) { + for (const element of getElements('a.set_field_value')) { if (element !== null) { function handleClick(event: Event) { // Don't reload the page (due to href="#"). diff --git a/netbox/utilities/forms/bulk_import.py b/netbox/utilities/forms/bulk_import.py index 5538f4f0c..b7f432e63 100644 --- a/netbox/utilities/forms/bulk_import.py +++ b/netbox/utilities/forms/bulk_import.py @@ -6,14 +6,14 @@ import yaml from django import forms from django.utils.translation import gettext as _ -from extras.forms.mixins import SyncedDataMixin +from core.forms.mixins import SyncedDataMixin from utilities.choices import ImportFormatChoices from utilities.forms.utils import parse_csv +from .mixins import BootstrapMixin from ..choices import ImportMethodChoices -from .forms import BootstrapMixin -class ImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): +class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): import_method = forms.ChoiceField( choices=ImportMethodChoices, required=False diff --git a/netbox/utilities/forms/widgets/misc.py b/netbox/utilities/forms/widgets/misc.py index 751c3179f..ca2e64319 100644 --- a/netbox/utilities/forms/widgets/misc.py +++ b/netbox/utilities/forms/widgets/misc.py @@ -3,6 +3,7 @@ from django import forms __all__ = ( 'ClearableFileInput', 'MarkdownWidget', + 'NumberWithOptions', 'SlugWidget', ) @@ -21,6 +22,22 @@ class MarkdownWidget(forms.Textarea): template_name = 'widgets/markdown_input.html' +class NumberWithOptions(forms.NumberInput): + """ + Number field with a dropdown pre-populated with common values for convenience. + """ + template_name = 'widgets/number_with_options.html' + + def __init__(self, options, attrs=None): + self.options = options + super().__init__(attrs) + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context['widget']['options'] = self.options + return context + + class SlugWidget(forms.TextInput): """ Subclass TextInput and add a slug regeneration button next to the form field. diff --git a/netbox/utilities/forms/widgets/select.py b/netbox/utilities/forms/widgets/select.py index f2cd6cb1d..2e2d829cd 100644 --- a/netbox/utilities/forms/widgets/select.py +++ b/netbox/utilities/forms/widgets/select.py @@ -7,8 +7,6 @@ __all__ = ( 'BulkEditNullBooleanSelect', 'ColorSelect', 'HTMXSelect', - 'SelectDurationWidget', - 'SelectSpeedWidget', 'SelectWithPK', ) @@ -63,17 +61,3 @@ class SelectWithPK(forms.Select): Include the primary key of each option in the option label (e.g. "Router7 (4721)"). """ option_template_name = 'widgets/select_option_with_pk.html' - - -class SelectDurationWidget(forms.NumberInput): - """ - Dropdown to select one of several common options for a time duration (in minutes). - """ - template_name = 'widgets/select_duration.html' - - -class SelectSpeedWidget(forms.NumberInput): - """ - Speed field with dropdown selections for convenience. - """ - template_name = 'widgets/select_speed.html' diff --git a/netbox/utilities/templates/widgets/number_with_options.html b/netbox/utilities/templates/widgets/number_with_options.html new file mode 100644 index 000000000..ed518650d --- /dev/null +++ b/netbox/utilities/templates/widgets/number_with_options.html @@ -0,0 +1,11 @@ +
+ {% include 'django/forms/widgets/number.html' %} + + +
diff --git a/netbox/utilities/templates/widgets/select_duration.html b/netbox/utilities/templates/widgets/select_duration.html deleted file mode 100644 index 639075a8c..000000000 --- a/netbox/utilities/templates/widgets/select_duration.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {% include 'django/forms/widgets/number.html' %} - - -
diff --git a/netbox/utilities/templates/widgets/select_speed.html b/netbox/utilities/templates/widgets/select_speed.html deleted file mode 100644 index d9c63c44a..000000000 --- a/netbox/utilities/templates/widgets/select_speed.html +++ /dev/null @@ -1,16 +0,0 @@ -
- {% include 'django/forms/widgets/number.html' %} - - -
diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index d381ffc80..b8cff2996 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -2,7 +2,7 @@ from django import forms from django.test import TestCase from utilities.choices import ImportFormatChoices -from utilities.forms.bulk_import import ImportForm +from utilities.forms.bulk_import import BulkImportForm from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern @@ -288,7 +288,7 @@ class ExpandAlphanumeric(TestCase): class ImportFormTest(TestCase): def test_format_detection(self): - form = ImportForm() + form = BulkImportForm() data = ( "a,b,c\n"