From 730813da1eeb9e660de8d32dfc88a6b3ce602eb9 Mon Sep 17 00:00:00 2001 From: crackeroil Date: Tue, 4 Mar 2025 10:55:00 +0100 Subject: [PATCH] Add icontains support to IntegerField --- README.md | 2 +- binder/models.py | 7 ++- tests/filters/test_integer_icontains.py | 63 +++++++++++++++++++++++ tests/plugins/test_loaded_values_mixin.py | 3 ++ tests/testapp/models/zoo_employee.py | 1 + 5 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/filters/test_integer_icontains.py diff --git a/README.md b/README.md index bec26022..1f8107fa 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Code Yellow backend framework for SPA webapps with REST-like API. - Run with `./test` - Access the test database directly by with `docker compose run --rm db psql -h db -U postgres`. -- It may be possible to recreate the test database (for example when you added/changed models). One way of achieving this is to just remove all the docker images that were build `docker compose rm`. The database will be created during the setup in `tests/__init__.py`. +- It may be possible to recreate the test database (for example when you added/changed models). One way of achieving this is to just remove all the docker images that were build `docker compose rm` or `docker compose down db binder`. The database will be created during the setup in `tests/__init__.py`. The tests are set up in such a way that there is no need to keep migration files. The setup procedure in `tests/__init__.py` handles the preparation of the database by directly calling some build-in Django commands. diff --git a/binder/models.py b/binder/models.py index fbcc341c..f38b7875 100644 --- a/binder/models.py +++ b/binder/models.py @@ -244,11 +244,14 @@ class IntegerFieldFilter(FieldFilter): models.ManyToManyField, models.ManyToManyRel, ] - allowed_qualifiers = [None, 'in', 'gt', 'gte', 'lt', 'lte', 'range', 'isnull'] + allowed_qualifiers = [None, 'in', 'gt', 'gte', 'lt', 'lte', 'range', 'isnull', 'icontains'] def clean_value(self, qualifier, v): try: - return int(v) + if qualifier == 'icontains': + return str(v) + else: + return int(v) except ValueError: raise ValidationError('Invalid value {{{}}} for {}.'.format(v, self.field_description())) diff --git a/tests/filters/test_integer_icontains.py b/tests/filters/test_integer_icontains.py new file mode 100644 index 00000000..2761c50d --- /dev/null +++ b/tests/filters/test_integer_icontains.py @@ -0,0 +1,63 @@ +import unittest, os + +from django.test import TestCase, Client +from django.contrib.auth.models import User + +from binder.json import jsonloads + +from ..testapp.models import Zoo, ZooEmployee + + +@unittest.skipIf( + os.environ.get('BINDER_TEST_MYSQL', '0') != '0', + "Only available with PostgreSQL" +) +class IntegerIcontainsTest(TestCase): + + def setUp(self): + super().setUp() + u = User(username='testuser', is_active=True, is_superuser=True) + u.set_password('test') + u.save() + + self.client = Client() + r = self.client.login(username='testuser', password='test') + self.assertTrue(r) + + zoo = Zoo(name='Yes') + zoo.save() + + ZooEmployee(name='Small Number Fan', favorite_number='3', zoo=zoo).save() + ZooEmployee(name='Big Number Enjoyer', favorite_number='100023', zoo=zoo).save() + ZooEmployee(name='Bob', favorite_number='101', zoo=zoo).save() + + def test_filter_partial_match(self): + response = self.client.get('/zoo_employee/', data={'.favorite_number:icontains': '3'}) + + self.assertEqual(response.status_code, 200) + + result = jsonloads(response.content) + self.assertEqual(2, len(result['data'])) + self.assertEqual('Small Number Fan', result['data'][0]['name']) + self.assertEqual('Big Number Enjoyer', result['data'][1]['name']) + + response = self.client.get('/zoo_employee/', data={'.favorite_number:icontains': '100'}) + + result = jsonloads(response.content) + self.assertEqual(1, len(result['data'])) + self.assertEqual('Big Number Enjoyer', result['data'][0]['name']) + + def test_filter_exact_match(self): + response = self.client.get('/zoo_employee/', data={'.favorite_number': '3'}) + + self.assertEqual(response.status_code, 200) + + result = jsonloads(response.content) + self.assertEqual(1, len(result['data'])) + self.assertEqual('Small Number Fan', result['data'][0]['name']) + + response = self.client.get('/zoo_employee/', data={'.favorite_number': '101'}) + + result = jsonloads(response.content) + self.assertEqual(1, len(result['data'])) + self.assertEqual('Bob', result['data'][0]['name']) diff --git a/tests/plugins/test_loaded_values_mixin.py b/tests/plugins/test_loaded_values_mixin.py index bd579441..9052ac99 100644 --- a/tests/plugins/test_loaded_values_mixin.py +++ b/tests/plugins/test_loaded_values_mixin.py @@ -175,6 +175,7 @@ def test_non_nullable_field_does_not_error_out_when_accessing(self): 'id': None, 'name': 'Henk', 'zoo': None, + 'favorite_number': None, 'hourly_wage': None, 'deleted': False, }, zoo_employee.get_old_values()) @@ -197,6 +198,7 @@ def test_non_nullable_field_does_not_error_out_when_accessing(self): 'id': zoo_employee.id, 'name': 'Henk', 'zoo': artis.id, + 'favorite_number': None, 'hourly_wage': None, 'deleted': False, }, zoo_employee.get_old_values()) @@ -216,6 +218,7 @@ def test_non_nullable_field_does_not_error_out_when_accessing(self): 'id': zoo_employee.id, 'name': 'Henk', 'zoo': artis.id, + 'favorite_number': None, 'hourly_wage': None, 'deleted': False, }, zoo_employee.get_old_values()) diff --git a/tests/testapp/models/zoo_employee.py b/tests/testapp/models/zoo_employee.py index 12ec8abc..9dd6939b 100644 --- a/tests/testapp/models/zoo_employee.py +++ b/tests/testapp/models/zoo_employee.py @@ -7,6 +7,7 @@ class ZooEmployee(LoadedValuesMixin, BinderModel): name = models.TextField(max_length=64) zoo = models.ForeignKey('Zoo', on_delete=models.CASCADE, related_name='zoo_employees') hourly_wage = models.DecimalField(max_digits=4, decimal_places=2, null=True, blank=True) + favorite_number = models.IntegerField(null=True, blank=True) deleted = models.BooleanField(default=False) # Softdelete def __str__(self):