Repository: vitalik/django-ninja
Pull request #234 (2021-09-27 04:09:48)

Improve #181, query with list type


Patch:
diff --git a/ninja/signature/details.py b/ninja/signature/details.py
index 4a77152..bf07b61 100644
--- a/ninja/signature/details.py
+++ b/ninja/signature/details.py
@@ -1,6 +1,6 @@
 import inspect
 from collections import defaultdict, namedtuple
-from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Tuple
+from typing import Any, Callable, Dict, Generator, List, Tuple
 
 import pydantic
 
@@ -11,10 +11,6 @@ from ninja.params import Body, File, Form, _MultiPartBody
 from ninja.params_models import TModel, TModels
 from ninja.signature.utils import get_path_param_names, get_typed_signature
 
-if TYPE_CHECKING:
-    from pydantic.fields import ModelField  # pragma: no cover
-
-
 __all__ = [
     "ViewSignature",
     "is_pydantic_model",
@@ -114,7 +110,9 @@ class ViewSignature:
             attrs["__annotations__"] = {i.name: i.annotation for i in args}
 
             # collection fields:
-            attrs["_collection_fields"] = detect_collection_fields(args)
+            attrs["_collection_fields"] = detect_collection_fields(
+                args, attrs.get("_flatten_map", {})
+            )
 
             base_cls = param_cls._model
             model_cls = type(cls_name, (base_cls,), attrs)
@@ -219,26 +217,16 @@ def is_pydantic_model(cls: Any) -> bool:
 
 def is_collection_type(annotation: Any) -> bool:
     origin = get_collection_origin(annotation)
-    return origin in (
-        List,
-        list,
-        set,
-        tuple,
-    )  # TODO: I guess we should handle only list
-
-
-def detect_pydantic_model_collection_fields(model: pydantic.BaseModel) -> List[str]:
-    "Extracts collection fields aliases from collection fields"
+    types = (List, list, set, tuple)
+    if origin is None:
+        return issubclass(annotation, types)
+    else:
+        return origin in types  # TODO: I guess we should handle only list
 
-    def _list_field_name(field: "ModelField") -> Optional[str]:
-        if is_collection_type(field.outer_type_):
-            return str(field.alias)
-        return None
 
-    return list(filter(None, map(_list_field_name, model.__fields__.values())))
-
-
-def detect_collection_fields(args: List[FuncParam]) -> List[str]:
+def detect_collection_fields(
+    args: List[FuncParam], flatten_map: Dict[str, Tuple[str, ...]]
+) -> List[str]:
     """
     QueryDict has values that are always lists, so we need to help django ninja to understand
     better the input parameters if it's a list or a single value
@@ -246,7 +234,25 @@ def detect_collection_fields(args: List[FuncParam]) -> List[str]:
     """
     result = [i.name for i in args if i.is_collection]
 
-    if len(args) == 1 and is_pydantic_model(args[0].annotation):
-        result += detect_pydantic_model_collection_fields(args[0].annotation)
+    if flatten_map:
+        args_d = {arg.alias: arg for arg in args}
+        for path in (p for p in flatten_map.values() if len(p) > 1):
+            annotation_or_field = args_d[path[0]].annotation
+            for attr in path[1:]:
+                annotation_or_field = next(
+                    (
+                        a
+                        for a in annotation_or_field.__fields__.values()
+                        if a.alias == attr
+                    ),
+                    annotation_or_field.__fields__.get(attr),
+                )  # pragma: no cover
+
+                annotation_or_field = getattr(
+                    annotation_or_field, "outer_type_", annotation_or_field
+                )
+
+            if is_collection_type(annotation_or_field):
+                result.append(path[-1])
 
     return result


Test patch:
diff --git a/tests/test_lists.py b/tests/test_lists.py
index c399a67..e6743d5 100644
--- a/tests/test_lists.py
+++ b/tests/test_lists.py
@@ -2,7 +2,7 @@ from typing import List
 
 import pytest
 from django.http import QueryDict  # noqa
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, conlist
 
 from ninja import Form, Query, Router, Schema
 from ninja.testing import TestClient
@@ -69,6 +69,26 @@ def listview4(
     }
 
 
+class ConListSchema(Schema):
+    query: conlist(int, min_items=1)
+
+
+class Data(Schema):
+    data: ConListSchema
+
+
+@router.post("/list5")
+def listview5(
+    request,
+    body: conlist(int, min_items=1),
+    a_query: Data = Query(...),
+):
+    return {
+        "query": a_query.data.query,
+        "body": body,
+    }
+
+
 client = TestClient(router)
 
 
@@ -116,6 +136,11 @@ client = TestClient(router)
             {},
             {"filters": {"tags": [], "other_tags": []}},
         ),
+        (
+            "/list5?query=1&query=2",
+            dict(json=[5, 6]),
+            {"query": [1, 2], "body": [5, 6]},
+        ),
     ]
     # fmt: on
 )