Closes #21712: Support description annotations for static choice form fields
This commit is contained in:
parent
48ce5e7e2c
commit
a7be755e01
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from .array import *
|
||||
from .choices import *
|
||||
from .content_types import *
|
||||
from .csv import *
|
||||
from .dynamic import *
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue