1
0
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:
Jeremy Stretch
2016-08-23 16:45:26 -04:00
parent fcd4c9f7de
commit d74d85a042
8 changed files with 73 additions and 42 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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)')),

View File

@ -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.")

View File

@ -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)

View File

@ -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 %}

View File

@ -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