Merge branch 'feature' into 12278-ipaddressfield-serialize

This commit is contained in:
Arthur 2023-04-20 14:37:55 -07:00
commit 7923d180ec
41 changed files with 219 additions and 139 deletions

View File

@ -4,7 +4,7 @@
### Enabling Error Reporting ### 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 ```python
SENTRY_ENABLED = True SENTRY_ENABLED = True

View File

@ -1,6 +1,6 @@
# Object-Based Permissions # 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: A permission in NetBox represents a relationship shared by several components:

View File

@ -27,7 +27,6 @@ The following context data is available within the template when rendering a cus
| Variable | Description | | Variable | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------| |-----------|-------------------------------------------------------------------------------------------------------------------|
| `object` | The NetBox object being displayed | | `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 | | `debug` | A boolean indicating whether debugging is enabled |
| `request` | The current WSGI request | | `request` | The current WSGI request |
| `user` | The current user (if authenticated) | | `user` | The current user (if authenticated) |

View File

@ -35,12 +35,9 @@ class MyScript(Script):
The `run()` method should accept two arguments: 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. * `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. 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. Any output generated by the script during its execution will be displayed under the "output" tab in the UI.

View File

@ -1,5 +1,7 @@
# Configuration Rendering # 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. 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 ```mermaid

View File

@ -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). [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" === "Ubuntu"
```no-highlight ```no-highlight

View File

@ -1,5 +1,7 @@
# Provider Accounts # Provider Accounts
!!! info "This model was introduced in NetBox v3.5."
This model can be used to represent individual accounts associated with a provider. This model can be used to represent individual accounts associated with a provider.
## Fields ## Fields

View File

@ -1,5 +1,7 @@
# ASN Ranges # 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). 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 ## Fields

View File

@ -1,7 +1,6 @@
# Dashboard Widgets # Dashboard Widgets
!!! note "Introduced in v3.5" !!! info "This feature was introduced in NetBox v3.5."
Support for custom dashboard widgets 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. 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.

View File

@ -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 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 ## Capabilities
The NetBox plugin architecture allows for the following: The NetBox plugin architecture allows for the following:

View File

@ -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 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 `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 `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 REST API schema is now generated using the OpenAPI 3.0 spec
* The URLs for the REST API schema documentation have changed: * The URLs for the REST API schema documentation have changed:
* `/api/docs/` is now `/api/schema/swagger-ui/` * `/api/docs/` is now `/api/schema/swagger-ui/`

View File

@ -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 # CircuitTerminations
# #
@ -38,3 +54,19 @@ class CircuitTerminationSideChoices(ChoiceSet):
(SIDE_A, 'A'), (SIDE_A, 'A'),
(SIDE_Z, 'Z') (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)'),
]

View File

@ -1,14 +1,14 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import * from circuits.models import *
from ipam.models import ASN from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import add_blank_choice from utilities.forms import add_blank_choice
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DatePicker from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
'CircuitBulkEditForm', 'CircuitBulkEditForm',
@ -139,7 +139,10 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
) )
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,
label=_('Commit rate (Kbps)') label=_('Commit rate (Kbps)'),
widget=NumberWithOptions(
options=CircuitCommitRateChoices
)
) )
description = forms.CharField( description = forms.CharField(
max_length=100, max_length=100,

View File

@ -1,14 +1,14 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import DatePicker from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
'CircuitFilterForm', 'CircuitFilterForm',
@ -168,6 +168,9 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,
min_value=0, min_value=0,
label=_('Commit rate (Kbps)') label=_('Commit rate (Kbps)'),
widget=NumberWithOptions(
options=CircuitCommitRateChoices
)
) )
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -1,12 +1,13 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.models import * from circuits.models import *
from dcim.models import Site from dcim.models import Site
from ipam.models import ASN from ipam.models import ASN
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.widgets import DatePicker, SelectSpeedWidget from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = ( __all__ = (
'CircuitForm', 'CircuitForm',
@ -116,7 +117,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
widgets = { widgets = {
'install_date': DatePicker(), 'install_date': DatePicker(),
'termination_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', 'xconnect_id', 'pp_info', 'description', 'tags',
] ]
widgets = { widgets = {
'port_speed': SelectSpeedWidget(), 'port_speed': NumberWithOptions(
'upstream_speed': SelectSpeedWidget(), options=CircuitTerminationPortSpeedChoices
),
'upstream_speed': NumberWithOptions(
options=CircuitTerminationPortSpeedChoices
),
} }

View File

@ -10,6 +10,7 @@ from drf_spectacular.plumbing import (
ComponentRegistry, ComponentRegistry,
ResolvedComponent, ResolvedComponent,
build_basic_type, build_basic_type,
build_choice_field,
build_media_type_object, build_media_type_object,
build_object_type, build_object_type,
is_serializer, is_serializer,
@ -38,7 +39,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
def map_serializer_field(self, auto_schema, direction): def map_serializer_field(self, auto_schema, direction):
if direction == 'request': if direction == 'request':
return build_basic_type(OpenApiTypes.STR) return build_choice_field(self.target)
elif direction == "response": elif direction == "response":
return build_object_type( return build_object_type(
@ -150,8 +151,12 @@ class NetBoxAutoSchema(AutoSchema):
def get_writable_class(self, serializer): def get_writable_class(self, serializer):
properties = {} properties = {}
fields = {} if hasattr(serializer, 'child') else serializer.fields fields = {} if hasattr(serializer, 'child') else serializer.fields
remove_fields = []
for child_name, child in fields.items(): 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)): if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField): elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
@ -165,7 +170,12 @@ class NetBoxAutoSchema(AutoSchema):
meta_class = getattr(type(serializer), 'Meta', None) meta_class = getattr(type(serializer), 'Meta', None)
if meta_class: if meta_class:
ref_name = 'Writable' + self.get_serializer_ref_name(serializer) 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 properties['Meta'] = writable_meta
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties) self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)

View File

@ -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',
}
)

View File

@ -2,8 +2,8 @@ import copy
from django import forms from django import forms
from core.forms.mixins import SyncedDataMixin
from core.models import * from core.models import *
from extras.forms.mixins import SyncedDataMixin
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from netbox.registry import registry from netbox.registry import registry
from utilities.forms import get_field_value from utilities.forms import get_field_value

View File

@ -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): class InterfaceDuplexChoices(ChoiceSet):
DUPLEX_HALF = 'half' DUPLEX_HALF = 'half'

View File

@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import BulkEditNullBooleanSelect, SelectSpeedWidget from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
__all__ = ( __all__ = (
'CableBulkEditForm', 'CableBulkEditForm',
@ -1169,8 +1169,9 @@ class InterfaceBulkEditForm(
) )
speed = forms.IntegerField( speed = forms.IntegerField(
required=False, required=False,
widget=SelectSpeedWidget(), widget=NumberWithOptions(
label=_('Speed') options=InterfaceSpeedChoices
)
) )
mgmt_only = forms.NullBooleanField( mgmt_only = forms.NullBooleanField(
required=False, required=False,

View File

@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField 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 * from wireless.choices import *
__all__ = ( __all__ = (
@ -1154,8 +1154,9 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
) )
speed = forms.IntegerField( speed = forms.IntegerField(
required=False, required=False,
label='Speed', widget=NumberWithOptions(
widget=SelectSpeedWidget() options=InterfaceSpeedChoices
)
) )
duplex = forms.MultipleChoiceField( duplex = forms.MultipleChoiceField(
choices=InterfaceDuplexChoices, choices=InterfaceDuplexChoices,

View File

@ -16,7 +16,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
NumericArrayField, SlugField, 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 virtualization.models import Cluster
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm from .common import InterfaceCommonForm, ModuleCommonForm
@ -1136,7 +1136,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
] ]
widgets = { widgets = {
'speed': SelectSpeedWidget(), 'speed': NumberWithOptions(
options=InterfaceSpeedChoices
),
'mode': HTMXSelect(), 'mode': HTMXSelect(),
} }
labels = { labels = {

View File

@ -116,7 +116,7 @@ class JournalEntryKindChoices(ChoiceSet):
# #
# Log Levels for Reports and Scripts # Reports and Scripts
# #
class LogLevelChoices(ChoiceSet): 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 # Job results
# #

View File

@ -1,16 +1,14 @@
from django.contrib.contenttypes.models import ContentType
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.models import DataFile, DataSource
from extras.models import *
from extras.choices import CustomFieldVisibilityChoices from extras.choices import CustomFieldVisibilityChoices
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField
__all__ = ( __all__ = (
'CustomFieldsMixin', 'CustomFieldsMixin',
'SavedFiltersMixin', 'SavedFiltersMixin',
'SyncedDataMixin',
) )
@ -74,19 +72,3 @@ class SavedFiltersMixin(forms.Form):
'usable': True, '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',
}
)

View File

@ -5,9 +5,9 @@ from django.db.models import Q
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ 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 dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.forms.mixins import SyncedDataMixin
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm

View File

@ -1,8 +1,9 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin 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 from utilities.utils import local_now
__all__ = ( __all__ = (
@ -21,7 +22,9 @@ class ReportForm(BootstrapMixin, forms.Form):
required=False, required=False,
min_value=1, min_value=1,
label=_("Recurs every"), label=_("Recurs every"),
widget=SelectDurationWidget(), widget=NumberWithOptions(
options=DurationChoices
),
help_text=_("Interval at which this report is re-run (in minutes)") help_text=_("Interval at which this report is re-run (in minutes)")
) )

View File

@ -1,8 +1,9 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin 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 from utilities.utils import local_now
__all__ = ( __all__ = (
@ -27,7 +28,9 @@ class ScriptForm(BootstrapMixin, forms.Form):
required=False, required=False,
min_value=1, min_value=1,
label=_("Recurs every"), label=_("Recurs every"),
widget=SelectDurationWidget(), widget=NumberWithOptions(
options=DurationChoices
),
help_text=_("Interval at which this script is re-run (in minutes)") help_text=_("Interval at which this script is re-run (in minutes)")
) )

View File

@ -40,7 +40,6 @@ def custom_links(context, obj):
# Pass select context data when rendering the CustomLink # Pass select context data when rendering the CustomLink
link_context = { link_context = {
'object': obj, 'object': obj,
'obj': obj, # TODO: Remove in NetBox v3.5
'debug': context.get('debug', False), # django.template.context_processors.debug 'debug': context.get('debug', False), # django.template.context_processors.debug
'request': context['request'], # django.template.context_processors.request 'request': context['request'], # django.template.context_processors.request
'user': context['user'], # django.contrib.auth.context_processors.auth 'user': context['user'], # django.contrib.auth.context_processors.auth

View File

@ -479,8 +479,8 @@ class CustomLinkTest(TestCase):
def test_view_object_with_custom_link(self): def test_view_object_with_custom_link(self):
customlink = CustomLink( customlink = CustomLink(
name='Test', name='Test',
link_text='FOO {{ obj.name }} BAR', link_text='FOO {{ object.name }} BAR',
link_url='http://example.com/?site={{ obj.slug }}', link_url='http://example.com/?site={{ object.slug }}',
new_window=False new_window=False
) )
customlink.save() customlink.save()

View File

@ -581,14 +581,16 @@ REST_FRAMEWORK = {
# #
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
"TITLE": "NetBox API", 'TITLE': 'NetBox API',
"DESCRIPTION": "API to access NetBox", 'DESCRIPTION': 'API to access NetBox',
"LICENSE": {"name": "Apache v2 License"}, 'LICENSE': {'name': 'Apache v2 License'},
"VERSION": VERSION, 'VERSION': VERSION,
'COMPONENT_SPLIT_REQUEST': True, 'COMPONENT_SPLIT_REQUEST': True,
'REDOC_DIST': 'SIDECAR',
'SERVERS': [{'url': f'/{BASE_PATH}api'}],
'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_DIST': 'SIDECAR',
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR', 'POSTPROCESSING_HOOKS': [],
} }
# #

View File

@ -20,7 +20,7 @@ from extras.signals import clear_webhooks
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields 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.htmx import is_embedded, is_htmx
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.utils import get_viewname from utilities.utils import get_viewname
@ -425,7 +425,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
# #
def get(self, request): def get(self, request):
form = ImportForm() form = BulkImportForm()
return render(request, self.template_name, { return render(request, self.template_name, {
'model': self.model_form._meta.model, 'model': self.model_form._meta.model,
@ -438,7 +438,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
def post(self, request): def post(self, request):
logger = logging.getLogger('netbox.views.BulkImportView') logger = logging.getLogger('netbox.views.BulkImportView')
model = self.model_form._meta.model model = self.model_form._meta.model
form = ImportForm(request.POST, request.FILES) form = BulkImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
logger.debug("Import form validation was successful") logger.debug("Import form validation was successful")

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,7 @@ import { getElements } from '../util';
* Set the value of the number input field based on the selection of the dropdown. * Set the value of the number input field based on the selection of the dropdown.
*/ */
export function initSpeedSelector(): void { export function initSpeedSelector(): void {
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) { for (const element of getElements<HTMLAnchorElement>('a.set_field_value')) {
if (element !== null) { if (element !== null) {
function handleClick(event: Event) { function handleClick(event: Event) {
// Don't reload the page (due to href="#"). // Don't reload the page (due to href="#").

View File

@ -6,14 +6,14 @@ import yaml
from django import forms from django import forms
from django.utils.translation import gettext as _ 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.choices import ImportFormatChoices
from utilities.forms.utils import parse_csv from utilities.forms.utils import parse_csv
from .mixins import BootstrapMixin
from ..choices import ImportMethodChoices from ..choices import ImportMethodChoices
from .forms import BootstrapMixin
class ImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
import_method = forms.ChoiceField( import_method = forms.ChoiceField(
choices=ImportMethodChoices, choices=ImportMethodChoices,
required=False required=False

View File

@ -3,6 +3,7 @@ from django import forms
__all__ = ( __all__ = (
'ClearableFileInput', 'ClearableFileInput',
'MarkdownWidget', 'MarkdownWidget',
'NumberWithOptions',
'SlugWidget', 'SlugWidget',
) )
@ -21,6 +22,22 @@ class MarkdownWidget(forms.Textarea):
template_name = 'widgets/markdown_input.html' 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): class SlugWidget(forms.TextInput):
""" """
Subclass TextInput and add a slug regeneration button next to the form field. Subclass TextInput and add a slug regeneration button next to the form field.

View File

@ -7,8 +7,6 @@ __all__ = (
'BulkEditNullBooleanSelect', 'BulkEditNullBooleanSelect',
'ColorSelect', 'ColorSelect',
'HTMXSelect', 'HTMXSelect',
'SelectDurationWidget',
'SelectSpeedWidget',
'SelectWithPK', 'SelectWithPK',
) )
@ -63,17 +61,3 @@ class SelectWithPK(forms.Select):
Include the primary key of each option in the option label (e.g. "Router7 (4721)"). Include the primary key of each option in the option label (e.g. "Router7 (4721)").
""" """
option_template_name = 'widgets/select_option_with_pk.html' 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'

View File

@ -0,0 +1,11 @@
<div class="input-group">
{% include 'django/forms/widgets/number.html' %}
<button type="button" class="btn btn-outline-dark border-input dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
{% for value, label in widget.options %}
<li>
<a href="#" target="id_{{ widget.name }}" data="{{ value }}" class="set_field_value dropdown-item">{{ label }}</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -1,11 +0,0 @@
<div class="input-group">
{% include 'django/forms/widgets/number.html' %}
<button type="button" class="btn btn-outline-dark border-input dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a href="#" target="id_{{ widget.name }}" data="60" class="set_speed dropdown-item">Hourly</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="720" class="set_speed dropdown-item">12 hours</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="1440" class="set_speed dropdown-item">Daily</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="10080" class="set_speed dropdown-item">Weekly</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="43200" class="set_speed dropdown-item">30 days</a></li>
</ul>
</div>

View File

@ -1,16 +0,0 @@
<div class="input-group">
{% include 'django/forms/widgets/number.html' %}
<button type="button" class="btn btn-outline-dark border-input dropdown-toggle" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a href="#" target="id_{{ widget.name }}" data="10000" class="set_speed dropdown-item">10 Mbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="100000" class="set_speed dropdown-item">100 Mbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="1000000" class="set_speed dropdown-item">1 Gbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="10000000" class="set_speed dropdown-item">10 Gbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="25000000" class="set_speed dropdown-item">25 Gbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="40000000" class="set_speed dropdown-item">40 Gbps</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="100000000" class="set_speed dropdown-item">100 Gbps</a></li>
<li><hr class="dropdown-divider"/></li>
<li><a href="#" target="id_{{ widget.name }}" data="1544" class="set_speed dropdown-item">T1 (1.544 Mbps)</a></li>
<li><a href="#" target="id_{{ widget.name }}" data="2048" class="set_speed dropdown-item">E1 (2.048 Mbps)</a></li>
</ul>
</div>

View File

@ -2,7 +2,7 @@ from django import forms
from django.test import TestCase from django.test import TestCase
from utilities.choices import ImportFormatChoices 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 from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
@ -288,7 +288,7 @@ class ExpandAlphanumeric(TestCase):
class ImportFormTest(TestCase): class ImportFormTest(TestCase):
def test_format_detection(self): def test_format_detection(self):
form = ImportForm() form = BulkImportForm()
data = ( data = (
"a,b,c\n" "a,b,c\n"