mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Added URL custom field type; added is_filterable toggle; fixed bulk editing
This commit is contained in:
@ -26,7 +26,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
|||||||
@admin.register(CustomField)
|
@admin.register(CustomField)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
inlines = [CustomFieldChoiceAdmin]
|
inlines = [CustomFieldChoiceAdmin]
|
||||||
list_display = ['name', 'models', 'type', 'required', 'default', 'description']
|
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
|
||||||
form = CustomFieldForm
|
form = CustomFieldForm
|
||||||
|
|
||||||
def models(self, obj):
|
def models(self, obj):
|
||||||
|
@ -28,6 +28,6 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
|||||||
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
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:
|
for cf in custom_fields:
|
||||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name)
|
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name)
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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
|
Retrieve all CustomFields applicable to the given ContentType
|
||||||
"""
|
"""
|
||||||
field_dict = {}
|
field_dict = OrderedDict()
|
||||||
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
kwargs = {'obj_type': content_type}
|
||||||
|
if filterable_only:
|
||||||
|
kwargs['is_filterable'] = True
|
||||||
|
custom_fields = CustomField.objects.filter(**kwargs)
|
||||||
|
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
field_name = 'cf_{}'.format(str(cf.name))
|
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
|
# Select
|
||||||
elif cf.type == CF_TYPE_SELECT:
|
elif cf.type == CF_TYPE_SELECT:
|
||||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
if bulk_edit:
|
||||||
if select_none and not cf.required:
|
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||||
choices = [(0, 'None')] + choices
|
if not cf.required:
|
||||||
if select_empty:
|
choices = [(0, 'None')] + choices
|
||||||
choices = [(None, '---------')] + choices
|
choices = [(None, '---------')] + choices
|
||||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||||
else:
|
else:
|
||||||
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
|
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
|
# Text
|
||||||
else:
|
else:
|
||||||
field = forms.CharField(max_length=255, required=cf.required, initial=cf.default)
|
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):
|
def _save_custom_fields(self):
|
||||||
|
|
||||||
for field_name in self.custom_fields:
|
for field_name in self.custom_fields:
|
||||||
if self.cleaned_data[field_name] not in [None, u'']:
|
try:
|
||||||
try:
|
cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
|
||||||
cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
|
obj_type=self.obj_type,
|
||||||
obj_type=self.obj_type,
|
obj_id=self.instance.pk)
|
||||||
obj_id=self.instance.pk)
|
except CustomFieldValue.DoesNotExist:
|
||||||
except CustomFieldValue.DoesNotExist:
|
# Skip this field if none exists already and its value is empty
|
||||||
cfv = CustomFieldValue(
|
if self.cleaned_data[field_name] in [None, u'']:
|
||||||
field=self.fields[field_name].model,
|
continue
|
||||||
obj_type=self.obj_type,
|
cfv = CustomFieldValue(
|
||||||
obj_id=self.instance.pk
|
field=self.fields[field_name].model,
|
||||||
)
|
obj_type=self.obj_type,
|
||||||
cfv.value = self.cleaned_data[field_name]
|
obj_id=self.instance.pk
|
||||||
cfv.save()
|
)
|
||||||
|
cfv.value = self.cleaned_data[field_name]
|
||||||
|
cfv.save()
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
obj = super(CustomFieldForm, self).save(commit)
|
obj = super(CustomFieldForm, self).save(commit)
|
||||||
@ -125,7 +138,7 @@ class CustomFieldBulkEditForm(forms.Form):
|
|||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
# Add all applicable CustomFields to the form
|
||||||
custom_fields = []
|
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
|
field.required = False
|
||||||
self.fields[name] = field
|
self.fields[name] = field
|
||||||
custom_fields.append(name)
|
custom_fields.append(name)
|
||||||
@ -141,8 +154,7 @@ class CustomFieldFilterForm(forms.Form):
|
|||||||
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
|
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
# Add all applicable CustomFields to the form
|
||||||
custom_fields = get_custom_fields_for_model(self.obj_type, select_empty=True, select_none=False)\
|
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
|
||||||
.items()
|
|
||||||
for name, field in custom_fields:
|
for name, field in custom_fields:
|
||||||
field.required = False
|
field.required = False
|
||||||
self.fields[name] = field
|
self.fields[name] = field
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -18,11 +18,12 @@ class Migration(migrations.Migration):
|
|||||||
name='CustomField',
|
name='CustomField',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('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)),
|
('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)),
|
('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.')),
|
('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)),
|
('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')),
|
('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)')),
|
('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)')),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -21,12 +22,14 @@ CF_TYPE_TEXT = 100
|
|||||||
CF_TYPE_INTEGER = 200
|
CF_TYPE_INTEGER = 200
|
||||||
CF_TYPE_BOOLEAN = 300
|
CF_TYPE_BOOLEAN = 300
|
||||||
CF_TYPE_DATE = 400
|
CF_TYPE_DATE = 400
|
||||||
CF_TYPE_SELECT = 500
|
CF_TYPE_URL = 500
|
||||||
|
CF_TYPE_SELECT = 600
|
||||||
CUSTOMFIELD_TYPE_CHOICES = (
|
CUSTOMFIELD_TYPE_CHOICES = (
|
||||||
(CF_TYPE_TEXT, 'Text'),
|
(CF_TYPE_TEXT, 'Text'),
|
||||||
(CF_TYPE_INTEGER, 'Integer'),
|
(CF_TYPE_INTEGER, 'Integer'),
|
||||||
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
|
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
|
||||||
(CF_TYPE_DATE, 'Date'),
|
(CF_TYPE_DATE, 'Date'),
|
||||||
|
(CF_TYPE_URL, 'URL'),
|
||||||
(CF_TYPE_SELECT, 'Selection'),
|
(CF_TYPE_SELECT, 'Selection'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,9 +77,9 @@ class CustomFieldModel(object):
|
|||||||
if hasattr(self, 'pk'):
|
if hasattr(self, 'pk'):
|
||||||
values = CustomFieldValue.objects.filter(obj_type=content_type, obj_id=self.pk).select_related('field')
|
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}
|
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:
|
else:
|
||||||
return {field: None for field in fields}
|
return OrderedDict([(field, None) for field in fields])
|
||||||
|
|
||||||
|
|
||||||
class CustomField(models.Model):
|
class CustomField(models.Model):
|
||||||
@ -90,6 +93,7 @@ class CustomField(models.Model):
|
|||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
|
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
|
||||||
"new objects or editing an existing object.")
|
"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 "
|
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 "
|
"\"false\" for booleans. N/A for selection "
|
||||||
"fields.")
|
"fields.")
|
||||||
|
@ -7,7 +7,7 @@ from dcim.models import Site
|
|||||||
|
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
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': True, 'empty_value': None},
|
||||||
{'field_type': CF_TYPE_BOOLEAN, 'field_value': False, '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_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)
|
obj_type = ContentType.objects.get_for_model(Site)
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
||||||
{% elif value == False %}
|
{% elif value == False %}
|
||||||
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
||||||
|
{% elif field.type == 500 and value %}
|
||||||
|
{{ value|urlizetrunc:75 }}
|
||||||
{% elif value %}
|
{% elif value %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% elif field.required %}
|
{% elif field.required %}
|
||||||
|
@ -15,8 +15,8 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm
|
from extras.forms import CustomFieldForm
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
from extras.models import CustomFieldValue, ExportTemplate, UserAction
|
||||||
|
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm
|
from .forms import ConfirmationForm
|
||||||
@ -327,6 +327,7 @@ class BulkEditView(View):
|
|||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
|
|
||||||
for name in fields:
|
for name in fields:
|
||||||
|
# Check for zero value (bulk editing)
|
||||||
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
||||||
fields_to_update[name] = None
|
fields_to_update[name] = None
|
||||||
elif form.cleaned_data[name]:
|
elif form.cleaned_data[name]:
|
||||||
@ -342,21 +343,31 @@ class BulkEditView(View):
|
|||||||
if form.cleaned_data[name] not in [None, u'']:
|
if form.cleaned_data[name] not in [None, u'']:
|
||||||
|
|
||||||
field = form.fields[name].model
|
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)
|
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.
|
# 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()]
|
update_list = [cfv['obj_id'] for cfv in existing_cfvs.values()]
|
||||||
create_list = list(set(pk_list) - set(update_list))
|
create_list = list(set(pk_list) - set(update_list))
|
||||||
|
|
||||||
# Update any existing CFVs.
|
# Creating/updating CFVs
|
||||||
existing_cfvs.update(serialized_value=serialized_value)
|
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.
|
# Deleting CFVs
|
||||||
CustomFieldValue.objects.bulk_create([
|
else:
|
||||||
CustomFieldValue(field=field, obj_type=obj_type, obj_id=pk, serialized_value=serialized_value)
|
existing_cfvs.delete()
|
||||||
for pk in create_list
|
|
||||||
])
|
|
||||||
|
|
||||||
objs_updated = True
|
objs_updated = True
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user