django-camomilla-cms 6.0.0b14__py2.py3-none-any.whl → 6.0.0b16__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.
- camomilla/__init__.py +1 -1
- camomilla/dynamic_pages_urls.py +2 -1
- camomilla/fields/__init__.py +1 -3
- camomilla/managers/pages.py +22 -33
- camomilla/model_api.py +9 -4
- camomilla/models/media.py +1 -1
- camomilla/models/menu.py +23 -11
- camomilla/models/page.py +76 -38
- camomilla/openapi/schema.py +4 -0
- camomilla/serializers/base/__init__.py +3 -1
- camomilla/serializers/fields/__init__.py +2 -2
- camomilla/serializers/fields/json.py +2 -2
- camomilla/serializers/fields/related.py +5 -1
- camomilla/serializers/mixins/__init__.py +56 -18
- camomilla/serializers/mixins/filter_fields.py +56 -0
- camomilla/serializers/utils.py +3 -1
- camomilla/serializers/validators.py +9 -5
- camomilla/settings.py +0 -4
- camomilla/storages/default.py +6 -0
- camomilla/storages/optimize.py +2 -2
- camomilla/storages/overwrite.py +2 -2
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin.py +1 -1
- camomilla/translation.py +1 -1
- camomilla/utils/query_parser.py +148 -0
- camomilla/utils/setters.py +37 -0
- camomilla/views/base/__init__.py +2 -2
- camomilla/views/menus.py +0 -3
- camomilla/views/mixins/__init__.py +9 -2
- camomilla/views/mixins/pagination.py +4 -13
- {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/METADATA +2 -3
- {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/RECORD +46 -40
- {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +17 -0
- tests/test_api.py +2 -11
- tests/test_camomilla_filters.py +7 -13
- tests/test_media.py +80 -0
- tests/test_model_api.py +68 -0
- tests/test_model_api_permissions.py +39 -0
- tests/test_query_parser.py +59 -0
- tests/test_utils.py +64 -64
- tests/utils/__init__.py +0 -0
- tests/utils/api.py +29 -0
- camomilla/structured/__init__.py +0 -125
- camomilla/structured/cache.py +0 -202
- camomilla/structured/fields.py +0 -150
- camomilla/structured/models.py +0 -47
- camomilla/structured/utils.py +0 -114
- {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/top_level.txt +0 -0
tests/test_utils.py
CHANGED
@@ -13,74 +13,74 @@ from django.utils.translation import activate, get_language
|
|
13
13
|
from requests import RequestException
|
14
14
|
|
15
15
|
|
16
|
-
class UtilsTestCase(TestCase):
|
17
|
-
|
18
|
-
|
16
|
+
# class UtilsTestCase(TestCase):
|
17
|
+
# def setUp(self):
|
18
|
+
# pass
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
# def test_get_host_url(self):
|
21
|
+
# """Our beloved get_host_url utility"""
|
22
|
+
# request_factory = RequestFactory()
|
23
|
+
# request = request_factory.get("/path")
|
24
|
+
# request.META["HTTP_HOST"] = "localhost"
|
25
|
+
# host_url = get_host_url(request)
|
26
|
+
# self.assertEqual(host_url, "http://localhost")
|
27
|
+
# host_url = get_host_url(None)
|
28
|
+
# self.assertEqual(host_url, None)
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
30
|
+
# def test_get_complete_url(self):
|
31
|
+
# """Our beloved get_complete_url utility"""
|
32
|
+
# request_factory = RequestFactory()
|
33
|
+
# request = request_factory.get("/path")
|
34
|
+
# request.META["HTTP_HOST"] = "localhost"
|
35
|
+
# complete_url = get_complete_url(request, "path")
|
36
|
+
# self.assertEqual(complete_url, "http://localhost/path")
|
37
|
+
# complete_url = get_complete_url(request, "path", "it")
|
38
|
+
# self.assertEqual(complete_url, "http://localhost/path")
|
39
|
+
# complete_url = get_complete_url(request, "path", "fr")
|
40
|
+
# self.assertEqual(complete_url, "http://localhost/fr/path")
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
# def test_get_page_with_default_seo(self):
|
43
|
+
# """Our beloved get_seo utility with auto attributes"""
|
44
|
+
# request_factory = RequestFactory()
|
45
|
+
# request = request_factory.get("/path")
|
46
|
+
# request.META["HTTP_HOST"] = "localhost"
|
47
|
+
# page = Page.get(request, identifier="home")
|
48
|
+
# self.assertEqual(page.og_url, "http://localhost/path")
|
49
|
+
# self.assertEqual(page.canonical, "http://localhost/path")
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
# def test_get_article_with_default_seo(self):
|
52
|
+
# """Our beloved get_seo utility with auto attributes"""
|
53
|
+
# request_factory = RequestFactory()
|
54
|
+
# request = request_factory.get("/path")
|
55
|
+
# request.META["HTTP_HOST"] = "localhost"
|
56
|
+
# Article.objects.create(permalink="main")
|
57
|
+
# article = Article.get(request, permalink="main")
|
58
|
+
# self.assertEqual(article.og_url, "http://localhost/path")
|
59
|
+
# self.assertEqual(article.canonical, "http://localhost/path")
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
61
|
+
# def test_compile_seo_overwrite(self):
|
62
|
+
# """Our beloved get_seo utility with auto attributes"""
|
63
|
+
# request_factory = RequestFactory()
|
64
|
+
# request = request_factory.get("/path")
|
65
|
+
# request.META["HTTP_HOST"] = "localhost"
|
66
|
+
# article = Article.objects.create(permalink="main")
|
67
|
+
# article.canonical = "/myarticle"
|
68
|
+
# article.og_url = "/myarticle"
|
69
|
+
# article.save()
|
70
|
+
# article = Article.get(request, permalink="main")
|
71
|
+
# self.assertEqual(article.og_url, "http://localhost/myarticle")
|
72
|
+
# self.assertEqual(article.canonical, "http://localhost/myarticle")
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
74
|
+
# def test_get_article_with_redirect(self):
|
75
|
+
# """Our beloved get_seo utility with auto attributes"""
|
76
|
+
# request_factory = RequestFactory()
|
77
|
+
# request = request_factory.get("/articles/articolo-1")
|
78
|
+
# request.META["HTTP_HOST"] = "localhost"
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
80
|
+
# article = Article.objects.create(permalink="article-1", language_code="en")
|
81
|
+
# article.translate("it")
|
82
|
+
# article.permalink = "articolo-1"
|
83
|
+
# article.save()
|
84
|
+
# self.assertRaises(NeedARedirect, Article.get, request, permalink="article-1")
|
85
|
+
# activate("de")
|
86
|
+
# self.assertRaises(Http404, Article.get, request, permalink="article-1")
|
tests/utils/__init__.py
ADDED
File without changes
|
tests/utils/api.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
from django.contrib.auth.models import User
|
2
|
+
from rest_framework.test import APIClient
|
3
|
+
from example.website.models import TestModel
|
4
|
+
|
5
|
+
client = APIClient()
|
6
|
+
|
7
|
+
|
8
|
+
def login_superuser():
|
9
|
+
User.objects.create_superuser("admin", "admin@test.com", "adminadmin")
|
10
|
+
response = client.post(
|
11
|
+
"/api/camomilla/token-auth/", {"username": "admin", "password": "adminadmin"}
|
12
|
+
)
|
13
|
+
return response.json()["token"]
|
14
|
+
|
15
|
+
|
16
|
+
def login_user():
|
17
|
+
User.objects.create_user("user", "user@test.com", "useruser")
|
18
|
+
response = client.post(
|
19
|
+
"/api/camomilla/token-auth/", {"username": "user", "password": "useruser"}
|
20
|
+
)
|
21
|
+
return response.json()["token"]
|
22
|
+
|
23
|
+
|
24
|
+
def login_staff():
|
25
|
+
User.objects.create_user("staff", "staff@test.com", "staffstaff", is_staff=True)
|
26
|
+
response = client.post(
|
27
|
+
"/api/camomilla/token-auth/", {"username": "staff", "password": "staffstaff"}
|
28
|
+
)
|
29
|
+
return response.json()["token"]
|
camomilla/structured/__init__.py
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from typing import Any, Union, Type, TYPE_CHECKING, List
|
3
|
-
|
4
|
-
from django.db.models import JSONField
|
5
|
-
from django.db.models.query_utils import DeferredAttribute
|
6
|
-
from django_jsonform.models.fields import JSONFormField
|
7
|
-
from pydantic import (
|
8
|
-
TypeAdapter,
|
9
|
-
ValidationInfo,
|
10
|
-
ValidatorFunctionWrapHandler,
|
11
|
-
WrapValidator,
|
12
|
-
)
|
13
|
-
|
14
|
-
from .fields import *
|
15
|
-
from .models import *
|
16
|
-
|
17
|
-
if TYPE_CHECKING:
|
18
|
-
from pydantic import BaseModel
|
19
|
-
from pydantic.fields import Field
|
20
|
-
from typing_extensions import Annotated
|
21
|
-
|
22
|
-
|
23
|
-
class StructuredJSONFormField(JSONFormField):
|
24
|
-
def __init__(self, *args, **kwargs):
|
25
|
-
super().__init__(*args, **kwargs)
|
26
|
-
self.encoder = kwargs.get("encoder", None)
|
27
|
-
self.decoder = kwargs.get("decoder", None)
|
28
|
-
|
29
|
-
def prepare_value(self, value):
|
30
|
-
if isinstance(value, list):
|
31
|
-
return json.dumps([v.model_dump() for v in value])
|
32
|
-
return value.model_dump_json()
|
33
|
-
|
34
|
-
|
35
|
-
class StructuredDescriptior(DeferredAttribute):
|
36
|
-
field: "StructuredJSONField"
|
37
|
-
|
38
|
-
def __set__(self, instance, value):
|
39
|
-
# TODO: check if it's better to validate here or in __get__ function (performance reasons)
|
40
|
-
# if not self.field.check_type(value):
|
41
|
-
# value = self.field.schema.validate_python(value)
|
42
|
-
instance.__dict__[self.field.attname] = value
|
43
|
-
|
44
|
-
def __get__(self, instance, cls=None):
|
45
|
-
value = super().__get__(instance, cls)
|
46
|
-
if not self.field.check_type(value):
|
47
|
-
return self.field.schema.validate_python(value)
|
48
|
-
return value
|
49
|
-
|
50
|
-
|
51
|
-
class StructuredJSONField(JSONField):
|
52
|
-
# TODO: share cache in querysets of models having this same field
|
53
|
-
# TODO: write queries for prefetch related for models inside the field
|
54
|
-
|
55
|
-
descriptor_class = StructuredDescriptior
|
56
|
-
|
57
|
-
@property
|
58
|
-
def list_data_validator(self):
|
59
|
-
def list_data_validator(
|
60
|
-
value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
61
|
-
) -> Any:
|
62
|
-
from camomilla.structured.cache import CacheBuilder
|
63
|
-
|
64
|
-
cache = CacheBuilder.from_model(self.orig_schema)
|
65
|
-
if info.mode == "json" and isinstance(value, str):
|
66
|
-
return self.schema.validate_python(
|
67
|
-
cache.inject_cache(json.loads(value))
|
68
|
-
)
|
69
|
-
return handler(cache.inject_cache(value))
|
70
|
-
|
71
|
-
return list_data_validator
|
72
|
-
|
73
|
-
def __init__(self, schema: Type[BaseModel], *args: Any, **kwargs: Any) -> None:
|
74
|
-
self.orig_schema = schema
|
75
|
-
self.schema = schema
|
76
|
-
default = kwargs.get("default", dict)
|
77
|
-
self.file_handler = kwargs.pop("file_handler", "")
|
78
|
-
self.many = kwargs.pop(
|
79
|
-
"many", isinstance(default() if callable(default) else default, list)
|
80
|
-
)
|
81
|
-
if self.many:
|
82
|
-
self.schema = TypeAdapter(
|
83
|
-
Annotated[
|
84
|
-
List[self.schema],
|
85
|
-
Field(default_factory=list),
|
86
|
-
WrapValidator(self.list_data_validator),
|
87
|
-
]
|
88
|
-
)
|
89
|
-
return super().__init__(*args, **kwargs)
|
90
|
-
|
91
|
-
def check_type(self, value: Any):
|
92
|
-
if self.many:
|
93
|
-
return isinstance(value, list) and all(
|
94
|
-
isinstance(v, self.orig_schema) for v in value
|
95
|
-
)
|
96
|
-
return isinstance(value, self.orig_schema)
|
97
|
-
|
98
|
-
def get_prep_value(
|
99
|
-
self, value: Union[List[Type[BaseModel]], Type[BaseModel]]
|
100
|
-
) -> str:
|
101
|
-
if isinstance(value, list) and self.many:
|
102
|
-
return self.schema.dump_json(value, exclude_unset=True).decode()
|
103
|
-
return value.model_dump_json(exclude_unset=True)
|
104
|
-
|
105
|
-
def from_db_value(self, value: Any, expression: Any, connection: Any) -> Any:
|
106
|
-
data = super().from_db_value(value, expression, connection)
|
107
|
-
if isinstance(data, str):
|
108
|
-
return json.loads(data)
|
109
|
-
return data
|
110
|
-
|
111
|
-
def deconstruct(self):
|
112
|
-
name, path, args, kwargs = super().deconstruct()
|
113
|
-
kwargs["schema"] = self.orig_schema
|
114
|
-
return name, path, args, kwargs
|
115
|
-
|
116
|
-
def formfield(self, **kwargs):
|
117
|
-
return super().formfield(
|
118
|
-
**{
|
119
|
-
"form_class": StructuredJSONFormField,
|
120
|
-
"schema": self.schema.json_schema(),
|
121
|
-
"model_name": self.model.__name__,
|
122
|
-
"file_handler": self.file_handler,
|
123
|
-
**kwargs,
|
124
|
-
}
|
125
|
-
)
|
camomilla/structured/cache.py
DELETED
@@ -1,202 +0,0 @@
|
|
1
|
-
from collections import defaultdict
|
2
|
-
from inspect import isclass
|
3
|
-
from typing import Any, Dict, Sequence, Type, Iterable, Union
|
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
|
-
from django.db.models import Model as DjangoModel
|
10
|
-
from camomilla.utils.getters import pointed_getter
|
11
|
-
|
12
|
-
|
13
|
-
# TODO:
|
14
|
-
# ::: Actually this is a first draft.
|
15
|
-
# The system should be more type safe.
|
16
|
-
# It should handle initialization made with model instance not only dicts
|
17
|
-
# Neet to check if there are problems with different formats for pks (model instaces or string or dicts)
|
18
|
-
|
19
|
-
|
20
|
-
class Cache(dict):
|
21
|
-
pass
|
22
|
-
|
23
|
-
|
24
|
-
class ValueWithCache:
|
25
|
-
def __init__(self, cache, model, value) -> None:
|
26
|
-
self.cache: Cache = cache
|
27
|
-
self.value: Union[Iterable[Union[str, int]], str, int] = value
|
28
|
-
self.model: Type[DjangoModel] = model
|
29
|
-
|
30
|
-
def retrieve(self):
|
31
|
-
cache = self.cache.get(self.model)
|
32
|
-
if hasattr(self.value, '__iter__'):
|
33
|
-
qs = self.model.objects.filter(pk__in=self.value)
|
34
|
-
setattr(
|
35
|
-
qs,
|
36
|
-
"_result_cache",
|
37
|
-
[cache[i] for i in self.value if i in cache] if cache else [],
|
38
|
-
)
|
39
|
-
return qs
|
40
|
-
else:
|
41
|
-
val = cache.get(self.value, None)
|
42
|
-
if val is None:
|
43
|
-
return self.model._default_manager.get(pk=self.value)
|
44
|
-
else:
|
45
|
-
return val
|
46
|
-
|
47
|
-
|
48
|
-
class RelInfo:
|
49
|
-
FKField: str = "fk"
|
50
|
-
QSField: str = "qs"
|
51
|
-
RIField: str = "rel"
|
52
|
-
RLField: str = "rel_l"
|
53
|
-
|
54
|
-
def __init__(self, model, type, field) -> None:
|
55
|
-
self.model = model
|
56
|
-
self.type = type
|
57
|
-
self.field = field
|
58
|
-
|
59
|
-
|
60
|
-
class CacheBuilder:
|
61
|
-
def __init__(self, related_fields: Dict[str, RelInfo]) -> None:
|
62
|
-
self.__related_fields__ = related_fields
|
63
|
-
|
64
|
-
@classmethod
|
65
|
-
def from_model(cls, model: BaseModel) -> "CacheBuilder":
|
66
|
-
return cls(related_fields=cls.inspect_related_fields(model))
|
67
|
-
|
68
|
-
@classmethod
|
69
|
-
def inspect_related_fields(cls, model: Type[BaseModel]) -> Dict[str, RelInfo]:
|
70
|
-
related = {}
|
71
|
-
for field_name, field in model.model_fields.items():
|
72
|
-
annotation = field.annotation
|
73
|
-
origin = get_origin(annotation)
|
74
|
-
args = get_args(annotation)
|
75
|
-
|
76
|
-
if isclass(origin) and issubclass(origin, ForeignKey):
|
77
|
-
related[field_name] = RelInfo(
|
78
|
-
get_type(annotation), RelInfo.FKField, field
|
79
|
-
)
|
80
|
-
elif isclass(origin) and issubclass(origin, QuerySet):
|
81
|
-
related[field_name] = RelInfo(
|
82
|
-
get_type(annotation), RelInfo.QSField, field
|
83
|
-
)
|
84
|
-
|
85
|
-
elif isclass(origin) and issubclass(origin, Sequence):
|
86
|
-
lazy_types = [
|
87
|
-
_LazyType(arg).evaluate(model)
|
88
|
-
for arg in args
|
89
|
-
if isinstance(arg, str)
|
90
|
-
]
|
91
|
-
subclass = next(
|
92
|
-
(
|
93
|
-
c
|
94
|
-
for c in list(args) + lazy_types
|
95
|
-
if isclass(c) and issubclass(c, BaseModel)
|
96
|
-
),
|
97
|
-
None,
|
98
|
-
)
|
99
|
-
if isclass(subclass) and issubclass(subclass, BaseModel):
|
100
|
-
related[field_name] = RelInfo(subclass, RelInfo.RLField, field)
|
101
|
-
|
102
|
-
elif isclass(annotation) and issubclass(annotation, BaseModel):
|
103
|
-
related[field_name] = RelInfo(annotation, RelInfo.RIField, field)
|
104
|
-
return related
|
105
|
-
|
106
|
-
def get_all_fk_data(self, data):
|
107
|
-
if isinstance(data, Sequence):
|
108
|
-
fk_data = defaultdict(list)
|
109
|
-
for index in range(len(data)):
|
110
|
-
child_fk_data = self.get_all_fk_data(data[index])
|
111
|
-
for model, touples in child_fk_data.items():
|
112
|
-
fk_data[model] += [(f"{index}.{t[0]}", t[1]) for t in touples]
|
113
|
-
return fk_data
|
114
|
-
return self.get_fk_data(data)
|
115
|
-
|
116
|
-
def get_fk_data(self, data):
|
117
|
-
fk_data = defaultdict(list)
|
118
|
-
if not data:
|
119
|
-
return fk_data
|
120
|
-
for field_name, info in self.__related_fields__.items():
|
121
|
-
if info.type == RelInfo.FKField:
|
122
|
-
value = pointed_getter(data, field_name, None)
|
123
|
-
if value:
|
124
|
-
if isinstance(
|
125
|
-
value, ValueWithCache
|
126
|
-
): # needed to break recursive cache builds
|
127
|
-
fk_data = {}
|
128
|
-
break
|
129
|
-
fk_data[info.model].append(
|
130
|
-
(
|
131
|
-
field_name,
|
132
|
-
pointed_getter(value, info.model._meta.pk.attname, value),
|
133
|
-
)
|
134
|
-
)
|
135
|
-
elif info.type == RelInfo.QSField:
|
136
|
-
value = pointed_getter(data, field_name, [])
|
137
|
-
if isinstance(value, list):
|
138
|
-
if any(
|
139
|
-
True for v in value if isinstance(v, ValueWithCache)
|
140
|
-
): # needed to break recursive cache builds
|
141
|
-
fk_data = {}
|
142
|
-
break
|
143
|
-
fk_data[info.model].append(
|
144
|
-
(
|
145
|
-
field_name,
|
146
|
-
[
|
147
|
-
pointed_getter(i, info.model._meta.pk.attname, i)
|
148
|
-
for i in value
|
149
|
-
if i
|
150
|
-
],
|
151
|
-
)
|
152
|
-
)
|
153
|
-
|
154
|
-
elif info.type == RelInfo.RLField:
|
155
|
-
values = pointed_getter(data, field_name, [])
|
156
|
-
if isinstance(values, list):
|
157
|
-
for index in range(len(values)):
|
158
|
-
for model, touples in (
|
159
|
-
self.from_model(info.model)
|
160
|
-
.get_all_fk_data(values[index])
|
161
|
-
.items()
|
162
|
-
):
|
163
|
-
fk_data[model] += [
|
164
|
-
(f"{field_name}.{index}.{t[0]}", t[1]) for t in touples
|
165
|
-
]
|
166
|
-
elif info.type == RelInfo.RIField:
|
167
|
-
value = pointed_getter(data, field_name, None)
|
168
|
-
child_fk_data = self.from_model(info.model).get_all_fk_data(value)
|
169
|
-
for model, touples in child_fk_data.items():
|
170
|
-
fk_data[model] += [(f"{field_name}.{t[0]}", t[1]) for t in touples]
|
171
|
-
return fk_data
|
172
|
-
|
173
|
-
def inject_cache(self, data: Any) -> Any:
|
174
|
-
if not STRUCTURED_FIELD_CACHE_ENABLED:
|
175
|
-
return data
|
176
|
-
fk_data = self.get_all_fk_data(data)
|
177
|
-
plainset = defaultdict(set)
|
178
|
-
for model, touples in fk_data.items():
|
179
|
-
for t in touples:
|
180
|
-
if isinstance(t[1], Sequence):
|
181
|
-
plainset[model].update(t[1])
|
182
|
-
else:
|
183
|
-
plainset[model].add(t[1])
|
184
|
-
cache = Cache()
|
185
|
-
for model, values in plainset.items():
|
186
|
-
models = []
|
187
|
-
pks = []
|
188
|
-
for value in values:
|
189
|
-
if isinstance(value, model):
|
190
|
-
models.append(value)
|
191
|
-
else:
|
192
|
-
pks.append(value)
|
193
|
-
models_pks = [m.pk for m in models]
|
194
|
-
pks = [pk for pk in pks if pk not in models_pks]
|
195
|
-
if len(pks):
|
196
|
-
models += list(model.objects.filter(pk__in=pks))
|
197
|
-
cache[model] = {obj.pk: obj for obj in models}
|
198
|
-
|
199
|
-
for model, touples in fk_data.items():
|
200
|
-
for t in touples:
|
201
|
-
pointed_setter(data, t[0], ValueWithCache(cache, model, t[1]))
|
202
|
-
return data
|
camomilla/structured/fields.py
DELETED
@@ -1,150 +0,0 @@
|
|
1
|
-
from typing import Any, Callable, Dict, Generic, TypeVar, Union
|
2
|
-
|
3
|
-
from django.db import models as django_models
|
4
|
-
from pydantic import SerializationInfo
|
5
|
-
from pydantic_core import core_schema as cs
|
6
|
-
|
7
|
-
from .utils import get_type
|
8
|
-
|
9
|
-
|
10
|
-
T = TypeVar("T", bound=django_models.Model)
|
11
|
-
|
12
|
-
|
13
|
-
class ForeignKey(Generic[T]):
|
14
|
-
@classmethod
|
15
|
-
def __get_pydantic_core_schema__(
|
16
|
-
cls, source: Any, handler: Callable[[Any], cs.CoreSchema]
|
17
|
-
) -> cs.CoreSchema:
|
18
|
-
from camomilla.structured.cache import ValueWithCache
|
19
|
-
|
20
|
-
model_class = get_type(source)
|
21
|
-
|
22
|
-
def validate_from_pk(pk: Union[int, str]) -> model_class:
|
23
|
-
return model_class._default_manager.get(pk=pk)
|
24
|
-
|
25
|
-
int_str_union = cs.union_schema([cs.str_schema(), cs.int_schema()])
|
26
|
-
from_pk_schema = cs.chain_schema(
|
27
|
-
[
|
28
|
-
int_str_union,
|
29
|
-
cs.no_info_plain_validator_function(validate_from_pk),
|
30
|
-
]
|
31
|
-
)
|
32
|
-
pk_attname = model_class._meta.pk.attname
|
33
|
-
|
34
|
-
def validate_from_dict(data: Dict[str, Union[str, int]]) -> model_class:
|
35
|
-
return validate_from_pk(data[pk_attname])
|
36
|
-
|
37
|
-
from_dict_schema = cs.chain_schema(
|
38
|
-
[
|
39
|
-
cs.typed_dict_schema({pk_attname: cs.typed_dict_field(int_str_union)}),
|
40
|
-
cs.no_info_plain_validator_function(validate_from_dict),
|
41
|
-
]
|
42
|
-
)
|
43
|
-
|
44
|
-
from_cache_schema = cs.chain_schema(
|
45
|
-
[
|
46
|
-
cs.is_instance_schema(ValueWithCache),
|
47
|
-
cs.no_info_plain_validator_function(lambda v: v.retrieve()),
|
48
|
-
]
|
49
|
-
)
|
50
|
-
|
51
|
-
def serialize_data(instance, info):
|
52
|
-
from camomilla.serializers.utils import build_standard_model_serializer
|
53
|
-
|
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
|
-
)
|
75
|
-
|
76
|
-
|
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)]
|
91
|
-
)
|
92
|
-
return model_class._default_manager.filter(pk__in=values).order_by(
|
93
|
-
preserved
|
94
|
-
)
|
95
|
-
|
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
|
-
|
130
|
-
if info.mode == "python":
|
131
|
-
serializer = build_standard_model_serializer(model_class, depth=1)
|
132
|
-
return serializer(instance=qs, many=True).data
|
133
|
-
return [x.pk for x in qs]
|
134
|
-
|
135
|
-
return cs.json_or_python_schema(
|
136
|
-
json_schema=cs.union_schema(
|
137
|
-
[from_cache_schema, from_pk_list_schema, from_dict_list_schema]
|
138
|
-
),
|
139
|
-
python_schema=cs.union_schema(
|
140
|
-
[
|
141
|
-
cs.is_instance_schema(django_models.QuerySet),
|
142
|
-
from_cache_schema,
|
143
|
-
from_pk_list_schema,
|
144
|
-
from_dict_list_schema,
|
145
|
-
]
|
146
|
-
),
|
147
|
-
serialization=cs.plain_serializer_function_ser_schema(
|
148
|
-
serialize_data, info_arg=True
|
149
|
-
),
|
150
|
-
)
|