Просмотр исходного кода

Merge pull request #19912 from miaow2/19903-regexp

Closes #19903: Add `regex` and `iregex` filter lookup expressions and corresponding tests
bctiemann 6 месяцев назад
Родитель
Сommit
47320f9958
3 измененных файлов с 48 добавлено и 12 удалено
  1. 14 12
      docs/reference/filtering.md
  2. 2 0
      netbox/utilities/constants.py
  3. 32 0
      netbox/utilities/tests/test_filters.py

+ 14 - 12
docs/reference/filtering.md

@@ -80,18 +80,20 @@ GET /api/ipam/vlans/?vid__gt=900
 
 String based (char) fields (Name, Address, etc) support these lookup expressions:
 
-| 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/null (boolean)                |
+| 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/null (boolean)                |
+| `regex`  | Regexp matching                        |
+| `iregex` | Regexp matching (case-insensitive)     |
 
 Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
 

+ 2 - 0
netbox/utilities/constants.py

@@ -13,6 +13,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict(
     ie='iexact',
     nie='iexact',
     empty='empty',
+    regex='regex',
+    iregex='iregex',
 )
 
 FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(

+ 32 - 0
netbox/utilities/tests/test_filters.py

@@ -180,6 +180,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['charfield__niew'].exclude, True)
         self.assertEqual(self.filters['charfield__empty'].lookup_expr, 'empty')
         self.assertEqual(self.filters['charfield__empty'].exclude, False)
+        self.assertEqual(self.filters['charfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['charfield__regex'].exclude, False)
+        self.assertEqual(self.filters['charfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['charfield__iregex'].exclude, False)
 
     def test_number_filter(self):
         self.assertIsInstance(self.filters['numberfield'], django_filters.NumberFilter)
@@ -220,6 +224,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
         self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
+        self.assertEqual(self.filters['macaddressfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['macaddressfield__regex'].exclude, False)
+        self.assertEqual(self.filters['macaddressfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['macaddressfield__iregex'].exclude, False)
 
     def test_model_choice_filter(self):
         self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
@@ -257,6 +265,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
         self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
+        self.assertEqual(self.filters['multivaluecharfield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['multivaluecharfield__regex'].exclude, False)
+        self.assertEqual(self.filters['multivaluecharfield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['multivaluecharfield__iregex'].exclude, False)
 
     def test_multi_value_date_filter(self):
         self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
@@ -340,6 +352,10 @@ class BaseFilterSetTest(TestCase):
         self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
         self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
         self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
+        self.assertEqual(self.filters['multiplechoicefield__regex'].lookup_expr, 'regex')
+        self.assertEqual(self.filters['multiplechoicefield__regex'].exclude, False)
+        self.assertEqual(self.filters['multiplechoicefield__iregex'].lookup_expr, 'iregex')
+        self.assertEqual(self.filters['multiplechoicefield__iregex'].exclude, False)
 
     def test_tag_filter(self):
         self.assertIsInstance(self.filters['tagfield'], TagFilter)
@@ -534,6 +550,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
         params = {'slug__niew': ['-1']}
         self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2)
 
+    def test_site_slug_regex(self):
+        params = {'slug__regex': ['^def-[a-z]*-2$']}
+        self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
+
+    def test_site_slug_iregex(self):
+        params = {'slug__iregex': ['^DEF-[a-z]*-2$']}
+        self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1)
+
     def test_provider_asn_lt(self):
         params = {'asn__lt': [65101]}
         self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
@@ -618,6 +642,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
         params = {'mac_address__nic': ['aa:', 'bb']}
         self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
 
+    def test_device_mac_address_regex(self):
+        params = {'mac_address__regex': ['^cc.*:03$']}
+        self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
+
+    def test_device_mac_address_iregex(self):
+        params = {'mac_address__iregex': ['^CC.*:03$']}
+        self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)
+
     def test_interface_rf_role_empty(self):
         params = {'rf_role__empty': 'true'}
         self.assertEqual(InterfaceFilterSet(params, Interface.objects.all()).qs.count(), 5)