Merge pull request #8090 from netbox-community/8054-configurable-choice-fields

Closes #8054: Configurable choice fields
This commit is contained in:
Jeremy Stretch 2021-12-16 11:17:32 -05:00 committed by GitHub
commit 134742a8b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 197 additions and 249 deletions

View File

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

View File

@ -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,23 +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', 'info'),
(STATUS_PROVISIONING, 'Provisioning'), (STATUS_PROVISIONING, 'Provisioning', 'primary'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'success'),
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'danger'),
(STATUS_DEPROVISIONING, 'Deprovisioning'), (STATUS_DEPROVISIONING, 'Deprovisioning', 'warning'),
(STATUS_DECOMMISSIONED, 'Decommissioned'), (STATUS_DECOMMISSIONED, 'Decommissioned', 'secondary'),
) ]
CSS_CLASSES = {
STATUS_DEPROVISIONING: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_PROVISIONING: 'primary',
STATUS_OFFLINE: 'danger',
STATUS_DECOMMISSIONED: 'secondary',
}
# #

View File

@ -135,7 +135,7 @@ class Circuit(PrimaryModel):
return reverse('circuits:circuit', args=[self.pk]) return reverse('circuits:circuit', args=[self.pk])
def get_status_class(self): def get_status_class(self):
return CircuitStatusChoices.CSS_CLASSES.get(self.status) return CircuitStatusChoices.colors.get(self.status, 'secondary')
@extras_features('webhooks') @extras_features('webhooks')

View File

@ -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,21 +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', 'info'),
(STATUS_STAGING, 'Staging'), (STATUS_STAGING, 'Staging', 'primary'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'primary'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'warning'),
(STATUS_RETIRED, 'Retired'), (STATUS_RETIRED, 'Retired', 'danger'),
) ]
CSS_CLASSES = {
STATUS_PLANNED: 'info',
STATUS_STAGING: 'primary',
STATUS_ACTIVE: 'success',
STATUS_DECOMMISSIONING: 'warning',
STATUS_RETIRED: 'danger',
}
# #
@ -67,6 +60,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,21 +68,13 @@ class RackStatusChoices(ChoiceSet):
STATUS_ACTIVE = 'active' STATUS_ACTIVE = 'active'
STATUS_DEPRECATED = 'deprecated' STATUS_DEPRECATED = 'deprecated'
CHOICES = ( CHOICES = [
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'warning'),
(STATUS_AVAILABLE, 'Available'), (STATUS_AVAILABLE, 'Available', 'success'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'info'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'primary'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'danger'),
) ]
CSS_CLASSES = {
STATUS_RESERVED: 'warning',
STATUS_AVAILABLE: 'success',
STATUS_PLANNED: 'info',
STATUS_ACTIVE: 'primary',
STATUS_DEPRECATED: 'danger',
}
class RackDimensionUnitChoices(ChoiceSet): class RackDimensionUnitChoices(ChoiceSet):
@ -144,6 +130,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,25 +140,15 @@ class DeviceStatusChoices(ChoiceSet):
STATUS_INVENTORY = 'inventory' STATUS_INVENTORY = 'inventory'
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = ( CHOICES = [
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'warning'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'success'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'info'),
(STATUS_STAGED, 'Staged'), (STATUS_STAGED, 'Staged', 'primary'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed', 'danger'),
(STATUS_INVENTORY, 'Inventory'), (STATUS_INVENTORY, 'Inventory', 'secondary'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'warning'),
) ]
CSS_CLASSES = {
STATUS_OFFLINE: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_STAGED: 'primary',
STATUS_FAILED: 'danger',
STATUS_INVENTORY: 'secondary',
STATUS_DECOMMISSIONING: 'warning',
}
class DeviceAirflowChoices(ChoiceSet): class DeviceAirflowChoices(ChoiceSet):
@ -1144,17 +1121,11 @@ class LinkStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = ( CHOICES = (
(STATUS_CONNECTED, 'Connected'), (STATUS_CONNECTED, 'Connected', 'success'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'info'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'warning'),
) )
CSS_CLASSES = {
STATUS_CONNECTED: 'success',
STATUS_PLANNED: 'info',
STATUS_DECOMMISSIONING: 'warning',
}
class CableLengthUnitChoices(ChoiceSet): class CableLengthUnitChoices(ChoiceSet):
@ -1183,25 +1154,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', 'warning'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'success'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'info'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed', 'danger'),
) ]
CSS_CLASSES = {
STATUS_OFFLINE: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_FAILED: 'danger',
}
class PowerFeedTypeChoices(ChoiceSet): class PowerFeedTypeChoices(ChoiceSet):
@ -1210,15 +1175,10 @@ class PowerFeedTypeChoices(ChoiceSet):
TYPE_REDUNDANT = 'redundant' TYPE_REDUNDANT = 'redundant'
CHOICES = ( CHOICES = (
(TYPE_PRIMARY, 'Primary'), (TYPE_PRIMARY, 'Primary', 'success'),
(TYPE_REDUNDANT, 'Redundant'), (TYPE_REDUNDANT, 'Redundant', 'info'),
) )
CSS_CLASSES = {
TYPE_PRIMARY: 'success',
TYPE_REDUNDANT: 'info',
}
class PowerFeedSupplyChoices(ChoiceSet): class PowerFeedSupplyChoices(ChoiceSet):

View File

@ -289,7 +289,7 @@ class Cable(PrimaryModel):
self._pk = self.pk self._pk = self.pk
def get_status_class(self): def get_status_class(self):
return LinkStatusChoices.CSS_CLASSES.get(self.status) return LinkStatusChoices.colors.get(self.status)
def get_compatible_types(self): def get_compatible_types(self):
""" """

View File

@ -862,7 +862,7 @@ class Device(PrimaryModel, ConfigContextModel):
return Device.objects.filter(parent_bay__device=self.pk) return Device.objects.filter(parent_bay__device=self.pk)
def get_status_class(self): def get_status_class(self):
return DeviceStatusChoices.CSS_CLASSES.get(self.status) return DeviceStatusChoices.colors.get(self.status, 'secondary')
# #

View File

@ -174,7 +174,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
return self.power_panel return self.power_panel
def get_type_class(self): def get_type_class(self):
return PowerFeedTypeChoices.CSS_CLASSES.get(self.type) return PowerFeedTypeChoices.colors.get(self.type)
def get_status_class(self): def get_status_class(self):
return PowerFeedStatusChoices.CSS_CLASSES.get(self.status) return PowerFeedStatusChoices.colors.get(self.status, 'secondary')

View File

@ -251,7 +251,7 @@ class Rack(PrimaryModel):
return reversed(range(1, self.u_height + 1)) return reversed(range(1, self.u_height + 1))
def get_status_class(self): def get_status_class(self):
return RackStatusChoices.CSS_CLASSES.get(self.status) return RackStatusChoices.colors.get(self.status, 'secondary')
def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True):
""" """

View File

@ -315,7 +315,7 @@ class Site(PrimaryModel):
return reverse('dcim:site', args=[self.pk]) return reverse('dcim:site', args=[self.pk])
def get_status_class(self): def get_status_class(self):
return SiteStatusChoices.CSS_CLASSES.get(self.status) return SiteStatusChoices.colors.get(self.status, 'secondary')
# #

View File

@ -91,17 +91,11 @@ class ObjectChangeActionChoices(ChoiceSet):
ACTION_DELETE = 'delete' ACTION_DELETE = 'delete'
CHOICES = ( CHOICES = (
(ACTION_CREATE, 'Created'), (ACTION_CREATE, 'Created', 'success'),
(ACTION_UPDATE, 'Updated'), (ACTION_UPDATE, 'Updated', 'primary'),
(ACTION_DELETE, 'Deleted'), (ACTION_DELETE, 'Deleted', 'danger'),
) )
CSS_CLASSES = {
ACTION_CREATE: 'success',
ACTION_UPDATE: 'primary',
ACTION_DELETE: 'danger',
}
# #
# Jounral entries # Jounral entries
@ -115,19 +109,12 @@ class JournalEntryKindChoices(ChoiceSet):
KIND_DANGER = 'danger' KIND_DANGER = 'danger'
CHOICES = ( CHOICES = (
(KIND_INFO, 'Info'), (KIND_INFO, 'Info', 'info'),
(KIND_SUCCESS, 'Success'), (KIND_SUCCESS, 'Success', 'success'),
(KIND_WARNING, 'Warning'), (KIND_WARNING, 'Warning', 'warning'),
(KIND_DANGER, 'Danger'), (KIND_DANGER, 'Danger', 'danger'),
) )
CSS_CLASSES = {
KIND_INFO: 'info',
KIND_SUCCESS: 'success',
KIND_WARNING: 'warning',
KIND_DANGER: 'danger',
}
# #
# Log Levels for Reports and Scripts # Log Levels for Reports and Scripts
@ -142,21 +129,13 @@ class LogLevelChoices(ChoiceSet):
LOG_FAILURE = 'failure' LOG_FAILURE = 'failure'
CHOICES = ( CHOICES = (
(LOG_DEFAULT, 'Default'), (LOG_DEFAULT, 'Default', 'secondary'),
(LOG_SUCCESS, 'Success'), (LOG_SUCCESS, 'Success', 'success'),
(LOG_INFO, 'Info'), (LOG_INFO, 'Info', 'info'),
(LOG_WARNING, 'Warning'), (LOG_WARNING, 'Warning', 'warning'),
(LOG_FAILURE, 'Failure'), (LOG_FAILURE, 'Failure', 'danger'),
) )
CSS_CLASSES = {
LOG_DEFAULT: 'secondary',
LOG_SUCCESS: 'success',
LOG_INFO: 'info',
LOG_WARNING: 'warning',
LOG_FAILURE: 'danger',
}
# #
# Job results # Job results

View File

@ -105,4 +105,4 @@ class ObjectChange(BigIDModel):
return reverse('extras:objectchange', args=[self.pk]) return reverse('extras:objectchange', args=[self.pk])
def get_action_class(self): def get_action_class(self):
return ObjectChangeActionChoices.CSS_CLASSES.get(self.action) return ObjectChangeActionChoices.colors.get(self.action)

View File

@ -440,7 +440,7 @@ class JournalEntry(ChangeLoggedModel):
return reverse('extras:journalentry', args=[self.pk]) return reverse('extras:journalentry', args=[self.pk])
def get_kind_class(self): def get_kind_class(self):
return JournalEntryKindChoices.CSS_CLASSES.get(self.kind) return JournalEntryKindChoices.colors.get(self.kind)
class JobResult(BigIDModel): class JobResult(BigIDModel):

View File

@ -167,8 +167,8 @@ class Report(object):
""" """
Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below. Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below.
""" """
if level not in LogLevelChoices.as_dict(): if level not in LogLevelChoices.values():
raise Exception("Unknown logging level: {}".format(level)) raise Exception(f"Unknown logging level: {level}")
self._results[self.active_test]['log'].append(( self._results[self.active_test]['log'].append((
timezone.now().isoformat(), timezone.now().isoformat(),
level, level,

View File

@ -12,6 +12,6 @@ def log_level(level):
Display a label indicating a syslog severity (e.g. info, warning, etc.). Display a label indicating a syslog severity (e.g. info, warning, etc.).
""" """
return { return {
'name': LogLevelChoices.as_dict()[level], 'name': dict(LogLevelChoices)[level],
'class': LogLevelChoices.CSS_CLASSES.get(level) 'class': LogLevelChoices.colors.get(level)
} }

View File

@ -17,25 +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', 'secondary'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'primary'),
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'info'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'danger'),
) ]
CSS_CLASSES = {
STATUS_CONTAINER: 'secondary',
STATUS_ACTIVE: 'primary',
STATUS_RESERVED: 'info',
STATUS_DEPRECATED: 'danger',
}
# #
@ -43,22 +37,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', 'primary'),
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'info'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'danger'),
) ]
CSS_CLASSES = {
STATUS_ACTIVE: 'primary',
STATUS_RESERVED: 'info',
STATUS_DEPRECATED: 'danger',
}
# #
@ -66,6 +55,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,21 +63,13 @@ class IPAddressStatusChoices(ChoiceSet):
STATUS_DHCP = 'dhcp' STATUS_DHCP = 'dhcp'
STATUS_SLAAC = 'slaac' STATUS_SLAAC = 'slaac'
CHOICES = ( CHOICES = [
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'primary'),
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'info'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'danger'),
(STATUS_DHCP, 'DHCP'), (STATUS_DHCP, 'DHCP', 'success'),
(STATUS_SLAAC, 'SLAAC'), (STATUS_SLAAC, 'SLAAC', 'success'),
) ]
CSS_CLASSES = {
STATUS_ACTIVE: 'primary',
STATUS_RESERVED: 'info',
STATUS_DEPRECATED: 'danger',
STATUS_DHCP: 'success',
STATUS_SLAAC: 'success',
}
class IPAddressRoleChoices(ChoiceSet): class IPAddressRoleChoices(ChoiceSet):
@ -102,27 +84,16 @@ class IPAddressRoleChoices(ChoiceSet):
ROLE_CARP = 'carp' ROLE_CARP = 'carp'
CHOICES = ( CHOICES = (
(ROLE_LOOPBACK, 'Loopback'), (ROLE_LOOPBACK, 'Loopback', 'secondary'),
(ROLE_SECONDARY, 'Secondary'), (ROLE_SECONDARY, 'Secondary', 'primary'),
(ROLE_ANYCAST, 'Anycast'), (ROLE_ANYCAST, 'Anycast', 'warning'),
(ROLE_VIP, 'VIP'), (ROLE_VIP, 'VIP'),
(ROLE_VRRP, 'VRRP'), (ROLE_VRRP, 'VRRP', 'success'),
(ROLE_HSRP, 'HSRP'), (ROLE_HSRP, 'HSRP', 'success'),
(ROLE_GLBP, 'GLBP'), (ROLE_GLBP, 'GLBP', 'success'),
(ROLE_CARP, 'CARP'), (ROLE_CARP, 'CARP'), 'success',
) )
CSS_CLASSES = {
ROLE_LOOPBACK: 'secondary',
ROLE_SECONDARY: 'primary',
ROLE_ANYCAST: 'warning',
ROLE_VIP: 'success',
ROLE_VRRP: 'success',
ROLE_HSRP: 'success',
ROLE_GLBP: 'success',
ROLE_CARP: 'success',
}
# #
# FHRP # FHRP
@ -161,22 +132,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', 'primary'),
(STATUS_RESERVED, 'Reserved'), (STATUS_RESERVED, 'Reserved', 'info'),
(STATUS_DEPRECATED, 'Deprecated'), (STATUS_DEPRECATED, 'Deprecated', 'danger'),
) ]
CSS_CLASSES = {
STATUS_ACTIVE: 'primary',
STATUS_RESERVED: 'info',
STATUS_DEPRECATED: 'danger',
}
# #

View File

@ -403,7 +403,7 @@ class Prefix(PrimaryModel):
prefix_length = property(fset=_set_prefix_length) prefix_length = property(fset=_set_prefix_length)
def get_status_class(self): def get_status_class(self):
return PrefixStatusChoices.CSS_CLASSES.get(self.status) return PrefixStatusChoices.colors.get(self.status, 'secondary')
def get_parents(self, include_self=False): def get_parents(self, include_self=False):
""" """
@ -692,7 +692,7 @@ class IPRange(PrimaryModel):
prefix_length = property(fset=_set_prefix_length) prefix_length = property(fset=_set_prefix_length)
def get_status_class(self): def get_status_class(self):
return IPRangeStatusChoices.CSS_CLASSES.get(self.status) return IPRangeStatusChoices.colors.get(self.status, 'secondary')
def get_child_ips(self): def get_child_ips(self):
""" """
@ -909,7 +909,7 @@ class IPAddress(PrimaryModel):
mask_length = property(fset=_set_mask_length) mask_length = property(fset=_set_mask_length)
def get_status_class(self): def get_status_class(self):
return IPAddressStatusChoices.CSS_CLASSES.get(self.status) return IPAddressStatusChoices.colors.get(self.status, 'secondary')
def get_role_class(self): def get_role_class(self):
return IPAddressRoleChoices.CSS_CLASSES.get(self.role) return IPAddressRoleChoices.colors.get(self.role)

View File

@ -173,7 +173,7 @@ class VLAN(PrimaryModel):
}) })
def get_status_class(self): def get_status_class(self):
return VLANStatusChoices.CSS_CLASSES.get(self.status) return VLANStatusChoices.colors.get(self.status, 'secondary')
def get_interfaces(self): def get_interfaces(self):
# Return all device interfaces assigned to this VLAN # Return all device interfaces assigned to this VLAN

View File

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

View File

@ -1,28 +1,56 @@
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
key = attrs.get('key')
if key:
try:
attrs['CHOICES'].extend(settings.FIELD_CHOICES[key])
except KeyError:
pass
# Define choice tuples and color maps
attrs['_choices'] = []
attrs['colors'] = {}
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]
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]
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-filters 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', ())
def __iter__(cls): def __iter__(cls):
choices = getattr(cls, 'CHOICES', ()) return iter(getattr(cls, '_choices', ()))
return iter(choices)
class ChoiceSet(metaclass=ChoiceSetMeta): class ChoiceSet(metaclass=ChoiceSetMeta):
"""
Holds an interable 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.
"""
CHOICES = list() CHOICES = list()
@classmethod @classmethod
def values(cls): def values(cls):
return [c[0] for c in unpack_grouped_choices(cls.CHOICES)] return [c[0] for c in unpack_grouped_choices(cls._choices)]
@classmethod
def as_dict(cls):
# Unpack grouped choices before casting as a dict
return dict(unpack_grouped_choices(cls.CHOICES))
def unpack_grouped_choices(choices): def unpack_grouped_choices(choices):

View File

@ -30,8 +30,3 @@ class ChoiceSetTestCase(TestCase):
def test_values(self): def test_values(self):
self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3]) self.assertListEqual(ExampleChoices.values(), ['a', 'b', 'c', 1, 2, 3])
def test_as_dict(self):
self.assertEqual(ExampleChoices.as_dict(), {
'a': 'A', 'b': 'B', 'c': 'C', 1: 'One', 2: 'Two', 3: 'Three'
})

View File

@ -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,20 +15,11 @@ class VirtualMachineStatusChoices(ChoiceSet):
STATUS_FAILED = 'failed' STATUS_FAILED = 'failed'
STATUS_DECOMMISSIONING = 'decommissioning' STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = ( CHOICES = [
(STATUS_OFFLINE, 'Offline'), (STATUS_OFFLINE, 'Offline', 'warning'),
(STATUS_ACTIVE, 'Active'), (STATUS_ACTIVE, 'Active', 'success'),
(STATUS_PLANNED, 'Planned'), (STATUS_PLANNED, 'Planned', 'info'),
(STATUS_STAGED, 'Staged'), (STATUS_STAGED, 'Staged', 'primary'),
(STATUS_FAILED, 'Failed'), (STATUS_FAILED, 'Failed', 'danger'),
(STATUS_DECOMMISSIONING, 'Decommissioning'), (STATUS_DECOMMISSIONING, 'Decommissioning', 'warning'),
) ]
CSS_CLASSES = {
STATUS_OFFLINE: 'warning',
STATUS_ACTIVE: 'success',
STATUS_PLANNED: 'info',
STATUS_STAGED: 'primary',
STATUS_FAILED: 'danger',
STATUS_DECOMMISSIONING: 'warning',
}

View File

@ -329,7 +329,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
}) })
def get_status_class(self): def get_status_class(self):
return VirtualMachineStatusChoices.CSS_CLASSES.get(self.status) return VirtualMachineStatusChoices.colors.get(self.status, 'secondary')
@property @property
def primary_ip(self): def primary_ip(self):

View File

@ -182,7 +182,7 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
return reverse('wireless:wirelesslink', args=[self.pk]) return reverse('wireless:wirelesslink', args=[self.pk])
def get_status_class(self): def get_status_class(self):
return LinkStatusChoices.CSS_CLASSES.get(self.status) return LinkStatusChoices.colors.get(self.status)
def clean(self): def clean(self):