From 5f8af6ad6683e43e7b06c92ba53b633fff1f0069 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 2 Mar 2022 11:43:28 -0500 Subject: [PATCH] Closes #8779: Enable the use of ChoiceSet by plugins --- docs/plugins/development/models.md | 67 ++++++++++++++++++++++++++++++ netbox/circuits/choices.py | 2 +- netbox/dcim/choices.py | 8 ++-- netbox/ipam/choices.py | 8 ++-- netbox/utilities/choices.py | 18 ++++---- netbox/virtualization/choices.py | 2 +- 6 files changed, 87 insertions(+), 18 deletions(-) diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md index 521420b1b..8c6b051a7 100644 --- a/docs/plugins/development/models.md +++ b/docs/plugins/development/models.md @@ -109,3 +109,70 @@ The example above will enable export templates and tags, but no other NetBox fea ::: netbox.models.features.TagsMixin ::: netbox.models.features.WebhooksMixin + +## Choice Sets + +For model fields which support the selection of one or more values from a predefined list of choices, NetBox provides the `ChoiceSet` utility class. This can be used in place of a regular choices tuple to provide enhanced functionality, namely dynamic configuration and colorization. + +To define choices for a model field, subclass `ChoiceSet` and define a tuple named `CHOICES`, of which each member is a two- or three-element tuple. These elements are: + +* The database value +* The corresponding human-friendly label +* The assigned color (optional) + +!!! note + Authors may find it useful to declare each of the database values as constants on the class, and reference them within `CHOICES` members. This convention allows the values to be referenced from outside the class, however it is not strictly required. + +### Dynamic Configuration + +To enable dynamic configuration for a ChoiceSet subclass, define its `key` as a string specifying the model and field name to which it applies. For example: + +```python +from utilities.choices import ChoiceSet + +class StatusChoices(ChoiceSet): + key = 'MyModel.status' +``` + +To extend or replace the default values for this choice set, a NetBox administrator can then reference it under the [`FIELD_CHOICES`](../../configuration/optional-settings.md#field_choices) configuration parameter. For example, the `status` field on `MyModel` in `my_plugin` would be referenced as: + +```python +FIELD_CHOICES = { + 'my_plugin.MyModel.status': ( + # Custom choices + ) +} +``` + +### Example + +```python +# choices.py +from utilities.choices import ChoiceSet + +class StatusChoices(ChoiceSet): + key = 'MyModel.status' + + STATUS_FOO = 'foo' + STATUS_BAR = 'bar' + STATUS_BAZ = 'baz' + + CHOICES = ( + (STATUS_FOO, 'Foo', 'red'), + (STATUS_BAR, 'Bar', 'green'), + (STATUS_BAZ, 'Baz', 'blue'), + ) +``` + +```python +# models.py +from django.db import models +from .choices import StatusChoices + +class MyModel(models.Model): + status = models.CharField( + max_length=50, + choices=StatusChoices, + default=StatusChoices.STATUS_FOO + ) +``` diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py index fd58abb97..ddb00c64b 100644 --- a/netbox/circuits/choices.py +++ b/netbox/circuits/choices.py @@ -6,7 +6,7 @@ from utilities.choices import ChoiceSet # class CircuitStatusChoices(ChoiceSet): - key = 'circuits.Circuit.status' + key = 'Circuit.status' STATUS_DEPROVISIONING = 'deprovisioning' STATUS_ACTIVE = 'active' diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2706c684d..f8000d53d 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -6,7 +6,7 @@ from utilities.choices import ChoiceSet # class SiteStatusChoices(ChoiceSet): - key = 'dcim.Site.status' + key = 'Site.status' STATUS_PLANNED = 'planned' STATUS_STAGING = 'staging' @@ -60,7 +60,7 @@ class RackWidthChoices(ChoiceSet): class RackStatusChoices(ChoiceSet): - key = 'dcim.Rack.status' + key = 'Rack.status' STATUS_RESERVED = 'reserved' STATUS_AVAILABLE = 'available' @@ -130,7 +130,7 @@ class DeviceFaceChoices(ChoiceSet): class DeviceStatusChoices(ChoiceSet): - key = 'dcim.Device.status' + key = 'Device.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active' @@ -1175,7 +1175,7 @@ class CableLengthUnitChoices(ChoiceSet): # class PowerFeedStatusChoices(ChoiceSet): - key = 'dcim.PowerFeed.status' + key = 'PowerFeed.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active' diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py index f9e4afffb..9d6e5e1e3 100644 --- a/netbox/ipam/choices.py +++ b/netbox/ipam/choices.py @@ -17,7 +17,7 @@ class IPAddressFamilyChoices(ChoiceSet): # class PrefixStatusChoices(ChoiceSet): - key = 'ipam.Prefix.status' + key = 'Prefix.status' STATUS_CONTAINER = 'container' STATUS_ACTIVE = 'active' @@ -37,7 +37,7 @@ class PrefixStatusChoices(ChoiceSet): # class IPRangeStatusChoices(ChoiceSet): - key = 'ipam.IPRange.status' + key = 'IPRange.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' @@ -55,7 +55,7 @@ class IPRangeStatusChoices(ChoiceSet): # class IPAddressStatusChoices(ChoiceSet): - key = 'ipam.IPAddress.status' + key = 'IPAddress.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' @@ -134,7 +134,7 @@ class FHRPGroupAuthTypeChoices(ChoiceSet): # class VLANStatusChoices(ChoiceSet): - key = 'ipam.VLAN.status' + key = 'VLAN.status' STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py index 415c11ddf..62f6837ec 100644 --- a/netbox/utilities/choices.py +++ b/netbox/utilities/choices.py @@ -8,14 +8,16 @@ class ChoiceSetMeta(type): def __new__(mcs, name, bases, attrs): # Extend static choices with any configured choices - replace_key = attrs.get('key') - extend_key = f'{replace_key}+' if replace_key else None - if replace_key and replace_key in settings.FIELD_CHOICES: - # Replace the stock choices - attrs['CHOICES'] = settings.FIELD_CHOICES[replace_key] - elif extend_key and extend_key in settings.FIELD_CHOICES: - # Extend the stock choices - attrs['CHOICES'].extend(settings.FIELD_CHOICES[extend_key]) + if key := attrs.get('key'): + app = attrs['__module__'].split('.', 1)[0] + replace_key = f'{app}.{key}' + extend_key = f'{replace_key}+' if replace_key else None + if replace_key and replace_key in settings.FIELD_CHOICES: + # Replace the stock choices + attrs['CHOICES'] = settings.FIELD_CHOICES[replace_key] + elif extend_key and extend_key in settings.FIELD_CHOICES: + # Extend the stock choices + attrs['CHOICES'].extend(settings.FIELD_CHOICES[extend_key]) # Define choice tuples and color maps attrs['_choices'] = [] diff --git a/netbox/virtualization/choices.py b/netbox/virtualization/choices.py index bb9220d09..693e53df6 100644 --- a/netbox/virtualization/choices.py +++ b/netbox/virtualization/choices.py @@ -6,7 +6,7 @@ from utilities.choices import ChoiceSet # class VirtualMachineStatusChoices(ChoiceSet): - key = 'virtualization.VirtualMachine.status' + key = 'VirtualMachine.status' STATUS_OFFLINE = 'offline' STATUS_ACTIVE = 'active'