Ver Fonte

Closes #3682: Add color field to front and rear ports

jeremystretch há 4 anos atrás
pai
commit
b3ed545d6a

+ 9 - 0
docs/release-notes/version-3.0.md

@@ -35,6 +35,7 @@ CustomValidator can also be subclassed to enforce more complex logic by overridi
 
 * [#2434](https://github.com/netbox-community/netbox/issues/2434) - Add option to assign IP address upon creating a new interface
 * [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API
+* [#3682](https://github.com/netbox-community/netbox/issues/3682) - Add `color` field to front and rear ports
 * [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized
 * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit
 * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
@@ -54,8 +55,16 @@ CustomValidator can also be subclassed to enforce more complex logic by overridi
     * Removed the `display_name` attribute (use `display` instead)
 * dcim.DeviceType
     * Removed the `display_name` attribute (use `display` instead)
+* dcim.FrontPort
+    * Added `color` field
+* dcim.FrontPortTemplate
+    * Added `color` field
 * dcim.Rack
     * Removed the `display_name` attribute (use `display` instead)
+* dcim.RearPort
+    * Added `color` field
+* dcim.RearPortTemplate
+    * Added `color` field
 * dcim.Site
     * `latitude` and `longitude` are now decimal fields rather than strings
 * extras.ContentType

+ 9 - 8
netbox/dcim/api/serializers.py

@@ -384,8 +384,8 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
     class Meta:
         model = RearPortTemplate
         fields = [
-            'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
-            'last_updated',
+            'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
+            'created', 'last_updated',
         ]
 
 
@@ -398,7 +398,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
     class Meta:
         model = FrontPortTemplate
         fields = [
-            'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
+            'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
             'description', 'created', 'last_updated',
         ]
 
@@ -665,8 +665,9 @@ class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
     class Meta:
         model = RearPort
         fields = [
-            'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
-            'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
+            'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description',
+            'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
+            'last_updated', '_occupied',
         ]
 
 
@@ -691,9 +692,9 @@ class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
     class Meta:
         model = FrontPort
         fields = [
-            'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
-            'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
-            'last_updated', '_occupied',
+            'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
+            'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields',
+            'created', 'last_updated', '_occupied',
         ]
 
 

+ 4 - 4
netbox/dcim/filtersets.py

@@ -538,7 +538,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
 
     class Meta:
         model = FrontPortTemplate
-        fields = ['id', 'name', 'type']
+        fields = ['id', 'name', 'type', 'color']
 
 
 class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
@@ -549,7 +549,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
 
     class Meta:
         model = RearPortTemplate
-        fields = ['id', 'name', 'type', 'positions']
+        fields = ['id', 'name', 'type', 'color', 'positions']
 
 
 class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
@@ -1027,7 +1027,7 @@ class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
 
     class Meta:
         model = FrontPort
-        fields = ['id', 'name', 'label', 'type', 'description']
+        fields = ['id', 'name', 'label', 'type', 'color', 'description']
 
 
 class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
@@ -1038,7 +1038,7 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
 
     class Meta:
         model = RearPort
-        fields = ['id', 'name', 'label', 'type', 'positions', 'description']
+        fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
 
 
 class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):

+ 56 - 16
netbox/dcim/forms.py

@@ -1610,7 +1610,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = FrontPortTemplate
         fields = [
-            'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
+            'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
         ]
         widgets = {
             'device_type': forms.HiddenInput(),
@@ -1639,7 +1639,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
         help_text='Select one rear port assignment for each front port being created.',
     )
     field_order = (
-        'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'rear_port_set', 'description',
+        'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description',
     )
 
     def __init__(self, *args, **kwargs):
@@ -1703,6 +1703,11 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False,
         widget=StaticSelect2()
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     description = forms.CharField(
         required=False
     )
@@ -1716,7 +1721,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = RearPortTemplate
         fields = [
-            'device_type', 'name', 'label', 'type', 'positions', 'description',
+            'device_type', 'name', 'label', 'type', 'color', 'positions', 'description',
         ]
         widgets = {
             'device_type': forms.HiddenInput(),
@@ -1729,13 +1734,20 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
         choices=PortTypeChoices,
         widget=StaticSelect2(),
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     positions = forms.IntegerField(
         min_value=REARPORT_POSITIONS_MIN,
         max_value=REARPORT_POSITIONS_MAX,
         initial=1,
         help_text='The number of front ports which may be mapped to each rear port'
     )
-    field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'positions', 'description')
+    field_order = (
+        'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description',
+    )
 
 
 class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -1752,6 +1764,11 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         required=False,
         widget=StaticSelect2()
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     description = forms.CharField(
         required=False
     )
@@ -3427,7 +3444,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
 
 class FrontPortFilterForm(DeviceComponentFilterForm):
     field_groups = [
-        ['name', 'label', 'type'],
+        ['name', 'label', 'type', 'color'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag']
     ]
@@ -3437,6 +3454,11 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
         required=False,
         widget=StaticSelect2Multiple()
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     tag = TagFilterField(model)
 
 
@@ -3449,8 +3471,8 @@ class FrontPortForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = FrontPort
         fields = [
-            'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'mark_connected', 'description',
-            'tags',
+            'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
+            'description', 'tags',
         ]
         widgets = {
             'device': forms.HiddenInput(),
@@ -3475,13 +3497,19 @@ class FrontPortCreateForm(ComponentCreateForm):
         choices=PortTypeChoices,
         widget=StaticSelect2(),
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     rear_port_set = forms.MultipleChoiceField(
         choices=[],
         label='Rear ports',
         help_text='Select one rear port assignment for each front port being created.',
     )
     field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'rear_port_set', 'mark_connected', 'description', 'tags',
+        'device', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'mark_connected', 'description',
+        'tags',
     )
 
     def __init__(self, *args, **kwargs):
@@ -3540,7 +3568,7 @@ class FrontPortCreateForm(ComponentCreateForm):
 
 
 class FrontPortBulkEditForm(
-    form_from_model(FrontPort, ['label', 'type', 'mark_connected', 'description']),
+    form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
     BootstrapMixin,
     AddRemoveTagsForm,
     CustomFieldBulkEditForm
@@ -3572,7 +3600,8 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
     class Meta:
         model = FrontPort
         fields = (
-            'device', 'name', 'label', 'type', 'mark_connected', 'rear_port', 'rear_port_position', 'description',
+            'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
+            'description',
         )
         help_texts = {
             'rear_port_position': 'Mapped position on corresponding rear port',
@@ -3608,7 +3637,7 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
 class RearPortFilterForm(DeviceComponentFilterForm):
     model = RearPort
     field_groups = [
-        ['name', 'label', 'type'],
+        ['name', 'label', 'type', 'color'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag']
     ]
@@ -3617,6 +3646,11 @@ class RearPortFilterForm(DeviceComponentFilterForm):
         required=False,
         widget=StaticSelect2Multiple()
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     tag = TagFilterField(model)
 
 
@@ -3629,7 +3663,7 @@ class RearPortForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = RearPort
         fields = [
-            'device', 'name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags',
+            'device', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
         ]
         widgets = {
             'device': forms.HiddenInput(),
@@ -3643,6 +3677,11 @@ class RearPortCreateForm(ComponentCreateForm):
         choices=PortTypeChoices,
         widget=StaticSelect2(),
     )
+    color = forms.CharField(
+        max_length=6,  # RGB color code
+        required=False,
+        widget=ColorSelect()
+    )
     positions = forms.IntegerField(
         min_value=REARPORT_POSITIONS_MIN,
         max_value=REARPORT_POSITIONS_MAX,
@@ -3650,12 +3689,13 @@ class RearPortCreateForm(ComponentCreateForm):
         help_text='The number of front ports which may be mapped to each rear port'
     )
     field_order = (
-        'device', 'name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags',
+        'device', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'mark_connected', 'description',
+        'tags',
     )
 
 
 class RearPortBulkCreateForm(
-    form_from_model(RearPort, ['type', 'positions', 'mark_connected']),
+    form_from_model(RearPort, ['type', 'color', 'positions', 'mark_connected']),
     DeviceBulkAddComponentForm
 ):
     model = RearPort
@@ -3663,7 +3703,7 @@ class RearPortBulkCreateForm(
 
 
 class RearPortBulkEditForm(
-    form_from_model(RearPort, ['label', 'type', 'mark_connected', 'description']),
+    form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
     BootstrapMixin,
     AddRemoveTagsForm,
     CustomFieldBulkEditForm
@@ -3689,7 +3729,7 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
 
     class Meta:
         model = RearPort
-        fields = ('device', 'name', 'label', 'type', 'mark_connected', 'positions', 'description')
+        fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description')
         help_texts = {
             'positions': 'Number of front ports which may be mapped'
         }

+ 32 - 0
netbox/dcim/migrations/0133_port_colors.py

@@ -0,0 +1,32 @@
+from django.db import migrations
+import utilities.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0132_cable_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='frontport',
+            name='color',
+            field=utilities.fields.ColorField(blank=True, max_length=6),
+        ),
+        migrations.AddField(
+            model_name='frontporttemplate',
+            name='color',
+            field=utilities.fields.ColorField(blank=True, max_length=6),
+        ),
+        migrations.AddField(
+            model_name='rearport',
+            name='color',
+            field=utilities.fields.ColorField(blank=True, max_length=6),
+        ),
+        migrations.AddField(
+            model_name='rearporttemplate',
+            name='color',
+            field=utilities.fields.ColorField(blank=True, max_length=6),
+        ),
+    ]

+ 9 - 1
netbox/dcim/models/device_component_templates.py

@@ -6,7 +6,7 @@ from dcim.choices import *
 from dcim.constants import *
 from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel
-from utilities.fields import NaturalOrderingField
+from utilities.fields import ColorField, NaturalOrderingField
 from utilities.querysets import RestrictedQuerySet
 from utilities.ordering import naturalize_interface
 from .device_components import (
@@ -267,6 +267,9 @@ class FrontPortTemplate(ComponentTemplateModel):
         max_length=50,
         choices=PortTypeChoices
     )
+    color = ColorField(
+        blank=True
+    )
     rear_port = models.ForeignKey(
         to='dcim.RearPortTemplate',
         on_delete=models.CASCADE,
@@ -314,6 +317,7 @@ class FrontPortTemplate(ComponentTemplateModel):
             name=self.name,
             label=self.label,
             type=self.type,
+            color=self.color,
             rear_port=rear_port,
             rear_port_position=self.rear_port_position
         )
@@ -328,6 +332,9 @@ class RearPortTemplate(ComponentTemplateModel):
         max_length=50,
         choices=PortTypeChoices
     )
+    color = ColorField(
+        blank=True
+    )
     positions = models.PositiveSmallIntegerField(
         default=1,
         validators=[
@@ -346,6 +353,7 @@ class RearPortTemplate(ComponentTemplateModel):
             name=self.name,
             label=self.label,
             type=self.type,
+            color=self.color,
             positions=self.positions
         )
 

+ 7 - 1
netbox/dcim/models/device_components.py

@@ -12,7 +12,7 @@ from dcim.constants import *
 from dcim.fields import MACAddressField
 from extras.utils import extras_features
 from netbox.models import PrimaryModel
-from utilities.fields import NaturalOrderingField
+from utilities.fields import ColorField, NaturalOrderingField
 from utilities.mptt import TreeManager
 from utilities.ordering import naturalize_interface
 from utilities.querysets import RestrictedQuerySet
@@ -614,6 +614,9 @@ class FrontPort(ComponentModel, CableTermination):
         max_length=50,
         choices=PortTypeChoices
     )
+    color = ColorField(
+        blank=True
+    )
     rear_port = models.ForeignKey(
         to='dcim.RearPort',
         on_delete=models.CASCADE,
@@ -663,6 +666,9 @@ class RearPort(ComponentModel, CableTermination):
         max_length=50,
         choices=PortTypeChoices
     )
+    color = ColorField(
+        blank=True
+    )
     positions = models.PositiveSmallIntegerField(
         default=1,
         validators=[

+ 9 - 5
netbox/dcim/tables/devices.py

@@ -549,6 +549,7 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
             'args': [Accessor('device_id')],
         }
     )
+    color = ColorColumn()
     rear_port_position = tables.Column(
         verbose_name='Position'
     )
@@ -562,10 +563,12 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
     class Meta(DeviceComponentTable.Meta):
         model = FrontPort
         fields = (
-            'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected',
-            'cable', 'cable_color', 'cable_peer', 'tags',
+            'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
+            'mark_connected', 'cable', 'cable_color', 'cable_peer', 'tags',
+        )
+        default_columns = (
+            'pk', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
         )
-        default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
 
 
 class DeviceFrontPortTable(FrontPortTable):
@@ -603,6 +606,7 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
             'args': [Accessor('device_id')],
         }
     )
+    color = ColorColumn()
     tags = TagColumn(
         url_name='dcim:rearport_list'
     )
@@ -610,10 +614,10 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
     class Meta(DeviceComponentTable.Meta):
         model = RearPort
         fields = (
-            'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable',
+            'pk', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable',
             'cable_color', 'cable_peer', 'tags',
         )
-        default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
+        default_columns = ('pk', 'device', 'name', 'label', 'type', 'color', 'description')
 
 
 class DeviceRearPortTable(RearPortTable):

+ 7 - 3
netbox/dcim/tables/devicetypes.py

@@ -4,7 +4,9 @@ from dcim.models import (
     ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
     Manufacturer, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
 )
-from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, LinkedCountColumn, TagColumn, ToggleColumn
+from utilities.tables import (
+    BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, TagColumn, ToggleColumn,
+)
 
 __all__ = (
     'ConsolePortTemplateTable',
@@ -164,6 +166,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
     rear_port_position = tables.Column(
         verbose_name='Position'
     )
+    color = ColorColumn()
     actions = ButtonsColumn(
         model=FrontPortTemplate,
         buttons=('edit', 'delete'),
@@ -172,11 +175,12 @@ class FrontPortTemplateTable(ComponentTemplateTable):
 
     class Meta(BaseTable.Meta):
         model = FrontPortTemplate
-        fields = ('pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'actions')
+        fields = ('pk', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'actions')
         empty_text = "None"
 
 
 class RearPortTemplateTable(ComponentTemplateTable):
+    color = ColorColumn()
     actions = ButtonsColumn(
         model=RearPortTemplate,
         buttons=('edit', 'delete'),
@@ -185,7 +189,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
 
     class Meta(BaseTable.Meta):
         model = RearPortTemplate
-        fields = ('pk', 'name', 'label', 'type', 'positions', 'description', 'actions')
+        fields = ('pk', 'name', 'label', 'type', 'color', 'positions', 'description', 'actions')
         empty_text = "None"
 
 

+ 29 - 12
netbox/dcim/tests/test_filtersets.py

@@ -6,6 +6,7 @@ from dcim.filtersets import *
 from dcim.models import *
 from ipam.models import IPAddress
 from tenancy.models import Tenant, TenantGroup
+from utilities.choices import ColorChoices
 from utilities.testing import ChangeLoggedFilterSetTests
 from virtualization.models import Cluster, ClusterType
 
@@ -959,9 +960,9 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
         RearPortTemplate.objects.bulk_create(rear_ports)
 
         FrontPortTemplate.objects.bulk_create((
-            FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C),
-            FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH),
-            FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC),
+            FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED),
+            FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN),
+            FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE),
         ))
 
     def test_name(self):
@@ -977,6 +978,10 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_color(self):
+        params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
 
 class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = RearPortTemplate.objects.all()
@@ -995,9 +1000,9 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
         DeviceType.objects.bulk_create(device_types)
 
         RearPortTemplate.objects.bulk_create((
-            RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1),
-            RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2),
-            RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3),
+            RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, positions=1),
+            RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, positions=2),
+            RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, positions=3),
         ))
 
     def test_name(self):
@@ -1013,6 +1018,10 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_color(self):
+        params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_positions(self):
         params = {'positions': [1, 2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2153,9 +2162,9 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
         RearPort.objects.bulk_create(rear_ports)
 
         front_ports = (
-            FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'),
-            FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
-            FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
+            FrontPort(device=devices[0], name='Front Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, rear_port=rear_ports[0], rear_port_position=1, description='First'),
+            FrontPort(device=devices[1], name='Front Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, rear_port=rear_ports[1], rear_port_position=2, description='Second'),
+            FrontPort(device=devices[2], name='Front Port 3', label='C', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, rear_port=rear_ports[2], rear_port_position=3, description='Third'),
             FrontPort(device=devices[3], name='Front Port 4', label='D', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1),
             FrontPort(device=devices[3], name='Front Port 5', label='E', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1),
             FrontPort(device=devices[3], name='Front Port 6', label='F', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1),
@@ -2179,6 +2188,10 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_color(self):
+        params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_description(self):
         params = {'description': ['First', 'Second']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2260,9 +2273,9 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
         Device.objects.bulk_create(devices)
 
         rear_ports = (
-            RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'),
-            RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'),
-            RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'),
+            RearPort(device=devices[0], name='Rear Port 1', label='A', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, positions=1, description='First'),
+            RearPort(device=devices[1], name='Rear Port 2', label='B', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, positions=2, description='Second'),
+            RearPort(device=devices[2], name='Rear Port 3', label='C', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, positions=3, description='Third'),
             RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
             RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
             RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
@@ -2286,6 +2299,10 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_color(self):
+        params = {'color': [ColorChoices.COLOR_RED, ColorChoices.COLOR_GREEN]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_positions(self):
         params = {'positions': [1, 2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 6 - 0
netbox/templates/dcim/frontport.html

@@ -35,6 +35,12 @@
                             <th scope="row">Type</th>
                             <td>{{ object.get_type_display }}</td>
                         </tr>
+                        <tr>
+                          <th scope="row">Color</th>
+                          <td>
+                            <span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
+                          </td>
+                        </tr>
                         <tr>
                             <th scope="row">Rear Port</th>
                             <td>

+ 6 - 0
netbox/templates/dcim/rearport.html

@@ -34,6 +34,12 @@
                             <th scope="row">Type</th>
                             <td>{{ object.get_type_display }}</td>
                         </tr>
+                        <tr>
+                          <th scope="row">Color</th>
+                          <td>
+                            <span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
+                          </td>
+                        </tr>
                         <tr>
                             <th scope="row">Positions</th>
                             <td>{{ object.positions }}</td>