mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 17:26:10 -06:00
Merge branch 'feature' into 12278-ipaddressfield-serialize
This commit is contained in:
commit
7923d180ec
@ -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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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) |
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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/`
|
||||||
|
@ -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)'),
|
||||||
|
]
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
25
netbox/core/forms/mixins.py
Normal file
25
netbox/core/forms/mixins.py
Normal 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',
|
||||||
|
}
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@ -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
|
||||||
|
@ -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)")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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")
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -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="#").
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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'
|
|
||||||
|
11
netbox/utilities/templates/widgets/number_with_options.html
Normal file
11
netbox/utilities/templates/widgets/number_with_options.html
Normal 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>
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user