Merge pull request #22580 from netbox-community/21712-static-select-descriptions

Closes #21712: Support description annotations for static choice form fields
This commit is contained in:
bctiemann 2026-07-06 14:47:18 -04:00 committed by GitHub
commit a6da836df8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2640 additions and 1415 deletions

View File

@ -56,6 +56,20 @@ FIELD_CHOICES = {
}
```
In addition to plain tuples, each choice may be defined as a dictionary, which allows specifying a description (shown as a subtitle beneath the option) alongside the value, label, and color. `value` and `label` are required; `color` and `description` are optional:
```python
FIELD_CHOICES = {
'dcim.Site.status': (
{'value': 'foo', 'label': 'Foo', 'color': 'red', 'description': 'The foo status'},
{'value': 'bar', 'label': 'Bar', 'color': 'green'},
)
}
```
!!! info "New in NetBox v4.7"
The dictionary-based format for declaring choices was introduced in NetBox v4.7. The tuple-based format remains supported, but will be deprecated in a future release and support for it will eventually be removed.
!!! info "Case-Insensitive Field Identifiers"
Field identifiers are case-insensitive. Both `dcim.Site.status` and `dcim.site.status` are valid and equivalent.

View File

@ -210,6 +210,35 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
options:
members: false
## Static Choice Fields
These fields render a standard HTML `<select>` element (as opposed to the API-backed widgets used by the dynamic object fields below). They extend Django's built-in choice fields to optionally display a short **description** beneath each option's label.
For choice set-backed fields, descriptions are defined per choice using a `Choice` object in the `ChoiceSet` and are rendered automatically. Pass `show_descriptions=False` to suppress them for a particular field.
```python
from utilities.choices import Choice, ChoiceSet
from utilities.forms.fields import ChoiceField
class StatusChoices(ChoiceSet):
ACTIVE = 'active'
RETIRED = 'retired'
CHOICES = (
Choice(ACTIVE, 'Active', description='Currently in service'),
Choice(RETIRED, 'Retired', description='No longer in service'),
)
status = ChoiceField(choices=StatusChoices)
```
::: utilities.forms.fields.ChoiceField
options:
members: false
::: utilities.forms.fields.MultipleChoiceField
options:
members: false
## Dynamic Object Fields
::: utilities.forms.fields.DynamicModelChoiceField

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# Circuits
@ -18,12 +18,18 @@ class CircuitStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = [
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_PROVISIONING, _('Provisioning'), 'blue'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_OFFLINE, _('Offline'), 'red'),
(STATUS_DEPROVISIONING, _('Deprovisioning'), 'yellow'),
(STATUS_DECOMMISSIONED, _('Decommissioned'), 'gray'),
Choice(
STATUS_PLANNED, _('Planned'), color='cyan',
description=_('Designated for future use but not yet installed')
),
Choice(STATUS_PROVISIONING, _('Provisioning'), color='blue', description=_('Being configured for service')),
Choice(STATUS_ACTIVE, _('Active'), color='green', description=_('Fully operational and in service')),
Choice(STATUS_OFFLINE, _('Offline'), color='red', description=_('Installed but not currently in service')),
Choice(STATUS_DEPROVISIONING, _('Deprovisioning'), color='yellow', description=_('Being removed from service')),
Choice(
STATUS_DECOMMISSIONED, _('Decommissioned'), color='gray',
description=_('Retired and no longer in service')
),
]
@ -31,17 +37,17 @@ 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'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
Choice(10000, '10 Mbps'),
Choice(100000, '100 Mbps'),
Choice(1000000, '1 Gbps'),
Choice(10000000, '10 Gbps'),
Choice(25000000, '25 Gbps'),
Choice(40000000, '40 Gbps'),
Choice(100000000, '100 Gbps'),
Choice(200000000, '200 Gbps'),
Choice(400000000, '400 Gbps'),
Choice(1544, 'T1 (1.544 Mbps)'),
Choice(2048, 'E1 (2.048 Mbps)'),
]
@ -55,8 +61,8 @@ class CircuitTerminationSideChoices(ChoiceSet):
SIDE_Z = 'Z'
CHOICES = (
(SIDE_A, 'A'),
(SIDE_Z, 'Z')
Choice(SIDE_A, 'A'),
Choice(SIDE_Z, 'Z')
)
@ -64,17 +70,17 @@ 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'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
Choice(10000, '10 Mbps'),
Choice(100000, '100 Mbps'),
Choice(1000000, '1 Gbps'),
Choice(10000000, '10 Gbps'),
Choice(25000000, '25 Gbps'),
Choice(40000000, '40 Gbps'),
Choice(100000000, '100 Gbps'),
Choice(200000000, '200 Gbps'),
Choice(400000000, '400 Gbps'),
Choice(1544, 'T1 (1.544 Mbps)'),
Choice(2048, 'E1 (2.048 Mbps)'),
]
@ -87,10 +93,10 @@ class CircuitPriorityChoices(ChoiceSet):
PRIORITY_INACTIVE = 'inactive'
CHOICES = [
(PRIORITY_PRIMARY, _('Primary')),
(PRIORITY_SECONDARY, _('Secondary')),
(PRIORITY_TERTIARY, _('Tertiary')),
(PRIORITY_INACTIVE, _('Inactive')),
Choice(PRIORITY_PRIMARY, _('Primary')),
Choice(PRIORITY_SECONDARY, _('Secondary')),
Choice(PRIORITY_TERTIARY, _('Tertiary')),
Choice(PRIORITY_INACTIVE, _('Inactive')),
]
@ -104,7 +110,7 @@ class VirtualCircuitTerminationRoleChoices(ChoiceSet):
ROLE_SPOKE = 'spoke'
CHOICES = [
(ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke'), 'orange'),
Choice(ROLE_PEER, _('Peer'), color='green', description=_('Connects to other peers as an equal endpoint')),
Choice(ROLE_HUB, _('Hub'), color='blue', description=_('Central endpoint to which spokes connect')),
Choice(ROLE_SPOKE, _('Spoke'), color='orange', description=_('Remote endpoint connecting to a hub')),
]

View File

@ -16,6 +16,7 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
from tenancy.models import Tenant
from utilities.forms import GenericObjectFormMixin, add_blank_choice
from utilities.forms.fields import (
ChoiceField,
ColorField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
@ -124,7 +125,7 @@ class CircuitBulkEditForm(PrimaryModelBulkEditForm):
'provider': '$provider'
}
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
@ -157,7 +158,7 @@ class CircuitBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
distance_unit = forms.ChoiceField(
distance_unit = ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,
@ -232,7 +233,7 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
queryset=Circuit.objects.all(),
required=False
)
priority = forms.ChoiceField(
priority = ChoiceField(
label=_('Priority'),
choices=add_blank_choice(CircuitPriorityChoices),
required=False
@ -274,7 +275,7 @@ class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
queryset=VirtualCircuitType.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
@ -297,7 +298,7 @@ class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
role = ChoiceField(
label=_('Role'),
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
required=False,

View File

@ -1,24 +1,29 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from circuits.choices import (
CircuitCommitRateChoices,
CircuitPriorityChoices,
CircuitStatusChoices,
CircuitTerminationPortSpeedChoices,
CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
from dcim.models import Interface
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from tenancy.forms import TenancyForm
from utilities.forms import GenericObjectFormMixin
from utilities.forms import GenericObjectFormMixin, add_blank_choice
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
GenericObjectChoiceField,
SlugField,
TypedChoiceField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields
@ -134,6 +139,16 @@ class CircuitTypeForm(OrganizationalModelForm):
class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
initial=CircuitStatusChoices.STATUS_ACTIVE,
)
distance_unit = TypedChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,
)
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
@ -185,6 +200,10 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
class CircuitTerminationForm(GenericObjectFormMixin, NetBoxModelForm):
term_side = ChoiceField(
label=_('Termination side'),
choices=CircuitTerminationSideChoices,
)
circuit = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
@ -236,6 +255,11 @@ class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
class CircuitGroupAssignmentForm(GenericObjectFormMixin, NetBoxModelForm):
priority = TypedChoiceField(
label=_('Priority'),
choices=add_blank_choice(CircuitPriorityChoices),
required=False,
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=CircuitGroup.objects.all(),
@ -275,6 +299,11 @@ class VirtualCircuitTypeForm(OrganizationalModelForm):
class VirtualCircuitForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
initial=CircuitStatusChoices.STATUS_ACTIVE,
)
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
@ -312,7 +341,7 @@ class VirtualCircuitTerminationForm(NetBoxModelForm):
queryset=VirtualCircuit.objects.all(),
selector=True
)
role = forms.ChoiceField(
role = ChoiceField(
choices=VirtualCircuitTerminationRoleChoices,
label=_('Role')
)

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# Data sources
@ -15,11 +15,11 @@ class DataSourceStatusChoices(ChoiceSet):
FAILED = 'failed'
CHOICES = (
(NEW, _('New'), 'blue'),
(QUEUED, _('Queued'), 'orange'),
(SYNCING, _('Syncing'), 'cyan'),
(COMPLETED, _('Completed'), 'green'),
(FAILED, _('Failed'), 'red'),
Choice(NEW, _('New'), color='blue', description=_('Newly created and not yet synchronized')),
Choice(QUEUED, _('Queued'), color='orange', description=_('Queued for synchronization')),
Choice(SYNCING, _('Syncing'), color='cyan', description=_('Synchronization in progress')),
Choice(COMPLETED, _('Completed'), color='green', description=_('Most recent synchronization succeeded')),
Choice(FAILED, _('Failed'), color='red', description=_('Most recent synchronization failed')),
)
@ -32,8 +32,8 @@ class ManagedFileRootPathChoices(ChoiceSet):
REPORTS = 'reports' # settings.REPORTS_ROOT
CHOICES = (
(SCRIPTS, _('Scripts')),
(REPORTS, _('Reports')),
Choice(SCRIPTS, _('Scripts')),
Choice(REPORTS, _('Reports')),
)
@ -51,12 +51,12 @@ class JobStatusChoices(ChoiceSet):
STATUS_FAILED = 'failed'
CHOICES = (
(STATUS_PENDING, _('Pending'), 'cyan'),
(STATUS_SCHEDULED, _('Scheduled'), 'gray'),
(STATUS_RUNNING, _('Running'), 'blue'),
(STATUS_COMPLETED, _('Completed'), 'green'),
(STATUS_ERRORED, _('Errored'), 'red'),
(STATUS_FAILED, _('Failed'), 'red'),
Choice(STATUS_PENDING, _('Pending'), color='cyan', description=_('Awaiting execution')),
Choice(STATUS_SCHEDULED, _('Scheduled'), color='gray', description=_('Scheduled to run at a future time')),
Choice(STATUS_RUNNING, _('Running'), color='blue', description=_('Currently executing')),
Choice(STATUS_COMPLETED, _('Completed'), color='green', description=_('Finished successfully')),
Choice(STATUS_ERRORED, _('Errored'), color='red', description=_('Terminated due to an unhandled error')),
Choice(STATUS_FAILED, _('Failed'), color='red', description=_('Failed to complete')),
)
ENQUEUED_STATE_CHOICES = (
@ -78,9 +78,9 @@ class JobNotificationChoices(ChoiceSet):
NOTIFICATION_NEVER = 'never'
CHOICES = (
(NOTIFICATION_ALWAYS, _('Always')),
(NOTIFICATION_ON_FAILURE, _('On failure')),
(NOTIFICATION_NEVER, _('Never')),
Choice(NOTIFICATION_ALWAYS, _('Always'), description=_('Notify after every job execution')),
Choice(NOTIFICATION_ON_FAILURE, _('On failure'), description=_('Notify only when a job fails')),
Choice(NOTIFICATION_NEVER, _('Never'), description=_('Never send job notifications')),
)
@ -91,12 +91,12 @@ class JobIntervalChoices(ChoiceSet):
INTERVAL_WEEKLY = 60 * 24 * 7
CHOICES = (
(INTERVAL_MINUTELY, _('Minutely')),
(INTERVAL_HOURLY, _('Hourly')),
(INTERVAL_HOURLY * 12, _('12 hours')),
(INTERVAL_DAILY, _('Daily')),
(INTERVAL_WEEKLY, _('Weekly')),
(INTERVAL_DAILY * 30, _('30 days')),
Choice(INTERVAL_MINUTELY, _('Minutely')),
Choice(INTERVAL_HOURLY, _('Hourly')),
Choice(INTERVAL_HOURLY * 12, _('12 hours')),
Choice(INTERVAL_DAILY, _('Daily')),
Choice(INTERVAL_WEEKLY, _('Weekly')),
Choice(INTERVAL_DAILY * 30, _('30 days')),
)
@ -111,7 +111,7 @@ class ObjectChangeActionChoices(ChoiceSet):
ACTION_DELETE = 'delete'
CHOICES = (
(ACTION_CREATE, _('Created'), 'green'),
(ACTION_UPDATE, _('Updated'), 'blue'),
(ACTION_DELETE, _('Deleted'), 'red'),
Choice(ACTION_CREATE, _('Created'), color='green'),
Choice(ACTION_UPDATE, _('Updated'), color='blue'),
Choice(ACTION_DELETE, _('Deleted'), color='red'),
)

View File

@ -5,6 +5,7 @@ from core.choices import JobIntervalChoices
from core.models import *
from netbox.forms import PrimaryModelBulkEditForm
from netbox.utils import get_data_backend_choices
from utilities.forms.fields import ChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
@ -14,7 +15,7 @@ __all__ = (
class DataSourceBulkEditForm(PrimaryModelBulkEditForm):
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=get_data_backend_choices,
required=False
@ -24,7 +25,7 @@ class DataSourceBulkEditForm(PrimaryModelBulkEditForm):
widget=BulkEditNullBooleanSelect(),
label=_('Enabled')
)
sync_interval = forms.ChoiceField(
sync_interval = ChoiceField(
choices=JobIntervalChoices,
required=False,
label=_('Sync interval')

View File

@ -13,7 +13,7 @@ from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices
from utilities.forms import get_field_value
from utilities.forms.fields import JSONField
from utilities.forms.fields import ChoiceField, JSONField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import HTMXSelect
@ -27,7 +27,7 @@ EMPTY_VALUES = ('', None, [], ())
class DataSourceForm(PrimaryModelForm):
type = forms.ChoiceField(
type = ChoiceField(
choices=get_data_backend_choices,
# No hx_target_id: changing type adds/removes the Backend Parameters fieldset entirely.
widget=HTMXSelect()

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ from tenancy.models import Tenant
from users.models import User
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import (
ChoiceField,
ColorField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
@ -111,7 +112,7 @@ class SiteGroupBulkEditForm(NestedGroupModelBulkEditForm):
class SiteBulkEditForm(PrimaryModelBulkEditForm):
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(SiteStatusChoices),
required=False,
@ -185,7 +186,7 @@ class LocationBulkEditForm(NestedGroupModelBulkEditForm):
'site_id': '$site'
}
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(LocationStatusChoices),
required=False,
@ -236,12 +237,12 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
queryset=Manufacturer.objects.all(),
required=False
)
form_factor = forms.ChoiceField(
form_factor = ChoiceField(
label=_('Form factor'),
choices=add_blank_choice(RackFormFactorChoices),
required=False
)
width = forms.ChoiceField(
width = ChoiceField(
label=_('Width'),
choices=add_blank_choice(RackWidthChoices),
required=False
@ -274,7 +275,7 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
required=False,
min_value=1
)
outer_unit = forms.ChoiceField(
outer_unit = ChoiceField(
label=_('Outer unit'),
choices=add_blank_choice(RackDimensionUnitChoices),
required=False
@ -294,7 +295,7 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
weight_unit = forms.ChoiceField(
weight_unit = ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
@ -362,7 +363,7 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(RackStatusChoices),
required=False,
@ -388,12 +389,12 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
max_length=50,
required=False
)
form_factor = forms.ChoiceField(
form_factor = ChoiceField(
label=_('Form factor'),
choices=add_blank_choice(RackFormFactorChoices),
required=False
)
width = forms.ChoiceField(
width = ChoiceField(
label=_('Width'),
choices=add_blank_choice(RackWidthChoices),
required=False
@ -422,7 +423,7 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
required=False,
min_value=1
)
outer_unit = forms.ChoiceField(
outer_unit = ChoiceField(
label=_('Outer unit'),
choices=add_blank_choice(RackDimensionUnitChoices),
required=False
@ -432,7 +433,7 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
required=False,
min_value=1
)
airflow = forms.ChoiceField(
airflow = ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(RackAirflowChoices),
required=False
@ -447,7 +448,7 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
weight_unit = forms.ChoiceField(
weight_unit = ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
@ -471,7 +472,7 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
class RackReservationBulkEditForm(PrimaryModelBulkEditForm):
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(RackReservationStatusChoices),
required=False,
@ -533,7 +534,7 @@ class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
widget=BulkEditNullBooleanSelect(),
label=_('Exclude from utilization')
)
airflow = forms.ChoiceField(
airflow = ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False
@ -543,7 +544,7 @@ class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
weight_unit = forms.ChoiceField(
weight_unit = ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
@ -589,7 +590,7 @@ class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm):
label=_('Part number'),
required=False
)
airflow = forms.ChoiceField(
airflow = ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(ModuleAirflowChoices),
required=False
@ -599,7 +600,7 @@ class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
weight_unit = forms.ChoiceField(
weight_unit = ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
@ -715,12 +716,12 @@ class DeviceBulkEditForm(PrimaryModelBulkEditForm):
queryset=Platform.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(DeviceStatusChoices),
required=False
)
airflow = forms.ChoiceField(
airflow = ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False
@ -774,7 +775,7 @@ class ModuleBulkEditForm(PrimaryModelBulkEditForm):
'parent': 'manufacturer',
}
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(ModuleStatusChoices),
required=False,
@ -812,19 +813,19 @@ class CableBundleBulkEditForm(PrimaryModelBulkEditForm):
class CableBulkEditForm(PrimaryModelBulkEditForm):
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(CableTypeChoices),
required=False,
initial=''
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(LinkStatusChoices),
required=False,
initial=''
)
profile = forms.ChoiceField(
profile = ChoiceField(
label=_('Profile'),
choices=add_blank_choice(CableProfileChoices),
required=False,
@ -854,7 +855,7 @@ class CableBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
length_unit = forms.ChoiceField(
length_unit = ChoiceField(
label=_('Length unit'),
choices=add_blank_choice(CableLengthUnitChoices),
required=False,
@ -938,25 +939,25 @@ class PowerFeedBulkEditForm(PrimaryModelBulkEditForm):
queryset=Rack.objects.all(),
required=False,
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(PowerFeedStatusChoices),
required=False,
initial=''
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerFeedTypeChoices),
required=False,
initial=''
)
supply = forms.ChoiceField(
supply = ChoiceField(
label=_('Supply'),
choices=add_blank_choice(PowerFeedSupplyChoices),
required=False,
initial=''
)
phase = forms.ChoiceField(
phase = ChoiceField(
label=_('Phase'),
choices=add_blank_choice(PowerFeedPhaseChoices),
required=False,
@ -1010,7 +1011,7 @@ class ConsolePortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False
@ -1029,7 +1030,7 @@ class ConsoleServerPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False
@ -1052,7 +1053,7 @@ class PowerPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerPortTypeChoices),
required=False
@ -1094,7 +1095,7 @@ class PowerOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerOutletTypeChoices),
required=False
@ -1108,7 +1109,7 @@ class PowerOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
queryset=PowerPortTemplate.objects.all(),
required=False
)
feed_leg = forms.ChoiceField(
feed_leg = ChoiceField(
label=_('Feed leg'),
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False
@ -1142,7 +1143,7 @@ class InterfaceTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(InterfaceTypeChoices),
required=False
@ -1161,19 +1162,19 @@ class InterfaceTemplateBulkEditForm(ComponentTemplateBulkEditForm):
label=_('Description'),
required=False
)
poe_mode = forms.ChoiceField(
poe_mode = ChoiceField(
choices=add_blank_choice(InterfacePoEModeChoices),
required=False,
initial='',
label=_('PoE mode')
)
poe_type = forms.ChoiceField(
poe_type = ChoiceField(
choices=add_blank_choice(InterfacePoETypeChoices),
required=False,
initial='',
label=_('PoE type')
)
rf_role = forms.ChoiceField(
rf_role = ChoiceField(
choices=add_blank_choice(WirelessRoleChoices),
required=False,
initial='',
@ -1193,7 +1194,7 @@ class FrontPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(PortTypeChoices),
required=False
@ -1220,7 +1221,7 @@ class RearPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
max_length=64,
required=False
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(PortTypeChoices),
required=False
@ -1488,13 +1489,13 @@ class InterfaceBulkEditForm(
widget=BulkEditNullBooleanSelect,
label=_('Management only')
)
poe_mode = forms.ChoiceField(
poe_mode = ChoiceField(
choices=add_blank_choice(InterfacePoEModeChoices),
required=False,
initial='',
label=_('PoE mode')
)
poe_type = forms.ChoiceField(
poe_type = ChoiceField(
choices=add_blank_choice(InterfacePoETypeChoices),
required=False,
initial='',
@ -1505,7 +1506,7 @@ class InterfaceBulkEditForm(
required=False,
widget=BulkEditNullBooleanSelect
)
mode = forms.ChoiceField(
mode = ChoiceField(
label=_('Mode'),
choices=add_blank_choice(InterfaceModeChoices),
required=False,
@ -1743,7 +1744,7 @@ class InventoryItemBulkEditForm(
queryset=Manufacturer.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(InventoryItemStatusChoices),
required=False,
@ -1787,7 +1788,7 @@ class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm):
queryset=Device.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
required=False,
choices=add_blank_choice(VirtualDeviceContextStatusChoices)

View File

@ -11,17 +11,21 @@ from dcim.models import *
from extras.models import ConfigTemplate
from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy
from netbox.choices import WeightUnitChoices
from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
from tenancy.forms import TenancyForm
from users.models import User
from utilities.choices import Choice
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
JSONField,
NumericArrayField,
SlugField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields, TabbedGroups
from utilities.forms.widgets import (
@ -34,6 +38,7 @@ from utilities.forms.widgets import (
)
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
from wireless.models import WirelessLAN, WirelessLANGroup
from .common import InterfaceCommonForm, ModuleCommonForm
@ -127,6 +132,11 @@ class SiteGroupForm(NestedGroupModelForm):
class SiteForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=SiteStatusChoices,
initial=SiteStatusChoices.STATUS_ACTIVE,
)
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
@ -207,6 +217,11 @@ class SiteForm(TenancyForm, PrimaryModelForm):
class LocationForm(TenancyForm, NestedGroupModelForm):
status = ChoiceField(
label=_('Status'),
choices=LocationStatusChoices,
initial=LocationStatusChoices.STATUS_ACTIVE,
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
@ -259,6 +274,20 @@ class RackRoleForm(OrganizationalModelForm):
class RackTypeForm(PrimaryModelForm):
form_factor = ChoiceField(
label=_('Form factor'),
choices=RackFormFactorChoices,
)
outer_unit = TypedChoiceField(
label=_('Outer unit'),
choices=add_blank_choice(RackDimensionUnitChoices),
required=False,
)
weight_unit = TypedChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
@ -290,6 +319,31 @@ class RackTypeForm(PrimaryModelForm):
class RackForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=RackStatusChoices,
initial=RackStatusChoices.STATUS_ACTIVE,
)
form_factor = TypedChoiceField(
label=_('Form factor'),
choices=add_blank_choice(RackFormFactorChoices),
required=False,
)
outer_unit = TypedChoiceField(
label=_('Outer unit'),
choices=add_blank_choice(RackDimensionUnitChoices),
required=False,
)
airflow = TypedChoiceField(
label=_('Airflow'),
choices=add_blank_choice(RackAirflowChoices),
required=False,
)
weight_unit = TypedChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
@ -367,6 +421,11 @@ class RackForm(TenancyForm, PrimaryModelForm):
class RackReservationForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=RackReservationStatusChoices,
initial=RackReservationStatusChoices.STATUS_ACTIVE,
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
@ -407,6 +466,25 @@ class ManufacturerForm(OrganizationalModelForm):
class DeviceTypeForm(PrimaryModelForm):
subdevice_role = TypedChoiceField(
label=_('Parent/child status'),
choices=add_blank_choice(SubdeviceRoleChoices),
required=False,
help_text=_(
'Parent devices house child devices in device bays. Leave blank if this device type is neither a '
'parent nor a child.'
),
)
airflow = TypedChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False,
)
weight_unit = TypedChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
@ -471,6 +549,16 @@ class ModuleTypeProfileForm(PrimaryModelForm):
class ModuleTypeForm(PrimaryModelForm):
airflow = TypedChoiceField(
label=_('Airflow'),
choices=add_blank_choice(ModuleAirflowChoices),
required=False,
)
weight_unit = TypedChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False,
)
profile = forms.ModelChoiceField(
queryset=ModuleTypeProfile.objects.all(),
label=_('Profile'),
@ -611,6 +699,16 @@ class PlatformForm(NestedGroupModelForm):
class DeviceForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=DeviceStatusChoices,
initial=DeviceStatusChoices.STATUS_ACTIVE,
)
airflow = TypedChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False,
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
@ -649,7 +747,7 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
},
)
)
face = forms.ChoiceField(
face = TypedChoiceField(
label=_('Face'),
choices=add_blank_choice(DeviceFaceChoices),
required=False,
@ -790,6 +888,11 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
class ModuleForm(ModuleCommonForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=ModuleStatusChoices,
initial=ModuleStatusChoices.STATUS_ACTIVE,
)
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
@ -853,7 +956,7 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
def get_termination_type_choices():
return add_blank_choice([
(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
Choice(f'{ct.app_label}.{ct.model}', ct.model_class()._meta.verbose_name.title())
for ct in ContentType.objects.filter(CABLE_TERMINATION_MODELS)
])
@ -870,13 +973,33 @@ class CableBundleForm(PrimaryModelForm):
class CableForm(TenancyForm, PrimaryModelForm):
a_terminations_type = forms.ChoiceField(
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(CableTypeChoices),
required=False,
)
status = ChoiceField(
label=_('Status'),
choices=LinkStatusChoices,
initial=LinkStatusChoices.STATUS_CONNECTED,
)
profile = ChoiceField(
label=_('Profile'),
choices=add_blank_choice(CableProfileChoices),
required=False,
)
length_unit = TypedChoiceField(
label=_('Length unit'),
choices=add_blank_choice(CableLengthUnitChoices),
required=False,
)
a_terminations_type = ChoiceField(
choices=get_termination_type_choices,
required=False,
widget=HTMXSelect(hx_target_id='cable-side-a'),
label=_('Type')
)
b_terminations_type = forms.ChoiceField(
b_terminations_type = ChoiceField(
choices=get_termination_type_choices,
required=False,
widget=HTMXSelect(hx_target_id='cable-side-b'),
@ -923,6 +1046,26 @@ class PowerPanelForm(PrimaryModelForm):
class PowerFeedForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=PowerFeedStatusChoices,
initial=PowerFeedStatusChoices.STATUS_ACTIVE,
)
type = ChoiceField(
label=_('Type'),
choices=PowerFeedTypeChoices,
initial=PowerFeedTypeChoices.TYPE_PRIMARY,
)
supply = ChoiceField(
label=_('Supply'),
choices=PowerFeedSupplyChoices,
initial=PowerFeedSupplyChoices.SUPPLY_AC,
)
phase = ChoiceField(
label=_('Phase'),
choices=PowerFeedPhaseChoices,
initial=PowerFeedPhaseChoices.PHASE_SINGLE,
)
power_panel = DynamicModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(),
@ -1106,6 +1249,12 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
class ConsolePortTemplateForm(ModularComponentTemplateForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
)
class Meta:
model = ConsolePortTemplate
fields = [
@ -1114,6 +1263,12 @@ class ConsolePortTemplateForm(ModularComponentTemplateForm):
class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
@ -1122,6 +1277,11 @@ class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
class PowerPortTemplateForm(ModularComponentTemplateForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerPortTypeChoices),
required=False,
)
fieldsets = (
FieldSet(
TabbedGroups(
@ -1140,6 +1300,17 @@ class PowerPortTemplateForm(ModularComponentTemplateForm):
class PowerOutletTemplateForm(ModularComponentTemplateForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
)
feed_leg = TypedChoiceField(
label=_('Feed leg'),
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
help_text=_('Phase (for three-phase feeds)'),
)
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(),
@ -1167,6 +1338,25 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
class InterfaceTemplateForm(ModularComponentTemplateForm):
type = ChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices,
)
poe_mode = TypedChoiceField(
label=_('PoE mode'),
choices=add_blank_choice(InterfacePoEModeChoices),
required=False,
)
poe_type = TypedChoiceField(
label=_('PoE type'),
choices=add_blank_choice(InterfacePoETypeChoices),
required=False,
)
rf_role = TypedChoiceField(
label=_('Wireless role'),
choices=add_blank_choice(WirelessRoleChoices),
required=False,
)
bridge = DynamicModelChoiceField(
label=_('Bridge'),
queryset=InterfaceTemplate.objects.all(),
@ -1198,6 +1388,10 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
type = ChoiceField(
label=_('Type'),
choices=PortTypeChoices,
)
fieldsets = (
FieldSet(
TabbedGroups(
@ -1238,6 +1432,10 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
class RearPortTemplateForm(ModularComponentTemplateForm):
type = ChoiceField(
label=_('Type'),
choices=PortTypeChoices,
)
fieldsets = (
FieldSet(
TabbedGroups(
@ -1461,6 +1659,12 @@ class ModularDeviceComponentForm(DeviceComponentForm):
class ConsolePortForm(ModularDeviceComponentForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
help_text=_('Physical port type'),
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
@ -1475,6 +1679,12 @@ class ConsolePortForm(ModularDeviceComponentForm):
class ConsoleServerPortForm(ModularDeviceComponentForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(ConsolePortTypeChoices),
required=False,
help_text=_('Physical port type'),
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
@ -1489,6 +1699,12 @@ class ConsoleServerPortForm(ModularDeviceComponentForm):
class PowerPortForm(ModularDeviceComponentForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerPortTypeChoices),
required=False,
help_text=_('Physical port type'),
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
@ -1505,6 +1721,23 @@ class PowerPortForm(ModularDeviceComponentForm):
class PowerOutletForm(ModularDeviceComponentForm):
type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerOutletTypeChoices),
required=False,
help_text=_('Physical port type'),
)
status = ChoiceField(
label=_('Status'),
choices=PowerOutletStatusChoices,
initial=PowerOutletStatusChoices.STATUS_ENABLED,
)
feed_leg = TypedChoiceField(
label=_('Feed leg'),
choices=add_blank_choice(PowerOutletFeedLegChoices),
required=False,
help_text=_('Phase (for three-phase feeds)'),
)
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(),
@ -1530,6 +1763,41 @@ class PowerOutletForm(ModularDeviceComponentForm):
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
type = ChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices,
)
duplex = TypedChoiceField(
label=_('Duplex'),
choices=add_blank_choice(InterfaceDuplexChoices),
required=False,
)
poe_mode = TypedChoiceField(
label=_('PoE mode'),
choices=add_blank_choice(InterfacePoEModeChoices),
required=False,
)
poe_type = TypedChoiceField(
label=_('PoE type'),
choices=add_blank_choice(InterfacePoETypeChoices),
required=False,
)
mode = TypedChoiceField(
label=_('802.1Q Mode'),
choices=add_blank_choice(InterfaceModeChoices),
required=False,
help_text=_('IEEE 802.1Q tagging strategy'),
)
rf_role = TypedChoiceField(
label=_('Wireless role'),
choices=add_blank_choice(WirelessRoleChoices),
required=False,
)
rf_channel = TypedChoiceField(
label=_('Wireless channel'),
choices=add_blank_choice(WirelessChannelChoices),
required=False,
)
vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
@ -1676,6 +1944,10 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
type = ChoiceField(
label=_('Type'),
choices=PortTypeChoices,
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
@ -1712,6 +1984,10 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
class RearPortForm(ModularDeviceComponentForm):
type = ChoiceField(
label=_('Type'),
choices=PortTypeChoices,
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
@ -1770,6 +2046,11 @@ class PopulateDeviceBayForm(forms.Form):
class InventoryItemForm(DeviceComponentForm):
status = ChoiceField(
label=_('Status'),
choices=InventoryItemStatusChoices,
initial=InventoryItemStatusChoices.STATUS_ACTIVE,
)
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItem.objects.all(),
@ -1930,6 +2211,10 @@ class InventoryItemRoleForm(OrganizationalModelForm):
class VirtualDeviceContextForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=VirtualDeviceContextStatusChoices,
)
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),

View File

@ -3,7 +3,7 @@ import logging
from django.utils.translation import gettext_lazy as _
from netbox.choices import ButtonColorChoices
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# CustomFields
@ -27,19 +27,23 @@ class CustomFieldTypeChoices(ChoiceSet):
TYPE_MULTIOBJECT = 'multiobject'
CHOICES = (
(TYPE_TEXT, _('Text')),
(TYPE_LONGTEXT, _('Text (long)')),
(TYPE_INTEGER, _('Integer')),
(TYPE_DECIMAL, _('Decimal')),
(TYPE_BOOLEAN, _('Boolean (true/false)')),
(TYPE_DATE, _('Date')),
(TYPE_DATETIME, _('Date & time')),
(TYPE_URL, _('URL')),
(TYPE_JSON, _('JSON')),
(TYPE_SELECT, _('Selection')),
(TYPE_MULTISELECT, _('Multiple selection')),
(TYPE_OBJECT, _('Object')),
(TYPE_MULTIOBJECT, _('Multiple objects')),
Choice(TYPE_TEXT, _('Text'), description=_('A single line of text')),
Choice(TYPE_LONGTEXT, _('Text (long)'), description=_('Multi-line text with Markdown support')),
Choice(TYPE_INTEGER, _('Integer'), description=_('A whole number (positive or negative)')),
Choice(TYPE_DECIMAL, _('Decimal'), description=_('A fixed-precision decimal number')),
Choice(TYPE_BOOLEAN, _('Boolean'), description=_('A true or false value')),
Choice(TYPE_DATE, _('Date'), description=_('A calendar date')),
Choice(TYPE_DATETIME, _('Date & time'), description=_('A calendar date and time')),
Choice(TYPE_URL, _('URL'), description=_('A hyperlink to an external resource')),
Choice(TYPE_JSON, _('JSON'), description=_('Arbitrary data encoded as JSON')),
Choice(TYPE_SELECT, _('Selection'), description=_('A single value chosen from a predefined list')),
Choice(
TYPE_MULTISELECT,
_('Multiple selection'),
description=_('One or more values chosen from a predefined list')
),
Choice(TYPE_OBJECT, _('Object'), description=_('A reference to a single NetBox object')),
Choice(TYPE_MULTIOBJECT, _('Multiple objects'), description=_('References to one or more NetBox objects')),
)
@ -50,9 +54,9 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
FILTER_EXACT = 'exact'
CHOICES = (
(FILTER_DISABLED, _('Disabled')),
(FILTER_LOOSE, _('Loose')),
(FILTER_EXACT, _('Exact')),
Choice(FILTER_DISABLED, _('Disabled'), description=_('The field cannot be used for filtering')),
Choice(FILTER_LOOSE, _('Loose'), description=_('Match on a partial value (case-insensitive substring)')),
Choice(FILTER_EXACT, _('Exact'), description=_('Match on the exact value')),
)
@ -63,9 +67,9 @@ class CustomFieldUIVisibleChoices(ChoiceSet):
HIDDEN = 'hidden'
CHOICES = (
(ALWAYS, _('Always'), 'green'),
(IF_SET, _('If set'), 'yellow'),
(HIDDEN, _('Hidden'), 'gray'),
Choice(ALWAYS, _('Always'), color='green', description=_('Always display the field')),
Choice(IF_SET, _('If set'), color='yellow', description=_('Display the field only if it has a value')),
Choice(HIDDEN, _('Hidden'), color='gray', description=_('Never display the field')),
)
@ -76,9 +80,9 @@ class CustomFieldUIEditableChoices(ChoiceSet):
HIDDEN = 'hidden'
CHOICES = (
(YES, _('Yes'), 'green'),
(NO, _('No'), 'red'),
(HIDDEN, _('Hidden'), 'gray'),
Choice(YES, _('Yes'), color='green', description=_('The field value can be edited by users')),
Choice(NO, _('No'), color='red', description=_('The field is displayed but cannot be edited')),
Choice(HIDDEN, _('Hidden'), color='gray', description=_('The field is neither displayed nor editable')),
)
@ -89,9 +93,9 @@ class CustomFieldChoiceSetBaseChoices(ChoiceSet):
UN_LOCODE = 'UN_LOCODE'
CHOICES = (
(IATA, 'IATA (Airport codes)'),
(ISO_3166, 'ISO 3166 (Country codes)'),
(UN_LOCODE, 'UN/LOCODE (Location codes)'),
Choice(IATA, 'IATA (Airport codes)'),
Choice(ISO_3166, 'ISO 3166 (Country codes)'),
Choice(UN_LOCODE, 'UN/LOCODE (Location codes)'),
)
@ -112,19 +116,19 @@ class CustomFieldChoiceColorChoices(ChoiceSet):
WHITE = 'white'
CHOICES = (
(BLUE, _('Blue'), BLUE),
(INDIGO, _('Indigo'), INDIGO),
(PURPLE, _('Purple'), PURPLE),
(PINK, _('Pink'), PINK),
(RED, _('Red'), RED),
(ORANGE, _('Orange'), ORANGE),
(YELLOW, _('Yellow'), YELLOW),
(GREEN, _('Green'), GREEN),
(TEAL, _('Teal'), TEAL),
(CYAN, _('Cyan'), CYAN),
(GRAY, _('Gray'), GRAY),
(BLACK, _('Black'), BLACK),
(WHITE, _('White'), WHITE),
Choice(BLUE, _('Blue'), color=BLUE),
Choice(INDIGO, _('Indigo'), color=INDIGO),
Choice(PURPLE, _('Purple'), color=PURPLE),
Choice(PINK, _('Pink'), color=PINK),
Choice(RED, _('Red'), color=RED),
Choice(ORANGE, _('Orange'), color=ORANGE),
Choice(YELLOW, _('Yellow'), color=YELLOW),
Choice(GREEN, _('Green'), color=GREEN),
Choice(TEAL, _('Teal'), color=TEAL),
Choice(CYAN, _('Cyan'), color=CYAN),
Choice(GRAY, _('Gray'), color=GRAY),
Choice(BLACK, _('Black'), color=BLACK),
Choice(WHITE, _('White'), color=WHITE),
)
@ -138,7 +142,7 @@ class CustomLinkButtonClassChoices(ButtonColorChoices):
CHOICES = (
*ButtonColorChoices.CHOICES,
(LINK, _('Link')),
Choice(LINK, _('Link'), description=_('Render the button as a borderless text link')),
)
@ -154,10 +158,10 @@ class BookmarkOrderingChoices(ChoiceSet):
ORDERING_ALPHABETICAL_ZA = '-name'
CHOICES = (
(ORDERING_NEWEST, _('Newest')),
(ORDERING_OLDEST, _('Oldest')),
(ORDERING_ALPHABETICAL_AZ, _('Alphabetical (A-Z)')),
(ORDERING_ALPHABETICAL_ZA, _('Alphabetical (Z-A)')),
Choice(ORDERING_NEWEST, _('Newest')),
Choice(ORDERING_OLDEST, _('Oldest')),
Choice(ORDERING_ALPHABETICAL_AZ, _('Alphabetical (A-Z)')),
Choice(ORDERING_ALPHABETICAL_ZA, _('Alphabetical (Z-A)')),
)
@ -174,10 +178,10 @@ class JournalEntryKindChoices(ChoiceSet):
KIND_DANGER = 'danger'
CHOICES = [
(KIND_INFO, _('Info'), 'cyan'),
(KIND_SUCCESS, _('Success'), 'green'),
(KIND_WARNING, _('Warning'), 'yellow'),
(KIND_DANGER, _('Danger'), 'red'),
Choice(KIND_INFO, _('Info'), color='cyan', description=_('An informational entry')),
Choice(KIND_SUCCESS, _('Success'), color='green', description=_('A record of a successful outcome')),
Choice(KIND_WARNING, _('Warning'), color='yellow', description=_('A cautionary note requiring attention')),
Choice(KIND_DANGER, _('Danger'), color='red', description=_('A record of a critical issue or failure')),
]
@ -194,11 +198,11 @@ class LogLevelChoices(ChoiceSet):
LOG_FAILURE = 'failure'
CHOICES = (
(LOG_DEBUG, _('Debug'), 'teal'),
(LOG_INFO, _('Info'), 'cyan'),
(LOG_SUCCESS, _('Success'), 'green'),
(LOG_WARNING, _('Warning'), 'yellow'),
(LOG_FAILURE, _('Failure'), 'red'),
Choice(LOG_DEBUG, _('Debug'), color='teal'),
Choice(LOG_INFO, _('Info'), color='cyan'),
Choice(LOG_SUCCESS, _('Success'), color='green'),
Choice(LOG_WARNING, _('Warning'), color='yellow'),
Choice(LOG_FAILURE, _('Failure'), color='red'),
)
@ -224,11 +228,11 @@ class WebhookHttpMethodChoices(ChoiceSet):
METHOD_DELETE = 'DELETE'
CHOICES = (
(METHOD_GET, 'GET'),
(METHOD_POST, 'POST'),
(METHOD_PUT, 'PUT'),
(METHOD_PATCH, 'PATCH'),
(METHOD_DELETE, 'DELETE'),
Choice(METHOD_GET, 'GET'),
Choice(METHOD_POST, 'POST'),
Choice(METHOD_PUT, 'PUT'),
Choice(METHOD_PATCH, 'PATCH'),
Choice(METHOD_DELETE, 'DELETE'),
)
@ -252,19 +256,19 @@ class DashboardWidgetColorChoices(ChoiceSet):
WHITE = 'white'
CHOICES = (
(BLUE, _('Blue')),
(INDIGO, _('Indigo')),
(PURPLE, _('Purple')),
(PINK, _('Pink')),
(RED, _('Red')),
(ORANGE, _('Orange')),
(YELLOW, _('Yellow')),
(GREEN, _('Green')),
(TEAL, _('Teal')),
(CYAN, _('Cyan')),
(GRAY, _('Gray')),
(BLACK, _('Black')),
(WHITE, _('White')),
Choice(BLUE, _('Blue')),
Choice(INDIGO, _('Indigo')),
Choice(PURPLE, _('Purple')),
Choice(PINK, _('Pink')),
Choice(RED, _('Red')),
Choice(ORANGE, _('Orange')),
Choice(YELLOW, _('Yellow')),
Choice(GREEN, _('Green')),
Choice(TEAL, _('Teal')),
Choice(CYAN, _('Cyan')),
Choice(GRAY, _('Gray')),
Choice(BLACK, _('Black')),
Choice(WHITE, _('White')),
)
@ -279,7 +283,11 @@ class EventRuleActionChoices(ChoiceSet):
NOTIFICATION = 'notification'
CHOICES = (
(WEBHOOK, _('Webhook')),
(SCRIPT, _('Script')),
(NOTIFICATION, _('Notification')),
Choice(WEBHOOK, _('Webhook'), description=_('Send an outgoing HTTP request to a remote endpoint')),
Choice(SCRIPT, _('Script'), description=_('Execute a custom script')),
Choice(
NOTIFICATION,
_('Notification'),
description=_('Generate a notification for one or more users or groups')
),
)

View File

@ -16,6 +16,7 @@ from django.utils.translation import gettext as _
from core.models import ObjectType
from extras.choices import BookmarkOrderingChoices
from utilities.choices import Choice
from utilities.object_types import object_type_identifier, object_type_name
from utilities.permissions import get_permission_for_model
from utilities.proxy import resolve_proxies
@ -40,7 +41,7 @@ logger = logging.getLogger('netbox.data_backends')
def get_object_type_choices():
return [
(object_type_identifier(ot), object_type_name(ot))
Choice(object_type_identifier(ot), object_type_name(ot))
for ot in ObjectType.objects.public().order_by('app_label', 'model')
]
@ -68,7 +69,7 @@ def object_list_widget_supports_model(model: Model) -> bool:
def get_bookmarks_object_type_choices():
return [
(object_type_identifier(ot), object_type_name(ot))
Choice(object_type_identifier(ot), object_type_name(ot))
for ot in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
]

View File

@ -7,7 +7,7 @@ from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelBulkEditForm, PrimaryModelBulkEditForm
from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
from utilities.forms import BulkEditForm, add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, JSONField
from utilities.forms.fields import ChoiceField, ColorField, CommentField, DynamicModelChoiceField, JSONField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
@ -61,12 +61,12 @@ class CustomFieldBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(),
required=False
)
ui_visible = forms.ChoiceField(
ui_visible = ChoiceField(
label=_("UI visible"),
choices=add_blank_choice(CustomFieldUIVisibleChoices),
required=False
)
ui_editable = forms.ChoiceField(
ui_editable = ChoiceField(
label=_("UI editable"),
choices=add_blank_choice(CustomFieldUIEditableChoices),
required=False
@ -110,7 +110,7 @@ class CustomFieldChoiceSetBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEd
queryset=CustomFieldChoiceSet.objects.all(),
widget=forms.MultipleHiddenInput
)
base_choices = forms.ChoiceField(
base_choices = ChoiceField(
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
required=False
)
@ -144,7 +144,7 @@ class CustomLinkBulkEditForm(ChangelogMessageMixin, OwnerMixin, BulkEditForm):
label=_('Weight'),
required=False
)
button_class = forms.ChoiceField(
button_class = ChoiceField(
label=_('Button class'),
choices=add_blank_choice(CustomLinkButtonClassChoices),
required=False
@ -252,7 +252,7 @@ class WebhookBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
max_length=200,
required=False
)
http_method = forms.ChoiceField(
http_method = ChoiceField(
choices=add_blank_choice(WebhookHttpMethodChoices),
required=False,
label=_('HTTP method')
@ -429,7 +429,7 @@ class JournalEntryBulkEditForm(ChangelogMessageMixin, BulkEditForm):
queryset=JournalEntry.objects.all(),
widget=forms.MultipleHiddenInput
)
kind = forms.ChoiceField(
kind = ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices),
required=False

View File

@ -17,15 +17,18 @@ from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.forms.mixins import ChangelogMessageMixin, OwnerMixin
from tenancy.models import Tenant, TenantGroup
from users.models import Group, User
from utilities.forms import get_field_value
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ChoiceField,
CommentField,
ContentTypeChoiceField,
ContentTypeMultipleChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
JSONField,
MultipleChoiceField,
SlugField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, ObjectAttribute
from utilities.forms.widgets import ChoicesWidget, HTMXSelect
@ -54,6 +57,33 @@ __all__ = (
class CustomFieldForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
type = ChoiceField(
label=_('Type'),
choices=CustomFieldTypeChoices,
initial=CustomFieldTypeChoices.TYPE_TEXT,
help_text=_(
'The type of data stored in this field. For object/multi-object fields, select the related object '
'type below.'
),
)
filter_logic = ChoiceField(
label=_('Filter logic'),
choices=CustomFieldFilterLogicChoices,
initial=CustomFieldFilterLogicChoices.FILTER_LOOSE,
help_text=_('Loose matches any instance of a given string; exact matches the entire field.'),
)
ui_visible = ChoiceField(
label=_('UI visible'),
choices=CustomFieldUIVisibleChoices,
initial=CustomFieldUIVisibleChoices.ALWAYS,
help_text=_('Specifies whether the custom field is displayed in the UI'),
)
ui_editable = ChoiceField(
label=_('UI editable'),
choices=CustomFieldUIEditableChoices,
initial=CustomFieldUIEditableChoices.YES,
help_text=_('Specifies whether the custom field value can be edited in the UI'),
)
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_fields'),
@ -189,6 +219,12 @@ class CustomFieldForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
base_choices = TypedChoiceField(
label=_('Base choices'),
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
required=False,
help_text=_('Base set of predefined choices (optional)'),
)
# TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model
extra_choices = forms.CharField(
widget=ChoicesWidget(),
@ -303,6 +339,12 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelFor
class CustomLinkForm(ChangelogMessageMixin, OwnerMixin, forms.ModelForm):
button_class = ChoiceField(
label=_('Button class'),
choices=CustomLinkButtonClassChoices,
initial=CustomLinkButtonClassChoices.DEFAULT,
help_text=_('The class of the first link in a group will be used for the dropdown button'),
)
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ObjectType.objects.with_feature('custom_links')
@ -531,6 +573,11 @@ class SubscriptionForm(forms.ModelForm):
class WebhookForm(OwnerMixin, NetBoxModelForm):
http_method = ChoiceField(
label=_('HTTP method'),
choices=WebhookHttpMethodChoices,
initial=WebhookHttpMethodChoices.METHOD_POST,
)
fieldsets = (
FieldSet('name', 'description', 'tags', name=_('Webhook')),
@ -551,15 +598,20 @@ class WebhookForm(OwnerMixin, NetBoxModelForm):
class EventRuleForm(OwnerMixin, NetBoxModelForm):
action_type = ChoiceField(
label=_('Action type'),
choices=EventRuleActionChoices,
initial=EventRuleActionChoices.WEBHOOK,
)
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ObjectType.objects.with_feature('event_rules'),
)
event_types = forms.MultipleChoiceField(
event_types = MultipleChoiceField(
choices=get_event_type_choices(),
label=_('Event types')
)
action_choice = forms.ChoiceField(
action_choice = ChoiceField(
label=_('Action choice'),
choices=[]
)
@ -893,7 +945,7 @@ class ImageAttachmentForm(forms.ModelForm):
class JournalEntryForm(NetBoxModelForm):
kind = forms.ChoiceField(
kind = ChoiceField(
label=_('Kind'),
choices=JournalEntryKindChoices
)

View File

@ -12,6 +12,7 @@ from netbox.models import ChangeLoggedModel
from netbox.models.features import has_feature
from netbox.registry import registry
from users.models import User
from utilities.choices import Choice
from utilities.querysets import RestrictedQuerySet
__all__ = (
@ -26,7 +27,7 @@ def get_event_type_choices():
Compile a list of choices from all registered event types
"""
return [
(name, event.text)
Choice(name, event.text)
for name, event in registry['event_types'].items()
]

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
class IPAddressFamilyChoices(ChoiceSet):
@ -9,8 +9,8 @@ class IPAddressFamilyChoices(ChoiceSet):
FAMILY_6 = 6
CHOICES = (
(FAMILY_4, 'IPv4'),
(FAMILY_6, 'IPv6'),
Choice(FAMILY_4, 'IPv4'),
Choice(FAMILY_6, 'IPv6'),
)
@ -27,10 +27,10 @@ class PrefixStatusChoices(ChoiceSet):
STATUS_DEPRECATED = 'deprecated'
CHOICES = [
(STATUS_CONTAINER, _('Container'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_RESERVED, _('Reserved'), 'cyan'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
Choice(STATUS_CONTAINER, _('Container'), color='gray', description=_('Organizes a set of child prefixes')),
Choice(STATUS_ACTIVE, _('Active'), color='blue', description=_('Provisioned and in use')),
Choice(STATUS_RESERVED, _('Reserved'), color='cyan', description=_('Designated for future use')),
Choice(STATUS_DEPRECATED, _('Deprecated'), color='red', description=_('No longer in use')),
]
@ -46,9 +46,9 @@ class IPRangeStatusChoices(ChoiceSet):
STATUS_DEPRECATED = 'deprecated'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_RESERVED, _('Reserved'), 'cyan'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
Choice(STATUS_ACTIVE, _('Active'), color='blue', description=_('Provisioned and in use')),
Choice(STATUS_RESERVED, _('Reserved'), color='cyan', description=_('Designated for future use')),
Choice(STATUS_DEPRECATED, _('Deprecated'), color='red', description=_('No longer in use')),
]
@ -66,11 +66,16 @@ class IPAddressStatusChoices(ChoiceSet):
STATUS_SLAAC = 'slaac'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_RESERVED, _('Reserved'), 'cyan'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
(STATUS_DHCP, _('DHCP'), 'green'),
(STATUS_SLAAC, _('SLAAC'), 'purple'),
Choice(STATUS_ACTIVE, _('Active'), color='blue', description=_('Provisioned and in use')),
Choice(STATUS_RESERVED, _('Reserved'), color='cyan', description=_('Designated for future use')),
Choice(STATUS_DEPRECATED, _('Deprecated'), color='red', description=_('No longer in use')),
Choice(STATUS_DHCP, _('DHCP'), color='green', description=_('Assigned dynamically via DHCP')),
Choice(
STATUS_SLAAC,
_('SLAAC'),
color='purple',
description=_('Assigned via IPv6 stateless address autoconfiguration')
),
]
@ -86,14 +91,14 @@ class IPAddressRoleChoices(ChoiceSet):
ROLE_CARP = 'carp'
CHOICES = (
(ROLE_LOOPBACK, _('Loopback'), 'gray'),
(ROLE_SECONDARY, _('Secondary'), 'blue'),
(ROLE_ANYCAST, _('Anycast'), 'yellow'),
(ROLE_VIP, 'VIP', 'purple'),
(ROLE_VRRP, 'VRRP', 'green'),
(ROLE_HSRP, 'HSRP', 'green'),
(ROLE_GLBP, 'GLBP', 'green'),
(ROLE_CARP, 'CARP', 'green'),
Choice(ROLE_LOOPBACK, _('Loopback'), color='gray', description=_('A loopback interface address')),
Choice(ROLE_SECONDARY, _('Secondary'), color='blue', description=_('A secondary address on an interface')),
Choice(ROLE_ANYCAST, _('Anycast'), color='yellow', description=_('An address shared among multiple nodes')),
Choice(ROLE_VIP, 'VIP', color='purple', description=_('A virtual IP address')),
Choice(ROLE_VRRP, 'VRRP', color='green', description=_('A virtual address managed by VRRP')),
Choice(ROLE_HSRP, 'HSRP', color='green', description=_('A virtual address managed by HSRP')),
Choice(ROLE_GLBP, 'GLBP', color='green', description=_('A virtual address managed by GLBP')),
Choice(ROLE_CARP, 'CARP', color='green', description=_('A virtual address managed by CARP')),
)
@ -113,18 +118,18 @@ class FHRPGroupProtocolChoices(ChoiceSet):
CHOICES = (
(_('Standard'), (
(PROTOCOL_VRRP2, 'VRRPv2'),
(PROTOCOL_VRRP3, 'VRRPv3'),
(PROTOCOL_CARP, 'CARP'),
Choice(PROTOCOL_VRRP2, 'VRRPv2', description=_('Virtual Router Redundancy Protocol version 2')),
Choice(PROTOCOL_VRRP3, 'VRRPv3', description=_('Virtual Router Redundancy Protocol version 3')),
Choice(PROTOCOL_CARP, 'CARP', description=_('Common Address Redundancy Protocol')),
)),
(_('CheckPoint'), (
(PROTOCOL_CLUSTERXL, 'ClusterXL'),
Choice(PROTOCOL_CLUSTERXL, 'ClusterXL', description=_('Check Point ClusterXL high-availability protocol')),
)),
(_('Cisco'), (
(PROTOCOL_HSRP, 'HSRP'),
(PROTOCOL_GLBP, 'GLBP'),
Choice(PROTOCOL_HSRP, 'HSRP', description=_('Hot Standby Router Protocol')),
Choice(PROTOCOL_GLBP, 'GLBP', description=_('Gateway Load Balancing Protocol')),
)),
(PROTOCOL_OTHER, 'Other'),
Choice(PROTOCOL_OTHER, 'Other'),
)
@ -134,8 +139,8 @@ class FHRPGroupAuthTypeChoices(ChoiceSet):
AUTHENTICATION_MD5 = 'md5'
CHOICES = (
(AUTHENTICATION_PLAINTEXT, _('Plaintext')),
(AUTHENTICATION_MD5, 'MD5'),
Choice(AUTHENTICATION_PLAINTEXT, _('Plaintext'), description=_('Authentication using a cleartext password')),
Choice(AUTHENTICATION_MD5, 'MD5', description=_('Authentication using an MD5 hash')),
)
@ -151,9 +156,9 @@ class VLANStatusChoices(ChoiceSet):
STATUS_DEPRECATED = 'deprecated'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_RESERVED, _('Reserved'), 'cyan'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
Choice(STATUS_ACTIVE, _('Active'), color='blue', description=_('Provisioned and in use')),
Choice(STATUS_RESERVED, _('Reserved'), color='cyan', description=_('Designated for future use')),
Choice(STATUS_DEPRECATED, _('Deprecated'), color='red', description=_('No longer in use')),
]
@ -163,8 +168,8 @@ class VLANQinQRoleChoices(ChoiceSet):
ROLE_CUSTOMER = 'cvlan'
CHOICES = [
(ROLE_SERVICE, _('Service'), 'blue'),
(ROLE_CUSTOMER, _('Customer'), 'orange'),
Choice(ROLE_SERVICE, _('Service'), color='blue', description=_('An outer service VLAN (S-VLAN)')),
Choice(ROLE_CUSTOMER, _('Customer'), color='orange', description=_('An inner customer VLAN (C-VLAN)')),
]
@ -179,7 +184,7 @@ class ServiceProtocolChoices(ChoiceSet):
PROTOCOL_SCTP = 'sctp'
CHOICES = (
(PROTOCOL_TCP, 'TCP'),
(PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
Choice(PROTOCOL_TCP, 'TCP'),
Choice(PROTOCOL_UDP, 'UDP'),
Choice(PROTOCOL_SCTP, 'SCTP'),
)

View File

@ -12,6 +12,7 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
from tenancy.models import Tenant
from utilities.forms import GenericObjectFormMixin, add_blank_choice
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
GenericObjectChoiceField,
@ -203,7 +204,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(PrefixStatusChoices),
required=False
@ -247,7 +248,7 @@ class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(IPRangeStatusChoices),
required=False
@ -294,12 +295,12 @@ class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(IPAddressStatusChoices),
required=False
)
role = forms.ChoiceField(
role = ChoiceField(
label=_('Role'),
choices=add_blank_choice(IPAddressRoleChoices),
required=False
@ -321,7 +322,7 @@ class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
class FHRPGroupBulkEditForm(PrimaryModelBulkEditForm):
protocol = forms.ChoiceField(
protocol = ChoiceField(
label=_('Protocol'),
choices=add_blank_choice(FHRPGroupProtocolChoices),
required=False
@ -331,7 +332,7 @@ class FHRPGroupBulkEditForm(PrimaryModelBulkEditForm):
required=False,
label=_('Group ID')
)
auth_type = forms.ChoiceField(
auth_type = ChoiceField(
choices=add_blank_choice(FHRPGroupAuthTypeChoices),
required=False,
label=_('Authentication type')
@ -415,7 +416,7 @@ class VLANBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(VLANStatusChoices),
required=False
@ -425,7 +426,7 @@ class VLANBulkEditForm(PrimaryModelBulkEditForm):
queryset=Role.objects.all(),
required=False
)
qinq_role = forms.ChoiceField(
qinq_role = ChoiceField(
label=_('Q-in-Q role'),
choices=add_blank_choice(VLANQinQRoleChoices),
required=False
@ -475,7 +476,7 @@ class VLANTranslationRuleBulkEditForm(NetBoxModelBulkEditForm):
class ServiceTemplateBulkEditForm(PrimaryModelBulkEditForm):
protocol = forms.ChoiceField(
protocol = ChoiceField(
label=_('Protocol'),
choices=add_blank_choice(ServiceProtocolChoices),
required=False

View File

@ -15,11 +15,13 @@ from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation
from utilities.forms import GenericObjectFormMixin, add_blank_choice
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
GenericObjectChoiceField,
NumericArrayField,
NumericRangeArrayField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
from utilities.forms.widgets import DatePicker
@ -203,6 +205,12 @@ class RoleForm(OrganizationalModelForm):
class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=PrefixStatusChoices,
initial=PrefixStatusChoices.STATUS_ACTIVE,
help_text=_('Operational status of this prefix'),
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@ -267,6 +275,12 @@ class PrefixBulkAddForm(PrefixForm):
class IPRangeForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=IPRangeStatusChoices,
initial=IPRangeStatusChoices.STATUS_ACTIVE,
help_text=_('Operational status of this range'),
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@ -296,6 +310,18 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
class IPAddressForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=IPAddressStatusChoices,
initial=IPAddressStatusChoices.STATUS_ACTIVE,
help_text=_('The operational status of this IP'),
)
role = TypedChoiceField(
label=_('Role'),
choices=add_blank_choice(IPAddressRoleChoices),
required=False,
help_text=_('The functional role of this IP'),
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@ -483,6 +509,18 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
class IPAddressBulkAddForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=IPAddressStatusChoices,
initial=IPAddressStatusChoices.STATUS_ACTIVE,
help_text=_('The operational status of this IP'),
)
role = TypedChoiceField(
label=_('Role'),
choices=add_blank_choice(IPAddressRoleChoices),
required=False,
help_text=_('The functional role of this IP'),
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@ -515,6 +553,15 @@ class IPAddressAssignForm(forms.Form):
class FHRPGroupForm(PrimaryModelForm):
protocol = ChoiceField(
label=_('Protocol'),
choices=FHRPGroupProtocolChoices,
)
auth_type = TypedChoiceField(
label=_('Authentication type'),
choices=add_blank_choice(FHRPGroupAuthTypeChoices),
required=False,
)
# Optionally create a new IPAddress along with the FHRPGroup
ip_vrf = DynamicModelChoiceField(
@ -526,7 +573,7 @@ class FHRPGroupForm(PrimaryModelForm):
required=False,
label=_('Address')
)
ip_status = forms.ChoiceField(
ip_status = ChoiceField(
choices=add_blank_choice(IPAddressStatusChoices),
required=False,
label=_('Status')
@ -646,6 +693,18 @@ class VLANGroupForm(GenericObjectFormMixin, TenancyForm, OrganizationalModelForm
class VLANForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=VLANStatusChoices,
initial=VLANStatusChoices.STATUS_ACTIVE,
help_text=_('Operational status of this VLAN'),
)
qinq_role = TypedChoiceField(
label=_('Q-in-Q role'),
choices=add_blank_choice(VLANQinQRoleChoices),
required=False,
help_text=_('Customer/service VLAN designation (for Q-in-Q/IEEE 802.1ad)'),
)
group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
@ -742,6 +801,10 @@ class VLANTranslationRuleForm(NetBoxModelForm):
class ServiceTemplateForm(PrimaryModelForm):
protocol = ChoiceField(
label=_('Protocol'),
choices=ServiceProtocolChoices,
)
ports = NumericArrayField(
label=_('Ports'),
base_field=forms.IntegerField(
@ -761,6 +824,10 @@ class ServiceTemplateForm(PrimaryModelForm):
class ServiceForm(GenericObjectFormMixin, PrimaryModelForm):
protocol = ChoiceField(
label=_('Protocol'),
choices=ServiceProtocolChoices,
)
parent = GenericObjectChoiceField(
label=_('Parent'),
content_type_queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS),

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
from utilities.constants import CSV_DELIMITERS
__all__ = (
@ -48,33 +48,33 @@ class ColorChoices(ChoiceSet):
COLOR_WHITE = 'ffffff'
CHOICES = (
(COLOR_DARK_RED, _('Dark Red')),
(COLOR_RED, _('Red')),
(COLOR_PINK, _('Pink')),
(COLOR_ROSE, _('Rose')),
(COLOR_FUCHSIA, _('Fuchsia')),
(COLOR_PURPLE, _('Purple')),
(COLOR_DARK_PURPLE, _('Dark Purple')),
(COLOR_INDIGO, _('Indigo')),
(COLOR_BLUE, _('Blue')),
(COLOR_LIGHT_BLUE, _('Light Blue')),
(COLOR_CYAN, _('Cyan')),
(COLOR_TEAL, _('Teal')),
(COLOR_AQUA, _('Aqua')),
(COLOR_DARK_GREEN, _('Dark Green')),
(COLOR_GREEN, _('Green')),
(COLOR_LIGHT_GREEN, _('Light Green')),
(COLOR_LIME, _('Lime')),
(COLOR_YELLOW, _('Yellow')),
(COLOR_AMBER, _('Amber')),
(COLOR_ORANGE, _('Orange')),
(COLOR_DARK_ORANGE, _('Dark Orange')),
(COLOR_BROWN, _('Brown')),
(COLOR_LIGHT_GREY, _('Light Grey')),
(COLOR_GREY, _('Grey')),
(COLOR_DARK_GREY, _('Dark Grey')),
(COLOR_BLACK, _('Black')),
(COLOR_WHITE, _('White')),
Choice(COLOR_DARK_RED, _('Dark Red')),
Choice(COLOR_RED, _('Red')),
Choice(COLOR_PINK, _('Pink')),
Choice(COLOR_ROSE, _('Rose')),
Choice(COLOR_FUCHSIA, _('Fuchsia')),
Choice(COLOR_PURPLE, _('Purple')),
Choice(COLOR_DARK_PURPLE, _('Dark Purple')),
Choice(COLOR_INDIGO, _('Indigo')),
Choice(COLOR_BLUE, _('Blue')),
Choice(COLOR_LIGHT_BLUE, _('Light Blue')),
Choice(COLOR_CYAN, _('Cyan')),
Choice(COLOR_TEAL, _('Teal')),
Choice(COLOR_AQUA, _('Aqua')),
Choice(COLOR_DARK_GREEN, _('Dark Green')),
Choice(COLOR_GREEN, _('Green')),
Choice(COLOR_LIGHT_GREEN, _('Light Green')),
Choice(COLOR_LIME, _('Lime')),
Choice(COLOR_YELLOW, _('Yellow')),
Choice(COLOR_AMBER, _('Amber')),
Choice(COLOR_ORANGE, _('Orange')),
Choice(COLOR_DARK_ORANGE, _('Dark Orange')),
Choice(COLOR_BROWN, _('Brown')),
Choice(COLOR_LIGHT_GREY, _('Light Grey')),
Choice(COLOR_GREY, _('Grey')),
Choice(COLOR_DARK_GREY, _('Dark Grey')),
Choice(COLOR_BLACK, _('Black')),
Choice(COLOR_WHITE, _('White')),
)
@ -100,20 +100,20 @@ class ButtonColorChoices(ChoiceSet):
WHITE = 'white'
CHOICES = (
(DEFAULT, _('Default')),
(BLUE, _('Blue')),
(INDIGO, _('Indigo')),
(PURPLE, _('Purple')),
(PINK, _('Pink')),
(RED, _('Red')),
(ORANGE, _('Orange')),
(YELLOW, _('Yellow')),
(GREEN, _('Green')),
(TEAL, _('Teal')),
(CYAN, _('Cyan')),
(GRAY, _('Gray')),
(BLACK, _('Black')),
(WHITE, _('White')),
Choice(DEFAULT, _('Default')),
Choice(BLUE, _('Blue')),
Choice(INDIGO, _('Indigo')),
Choice(PURPLE, _('Purple')),
Choice(PINK, _('Pink')),
Choice(RED, _('Red')),
Choice(ORANGE, _('Orange')),
Choice(YELLOW, _('Yellow')),
Choice(GREEN, _('Green')),
Choice(TEAL, _('Teal')),
Choice(CYAN, _('Cyan')),
Choice(GRAY, _('Gray')),
Choice(BLACK, _('Black')),
Choice(WHITE, _('White')),
)
@ -127,9 +127,9 @@ class ImportMethodChoices(ChoiceSet):
DATA_FILE = 'datafile'
CHOICES = [
(DIRECT, _('Direct')),
(UPLOAD, _('Upload')),
(DATA_FILE, _('Data file')),
Choice(DIRECT, _('Direct'), description=_('Enter data directly into a form field')),
Choice(UPLOAD, _('Upload'), description=_('Upload a file from the local filesystem')),
Choice(DATA_FILE, _('Data file'), description=_('Reference a file from a synced data source')),
]
@ -140,10 +140,10 @@ class ImportFormatChoices(ChoiceSet):
YAML = 'yaml'
CHOICES = [
(AUTO, _('Auto-detect')),
(CSV, 'CSV'),
(JSON, 'JSON'),
(YAML, 'YAML'),
Choice(AUTO, _('Auto-detect')),
Choice(CSV, 'CSV'),
Choice(JSON, 'JSON'),
Choice(YAML, 'YAML'),
]
@ -155,11 +155,11 @@ class CSVDelimiterChoices(ChoiceSet):
TAB = CSV_DELIMITERS['tab']
CHOICES = [
(AUTO, _('Auto-detect')),
(COMMA, _('Comma')),
(SEMICOLON, _('Semicolon')),
(PIPE, _('Pipe')),
(TAB, _('Tab')),
Choice(AUTO, _('Auto-detect')),
Choice(COMMA, _('Comma')),
Choice(SEMICOLON, _('Semicolon')),
Choice(PIPE, _('Pipe')),
Choice(TAB, _('Tab')),
]
@ -174,10 +174,10 @@ class DistanceUnitChoices(ChoiceSet):
UNIT_FOOT = 'ft'
CHOICES = (
(UNIT_KILOMETER, _('Kilometers')),
(UNIT_METER, _('Meters')),
(UNIT_MILE, _('Miles')),
(UNIT_FOOT, _('Feet')),
Choice(UNIT_KILOMETER, _('Kilometers')),
Choice(UNIT_METER, _('Meters')),
Choice(UNIT_MILE, _('Miles')),
Choice(UNIT_FOOT, _('Feet')),
)
@ -192,8 +192,8 @@ class WeightUnitChoices(ChoiceSet):
UNIT_OUNCE = 'oz'
CHOICES = (
(UNIT_KILOGRAM, _('Kilograms')),
(UNIT_GRAM, _('Grams')),
(UNIT_POUND, _('Pounds')),
(UNIT_OUNCE, _('Ounces')),
Choice(UNIT_KILOGRAM, _('Kilograms')),
Choice(UNIT_GRAM, _('Grams')),
Choice(UNIT_POUND, _('Pounds')),
Choice(UNIT_OUNCE, _('Ounces')),
)

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass
from netbox.registry import registry
from utilities.choices import Choice
EVENT_TYPE_KIND_INFO = 'info'
EVENT_TYPE_KIND_SUCCESS = 'success'
@ -31,7 +32,7 @@ def get_event_text(name):
def get_event_type_choices():
return [
(event.name, event.text) for event in registry['event_types'].values()
Choice(event.name, event.text) for event in registry['event_types'].values()
]

View File

@ -3,13 +3,14 @@ from django.utils.translation import gettext_lazy as _
from netbox.registry import registry
from users.preferences import UserPreference
from utilities.choices import Choice
from utilities.constants import CSV_DELIMITERS
from utilities.paginator import EnhancedPaginator
def get_page_lengths():
return [
(v, str(v)) for v in EnhancedPaginator.default_page_lengths
Choice(v, str(v)) for v in EnhancedPaginator.default_page_lengths
]
@ -19,7 +20,7 @@ def get_csv_delimiters():
label = _(k.title())
if v.strip():
label = f'{label} ({v})'
choices.append((k, label))
choices.append(Choice(k, label))
return choices

View File

@ -1,4 +1,5 @@
from netbox.registry import registry
from utilities.choices import Choice
__all__ = (
'get_data_backend_choices',
@ -12,7 +13,7 @@ def get_data_backend_choices():
return [
(None, '---------'),
*[
(name, cls.label) for name, cls in registry['data_backends'].items()
Choice(name, cls.label) for name, cls in registry['data_backends'].items()
]
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,17 @@ import { NetBoxTomSelect } from './classes/netboxTomSelect';
import { getPlugins } from './config';
import { getElements } from '../util';
// Render a static option, appending a description subtitle when one is present. TomSelect copies an
// <option>'s data-* attributes into the option data, so a `data-description` attribute surfaces here as
// `data.description` without any DOM lookup.
function renderOption(data: TomOption, escape: typeof escape_html) {
let html = `<div>${escape(data.text)}`;
if (data.description) {
html = `${html}<br /><small class="text-secondary">${escape(data.description)}</small>`;
}
return `${html}</div>`;
}
// Initialize <select> elements with statically-defined options
export function initStaticSelects(): void {
for (const select of getElements<HTMLSelectElement>(
@ -12,6 +23,11 @@ export function initStaticSelects(): void {
new NetBoxTomSelect(select, {
...getPlugins(select),
maxOptions: undefined,
render: {
// Only `option` (the dropdown list) renders the description; `item` (the compact selected-value display
// shown inside the input) is intentionally left at the default so the subtitle doesn't clutter it.
option: renderOption,
},
});
}
}

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# Contacts
@ -14,8 +14,8 @@ class ContactPriorityChoices(ChoiceSet):
PRIORITY_INACTIVE = 'inactive'
CHOICES = (
(PRIORITY_PRIMARY, _('Primary')),
(PRIORITY_SECONDARY, _('Secondary')),
(PRIORITY_TERTIARY, _('Tertiary')),
(PRIORITY_INACTIVE, _('Inactive')),
Choice(PRIORITY_PRIMARY, _('Primary')),
Choice(PRIORITY_SECONDARY, _('Secondary')),
Choice(PRIORITY_TERTIARY, _('Tertiary')),
Choice(PRIORITY_INACTIVE, _('Inactive')),
)

View File

@ -8,7 +8,7 @@ from netbox.forms import (
PrimaryModelBulkEditForm,
)
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.fields import ChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from ..choices import ContactPriorityChoices
@ -137,7 +137,7 @@ class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):
queryset=ContactRole.objects.all(),
required=False
)
priority = forms.ChoiceField(
priority = ChoiceField(
label=_('Priority'),
choices=add_blank_choice(ContactPriorityChoices),
required=False

View File

@ -2,7 +2,14 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from netbox.forms import NestedGroupModelForm, NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from tenancy.choices import ContactPriorityChoices
from utilities.forms import add_blank_choice
from utilities.forms.fields import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
SlugField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, ObjectAttribute
from ..models import *
@ -118,6 +125,11 @@ class ContactForm(PrimaryModelForm):
class ContactAssignmentForm(NetBoxModelForm):
priority = TypedChoiceField(
label=_('Priority'),
choices=add_blank_choice(ContactPriorityChoices),
required=False,
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=ContactGroup.objects.all(),

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
__all__ = (
'TokenVersionChoices',
@ -12,6 +12,6 @@ class TokenVersionChoices(ChoiceSet):
V2 = 2
CHOICES = [
(V1, _('v1')),
(V2, _('v2')),
Choice(V1, _('v1')),
Choice(V2, _('v2')),
]

View File

@ -19,6 +19,7 @@ from users.choices import TokenVersionChoices
from users.constants import *
from users.models import *
from users.utils import user_may_grant_token
from utilities.choices import Choice
from utilities.data import flatten_dict
from utilities.forms.fields import (
ContentTypeMultipleChoiceField,
@ -326,7 +327,7 @@ def get_object_types_choices():
model_class = ot.model_class()
model_name = model_class._meta.verbose_name if model_class else ot.model
choices_by_app[app_label].append((ot.pk, title(model_name)))
choices_by_app[app_label].append(Choice(ot.pk, title(model_name)))
return list(choices_by_app.items())

View File

@ -1,18 +1,50 @@
import enum
from typing import Any
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import Promise
from django.utils.translation import gettext_lazy as _
from utilities.data import get_config_value_ci
from utilities.string import enum_key
__all__ = (
'Choice',
'ChoiceSet',
'unpack_grouped_choices',
)
class Choice(tuple):
"""
A single choice within a ChoiceSet. Carries the choice's label, and optionally a color and a description, in a
single object. A Choice **is** a `(value, label)` two-tuple for backward compatibility with the plain-tuple choice
format (so it satisfies `isinstance(choice, tuple)` checks in Django, DRF, etc.); its `color` and `description`
are exposed only as attributes.
"""
value: Any
label: str | Promise
color: str | None
description: str | Promise | None
def __new__(cls, value, label, color=None, description=None):
instance = super().__new__(cls, (value, label))
instance.value = value
instance.label = label
instance.color = color
instance.description = description
return instance
def __getnewargs__(self):
# tuple's default reconstructor would call Choice((value, label)) with a single argument, which fails
# __new__'s (value, label, ...) signature. Supply the real constructor args so copy/deepcopy/pickle work.
return self.value, self.label, self.color, self.description
def __repr__(self):
return f'Choice(value={self.value!r}, label={self.label!r})'
class ChoiceSetMeta(type):
"""
Metaclass for ChoiceSet
@ -36,21 +68,35 @@ class ChoiceSetMeta(type):
if extend_choices is not None:
attrs['CHOICES'].extend(extend_choices)
# Define choice tuples and color maps
# Build the normalized choice list and the derived color map. Each choice may be defined as a Choice object
# (which is preserved as-is so consumers can reference its color/description) or as a plain (value, label) or
# (value, label, color) tuple. The colors map is kept for model-level consumers (e.g. get_FOO_color()).
attrs['_choices'] = []
attrs['colors'] = {}
def register(entry):
# A choice may be given as a dict (e.g. from FIELD_CHOICES config, to avoid importing Choice), a Choice
# object, or a legacy (value, label[, color]) tuple. Dicts and Choices are preserved as Choice objects so
# consumers can reference their color/description; legacy tuples are reduced to (value, label). Any color
# is also recorded on the colors map for model-level consumers.
if isinstance(entry, dict):
entry = Choice(**entry)
if isinstance(entry, Choice):
if entry.color is not None:
attrs['colors'][entry.value] = entry.color
return entry
value, label = entry[0], entry[1]
if len(entry) >= 3:
attrs['colors'][value] = entry[2]
return value, label
for choice in attrs['CHOICES']:
if isinstance(choice[1], (list, tuple)):
grouped_choices = []
for c in choice[1]:
grouped_choices.append((c[0], c[1]))
if len(c) == 3:
attrs['colors'][c[0]] = c[2]
# A grouped choice is a (group_label, [members]) tuple; Choice and dict entries are always flat choices
if not isinstance(choice, (Choice, dict)) and isinstance(choice[1], (list, tuple)):
grouped_choices = [register(c) for c in choice[1]]
attrs['_choices'].append((choice[0], grouped_choices))
else:
attrs['_choices'].append((choice[0], choice[1]))
if len(choice) == 3:
attrs['colors'][choice[0]] = choice[2]
attrs['_choices'].append(register(choice))
return super().__new__(mcs, name, bases, attrs)
@ -64,8 +110,10 @@ class ChoiceSetMeta(type):
class ChoiceSet(metaclass=ChoiceSetMeta):
"""
Holds an iterable of choice tuples suitable for passing to a Django model or form field. Choices can be defined
statically within the class as CHOICES and/or gleaned from the FIELD_CHOICES configuration parameter.
Holds an iterable of choices suitable for passing to a Django model or form field. Choices can be defined
statically within the class as CHOICES and/or gleaned from the FIELD_CHOICES configuration parameter. Each
choice may be defined as a Choice object (to carry a color and/or description) or as a plain (value, label) or
(value, label, color) tuple.
"""
CHOICES = list()

View File

@ -1,4 +1,5 @@
from .array import *
from .choices import *
from .content_types import *
from .csv import *
from .dynamic import *

View File

@ -0,0 +1,100 @@
from django import forms
from utilities.choices import Choice
from utilities.forms import widgets
__all__ = (
'ChoiceField',
'MultipleChoiceField',
'TypedChoiceField',
)
def _map_choice_attr(choices, attr):
"""
Build a {value: attr_value} mapping from the Choice objects among an iterable of choices (descending into
optgroups), for the given Choice attribute (e.g. 'description' or 'color'). Values of None are omitted.
Django flattens choices to plain (value, label) tuples before they reach the widget, so this is collected from
the field's original choices, where the Choice objects are still intact. A ChoiceSet is iterable (yielding its
Choice objects); a plain callable (lazy choices) is not, and yields an empty mapping.
"""
mapping = {}
try:
entries = iter(choices)
except TypeError:
return mapping
for choice in entries:
# Descend into an optgroup's members; a Choice is always a flat choice
members = [choice]
if not isinstance(choice, Choice) and isinstance(choice[1], (list, tuple)):
members = choice[1]
for member in members:
if isinstance(member, Choice) and (value := getattr(member, attr)) is not None:
mapping[member.value] = value
return mapping
def _parent_choices_property(cls):
"""
Return the `choices` property inherited from the first class after AttrChoiceMixin in cls's MRO (i.e. the
Django field's own property). Used to delegate to the parent getter/setter without hardcoding a specific base
class, so a setter override on a future Django subclass (e.g. TypedChoiceField/MultipleChoiceField) isn't
bypassed.
"""
mro = cls.__mro__
for klass in mro[mro.index(AttrChoiceMixin) + 1:]:
if isinstance(prop := klass.__dict__.get('choices'), property):
return prop
raise AttributeError(f"No parent 'choices' property found for {cls.__name__}") # pragma: no cover
class AttrChoiceMixin:
"""
Reads option descriptions from the Choice objects among a field's choices and passes them to a
description-aware Select widget for rendering as option subtitles. Set `show_descriptions=False` to suppress.
"""
def __init__(self, *, choices=(), show_descriptions=True, **kwargs):
self.show_descriptions = show_descriptions
super().__init__(choices=choices, **kwargs)
def _get_choices(self):
return _parent_choices_property(type(self)).fget(self)
def _set_choices(self, value):
# Delegate to the parent setter (updates self._choices and self.widget.choices), then refresh the widget's
# description map from the same choices. Collecting descriptions here (rather than once in __init__) keeps
# them in sync should the field's choices be reassigned after construction. Descriptions are read from the
# raw choices, where the Choice objects are still intact, before Django normalizes them to (value, label).
_parent_choices_property(type(self)).fset(self, value)
if getattr(self, 'show_descriptions', True):
self.widget.descriptions = _map_choice_attr(value, 'description')
choices = property(_get_choices, _set_choices)
class ChoiceField(AttrChoiceMixin, forms.ChoiceField):
"""
Extends Django's ChoiceField to render the description defined on each Choice as an option subtitle.
"""
widget = widgets.Select
class TypedChoiceField(AttrChoiceMixin, forms.TypedChoiceField):
"""
A description-aware ChoiceField for use on nullable model choice fields. Like Django's TypedChoiceField, an empty
selection is coerced to `empty_value` (which defaults to None here) so that a blank submission is stored as NULL
rather than an empty string. This mirrors the form field Django generates automatically for a nullable choice
field, while also rendering the description defined on each Choice as an option subtitle.
"""
widget = widgets.Select
def __init__(self, *, empty_value=None, **kwargs):
super().__init__(empty_value=empty_value, **kwargs)
class MultipleChoiceField(AttrChoiceMixin, forms.MultipleChoiceField):
"""
Extends Django's MultipleChoiceField to render the description defined on each Choice as an option subtitle.
"""
widget = widgets.SelectMultiple

View File

@ -195,9 +195,10 @@ def get_selected_values(form, field_name):
def add_blank_choice(choices):
"""
Add a blank choice to the beginning of a choices list.
Add a blank choice to the beginning of a choices list. Any Choice objects are preserved (rather than reduced to
plain tuples) so that description-aware fields can still reference their descriptions.
"""
return ((None, '---------'),) + tuple(choices)
return ((None, '---------'), *choices)
def form_from_model(model, fields):

View File

@ -9,11 +9,44 @@ __all__ = (
'ClearableSelect',
'ColorSelect',
'HTMXSelect',
'Select',
'SelectMultiple',
'SelectWithPK',
'SplitMultiSelectWidget',
)
class AttrSelectMixin:
"""
Annotates each rendered <option> with a `data-description` attribute, which is displayed as subtitle text
beneath the option's label. Descriptions are sourced from an explicit value-to-description mapping set on
`descriptions`.
"""
def __init__(self, *args, descriptions=None, **kwargs):
super().__init__(*args, **kwargs)
self.descriptions = descriptions or {}
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
option = super().create_option(name, value, label, selected, index, subindex, attrs)
if description := self.descriptions.get(value, ''):
option['attrs']['data-description'] = description
return option
class Select(AttrSelectMixin, forms.Select):
"""
A Select widget which renders an optional description beneath each option's label.
"""
class SelectMultiple(AttrSelectMixin, forms.SelectMultiple):
"""
A SelectMultiple widget which renders an optional description beneath each option's label.
"""
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
"""
A Select widget for NullBooleanFields

View File

@ -1,7 +1,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
class ExampleChoices(ChoiceSet):
@ -27,11 +27,109 @@ class ExampleChoices(ChoiceSet):
)
class ChoiceDataclassTestCase(TestCase):
"""
Validate that a Choice behaves as a (value, label) two-tuple for backward compatibility.
"""
def test_choice_is_tuple_compatible(self):
choice = Choice('a', 'A', color='red', description='desc')
self.assertEqual(list(choice), ['a', 'A'])
self.assertEqual(choice[0], 'a')
self.assertEqual(choice[1], 'A')
self.assertEqual(len(choice), 2)
def test_choice_unpacking(self):
value, label = Choice('a', 'A', color='red')
self.assertEqual(value, 'a')
self.assertEqual(label, 'A')
def test_choice_attributes(self):
choice = Choice('a', 'A')
self.assertIsNone(choice.color)
self.assertIsNone(choice.description)
def test_choice_is_copyable_and_picklable(self):
"""A Choice must survive copy/deepcopy/pickle with all of its attributes intact."""
import copy
import pickle
choice = Choice('a', 'A', color='red', description='desc')
for clone in (copy.copy(choice), copy.deepcopy(choice), pickle.loads(pickle.dumps(choice))):
self.assertIsInstance(clone, Choice)
self.assertEqual(tuple(clone), ('a', 'A'))
self.assertEqual(clone.color, 'red')
self.assertEqual(clone.description, 'desc')
class ChoiceSetTestCase(TestCase):
def test_values(self):
self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3])
def test_choice_objects_preserved_and_populate_colors(self):
"""A ChoiceSet built from Choice objects preserves them in _choices and derives the colors map."""
class ChoiceObjectChoices(ChoiceSet):
CHOICES = [
Choice('a', 'A', color='red', description='First'),
Choice('b', 'B', color='green'),
Choice('c', 'C'),
]
# Choice objects are preserved (and remain (value, label)-compatible)
entries = list(ChoiceObjectChoices)
self.assertTrue(all(isinstance(c, Choice) for c in entries))
self.assertEqual([tuple(c) for c in entries], [('a', 'A'), ('b', 'B'), ('c', 'C')])
self.assertEqual(entries[0].description, 'First')
self.assertEqual(ChoiceObjectChoices.colors, {'a': 'red', 'b': 'green'})
def test_dict_choices_normalized_to_choice(self):
"""A ChoiceSet may define choices as dicts (e.g. from config), which normalize to Choice objects."""
class DictChoices(ChoiceSet):
CHOICES = [
{'value': 'a', 'label': 'A', 'color': 'red', 'description': 'First'},
{'value': 'b', 'label': 'B'},
]
entries = list(DictChoices)
self.assertTrue(all(isinstance(c, Choice) for c in entries))
self.assertEqual([tuple(c) for c in entries], [('a', 'A'), ('b', 'B')])
self.assertEqual(entries[0].description, 'First')
self.assertEqual(DictChoices.colors, {'a': 'red'})
def test_dict_choice_with_invalid_key_raises(self):
"""A choice dict with an unrecognized key raises a clear error."""
with self.assertRaises(TypeError):
class BadChoices(ChoiceSet):
CHOICES = [{'value': 'a', 'label': 'A', 'colour': 'red'}]
def test_legacy_tuples_populate_colors(self):
"""Plain 2-/3-tuples continue to reduce to (value, label) and build the colors map."""
class LegacyChoices(ChoiceSet):
CHOICES = [
('a', 'A', 'red'),
('b', 'B'),
]
self.assertEqual(list(LegacyChoices), [('a', 'A'), ('b', 'B')])
self.assertEqual(LegacyChoices.colors, {'a': 'red'})
def test_grouped_choices_with_choice_objects(self):
"""Grouped choices normalize whether members are Choice objects or tuples."""
class GroupedChoices(ChoiceSet):
CHOICES = (
('Group 1', (
Choice('a', 'A', color='red', description='First'),
('b', 'B'),
)),
)
group_label, members = list(GroupedChoices)[0]
self.assertEqual(group_label, 'Group 1')
self.assertEqual([tuple(m) for m in members], [('a', 'A'), ('b', 'B')])
self.assertEqual(members[0].description, 'First')
self.assertEqual(GroupedChoices.colors, {'a': 'red'})
self.assertEqual(GroupedChoices.values(), ['a', 'b'])
def test_key_with_non_list_choices_raises(self):
"""A ChoiceSet declaring a key must define CHOICES as a list."""
with self.assertRaises(ImproperlyConfigured):
@ -70,3 +168,29 @@ class FieldChoicesCaseInsensitiveTestCase(TestCase):
CHOICES = [('base', 'Base')]
self.assertEqual(TestStatusChoices.CHOICES, [('base', 'Base'), ('extra', 'Extra')])
def test_config_choices_as_choice_objects(self):
"""FIELD_CHOICES may provide Choice objects to define colors and descriptions."""
config = {'utilities.teststatus': [Choice('new', 'New', color='red', description='A new thing')]}
with override_settings(FIELD_CHOICES=config):
class TestStatusChoices(ChoiceSet):
key = 'TestStatus'
CHOICES = [('old', 'Old')]
entries = list(TestStatusChoices)
self.assertEqual([tuple(c) for c in entries], [('new', 'New')])
self.assertEqual(entries[0].description, 'A new thing')
self.assertEqual(TestStatusChoices.colors, {'new': 'red'})
def test_config_choices_as_dicts(self):
"""FIELD_CHOICES may provide plain dicts, avoiding any import in configuration.py."""
choice = {'value': 'new', 'label': 'New', 'color': 'red', 'description': 'A new thing'}
with override_settings(FIELD_CHOICES={'utilities.teststatus': [choice]}):
class TestStatusChoices(ChoiceSet):
key = 'TestStatus'
CHOICES = [('old', 'Old')]
entries = list(TestStatusChoices)
self.assertEqual([tuple(c) for c in entries], [('new', 'New')])
self.assertEqual(entries[0].description, 'A new thing')
self.assertEqual(TestStatusChoices.colors, {'new': 'red'})

View File

@ -7,7 +7,9 @@ from django.test import TestCase, override_settings
from dcim.models import Site
from netbox.choices import ImportFormatChoices
from utilities.choices import Choice, ChoiceSet
from utilities.forms.bulk_import import BulkImportForm
from utilities.forms.fields import ChoiceField, MultipleChoiceField, TypedChoiceField
from utilities.forms.fields.csv import CSVSelectWidget
from utilities.forms.fields.dynamic import DynamicChoiceField, DynamicMultipleChoiceField
from utilities.forms.fields.generic import GenericObjectChoiceField
@ -15,12 +17,13 @@ from utilities.forms.forms import BulkRenameForm
from utilities.forms.mixins import GenericObjectFormMixin
from utilities.forms.rendering import FieldSet
from utilities.forms.utils import (
add_blank_choice,
expand_alphanumeric_pattern,
expand_ipnetwork_pattern,
get_capacity_unit_label,
get_field_value,
)
from utilities.forms.widgets.select import AvailableOptions, HTMXSelect, SelectedOptions
from utilities.forms.widgets.select import AvailableOptions, HTMXSelect, Select, SelectedOptions
class ExpandIPNetworkTestCase(TestCase):
@ -898,3 +901,110 @@ class HTMXSelectTestCase(TestCase):
def test_hx_target_id_include_stays_on_form_fields(self):
widget = HTMXSelect(hx_target_id='my-fieldset')
self.assertEqual(widget.attrs['hx-include'], '#form_fields')
class DescriptionSelectTestCase(TestCase):
"""
Validate the rendering of option descriptions in static select fields.
"""
class ExampleChoices(ChoiceSet):
FOO = 'foo'
BAR = 'bar'
CHOICES = (
Choice(FOO, 'Foo', description='Description of foo'),
Choice(BAR, 'Bar'),
)
def test_choiceset_descriptions_populate_widget(self):
field = ChoiceField(choices=self.ExampleChoices)
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})
def test_multiplechoicefield_descriptions_populate_widget(self):
field = MultipleChoiceField(choices=self.ExampleChoices)
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})
def test_show_descriptions_false_suppresses_descriptions(self):
field = ChoiceField(choices=self.ExampleChoices, show_descriptions=False)
self.assertEqual(field.widget.descriptions, {})
def test_add_blank_choice_preserves_descriptions(self):
field = ChoiceField(choices=add_blank_choice(self.ExampleChoices))
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})
def test_data_description_rendered_on_option(self):
field = ChoiceField(choices=self.ExampleChoices)
html = field.widget.render('test', None)
self.assertInHTML(
'<option value="foo" data-description="Description of foo">Foo</option>',
html
)
# Options without a description should not receive the attribute
self.assertInHTML('<option value="bar">Bar</option>', html)
def test_choiceset_without_descriptions(self):
class NoDescriptions(ChoiceSet):
CHOICES = (('a', 'A'),)
field = ChoiceField(choices=NoDescriptions)
self.assertEqual(field.widget.descriptions, {})
def test_descriptions_refresh_when_choices_reassigned(self):
"""Reassigning a field's choices after construction should refresh the widget's description map."""
class OtherChoices(ChoiceSet):
CHOICES = (
Choice('baz', 'Baz', description='Description of baz'),
)
field = ChoiceField(choices=self.ExampleChoices)
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})
field.choices = OtherChoices
self.assertEqual(field.widget.descriptions, {'baz': 'Description of baz'})
def test_show_descriptions_false_suppresses_on_reassignment(self):
field = ChoiceField(choices=self.ExampleChoices, show_descriptions=False)
field.choices = self.ExampleChoices
self.assertEqual(field.widget.descriptions, {})
def test_typedchoicefield_populates_descriptions(self):
field = TypedChoiceField(choices=self.ExampleChoices)
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})
def test_typedchoicefield_empty_value_defaults_to_none(self):
"""A blank selection on a TypedChoiceField resolves to None (for storage as NULL) rather than ''."""
field = TypedChoiceField(choices=add_blank_choice(self.ExampleChoices), required=False)
self.assertIsNone(field.empty_value)
self.assertIsNone(field.clean(''))
def test_explicit_descriptions_mapping(self):
widget = Select(choices=[('x', 'X'), ('y', 'Y')], descriptions={'x': 'Description X'})
html = widget.render('test', None)
self.assertInHTML('<option value="x" data-description="Description X">X</option>', html)
self.assertInHTML('<option value="y">Y</option>', html)
def test_choices_setter_delegates_through_mro(self):
"""
AttrChoiceMixin must delegate to the parent field's choices setter via the MRO, not a hardcoded base,
so a setter override on an intermediate class is not bypassed.
"""
from django import forms
from utilities.forms.fields.choices import AttrChoiceMixin
from utilities.forms.widgets import Select as DescriptionSelect
calls = []
class OverridingChoiceField(forms.ChoiceField):
def _set_choices(self, value):
calls.append(value)
forms.ChoiceField.choices.fset(self, value)
choices = property(forms.ChoiceField.choices.fget, _set_choices)
class CustomField(AttrChoiceMixin, OverridingChoiceField):
widget = DescriptionSelect
field = CustomField(choices=self.ExampleChoices)
# The intermediate class's setter must have been invoked (delegation not bypassed)
self.assertTrue(calls)
# And descriptions are still collected as normal
self.assertEqual(field.widget.descriptions, {self.ExampleChoices.FOO: 'Description of foo'})

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# Clusters
@ -17,11 +17,26 @@ class ClusterStatusChoices(ChoiceSet):
STATUS_OFFLINE = 'offline'
CHOICES = [
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGING, _('Staging'), 'blue'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
(STATUS_OFFLINE, _('Offline'), 'red'),
Choice(
STATUS_PLANNED, _('Planned'), color='cyan',
description=_('Designated for future use but not yet in service')
),
Choice(
STATUS_STAGING, _('Staging'), color='blue',
description=_('Being prepared for production use')
),
Choice(
STATUS_ACTIVE, _('Active'), color='green',
description=_('Fully operational and in service')
),
Choice(
STATUS_DECOMMISSIONING, _('Decommissioning'), color='yellow',
description=_('Being removed from service')
),
Choice(
STATUS_OFFLINE, _('Offline'), color='red',
description=_('Not currently in service')
),
]
@ -41,13 +56,34 @@ class VirtualMachineStatusChoices(ChoiceSet):
STATUS_PAUSED = 'paused'
CHOICES = [
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGED, _('Staged'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
(STATUS_PAUSED, _('Paused'), 'orange'),
Choice(
STATUS_OFFLINE, _('Offline'), color='gray',
description=_('Powered off or not currently running')
),
Choice(
STATUS_ACTIVE, _('Active'), color='green',
description=_('Powered on and operational')
),
Choice(
STATUS_PLANNED, _('Planned'), color='cyan',
description=_('Designated for future use but not yet provisioned')
),
Choice(
STATUS_STAGED, _('Staged'), color='blue',
description=_('Provisioned but not yet in service')
),
Choice(
STATUS_FAILED, _('Failed'), color='red',
description=_('In an error state or otherwise not functioning')
),
Choice(
STATUS_DECOMMISSIONING, _('Decommissioning'), color='yellow',
description=_('Being removed from service')
),
Choice(
STATUS_PAUSED, _('Paused'), color='orange',
description=_('Suspended with its state retained in memory')
),
]
@ -59,7 +95,16 @@ class VirtualMachineStartOnBootChoices(ChoiceSet):
STATUS_LAST_STATE = 'laststate'
CHOICES = [
(STATUS_ON, _('On'), 'green'),
(STATUS_OFF, _('Off'), 'gray'),
(STATUS_LAST_STATE, _('Last State'), 'cyan')
Choice(
STATUS_ON, _('On'), color='green',
description=_('Automatically start when the host boots')
),
Choice(
STATUS_OFF, _('Off'), color='gray',
description=_('Do not start automatically when the host boots')
),
Choice(
STATUS_LAST_STATE, _('Last State'), color='cyan',
description=_('Restore the power state the machine had before the host shut down')
),
]

View File

@ -12,7 +12,7 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
from netbox.forms.mixins import OwnerMixin
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.fields import ChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.utils import get_capacity_unit_label
from utilities.forms.widgets import BulkEditNullBooleanSelect
@ -58,7 +58,7 @@ class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
queryset=ClusterGroup.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(ClusterStatusChoices),
required=False,
@ -111,13 +111,13 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
queryset=VirtualMachineType.objects.all(),
required=False
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(VirtualMachineStatusChoices),
required=False,
initial='',
)
start_on_boot = forms.ChoiceField(
start_on_boot = ChoiceField(
label=_('Start on boot'),
choices=add_blank_choice(VirtualMachineStartOnBootChoices),
required=False,
@ -240,7 +240,7 @@ class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
max_length=100,
required=False
)
mode = forms.ChoiceField(
mode = ChoiceField(
label=_('Mode'),
choices=add_blank_choice(InterfaceModeChoices),
required=False

View File

@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices
from dcim.forms.common import InterfaceCommonForm
from dcim.forms.mixins import ScopedForm
from dcim.models import Device, DeviceRole, MACAddress, Platform, Rack, Region, Site, SiteGroup
@ -14,11 +15,23 @@ from ipam.models import VLAN, VRF, IPAddress, VLANGroup, VLANTranslationPolicy
from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from netbox.forms.mixins import OwnerMixin
from tenancy.forms import TenancyForm
from utilities.forms import ConfirmationForm, get_field_value
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField
from utilities.forms import ConfirmationForm, add_blank_choice, get_field_value
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
JSONField,
SlugField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.utils import get_capacity_unit_label
from utilities.forms.widgets import HTMXSelect
from virtualization.choices import (
ClusterStatusChoices,
VirtualMachineStartOnBootChoices,
VirtualMachineStatusChoices,
)
from ..models import *
@ -60,6 +73,11 @@ class ClusterGroupForm(OrganizationalModelForm):
class ClusterForm(TenancyForm, ScopedForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=ClusterStatusChoices,
initial=ClusterStatusChoices.STATUS_ACTIVE,
)
type = DynamicModelChoiceField(
label=_('Type'),
queryset=ClusterType.objects.all(),
@ -194,6 +212,16 @@ class VirtualMachineTypeForm(PrimaryModelForm):
class VirtualMachineForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=VirtualMachineStatusChoices,
initial=VirtualMachineStatusChoices.STATUS_ACTIVE,
)
start_on_boot = ChoiceField(
label=_('Start on boot'),
choices=VirtualMachineStartOnBootChoices,
initial=VirtualMachineStartOnBootChoices.STATUS_OFF,
)
virtual_machine_type = forms.ModelChoiceField(
label=_('Type'),
queryset=VirtualMachineType.objects.all(),
@ -381,6 +409,12 @@ class VMComponentForm(OwnerMixin, NetBoxModelForm):
class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
mode = TypedChoiceField(
label=_('802.1Q Mode'),
choices=add_blank_choice(InterfaceModeChoices),
required=False,
help_text=_('IEEE 802.1Q tagging strategy'),
)
primary_mac_address = DynamicModelChoiceField(
queryset=MACAddress.objects.all(),
label=_('Primary MAC address'),

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
#
# Tunnels
@ -15,9 +15,14 @@ class TunnelStatusChoices(ChoiceSet):
STATUS_DISABLED = 'disabled'
CHOICES = [
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_DISABLED, _('Disabled'), 'red'),
Choice(
STATUS_PLANNED,
_('Planned'),
color='cyan',
description=_('Designated for future use but not yet in service')
),
Choice(STATUS_ACTIVE, _('Active'), color='green', description=_('Established and carrying traffic')),
Choice(STATUS_DISABLED, _('Disabled'), color='red', description=_('Administratively disabled')),
]
@ -32,14 +37,22 @@ class TunnelEncapsulationChoices(ChoiceSet):
ENCAP_WIREGUARD = 'wireguard'
CHOICES = [
(ENCAP_IPSEC_TRANSPORT, _('IPsec - Transport')),
(ENCAP_IPSEC_TUNNEL, _('IPsec - Tunnel')),
(ENCAP_IP_IP, _('IP-in-IP')),
(ENCAP_GRE, _('GRE')),
(ENCAP_WIREGUARD, _('WireGuard')),
(ENCAP_OPENVPN, _('OpenVPN')),
(ENCAP_L2TP, _('L2TP')),
(ENCAP_PPTP, _('PPTP')),
Choice(
ENCAP_IPSEC_TRANSPORT,
_('IPsec - Transport'),
description=_('IPsec encrypting only the packet payload between endpoints')
),
Choice(
ENCAP_IPSEC_TUNNEL,
_('IPsec - Tunnel'),
description=_('IPsec encrypting the entire original IP packet')
),
Choice(ENCAP_IP_IP, _('IP-in-IP'), description=_('Encapsulation of one IP packet within another')),
Choice(ENCAP_GRE, _('GRE'), description=_('Generic Routing Encapsulation')),
Choice(ENCAP_WIREGUARD, _('WireGuard')),
Choice(ENCAP_OPENVPN, _('OpenVPN')),
Choice(ENCAP_L2TP, _('L2TP'), description=_('Layer 2 Tunneling Protocol')),
Choice(ENCAP_PPTP, _('PPTP'), description=_('Point-to-Point Tunneling Protocol')),
]
@ -49,8 +62,8 @@ class TunnelTerminationTypeChoices(ChoiceSet):
TYPE_VIRTUALMACHINE = 'virtualization.virtualmachine'
CHOICES = (
(TYPE_DEVICE, _('Device')),
(TYPE_VIRTUALMACHINE, _('Virtual Machine')),
Choice(TYPE_DEVICE, _('Device')),
Choice(TYPE_VIRTUALMACHINE, _('Virtual Machine')),
)
@ -60,9 +73,9 @@ class TunnelTerminationRoleChoices(ChoiceSet):
ROLE_SPOKE = 'spoke'
CHOICES = [
(ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke'), 'orange'),
Choice(ROLE_PEER, _('Peer'), color='green', description=_('Symmetric endpoint in a point-to-point tunnel')),
Choice(ROLE_HUB, _('Hub'), color='blue', description=_('Central endpoint in a hub-and-spoke topology')),
Choice(ROLE_SPOKE, _('Spoke'), color='orange', description=_('Remote endpoint connecting to a hub')),
]
@ -75,8 +88,8 @@ class IKEVersionChoices(ChoiceSet):
VERSION_2 = 2
CHOICES = (
(VERSION_1, 'IKEv1'),
(VERSION_2, 'IKEv2'),
Choice(VERSION_1, 'IKEv1'),
Choice(VERSION_2, 'IKEv2'),
)
@ -85,8 +98,8 @@ class IKEModeChoices(ChoiceSet):
MAIN = 'main'
CHOICES = (
(AGGRESSIVE, _('Aggressive')),
(MAIN, _('Main')),
Choice(AGGRESSIVE, _('Aggressive')),
Choice(MAIN, _('Main')),
)
@ -97,10 +110,10 @@ class AuthenticationMethodChoices(ChoiceSet):
DSA_SIGNATURES = 'dsa-signatures'
CHOICES = (
(PRESHARED_KEYS, _('Pre-shared keys')),
(CERTIFICATES, _('Certificates')),
(RSA_SIGNATURES, _('RSA signatures')),
(DSA_SIGNATURES, _('DSA signatures')),
Choice(PRESHARED_KEYS, _('Pre-shared keys')),
Choice(CERTIFICATES, _('Certificates')),
Choice(RSA_SIGNATURES, _('RSA signatures')),
Choice(DSA_SIGNATURES, _('DSA signatures')),
)
@ -109,8 +122,8 @@ class IPSecModeChoices(ChoiceSet):
AH = 'ah'
CHOICES = (
(ESP, 'ESP'),
(AH, 'AH'),
Choice(ESP, 'ESP'),
Choice(AH, 'AH'),
)
@ -125,14 +138,14 @@ class EncryptionAlgorithmChoices(ChoiceSet):
ENCRYPTION_DES = 'des-cbc'
CHOICES = (
(ENCRYPTION_AES128_CBC, '128-bit AES (CBC)'),
(ENCRYPTION_AES128_GCM, '128-bit AES (GCM)'),
(ENCRYPTION_AES192_CBC, '192-bit AES (CBC)'),
(ENCRYPTION_AES192_GCM, '192-bit AES (GCM)'),
(ENCRYPTION_AES256_CBC, '256-bit AES (CBC)'),
(ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
(ENCRYPTION_3DES, '3DES'),
(ENCRYPTION_DES, 'DES'),
Choice(ENCRYPTION_AES128_CBC, '128-bit AES (CBC)'),
Choice(ENCRYPTION_AES128_GCM, '128-bit AES (GCM)'),
Choice(ENCRYPTION_AES192_CBC, '192-bit AES (CBC)'),
Choice(ENCRYPTION_AES192_GCM, '192-bit AES (GCM)'),
Choice(ENCRYPTION_AES256_CBC, '256-bit AES (CBC)'),
Choice(ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
Choice(ENCRYPTION_3DES, '3DES'),
Choice(ENCRYPTION_DES, 'DES'),
)
@ -144,11 +157,11 @@ class AuthenticationAlgorithmChoices(ChoiceSet):
AUTH_HMAC_MD5 = 'hmac-md5'
CHOICES = (
(AUTH_HMAC_SHA1, 'SHA-1 HMAC'),
(AUTH_HMAC_SHA256, 'SHA-256 HMAC'),
(AUTH_HMAC_SHA384, 'SHA-384 HMAC'),
(AUTH_HMAC_SHA512, 'SHA-512 HMAC'),
(AUTH_HMAC_MD5, 'MD5 HMAC'),
Choice(AUTH_HMAC_SHA1, 'SHA-1 HMAC'),
Choice(AUTH_HMAC_SHA256, 'SHA-256 HMAC'),
Choice(AUTH_HMAC_SHA384, 'SHA-384 HMAC'),
Choice(AUTH_HMAC_SHA512, 'SHA-512 HMAC'),
Choice(AUTH_HMAC_MD5, 'MD5 HMAC'),
)
@ -183,30 +196,30 @@ class DHGroupChoices(ChoiceSet):
CHOICES = (
# Strings are formatted in this manner to optimize translations
(GROUP_1, _('Group {n}').format(n=1)),
(GROUP_2, _('Group {n}').format(n=2)),
(GROUP_5, _('Group {n}').format(n=5)),
(GROUP_14, _('Group {n}').format(n=14)),
(GROUP_15, _('Group {n}').format(n=15)),
(GROUP_16, _('Group {n}').format(n=16)),
(GROUP_17, _('Group {n}').format(n=17)),
(GROUP_18, _('Group {n}').format(n=18)),
(GROUP_19, _('Group {n}').format(n=19)),
(GROUP_20, _('Group {n}').format(n=20)),
(GROUP_21, _('Group {n}').format(n=21)),
(GROUP_22, _('Group {n}').format(n=22)),
(GROUP_23, _('Group {n}').format(n=23)),
(GROUP_24, _('Group {n}').format(n=24)),
(GROUP_25, _('Group {n}').format(n=25)),
(GROUP_26, _('Group {n}').format(n=26)),
(GROUP_27, _('Group {n}').format(n=27)),
(GROUP_28, _('Group {n}').format(n=28)),
(GROUP_29, _('Group {n}').format(n=29)),
(GROUP_30, _('Group {n}').format(n=30)),
(GROUP_31, _('Group {n}').format(n=31)),
(GROUP_32, _('Group {n}').format(n=32)),
(GROUP_33, _('Group {n}').format(n=33)),
(GROUP_34, _('Group {n}').format(n=34)),
Choice(GROUP_1, _('Group {n}').format(n=1)),
Choice(GROUP_2, _('Group {n}').format(n=2)),
Choice(GROUP_5, _('Group {n}').format(n=5)),
Choice(GROUP_14, _('Group {n}').format(n=14)),
Choice(GROUP_15, _('Group {n}').format(n=15)),
Choice(GROUP_16, _('Group {n}').format(n=16)),
Choice(GROUP_17, _('Group {n}').format(n=17)),
Choice(GROUP_18, _('Group {n}').format(n=18)),
Choice(GROUP_19, _('Group {n}').format(n=19)),
Choice(GROUP_20, _('Group {n}').format(n=20)),
Choice(GROUP_21, _('Group {n}').format(n=21)),
Choice(GROUP_22, _('Group {n}').format(n=22)),
Choice(GROUP_23, _('Group {n}').format(n=23)),
Choice(GROUP_24, _('Group {n}').format(n=24)),
Choice(GROUP_25, _('Group {n}').format(n=25)),
Choice(GROUP_26, _('Group {n}').format(n=26)),
Choice(GROUP_27, _('Group {n}').format(n=27)),
Choice(GROUP_28, _('Group {n}').format(n=28)),
Choice(GROUP_29, _('Group {n}').format(n=29)),
Choice(GROUP_30, _('Group {n}').format(n=30)),
Choice(GROUP_31, _('Group {n}').format(n=31)),
Choice(GROUP_32, _('Group {n}').format(n=32)),
Choice(GROUP_33, _('Group {n}').format(n=33)),
Choice(GROUP_34, _('Group {n}').format(n=34)),
)
@ -232,32 +245,52 @@ class L2VPNTypeChoices(ChoiceSet):
CHOICES = (
('VPLS', (
(TYPE_VPWS, 'VPWS'),
(TYPE_VPLS, 'VPLS'),
Choice(
TYPE_VPWS,
'VPWS',
description=_('Virtual Private Wire Service: point-to-point Layer 2 connectivity')
),
Choice(
TYPE_VPLS,
'VPLS',
description=_('Virtual Private LAN Service: multipoint Layer 2 connectivity')
),
)),
('VXLAN', (
(TYPE_VXLAN, 'VXLAN'),
(TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
Choice(TYPE_VXLAN, 'VXLAN', description=_('Virtual Extensible LAN overlay')),
Choice(TYPE_VXLAN_EVPN, 'VXLAN-EVPN', description=_('VXLAN with EVPN control plane')),
)),
('L2VPN E-VPN', (
(TYPE_MPLS_EVPN, 'MPLS EVPN'),
(TYPE_PBB_EVPN, 'PBB EVPN'),
(TYPE_EVPN_VPWS, 'EVPN VPWS')
Choice(TYPE_MPLS_EVPN, 'MPLS EVPN', description=_('Ethernet VPN over an MPLS transport')),
Choice(TYPE_PBB_EVPN, 'PBB EVPN', description=_('Provider Backbone Bridging with EVPN')),
Choice(TYPE_EVPN_VPWS, 'EVPN VPWS', description=_('Point-to-point service using an EVPN control plane'))
)),
('E-Line', (
(TYPE_EPL, 'EPL'),
(TYPE_EVPL, 'EVPL'),
Choice(TYPE_EPL, 'EPL', description=_('Ethernet Private Line: dedicated point-to-point service')),
Choice(
TYPE_EVPL,
'EVPL',
description=_('Ethernet Virtual Private Line: multiplexed point-to-point service')
),
)),
('E-LAN', (
(TYPE_EPLAN, _('Ethernet Private LAN')),
(TYPE_EVPLAN, _('Ethernet Virtual Private LAN')),
Choice(TYPE_EPLAN, _('Ethernet Private LAN'), description=_('Dedicated multipoint-to-multipoint service')),
Choice(
TYPE_EVPLAN,
_('Ethernet Virtual Private LAN'),
description=_('Multiplexed multipoint-to-multipoint service')
),
)),
('E-Tree', (
(TYPE_EPTREE, _('Ethernet Private Tree')),
(TYPE_EVPTREE, _('Ethernet Virtual Private Tree')),
Choice(TYPE_EPTREE, _('Ethernet Private Tree'), description=_('Dedicated rooted multipoint service')),
Choice(
TYPE_EVPTREE,
_('Ethernet Virtual Private Tree'),
description=_('Multiplexed rooted multipoint service')
),
)),
('Other', (
(TYPE_SPB, _('SPB')),
Choice(TYPE_SPB, _('SPB'), description=_('Shortest Path Bridging')),
)),
)
@ -277,7 +310,12 @@ class L2VPNStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'red'),
Choice(STATUS_ACTIVE, _('Active'), color='green', description=_('Established and carrying traffic')),
Choice(
STATUS_PLANNED,
_('Planned'),
color='cyan',
description=_('Designated for future use but not yet in service')
),
Choice(STATUS_DECOMMISSIONING, _('Decommissioning'), color='red', description=_('Being retired from service')),
]

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.fields import ChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet
from vpn.choices import *
from vpn.models import *
@ -29,7 +29,7 @@ class TunnelGroupBulkEditForm(OrganizationalModelBulkEditForm):
class TunnelBulkEditForm(PrimaryModelBulkEditForm):
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(TunnelStatusChoices),
required=False
@ -39,7 +39,7 @@ class TunnelBulkEditForm(PrimaryModelBulkEditForm):
label=_('Tunnel group'),
required=False
)
encapsulation = forms.ChoiceField(
encapsulation = ChoiceField(
label=_('Encapsulation'),
choices=add_blank_choice(TunnelEncapsulationChoices),
required=False
@ -71,7 +71,7 @@ class TunnelBulkEditForm(PrimaryModelBulkEditForm):
class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
role = ChoiceField(
label=_('Role'),
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False
@ -81,22 +81,22 @@ class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm):
class IKEProposalBulkEditForm(PrimaryModelBulkEditForm):
authentication_method = forms.ChoiceField(
authentication_method = ChoiceField(
label=_('Authentication method'),
choices=add_blank_choice(AuthenticationMethodChoices),
required=False
)
encryption_algorithm = forms.ChoiceField(
encryption_algorithm = ChoiceField(
label=_('Encryption algorithm'),
choices=add_blank_choice(EncryptionAlgorithmChoices),
required=False
)
authentication_algorithm = forms.ChoiceField(
authentication_algorithm = ChoiceField(
label=_('Authentication algorithm'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False
)
group = forms.ChoiceField(
group = ChoiceField(
label=_('Group'),
choices=add_blank_choice(DHGroupChoices),
required=False
@ -119,12 +119,12 @@ class IKEProposalBulkEditForm(PrimaryModelBulkEditForm):
class IKEPolicyBulkEditForm(PrimaryModelBulkEditForm):
version = forms.ChoiceField(
version = ChoiceField(
label=_('Version'),
choices=add_blank_choice(IKEVersionChoices),
required=False
)
mode = forms.ChoiceField(
mode = ChoiceField(
label=_('Mode'),
choices=add_blank_choice(IKEModeChoices),
required=False
@ -144,12 +144,12 @@ class IKEPolicyBulkEditForm(PrimaryModelBulkEditForm):
class IPSecProposalBulkEditForm(PrimaryModelBulkEditForm):
encryption_algorithm = forms.ChoiceField(
encryption_algorithm = ChoiceField(
label=_('Encryption algorithm'),
choices=add_blank_choice(EncryptionAlgorithmChoices),
required=False
)
authentication_algorithm = forms.ChoiceField(
authentication_algorithm = ChoiceField(
label=_('Authentication algorithm'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False
@ -176,7 +176,7 @@ class IPSecProposalBulkEditForm(PrimaryModelBulkEditForm):
class IPSecPolicyBulkEditForm(PrimaryModelBulkEditForm):
pfs_group = forms.ChoiceField(
pfs_group = ChoiceField(
label=_('PFS group'),
choices=add_blank_choice(DHGroupChoices),
required=False
@ -192,7 +192,7 @@ class IPSecPolicyBulkEditForm(PrimaryModelBulkEditForm):
class IPSecProfileBulkEditForm(PrimaryModelBulkEditForm):
mode = forms.ChoiceField(
mode = ChoiceField(
label=_('Mode'),
choices=add_blank_choice(IPSecModeChoices),
required=False
@ -218,11 +218,11 @@ class IPSecProfileBulkEditForm(PrimaryModelBulkEditForm):
class L2VPNBulkEditForm(PrimaryModelBulkEditForm):
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=L2VPNStatusChoices,
)
type = forms.ChoiceField(
type = ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices),
required=False

View File

@ -6,7 +6,13 @@ from dcim.models import Device, Interface
from ipam.models import VLAN, IPAddress, RouteTarget
from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.fields import (
ChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
SlugField,
TypedChoiceField,
)
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.utils import add_blank_choice, get_field_value
from utilities.forms.widgets import HTMXSelect
@ -42,6 +48,15 @@ class TunnelGroupForm(OrganizationalModelForm):
class TunnelForm(TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=TunnelStatusChoices,
initial=TunnelStatusChoices.STATUS_ACTIVE,
)
encapsulation = ChoiceField(
label=_('Encapsulation'),
choices=TunnelEncapsulationChoices,
)
group = DynamicModelChoiceField(
queryset=TunnelGroup.objects.all(),
label=_('Tunnel Group'),
@ -70,12 +85,12 @@ class TunnelForm(TenancyForm, PrimaryModelForm):
class TunnelCreateForm(TunnelForm):
# First termination
termination1_role = forms.ChoiceField(
termination1_role = ChoiceField(
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False,
label=_('Role')
)
termination1_type = forms.ChoiceField(
termination1_type = ChoiceField(
choices=TunnelTerminationTypeChoices,
required=False,
widget=HTMXSelect(hx_target_id='tunnel-termination1'),
@ -105,12 +120,12 @@ class TunnelCreateForm(TunnelForm):
)
# Second termination
termination2_role = forms.ChoiceField(
termination2_role = ChoiceField(
choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False,
label=_('Role')
)
termination2_type = forms.ChoiceField(
termination2_type = ChoiceField(
choices=TunnelTerminationTypeChoices,
required=False,
widget=HTMXSelect(hx_target_id='tunnel-termination2'),
@ -220,10 +235,15 @@ class TunnelCreateForm(TunnelForm):
class TunnelTerminationForm(NetBoxModelForm):
role = ChoiceField(
label=_('Role'),
choices=TunnelTerminationRoleChoices,
initial=TunnelTerminationRoleChoices.ROLE_PEER,
)
tunnel = DynamicModelChoiceField(
queryset=Tunnel.objects.all()
)
type = forms.ChoiceField(
type = ChoiceField(
choices=TunnelTerminationTypeChoices,
widget=HTMXSelect(hx_target_id='tunnel-termination'),
label=_('Type')
@ -294,6 +314,19 @@ class TunnelTerminationForm(NetBoxModelForm):
class IKEProposalForm(PrimaryModelForm):
authentication_method = ChoiceField(
label=_('Authentication method'),
choices=AuthenticationMethodChoices,
)
encryption_algorithm = ChoiceField(
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices,
)
authentication_algorithm = TypedChoiceField(
label=_('Authentication algorithm'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False,
)
fieldsets = (
FieldSet('name', 'description', 'tags', name=_('Proposal')),
@ -312,6 +345,11 @@ class IKEProposalForm(PrimaryModelForm):
class IKEPolicyForm(PrimaryModelForm):
mode = TypedChoiceField(
label=_('Mode'),
choices=add_blank_choice(IKEModeChoices),
required=False,
)
proposals = DynamicModelMultipleChoiceField(
queryset=IKEProposal.objects.all(),
label=_('Proposals'),
@ -331,6 +369,16 @@ class IKEPolicyForm(PrimaryModelForm):
class IPSecProposalForm(PrimaryModelForm):
encryption_algorithm = TypedChoiceField(
label=_('Encryption'),
choices=add_blank_choice(EncryptionAlgorithmChoices),
required=False,
)
authentication_algorithm = TypedChoiceField(
label=_('Authentication'),
choices=add_blank_choice(AuthenticationAlgorithmChoices),
required=False,
)
fieldsets = (
FieldSet('name', 'description', 'tags', name=_('Proposal')),
@ -368,6 +416,10 @@ class IPSecPolicyForm(PrimaryModelForm):
class IPSecProfileForm(PrimaryModelForm):
mode = ChoiceField(
label=_('Mode'),
choices=IPSecModeChoices,
)
ike_policy = DynamicModelChoiceField(
queryset=IKEPolicy.objects.all(),
label=_('IKE policy')
@ -394,6 +446,15 @@ class IPSecProfileForm(PrimaryModelForm):
#
class L2VPNForm(TenancyForm, PrimaryModelForm):
type = ChoiceField(
label=_('Type'),
choices=L2VPNTypeChoices,
)
status = ChoiceField(
label=_('Status'),
choices=L2VPNStatusChoices,
initial=L2VPNStatusChoices.STATUS_ACTIVE,
)
slug = SlugField()
import_targets = DynamicModelMultipleChoiceField(
label=_('Import targets'),

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
from utilities.choices import Choice, ChoiceSet
class WirelessRoleChoices(ChoiceSet):
@ -8,8 +8,8 @@ class WirelessRoleChoices(ChoiceSet):
ROLE_STATION = 'station'
CHOICES = (
(ROLE_AP, _('Access point')),
(ROLE_STATION, _('Station')),
Choice(ROLE_AP, _('Access point'), description=_('Provides wireless network access to client devices')),
Choice(ROLE_STATION, _('Station'), description=_('Client device connecting to a wireless network')),
)
@ -22,10 +22,20 @@ class WirelessLANStatusChoices(ChoiceSet):
STATUS_DEPRECATED = 'deprecated'
CHOICES = [
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_RESERVED, _('Reserved'), 'cyan'),
(STATUS_DISABLED, _('Disabled'), 'orange'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
Choice(STATUS_ACTIVE, _('Active'), color='green', description=_('In service and operational')),
Choice(STATUS_RESERVED, _('Reserved'), color='cyan', description=_('Set aside for future use')),
Choice(
STATUS_DISABLED,
_('Disabled'),
color='orange',
description=_('Configured but not currently in service')
),
Choice(
STATUS_DEPRECATED,
_('Deprecated'),
color='red',
description=_('Retained for reference but no longer in use')
),
]
@ -240,218 +250,218 @@ class WirelessChannelChoices(ChoiceSet):
(
'2.4 GHz (802.11b/g/n/ax)',
(
(CHANNEL_24G_1, '1 (2412 MHz)'),
(CHANNEL_24G_2, '2 (2417 MHz)'),
(CHANNEL_24G_3, '3 (2422 MHz)'),
(CHANNEL_24G_4, '4 (2427 MHz)'),
(CHANNEL_24G_5, '5 (2432 MHz)'),
(CHANNEL_24G_6, '6 (2437 MHz)'),
(CHANNEL_24G_7, '7 (2442 MHz)'),
(CHANNEL_24G_8, '8 (2447 MHz)'),
(CHANNEL_24G_9, '9 (2452 MHz)'),
(CHANNEL_24G_10, '10 (2457 MHz)'),
(CHANNEL_24G_11, '11 (2462 MHz)'),
(CHANNEL_24G_12, '12 (2467 MHz)'),
(CHANNEL_24G_13, '13 (2472 MHz)'),
Choice(CHANNEL_24G_1, '1 (2412 MHz)'),
Choice(CHANNEL_24G_2, '2 (2417 MHz)'),
Choice(CHANNEL_24G_3, '3 (2422 MHz)'),
Choice(CHANNEL_24G_4, '4 (2427 MHz)'),
Choice(CHANNEL_24G_5, '5 (2432 MHz)'),
Choice(CHANNEL_24G_6, '6 (2437 MHz)'),
Choice(CHANNEL_24G_7, '7 (2442 MHz)'),
Choice(CHANNEL_24G_8, '8 (2447 MHz)'),
Choice(CHANNEL_24G_9, '9 (2452 MHz)'),
Choice(CHANNEL_24G_10, '10 (2457 MHz)'),
Choice(CHANNEL_24G_11, '11 (2462 MHz)'),
Choice(CHANNEL_24G_12, '12 (2467 MHz)'),
Choice(CHANNEL_24G_13, '13 (2472 MHz)'),
)
),
(
'5 GHz (802.11a/n/ac/ax)',
(
(CHANNEL_5G_32, '32 (5160/20 MHz)'),
(CHANNEL_5G_34, '34 (5170/40 MHz)'),
(CHANNEL_5G_36, '36 (5180/20 MHz)'),
(CHANNEL_5G_38, '38 (5190/40 MHz)'),
(CHANNEL_5G_40, '40 (5200/20 MHz)'),
(CHANNEL_5G_42, '42 (5210/80 MHz)'),
(CHANNEL_5G_44, '44 (5220/20 MHz)'),
(CHANNEL_5G_46, '46 (5230/40 MHz)'),
(CHANNEL_5G_48, '48 (5240/20 MHz)'),
(CHANNEL_5G_50, '50 (5250/160 MHz)'),
(CHANNEL_5G_52, '52 (5260/20 MHz)'),
(CHANNEL_5G_54, '54 (5270/40 MHz)'),
(CHANNEL_5G_56, '56 (5280/20 MHz)'),
(CHANNEL_5G_58, '58 (5290/80 MHz)'),
(CHANNEL_5G_60, '60 (5300/20 MHz)'),
(CHANNEL_5G_62, '62 (5310/40 MHz)'),
(CHANNEL_5G_64, '64 (5320/20 MHz)'),
(CHANNEL_5G_100, '100 (5500/20 MHz)'),
(CHANNEL_5G_102, '102 (5510/40 MHz)'),
(CHANNEL_5G_104, '104 (5520/20 MHz)'),
(CHANNEL_5G_106, '106 (5530/80 MHz)'),
(CHANNEL_5G_108, '108 (5540/20 MHz)'),
(CHANNEL_5G_110, '110 (5550/40 MHz)'),
(CHANNEL_5G_112, '112 (5560/20 MHz)'),
(CHANNEL_5G_114, '114 (5570/160 MHz)'),
(CHANNEL_5G_116, '116 (5580/20 MHz)'),
(CHANNEL_5G_118, '118 (5590/40 MHz)'),
(CHANNEL_5G_120, '120 (5600/20 MHz)'),
(CHANNEL_5G_122, '122 (5610/80 MHz)'),
(CHANNEL_5G_124, '124 (5620/20 MHz)'),
(CHANNEL_5G_126, '126 (5630/40 MHz)'),
(CHANNEL_5G_128, '128 (5640/20 MHz)'),
(CHANNEL_5G_132, '132 (5660/20 MHz)'),
(CHANNEL_5G_134, '134 (5670/40 MHz)'),
(CHANNEL_5G_136, '136 (5680/20 MHz)'),
(CHANNEL_5G_138, '138 (5690/80 MHz)'),
(CHANNEL_5G_140, '140 (5700/20 MHz)'),
(CHANNEL_5G_142, '142 (5710/40 MHz)'),
(CHANNEL_5G_144, '144 (5720/20 MHz)'),
(CHANNEL_5G_149, '149 (5745/20 MHz)'),
(CHANNEL_5G_151, '151 (5755/40 MHz)'),
(CHANNEL_5G_153, '153 (5765/20 MHz)'),
(CHANNEL_5G_155, '155 (5775/80 MHz)'),
(CHANNEL_5G_157, '157 (5785/20 MHz)'),
(CHANNEL_5G_159, '159 (5795/40 MHz)'),
(CHANNEL_5G_161, '161 (5805/20 MHz)'),
(CHANNEL_5G_163, '163 (5815/160 MHz)'),
(CHANNEL_5G_165, '165 (5825/20 MHz)'),
(CHANNEL_5G_167, '167 (5835/40 MHz)'),
(CHANNEL_5G_169, '169 (5845/20 MHz)'),
(CHANNEL_5G_171, '171 (5855/80 MHz)'),
(CHANNEL_5G_173, '173 (5865/20 MHz)'),
(CHANNEL_5G_175, '175 (5875/40 MHz)'),
(CHANNEL_5G_177, '177 (5885/20 MHz)'),
Choice(CHANNEL_5G_32, '32 (5160/20 MHz)'),
Choice(CHANNEL_5G_34, '34 (5170/40 MHz)'),
Choice(CHANNEL_5G_36, '36 (5180/20 MHz)'),
Choice(CHANNEL_5G_38, '38 (5190/40 MHz)'),
Choice(CHANNEL_5G_40, '40 (5200/20 MHz)'),
Choice(CHANNEL_5G_42, '42 (5210/80 MHz)'),
Choice(CHANNEL_5G_44, '44 (5220/20 MHz)'),
Choice(CHANNEL_5G_46, '46 (5230/40 MHz)'),
Choice(CHANNEL_5G_48, '48 (5240/20 MHz)'),
Choice(CHANNEL_5G_50, '50 (5250/160 MHz)'),
Choice(CHANNEL_5G_52, '52 (5260/20 MHz)'),
Choice(CHANNEL_5G_54, '54 (5270/40 MHz)'),
Choice(CHANNEL_5G_56, '56 (5280/20 MHz)'),
Choice(CHANNEL_5G_58, '58 (5290/80 MHz)'),
Choice(CHANNEL_5G_60, '60 (5300/20 MHz)'),
Choice(CHANNEL_5G_62, '62 (5310/40 MHz)'),
Choice(CHANNEL_5G_64, '64 (5320/20 MHz)'),
Choice(CHANNEL_5G_100, '100 (5500/20 MHz)'),
Choice(CHANNEL_5G_102, '102 (5510/40 MHz)'),
Choice(CHANNEL_5G_104, '104 (5520/20 MHz)'),
Choice(CHANNEL_5G_106, '106 (5530/80 MHz)'),
Choice(CHANNEL_5G_108, '108 (5540/20 MHz)'),
Choice(CHANNEL_5G_110, '110 (5550/40 MHz)'),
Choice(CHANNEL_5G_112, '112 (5560/20 MHz)'),
Choice(CHANNEL_5G_114, '114 (5570/160 MHz)'),
Choice(CHANNEL_5G_116, '116 (5580/20 MHz)'),
Choice(CHANNEL_5G_118, '118 (5590/40 MHz)'),
Choice(CHANNEL_5G_120, '120 (5600/20 MHz)'),
Choice(CHANNEL_5G_122, '122 (5610/80 MHz)'),
Choice(CHANNEL_5G_124, '124 (5620/20 MHz)'),
Choice(CHANNEL_5G_126, '126 (5630/40 MHz)'),
Choice(CHANNEL_5G_128, '128 (5640/20 MHz)'),
Choice(CHANNEL_5G_132, '132 (5660/20 MHz)'),
Choice(CHANNEL_5G_134, '134 (5670/40 MHz)'),
Choice(CHANNEL_5G_136, '136 (5680/20 MHz)'),
Choice(CHANNEL_5G_138, '138 (5690/80 MHz)'),
Choice(CHANNEL_5G_140, '140 (5700/20 MHz)'),
Choice(CHANNEL_5G_142, '142 (5710/40 MHz)'),
Choice(CHANNEL_5G_144, '144 (5720/20 MHz)'),
Choice(CHANNEL_5G_149, '149 (5745/20 MHz)'),
Choice(CHANNEL_5G_151, '151 (5755/40 MHz)'),
Choice(CHANNEL_5G_153, '153 (5765/20 MHz)'),
Choice(CHANNEL_5G_155, '155 (5775/80 MHz)'),
Choice(CHANNEL_5G_157, '157 (5785/20 MHz)'),
Choice(CHANNEL_5G_159, '159 (5795/40 MHz)'),
Choice(CHANNEL_5G_161, '161 (5805/20 MHz)'),
Choice(CHANNEL_5G_163, '163 (5815/160 MHz)'),
Choice(CHANNEL_5G_165, '165 (5825/20 MHz)'),
Choice(CHANNEL_5G_167, '167 (5835/40 MHz)'),
Choice(CHANNEL_5G_169, '169 (5845/20 MHz)'),
Choice(CHANNEL_5G_171, '171 (5855/80 MHz)'),
Choice(CHANNEL_5G_173, '173 (5865/20 MHz)'),
Choice(CHANNEL_5G_175, '175 (5875/40 MHz)'),
Choice(CHANNEL_5G_177, '177 (5885/20 MHz)'),
)
),
(
'6 GHz (802.11ax)',
(
(CHANNEL_6G_1, '1 (5955/20 MHz)'),
(CHANNEL_6G_3, '3 (5965/40 MHz)'),
(CHANNEL_6G_5, '5 (5975/20 MHz)'),
(CHANNEL_6G_7, '7 (5985/80 MHz)'),
(CHANNEL_6G_9, '9 (5995/20 MHz)'),
(CHANNEL_6G_11, '11 (6005/40 MHz)'),
(CHANNEL_6G_13, '13 (6015/20 MHz)'),
(CHANNEL_6G_15, '15 (6025/160 MHz)'),
(CHANNEL_6G_17, '17 (6035/20 MHz)'),
(CHANNEL_6G_19, '19 (6045/40 MHz)'),
(CHANNEL_6G_21, '21 (6055/20 MHz)'),
(CHANNEL_6G_23, '23 (6065/80 MHz)'),
(CHANNEL_6G_25, '25 (6075/20 MHz)'),
(CHANNEL_6G_27, '27 (6085/40 MHz)'),
(CHANNEL_6G_29, '29 (6095/20 MHz)'),
(CHANNEL_6G_31, '31 (6105/320 MHz)'),
(CHANNEL_6G_33, '33 (6115/20 MHz)'),
(CHANNEL_6G_35, '35 (6125/40 MHz)'),
(CHANNEL_6G_37, '37 (6135/20 MHz)'),
(CHANNEL_6G_39, '39 (6145/80 MHz)'),
(CHANNEL_6G_41, '41 (6155/20 MHz)'),
(CHANNEL_6G_43, '43 (6165/40 MHz)'),
(CHANNEL_6G_45, '45 (6175/20 MHz)'),
(CHANNEL_6G_47, '47 (6185/160 MHz)'),
(CHANNEL_6G_49, '49 (6195/20 MHz)'),
(CHANNEL_6G_51, '51 (6205/40 MHz)'),
(CHANNEL_6G_53, '53 (6215/20 MHz)'),
(CHANNEL_6G_55, '55 (6225/80 MHz)'),
(CHANNEL_6G_57, '57 (6235/20 MHz)'),
(CHANNEL_6G_59, '59 (6245/40 MHz)'),
(CHANNEL_6G_61, '61 (6255/20 MHz)'),
(CHANNEL_6G_65, '65 (6275/20 MHz)'),
(CHANNEL_6G_67, '67 (6285/40 MHz)'),
(CHANNEL_6G_69, '69 (6295/20 MHz)'),
(CHANNEL_6G_71, '71 (6305/80 MHz)'),
(CHANNEL_6G_73, '73 (6315/20 MHz)'),
(CHANNEL_6G_75, '75 (6325/40 MHz)'),
(CHANNEL_6G_77, '77 (6335/20 MHz)'),
(CHANNEL_6G_79, '79 (6345/160 MHz)'),
(CHANNEL_6G_81, '81 (6355/20 MHz)'),
(CHANNEL_6G_83, '83 (6365/40 MHz)'),
(CHANNEL_6G_85, '85 (6375/20 MHz)'),
(CHANNEL_6G_87, '87 (6385/80 MHz)'),
(CHANNEL_6G_89, '89 (6395/20 MHz)'),
(CHANNEL_6G_91, '91 (6405/40 MHz)'),
(CHANNEL_6G_93, '93 (6415/20 MHz)'),
(CHANNEL_6G_95, '95 (6425/320 MHz)'),
(CHANNEL_6G_97, '97 (6435/20 MHz)'),
(CHANNEL_6G_99, '99 (6445/40 MHz)'),
(CHANNEL_6G_101, '101 (6455/20 MHz)'),
(CHANNEL_6G_103, '103 (6465/80 MHz)'),
(CHANNEL_6G_105, '105 (6475/20 MHz)'),
(CHANNEL_6G_107, '107 (6485/40 MHz)'),
(CHANNEL_6G_109, '109 (6495/20 MHz)'),
(CHANNEL_6G_111, '111 (6505/160 MHz)'),
(CHANNEL_6G_113, '113 (6515/20 MHz)'),
(CHANNEL_6G_115, '115 (6525/40 MHz)'),
(CHANNEL_6G_117, '117 (6535/20 MHz)'),
(CHANNEL_6G_119, '119 (6545/80 MHz)'),
(CHANNEL_6G_121, '121 (6555/20 MHz)'),
(CHANNEL_6G_123, '123 (6565/40 MHz)'),
(CHANNEL_6G_125, '125 (6575/20 MHz)'),
(CHANNEL_6G_129, '129 (6595/20 MHz)'),
(CHANNEL_6G_131, '131 (6605/40 MHz)'),
(CHANNEL_6G_133, '133 (6615/20 MHz)'),
(CHANNEL_6G_135, '135 (6625/80 MHz)'),
(CHANNEL_6G_137, '137 (6635/20 MHz)'),
(CHANNEL_6G_139, '139 (6645/40 MHz)'),
(CHANNEL_6G_141, '141 (6655/20 MHz)'),
(CHANNEL_6G_143, '143 (6665/160 MHz)'),
(CHANNEL_6G_145, '145 (6675/20 MHz)'),
(CHANNEL_6G_147, '147 (6685/40 MHz)'),
(CHANNEL_6G_149, '149 (6695/20 MHz)'),
(CHANNEL_6G_151, '151 (6705/80 MHz)'),
(CHANNEL_6G_153, '153 (6715/20 MHz)'),
(CHANNEL_6G_155, '155 (6725/40 MHz)'),
(CHANNEL_6G_157, '157 (6735/20 MHz)'),
(CHANNEL_6G_159, '159 (6745/320 MHz)'),
(CHANNEL_6G_161, '161 (6755/20 MHz)'),
(CHANNEL_6G_163, '163 (6765/40 MHz)'),
(CHANNEL_6G_165, '165 (6775/20 MHz)'),
(CHANNEL_6G_167, '167 (6785/80 MHz)'),
(CHANNEL_6G_169, '169 (6795/20 MHz)'),
(CHANNEL_6G_171, '171 (6805/40 MHz)'),
(CHANNEL_6G_173, '173 (6815/20 MHz)'),
(CHANNEL_6G_175, '175 (6825/160 MHz)'),
(CHANNEL_6G_177, '177 (6835/20 MHz)'),
(CHANNEL_6G_179, '179 (6845/40 MHz)'),
(CHANNEL_6G_181, '181 (6855/20 MHz)'),
(CHANNEL_6G_183, '183 (6865/80 MHz)'),
(CHANNEL_6G_185, '185 (6875/20 MHz)'),
(CHANNEL_6G_187, '187 (6885/40 MHz)'),
(CHANNEL_6G_189, '189 (6895/20 MHz)'),
(CHANNEL_6G_193, '193 (6915/20 MHz)'),
(CHANNEL_6G_195, '195 (6925/40 MHz)'),
(CHANNEL_6G_197, '197 (6935/20 MHz)'),
(CHANNEL_6G_199, '199 (6945/80 MHz)'),
(CHANNEL_6G_201, '201 (6955/20 MHz)'),
(CHANNEL_6G_203, '203 (6965/40 MHz)'),
(CHANNEL_6G_205, '205 (6975/20 MHz)'),
(CHANNEL_6G_207, '207 (6985/160 MHz)'),
(CHANNEL_6G_209, '209 (6995/20 MHz)'),
(CHANNEL_6G_211, '211 (7005/40 MHz)'),
(CHANNEL_6G_213, '213 (7015/20 MHz)'),
(CHANNEL_6G_215, '215 (7025/80 MHz)'),
(CHANNEL_6G_217, '217 (7035/20 MHz)'),
(CHANNEL_6G_219, '219 (7045/40 MHz)'),
(CHANNEL_6G_221, '221 (7055/20 MHz)'),
(CHANNEL_6G_225, '225 (7075/20 MHz)'),
(CHANNEL_6G_227, '227 (7085/40 MHz)'),
(CHANNEL_6G_229, '229 (7095/20 MHz)'),
(CHANNEL_6G_233, '233 (7115/20 MHz)'),
Choice(CHANNEL_6G_1, '1 (5955/20 MHz)'),
Choice(CHANNEL_6G_3, '3 (5965/40 MHz)'),
Choice(CHANNEL_6G_5, '5 (5975/20 MHz)'),
Choice(CHANNEL_6G_7, '7 (5985/80 MHz)'),
Choice(CHANNEL_6G_9, '9 (5995/20 MHz)'),
Choice(CHANNEL_6G_11, '11 (6005/40 MHz)'),
Choice(CHANNEL_6G_13, '13 (6015/20 MHz)'),
Choice(CHANNEL_6G_15, '15 (6025/160 MHz)'),
Choice(CHANNEL_6G_17, '17 (6035/20 MHz)'),
Choice(CHANNEL_6G_19, '19 (6045/40 MHz)'),
Choice(CHANNEL_6G_21, '21 (6055/20 MHz)'),
Choice(CHANNEL_6G_23, '23 (6065/80 MHz)'),
Choice(CHANNEL_6G_25, '25 (6075/20 MHz)'),
Choice(CHANNEL_6G_27, '27 (6085/40 MHz)'),
Choice(CHANNEL_6G_29, '29 (6095/20 MHz)'),
Choice(CHANNEL_6G_31, '31 (6105/320 MHz)'),
Choice(CHANNEL_6G_33, '33 (6115/20 MHz)'),
Choice(CHANNEL_6G_35, '35 (6125/40 MHz)'),
Choice(CHANNEL_6G_37, '37 (6135/20 MHz)'),
Choice(CHANNEL_6G_39, '39 (6145/80 MHz)'),
Choice(CHANNEL_6G_41, '41 (6155/20 MHz)'),
Choice(CHANNEL_6G_43, '43 (6165/40 MHz)'),
Choice(CHANNEL_6G_45, '45 (6175/20 MHz)'),
Choice(CHANNEL_6G_47, '47 (6185/160 MHz)'),
Choice(CHANNEL_6G_49, '49 (6195/20 MHz)'),
Choice(CHANNEL_6G_51, '51 (6205/40 MHz)'),
Choice(CHANNEL_6G_53, '53 (6215/20 MHz)'),
Choice(CHANNEL_6G_55, '55 (6225/80 MHz)'),
Choice(CHANNEL_6G_57, '57 (6235/20 MHz)'),
Choice(CHANNEL_6G_59, '59 (6245/40 MHz)'),
Choice(CHANNEL_6G_61, '61 (6255/20 MHz)'),
Choice(CHANNEL_6G_65, '65 (6275/20 MHz)'),
Choice(CHANNEL_6G_67, '67 (6285/40 MHz)'),
Choice(CHANNEL_6G_69, '69 (6295/20 MHz)'),
Choice(CHANNEL_6G_71, '71 (6305/80 MHz)'),
Choice(CHANNEL_6G_73, '73 (6315/20 MHz)'),
Choice(CHANNEL_6G_75, '75 (6325/40 MHz)'),
Choice(CHANNEL_6G_77, '77 (6335/20 MHz)'),
Choice(CHANNEL_6G_79, '79 (6345/160 MHz)'),
Choice(CHANNEL_6G_81, '81 (6355/20 MHz)'),
Choice(CHANNEL_6G_83, '83 (6365/40 MHz)'),
Choice(CHANNEL_6G_85, '85 (6375/20 MHz)'),
Choice(CHANNEL_6G_87, '87 (6385/80 MHz)'),
Choice(CHANNEL_6G_89, '89 (6395/20 MHz)'),
Choice(CHANNEL_6G_91, '91 (6405/40 MHz)'),
Choice(CHANNEL_6G_93, '93 (6415/20 MHz)'),
Choice(CHANNEL_6G_95, '95 (6425/320 MHz)'),
Choice(CHANNEL_6G_97, '97 (6435/20 MHz)'),
Choice(CHANNEL_6G_99, '99 (6445/40 MHz)'),
Choice(CHANNEL_6G_101, '101 (6455/20 MHz)'),
Choice(CHANNEL_6G_103, '103 (6465/80 MHz)'),
Choice(CHANNEL_6G_105, '105 (6475/20 MHz)'),
Choice(CHANNEL_6G_107, '107 (6485/40 MHz)'),
Choice(CHANNEL_6G_109, '109 (6495/20 MHz)'),
Choice(CHANNEL_6G_111, '111 (6505/160 MHz)'),
Choice(CHANNEL_6G_113, '113 (6515/20 MHz)'),
Choice(CHANNEL_6G_115, '115 (6525/40 MHz)'),
Choice(CHANNEL_6G_117, '117 (6535/20 MHz)'),
Choice(CHANNEL_6G_119, '119 (6545/80 MHz)'),
Choice(CHANNEL_6G_121, '121 (6555/20 MHz)'),
Choice(CHANNEL_6G_123, '123 (6565/40 MHz)'),
Choice(CHANNEL_6G_125, '125 (6575/20 MHz)'),
Choice(CHANNEL_6G_129, '129 (6595/20 MHz)'),
Choice(CHANNEL_6G_131, '131 (6605/40 MHz)'),
Choice(CHANNEL_6G_133, '133 (6615/20 MHz)'),
Choice(CHANNEL_6G_135, '135 (6625/80 MHz)'),
Choice(CHANNEL_6G_137, '137 (6635/20 MHz)'),
Choice(CHANNEL_6G_139, '139 (6645/40 MHz)'),
Choice(CHANNEL_6G_141, '141 (6655/20 MHz)'),
Choice(CHANNEL_6G_143, '143 (6665/160 MHz)'),
Choice(CHANNEL_6G_145, '145 (6675/20 MHz)'),
Choice(CHANNEL_6G_147, '147 (6685/40 MHz)'),
Choice(CHANNEL_6G_149, '149 (6695/20 MHz)'),
Choice(CHANNEL_6G_151, '151 (6705/80 MHz)'),
Choice(CHANNEL_6G_153, '153 (6715/20 MHz)'),
Choice(CHANNEL_6G_155, '155 (6725/40 MHz)'),
Choice(CHANNEL_6G_157, '157 (6735/20 MHz)'),
Choice(CHANNEL_6G_159, '159 (6745/320 MHz)'),
Choice(CHANNEL_6G_161, '161 (6755/20 MHz)'),
Choice(CHANNEL_6G_163, '163 (6765/40 MHz)'),
Choice(CHANNEL_6G_165, '165 (6775/20 MHz)'),
Choice(CHANNEL_6G_167, '167 (6785/80 MHz)'),
Choice(CHANNEL_6G_169, '169 (6795/20 MHz)'),
Choice(CHANNEL_6G_171, '171 (6805/40 MHz)'),
Choice(CHANNEL_6G_173, '173 (6815/20 MHz)'),
Choice(CHANNEL_6G_175, '175 (6825/160 MHz)'),
Choice(CHANNEL_6G_177, '177 (6835/20 MHz)'),
Choice(CHANNEL_6G_179, '179 (6845/40 MHz)'),
Choice(CHANNEL_6G_181, '181 (6855/20 MHz)'),
Choice(CHANNEL_6G_183, '183 (6865/80 MHz)'),
Choice(CHANNEL_6G_185, '185 (6875/20 MHz)'),
Choice(CHANNEL_6G_187, '187 (6885/40 MHz)'),
Choice(CHANNEL_6G_189, '189 (6895/20 MHz)'),
Choice(CHANNEL_6G_193, '193 (6915/20 MHz)'),
Choice(CHANNEL_6G_195, '195 (6925/40 MHz)'),
Choice(CHANNEL_6G_197, '197 (6935/20 MHz)'),
Choice(CHANNEL_6G_199, '199 (6945/80 MHz)'),
Choice(CHANNEL_6G_201, '201 (6955/20 MHz)'),
Choice(CHANNEL_6G_203, '203 (6965/40 MHz)'),
Choice(CHANNEL_6G_205, '205 (6975/20 MHz)'),
Choice(CHANNEL_6G_207, '207 (6985/160 MHz)'),
Choice(CHANNEL_6G_209, '209 (6995/20 MHz)'),
Choice(CHANNEL_6G_211, '211 (7005/40 MHz)'),
Choice(CHANNEL_6G_213, '213 (7015/20 MHz)'),
Choice(CHANNEL_6G_215, '215 (7025/80 MHz)'),
Choice(CHANNEL_6G_217, '217 (7035/20 MHz)'),
Choice(CHANNEL_6G_219, '219 (7045/40 MHz)'),
Choice(CHANNEL_6G_221, '221 (7055/20 MHz)'),
Choice(CHANNEL_6G_225, '225 (7075/20 MHz)'),
Choice(CHANNEL_6G_227, '227 (7085/40 MHz)'),
Choice(CHANNEL_6G_229, '229 (7095/20 MHz)'),
Choice(CHANNEL_6G_233, '233 (7115/20 MHz)'),
)
),
(
'60 GHz (802.11ad/ay)',
(
(CHANNEL_60G_1, '1 (58.32/2.16 GHz)'),
(CHANNEL_60G_2, '2 (60.48/2.16 GHz)'),
(CHANNEL_60G_3, '3 (62.64/2.16 GHz)'),
(CHANNEL_60G_4, '4 (64.80/2.16 GHz)'),
(CHANNEL_60G_5, '5 (66.96/2.16 GHz)'),
(CHANNEL_60G_6, '6 (69.12/2.16 GHz)'),
(CHANNEL_60G_9, '9 (59.40/4.32 GHz)'),
(CHANNEL_60G_10, '10 (61.56/4.32 GHz)'),
(CHANNEL_60G_11, '11 (63.72/4.32 GHz)'),
(CHANNEL_60G_12, '12 (65.88/4.32 GHz)'),
(CHANNEL_60G_13, '13 (68.04/4.32 GHz)'),
(CHANNEL_60G_17, '17 (60.48/6.48 GHz)'),
(CHANNEL_60G_18, '18 (62.64/6.48 GHz)'),
(CHANNEL_60G_19, '19 (64.80/6.48 GHz)'),
(CHANNEL_60G_20, '20 (66.96/6.48 GHz)'),
(CHANNEL_60G_25, '25 (61.56/8.64 GHz)'),
(CHANNEL_60G_26, '26 (63.72/8.64 GHz)'),
(CHANNEL_60G_27, '27 (65.88/8.64 GHz)'),
Choice(CHANNEL_60G_1, '1 (58.32/2.16 GHz)'),
Choice(CHANNEL_60G_2, '2 (60.48/2.16 GHz)'),
Choice(CHANNEL_60G_3, '3 (62.64/2.16 GHz)'),
Choice(CHANNEL_60G_4, '4 (64.80/2.16 GHz)'),
Choice(CHANNEL_60G_5, '5 (66.96/2.16 GHz)'),
Choice(CHANNEL_60G_6, '6 (69.12/2.16 GHz)'),
Choice(CHANNEL_60G_9, '9 (59.40/4.32 GHz)'),
Choice(CHANNEL_60G_10, '10 (61.56/4.32 GHz)'),
Choice(CHANNEL_60G_11, '11 (63.72/4.32 GHz)'),
Choice(CHANNEL_60G_12, '12 (65.88/4.32 GHz)'),
Choice(CHANNEL_60G_13, '13 (68.04/4.32 GHz)'),
Choice(CHANNEL_60G_17, '17 (60.48/6.48 GHz)'),
Choice(CHANNEL_60G_18, '18 (62.64/6.48 GHz)'),
Choice(CHANNEL_60G_19, '19 (64.80/6.48 GHz)'),
Choice(CHANNEL_60G_20, '20 (66.96/6.48 GHz)'),
Choice(CHANNEL_60G_25, '25 (61.56/8.64 GHz)'),
Choice(CHANNEL_60G_26, '26 (63.72/8.64 GHz)'),
Choice(CHANNEL_60G_27, '27 (65.88/8.64 GHz)'),
)
),
)
@ -464,10 +474,18 @@ class WirelessAuthTypeChoices(ChoiceSet):
TYPE_WPA_ENTERPRISE = 'wpa-enterprise'
CHOICES = (
(TYPE_OPEN, _('Open')),
(TYPE_WEP, 'WEP'),
(TYPE_WPA_PERSONAL, _('WPA Personal (PSK)')),
(TYPE_WPA_ENTERPRISE, _('WPA Enterprise')),
Choice(TYPE_OPEN, _('Open'), description=_('No authentication or encryption')),
Choice(TYPE_WEP, 'WEP', description=_('Wired Equivalent Privacy (legacy, insecure)')),
Choice(
TYPE_WPA_PERSONAL,
_('WPA Personal (PSK)'),
description=_('Wi-Fi Protected Access using a shared pre-shared key')
),
Choice(
TYPE_WPA_ENTERPRISE,
_('WPA Enterprise'),
description=_('Wi-Fi Protected Access using 802.1X authentication')
),
)
@ -477,7 +495,7 @@ class WirelessAuthCipherChoices(ChoiceSet):
CIPHER_AES = 'aes'
CHOICES = (
(CIPHER_AUTO, _('Auto')),
(CIPHER_TKIP, 'TKIP'),
(CIPHER_AES, 'AES'),
Choice(CIPHER_AUTO, _('Auto'), description=_('Automatically negotiate the encryption cipher')),
Choice(CIPHER_TKIP, 'TKIP', description=_('Temporal Key Integrity Protocol (legacy)')),
Choice(CIPHER_AES, 'AES', description=_('Advanced Encryption Standard')),
)

View File

@ -8,7 +8,7 @@ from netbox.choices import *
from netbox.forms import NestedGroupModelBulkEditForm, PrimaryModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField
from utilities.forms.fields import ChoiceField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from wireless.choices import *
from wireless.constants import SSID_MAX_LENGTH
@ -36,7 +36,7 @@ class WirelessLANGroupBulkEditForm(NestedGroupModelBulkEditForm):
class WirelessLANBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(WirelessLANStatusChoices),
required=False
@ -61,12 +61,12 @@ class WirelessLANBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
auth_type = forms.ChoiceField(
auth_type = ChoiceField(
label=_('Authentication type'),
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False
)
auth_cipher = forms.ChoiceField(
auth_cipher = ChoiceField(
label=_('Authentication cipher'),
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False
@ -93,7 +93,7 @@ class WirelessLinkBulkEditForm(PrimaryModelBulkEditForm):
required=False,
label=_('SSID')
)
status = forms.ChoiceField(
status = ChoiceField(
label=_('Status'),
choices=add_blank_choice(LinkStatusChoices),
required=False
@ -103,12 +103,12 @@ class WirelessLinkBulkEditForm(PrimaryModelBulkEditForm):
queryset=Tenant.objects.all(),
required=False
)
auth_type = forms.ChoiceField(
auth_type = ChoiceField(
label=_('Authentication type'),
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False
)
auth_cipher = forms.ChoiceField(
auth_cipher = ChoiceField(
label=_('Authentication cipher'),
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False
@ -122,7 +122,7 @@ class WirelessLinkBulkEditForm(PrimaryModelBulkEditForm):
min_value=0,
required=False
)
distance_unit = forms.ChoiceField(
distance_unit = ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,

View File

@ -1,14 +1,22 @@
from django.forms import PasswordInput
from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices
from dcim.forms.mixins import ScopedForm
from dcim.models import Device, Interface, Location, Site
from ipam.models import VLAN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NestedGroupModelForm, PrimaryModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import DynamicModelChoiceField
from utilities.forms import add_blank_choice
from utilities.forms.fields import ChoiceField, DynamicModelChoiceField, TypedChoiceField
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
from wireless.choices import (
WirelessAuthCipherChoices,
WirelessAuthTypeChoices,
WirelessLANStatusChoices,
)
from wireless.models import *
__all__ = (
@ -37,6 +45,21 @@ class WirelessLANGroupForm(NestedGroupModelForm):
class WirelessLANForm(ScopedForm, TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=WirelessLANStatusChoices,
initial=WirelessLANStatusChoices.STATUS_ACTIVE,
)
auth_type = TypedChoiceField(
label=_('Authentication type'),
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False,
)
auth_cipher = TypedChoiceField(
label=_('Authentication cipher'),
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False,
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=WirelessLANGroup.objects.all(),
@ -72,6 +95,26 @@ class WirelessLANForm(ScopedForm, TenancyForm, PrimaryModelForm):
class WirelessLinkForm(DistanceValidationMixin, TenancyForm, PrimaryModelForm):
status = ChoiceField(
label=_('Status'),
choices=LinkStatusChoices,
initial=LinkStatusChoices.STATUS_CONNECTED,
)
auth_type = TypedChoiceField(
label=_('Type'),
choices=add_blank_choice(WirelessAuthTypeChoices),
required=False,
)
auth_cipher = TypedChoiceField(
label=_('Cipher'),
choices=add_blank_choice(WirelessAuthCipherChoices),
required=False,
)
distance_unit = TypedChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,
)
site_a = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,