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)
|
||||
class CustomFieldAdmin(admin.ModelAdmin):
|
||||
inlines = [CustomFieldChoiceAdmin]
|
||||
list_display = ['name', 'models', 'type', 'required', 'default', 'weight', 'description']
|
||||
list_display = ['name', 'models', 'type', 'required', 'filter_logic', 'default', 'weight', 'description']
|
||||
form = CustomFieldForm
|
||||
|
||||
def models(self, obj):
|
||||
|
@ -26,6 +26,16 @@ CUSTOMFIELD_TYPE_CHOICES = (
|
||||
(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_TYPE_INTERFACE = 100
|
||||
GRAPH_TYPE_PROVIDER = 200
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
def __init__(self, cf_type, *args, **kwargs):
|
||||
self.cf_type = cf_type
|
||||
def __init__(self, custom_field, *args, **kwargs):
|
||||
self.cf_type = custom_field.type
|
||||
self.filter_logic = custom_field.filter_logic
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
@ -41,10 +42,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value__icontains=value,
|
||||
)
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
queryset = queryset.filter(custom_field_values__field__name=self.name)
|
||||
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):
|
||||
@ -56,9 +59,9 @@ 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, is_filterable=True)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
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):
|
||||
|
@ -6,7 +6,7 @@ from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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
|
||||
"""
|
||||
field_dict = OrderedDict()
|
||||
kwargs = {'obj_type': content_type}
|
||||
custom_fields = CustomField.objects.filter(obj_type=content_type)
|
||||
if filterable_only:
|
||||
kwargs['is_filterable'] = True
|
||||
custom_fields = CustomField.objects.filter(**kwargs)
|
||||
custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
|
||||
for cf in custom_fields:
|
||||
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'),
|
||||
(0, 'False'),
|
||||
)
|
||||
if initial.lower() in ['true', 'yes', '1']:
|
||||
if initial is not None and initial.lower() in ['true', 'yes', '1']:
|
||||
initial = 1
|
||||
elif initial.lower() in ['false', 'no', '0']:
|
||||
elif initial is not None and initial.lower() in ['false', 'no', '0']:
|
||||
initial = 0
|
||||
else:
|
||||
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
|
||||
class CustomField(models.Model):
|
||||
obj_type = models.ManyToManyField(ContentType, related_name='custom_fields', verbose_name='Object(s)',
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='custom_fields',
|
||||
verbose_name='Object(s)',
|
||||
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
|
||||
help_text="The object(s) to which this field applies.")
|
||||
type = models.PositiveSmallIntegerField(choices=CUSTOMFIELD_TYPE_CHOICES, default=CF_TYPE_TEXT)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
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="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.")
|
||||
weight = models.PositiveSmallIntegerField(default=100, help_text="Fields with higher weights appear lower in a "
|
||||
"form")
|
||||
help_text='The object(s) to which this field applies.'
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=CUSTOMFIELD_TYPE_CHOICES,
|
||||
default=CF_TYPE_TEXT
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
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:
|
||||
ordering = ['weight', 'name']
|
||||
|
Reference in New Issue
Block a user