diff --git a/src/capyc/django/serializer.py b/src/capyc/django/serializer.py index 0b0db3e..6356c2d 100644 --- a/src/capyc/django/serializer.py +++ b/src/capyc/django/serializer.py @@ -1033,13 +1033,15 @@ def _validate_filter(cls, x: str, parents: Optional[list[str]] = None) -> Filter if "," in value: value = value.split(",") - operation = "in" + operation += "__in" field = cls._rewrites.get(field, field) return None, {"field": field, "operation": operation, "value": value, "parents": parents} elif "~=" in x: + print("here") + field, value = x.split("~=") handler, error_handler, supported_operations = cls._filter_map[field] @@ -1051,9 +1053,10 @@ def _validate_filter(cls, x: str, parents: Optional[list[str]] = None) -> Filter if "," in value: value = value.split(",") - operation = "in" + operation += "__in" field = cls._rewrites.get(field, field) + print(field, operation, value) return {"field": field, "operation": operation, "value": value, "parents": parents}, None @@ -1187,16 +1190,45 @@ def _validate_child_filter( return ser._validate_child_filter(".".join(rest), parents + [forward]) def _query_filter(self, qs: QuerySet) -> QuerySet: + from django.db.models import Q + def build_filter(filters: list[FilterOperation]): - res = {} + named = {} + unnamed = [] for filter in filters: + is_iexact_in = filter["operation"] == "iexact__in" or filter["operation"] == "in__iexact" + + if filter["parents"] and is_iexact_in: + print(1) + query = Q() + for value in filter["value"]: + kwargs = {} + kwargs["__".join(filter["parents"]) + f"__{filter['field']}__iexact"] = value + query |= Q(**kwargs) + + unnamed.append(query) + + elif filter["parents"]: + print(2) + named["__".join(filter["parents"]) + f"__{filter['field']}__{filter['operation']}"] = filter[ + "value" + ] + + elif is_iexact_in: + print(3) + query = Q() + for value in filter["value"]: + kwargs = {} + kwargs[f"{filter['field']}__iexact"] = value + query |= Q(**kwargs) + + unnamed.append(query) - if filter["parents"]: - res["__".join(filter["parents"]) + f"__{filter['field']}__{filter['operation']}"] = filter["value"] else: - res[f"{filter['field']}__{filter['operation']}"] = filter["value"] + print(4) + named[f"{filter['field']}__{filter['operation']}"] = filter["value"] - return res + return unnamed, named query_filters: list[FilterOperation] = [] exclude_filters: list[FilterOperation] = [] @@ -1226,11 +1258,16 @@ def build_filter(filters: list[FilterOperation]): if exclude: exclude_filters.append(exclude) + print(build_filter(query_filters)) + print(build_filter(exclude_filters)) + if query_filters: - qs = qs.filter(**build_filter(query_filters)) + args, kwargs = build_filter(query_filters) + qs = qs.filter(*args, **kwargs) if exclude_filters: - qs = qs.exclude(**build_filter(exclude_filters)) + args, kwargs = build_filter(exclude_filters) + qs = qs.exclude(*args, **kwargs) return qs diff --git a/tests/django/test_serializer.py b/tests/django/test_serializer.py index 6ca50a4..d12e376 100644 --- a/tests/django/test_serializer.py +++ b/tests/django/test_serializer.py @@ -26,6 +26,7 @@ class ContentTypeSerializer(Serializer): } filters = ("app_label",) depth = 2 + ttl = 2 # duplicate @@ -671,12 +672,21 @@ def test_permission__iexact__in(self, database: capy.Database, django_assert_num serializer = PermissionSerializer(request=request) assert serializer.filter(id__in=[x.id for x in model.permission]) == { - "count": 0, + "count": 2, "first": "/permission?limit=20&offset=0", "last": "/permission?limit=20&offset=0", "next": None, "previous": None, - "results": [], + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], } # count + select @@ -692,21 +702,12 @@ def test_permission__not_iexact__in(self, database: capy.Database, django_assert serializer = PermissionSerializer(request=request) assert serializer.filter(id__in=[x.id for x in model.permission]) == { - "count": 2, + "count": 0, "first": "/permission?limit=20&offset=0", "last": "/permission?limit=20&offset=0", "next": None, "previous": None, - "results": [ - { - "id": model.permission[0].id, - "name": model.permission[0].name, - }, - { - "id": model.permission[1].id, - "name": model.permission[1].name, - }, - ], + "results": [], } # count + select @@ -964,6 +965,9 @@ class TestFilterM2M: # count + select def test_permission__exact(self, database: capy.Database, django_assert_num_queries): model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + factory = APIRequestFactory() request = factory.get(f"/notes/547/?groups.name={model.group[0].name}") @@ -971,7 +975,7 @@ def test_permission__exact(self, database: capy.Database, django_assert_num_quer serializer = PermissionSerializer(request=request) assert serializer.filter(id__in=[x.id for x in model.permission]) == { - "count": 2, + "count": 1, "first": "/permission?limit=20&offset=0", "last": "/permission?limit=20&offset=0", "next": None, @@ -981,6 +985,28 @@ def test_permission__exact(self, database: capy.Database, django_assert_num_quer "id": model.permission[0].id, "name": model.permission[0].name, }, + ], + } + + # count + select + def test_permission__not_exact(self, database: capy.Database, django_assert_num_queries): + model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name!={model.group[0].name}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ { "id": model.permission[1].id, "name": model.permission[1].name, @@ -988,11 +1014,119 @@ def test_permission__exact(self, database: capy.Database, django_assert_num_quer ], } + # count + select def test_permission__iexact(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): model = database.create(permission=2, group=[{"name": fake.name().upper()} for _ in range(2)]) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + factory = APIRequestFactory() request = factory.get(f"/notes/547/?groups.name~={model.group[0].name.lower()}") + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + ], + } + + # count + select + def test_permission__not_iexact(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=[{"name": fake.name().upper()} for _ in range(2)]) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name!~={model.group[0].name.lower()}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__lookup(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name[startswith]={model.group[0].name[:3]}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + ], + } + + # count + select + def test_permission__not_lookup(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name![startswith]={model.group[0].name[:3]}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name={model.group[0].name},{model.group[1].name}") + with django_assert_num_queries(2) as captured: serializer = PermissionSerializer(request=request) @@ -1014,10 +1148,373 @@ def test_permission__iexact(self, database: capy.Database, django_assert_num_que ], } - def test_permission__not_iexact(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + # count + select + def test_permission__not_in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=2) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name!={model.group[0].name},{model.group[1].name}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 0, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [], + } + + # count + select + def test_permission__iexact__in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): model = database.create(permission=2, group=[{"name": fake.name().upper()} for _ in range(2)]) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + factory = APIRequestFactory() - request = factory.get(f"/notes/547/?groups.name!~={model.group[0].name.lower()}") + request = factory.get(f"/notes/547/?groups.name~={model.group[0].name.lower()},{model.group[1].name.lower()}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 2, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__iexact__not_in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, group=[{"name": fake.name().upper()} for _ in range(2)]) + model.group[0].permissions.set([model.permission[0]]) + model.group[1].permissions.set([model.permission[1]]) + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?groups.name!~={model.group[0].name.lower()},{model.group[1].name.lower()}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 0, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [], + } + + +class TestFilterM2O: + # count + select + def test_permission__exact(self, database: capy.Database, django_assert_num_queries): + model = database.create(permission=2, content_type=2) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label={model.content_type[0].app_label}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + ], + } + + # count + select + def test_permission__not_exact(self, database: capy.Database, django_assert_num_queries): + model = database.create(permission=2, content_type=2) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label!={model.content_type[0].app_label}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__iexact(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=[{"app_label": fake.name().upper()} for _ in range(2)]) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label~={model.content_type[0].app_label.lower()}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + ], + } + + # count + select + def test_permission__not_iexact(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=[{"app_label": fake.name().upper()} for _ in range(2)]) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label!~={model.content_type[0].app_label.lower()}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__lookup(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=[{"app_label": fake.name().upper()} for _ in range(2)]) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label[startswith]={model.content_type[0].app_label[:3]}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + ], + } + + # count + select + def test_permission__not_lookup(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=2) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get(f"/notes/547/?content_type.app_label![startswith]={model.content_type[0].app_label[:3]}") + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 1, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=2) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get( + f"/notes/547/?content_type.app_label={model.content_type[0].app_label},{model.content_type[1].app_label}" + ) + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 2, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__not_in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=2) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get( + f"/notes/547/?content_type.app_label!={model.content_type[0].app_label},{model.content_type[1].app_label}" + ) + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 0, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [], + } + + # count + select + def test_permission__iexact__in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=[{"app_label": fake.name().upper()} for _ in range(2)]) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get( + f"/notes/547/?content_type.app_label~={model.content_type[0].app_label.lower()},{model.content_type[1].app_label.lower()}" + ) + + with django_assert_num_queries(2) as captured: + serializer = PermissionSerializer(request=request) + + assert serializer.filter(id__in=[x.id for x in model.permission]) == { + "count": 2, + "first": "/permission?limit=20&offset=0", + "last": "/permission?limit=20&offset=0", + "next": None, + "previous": None, + "results": [ + { + "id": model.permission[0].id, + "name": model.permission[0].name, + }, + { + "id": model.permission[1].id, + "name": model.permission[1].name, + }, + ], + } + + # count + select + def test_permission__iexact__not_in(self, database: capy.Database, django_assert_num_queries, fake: capy.Fake): + model = database.create(permission=2, content_type=[{"app_label": fake.name().upper()} for _ in range(2)]) + + model.permission[0].content_type = model.content_type[0] + model.permission[0].save() + + model.permission[1].content_type = model.content_type[1] + model.permission[1].save() + + factory = APIRequestFactory() + request = factory.get( + f"/notes/547/?content_type.app_label!~={model.content_type[0].app_label.lower()},{model.content_type[1].app_label.lower()}" + ) with django_assert_num_queries(2) as captured: serializer = PermissionSerializer(request=request)