mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #1693: Allow specifying loose or exact matching for custom field filters
This commit is contained in:
@ -39,7 +39,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', 'weight', 'description']
|
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||||
form = CustomFieldForm
|
form = CustomFieldForm
|
||||||
|
|
||||||
def models(self, obj):
|
def models(self, obj):
|
||||||
|
@ -26,6 +26,16 @@ CUSTOMFIELD_TYPE_CHOICES = (
|
|||||||
(CF_TYPE_SELECT, 'Selection'),
|
(CF_TYPE_SELECT, 'Selection'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Custom field filter logic choices
|
||||||
|
CF_FILTER_DISABLED = 0
|
||||||
|
CF_FILTER_LOOSE = 1
|
||||||
|
CF_FILTER_EXACT = 2
|
||||||
|
CF_FILTER_CHOICES = (
|
||||||
|
(CF_FILTER_DISABLED, 'Disabled'),
|
||||||
|
(CF_FILTER_LOOSE, 'Loose'),
|
||||||
|
(CF_FILTER_EXACT, 'Exact'),
|
||||||
|
)
|
||||||
|
|
||||||
# Graph types
|
# Graph types
|
||||||
GRAPH_TYPE_INTERFACE = 100
|
GRAPH_TYPE_INTERFACE = 100
|
||||||
GRAPH_TYPE_PROVIDER = 200
|
GRAPH_TYPE_PROVIDER = 200
|
||||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from .constants import CF_TYPE_SELECT
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
|
||||||
|
|
||||||
|
|
||||||
@ -14,8 +14,9 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cf_type, *args, **kwargs):
|
def __init__(self, custom_field, *args, **kwargs):
|
||||||
self.cf_type = cf_type
|
self.cf_type = custom_field.type
|
||||||
|
self.filter_logic = custom_field.filter_logic
|
||||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def filter(self, queryset, value):
|
def filter(self, queryset, value):
|
||||||
@ -41,10 +42,12 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
return queryset.filter(
|
# Apply the assigned filter logic (exact or loose)
|
||||||
custom_field_values__field__name=self.name,
|
queryset = queryset.filter(custom_field_values__field__name=self.name)
|
||||||
custom_field_values__serialized_value__icontains=value,
|
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||||
)
|
return queryset.filter(custom_field_values__serialized_value=value)
|
||||||
|
else:
|
||||||
|
return queryset.filter(custom_field_values__serialized_value__icontains=value)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||||
@ -56,9 +59,9 @@ 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, is_filterable=True)
|
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
|
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, custom_field=cf)
|
||||||
|
|
||||||
|
|
||||||
class GraphFilter(django_filters.FilterSet):
|
class GraphFilter(django_filters.FilterSet):
|
||||||
|
@ -6,7 +6,7 @@ from django import forms
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
|
||||||
from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
|
||||||
from .models import CustomField, CustomFieldValue, ImageAttachment
|
from .models import CustomField, CustomFieldValue, ImageAttachment
|
||||||
|
|
||||||
|
|
||||||
@ -15,10 +15,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
Retrieve all CustomFields applicable to the given ContentType
|
Retrieve all CustomFields applicable to the given ContentType
|
||||||
"""
|
"""
|
||||||
field_dict = OrderedDict()
|
field_dict = OrderedDict()
|
||||||
kwargs = {'obj_type': content_type}
|
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||||
if filterable_only:
|
if filterable_only:
|
||||||
kwargs['is_filterable'] = True
|
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
|
||||||
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))
|
||||||
@ -35,9 +34,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
(1, 'True'),
|
(1, 'True'),
|
||||||
(0, 'False'),
|
(0, 'False'),
|
||||||
)
|
)
|
||||||
if initial.lower() in ['true', 'yes', '1']:
|
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||||
initial = 1
|
initial = 1
|
||||||
elif initial.lower() in ['false', 'no', '0']:
|
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||||
initial = 0
|
initial = 0
|
||||||
else:
|
else:
|
||||||
initial = None
|
initial = None
|
||||||
|
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
51
netbox/extras/migrations/0010_customfield_filter_logic.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||||
|
|
||||||
|
|
||||||
|
def is_filterable_to_filter_logic(apps, schema_editor):
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
|
||||||
|
CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
|
||||||
|
# Select fields match on primary key only
|
||||||
|
CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_logic_to_is_filterable(apps, schema_editor):
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
|
||||||
|
CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0009_topologymap_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='filter_logic',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='required',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='weight',
|
||||||
|
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(is_filterable_to_filter_logic, filter_logic_to_is_filterable),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='is_filterable',
|
||||||
|
),
|
||||||
|
]
|
@ -55,22 +55,48 @@ class CustomFieldModel(object):
|
|||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class CustomField(models.Model):
|
class CustomField(models.Model):
|
||||||
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
obj_type = models.ManyToManyField(
|
||||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
to=ContentType,
|
||||||
help_text="The object(s) to which this field applies.")
|
related_name='custom_fields',
|
||||||
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
|
verbose_name='Object(s)',
|
||||||
name = models.CharField(max_length=50, unique=True)
|
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||||
label = models.CharField(max_length=50, blank=True, help_text="Name of the field as displayed to users (if not "
|
help_text='The object(s) to which this field applies.'
|
||||||
"provided, the field's name will be used)")
|
)
|
||||||
description = models.CharField(max_length=100, blank=True)
|
type = models.PositiveSmallIntegerField(
|
||||||
required = models.BooleanField(default=False, help_text="Determines whether this field is required when creating "
|
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||||
"new objects or editing an existing object.")
|
default=CF_TYPE_TEXT
|
||||||
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 "
|
name = models.CharField(
|
||||||
"\"false\" for booleans. N/A for selection "
|
max_length=50,
|
||||||
"fields.")
|
unique=True
|
||||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Fields with higher weights appear lower in a "
|
)
|
||||||
"form")
|
label = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
help_text='Name of the field as displayed to users (if not provided, the field\'s name will be used)'
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
required = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='If true, this field is required when creating new objects or editing an existing object.'
|
||||||
|
)
|
||||||
|
filter_logic = models.PositiveSmallIntegerField(
|
||||||
|
choices=CF_FILTER_CHOICES,
|
||||||
|
default=CF_FILTER_LOOSE,
|
||||||
|
help_text="Loose matches any instance of a given string; exact matches the entire field."
|
||||||
|
)
|
||||||
|
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.'
|
||||||
|
)
|
||||||
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
default=100,
|
||||||
|
help_text='Fields with higher weights appear lower in a form.'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'name']
|
ordering = ['weight', 'name']
|
||||||
|
Reference in New Issue
Block a user