Просмотр исходного кода

#8054: Support configurable status choices

jeremystretch 4 лет назад
Родитель
Сommit
419f86a4a5

+ 35 - 0
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
 ## HTTP_PROXIES
 
 
 Default: None
 Default: None

+ 3 - 2
netbox/circuits/choices.py

@@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
 #
 #
 
 
 class CircuitStatusChoices(ChoiceSet):
 class CircuitStatusChoices(ChoiceSet):
+    key = 'circuits.Circuit.status'
 
 
     STATUS_DEPROVISIONING = 'deprovisioning'
     STATUS_DEPROVISIONING = 'deprovisioning'
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
@@ -14,14 +15,14 @@ class CircuitStatusChoices(ChoiceSet):
     STATUS_OFFLINE = 'offline'
     STATUS_OFFLINE = 'offline'
     STATUS_DECOMMISSIONED = 'decommissioned'
     STATUS_DECOMMISSIONED = 'decommissioned'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PROVISIONING, 'Provisioning'),
         (STATUS_PROVISIONING, 'Provisioning'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_DEPROVISIONING, 'Deprovisioning'),
         (STATUS_DEPROVISIONING, 'Deprovisioning'),
         (STATUS_DECOMMISSIONED, 'Decommissioned'),
         (STATUS_DECOMMISSIONED, 'Decommissioned'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_DEPROVISIONING: 'warning',
         STATUS_DEPROVISIONING: 'warning',

+ 12 - 8
netbox/dcim/choices.py

@@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
 #
 #
 
 
 class SiteStatusChoices(ChoiceSet):
 class SiteStatusChoices(ChoiceSet):
+    key = 'dcim.Site.status'
 
 
     STATUS_PLANNED = 'planned'
     STATUS_PLANNED = 'planned'
     STATUS_STAGING = 'staging'
     STATUS_STAGING = 'staging'
@@ -13,13 +14,13 @@ class SiteStatusChoices(ChoiceSet):
     STATUS_DECOMMISSIONING = 'decommissioning'
     STATUS_DECOMMISSIONING = 'decommissioning'
     STATUS_RETIRED = 'retired'
     STATUS_RETIRED = 'retired'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_STAGING, 'Staging'),
         (STATUS_STAGING, 'Staging'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
         (STATUS_RETIRED, 'Retired'),
         (STATUS_RETIRED, 'Retired'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_PLANNED: 'info',
         STATUS_PLANNED: 'info',
@@ -67,6 +68,7 @@ class RackWidthChoices(ChoiceSet):
 
 
 
 
 class RackStatusChoices(ChoiceSet):
 class RackStatusChoices(ChoiceSet):
+    key = 'dcim.Rack.status'
 
 
     STATUS_RESERVED = 'reserved'
     STATUS_RESERVED = 'reserved'
     STATUS_AVAILABLE = 'available'
     STATUS_AVAILABLE = 'available'
@@ -74,13 +76,13 @@ class RackStatusChoices(ChoiceSet):
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_DEPRECATED = 'deprecated'
     STATUS_DEPRECATED = 'deprecated'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_AVAILABLE, 'Available'),
         (STATUS_AVAILABLE, 'Available'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DEPRECATED, 'Deprecated'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_RESERVED: 'warning',
         STATUS_RESERVED: 'warning',
@@ -144,6 +146,7 @@ class DeviceFaceChoices(ChoiceSet):
 
 
 
 
 class DeviceStatusChoices(ChoiceSet):
 class DeviceStatusChoices(ChoiceSet):
+    key = 'dcim.Device.status'
 
 
     STATUS_OFFLINE = 'offline'
     STATUS_OFFLINE = 'offline'
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
@@ -153,7 +156,7 @@ class DeviceStatusChoices(ChoiceSet):
     STATUS_INVENTORY = 'inventory'
     STATUS_INVENTORY = 'inventory'
     STATUS_DECOMMISSIONING = 'decommissioning'
     STATUS_DECOMMISSIONING = 'decommissioning'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
@@ -161,7 +164,7 @@ class DeviceStatusChoices(ChoiceSet):
         (STATUS_FAILED, 'Failed'),
         (STATUS_FAILED, 'Failed'),
         (STATUS_INVENTORY, 'Inventory'),
         (STATUS_INVENTORY, 'Inventory'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_OFFLINE: 'warning',
         STATUS_OFFLINE: 'warning',
@@ -1183,18 +1186,19 @@ class CableLengthUnitChoices(ChoiceSet):
 #
 #
 
 
 class PowerFeedStatusChoices(ChoiceSet):
 class PowerFeedStatusChoices(ChoiceSet):
+    key = 'dcim.PowerFeed.status'
 
 
     STATUS_OFFLINE = 'offline'
     STATUS_OFFLINE = 'offline'
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_PLANNED = 'planned'
     STATUS_PLANNED = 'planned'
     STATUS_FAILED = 'failed'
     STATUS_FAILED = 'failed'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_FAILED, 'Failed'),
         (STATUS_FAILED, 'Failed'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_OFFLINE: 'warning',
         STATUS_OFFLINE: 'warning',

+ 12 - 8
netbox/ipam/choices.py

@@ -17,18 +17,19 @@ class IPAddressFamilyChoices(ChoiceSet):
 #
 #
 
 
 class PrefixStatusChoices(ChoiceSet):
 class PrefixStatusChoices(ChoiceSet):
+    key = 'ipam.Prefix.status'
 
 
     STATUS_CONTAINER = 'container'
     STATUS_CONTAINER = 'container'
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_RESERVED = 'reserved'
     STATUS_RESERVED = 'reserved'
     STATUS_DEPRECATED = 'deprecated'
     STATUS_DEPRECATED = 'deprecated'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_CONTAINER, 'Container'),
         (STATUS_CONTAINER, 'Container'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DEPRECATED, 'Deprecated'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_CONTAINER: 'secondary',
         STATUS_CONTAINER: 'secondary',
@@ -43,16 +44,17 @@ class PrefixStatusChoices(ChoiceSet):
 #
 #
 
 
 class IPRangeStatusChoices(ChoiceSet):
 class IPRangeStatusChoices(ChoiceSet):
+    key = 'ipam.IPRange.status'
 
 
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_RESERVED = 'reserved'
     STATUS_RESERVED = 'reserved'
     STATUS_DEPRECATED = 'deprecated'
     STATUS_DEPRECATED = 'deprecated'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DEPRECATED, 'Deprecated'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_ACTIVE: 'primary',
         STATUS_ACTIVE: 'primary',
@@ -66,6 +68,7 @@ class IPRangeStatusChoices(ChoiceSet):
 #
 #
 
 
 class IPAddressStatusChoices(ChoiceSet):
 class IPAddressStatusChoices(ChoiceSet):
+    key = 'ipam.IPAddress.status'
 
 
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_RESERVED = 'reserved'
     STATUS_RESERVED = 'reserved'
@@ -73,13 +76,13 @@ class IPAddressStatusChoices(ChoiceSet):
     STATUS_DHCP = 'dhcp'
     STATUS_DHCP = 'dhcp'
     STATUS_SLAAC = 'slaac'
     STATUS_SLAAC = 'slaac'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DHCP, 'DHCP'),
         (STATUS_DHCP, 'DHCP'),
         (STATUS_SLAAC, 'SLAAC'),
         (STATUS_SLAAC, 'SLAAC'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_ACTIVE: 'primary',
         STATUS_ACTIVE: 'primary',
@@ -161,16 +164,17 @@ class FHRPGroupAuthTypeChoices(ChoiceSet):
 #
 #
 
 
 class VLANStatusChoices(ChoiceSet):
 class VLANStatusChoices(ChoiceSet):
+    key = 'ipam.VLAN.status'
 
 
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
     STATUS_RESERVED = 'reserved'
     STATUS_RESERVED = 'reserved'
     STATUS_DEPRECATED = 'deprecated'
     STATUS_DEPRECATED = 'deprecated'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_RESERVED, 'Reserved'),
         (STATUS_DEPRECATED, 'Deprecated'),
         (STATUS_DEPRECATED, 'Deprecated'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_ACTIVE: 'primary',
         STATUS_ACTIVE: 'primary',

+ 1 - 0
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'))
 DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
 EMAIL = getattr(configuration, 'EMAIL', {})
 EMAIL = getattr(configuration, 'EMAIL', {})
 EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
 EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
+FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
 HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
 HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
 INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
 INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
 LOGGING = getattr(configuration, 'LOGGING', {})
 LOGGING = getattr(configuration, 'LOGGING', {})

+ 14 - 0
netbox/utilities/choices.py

@@ -1,7 +1,21 @@
+from django.conf import settings
+
+
 class ChoiceSetMeta(type):
 class ChoiceSetMeta(type):
     """
     """
     Metaclass for ChoiceSet
     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):
     def __call__(cls, *args, **kwargs):
         # Django will check if a 'choices' value is callable, and if so assume that it returns an iterable
         # Django will check if a 'choices' value is callable, and if so assume that it returns an iterable
         return getattr(cls, 'CHOICES', ())
         return getattr(cls, 'CHOICES', ())

+ 3 - 2
netbox/virtualization/choices.py

@@ -6,6 +6,7 @@ from utilities.choices import ChoiceSet
 #
 #
 
 
 class VirtualMachineStatusChoices(ChoiceSet):
 class VirtualMachineStatusChoices(ChoiceSet):
+    key = 'virtualization.VirtualMachine.status'
 
 
     STATUS_OFFLINE = 'offline'
     STATUS_OFFLINE = 'offline'
     STATUS_ACTIVE = 'active'
     STATUS_ACTIVE = 'active'
@@ -14,14 +15,14 @@ class VirtualMachineStatusChoices(ChoiceSet):
     STATUS_FAILED = 'failed'
     STATUS_FAILED = 'failed'
     STATUS_DECOMMISSIONING = 'decommissioning'
     STATUS_DECOMMISSIONING = 'decommissioning'
 
 
-    CHOICES = (
+    CHOICES = [
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_OFFLINE, 'Offline'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_ACTIVE, 'Active'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_PLANNED, 'Planned'),
         (STATUS_STAGED, 'Staged'),
         (STATUS_STAGED, 'Staged'),
         (STATUS_FAILED, 'Failed'),
         (STATUS_FAILED, 'Failed'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
         (STATUS_DECOMMISSIONING, 'Decommissioning'),
-    )
+    ]
 
 
     CSS_CLASSES = {
     CSS_CLASSES = {
         STATUS_OFFLINE: 'warning',
         STATUS_OFFLINE: 'warning',