diff --git a/netbox/dcim/fixtures/initial_data.json b/netbox/dcim/fixtures/initial_data.json
index a26cbfcc5..e765de227 100644
--- a/netbox/dcim/fixtures/initial_data.json
+++ b/netbox/dcim/fixtures/initial_data.json
@@ -5,7 +5,7 @@
"fields": {
"name": "Console Server",
"slug": "console-server",
- "color": "teal"
+ "color": "009688"
}
},
{
@@ -14,7 +14,7 @@
"fields": {
"name": "Core Switch",
"slug": "core-switch",
- "color": "blue"
+ "color": "2196f3"
}
},
{
@@ -23,7 +23,7 @@
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
- "color": "blue"
+ "color": "2196f3"
}
},
{
@@ -32,7 +32,7 @@
"fields": {
"name": "Access Switch",
"slug": "access-switch",
- "color": "blue"
+ "color": "2196f3"
}
},
{
@@ -41,7 +41,7 @@
"fields": {
"name": "Management Switch",
"slug": "management-switch",
- "color": "orange"
+ "color": "ff9800"
}
},
{
@@ -50,7 +50,7 @@
"fields": {
"name": "Firewall",
"slug": "firewall",
- "color": "red"
+ "color": "f44336"
}
},
{
@@ -59,7 +59,7 @@
"fields": {
"name": "Router",
"slug": "router",
- "color": "purple"
+ "color": "9c27b0"
}
},
{
@@ -68,7 +68,7 @@
"fields": {
"name": "Server",
"slug": "server",
- "color": "medium_gray"
+ "color": "9e9e9e"
}
},
{
@@ -77,7 +77,7 @@
"fields": {
"name": "PDU",
"slug": "pdu",
- "color": "dark_gray"
+ "color": "607d8b"
}
},
{
diff --git a/netbox/dcim/migrations/0022_color_names_to_rgb.py b/netbox/dcim/migrations/0022_color_names_to_rgb.py
new file mode 100644
index 000000000..97e5de9ca
--- /dev/null
+++ b/netbox/dcim/migrations/0022_color_names_to_rgb.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10 on 2016-12-06 16:35
+from __future__ import unicode_literals
+
+from django.db import migrations
+import utilities.fields
+
+
+COLOR_CONVERSION = {
+ 'teal': '009688',
+ 'green': '4caf50',
+ 'blue': '2196f3',
+ 'purple': '9c27b0',
+ 'yellow': 'ffeb3b',
+ 'orange': 'ff9800',
+ 'red': 'f44336',
+ 'light_gray': 'c0c0c0',
+ 'medium_gray': '9e9e9e',
+ 'dark_gray': '607d8b',
+}
+
+
+def color_names_to_rgb(apps, schema_editor):
+ RackRole = apps.get_model('dcim', 'RackRole')
+ DeviceRole = apps.get_model('dcim', 'DeviceRole')
+ for color_name, color_rgb in COLOR_CONVERSION.items():
+ RackRole.objects.filter(color=color_name).update(color=color_rgb)
+ DeviceRole.objects.filter(color=color_name).update(color=color_rgb)
+
+
+def color_rgb_to_name(apps, schema_editor):
+ RackRole = apps.get_model('dcim', 'RackRole')
+ DeviceRole = apps.get_model('dcim', 'DeviceRole')
+ for color_name, color_rgb in COLOR_CONVERSION.items():
+ RackRole.objects.filter(color=color_rgb).update(color=color_name)
+ DeviceRole.objects.filter(color=color_rgb).update(color=color_name)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0021_add_ff_flexstack'),
+ ]
+
+ operations = [
+ migrations.RunPython(color_names_to_rgb, color_rgb_to_name),
+ migrations.AlterField(
+ model_name='devicerole',
+ name='color',
+ field=utilities.fields.ColorField(max_length=6),
+ ),
+ migrations.AlterField(
+ model_name='rackrole',
+ name='color',
+ field=utilities.fields.ColorField(max_length=6),
+ ),
+ ]
diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py
index 01b373376..e8967b0c0 100644
--- a/netbox/dcim/models.py
+++ b/netbox/dcim/models.py
@@ -3,7 +3,7 @@ from collections import OrderedDict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation
-from django.core.exceptions import MultipleObjectsReturned, ValidationError
+from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -12,7 +12,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
-from utilities.fields import NullableCharField
+from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
@@ -54,29 +54,6 @@ SUBDEVICE_ROLE_CHOICES = (
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
-COLOR_TEAL = 'teal'
-COLOR_GREEN = 'green'
-COLOR_BLUE = 'blue'
-COLOR_PURPLE = 'purple'
-COLOR_YELLOW = 'yellow'
-COLOR_ORANGE = 'orange'
-COLOR_RED = 'red'
-COLOR_GRAY1 = 'light_gray'
-COLOR_GRAY2 = 'medium_gray'
-COLOR_GRAY3 = 'dark_gray'
-ROLE_COLOR_CHOICES = [
- [COLOR_TEAL, 'Teal'],
- [COLOR_GREEN, 'Green'],
- [COLOR_BLUE, 'Blue'],
- [COLOR_PURPLE, 'Purple'],
- [COLOR_YELLOW, 'Yellow'],
- [COLOR_ORANGE, 'Orange'],
- [COLOR_RED, 'Red'],
- [COLOR_GRAY1, 'Light Gray'],
- [COLOR_GRAY2, 'Medium Gray'],
- [COLOR_GRAY3, 'Dark Gray'],
-]
-
# Virtual
IFACE_FF_VIRTUAL = 0
# Ethernet
@@ -345,7 +322,7 @@ class RackRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
- color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
+ color = ColorField()
class Meta:
ordering = ['name']
@@ -761,7 +738,7 @@ class DeviceRole(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
- color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
+ color = ColorField()
class Meta:
ordering = ['name']
diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py
index 6c138b446..c81c24f82 100644
--- a/netbox/dcim/tables.py
+++ b/netbox/dcim/tables.py
@@ -11,7 +11,7 @@ from .models import (
COLOR_LABEL = """
-
+
"""
DEVICE_LINK = """
@@ -34,7 +34,7 @@ RACKROLE_ACTIONS = """
RACK_ROLE = """
{% if record.role %}
-
+
{% else %}
—
{% endif %}
@@ -59,7 +59,7 @@ PLATFORM_ACTIONS = """
"""
DEVICE_ROLE = """
-
+
"""
STATUS_ICON = """
diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css
index 2be592053..ff9eb98c1 100644
--- a/netbox/project-static/css/base.css
+++ b/netbox/project-static/css/base.css
@@ -296,18 +296,6 @@ li.occupied + li.available {
border-top: 1px solid #474747;
}
-/* Colors (from http://flatuicolors.com) */
-.teal { background-color: #1abc9c; }
-.green { background-color: #2ecc71; }
-.blue { background-color: #3498db; }
-.purple { background-color: #9b59b6; }
-.yellow { background-color: #f1c40f; }
-.orange { background-color: #e67e22; }
-.red { background-color: #e74c3c; }
-.light_gray { background-color: #dce2e3; }
-.medium_gray { background-color: #95a5a6; }
-.dark_gray { background-color: #34495e; }
-
/* Misc */
.banner-bottom {
margin-bottom: 50px;
diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py
index 017ceb275..14d1c7d8f 100644
--- a/netbox/utilities/fields.py
+++ b/netbox/utilities/fields.py
@@ -1,5 +1,11 @@
+from django.core.validators import RegexValidator
from django.db import models
+from .forms import ColorSelect
+
+
+validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')
+
class NullableCharField(models.CharField):
description = "Stores empty values as NULL rather than ''"
@@ -11,3 +17,16 @@ class NullableCharField(models.CharField):
def get_prep_value(self, value):
return value or None
+
+
+class ColorField(models.CharField):
+ default_validators = [validate_color]
+ description = "A hexadecimal RGB color code"
+
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = 6
+ super(ColorField, self).__init__(*args, **kwargs)
+
+ def formfield(self, **kwargs):
+ kwargs['widget'] = ColorSelect
+ return super(ColorField, self).formfield(**kwargs)
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
index 7104e34c0..979706ace 100644
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -11,6 +11,32 @@ from django.utils.html import format_html
from django.utils.safestring import mark_safe
+COLOR_CHOICES = (
+ ('aa1409', 'Dark red'),
+ ('f44336', 'Red'),
+ ('e91e63', 'Pink'),
+ ('ff66ff', 'Fuschia'),
+ ('9c27b0', 'Purple'),
+ ('673ab7', 'Dark purple'),
+ ('3f51b5', 'Indigo'),
+ ('2196f3', 'Blue'),
+ ('03a9f4', 'Light blue'),
+ ('00bcd4', 'Cyan'),
+ ('009688', 'Teal'),
+ ('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'),
+)
NUMERIC_EXPANSION_PATTERN = '\[(\d+-\d+)\]'
IP4_EXPANSION_PATTERN = '\[([0-9]{1,3}-[0-9]{1,3})\]'
IP6_EXPANSION_PATTERN = '\[([0-9a-f]{1,4}-[0-9a-f]{1,4})\]'
@@ -71,6 +97,27 @@ class SmallTextarea(forms.Textarea):
pass
+class ColorSelect(forms.Select):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['choices'] = COLOR_CHOICES
+ super(ColorSelect, self).__init__(*args, **kwargs)
+
+ def render_option(self, selected_choices, option_value, option_label):
+ if option_value is None:
+ option_value = ''
+ option_value = force_text(option_value)
+ if option_value in selected_choices:
+ selected_html = mark_safe(' selected')
+ if not self.allow_multiple_selected:
+ # Only allow for a single selection.
+ selected_choices.remove(option_value)
+ else:
+ selected_html = ''
+ return format_html('',
+ option_value, selected_html, option_value, force_text(option_label))
+
+
class SelectWithDisabled(forms.Select):
"""
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include