diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index d8d79b6ec..97c1b6201 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -140,6 +140,41 @@ EXEMPT_VIEW_PERMISSIONS = ['*'] --- +## FIELD_CHOICES + +Default: Empty dictionary + +Some static choice fields on models can be configured with custom values. This is done by defining `FIELD_CHOICES` as a dictionary mapping model fields to their choices list. Each choice in the list must have a database value and a human-friendly label, and may optionally specify a color. + +For example, to specify a custom set of choices for the site status field: + +```python +FIELD_CHOICES = { + 'dcim.Site.status': ( + ('foo', 'Foo'), + ('bar', 'Bar'), + ('baz', 'Baz'), + ) +} +``` + +These will be appended to the stock choices for the field. + +The following model field support configurable choices: + +* `circuits.Circuit.status` +* `dcim.Device.status` +* `dcim.PowerFeed.status` +* `dcim.Rack.status` +* `dcim.Site.status` +* `ipam.IPAddress.status` +* `ipam.IPRange.status` +* `ipam.Prefix.status` +* `ipam.VLAN.status` +* `virtualization.VirtualMachine.status` + +--- + ## HTTP_PROXIES Default: None diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py index 0efa431fa..e3177adb4 100644 --- a/netbox/circuits/choices.py +++ b/netbox/circuits/choices.py @@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet # class CircuitStatusChoices(ChoiceSet): + key = 'circuits.Circuit.status' STATUS_DEPROVISIONING = 'deprovisioning' STATUS_ACTIVE = 'active' @@ -14,14 +15,14 @@ class CircuitStatusChoices(ChoiceSet): STATUS_OFFLINE = 'offline' STATUS_DECOMMISSIONED = 'decommissioned' - CHOICES = ( + CHOICES = [ (STATUS_PLANNED, 'Planned'), (STATUS_PROVISIONING, 'Provisioning'), (STATUS_ACTIVE, 'Active'), (STATUS_OFFLINE, 'Offline'), (STATUS_DEPROVISIONING, 'Deprovisioning'), (STATUS_DECOMMISSIONED, 'Decommissioned'), - ) + ] CSS_CLASSES = { STATUS_DEPROVISIONING: 'warning', diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index fcb37211f..208a06c5d 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet # class SiteStatusChoices(ChoiceSet): + key = 'dcim.Site.status' STATUS_PLANNED = 'planned' STATUS_STAGING = 'staging' @@ -13,13 +14,13 @@ class SiteStatusChoices(ChoiceSet): STATUS_DECOMMISSIONING = 'decommissioning' STATUS_RETIRED = 'retired' - CHOICES = ( + CHOICES = [ (STATUS_PLANNED, 'Planned'), (STATUS_STAGING, 'Staging'), (STATUS_ACTIVE, 'Active'), (STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_RETIRED, 'Retired'), - ) + ] CSS_CLASSES = { STATUS_PLANNED: 'info', @@ -67,6 +68,7 @@ class RackWidthChoices(ChoiceSet): class RackStatusChoices(ChoiceSet): + key = 'dcim.Rack.status' STATUS_RESERVED = 'reserved' STATUS_AVAILABLE = 'available' @@ -74,13 +76,13 @@ class RackStatusChoices(ChoiceSet): STATUS_ACTIVE = 'active' STATUS_DEPRECATED = 'deprecated' - CHOICES = ( + CHOICES = [ (STATUS_RESERVED, 'Reserved'), (STATUS_AVAILABLE, 'Available'), (STATUS_PLANNED, 'Planned'), (STATUS_ACTIVE, 'Active'), (STATUS_DEPRECATED, 'Deprecated'), - ) + ] CSS_CLASSES = { STATUS_RESERVED: 'warning', @@ -144,6 +146,7 @@ class DeviceFaceChoices(ChoiceSet): class DeviceStatusChoices(ChoiceSet): + key = 'dcim.Device.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active' @@ -153,7 +156,7 @@ class DeviceStatusChoices(ChoiceSet): STATUS_INVENTORY = 'inventory' STATUS_DECOMMISSIONING = 'decommissioning' - CHOICES = ( + CHOICES = [ (STATUS_OFFLINE, 'Offline'), (STATUS_ACTIVE, 'Active'), (STATUS_PLANNED, 'Planned'), @@ -161,7 +164,7 @@ class DeviceStatusChoices(ChoiceSet): (STATUS_FAILED, 'Failed'), (STATUS_INVENTORY, 'Inventory'), (STATUS_DECOMMISSIONING, 'Decommissioning'), - ) + ] CSS_CLASSES = { STATUS_OFFLINE: 'warning', @@ -1183,18 +1186,19 @@ class CableLengthUnitChoices(ChoiceSet): # class PowerFeedStatusChoices(ChoiceSet): + key = 'dcim.PowerFeed.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active' STATUS_PLANNED = 'planned' STATUS_FAILED = 'failed' - CHOICES = ( + CHOICES = [ (STATUS_OFFLINE, 'Offline'), (STATUS_ACTIVE, 'Active'), (STATUS_PLANNED, 'Planned'), (STATUS_FAILED, 'Failed'), - ) + ] CSS_CLASSES = { STATUS_OFFLINE: 'warning', diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index 638ef62f6..c414fc115 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -17,18 +17,19 @@ class IPAddressFamilyChoices(ChoiceSet): # class PrefixStatusChoices(ChoiceSet): + key = 'ipam.Prefix.status' STATUS_CONTAINER = 'container' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' STATUS_DEPRECATED = 'deprecated' - CHOICES = ( + CHOICES = [ (STATUS_CONTAINER, 'Container'), (STATUS_ACTIVE, 'Active'), (STATUS_RESERVED, 'Reserved'), (STATUS_DEPRECATED, 'Deprecated'), - ) + ] CSS_CLASSES = { STATUS_CONTAINER: 'secondary', @@ -43,16 +44,17 @@ class PrefixStatusChoices(ChoiceSet): # class IPRangeStatusChoices(ChoiceSet): + key = 'ipam.IPRange.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' STATUS_DEPRECATED = 'deprecated' - CHOICES = ( + CHOICES = [ (STATUS_ACTIVE, 'Active'), (STATUS_RESERVED, 'Reserved'), (STATUS_DEPRECATED, 'Deprecated'), - ) + ] CSS_CLASSES = { STATUS_ACTIVE: 'primary', @@ -66,6 +68,7 @@ class IPRangeStatusChoices(ChoiceSet): # class IPAddressStatusChoices(ChoiceSet): + key = 'ipam.IPAddress.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' @@ -73,13 +76,13 @@ class IPAddressStatusChoices(ChoiceSet): STATUS_DHCP = 'dhcp' STATUS_SLAAC = 'slaac' - CHOICES = ( + CHOICES = [ (STATUS_ACTIVE, 'Active'), (STATUS_RESERVED, 'Reserved'), (STATUS_DEPRECATED, 'Deprecated'), (STATUS_DHCP, 'DHCP'), (STATUS_SLAAC, 'SLAAC'), - ) + ] CSS_CLASSES = { STATUS_ACTIVE: 'primary', @@ -161,16 +164,17 @@ class FHRPGroupAuthTypeChoices(ChoiceSet): # class VLANStatusChoices(ChoiceSet): + key = 'ipam.VLAN.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' STATUS_DEPRECATED = 'deprecated' - CHOICES = ( + CHOICES = [ (STATUS_ACTIVE, 'Active'), (STATUS_RESERVED, 'Reserved'), (STATUS_DEPRECATED, 'Deprecated'), - ) + ] CSS_CLASSES = { STATUS_ACTIVE: 'primary', diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 9bc0dbc0c..9d956250b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -86,6 +86,7 @@ DEVELOPER = getattr(configuration, 'DEVELOPER', False) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) EMAIL = getattr(configuration, 'EMAIL', {}) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) +FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) LOGGING = getattr(configuration, 'LOGGING', {}) diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py index b831b3490..46d74490a 100644 --- a/netbox/utilities/choices.py +++ b/netbox/utilities/choices.py @@ -1,7 +1,21 @@ +from django.conf import settings + + class ChoiceSetMeta(type): """ Metaclass for ChoiceSet """ + def __new__(mcs, name, bases, attrs): + + # Extend static choices with any configured choices + if 'key' in attrs: + try: + attrs['CHOICES'].extend(settings.FIELD_CHOICES[attrs['key']]) + except KeyError: + pass + + return super().__new__(mcs, name, bases, attrs) + def __call__(cls, *args, **kwargs): # Django will check if a 'choices' value is callable, and if so assume that it returns an iterable return getattr(cls, 'CHOICES', ()) diff --git a/netbox/virtualization/choices.py b/netbox/virtualization/choices.py index 9c4eb6cd5..c121d052e 100644 --- a/netbox/virtualization/choices.py +++ b/netbox/virtualization/choices.py @@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet # class VirtualMachineStatusChoices(ChoiceSet): + key = 'virtualization.VirtualMachine.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active' @@ -14,14 +15,14 @@ class VirtualMachineStatusChoices(ChoiceSet): STATUS_FAILED = 'failed' STATUS_DECOMMISSIONING = 'decommissioning' - CHOICES = ( + CHOICES = [ (STATUS_OFFLINE, 'Offline'), (STATUS_ACTIVE, 'Active'), (STATUS_PLANNED, 'Planned'), (STATUS_STAGED, 'Staged'), (STATUS_FAILED, 'Failed'), (STATUS_DECOMMISSIONING, 'Decommissioning'), - ) + ] CSS_CLASSES = { STATUS_OFFLINE: 'warning',