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
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

View File

@ -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:

View File

@ -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) |

View File

@ -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.

View File

@ -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

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).
!!! 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

View File

@ -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.
# 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.

View File

@ -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

View File

@ -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.

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 supported on NetBox v2.8 and later.
## Capabilities
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 `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/`

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

View File

@ -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,

View File

@ -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)

View File

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

View File

@ -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)

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 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

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):
DUPLEX_HALF = 'half'

View File

@ -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,

View File

@ -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,

View File

@ -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 = {

View File

@ -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
#

View File

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

View File

@ -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

View File

@ -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)")
)

View File

@ -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)")
)

View File

@ -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

View File

@ -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()

View File

@ -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': [],
}
#

View File

@ -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")

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.
*/
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) {
function handleClick(event: Event) {
// Don't reload the page (due to href="#").

View File

@ -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

View File

@ -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.

View File

@ -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'

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 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"