From 76a61195845b7d57c213f9376178d4df4196cbcc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 1 Jul 2021 15:17:46 -0400 Subject: [PATCH] Closes #6138: Add an 'empty' filter modifier for character fields --- docs/release-notes/version-2.11.md | 1 + docs/rest-api/filtering.md | 33 +++++++++++++++++------------- netbox/extras/apps.py | 1 + netbox/extras/lookups.py | 17 +++++++++++++++ netbox/netbox/filtersets.py | 13 +++++------- netbox/utilities/constants.py | 3 ++- 6 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 netbox/extras/lookups.py diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 6516bcd29..5c09cbe96 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields * [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view * [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view * [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md index b77513297..471beffee 100644 --- a/docs/rest-api/filtering.md +++ b/docs/rest-api/filtering.md @@ -61,25 +61,30 @@ These lookup expressions can be applied by adding a suffix to the desired field' Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions: -- `n` - not equal to (negation) -- `lt` - less than -- `lte` - less than or equal -- `gt` - greater than -- `gte` - greater than or equal +| Filter | Description | +|--------|-------------| +| `n` | Not equal to | +| `lt` | Less than | +| `lte` | Less than or equal to | +| `gt` | Greater than | +| `gte` | Greater than or equal to | ### String Fields String based (char) fields (Name, Address, etc) support these lookup expressions: -- `n` - not equal to (negation) -- `ic` - case insensitive contains -- `nic` - negated case insensitive contains -- `isw` - case insensitive starts with -- `nisw` - negated case insensitive starts with -- `iew` - case insensitive ends with -- `niew` - negated case insensitive ends with -- `ie` - case insensitive exact match -- `nie` - negated case insensitive exact match +| Filter | Description | +|--------|-------------| +| `n` | Not equal to | +| `ic` | Contains (case-insensitive) | +| `nic` | Does not contain (case-insensitive) | +| `isw` | Starts with (case-insensitive) | +| `nisw` | Does not start with (case-insensitive) | +| `iew` | Ends with (case-insensitive) | +| `niew` | Does not end with (case-insensitive) | +| `ie` | Exact match (case-insensitive) | +| `nie` | Inverse exact match (case-insensitive) | +| `empty` | Is empty (boolean) | ### Foreign Keys & Other Fields diff --git a/netbox/extras/apps.py b/netbox/extras/apps.py index 3201c3bb2..7500157c0 100644 --- a/netbox/extras/apps.py +++ b/netbox/extras/apps.py @@ -5,4 +5,5 @@ class ExtrasConfig(AppConfig): name = "extras" def ready(self): + import extras.lookups import extras.signals diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py new file mode 100644 index 000000000..7197efcfc --- /dev/null +++ b/netbox/extras/lookups.py @@ -0,0 +1,17 @@ +from django.db.models import CharField, Lookup + + +class Empty(Lookup): + """ + Filter on whether a string is empty. + """ + lookup_name = 'empty' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params + + +CharField.register_lookup(Empty) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index aa9e15385..791c21d19 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -89,13 +89,13 @@ class BaseFilterSet(django_filters.FilterSet): filters.MultiValueNumberFilter, filters.MultiValueTimeFilter )): - lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP + return FILTER_NUMERIC_BASED_LOOKUP_MAP elif isinstance(existing_filter, ( filters.TreeNodeMultipleChoiceFilter, )): # TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression - lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP + return FILTER_TREENODE_NEGATION_LOOKUP_MAP elif isinstance(existing_filter, ( django_filters.ModelChoiceFilter, @@ -103,7 +103,7 @@ class BaseFilterSet(django_filters.FilterSet): TagFilter )) or existing_filter.extra.get('choices'): # These filter types support only negation - lookup_map = FILTER_NEGATION_LOOKUP_MAP + return FILTER_NEGATION_LOOKUP_MAP elif isinstance(existing_filter, ( django_filters.filters.CharFilter, @@ -111,12 +111,9 @@ class BaseFilterSet(django_filters.FilterSet): filters.MultiValueCharFilter, filters.MultiValueMACAddressFilter )): - lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP + return FILTER_CHAR_BASED_LOOKUP_MAP - else: - lookup_map = None - - return lookup_map + return None @classmethod def get_filters(cls): diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py index 8cf047c42..c3fbd0687 100644 --- a/netbox/utilities/constants.py +++ b/netbox/utilities/constants.py @@ -11,7 +11,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict( isw='istartswith', nisw='istartswith', ie='iexact', - nie='iexact' + nie='iexact', + empty='empty', ) FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(