django-camomilla-cms 6.0.0b16__py2.py3-none-any.whl → 6.0.0b17__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 (62) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
  3. camomilla/dynamic_pages_urls.py +6 -2
  4. camomilla/managers/pages.py +87 -2
  5. camomilla/model_api.py +6 -4
  6. camomilla/models/menu.py +9 -4
  7. camomilla/models/page.py +178 -117
  8. camomilla/openapi/schema.py +15 -10
  9. camomilla/redirects.py +10 -0
  10. camomilla/serializers/base/__init__.py +4 -4
  11. camomilla/serializers/fields/__init__.py +5 -17
  12. camomilla/serializers/fields/related.py +5 -3
  13. camomilla/serializers/mixins/__init__.py +23 -240
  14. camomilla/serializers/mixins/fields.py +20 -0
  15. camomilla/serializers/mixins/filter_fields.py +9 -8
  16. camomilla/serializers/mixins/json.py +34 -0
  17. camomilla/serializers/mixins/language.py +32 -0
  18. camomilla/serializers/mixins/nesting.py +35 -0
  19. camomilla/serializers/mixins/optimize.py +91 -0
  20. camomilla/serializers/mixins/ordering.py +34 -0
  21. camomilla/serializers/mixins/page.py +58 -0
  22. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  23. camomilla/serializers/utils.py +3 -3
  24. camomilla/serializers/validators.py +6 -2
  25. camomilla/settings.py +10 -2
  26. camomilla/storages/default.py +7 -1
  27. camomilla/templates/defaults/parts/menu.html +1 -1
  28. camomilla/templatetags/menus.py +3 -0
  29. camomilla/theme/__init__.py +1 -1
  30. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  31. camomilla/theme/admin/pages.py +46 -0
  32. camomilla/theme/admin/translations.py +13 -0
  33. camomilla/theme/apps.py +1 -5
  34. camomilla/translation.py +7 -1
  35. camomilla/urls.py +2 -5
  36. camomilla/utils/query_parser.py +42 -23
  37. camomilla/utils/translation.py +47 -5
  38. camomilla/views/base/__init__.py +35 -5
  39. camomilla/views/medias.py +1 -1
  40. camomilla/views/mixins/__init__.py +17 -76
  41. camomilla/views/mixins/bulk_actions.py +22 -0
  42. camomilla/views/mixins/language.py +33 -0
  43. camomilla/views/mixins/optimize.py +18 -0
  44. camomilla/views/mixins/pagination.py +11 -8
  45. camomilla/views/mixins/permissions.py +6 -0
  46. camomilla/views/pages.py +12 -2
  47. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
  48. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/RECORD +60 -43
  49. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
  50. tests/test_camomilla_filters.py +1 -1
  51. tests/test_media.py +98 -65
  52. tests/test_menu.py +97 -0
  53. tests/test_model_api_register.py +393 -0
  54. tests/test_pages.py +343 -0
  55. tests/test_query_parser.py +1 -2
  56. tests/test_templates_context.py +111 -0
  57. tests/utils/api.py +0 -1
  58. tests/utils/media.py +9 -0
  59. camomilla/contrib/rest_framework/__init__.py +0 -0
  60. camomilla/serializers/fields/json.py +0 -48
  61. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
  62. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
@@ -1,240 +1,23 @@
1
- import django
2
- from django.conf import settings as django_settings
3
- from django.db.models.aggregates import Max
4
- from django.db.models.functions import Coalesce
5
- from django.utils import translation
6
- from rest_framework import serializers
7
- from rest_framework.utils import model_meta
8
-
9
- from camomilla.models import UrlNode
10
- from camomilla.fields import ORDERING_ACCEPTED_FIELDS
11
- from camomilla.serializers.fields.related import RelatedField
12
- from camomilla.serializers.utils import build_standard_model_serializer
13
- from camomilla.serializers.validators import UniquePermalinkValidator
14
- from camomilla.utils import dict_merge
15
- from camomilla import settings
16
-
17
- if django.VERSION >= (4, 0):
18
- from django.db.models import JSONField as DjangoJSONField
19
- else:
20
- from django.contrib.postgres.fields import JSONField as DjangoJSONField
21
-
22
- from typing import TYPE_CHECKING
23
-
24
- if TYPE_CHECKING:
25
- from camomilla.models.page import AbstractPage
26
-
27
-
28
- # TODO: decide what to do with LangInfoMixin mixin!
29
- class LangInfoMixin(metaclass=serializers.SerializerMetaclass):
30
- """
31
- This mixin adds a "lang_info" field to the serializer.
32
- This field contains information about the languages available in the site.
33
- """
34
- lang_info = serializers.SerializerMethodField("get_lang_info", read_only=True)
35
-
36
- def get_lang_info(self, obj, *args, **kwargs):
37
- languages = []
38
- for key, language in django_settings.LANGUAGES:
39
- languages.append({"id": key, "name": language})
40
- return {
41
- "default": django_settings.LANGUAGE_CODE,
42
- "active": translation.get_language(),
43
- "site_languages": languages,
44
- }
45
-
46
- def get_default_field_names(self, *args):
47
- field_names = super().get_default_field_names(*args)
48
- self.action = getattr(
49
- self, "action", self.context and self.context.get("action", "list")
50
- )
51
- if self.action != "retrieve":
52
- return [f for f in field_names if f != "lang_info"]
53
- return field_names
54
-
55
-
56
- class SetupEagerLoadingMixin:
57
- """
58
- This mixin allows to use the setup_eager_loading method to optimize the queries.
59
- """
60
-
61
- @classmethod
62
- def optimize_qs(cls, queryset, context=None):
63
- if hasattr(cls, "setup_eager_loading"):
64
- queryset = cls.setup_eager_loading(queryset, context=context)
65
- return cls.auto_optimize_queryset(queryset, context=context)
66
-
67
- @classmethod
68
- def auto_optimize_queryset(cls, queryset, context=None):
69
- request = context.get("request", None)
70
- if request and request.method == "GET":
71
- model = getattr(cls.Meta, "model", None)
72
- info = model_meta.get_field_info(model)
73
- only = set()
74
- prefetch_related = set()
75
- select_related = set()
76
- serializer_fields = cls(context=context).fields.keys()
77
- filtered_fields = set()
78
- for field in request.query_params.get("fields", "").split(","):
79
- if "__" in field:
80
- field, _ = field.split("__", 1)
81
- if field in serializer_fields:
82
- filtered_fields.add(field)
83
- if len(filtered_fields) == 0:
84
- filtered_fields = serializer_fields
85
- for field in filtered_fields:
86
- complete_field = field
87
- if "__" in field:
88
- field, sub_field = field.split("__", 1)
89
- complete_field = f"{field}__{sub_field}"
90
- if field in info.forward_relations and not info.forward_relations[field].to_many:
91
- select_related.add(field)
92
- only.add(complete_field)
93
- elif field in info.reverse_relations or field in info.forward_relations and info.forward_relations[field].to_many:
94
- prefetch_related.add(field)
95
- only.add(complete_field)
96
- elif field in info.fields or field == info.pk.name:
97
- only.add(complete_field)
98
- if len(only) > 0:
99
- queryset = queryset.only(*only)
100
- if len(select_related) > 0:
101
- queryset = queryset.select_related(*select_related)
102
- if len(prefetch_related) > 0:
103
- queryset = queryset.prefetch_related(*prefetch_related)
104
- return queryset
105
-
106
-
107
- class OrderingMixin:
108
- """
109
- This mixin allows to set the default value of an ordering field to the max value + 1.
110
- """
111
-
112
- def get_max_order(self, order_field):
113
- return self.Meta.model.objects.aggregate(
114
- max_order=Coalesce(Max(order_field), 0)
115
- )["max_order"]
116
-
117
- def _get_ordering_field_name(self):
118
- try:
119
- field_name = self.Meta.model._meta.ordering[0]
120
- if field_name[0] == "-":
121
- field_name = field_name[1:]
122
- return field_name
123
- except (AttributeError, IndexError):
124
- return None
125
-
126
- def build_standard_field(self, field_name, model_field):
127
- field_class, field_kwargs = super().build_standard_field(
128
- field_name, model_field
129
- )
130
- if (
131
- isinstance(model_field, ORDERING_ACCEPTED_FIELDS)
132
- and field_name == self._get_ordering_field_name()
133
- ):
134
- field_kwargs["default"] = self.get_max_order(field_name) + 1
135
- return field_class, field_kwargs
136
-
137
-
138
- class JSONFieldPatchMixin:
139
- """
140
- This mixin allows to patch JSONField values during partial updates.
141
- This means that, if a JSONField is present in the request and the requsest uses PATCH method,
142
- the serializer will merge the new data with the old one.
143
- """
144
-
145
- def is_json_field(self, attr, value, info):
146
- return (
147
- attr in info.fields
148
- and isinstance(info.fields[attr], DjangoJSONField)
149
- and isinstance(value, dict)
150
- )
151
-
152
- def update(self, instance, validated_data):
153
- if self.partial:
154
- info = model_meta.get_field_info(instance)
155
- for attr, value in validated_data.items():
156
- if self.is_json_field(attr, value, info):
157
- validated_data[attr] = dict_merge(
158
- getattr(instance, attr, {}), value
159
- )
160
- return super().update(instance, validated_data)
161
-
162
-
163
- class NestMixin:
164
- """
165
- This mixin automatically creates nested serializers for relational fields.
166
- The depth of the nesting can be set using the "depth" attribute of the Meta class.
167
- If the depth is not set, the serializer will use the value coming from the settings.
168
-
169
- CAMOMILLA = { "API": {"NESTING_DEPTH": 10} }
170
- """
171
-
172
- def __init__(self, *args, **kwargs):
173
- self._depth = kwargs.pop("depth", None)
174
- return super().__init__(*args, **kwargs)
175
-
176
- def build_nested_field(self, field_name, relation_info, nested_depth):
177
- return self.build_relational_field(field_name, relation_info, nested_depth)
178
-
179
- def build_relational_field(
180
- self, field_name, relation_info, nested_depth=settings.API_NESTING_DEPTH + 1
181
- ):
182
- nested_depth = nested_depth if self._depth is None else self._depth
183
- field_class, field_kwargs = super().build_relational_field(
184
- field_name, relation_info
185
- )
186
- if (
187
- field_class is RelatedField and nested_depth > 1
188
- ): # stop recursion one step before the jump :P
189
- field_kwargs["serializer"] = build_standard_model_serializer(
190
- relation_info[1], nested_depth - 1
191
- )
192
- return field_class, field_kwargs
193
-
194
-
195
- class AbstractPageMixin(serializers.ModelSerializer):
196
- """
197
- This mixin is needed to serialize AbstractPage models.
198
- It provides permalink validation and some extra fields serialization.
199
-
200
- Use it as a base class for your serializer if you need to serialize custom AbstractPage models.
201
- """
202
-
203
- breadcrumbs = serializers.SerializerMethodField()
204
- routerlink = serializers.CharField(read_only=True)
205
- template_file = serializers.SerializerMethodField()
206
-
207
- def get_template_file(self, instance: "AbstractPage"):
208
- return instance.get_template_path()
209
-
210
- def get_breadcrumbs(self, instance: "AbstractPage"):
211
- return instance.breadcrumbs
212
-
213
- @property
214
- def translation_fields(self):
215
- return super().translation_fields + ["permalink"]
216
-
217
- def get_default_field_names(self, *args):
218
- from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
219
- default_fields = super().get_default_field_names(*args)
220
- filtered_fields = getattr(self, "filtered_fields", [])
221
- if len(filtered_fields) > 0:
222
- return filtered_fields
223
- if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
224
- return default_fields
225
- return (
226
- [f for f in default_fields if f != "url_node"]
227
- + UrlNode.LANG_PERMALINK_FIELDS
228
- + ["permalink"]
229
- )
230
-
231
- def build_field(self, field_name, info, model_class, nested_depth):
232
- if field_name in UrlNode.LANG_PERMALINK_FIELDS + ["permalink"]:
233
- return serializers.CharField, {
234
- "required": False,
235
- "allow_blank": True,
236
- }
237
- return super().build_field(field_name, info, model_class, nested_depth)
238
-
239
- def get_validators(self):
240
- return super().get_validators() + [UniquePermalinkValidator()]
1
+ from .fields import FieldsOverrideMixin
2
+ from .filter_fields import FilterFieldsMixin
3
+ from .json import JSONFieldPatchMixin
4
+ from .language import LangInfoMixin
5
+ from .nesting import NestMixin
6
+ from .optimize import SetupEagerLoadingMixin
7
+ from .ordering import OrderingMixin
8
+ from .page import AbstractPageMixin
9
+ from .translation import TranslationsMixin, RemoveTranslationsMixin
10
+
11
+
12
+ __all__ = [
13
+ "FieldsOverrideMixin",
14
+ "FilterFieldsMixin",
15
+ "JSONFieldPatchMixin",
16
+ "LangInfoMixin",
17
+ "NestMixin",
18
+ "SetupEagerLoadingMixin",
19
+ "OrderingMixin",
20
+ "AbstractPageMixin",
21
+ "TranslationsMixin",
22
+ "RemoveTranslationsMixin",
23
+ ]
@@ -0,0 +1,20 @@
1
+ from django.db import models
2
+ from rest_framework import serializers
3
+
4
+ from structured.fields import StructuredJSONField as ModelStructuredJSONField
5
+ from camomilla.serializers.fields import FileField, ImageField, RelatedField
6
+ from structured.contrib.restframework import StructuredJSONField
7
+
8
+
9
+ class FieldsOverrideMixin:
10
+ """
11
+ This mixin automatically overrides the fields of the serializer with camomilla's backed ones.
12
+ """
13
+
14
+ serializer_field_mapping = {
15
+ **serializers.ModelSerializer.serializer_field_mapping,
16
+ models.FileField: FileField,
17
+ models.ImageField: ImageField,
18
+ ModelStructuredJSONField: StructuredJSONField,
19
+ }
20
+ serializer_related_field = RelatedField
@@ -7,15 +7,13 @@ class FilterFieldsMixin(serializers.ModelSerializer):
7
7
  """
8
8
  Mixin to filter fields from a serializer, including handling nested fields.
9
9
  """
10
-
10
+
11
11
  def __init__(self, *args, **kwargs):
12
12
  self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
13
13
  return super().__init__(*args, **kwargs)
14
-
15
14
 
16
15
  inherited_fields_filter = []
17
16
 
18
-
19
17
  def get_default_field_names(self, *args):
20
18
  field_names = super().get_default_field_names(*args)
21
19
  request = self.context.get("request", None)
@@ -34,8 +32,6 @@ class FilterFieldsMixin(serializers.ModelSerializer):
34
32
  if parent_field in field_names:
35
33
  self.filtered_fields.add(parent_field)
36
34
  self.childs_fields[parent_field].add(child_field)
37
-
38
-
39
35
  else:
40
36
  if field in field_names:
41
37
  self.filtered_fields.add(field)
@@ -48,9 +44,14 @@ class FilterFieldsMixin(serializers.ModelSerializer):
48
44
  return field_names
49
45
 
50
46
  def build_field(self, field_name, info, model_class, nested_depth):
51
- field_class, field_kwargs = super().build_field(field_name, info, model_class, nested_depth)
52
- inherited_fields_filter = self.childs_fields.get(field_name, []) if hasattr(self, "childs_fields") else []
47
+ field_class, field_kwargs = super().build_field(
48
+ field_name, info, model_class, nested_depth
49
+ )
50
+ inherited_fields_filter = (
51
+ self.childs_fields.get(field_name, [])
52
+ if hasattr(self, "childs_fields")
53
+ else []
54
+ )
53
55
  if len(inherited_fields_filter) > 0 and issubclass(field_class, RelatedField):
54
56
  field_kwargs["inherited_fields_filter"] = list(inherited_fields_filter)
55
57
  return field_class, field_kwargs
56
-
@@ -0,0 +1,34 @@
1
+ import django
2
+ from rest_framework.utils import model_meta
3
+
4
+ from camomilla.utils import dict_merge
5
+
6
+ if django.VERSION >= (4, 0):
7
+ from django.db.models import JSONField as DjangoJSONField
8
+ else:
9
+ from django.contrib.postgres.fields import JSONField as DjangoJSONField
10
+
11
+
12
+ class JSONFieldPatchMixin:
13
+ """
14
+ This mixin allows to patch JSONField values during partial updates.
15
+ This means that, if a JSONField is present in the request and the requsest uses PATCH method,
16
+ the serializer will merge the new data with the old one.
17
+ """
18
+
19
+ def is_json_field(self, attr, value, info):
20
+ return (
21
+ attr in info.fields
22
+ and isinstance(info.fields[attr], DjangoJSONField)
23
+ and isinstance(value, dict)
24
+ )
25
+
26
+ def update(self, instance, validated_data):
27
+ if self.partial:
28
+ info = model_meta.get_field_info(instance)
29
+ for attr, value in validated_data.items():
30
+ if self.is_json_field(attr, value, info):
31
+ validated_data[attr] = dict_merge(
32
+ getattr(instance, attr, {}), value
33
+ )
34
+ return super().update(instance, validated_data)
@@ -0,0 +1,32 @@
1
+ from django.conf import settings as django_settings
2
+ from django.utils import translation
3
+ from rest_framework import serializers
4
+
5
+
6
+ # TODO: decide what to do with LangInfoMixin mixin!
7
+ class LangInfoMixin(metaclass=serializers.SerializerMetaclass):
8
+ """
9
+ This mixin adds a "lang_info" field to the serializer.
10
+ This field contains information about the languages available in the site.
11
+ """
12
+
13
+ lang_info = serializers.SerializerMethodField("get_lang_info", read_only=True)
14
+
15
+ def get_lang_info(self, obj, *args, **kwargs):
16
+ languages = []
17
+ for key, language in django_settings.LANGUAGES:
18
+ languages.append({"id": key, "name": language})
19
+ return {
20
+ "default": django_settings.LANGUAGE_CODE,
21
+ "active": translation.get_language(),
22
+ "site_languages": languages,
23
+ }
24
+
25
+ def get_default_field_names(self, *args):
26
+ field_names = super().get_default_field_names(*args)
27
+ self.action = getattr(
28
+ self, "action", self.context and self.context.get("action", "list")
29
+ )
30
+ if self.action != "retrieve":
31
+ return [f for f in field_names if f != "lang_info"]
32
+ return field_names
@@ -0,0 +1,35 @@
1
+ from camomilla.serializers.fields.related import RelatedField
2
+ from camomilla.serializers.utils import build_standard_model_serializer
3
+ from camomilla import settings
4
+
5
+
6
+ class NestMixin:
7
+ """
8
+ This mixin automatically creates nested serializers for relational fields.
9
+ The depth of the nesting can be set using the "depth" attribute of the Meta class.
10
+ If the depth is not set, the serializer will use the value coming from the settings.
11
+
12
+ CAMOMILLA = { "API": {"NESTING_DEPTH": 10} }
13
+ """
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ self._depth = kwargs.pop("depth", None)
17
+ return super().__init__(*args, **kwargs)
18
+
19
+ def build_nested_field(self, field_name, relation_info, nested_depth):
20
+ return self.build_relational_field(field_name, relation_info, nested_depth)
21
+
22
+ def build_relational_field(
23
+ self, field_name, relation_info, nested_depth=settings.API_NESTING_DEPTH + 1
24
+ ):
25
+ nested_depth = nested_depth if self._depth is None else self._depth
26
+ field_class, field_kwargs = super().build_relational_field(
27
+ field_name, relation_info
28
+ )
29
+ if (
30
+ field_class is RelatedField and nested_depth > 1
31
+ ): # stop recursion one step before the jump :P
32
+ field_kwargs["serializer"] = build_standard_model_serializer(
33
+ relation_info[1], nested_depth - 1
34
+ )
35
+ return field_class, field_kwargs
@@ -0,0 +1,91 @@
1
+ from rest_framework.utils import model_meta
2
+
3
+
4
+ class Optimizations:
5
+ only = set()
6
+ select_related = set()
7
+ prefetch_related = set()
8
+
9
+ def __init__(self):
10
+ self.only = set()
11
+ self.select_related = set()
12
+ self.prefetch_related = set()
13
+
14
+ def __str__(self):
15
+ return f"Optimizations(only={self.only}, select_related={self.select_related}, prefetch_related={self.prefetch_related})"
16
+
17
+
18
+ def recursive_extract_optimizations(fields, info, max_depth=100) -> Optimizations:
19
+ optimizations = Optimizations()
20
+ if max_depth == 0:
21
+ return optimizations
22
+ for field in fields:
23
+ if "__" in field and field not in info.fields:
24
+ field_part_1, field_part_2 = field.split("__", 1)
25
+ if field_part_1 in info.relations:
26
+ field_info = info.relations[field_part_1]
27
+ nested_info = model_meta.get_field_info(field_info.related_model)
28
+ if field_info.to_many:
29
+ optimizations.prefetch_related.add(field_part_1)
30
+ optimizations.only.add(field_part_1)
31
+ else:
32
+ optimizations.select_related.add(field_part_1)
33
+ optimizations.only.add(field_part_1)
34
+ nested_optimizations = recursive_extract_optimizations(
35
+ [field_part_2], nested_info, max_depth - 1
36
+ )
37
+
38
+ for nested_field in nested_optimizations.only:
39
+ optimizations.only.add(f"{field_part_1}__{nested_field}")
40
+ for nested_field in nested_optimizations.select_related:
41
+ optimizations.select_related.add(f"{field_part_1}__{nested_field}")
42
+ for nested_field in nested_optimizations.prefetch_related:
43
+ optimizations.prefetch_related.add(
44
+ f"{field_part_1}__{nested_field}"
45
+ )
46
+ else:
47
+ if field in info.relations:
48
+ if info.relations[field].to_many:
49
+ optimizations.prefetch_related.add(field)
50
+ else:
51
+ optimizations.select_related.add(field)
52
+ optimizations.only.add(field)
53
+ elif field in info.fields:
54
+ optimizations.only.add(field)
55
+ return optimizations
56
+
57
+
58
+ class SetupEagerLoadingMixin:
59
+ """
60
+ This mixin allows to use the setup_eager_loading method to optimize the queries.
61
+ """
62
+
63
+ @classmethod
64
+ def optimize_qs(cls, queryset, context=None):
65
+ if hasattr(cls, "setup_eager_loading"):
66
+ queryset = cls.setup_eager_loading(queryset, context=context)
67
+ return cls.auto_optimize_queryset(queryset, context=context)
68
+
69
+ @classmethod
70
+ def auto_optimize_queryset(cls, queryset, context=None):
71
+ request = context.get("request", None)
72
+ if request and request.method == "GET":
73
+ serializer_fields = cls(context=context).fields.keys()
74
+ filtered_fields = set()
75
+ for field in request.query_params.get("fields", "").split(","):
76
+ filtered_fields.add(field)
77
+ if len(filtered_fields) == 0:
78
+ filtered_fields = serializer_fields
79
+ model = getattr(cls.Meta, "model", None)
80
+ if not model:
81
+ return queryset
82
+ optimizations = recursive_extract_optimizations(
83
+ filtered_fields, model_meta.get_field_info(model)
84
+ )
85
+ if len(optimizations.only) > 0:
86
+ queryset = queryset.only(*optimizations.only)
87
+ if len(optimizations.select_related) > 0:
88
+ queryset = queryset.select_related(*optimizations.select_related)
89
+ if len(optimizations.prefetch_related) > 0:
90
+ queryset = queryset.prefetch_related(*optimizations.prefetch_related)
91
+ return queryset
@@ -0,0 +1,34 @@
1
+ from django.db.models.aggregates import Max
2
+ from django.db.models.functions import Coalesce
3
+ from camomilla.fields import ORDERING_ACCEPTED_FIELDS
4
+
5
+
6
+ class OrderingMixin:
7
+ """
8
+ This mixin allows to set the default value of an ordering field to the max value + 1.
9
+ """
10
+
11
+ def get_max_order(self, order_field):
12
+ return self.Meta.model.objects.aggregate(
13
+ max_order=Coalesce(Max(order_field), 0)
14
+ )["max_order"]
15
+
16
+ def _get_ordering_field_name(self):
17
+ try:
18
+ field_name = self.Meta.model._meta.ordering[0]
19
+ if field_name[0] == "-":
20
+ field_name = field_name[1:]
21
+ return field_name
22
+ except (AttributeError, IndexError):
23
+ return None
24
+
25
+ def build_standard_field(self, field_name, model_field):
26
+ field_class, field_kwargs = super().build_standard_field(
27
+ field_name, model_field
28
+ )
29
+ if (
30
+ isinstance(model_field, ORDERING_ACCEPTED_FIELDS)
31
+ and field_name == self._get_ordering_field_name()
32
+ ):
33
+ field_kwargs["default"] = self.get_max_order(field_name) + 1
34
+ return field_class, field_kwargs
@@ -0,0 +1,58 @@
1
+ from rest_framework import serializers
2
+ from camomilla.models import UrlNode
3
+ from camomilla.serializers.validators import UniquePermalinkValidator
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from camomilla.models.page import AbstractPage
8
+
9
+
10
+ class AbstractPageMixin(serializers.ModelSerializer):
11
+ """
12
+ This mixin is needed to serialize AbstractPage models.
13
+ It provides permalink validation and some extra fields serialization.
14
+
15
+ Use it as a base class for your serializer if you need to serialize custom AbstractPage models.
16
+ """
17
+
18
+ breadcrumbs = serializers.SerializerMethodField()
19
+ routerlink = serializers.CharField(read_only=True)
20
+ template_file = serializers.SerializerMethodField()
21
+
22
+ def get_template_file(self, instance: "AbstractPage"):
23
+ return instance.get_template_path()
24
+
25
+ def get_breadcrumbs(self, instance: "AbstractPage"):
26
+ return instance.breadcrumbs
27
+
28
+ @property
29
+ def translation_fields(self):
30
+ return super().translation_fields + ["permalink"]
31
+
32
+ def get_default_field_names(self, *args):
33
+ from camomilla.serializers.mixins.translation import RemoveTranslationsMixin
34
+
35
+ default_fields = super().get_default_field_names(*args)
36
+ filtered_fields = getattr(self, "filtered_fields", [])
37
+ if len(filtered_fields) > 0:
38
+ return filtered_fields
39
+ if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
40
+ return default_fields
41
+ return list(
42
+ set(
43
+ [f for f in default_fields if f != "url_node"]
44
+ + UrlNode.LANG_PERMALINK_FIELDS
45
+ + ["permalink"]
46
+ )
47
+ )
48
+
49
+ def build_field(self, field_name, info, model_class, nested_depth):
50
+ if field_name in UrlNode.LANG_PERMALINK_FIELDS + ["permalink"]:
51
+ return serializers.CharField, {
52
+ "required": False,
53
+ "allow_blank": True,
54
+ }
55
+ return super().build_field(field_name, info, model_class, nested_depth)
56
+
57
+ def get_validators(self):
58
+ return super().get_validators() + [UniquePermalinkValidator()]