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

Merge branch 'develop' into develop-2.9

Jeremy Stretch 5 лет назад
Родитель
Сommit
28a14cf5ae

+ 3 - 4
.github/ISSUE_TEMPLATE/bug_report.md

@@ -30,10 +30,9 @@ about: Report a reproducible bug in the current release of NetBox
     library such as pynetbox.
     library such as pynetbox.
 -->
 -->
 ### Steps to Reproduce
 ### Steps to Reproduce
-1. Disable any installed plugins by commenting out the `PLUGINS` setting in
-   `configuration.py`.
-2.
-3.
+1. 
+2. 
+3. 
 
 
 <!-- What did you expect to happen? -->
 <!-- What did you expect to happen? -->
 ### Expected Behavior
 ### Expected Behavior

+ 17 - 2
docs/configuration/optional-settings.md

@@ -86,7 +86,12 @@ CORS_ORIGIN_WHITELIST = [
 
 
 Default: False
 Default: False
 
 
-This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
+This setting enables debugging. This should be done only during development or troubleshooting. Note that only clients
+which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
+interface.
+
+!!! warning
+    Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users.
 
 
 ---
 ---
 
 
@@ -184,6 +189,16 @@ HTTP_PROXIES = {
 
 
 ---
 ---
 
 
+## INTERNAL_IPS
+
+Default: `('127.0.0.1', '::1',)`
+
+A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
+example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
+addresses (and [`DEBUG`](#debug) is true).
+
+---
+
 ## LOGGING
 ## LOGGING
 
 
 By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
 By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
@@ -385,7 +400,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
 
 
 ## REMOTE_AUTH_AUTO_CREATE_USER
 ## REMOTE_AUTH_AUTO_CREATE_USER
 
 
-Default: `True`
+Default: `False`
 
 
 If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
 If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
 
 

+ 13 - 2
docs/release-notes/version-2.8.md

@@ -1,21 +1,32 @@
 # NetBox v2.8
 # NetBox v2.8
 
 
-v2.8.5 (FUTURE)
+## v2.8.5 (2020-05-26)
 
 
 **Note:** The minimum required version of PostgreSQL is now 9.6.
 **Note:** The minimum required version of PostgreSQL is now 9.6.
 
 
+### Enhancements
+
+* [#4650](https://github.com/netbox-community/netbox/issues/4650) - Expose `INTERNAL_IPS` configuration parameter
+* [#4651](https://github.com/netbox-community/netbox/issues/4651) - Add `csrf_token` context for plugin templates
+* [#4652](https://github.com/netbox-community/netbox/issues/4652) - Add permissions context for plugin templates
+* [#4665](https://github.com/netbox-community/netbox/issues/4665) - Add NEMA L14 and L21 power port/outlet types
+* [#4672](https://github.com/netbox-community/netbox/issues/4672) - Set default color for rack and devices roles
+
 ### Bug Fixes
 ### Bug Fixes
 
 
 * [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
 * [#3304](https://github.com/netbox-community/netbox/issues/3304) - Fix caching invalidation issue related to device/virtual machine primary IP addresses
+* [#4525](https://github.com/netbox-community/netbox/issues/4525) - Allow passing initial data to custom script MultiObjectVar
 * [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
 * [#4644](https://github.com/netbox-community/netbox/issues/4644) - Fix ordering of services table by parent
 * [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
 * [#4646](https://github.com/netbox-community/netbox/issues/4646) - Correct UI link for reports with custom name
 * [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
 * [#4647](https://github.com/netbox-community/netbox/issues/4647) - Fix caching invalidation issue related to assigning new IP addresses to interfaces
 * [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
 * [#4648](https://github.com/netbox-community/netbox/issues/4648) - Fix bulk CSV import of child devices
 * [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
 * [#4649](https://github.com/netbox-community/netbox/issues/4649) - Fix interface assignment for bulk-imported IP addresses
+* [#4676](https://github.com/netbox-community/netbox/issues/4676) - Set default value of `REMOTE_AUTH_AUTO_CREATE_USER` as `False` in docs
+* [#4684](https://github.com/netbox-community/netbox/issues/4684) - Respect `comments` field when importing device type in YAML/JSON format
 
 
 ---
 ---
 
 
-v2.8.4 (2020-05-13)
+## v2.8.4 (2020-05-13)
 
 
 ### Enhancements
 ### Enhancements
 
 

+ 16 - 0
netbox/dcim/choices.py

@@ -276,6 +276,10 @@ class PowerPortTypeChoices(ChoiceSet):
     TYPE_NEMA_L620P = 'nema-l6-20p'
     TYPE_NEMA_L620P = 'nema-l6-20p'
     TYPE_NEMA_L630P = 'nema-l6-30p'
     TYPE_NEMA_L630P = 'nema-l6-30p'
     TYPE_NEMA_L650P = 'nema-l6-50p'
     TYPE_NEMA_L650P = 'nema-l6-50p'
+    TYPE_NEMA_L1420P = 'nema-l14-20p'
+    TYPE_NEMA_L1430P = 'nema-l14-30p'
+    TYPE_NEMA_L2120P = 'nema-l21-20p'
+    TYPE_NEMA_L2130P = 'nema-l21-30p'
     # California style
     # California style
     TYPE_CS6361C = 'cs6361c'
     TYPE_CS6361C = 'cs6361c'
     TYPE_CS6365C = 'cs6365c'
     TYPE_CS6365C = 'cs6365c'
@@ -337,6 +341,10 @@ class PowerPortTypeChoices(ChoiceSet):
             (TYPE_NEMA_L620P, 'NEMA L6-20P'),
             (TYPE_NEMA_L620P, 'NEMA L6-20P'),
             (TYPE_NEMA_L630P, 'NEMA L6-30P'),
             (TYPE_NEMA_L630P, 'NEMA L6-30P'),
             (TYPE_NEMA_L650P, 'NEMA L6-50P'),
             (TYPE_NEMA_L650P, 'NEMA L6-50P'),
+            (TYPE_NEMA_L1420P, 'NEMA L14-20P'),
+            (TYPE_NEMA_L1430P, 'NEMA L14-30P'),
+            (TYPE_NEMA_L2120P, 'NEMA L21-20P'),
+            (TYPE_NEMA_L2130P, 'NEMA L21-30P'),
         )),
         )),
         ('California Style', (
         ('California Style', (
             (TYPE_CS6361C, 'CS6361C'),
             (TYPE_CS6361C, 'CS6361C'),
@@ -405,6 +413,10 @@ class PowerOutletTypeChoices(ChoiceSet):
     TYPE_NEMA_L620R = 'nema-l6-20r'
     TYPE_NEMA_L620R = 'nema-l6-20r'
     TYPE_NEMA_L630R = 'nema-l6-30r'
     TYPE_NEMA_L630R = 'nema-l6-30r'
     TYPE_NEMA_L650R = 'nema-l6-50r'
     TYPE_NEMA_L650R = 'nema-l6-50r'
+    TYPE_NEMA_L1420R = 'nema-l14-20r'
+    TYPE_NEMA_L1430R = 'nema-l14-30r'
+    TYPE_NEMA_L2120R = 'nema-l21-20r'
+    TYPE_NEMA_L2130R = 'nema-l21-30r'
     # California style
     # California style
     TYPE_CS6360C = 'CS6360C'
     TYPE_CS6360C = 'CS6360C'
     TYPE_CS6364C = 'CS6364C'
     TYPE_CS6364C = 'CS6364C'
@@ -467,6 +479,10 @@ class PowerOutletTypeChoices(ChoiceSet):
             (TYPE_NEMA_L620R, 'NEMA L6-20R'),
             (TYPE_NEMA_L620R, 'NEMA L6-20R'),
             (TYPE_NEMA_L630R, 'NEMA L6-30R'),
             (TYPE_NEMA_L630R, 'NEMA L6-30R'),
             (TYPE_NEMA_L650R, 'NEMA L6-50R'),
             (TYPE_NEMA_L650R, 'NEMA L6-50R'),
+            (TYPE_NEMA_L1420R, 'NEMA L14-20R'),
+            (TYPE_NEMA_L1430R, 'NEMA L14-30R'),
+            (TYPE_NEMA_L2120R, 'NEMA L21-20R'),
+            (TYPE_NEMA_L2130R, 'NEMA L21-30R'),
         )),
         )),
         ('California Style', (
         ('California Style', (
             (TYPE_CS6360C, 'CS6360C'),
             (TYPE_CS6360C, 'CS6360C'),

+ 2 - 2
netbox/dcim/filters.py

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
 from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
-from utilities.constants import COLOR_CHOICES
+from utilities.choices import ColorChoices
 from utilities.filters import (
 from utilities.filters import (
     BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
     BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
     NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
     NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
@@ -1084,7 +1084,7 @@ class CableFilterSet(BaseFilterSet):
         choices=CableStatusChoices
         choices=CableStatusChoices
     )
     )
     color = django_filters.MultipleChoiceFilter(
     color = django_filters.MultipleChoiceFilter(
-        choices=COLOR_CHOICES
+        choices=ColorChoices
     )
     )
     device_id = MultiValueNumberFilter(
     device_id = MultiValueNumberFilter(
         method='filter_device'
         method='filter_device'

+ 1 - 0
netbox/dcim/forms.py

@@ -932,6 +932,7 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
         model = DeviceType
         model = DeviceType
         fields = [
         fields = [
             'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
             'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
+            'comments',
         ]
         ]
 
 
 
 

+ 24 - 0
netbox/dcim/migrations/0106_role_default_color.py

@@ -0,0 +1,24 @@
+# Generated by Django 3.0.6 on 2020-05-26 13:33
+
+from django.db import migrations
+import utilities.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0105_interface_name_collation'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='devicerole',
+            name='color',
+            field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
+        ),
+        migrations.AlterField(
+            model_name='rackrole',
+            name='color',
+            field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
+        ),
+    ]

+ 7 - 2
netbox/dcim/models/__init__.py

@@ -23,6 +23,7 @@ from dcim.fields import ASNField
 from dcim.elevations import RackElevationSVG
 from dcim.elevations import RackElevationSVG
 from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
 from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
 from extras.utils import extras_features
 from extras.utils import extras_features
+from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 from utilities.utils import serialize_object, to_meters
 from utilities.utils import serialize_object, to_meters
@@ -379,7 +380,9 @@ class RackRole(ChangeLoggedModel):
     slug = models.SlugField(
     slug = models.SlugField(
         unique=True
         unique=True
     )
     )
-    color = ColorField()
+    color = ColorField(
+        default=ColorChoices.COLOR_GREY
+    )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,
         blank=True,
         blank=True,
@@ -1190,7 +1193,9 @@ class DeviceRole(ChangeLoggedModel):
     slug = models.SlugField(
     slug = models.SlugField(
         unique=True
         unique=True
     )
     )
-    color = ColorField()
+    color = ColorField(
+        default=ColorChoices.COLOR_GREY
+    )
     vm_role = models.BooleanField(
     vm_role = models.BooleanField(
         default=True,
         default=True,
         verbose_name='VM Role',
         verbose_name='VM Role',

+ 2 - 0
netbox/dcim/tests/test_views.py

@@ -366,6 +366,7 @@ manufacturer: Generic
 model: TEST-1000
 model: TEST-1000
 slug: test-1000
 slug: test-1000
 u_height: 2
 u_height: 2
+comments: test comment
 console-ports:
 console-ports:
   - name: Console Port 1
   - name: Console Port 1
     type: de-9
     type: de-9
@@ -456,6 +457,7 @@ device-bays:
         self.assertHttpStatus(response, 200)
         self.assertHttpStatus(response, 200)
 
 
         dt = DeviceType.objects.get(model='TEST-1000')
         dt = DeviceType.objects.get(model='TEST-1000')
+        self.assertEqual(dt.comments, 'test comment')
 
 
         # Verify all of the components were created
         # Verify all of the components were created
         self.assertEqual(dt.consoleport_templates.count(), 3)
         self.assertEqual(dt.consoleport_templates.count(), 3)

+ 3 - 3
netbox/extras/forms.py

@@ -432,11 +432,11 @@ class ScriptForm(BootstrapMixin, forms.Form):
 
 
     def __init__(self, vars, *args, commit_default=True, **kwargs):
     def __init__(self, vars, *args, commit_default=True, **kwargs):
 
 
-        super().__init__(*args, **kwargs)
-
         # Dynamically populate fields for variables
         # Dynamically populate fields for variables
         for name, var in vars.items():
         for name, var in vars.items():
-            self.fields[name] = var.as_field()
+            self.base_fields[name] = var.as_field()
+
+        super().__init__(*args, **kwargs)
 
 
         # Toggle default commit behavior based on Meta option
         # Toggle default commit behavior based on Meta option
         if not commit_default:
         if not commit_default:

+ 2 - 1
netbox/extras/models/tags.py

@@ -3,6 +3,7 @@ from django.urls import reverse
 from django.utils.text import slugify
 from django.utils.text import slugify
 from taggit.models import TagBase, GenericTaggedItemBase
 from taggit.models import TagBase, GenericTaggedItemBase
 
 
+from utilities.choices import ColorChoices
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 from utilities.models import ChangeLoggedModel
 from utilities.models import ChangeLoggedModel
 
 
@@ -13,7 +14,7 @@ from utilities.models import ChangeLoggedModel
 
 
 class Tag(TagBase, ChangeLoggedModel):
 class Tag(TagBase, ChangeLoggedModel):
     color = ColorField(
     color = ColorField(
-        default='9e9e9e'
+        default=ColorChoices.COLOR_GREY
     )
     )
     description = models.CharField(
     description = models.CharField(
         max_length=200,
         max_length=200,

+ 2 - 0
netbox/extras/templatetags/plugins.py

@@ -18,6 +18,8 @@ def _get_registered_content(obj, method, template_context):
         'object': obj,
         'object': obj,
         'request': template_context['request'],
         'request': template_context['request'],
         'settings': template_context['settings'],
         'settings': template_context['settings'],
+        'csrf_token': template_context['csrf_token'],
+        'perms': template_context['perms'],
     }
     }
 
 
     model_name = obj._meta.label_lower
     model_name = obj._meta.label_lower

+ 4 - 0
netbox/netbox/configuration.example.py

@@ -132,6 +132,10 @@ EXEMPT_VIEW_PERMISSIONS = [
 #     'https': 'http://10.10.1.10:1080',
 #     'https': 'http://10.10.1.10:1080',
 # }
 # }
 
 
+# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing
+# NetBox from an internal IP.
+INTERNAL_IPS = ('127.0.0.1', '::1')
+
 # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
 # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
 #   https://docs.djangoproject.com/en/stable/topics/logging/
 #   https://docs.djangoproject.com/en/stable/topics/logging/
 LOGGING = {}
 LOGGING = {}

+ 1 - 9
netbox/netbox/settings.py

@@ -78,6 +78,7 @@ EMAIL = getattr(configuration, 'EMAIL', {})
 ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
 ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
 EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
 EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
 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'))
 LOGGING = getattr(configuration, 'LOGGING', {})
 LOGGING = getattr(configuration, 'LOGGING', {})
 LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
 LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
 LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
 LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
@@ -615,15 +616,6 @@ RQ_QUEUES = {
     'check_releases': RQ_PARAMS,
     'check_releases': RQ_PARAMS,
 }
 }
 
 
-#
-# Django debug toolbar
-#
-
-INTERNAL_IPS = (
-    '127.0.0.1',
-    '::1',
-)
-
 
 
 #
 #
 # NetBox internal settings
 # NetBox internal settings

+ 64 - 0
netbox/utilities/choices.py

@@ -80,6 +80,70 @@ def unpack_grouped_choices(choices):
     return unpacked_choices
     return unpacked_choices
 
 
 
 
+#
+# Generic color choices
+#
+
+class ColorChoices(ChoiceSet):
+    COLOR_DARK_RED = 'aa1409'
+    COLOR_RED = 'f44336'
+    COLOR_PINK = 'e91e63'
+    COLOR_ROSE = 'ffe4e1'
+    COLOR_FUCHSIA = 'ff66ff'
+    COLOR_PURPLE = '9c27b0'
+    COLOR_DARK_PURPLE = '673ab7'
+    COLOR_INDIGO = '3f51b5'
+    COLOR_BLUE = '2196f3'
+    COLOR_LIGHT_BLUE = '03a9f4'
+    COLOR_CYAN = '00bcd4'
+    COLOR_TEAL = '009688'
+    COLOR_AQUA = '00ffff'
+    COLOR_DARK_GREEN = '2f6a31'
+    COLOR_GREEN = '4caf50'
+    COLOR_LIGHT_GREEN = '8bc34a'
+    COLOR_LIME = 'cddc39'
+    COLOR_YELLOW = 'ffeb3b'
+    COLOR_AMBER = 'ffc107'
+    COLOR_ORANGE = 'ff9800'
+    COLOR_DARK_ORANGE = 'ff5722'
+    COLOR_BROWN = '795548'
+    COLOR_LIGHT_GREY = 'c0c0c0'
+    COLOR_GREY = '9e9e9e'
+    COLOR_DARK_GREY = '607d8b'
+    COLOR_BLACK = '111111'
+    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'),
+    )
+
+
 #
 #
 # Button color choices
 # Button color choices
 #
 #

+ 0 - 31
netbox/utilities/constants.py

@@ -1,34 +1,3 @@
-COLOR_CHOICES = (
-    ('aa1409', 'Dark red'),
-    ('f44336', 'Red'),
-    ('e91e63', 'Pink'),
-    ('ffe4e1', 'Rose'),
-    ('ff66ff', 'Fuschia'),
-    ('9c27b0', 'Purple'),
-    ('673ab7', 'Dark purple'),
-    ('3f51b5', 'Indigo'),
-    ('2196f3', 'Blue'),
-    ('03a9f4', 'Light blue'),
-    ('00bcd4', 'Cyan'),
-    ('009688', 'Teal'),
-    ('00ffff', 'Aqua'),
-    ('2f6a31', 'Dark green'),
-    ('4caf50', 'Green'),
-    ('8bc34a', 'Light green'),
-    ('cddc39', 'Lime'),
-    ('ffeb3b', 'Yellow'),
-    ('ffc107', 'Amber'),
-    ('ff9800', 'Orange'),
-    ('ff5722', 'Dark orange'),
-    ('795548', 'Brown'),
-    ('c0c0c0', 'Light grey'),
-    ('9e9e9e', 'Grey'),
-    ('607d8b', 'Dark grey'),
-    ('111111', 'Black'),
-    ('ffffff', 'White'),
-)
-
-
 #
 #
 # Filter lookup expressions
 # Filter lookup expressions
 #
 #

+ 14 - 6
netbox/utilities/forms.py

@@ -14,8 +14,7 @@ from django.forms import BoundField
 from django.forms.models import fields_for_model
 from django.forms.models import fields_for_model
 from django.urls import reverse
 from django.urls import reverse
 
 
-from .choices import unpack_grouped_choices
-from .constants import *
+from .choices import ColorChoices, unpack_grouped_choices
 from .validators import EnhancedURLValidator
 from .validators import EnhancedURLValidator
 
 
 NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
 NUMERIC_EXPANSION_PATTERN = r'\[((?:\d+[?:,-])+\d+)\]'
@@ -163,7 +162,7 @@ class ColorSelect(forms.Select):
     option_template_name = 'widgets/colorselect_option.html'
     option_template_name = 'widgets/colorselect_option.html'
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        kwargs['choices'] = add_blank_choice(COLOR_CHOICES)
+        kwargs['choices'] = add_blank_choice(ColorChoices)
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
         self.attrs['class'] = 'netbox-select2-color-picker'
         self.attrs['class'] = 'netbox-select2-color-picker'
 
 
@@ -607,15 +606,18 @@ class DynamicModelChoiceMixin:
     filter = django_filters.ModelChoiceFilter
     filter = django_filters.ModelChoiceFilter
     widget = APISelect
     widget = APISelect
 
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def _get_initial_value(self, initial_data, field_name):
+        return initial_data.get(field_name)
 
 
     def get_bound_field(self, form, field_name):
     def get_bound_field(self, form, field_name):
         bound_field = BoundField(form, self, field_name)
         bound_field = BoundField(form, self, field_name)
 
 
+        # Override initial() to allow passing multiple values
+        bound_field.initial = self._get_initial_value(form.initial, field_name)
+
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
         # will be populated on-demand via the APISelect widget.
         # will be populated on-demand via the APISelect widget.
-        data = self.prepare_value(bound_field.data or bound_field.initial)
+        data = bound_field.value()
         if data:
         if data:
             filter = self.filter(field_name=self.to_field_name or 'pk', queryset=self.queryset)
             filter = self.filter(field_name=self.to_field_name or 'pk', queryset=self.queryset)
             self.queryset = filter.filter(self.queryset, data)
             self.queryset = filter.filter(self.queryset, data)
@@ -648,6 +650,12 @@ class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultip
     filter = django_filters.ModelMultipleChoiceFilter
     filter = django_filters.ModelMultipleChoiceFilter
     widget = APISelectMultiple
     widget = APISelectMultiple
 
 
+    def _get_initial_value(self, initial_data, field_name):
+        # If a QueryDict has been passed as initial form data, get *all* listed values
+        if hasattr(initial_data, 'getlist'):
+            return initial_data.getlist(field_name)
+        return initial_data.get(field_name)
+
 
 
 class LaxURLField(forms.URLField):
 class LaxURLField(forms.URLField):
     """
     """