django-camomilla-cms 6.0.0b2__py2.py3-none-any.whl → 6.0.0b4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/apps.py +3 -0
  3. camomilla/contrib/modeltranslation/hvad_migration.py +1 -2
  4. camomilla/contrib/rest_framework/serializer.py +31 -1
  5. camomilla/dynamic_pages_urls.py +7 -1
  6. camomilla/fields/json.py +12 -9
  7. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  8. camomilla/model_api.py +6 -4
  9. camomilla/models/__init__.py +5 -5
  10. camomilla/models/article.py +0 -1
  11. camomilla/models/media.py +1 -2
  12. camomilla/models/menu.py +44 -42
  13. camomilla/models/mixins/__init__.py +0 -1
  14. camomilla/models/page.py +66 -32
  15. camomilla/openapi/schema.py +27 -0
  16. camomilla/parsers.py +0 -1
  17. camomilla/serializers/fields/json.py +7 -75
  18. camomilla/serializers/mixins/__init__.py +16 -2
  19. camomilla/serializers/page.py +47 -0
  20. camomilla/serializers/user.py +2 -3
  21. camomilla/serializers/utils.py +22 -17
  22. camomilla/settings.py +21 -1
  23. camomilla/storages/optimize.py +1 -1
  24. camomilla/structured/__init__.py +90 -75
  25. camomilla/structured/cache.py +193 -0
  26. camomilla/structured/fields.py +132 -275
  27. camomilla/structured/models.py +45 -138
  28. camomilla/structured/utils.py +114 -0
  29. camomilla/templatetags/camomilla_filters.py +0 -1
  30. camomilla/theme/__init__.py +1 -1
  31. camomilla/theme/admin.py +96 -0
  32. camomilla/theme/apps.py +12 -1
  33. camomilla/translation.py +4 -2
  34. camomilla/urls.py +13 -6
  35. camomilla/utils/__init__.py +1 -1
  36. camomilla/utils/getters.py +11 -1
  37. camomilla/utils/templates.py +2 -2
  38. camomilla/utils/translation.py +9 -6
  39. camomilla/views/__init__.py +1 -1
  40. camomilla/views/articles.py +0 -1
  41. camomilla/views/contents.py +0 -1
  42. camomilla/views/decorators.py +26 -0
  43. camomilla/views/medias.py +1 -2
  44. camomilla/views/menus.py +45 -1
  45. camomilla/views/pages.py +13 -1
  46. camomilla/views/tags.py +0 -1
  47. camomilla/views/users.py +0 -2
  48. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/METADATA +4 -3
  49. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/RECORD +53 -49
  50. tests/test_api.py +1 -0
  51. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/LICENSE +0 -0
  52. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/WHEEL +0 -0
  53. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,193 @@
1
+ from collections import defaultdict
2
+ from inspect import isclass
3
+ from typing import Any, Dict, Sequence
4
+ from typing_extensions import get_args, get_origin
5
+ from camomilla.settings import STRUCTURED_FIELD_CACHE_ENABLED
6
+ from camomilla.structured.fields import ForeignKey, QuerySet
7
+ from camomilla.structured.models import BaseModel
8
+ from camomilla.structured.utils import _LazyType, get_type, pointed_setter
9
+
10
+ from camomilla.utils.getters import pointed_getter
11
+
12
+ # TODO:
13
+ # ::: Actually this is a first draft.
14
+ # The system should be more type safe.
15
+ # It should handle initialization made with model instance not only dicts
16
+ # Neet to check if there are problems with different formats for pks (model instaces or string or dicts)
17
+
18
+
19
+ class ValueWithCache:
20
+ def __init__(self, cache, model, value) -> None:
21
+ self.cache = cache
22
+ self.value = value
23
+ self.model = model
24
+
25
+ def retrieve(self):
26
+ cache = self.cache.get(self.model)
27
+ if isinstance(self.value, Sequence):
28
+ qs = self.model._default_manager.filter(pk__in=self.value)
29
+ setattr(
30
+ qs,
31
+ "_result_cache",
32
+ [cache[i] for i in self.value if i in cache] if cache else [],
33
+ )
34
+ return qs
35
+ else:
36
+ val = cache.get(self.value, None)
37
+ if val is None:
38
+ return self.model._default_manager.get(pk=self.value)
39
+ else:
40
+ return val
41
+
42
+
43
+ class RelInfo:
44
+ FKField: str = "fk"
45
+ QSField: str = "qs"
46
+ RIField: str = "rel"
47
+ RLField: str = "rel_l"
48
+
49
+ def __init__(self, model, type, field) -> None:
50
+ self.model = model
51
+ self.type = type
52
+ self.field = field
53
+
54
+
55
+ class CacheBuilder:
56
+ def __init__(self, related_fields: Dict[str, RelInfo]) -> None:
57
+ self.__related_fields__ = related_fields
58
+
59
+ @classmethod
60
+ def from_model(cls, model: BaseModel) -> 'CacheBuilder':
61
+ return cls(related_fields=cls.inspect_related_fields(model))
62
+
63
+ @classmethod
64
+ def inspect_related_fields(cls, model: type[BaseModel]) -> Dict[str, RelInfo]:
65
+ related = {}
66
+ for field_name, field in model.model_fields.items():
67
+ annotation = field.annotation
68
+ origin = get_origin(annotation)
69
+ args = get_args(annotation)
70
+
71
+ if isclass(origin) and issubclass(origin, ForeignKey):
72
+ related[field_name] = RelInfo(
73
+ get_type(annotation), RelInfo.FKField, field
74
+ )
75
+ elif isclass(origin) and issubclass(origin, QuerySet):
76
+ related[field_name] = RelInfo(
77
+ get_type(annotation), RelInfo.QSField, field
78
+ )
79
+
80
+ elif isclass(origin) and issubclass(origin, Sequence):
81
+ lazy_types = [
82
+ _LazyType(arg).evaluate(model)
83
+ for arg in args
84
+ if isinstance(arg, str)
85
+ ]
86
+ subclass = next(
87
+ (
88
+ c
89
+ for c in list(args) + lazy_types
90
+ if isclass(c) and issubclass(c, BaseModel)
91
+ ),
92
+ None,
93
+ )
94
+ if isclass(subclass) and issubclass(subclass, BaseModel):
95
+ related[field_name] = RelInfo(subclass, RelInfo.RLField, field)
96
+
97
+ elif isclass(annotation) and issubclass(annotation, BaseModel):
98
+ related[field_name] = RelInfo(annotation, RelInfo.RIField, field)
99
+ return related
100
+
101
+ def get_all_fk_data(self, data):
102
+ if isinstance(data, Sequence):
103
+ fk_data = defaultdict(list)
104
+ for index in range(len(data)):
105
+ child_fk_data = self.get_all_fk_data(data[index])
106
+ for model, touples in child_fk_data.items():
107
+ fk_data[model] += [(f"{index}.{t[0]}", t[1]) for t in touples]
108
+ return fk_data
109
+ return self.get_fk_data(data)
110
+
111
+ def get_fk_data(self, data):
112
+ fk_data = defaultdict(list)
113
+ if not data:
114
+ return fk_data
115
+ for field_name, info in self.__related_fields__.items():
116
+ if info.type == RelInfo.FKField:
117
+ value = pointed_getter(data, field_name, None)
118
+ if value:
119
+ if isinstance(value, ValueWithCache): # needed to break recursive cache builds
120
+ fk_data = {}
121
+ break
122
+ fk_data[info.model].append(
123
+ (
124
+ field_name,
125
+ pointed_getter(value, info.model._meta.pk.attname, value),
126
+ )
127
+ )
128
+ elif info.type == RelInfo.QSField:
129
+ value = pointed_getter(data, field_name, [])
130
+ if isinstance(value, list):
131
+ if any(True for v in value if isinstance(v, ValueWithCache)): # needed to break recursive cache builds
132
+ fk_data = {}
133
+ break
134
+ fk_data[info.model].append(
135
+ (
136
+ field_name,
137
+ [
138
+ pointed_getter(i, info.model._meta.pk.attname, i)
139
+ for i in value
140
+ if i
141
+ ],
142
+ )
143
+ )
144
+
145
+ elif info.type == RelInfo.RLField:
146
+ values = pointed_getter(data, field_name, [])
147
+ if isinstance(values, list):
148
+ for index in range(len(values)):
149
+ for model, touples in (
150
+ self.from_model(info.model)
151
+ .get_all_fk_data(values[index])
152
+ .items()
153
+ ):
154
+ fk_data[model] += [
155
+ (f"{field_name}.{index}.{t[0]}", t[1]) for t in touples
156
+ ]
157
+ elif info.type == RelInfo.RIField:
158
+ value = pointed_getter(data, field_name, None)
159
+ child_fk_data = self.from_model(info.model).get_all_fk_data(value)
160
+ for model, touples in child_fk_data.items():
161
+ fk_data[model] += [(f"{field_name}.{t[0]}", t[1]) for t in touples]
162
+ return fk_data
163
+
164
+ def inject_cache(self, data: Any) -> Any:
165
+ if not STRUCTURED_FIELD_CACHE_ENABLED:
166
+ return data
167
+ fk_data = self.get_all_fk_data(data)
168
+ plainset = defaultdict(set)
169
+ for model, touples in fk_data.items():
170
+ for t in touples:
171
+ if isinstance(t[1], Sequence):
172
+ plainset[model].update(t[1])
173
+ else:
174
+ plainset[model].add(t[1])
175
+ cache = {}
176
+ for model, values in plainset.items():
177
+ models = []
178
+ pks = []
179
+ for value in values:
180
+ if isinstance(value, model):
181
+ models.append(value)
182
+ else:
183
+ pks.append(value)
184
+ models_pks = [m.pk for m in models]
185
+ pks = [pk for pk in pks if pk not in models_pks]
186
+ if len(pks):
187
+ models += list(model.objects.filter(pk__in=pks))
188
+ cache[model] = {obj.pk: obj for obj in models}
189
+
190
+ for model, touples in fk_data.items():
191
+ for t in touples:
192
+ pointed_setter(data, t[0], ValueWithCache(cache, model, t[1]))
193
+ return data
@@ -1,292 +1,149 @@
1
- import datetime
2
- import decimal
1
+ from typing import Any, Callable, Dict, Generic, TypeVar, Union
3
2
 
4
- from django.core import validators
5
- from django.db import models
6
- from django.utils.dateparse import parse_duration
7
- from django.utils.duration import duration_string
8
- from jsonmodels import fields
9
- from jsonmodels.validators import ValidationError
10
- from camomilla.utils.getters import pointed_getter
3
+ from django.db import models as django_models
4
+ from pydantic import SerializationInfo
5
+ from pydantic_core import core_schema as cs
11
6
 
7
+ from .utils import get_type
8
+ from devtools import debug
12
9
 
13
- __all__ = [
14
- "CharField",
15
- "DecimalField",
16
- "IntegerField",
17
- "FloatField",
18
- "BooleanField",
19
- "DateTimeField",
20
- "DateField",
21
- "TimeField",
22
- "DurationField",
23
- "EmailField",
24
- "SlugField",
25
- "URLField",
26
- "ListField",
27
- "DictField",
28
- "EmbeddedField",
29
- "ForeignKey",
30
- "ForeignKeyList",
31
- ]
32
10
 
11
+ T = TypeVar("T", bound=django_models.Model)
33
12
 
34
- class Field(fields.BaseField):
35
- additional_kwargs = {}
36
- additional_validators = []
37
- __validators = []
38
- parent = None
39
-
40
- def __init__(self, *args, **kwargs):
41
- for key, value in self.additional_kwargs.items():
42
- setattr(self, key, kwargs.pop(key, value))
43
- super(Field, self).__init__(*args, **kwargs)
44
-
45
- @property
46
- def validators(self):
47
- validators = self.__validators
48
- for validator in self.additional_validators:
49
- if isinstance(validator, dict):
50
- validator_class = validator["validator"]
51
- args = [
52
- getattr(self, arg)
53
- for arg in validator.get("args", [])
54
- if getattr(self, arg, fields.NotSet) != fields.NotSet
55
- ]
56
- kwargs = {
57
- key: getattr(self, value)
58
- for key, value in validator.get("kwargs", {}).items()
59
- if getattr(self, value, fields.NotSet) != fields.NotSet
60
- }
61
- if len(args) or len(kwargs.keys()):
62
- validators.append(validator_class(*args, **kwargs))
63
- else:
64
- validators.append(validator)
65
- return validators
66
-
67
- @validators.setter
68
- def validators(self, value):
69
- self.__validators = value
70
-
71
- def bind(self, parent):
72
- self.parent = parent
73
-
74
- @classmethod
75
- def to_db_transform(cls, data):
76
- return data
77
13
 
14
+ class ForeignKey(django_models.Model, Generic[T]):
78
15
  @classmethod
79
- def from_db_transform(cls, data):
80
- return data
81
-
82
-
83
- class CharField(fields.StringField, Field):
84
- additional_kwargs = {
85
- "max_length": fields.NotSet,
86
- }
87
- additional_validators = [
88
- {"validator": validators.MaxLengthValidator, "args": ["max_length"]}
89
- ]
90
-
91
-
92
- class DecimalField(fields.StringField, Field):
93
- types = (decimal.Decimal, int, float)
94
-
95
- additional_kwargs = {
96
- "max_digits": fields.NotSet,
97
- "decimal_places": fields.NotSet,
98
- }
99
- additional_validators = [
100
- {
101
- "validator": validators.DecimalValidator,
102
- "args": ["max_digits", "decimal_places"],
103
- }
104
- ]
105
-
106
-
107
- class IntegerField(fields.IntField, Field):
108
- additional_kwargs = {
109
- "min_value": fields.NotSet,
110
- "max_value": fields.NotSet,
111
- }
112
- additional_validators = [
113
- {"validator": validators.MinValueValidator, "args": ["min_value"]},
114
- {"validator": validators.MaxValueValidator, "args": ["max_value"]},
115
- ]
116
-
117
-
118
- class FloatField(IntegerField):
119
- types = (float, int)
120
-
121
-
122
- class BooleanField(fields.BoolField, Field):
123
- pass
124
-
125
-
126
- class DateTimeField(fields.DateTimeField, Field):
127
- pass
128
-
129
-
130
- class DateField(fields.DateField, Field):
131
- pass
132
-
133
-
134
- class TimeField(fields.TimeField, Field):
135
- pass
136
-
137
-
138
- class DurationField(fields.StringField, Field):
139
- types = (datetime.timedelta,)
140
-
141
- def to_struct(self, value):
142
- return value and duration_string(value)
143
-
144
- def parse_value(self, value):
145
- if value is None:
146
- return value
147
- if isinstance(value, datetime.timedelta):
148
- return value
149
- try:
150
- parsed = parse_duration(value)
151
- except ValueError:
152
- pass
153
- else:
154
- if parsed is not None:
155
- return parsed
156
-
157
- raise fields.ValidationError(
158
- "“%(value)s” value has an invalid format. It must be in '[DD] [[HH:]MM:]ss[.uuuuuu] format.'".format(
159
- value=value
160
- )
16
+ def __get_pydantic_core_schema__(
17
+ cls, source: Any, handler: Callable[[Any], cs.CoreSchema]
18
+ ) -> cs.CoreSchema:
19
+ from camomilla.structured.cache import ValueWithCache
20
+
21
+ model_class = get_type(source)
22
+
23
+ def validate_from_pk(pk: Union[int, str]) -> model_class:
24
+ return model_class._default_manager.get(pk=pk)
25
+
26
+ int_str_union = cs.union_schema([cs.str_schema(), cs.int_schema()])
27
+ from_pk_schema = cs.chain_schema(
28
+ [
29
+ int_str_union,
30
+ cs.no_info_plain_validator_function(validate_from_pk),
31
+ ]
161
32
  )
33
+ pk_attname = model_class._meta.pk.attname
162
34
 
35
+ def validate_from_dict(data: Dict[str, Union[str, int]]) -> model_class:
36
+ return validate_from_pk(data[pk_attname])
163
37
 
164
- class EmailField(CharField):
165
- additional_validators = [validators.validate_email]
166
-
167
-
168
- class SlugField(CharField):
169
- additional_validators = [validators.validate_slug]
170
-
38
+ from_dict_schema = cs.chain_schema(
39
+ [
40
+ cs.typed_dict_schema({pk_attname: cs.typed_dict_field(int_str_union)}),
41
+ cs.no_info_plain_validator_function(validate_from_dict),
42
+ ]
43
+ )
171
44
 
172
- class URLField(CharField):
173
- additional_validators = [validators.URLValidator()]
45
+ from_cache_schema = cs.chain_schema(
46
+ [
47
+ cs.is_instance_schema(ValueWithCache),
48
+ cs.no_info_plain_validator_function(lambda v: v.retrieve()),
49
+ ]
50
+ )
51
+
52
+ def serialize_data(instance, info):
53
+ from camomilla.serializers.utils import build_standard_model_serializer
54
+ if info.mode == "python":
55
+ serializer = build_standard_model_serializer(model_class, depth=1)
56
+ return serializer(instance=instance).data
57
+ return instance.pk
58
+
59
+ return cs.json_or_python_schema(
60
+ json_schema=cs.union_schema(
61
+ [from_cache_schema, from_pk_schema, from_dict_schema]
62
+ ),
63
+ python_schema=cs.union_schema(
64
+ [
65
+ cs.is_instance_schema(model_class),
66
+ from_cache_schema,
67
+ from_pk_schema,
68
+ from_dict_schema,
69
+ ]
70
+ ),
71
+ serialization=cs.plain_serializer_function_ser_schema(
72
+ serialize_data, info_arg=True
73
+ ),
74
+ )
174
75
 
175
76
 
176
- class ListField(fields.ListField, Field):
177
-
178
- def parse_value(self, values):
179
- """Cast value to proper collection."""
180
- result = self.get_default_value()
181
- if not values:
182
- return result
183
- if not isinstance(values, list):
184
- return values
185
- parent = self.parent
186
- return [self._cast_value(value, parent) for value in values]
187
-
188
- def _cast_value(self, value, parent):
189
- if isinstance(value, self.items_types):
190
- return value
191
- else:
192
- main_type = self._get_main_type()
193
- from camomilla.structured.models import Model
194
- if issubclass(main_type, Model):
195
- instance = main_type()
196
- relations = instance.prepopulate(**value)
197
- instance.bind(parent)
198
- instance.populate(**relations)
199
- return instance
200
- return main_type(**value)
201
-
202
- def _get_main_type(self):
203
- if len(self.items_types) != 1:
204
- raise ValidationError(
205
- 'Cannot decide which type to choose from "{items_types}".'.format(
206
- items_types=", ".join([t.__name__ for t in self.items_types])
207
- )
77
+ class QuerySet(Generic[T]):
78
+ @classmethod
79
+ def __get_pydantic_core_schema__(
80
+ cls, source: Any, handler: Callable[[Any], cs.CoreSchema]
81
+ ) -> cs.CoreSchema:
82
+ from camomilla.structured.cache import ValueWithCache
83
+
84
+ model_class = get_type(source)
85
+
86
+ def validate_from_pk_list(
87
+ values: list[Union[int, str]]
88
+ ) -> django_models.QuerySet:
89
+ preserved = django_models.Case(
90
+ *[django_models.When(pk=pk, then=pos) for pos, pk in enumerate(values)]
208
91
  )
209
- return self.items_types[0]
210
-
211
- class DictField(fields.DictField, Field):
212
- pass
213
-
214
-
215
- class EmbeddedField(fields.EmbeddedField, Field):
216
- def parse_value(self, value):
217
- if not isinstance(value, dict):
218
- return value
219
- embed_instance = self._get_embed_type()()
220
- relations = embed_instance.prepopulate(**value)
221
- embed_instance.bind(self.parent)
222
- embed_instance.populate(**relations)
223
- return embed_instance
224
-
225
-
226
- class ForeignKey(Field):
227
- types = (models.Model, int, str)
228
-
229
- def __init__(self, to, *args, **kwargs):
230
- self.model = to
231
- self.queryset = kwargs.pop("queryset", None)
232
- super(ForeignKey, self).__init__(*args, **kwargs)
233
-
234
- def get_queryset(self):
235
- return self.queryset or self.model.objects.all()
236
-
237
- def to_struct(self, value):
238
- return getattr(value, "pk", value)
239
-
240
- def parse_value(self, value):
241
- try:
242
- if isinstance(value, bool):
243
- raise TypeError
244
- if isinstance(value, self.model):
245
- return value
246
- cache = pointed_getter(self, "parent.get_prefetched_data", lambda: None)()
247
- if cache is not None:
248
- return cache.get(self.model, {}).get(value)
249
- return value and self.get_queryset().get(pk=value)
250
- except (TypeError, ValueError):
251
- raise fields.ValidationError(
252
- "Incorrect type. Expected pk value, received {data_type}.".format(
253
- data_type=type(value).__name__
254
- )
92
+ return model_class._default_manager.filter(pk__in=values).order_by(
93
+ preserved
255
94
  )
256
95
 
257
-
258
- class ForeignKeyList(ListField):
259
- types = (list, models.QuerySet)
260
-
261
- def __init__(self, to, *args, **kwargs):
262
- self.inner_model = to
263
- self.inner_queryset = kwargs.pop("queryset", None)
264
- kwargs["items_types"] = to
265
- super(ForeignKeyList, self).__init__(*args, **kwargs)
266
-
267
- def parse_value(self, values):
268
- result = self.get_default_value()
269
- if not values:
270
- return result
271
- if not isinstance(values, list):
272
- return values
273
- cache = pointed_getter(self, "parent.get_prefetched_data", lambda: None)()
274
- if cache is not None:
275
- data = cache.get(self.inner_model)
276
- qs = self.get_inner_queryset().filter(pk__in=values)
277
- setattr(
278
- qs,
279
- "_result_cache",
280
- [data[i] for i in values if i in data] if data else [],
281
- )
282
- return qs
283
- preserved = models.Case(
284
- *[models.When(pk=pk, then=pos) for pos, pk in enumerate(values)]
96
+ int_str_union = cs.union_schema([cs.str_schema(), cs.int_schema()])
97
+ from_pk_list_schema = cs.chain_schema(
98
+ [
99
+ cs.list_schema(int_str_union),
100
+ cs.no_info_plain_validator_function(validate_from_pk_list),
101
+ ]
102
+ )
103
+ pk_attname = model_class._meta.pk.attname
104
+
105
+ def validate_from_dict(
106
+ values: list[Dict[str, Union[str, int]]]
107
+ ) -> django_models.QuerySet:
108
+ return validate_from_pk_list([data[pk_attname] for data in values])
109
+
110
+ from_dict_list_schema = cs.chain_schema(
111
+ [
112
+ cs.list_schema(
113
+ cs.typed_dict_schema(
114
+ {pk_attname: cs.typed_dict_field(int_str_union)}
115
+ )
116
+ ),
117
+ cs.no_info_plain_validator_function(validate_from_dict),
118
+ ]
119
+ )
120
+ from_cache_schema = cs.chain_schema(
121
+ [
122
+ cs.is_instance_schema(ValueWithCache),
123
+ cs.no_info_plain_validator_function(lambda v: v.retrieve()),
124
+ ]
125
+ )
126
+
127
+ def serialize_data(qs: django_models.QuerySet, info: SerializationInfo):
128
+ from camomilla.serializers.utils import build_standard_model_serializer
129
+ if info.mode == "python":
130
+ serializer = build_standard_model_serializer(model_class, depth=1)
131
+ return serializer(instance=qs, many=True).data
132
+ return [x.pk for x in qs]
133
+
134
+ return cs.json_or_python_schema(
135
+ json_schema=cs.union_schema(
136
+ [from_cache_schema, from_pk_list_schema, from_dict_list_schema]
137
+ ),
138
+ python_schema=cs.union_schema(
139
+ [
140
+ cs.is_instance_schema(django_models.QuerySet),
141
+ from_cache_schema,
142
+ from_pk_list_schema,
143
+ from_dict_list_schema,
144
+ ]
145
+ ),
146
+ serialization=cs.plain_serializer_function_ser_schema(
147
+ serialize_data, info_arg=True
148
+ ),
285
149
  )
286
- return self.get_inner_queryset().filter(pk__in=values).order_by(preserved)
287
-
288
- def get_inner_queryset(self):
289
- return self.inner_queryset or self.inner_model._default_manager.all()
290
-
291
- def to_struct(self, value):
292
- return [getattr(v, "pk", v) for v in value]