diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py
index 99afafa28..402cd6093 100644
--- a/netbox/extras/admin.py
+++ b/netbox/extras/admin.py
@@ -26,7 +26,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
@admin.register(CustomField)
class CustomFieldAdmin(admin.ModelAdmin):
inlines = [CustomFieldChoiceAdmin]
- list_display = ['name', 'models', 'type', 'required', 'default', 'description']
+ list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
form = CustomFieldForm
def models(self, obj):
diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py
index 447694ce3..d8ccbf986 100644
--- a/netbox/extras/filters.py
+++ b/netbox/extras/filters.py
@@ -28,6 +28,6 @@ class CustomFieldFilterSet(django_filters.FilterSet):
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model)
- custom_fields = CustomField.objects.filter(obj_type=obj_type)
+ custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name)
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
index 1f36e174e..bb41d59a2 100644
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -1,15 +1,22 @@
+from collections import OrderedDict
+
from django import forms
from django.contrib.contenttypes.models import ContentType
-from .models import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CustomField, CustomFieldValue
+from .models import (
+ CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
+)
-def get_custom_fields_for_model(content_type, select_empty=False, select_none=True):
+def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
"""
Retrieve all CustomFields applicable to the given ContentType
"""
- field_dict = {}
- custom_fields = CustomField.objects.filter(obj_type=content_type)
+ field_dict = OrderedDict()
+ kwargs = {'obj_type': content_type}
+ if filterable_only:
+ kwargs['is_filterable'] = True
+ custom_fields = CustomField.objects.filter(**kwargs)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
@@ -40,15 +47,19 @@ def get_custom_fields_for_model(content_type, select_empty=False, select_none=Tr
# Select
elif cf.type == CF_TYPE_SELECT:
- choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
- if select_none and not cf.required:
- choices = [(0, 'None')] + choices
- if select_empty:
+ if bulk_edit:
+ choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
+ if not cf.required:
+ choices = [(0, 'None')] + choices
choices = [(None, '---------')] + choices
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
else:
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
+ # URL
+ elif cf.type == CF_TYPE_URL:
+ field = forms.URLField(required=cf.required, initial=cf.default)
+
# Text
else:
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
@@ -88,19 +99,21 @@ class CustomFieldForm(forms.ModelForm):
def _save_custom_fields(self):
for field_name in self.custom_fields:
- if self.cleaned_data[field_name] not in [None, u'']:
- try:
- cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
- obj_type=self.obj_type,
- obj_id=self.instance.pk)
- except CustomFieldValue.DoesNotExist:
- cfv = CustomFieldValue(
- field=self.fields[field_name].model,
- obj_type=self.obj_type,
- obj_id=self.instance.pk
- )
- cfv.value = self.cleaned_data[field_name]
- cfv.save()
+ try:
+ cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
+ obj_type=self.obj_type,
+ obj_id=self.instance.pk)
+ except CustomFieldValue.DoesNotExist:
+ # Skip this field if none exists already and its value is empty
+ if self.cleaned_data[field_name] in [None, u'']:
+ continue
+ cfv = CustomFieldValue(
+ field=self.fields[field_name].model,
+ obj_type=self.obj_type,
+ obj_id=self.instance.pk
+ )
+ cfv.value = self.cleaned_data[field_name]
+ cfv.save()
def save(self, commit=True):
obj = super(CustomFieldForm, self).save(commit)
@@ -125,7 +138,7 @@ class CustomFieldBulkEditForm(forms.Form):
# Add all applicable CustomFields to the form
custom_fields = []
- for name, field in get_custom_fields_for_model(self.obj_type, select_empty=True).items():
+ for name, field in get_custom_fields_for_model(self.obj_type, bulk_edit=True).items():
field.required = False
self.fields[name] = field
custom_fields.append(name)
@@ -141,8 +154,7 @@ class CustomFieldFilterForm(forms.Form):
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
# Add all applicable CustomFields to the form
- custom_fields = get_custom_fields_for_model(self.obj_type, select_empty=True, select_none=False)\
- .items()
+ custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
for name, field in custom_fields:
field.required = False
self.fields[name] = field
diff --git a/netbox/extras/migrations/0002_custom_fields.py b/netbox/extras/migrations/0002_custom_fields.py
index 4afab3822..1d33ca281 100644
--- a/netbox/extras/migrations/0002_custom_fields.py
+++ b/netbox/extras/migrations/0002_custom_fields.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10 on 2016-08-23 16:24
+# Generated by Django 1.10 on 2016-08-23 20:33
from __future__ import unicode_literals
from django.db import migrations, models
@@ -18,11 +18,12 @@ class Migration(migrations.Migration):
name='CustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('type', models.PositiveSmallIntegerField(choices=[(100, b'Text'), (200, b'Integer'), (300, b'Boolean (true/false)'), (400, b'Date'), (500, b'Selection')], default=100)),
+ ('type', models.PositiveSmallIntegerField(choices=[(100, b'Text'), (200, b'Integer'), (300, b'Boolean (true/false)'), (400, b'Date'), (500, b'URL'), (600, b'Selection')], default=100)),
('name', models.CharField(max_length=50, unique=True)),
('label', models.CharField(blank=True, help_text=b"Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
('description', models.CharField(blank=True, max_length=100)),
('required', models.BooleanField(default=False, help_text=b'Determines whether this field is required when creating new objects or editing an existing object.')),
+ ('is_filterable', models.BooleanField(default=True, help_text=b'This field can be used to filter objects.')),
('default', models.CharField(blank=True, help_text=b'Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100)),
('weight', models.PositiveSmallIntegerField(default=100, help_text=b'Fields with higher weights appear lower in a form')),
('obj_type', models.ManyToManyField(help_text=b'The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name=b'Object(s)')),
diff --git a/netbox/extras/models.py b/netbox/extras/models.py
index 217c75780..6d173b62d 100644
--- a/netbox/extras/models.py
+++ b/netbox/extras/models.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
from datetime import date
from django.contrib.auth.models import User
@@ -21,12 +22,14 @@ CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
-CF_TYPE_SELECT = 500
+CF_TYPE_URL = 500
+CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
+ (CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
@@ -74,9 +77,9 @@ class CustomFieldModel(object):
if hasattr(self, 'pk'):
values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
values_dict = {cfv.field_id: cfv.value for cfv in values}
- return {field: values_dict.get(field.pk) for field in fields}
+ return OrderedDict([(field, values_dict.get(field.pk)) for field in fields])
else:
- return {field: None for field in fields}
+ return OrderedDict([(field, None) for field in fields])
class CustomField(models.Model):
@@ -90,6 +93,7 @@ class CustomField(models.Model):
description = models.CharField(max_length=100, blank=True)
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
"new objects or editing an existing object.")
+ is_filterable = models.BooleanField(default=True, help_text="This field can be used to filter objects.")
default = models.CharField(max_length=100, blank=True, help_text="Default value for the field. Use \"true\" or "
"\"false\" for booleans. N/A for selection "
"fields.")
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 08ebb14b4..791c6a1a2 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -7,7 +7,7 @@ from dcim.models import Site
from extras.models import (
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
- CF_TYPE_SELECT,
+ CF_TYPE_SELECT, CF_TYPE_URL,
)
@@ -30,6 +30,7 @@ class CustomFieldTestCase(TestCase):
{'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
{'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
{'field_type': CF_TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
+ {'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
)
obj_type = ContentType.objects.get_for_model(Site)
diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html
index 6998c0e00..fed172aa6 100644
--- a/netbox/templates/inc/custom_fields_panel.html
+++ b/netbox/templates/inc/custom_fields_panel.html
@@ -12,6 +12,8 @@
{% elif value == False %}
+ {% elif field.type == 500 and value %}
+ {{ value|urlizetrunc:75 }}
{% elif value %}
{{ value }}
{% elif field.required %}
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py
index 674a33d22..f36beb69d 100644
--- a/netbox/utilities/views.py
+++ b/netbox/utilities/views.py
@@ -15,8 +15,8 @@ from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url
from django.views.generic import View
-from extras.forms import CustomFieldForm, CustomFieldBulkEditForm
-from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
+from extras.forms import CustomFieldForm
+from extras.models import CustomFieldValue, ExportTemplate, UserAction
from .error_handlers import handle_protectederror
from .forms import ConfirmationForm
@@ -327,6 +327,7 @@ class BulkEditView(View):
fields_to_update = {}
for name in fields:
+ # Check for zero value (bulk editing)
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
fields_to_update[name] = None
elif form.cleaned_data[name]:
@@ -342,21 +343,31 @@ class BulkEditView(View):
if form.cleaned_data[name] not in [None, u'']:
field = form.fields[name].model
- serialized_value = field.serialize_value(form.cleaned_data[name])
+
+ # Check for zero value (bulk editing)
+ if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
+ serialized_value = field.serialize_value(None)
+ else:
+ serialized_value = field.serialize_value(form.cleaned_data[name])
+
+ # Gather any pre-existing CustomFieldValues for the objects being edited.
existing_cfvs = CustomFieldValue.objects.filter(field=field, obj_type=obj_type, obj_id__in=pk_list)
# Determine which objects have an existing CFV to update and which need a new CFV created.
update_list = [cfv['obj_id'] for cfv in existing_cfvs.values()]
create_list = list(set(pk_list) - set(update_list))
- # Update any existing CFVs.
- existing_cfvs.update(serialized_value=serialized_value)
+ # Creating/updating CFVs
+ if serialized_value:
+ existing_cfvs.update(serialized_value=serialized_value)
+ CustomFieldValue.objects.bulk_create([
+ CustomFieldValue(field=field, obj_type=obj_type, obj_id=pk, serialized_value=serialized_value)
+ for pk in create_list
+ ])
- # Create new CFVs as needed.
- CustomFieldValue.objects.bulk_create([
- CustomFieldValue(field=field, obj_type=obj_type, obj_id=pk, serialized_value=serialized_value)
- for pk in create_list
- ])
+ # Deleting CFVs
+ else:
+ existing_cfvs.delete()
objs_updated = True