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
@@ -1,15 +1,6 @@
1
1
  from rest_framework import serializers
2
2
  from rest_framework.utils import model_meta
3
3
 
4
- from camomilla.serializers.utils import build_standard_model_serializer
5
- from camomilla.structured.fields import (
6
- EmbeddedField,
7
- ForeignKey,
8
- ForeignKeyList,
9
- ListField,
10
- )
11
- from camomilla.structured.models import Model
12
-
13
4
 
14
5
  class StructuredJSONField(serializers.JSONField):
15
6
  def __init__(self, **kwargs):
@@ -21,72 +12,13 @@ class StructuredJSONField(serializers.JSONField):
21
12
  info = model_meta.get_field_info(parent.Meta.model)
22
13
  field = info.fields[field_name]
23
14
  self.schema = field.schema
15
+ self.many = field.many
16
+ self.json_schema = field.schema.json_schema()
24
17
  super().bind(field_name, parent)
25
18
 
26
- def to_internal_value(self, data):
27
- if isinstance(data, dict):
28
- data = to_internal_model_value(self.schema, data)
29
- elif isinstance(data, list):
30
- data = [to_internal_model_value(self.schema, v) for v in data]
31
- return super().to_internal_value(data)
32
-
33
19
  def to_representation(self, instance):
34
- return super().to_representation(expanded_rapresentation(instance))
35
-
36
-
37
- def expanded_model_rapresentation(schema: Model):
38
- stack = {}
39
- for _, name, field in schema.iterate_with_name():
40
- value = field.__get__(schema)
41
- if value is None:
42
- continue
43
- elif isinstance(field, ForeignKey):
44
- serializer = build_standard_model_serializer(field.model, depth=1)
45
- data = serializer(instance=value).data
46
- elif isinstance(field, ForeignKeyList):
47
- serializer = build_standard_model_serializer(field.inner_model, depth=1)
48
- data = serializer(instance=value, many=True).data
49
- elif isinstance(field, EmbeddedField):
50
- data = expanded_model_rapresentation(value)
51
- elif isinstance(field, ListField):
52
- data = []
53
- for v in value:
54
- if isinstance(v, Model):
55
- v = expanded_model_rapresentation(v)
56
- data.append(v)
57
- else:
58
- data = field.to_struct(value)
59
- stack[name] = data
60
- return stack
61
-
62
-
63
- def expanded_rapresentation(value):
64
- if isinstance(value, dict):
65
- value = {k: expanded_rapresentation(v) for k, v in value.items()}
66
- elif isinstance(value, list):
67
- value = [expanded_rapresentation(v) for v in value]
68
- elif isinstance(value, Model):
69
- value = expanded_model_rapresentation(value)
70
- return value
71
-
72
-
73
- def to_internal_model_value(schema: Model, data):
74
- stack = {}
75
- for _, name, field in schema.iterate_with_name():
76
- value = data.get(name, None)
77
- if value is None:
78
- continue
79
- if isinstance(field, ForeignKey) and isinstance(value, dict):
80
- value = value.get(field.model._meta.pk.attname, None)
81
- elif isinstance(field, ForeignKeyList) and isinstance(value, list):
82
- attname = field.model._meta.pk.attname
83
- value = [v.get(attname, None) for v in value]
84
- elif isinstance(field, EmbeddedField):
85
- value = to_internal_model_value(field._get_embed_type(), value)
86
- elif isinstance(field, ListField):
87
- main_type = field._get_main_type()
88
- value = [to_internal_model_value(main_type, v) for v in value]
89
- else:
90
- value = field.to_struct(value)
91
- stack[name] = value
92
- return stack
20
+ if isinstance(instance, list) and self.many:
21
+ return super().to_representation(
22
+ self.schema.dump_python(instance, exclude_unset=True)
23
+ )
24
+ return super().to_representation(instance.model_dump(exclude_unset=True))
@@ -20,6 +20,9 @@ if django.VERSION >= (4, 0):
20
20
  else:
21
21
  from django.contrib.postgres.fields import JSONField as DjangoJSONField
22
22
 
23
+ from typing import TYPE_CHECKING
24
+ if TYPE_CHECKING:
25
+ from camomilla.models.page import AbstractPage
23
26
 
24
27
  # TODO: decide what to do with LangInfoMixin mixin!
25
28
  class LangInfoMixin(metaclass=serializers.SerializerMetaclass):
@@ -121,8 +124,17 @@ class NestMixin:
121
124
  return field_class, field_kwargs
122
125
 
123
126
 
124
-
125
127
  class AbstractPageMixin(serializers.ModelSerializer):
128
+ breadcrumbs = serializers.SerializerMethodField()
129
+ routerlink = serializers.CharField(read_only=True)
130
+ template = serializers.SerializerMethodField()
131
+
132
+ def get_template(self, instance: 'AbstractPage'):
133
+ return instance.get_template_path()
134
+
135
+ def get_breadcrumbs(self, instance: 'AbstractPage'):
136
+ return instance.breadcrumbs
137
+
126
138
  LANG_PERMALINK_FIELDS = [
127
139
  build_localized_fieldname("permalink", lang)
128
140
  for lang in AVAILABLE_LANGUAGES
@@ -134,6 +146,9 @@ class AbstractPageMixin(serializers.ModelSerializer):
134
146
  return super().translation_fields + ["permalink"]
135
147
 
136
148
  def get_default_field_names(self, *args):
149
+ from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
150
+ if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
151
+ return super().get_default_field_names(*args)
137
152
  return (
138
153
  [f for f in super().get_default_field_names(*args) if f != "url_node"]
139
154
  + self.LANG_PERMALINK_FIELDS
@@ -150,4 +165,3 @@ class AbstractPageMixin(serializers.ModelSerializer):
150
165
 
151
166
  def get_validators(self):
152
167
  return super().get_validators() + [UniquePermalinkValidator()]
153
-
@@ -1,7 +1,13 @@
1
+ from camomilla.models.page import UrlNode
1
2
  from camomilla.serializers.mixins import AbstractPageMixin
2
3
  from camomilla.models import Content, Page
3
4
  from camomilla.serializers.base import BaseModelSerializer
5
+ from rest_framework import serializers
4
6
 
7
+ from camomilla.serializers.utils import (
8
+ build_standard_model_serializer,
9
+ get_standard_bases,
10
+ )
5
11
 
6
12
 
7
13
  class ContentSerializer(BaseModelSerializer):
@@ -14,3 +20,44 @@ class PageSerializer(AbstractPageMixin, BaseModelSerializer):
14
20
  class Meta:
15
21
  model = Page
16
22
  fields = "__all__"
23
+
24
+
25
+ class BasicUrlNodeSerializer(BaseModelSerializer):
26
+ is_public = serializers.SerializerMethodField()
27
+ status = serializers.SerializerMethodField()
28
+ indexable = serializers.SerializerMethodField()
29
+
30
+ class Meta:
31
+ model = UrlNode
32
+ fields = ("id", "permalink", "status", "indexable", "is_public")
33
+
34
+ def get_is_public(self, instance: UrlNode):
35
+ return instance.page.is_public
36
+
37
+ def get_status(self, instance: UrlNode):
38
+ return instance.page.status
39
+
40
+ def get_indexable(self, instance: UrlNode):
41
+ return instance.page.indexable
42
+
43
+
44
+ class UrlNodeSerializer(BasicUrlNodeSerializer):
45
+ alternates = serializers.SerializerMethodField()
46
+
47
+ def get_alternates(self, instance: UrlNode):
48
+ return instance.page.alternate_urls()
49
+
50
+ def to_representation(self, instance: UrlNode):
51
+ model_serializer = build_standard_model_serializer(
52
+ instance.page.__class__,
53
+ depth=10,
54
+ bases=(AbstractPageMixin,) + get_standard_bases(),
55
+ )
56
+ return {
57
+ **super().to_representation(instance),
58
+ **model_serializer(instance.page, context=self.context).data,
59
+ }
60
+
61
+ class Meta:
62
+ model = UrlNode
63
+ fields = "__all__"
@@ -40,8 +40,8 @@ class UserProfileSerializer(BaseModelSerializer):
40
40
  def get_all_permissions(self, instance):
41
41
  return PermissionSerializer(
42
42
  Permission.objects.filter(
43
- Q(group__pk__in=instance.groups.values_list("pk", flat=True)) |
44
- Q(pk__in=instance.user_permissions.values_list("pk", flat=True))
43
+ Q(group__pk__in=instance.groups.values_list("pk", flat=True))
44
+ | Q(pk__in=instance.user_permissions.values_list("pk", flat=True))
45
45
  ),
46
46
  context=self.context,
47
47
  many=True,
@@ -68,7 +68,6 @@ class UserProfileSerializer(BaseModelSerializer):
68
68
 
69
69
 
70
70
  class UserSerializer(BaseModelSerializer):
71
-
72
71
  id = serializers.IntegerField(read_only=True)
73
72
  password = serializers.CharField(
74
73
  write_only=True, required=False, allow_null=True, allow_blank=True
@@ -1,22 +1,27 @@
1
+ def get_standard_bases() -> tuple:
2
+ from rest_framework.serializers import ModelSerializer
3
+ from camomilla.serializers.fields import FieldsOverrideMixin
4
+ from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
5
+ from camomilla.serializers.mixins import (
6
+ JSONFieldPatchMixin,
7
+ NestMixin,
8
+ OrderingMixin,
9
+ SetupEagerLoadingMixin,
10
+ )
11
+
12
+ return (
13
+ NestMixin,
14
+ FieldsOverrideMixin,
15
+ JSONFieldPatchMixin,
16
+ OrderingMixin,
17
+ RemoveTranslationsMixin,
18
+ SetupEagerLoadingMixin,
19
+ ModelSerializer,
20
+ )
21
+
1
22
  def build_standard_model_serializer(model, depth, bases=None):
2
23
  if bases is None:
3
- from rest_framework.serializers import ModelSerializer
4
- from camomilla.serializers.fields import FieldsOverrideMixin
5
- from camomilla.serializers.mixins import (
6
- JSONFieldPatchMixin,
7
- NestMixin,
8
- OrderingMixin,
9
- SetupEagerLoadingMixin,
10
- )
11
-
12
- bases = (
13
- NestMixin,
14
- FieldsOverrideMixin,
15
- JSONFieldPatchMixin,
16
- OrderingMixin,
17
- SetupEagerLoadingMixin,
18
- ModelSerializer,
19
- )
24
+ bases = get_standard_bases()
20
25
  return type(
21
26
  f"{model.__name__}StandardSerializer",
22
27
  bases,
camomilla/settings.py CHANGED
@@ -67,6 +67,20 @@ ENABLE_MEDIA_OPTIMIZATION = pointed_getter(
67
67
 
68
68
  API_NESTING_DEPTH = pointed_getter(django_settings, "CAMOMILLA.API.NESTING_DEPTH", 10)
69
69
 
70
+ AUTO_CREATE_HOMEPAGE = pointed_getter(
71
+ django_settings, "CAMOMILLA.RENDER.AUTO_CREATE_HOMEPAGE", True
72
+ )
73
+
74
+ TEMPLATE_CONTEXT_FILES = pointed_getter(
75
+ django_settings, "CAMOMILLA.RENDER.TEMPLATE_CONTEXT_FILES", []
76
+ )
77
+
78
+ STRUCTURED_FIELD_CACHE_ENABLED = pointed_getter(
79
+ django_settings, "CAMOMILLA.STRUCTURED_FIELD.CACHE_ENABLED", True
80
+ )
81
+
82
+ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
83
+
70
84
  # camomilla settings example
71
85
  # CAMOMILLA = {
72
86
  # "PROJECT_TITLE": "",
@@ -78,8 +92,14 @@ API_NESTING_DEPTH = pointed_getter(django_settings, "CAMOMILLA.API.NESTING_DEPTH
78
92
  # "THUMBNAIL": {"FOLDER": "", "WIDTH": 50, "HEIGHT": 50}
79
93
  # },
80
94
  # "RENDER": {
95
+ # "TEMPLATE_CONTEXT_FILES": [],
96
+ # "AUTO_CREATE_HOMEPAGE": True,
81
97
  # "ARTICLE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None },
82
98
  # "PAGE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None }
83
99
  # },
84
- # "API": {"NESTING_DEPTH": 10 }
100
+ # "STRUCTURED_FIELD": {
101
+ # "CACHE_ENABLED": True
102
+ # }
103
+ # "API": {"NESTING_DEPTH": 10 },
104
+ # "DEBUG": False
85
105
  # }
@@ -47,6 +47,6 @@ class OptimizedStorage(get_storage_class()):
47
47
  tmp.close()
48
48
  content.close()
49
49
  return optimized_content, True
50
- except:
50
+ except Exception:
51
51
  traceback.print_exc()
52
52
  return content, False
@@ -1,40 +1,47 @@
1
- from collections import defaultdict
1
+ import json
2
2
  from typing import Any
3
3
 
4
4
  from django.db.models import JSONField
5
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
+ )
6
13
 
7
14
  from .fields import *
8
15
  from .models import *
9
- from .models import _Cache, build_model_cache
16
+
17
+
18
+ class StructuredJSONFormField(JSONFormField):
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__(*args, **kwargs)
21
+ self.encoder = kwargs.get("encoder", None)
22
+ self.decoder = kwargs.get("decoder", None)
23
+
24
+ def prepare_value(self, value):
25
+ if isinstance(value, list):
26
+ return json.dumps([v.model_dump() for v in value])
27
+ return value.model_dump_json()
10
28
 
11
29
 
12
30
  class StructuredDescriptior(DeferredAttribute):
31
+ field: "StructuredJSONField"
32
+
13
33
  def __set__(self, instance, value):
14
- if isinstance(value, dict):
15
- self.field.prefetch_related(value)
16
- value = self.field.populate_schema(value)
17
- elif isinstance(value, list):
18
- self.field.prefetch_related(value)
19
- _stack = []
20
- for v in value:
21
- if isinstance(v, dict):
22
- v = self.field.populate_schema(v)
23
- elif not isinstance(v, self.field.schema):
24
- raise TypeError(
25
- f"{type(v)} is not a valid type for the given schema ({self.field.schema})."
26
- )
27
- v.validate()
28
- _stack.append(v)
29
- value = _stack
30
- elif isinstance(value, self.field.schema):
31
- value.validate()
32
- else:
33
- raise TypeError(
34
- f"{type(value)} is not a valid type for the given schema ({self.field.schema})."
35
- )
34
+ # TODO: check if it's better to validate here or in __get__ function (performance reasons)
35
+ # if not self.field.check_type(value):
36
+ # value = self.field.schema.validate_python(value)
36
37
  instance.__dict__[self.field.attname] = value
37
38
 
39
+ def __get__(self, instance, cls=None):
40
+ value = super().__get__(instance, cls)
41
+ if not self.field.check_type(value):
42
+ return self.field.schema.validate_python(value)
43
+ return value
44
+
38
45
 
39
46
  class StructuredJSONField(JSONField):
40
47
  # TODO: share cache in querysets of models having this same field
@@ -42,64 +49,72 @@ class StructuredJSONField(JSONField):
42
49
 
43
50
  descriptor_class = StructuredDescriptior
44
51
 
45
- def __init__(self, schema, *args: Any, **kwargs: Any) -> None:
46
- self.schema = schema
47
- self._cache = _Cache(self)
48
- return super().__init__(*args, **kwargs)
52
+ @property
53
+ def list_data_validator(self):
54
+ def list_data_validator(
55
+ value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
56
+ ) -> Any:
57
+ from camomilla.structured.cache import CacheBuilder
49
58
 
50
- def populate_schema(self, struct):
51
- schema_intance = self.schema()
52
- relations = schema_intance.prepopulate(**struct)
53
- schema_intance.bind(self)
54
- schema_intance.populate(**relations)
55
- return schema_intance
59
+ cache = CacheBuilder.from_model(self.orig_schema)
60
+ if info.mode == "json" and isinstance(value, str):
61
+ return self.schema.validate_python(
62
+ cache.inject_cache(json.loads(value))
63
+ )
64
+ return handler(cache.inject_cache(value))
56
65
 
57
- def get_prep_value(self, value):
58
- if not value:
59
- return super().get_prep_value(value)
60
- if isinstance(value, list):
61
- value = [
62
- (v if isinstance(v, self.schema) else self.populate_schema(v))
63
- for v in value
64
- ]
65
- value = [self.schema.to_db_transform(v.to_struct()) for v in value]
66
- elif isinstance(value, dict):
67
- value = self.schema.to_db_transform(self.populate_schema(value).to_struct())
68
- elif isinstance(value, self.schema):
69
- value = self.schema.to_db_transform(value.to_struct())
70
- else:
71
- raise TypeError(f"{type(value)} is not a valid type for the given schema.")
72
- return super().get_prep_value(value)
73
-
74
- def from_db_value(self, value, expression, connection):
75
- return self.schema.from_db_transform(
76
- super().from_db_value(value, expression, connection)
66
+ return list_data_validator
67
+
68
+ def __init__(self, schema: type[BaseModel], *args: Any, **kwargs: Any) -> None:
69
+ self.orig_schema = schema
70
+ self.schema = schema
71
+ default = kwargs.get("default", dict)
72
+ self.file_handler = kwargs.pop("file_handler", "")
73
+ self.many = kwargs.pop(
74
+ "many", isinstance(default() if callable(default) else default, list)
77
75
  )
76
+ if self.many:
77
+ self.schema = TypeAdapter(
78
+ Annotated[
79
+ list[self.schema],
80
+ Field(default_factory=list),
81
+ WrapValidator(self.list_data_validator),
82
+ ]
83
+ )
84
+ return super().__init__(*args, **kwargs)
78
85
 
79
- def get_prefetched_data(self):
80
- return self._cache.get_prefetched_data()
81
-
82
- def get_all_relateds(self, struct):
83
- if isinstance(struct, list):
84
- relateds = defaultdict(set)
85
- for inner_struct in struct:
86
- if isinstance(inner_struct, dict):
87
- child_relateds = self.schema.get_all_relateds(inner_struct)
88
- for model, pks in child_relateds.items():
89
- relateds[model].update(pks)
90
- return relateds
91
- return self.schema.get_all_relateds(struct)
92
-
93
- def prefetch_related(self, struct):
94
- relateds = self.get_all_relateds(struct)
95
- for model, pks in relateds.items():
96
- self._cache.prefetched_data[model] = (
97
- {obj.pk: obj for obj in build_model_cache(model, pks)}
98
- if len(pks) > 0
99
- else {}
86
+ def check_type(self, value: Any):
87
+ if self.many:
88
+ return isinstance(value, list) and all(
89
+ isinstance(v, self.orig_schema) for v in value
100
90
  )
91
+ return isinstance(value, self.orig_schema)
92
+
93
+ def get_prep_value(
94
+ self, value: Union[list[type[BaseModel]], type[BaseModel]]
95
+ ) -> str:
96
+ if isinstance(value, list) and self.many:
97
+ return self.schema.dump_json(value, exclude_unset=True).decode()
98
+ return value.model_dump_json(exclude_unset=True)
99
+
100
+ def from_db_value(self, value: Any, expression: Any, connection: Any) -> Any:
101
+ data = super().from_db_value(value, expression, connection)
102
+ if isinstance(data, str):
103
+ return json.loads(data)
104
+ return data
101
105
 
102
106
  def deconstruct(self):
103
107
  name, path, args, kwargs = super().deconstruct()
104
108
  kwargs["schema"] = self.schema
105
109
  return name, path, args, kwargs
110
+
111
+ def formfield(self, **kwargs):
112
+ return super().formfield(
113
+ **{
114
+ "form_class": StructuredJSONFormField,
115
+ "schema": self.schema.json_schema(),
116
+ "model_name": self.model.__name__,
117
+ "file_handler": self.file_handler,
118
+ **kwargs,
119
+ }
120
+ )