1+ import logging
12from dataclasses import is_dataclass
23from typing import Any , Union
34
4- from django .db .models import Model
5+ from django .db .models import Model , QuerySet
56
6- from django_unicorn .components import UnicornField , UnicornView
7+ from django_unicorn .components import QueryType , UnicornField , UnicornView
78from django_unicorn .decorators import timed
89from django_unicorn .utils import get_type_hints
910
1011
12+ try :
13+ from typing import get_args , get_origin
14+ except ImportError :
15+ # Fallback to dunder methods for older versions of Python
16+ def get_args (type_hint ):
17+ if hasattr (type_hint , "__args__" ):
18+ return type_hint .__args__
19+
20+ def get_origin (type_hint ):
21+ if hasattr (type_hint , "__origin__" ):
22+ return type_hint .__origin__
23+
24+
25+ logger = logging .getLogger (__name__ )
26+
27+
1128@timed
1229def set_property_from_data (
1330 component_or_field : Union [UnicornView , UnicornField , Model ], name : str , value : Any ,
@@ -29,6 +46,8 @@ def set_property_from_data(
2946 # Re-get the field since it might have been set in `_is_component_field_model_or_unicorn_field`
3047 field = getattr (component_or_field , name )
3148
49+ type_hints = get_type_hints (component_or_field )
50+
3251 if isinstance (value , dict ):
3352 for key in value .keys ():
3453 key_value = value [key ]
@@ -37,18 +56,20 @@ def set_property_from_data(
3756 set_property_from_data (field , field .name , value )
3857 else :
3958 type_hints = get_type_hints (component_or_field )
59+ type_hint = type_hints .get (name )
4060
41- if name in type_hints :
42- # Construct the specified type by passing the value in
43- # Usually the value will be a string (because it is coming from JSON)
44- # and basic types can be constructed by passing in a string,
45- # i.e. int("1") or float("1.1")
46-
47- if is_dataclass (type_hints [name ]):
48- value = type_hints [name ](** value )
61+ if _is_queryset (field , type_hint , value ):
62+ value = _create_queryset (field , type_hint , value )
63+ elif type_hint :
64+ if is_dataclass (type_hint ):
65+ value = type_hint (** value )
4966 else :
67+ # Construct the specified type by passing the value in
68+ # Usually the value will be a string (because it is coming from JSON)
69+ # and basic types can be constructed by passing in a string,
70+ # i.e. int("1") or float("1.1")
5071 try :
51- value = type_hints [ name ] (value )
72+ value = type_hint (value )
5273 except TypeError :
5374 # Ignore this exception because some type-hints can't be instantiated like this (e.g. `List[]`)
5475 pass
@@ -107,3 +128,80 @@ def _is_component_field_model_or_unicorn_field(
107128 pass
108129
109130 return is_subclass_of_model or is_subclass_of_unicorn_field
131+
132+
133+ def _is_queryset (field , type_hint , value ):
134+ """
135+ Determines whether a field is a `QuerySet` or not based on the current instance of the
136+ component or the type hint.
137+ """
138+
139+ return (
140+ isinstance (field , QuerySet )
141+ or (type_hint and get_origin (type_hint ) is QueryType )
142+ ) and isinstance (value , list )
143+
144+
145+ def _create_queryset (field , type_hint , value ) -> QuerySet :
146+ """
147+ Create a queryset based on the `value`. If needed, the queryset will be created based on the `QueryType`.
148+
149+ For example, all of these ways fields are equivalent:
150+
151+ ```
152+ class TestComponent(UnicornView):
153+ queryset_with_empty_list: QueryType[SomeModel] = []
154+ queryset_with_none: QueryType[SomeModel] = None
155+ queryset_with_empty_queryset: QueryType[SomeModel] = SomeModel.objects.none()
156+ queryset_with_no_typehint = SomeModel.objects.none()
157+ ```
158+
159+ Params:
160+ field: Field of the component.
161+ type_hint: The optional type hint for the field.
162+ value: JSON.
163+ """
164+
165+ # Get original queryset, update it with dictionary data and then
166+ # re-set the queryset; this is required because otherwise the
167+ # property changes type from a queryset to the serialized queryset
168+ # (which is an array of dictionaries)
169+ queryset = field
170+ model_type = None
171+
172+ if type_hint and not isinstance (queryset , QuerySet ):
173+ type_arguments = get_args (type_hint )
174+
175+ if type_arguments :
176+ # First type argument should be the type of the model
177+ queryset = type_arguments [0 ].objects .none ()
178+ model_type = type_arguments [0 ]
179+
180+ if not model_type and not isinstance (queryset , QuerySet ):
181+ raise Exception (f"Getting Django Model type failed for type: { type (queryset )} " )
182+
183+ if not model_type :
184+ # Assume that `queryset` is _actually_ a QuerySet so grab the
185+ # `model` attribute in that case
186+ model_type = queryset .model
187+
188+ for model_value in value :
189+ model_found = False
190+
191+ # The following portion uses the internal `_result_cache` QuerySet API which
192+ # is private and could potentially change in the future, but not sure how
193+ # else to change internal models or append a new model to a QuerySet (probably
194+ # because it isn't really allowed)
195+ if queryset ._result_cache is None :
196+ # Explicitly set `_result_cache` to an empty list
197+ queryset ._result_cache = []
198+
199+ for (idx , model ) in enumerate (queryset ._result_cache ):
200+ if model .pk == model_value .get ("pk" ):
201+ queryset ._result_cache [idx ] = model_type (** model_value )
202+ model_found = True
203+
204+ if not model_found :
205+ queryset ._result_cache .append (model_type (** model_value ))
206+
207+ return queryset
0 commit comments