django-camomilla-cms 5.8.5__py2.py3-none-any.whl → 6.0.0__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 (143) hide show
  1. camomilla/__init__.py +8 -2
  2. camomilla/apps.py +9 -1
  3. camomilla/context_processors.py +6 -0
  4. camomilla/contrib/modeltranslation/__init__.py +0 -0
  5. camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
  6. camomilla/dynamic_pages_urls.py +33 -0
  7. camomilla/fields/__init__.py +13 -0
  8. camomilla/{fields.py → fields/json.py} +15 -18
  9. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  10. camomilla/managers/__init__.py +3 -0
  11. camomilla/managers/pages.py +116 -0
  12. camomilla/model_api.py +86 -0
  13. camomilla/models/__init__.py +5 -6
  14. camomilla/models/article.py +26 -44
  15. camomilla/models/content.py +8 -15
  16. camomilla/models/media.py +70 -97
  17. camomilla/models/menu.py +106 -0
  18. camomilla/models/mixins/__init__.py +10 -48
  19. camomilla/models/page.py +521 -20
  20. camomilla/openapi/__init__.py +0 -0
  21. camomilla/openapi/schema.py +67 -0
  22. camomilla/parsers.py +0 -1
  23. camomilla/redirects.py +10 -0
  24. camomilla/serializers/__init__.py +2 -0
  25. camomilla/serializers/article.py +5 -10
  26. camomilla/serializers/base/__init__.py +21 -17
  27. camomilla/serializers/content_type.py +17 -0
  28. camomilla/serializers/fields/__init__.py +6 -20
  29. camomilla/serializers/fields/file.py +5 -0
  30. camomilla/serializers/fields/related.py +24 -4
  31. camomilla/serializers/media.py +6 -8
  32. camomilla/serializers/menu.py +17 -0
  33. camomilla/serializers/mixins/__init__.py +23 -187
  34. camomilla/serializers/mixins/fields.py +20 -0
  35. camomilla/serializers/mixins/filter_fields.py +57 -0
  36. camomilla/serializers/mixins/json.py +34 -0
  37. camomilla/serializers/mixins/language.py +32 -0
  38. camomilla/serializers/mixins/nesting.py +35 -0
  39. camomilla/serializers/mixins/optimize.py +91 -0
  40. camomilla/serializers/mixins/ordering.py +34 -0
  41. camomilla/serializers/mixins/page.py +58 -0
  42. camomilla/serializers/mixins/translation.py +103 -0
  43. camomilla/serializers/page.py +53 -4
  44. camomilla/serializers/user.py +5 -4
  45. camomilla/serializers/utils.py +38 -0
  46. camomilla/serializers/validators.py +51 -0
  47. camomilla/settings.py +118 -0
  48. camomilla/sitemap.py +30 -0
  49. camomilla/storages/__init__.py +4 -0
  50. camomilla/storages/default.py +12 -0
  51. camomilla/storages/optimize.py +71 -0
  52. camomilla/{storages.py → storages/overwrite.py} +2 -2
  53. camomilla/templates/admin/camomilla/page/change_form.html +10 -0
  54. camomilla/templates/defaults/articles/default.html +7 -0
  55. camomilla/templates/defaults/base.html +170 -0
  56. camomilla/templates/defaults/pages/default.html +3 -0
  57. camomilla/templates/defaults/parts/langswitch.html +83 -0
  58. camomilla/templates/defaults/parts/menu.html +15 -0
  59. camomilla/templates_context/__init__.py +0 -0
  60. camomilla/templates_context/autodiscover.py +51 -0
  61. camomilla/templates_context/rendering.py +89 -0
  62. camomilla/templatetags/camomilla_filters.py +6 -5
  63. camomilla/templatetags/menus.py +37 -0
  64. camomilla/templatetags/model_extras.py +77 -0
  65. camomilla/theme/__init__.py +1 -1
  66. camomilla/theme/admin/__init__.py +99 -0
  67. camomilla/theme/admin/pages.py +46 -0
  68. camomilla/theme/admin/translations.py +13 -0
  69. camomilla/theme/apps.py +38 -0
  70. camomilla/theme/static/admin/css/responsive.css +5 -1021
  71. camomilla/theme/static/admin/img/favicon.ico +0 -0
  72. camomilla/theme/static/admin/img/logo.svg +31 -0
  73. camomilla/theme/templates/admin/base.html +7 -0
  74. camomilla/theme/templates/rosetta/base.html +196 -0
  75. camomilla/translation.py +61 -0
  76. camomilla/urls.py +38 -17
  77. camomilla/utils/__init__.py +4 -0
  78. camomilla/utils/getters.py +27 -0
  79. camomilla/utils/normalization.py +7 -0
  80. camomilla/utils/query_parser.py +167 -0
  81. camomilla/{utils.py → utils/seo.py} +13 -15
  82. camomilla/utils/setters.py +37 -0
  83. camomilla/utils/templates.py +32 -0
  84. camomilla/utils/translation.py +114 -0
  85. camomilla/views/__init__.py +1 -1
  86. camomilla/views/articles.py +5 -7
  87. camomilla/views/base/__init__.py +35 -5
  88. camomilla/views/contents.py +6 -11
  89. camomilla/views/decorators.py +26 -0
  90. camomilla/views/medias.py +24 -19
  91. camomilla/views/menus.py +81 -0
  92. camomilla/views/mixins/__init__.py +17 -73
  93. camomilla/views/mixins/bulk_actions.py +22 -0
  94. camomilla/views/mixins/language.py +33 -0
  95. camomilla/views/mixins/optimize.py +18 -0
  96. camomilla/views/mixins/ordering.py +2 -2
  97. camomilla/views/mixins/pagination.py +12 -18
  98. camomilla/views/mixins/permissions.py +6 -0
  99. camomilla/views/pages.py +28 -6
  100. camomilla/views/tags.py +5 -6
  101. camomilla/views/users.py +7 -12
  102. django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
  103. django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
  104. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
  105. tests/fixtures/__init__.py +14 -0
  106. tests/test_api.py +22 -39
  107. tests/test_camomilla_filters.py +11 -13
  108. tests/test_media.py +152 -0
  109. tests/test_menu.py +112 -0
  110. tests/test_model_api.py +113 -0
  111. tests/test_model_api_permissions.py +44 -0
  112. tests/test_model_api_register.py +355 -0
  113. tests/test_pages.py +351 -0
  114. tests/test_query_parser.py +58 -0
  115. tests/test_templates_context.py +149 -0
  116. tests/test_utils.py +64 -64
  117. tests/utils/__init__.py +0 -0
  118. tests/utils/api.py +28 -0
  119. tests/utils/media.py +10 -0
  120. camomilla/admin.py +0 -98
  121. camomilla/migrations/0001_initial.py +0 -577
  122. camomilla/migrations/0002_auto_20200214_1127.py +0 -33
  123. camomilla/migrations/0003_auto_20210130_1610.py +0 -30
  124. camomilla/migrations/0004_auto_20210511_0937.py +0 -25
  125. camomilla/migrations/0005_media_image_props.py +0 -19
  126. camomilla/migrations/0006_auto_20220103_1845.py +0 -35
  127. camomilla/migrations/0007_auto_20220211_1622.py +0 -18
  128. camomilla/migrations/0008_auto_20220309_1616.py +0 -60
  129. camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
  130. camomilla/migrations/0010_auto_20220802_1406.py +0 -83
  131. camomilla/migrations/0011_auto_20220902_1000.py +0 -15
  132. camomilla/models/category.py +0 -25
  133. camomilla/models/tag.py +0 -19
  134. camomilla/theme/static/admin/img/logo.png +0 -0
  135. camomilla/theme/templates/admin/base_site.html +0 -18
  136. camomilla/views/categories.py +0 -13
  137. django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
  138. django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
  139. tests/urls.py +0 -21
  140. /camomilla/{migrations → contrib}/__init__.py +0 -0
  141. /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
  142. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
  143. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
@@ -1,35 +1,39 @@
1
- from hvad.contrib.restframework import TranslatableModelSerializer
2
1
  from rest_framework import serializers
3
2
 
4
- from ..fields import FieldsOverrideMixin, TranslatableFieldsOverrideMixin
5
- from ..mixins import (
3
+ from camomilla.serializers.mixins import (
6
4
  JSONFieldPatchMixin,
7
- LangInfoMixin,
5
+ NestMixin,
8
6
  OrderingMixin,
9
7
  SetupEagerLoadingMixin,
10
- TranslationSetMixin,
11
- NestMixin,
8
+ FilterFieldsMixin,
9
+ FieldsOverrideMixin,
10
+ TranslationsMixin,
12
11
  )
13
12
 
14
13
 
15
14
  class BaseModelSerializer(
15
+ SetupEagerLoadingMixin,
16
16
  NestMixin,
17
+ FilterFieldsMixin,
17
18
  FieldsOverrideMixin,
18
19
  JSONFieldPatchMixin,
19
20
  OrderingMixin,
20
- SetupEagerLoadingMixin,
21
+ TranslationsMixin,
21
22
  serializers.ModelSerializer,
22
23
  ):
24
+ """
25
+ This is the base serializer for all the models.
26
+ It adds support for:
27
+ - nesting translations fields under a "translations" field
28
+ - overriding related fields with auto-generated serializers
29
+ - patching JSONField
30
+ - ordering
31
+ - eager loading
32
+ """
33
+
23
34
  pass
24
35
 
25
36
 
26
- class BaseTranslatableModelSerializer(
27
- NestMixin,
28
- TranslatableFieldsOverrideMixin,
29
- JSONFieldPatchMixin,
30
- OrderingMixin,
31
- LangInfoMixin,
32
- TranslationSetMixin,
33
- TranslatableModelSerializer,
34
- ):
35
- pass
37
+ __all__ = [
38
+ "BaseModelSerializer",
39
+ ]
@@ -0,0 +1,17 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from rest_framework import serializers
3
+
4
+
5
+ class ContentTypeSerializer(serializers.ModelSerializer):
6
+ verbose_name = serializers.SerializerMethodField()
7
+ verbose_name_plural = serializers.SerializerMethodField()
8
+
9
+ def get_verbose_name(self, obj):
10
+ return obj.model_class()._meta.verbose_name.title()
11
+
12
+ def get_verbose_name_plural(self, obj):
13
+ return obj.model_class()._meta.verbose_name_plural.title()
14
+
15
+ class Meta:
16
+ model = ContentType
17
+ fields = "__all__"
@@ -1,23 +1,9 @@
1
+ from .file import FileField, ImageField
1
2
  from .related import RelatedField
2
- from .file import ImageField, FileField
3
- from hvad.contrib.restframework import TranslatableModelSerializer
4
- from django.db import models
5
- from rest_framework import serializers
6
3
 
7
4
 
8
- class FieldsOverrideMixin:
9
- serializer_field_mapping = {
10
- **serializers.ModelSerializer.serializer_field_mapping,
11
- models.FileField: FileField,
12
- models.ImageField: ImageField,
13
- }
14
- serializer_related_field = RelatedField
15
-
16
-
17
- class TranslatableFieldsOverrideMixin(FieldsOverrideMixin):
18
-
19
- serializer_field_mapping = {
20
- **TranslatableModelSerializer.serializer_field_mapping,
21
- models.FileField: FileField,
22
- models.ImageField: ImageField,
23
- }
5
+ __all__ = [
6
+ "FileField",
7
+ "ImageField",
8
+ "RelatedField",
9
+ ]
@@ -4,6 +4,11 @@ from rest_framework.fields import Field
4
4
 
5
5
 
6
6
  class SafeFileFieldMixin(Field):
7
+ """
8
+ This mixin prevents errors when trying to upload a file passing its current url.
9
+ In such cases, the current file will be kept.
10
+ """
11
+
7
12
  def to_internal_value(self, data):
8
13
  current = getattr(self.parent.instance, self.field_name, None)
9
14
  if (
@@ -3,7 +3,22 @@ from rest_framework import serializers, relations
3
3
 
4
4
 
5
5
  class RelatedField(serializers.PrimaryKeyRelatedField):
6
+ """
7
+ This field helps to serialize and deserialize related data.
8
+ It serializes related data building a nested serializer.
9
+ Allowing insertions with both nested and plain data.
10
+
11
+
12
+ For example it accepts as input data both:
13
+ ```json
14
+ {"related_field": 1}
15
+ {"related_field": {"id": 1, "field": "value"}}
16
+ ```
17
+
18
+ """
19
+
6
20
  def __init__(self, **kwargs):
21
+ self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
7
22
  self.serializer = kwargs.pop("serializer", None)
8
23
  self.lookup = kwargs.pop("lookup", "id")
9
24
  if self.serializer is not None:
@@ -28,7 +43,10 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
28
43
 
29
44
  def to_representation(self, instance):
30
45
  if self.serializer:
31
- return self.serializer(instance, context=self.context).data
46
+ kwargs = {"context": self.context}
47
+ if self.inherited_fields_filter:
48
+ kwargs["inherited_fields_filter"] = self.inherited_fields_filter
49
+ return self.serializer(instance, **kwargs).data
32
50
  return super().to_representation(instance)
33
51
 
34
52
  def to_internal_value(self, data):
@@ -79,9 +97,11 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
79
97
  for item in child.get_queryset().filter(
80
98
  **{
81
99
  f"{child.lookup}__in": [
82
- item.get(child.lookup, None)
83
- if isinstance(item, dict)
84
- else item
100
+ (
101
+ item.get(child.lookup, None)
102
+ if isinstance(item, dict)
103
+ else item
104
+ )
85
105
  for item in data
86
106
  ]
87
107
  }
@@ -1,11 +1,11 @@
1
1
  from rest_framework import serializers
2
2
 
3
- from ..models import Media, MediaFolder
4
- from ..storages import OverwriteStorage
5
- from .base import BaseTranslatableModelSerializer
3
+ from camomilla.models import Media, MediaFolder
4
+ from camomilla.serializers.base import BaseModelSerializer
5
+ from camomilla.storages import OverwriteStorage
6
6
 
7
7
 
8
- class MediaListSerializer(BaseTranslatableModelSerializer):
8
+ class MediaListSerializer(BaseModelSerializer):
9
9
  is_image = serializers.SerializerMethodField("get_is_image")
10
10
 
11
11
  def get_is_image(self, obj):
@@ -16,7 +16,7 @@ class MediaListSerializer(BaseTranslatableModelSerializer):
16
16
  fields = "__all__"
17
17
 
18
18
 
19
- class MediaSerializer(BaseTranslatableModelSerializer):
19
+ class MediaSerializer(BaseModelSerializer):
20
20
  links = serializers.SerializerMethodField("get_linked_instances")
21
21
  is_image = serializers.SerializerMethodField("get_is_image")
22
22
 
@@ -29,8 +29,6 @@ class MediaSerializer(BaseTranslatableModelSerializer):
29
29
  links = obj.get_foreign_fields()
30
30
  for link in links:
31
31
  manager = getattr(obj, link)
32
- if hasattr(manager, "language"):
33
- manager = manager.language()
34
32
  for item in manager.all():
35
33
  if item.__class__.__name__ != "MediaTranslation":
36
34
  result.append(
@@ -57,7 +55,7 @@ class MediaSerializer(BaseTranslatableModelSerializer):
57
55
  return obj.is_image
58
56
 
59
57
 
60
- class MediaFolderSerializer(BaseTranslatableModelSerializer):
58
+ class MediaFolderSerializer(BaseModelSerializer):
61
59
  icon = MediaSerializer(read_only=True)
62
60
 
63
61
  class Meta:
@@ -0,0 +1,17 @@
1
+ from camomilla.models import Menu
2
+ from camomilla.serializers.base import BaseModelSerializer
3
+
4
+
5
+ class MenuSerializer(BaseModelSerializer):
6
+ def get_default_field_names(self, *args):
7
+ field_names = super().get_default_field_names(*args)
8
+ self.action = getattr(
9
+ self, "action", self.context and self.context.get("action", "list")
10
+ )
11
+ if self.action == "list":
12
+ return [f for f in field_names if f != "nodes"]
13
+ return field_names
14
+
15
+ class Meta:
16
+ model = Menu
17
+ fields = "__all__"
@@ -1,187 +1,23 @@
1
- import django
2
- from django.conf import 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 hvad.contrib.restframework import TranslatableModelSerializer, TranslationsMixin
7
- from hvad.models import TranslatableModel
8
- from rest_framework import serializers
9
- from rest_framework.utils import model_meta
10
-
11
- from ...fields import ORDERING_ACCEPTED_FIELDS
12
- from ...utils import dict_merge
13
- from ..fields import FieldsOverrideMixin, TranslatableFieldsOverrideMixin
14
- from ..fields.related import RelatedField
15
-
16
- if django.VERSION >= (4, 0):
17
- from django.db.models import JSONField as DjangoJSONField
18
- else:
19
- from django.contrib.postgres.fields import JSONField as DjangoJSONField
20
-
21
-
22
- class LangInfoMixin(metaclass=serializers.SerializerMetaclass):
23
- lang_info = serializers.SerializerMethodField("get_lang_info", read_only=True)
24
-
25
- def get_lang_info(self, obj, *args, **kwargs):
26
- languages = []
27
- for key, language in settings.LANGUAGES:
28
- languages.append({"id": key, "name": language})
29
- return {
30
- "default": settings.LANGUAGE_CODE,
31
- "active": translation.get_language(),
32
- "translated_in": obj.translations.all_languages(),
33
- "site_languages": languages,
34
- }
35
-
36
- def get_default_field_names(self, *args):
37
- field_names = super().get_default_field_names(*args)
38
- self.action = getattr(
39
- self, "action", self.context and self.context.get("action", "list")
40
- )
41
- if self.action != "retrieve":
42
- return [f for f in field_names if f != "lang_info"]
43
- return field_names
44
-
45
-
46
- class TranslationSetMixin(TranslationsMixin):
47
- def get_default_field_names(self, *args):
48
- field_names = super(TranslationsMixin, self).get_default_field_names(*args)
49
- self.action = getattr(
50
- self, "action", self.context and self.context.get("action", "list")
51
- )
52
- if self.action != "list":
53
- field_names += [self.Meta.model._meta.translations_accessor]
54
- return field_names
55
-
56
-
57
- class SetupEagerLoadingMixin:
58
- @staticmethod
59
- def setup_eager_loading(queryset):
60
- return queryset
61
-
62
-
63
- class OrderingMixin:
64
- def get_max_order(self, order_field):
65
- return self.Meta.model.objects.aggregate(
66
- max_order=Coalesce(Max(order_field), 0)
67
- )["max_order"]
68
-
69
- def _get_ordering_field_name(self):
70
- try:
71
- field_name = self.Meta.model._meta.ordering[0]
72
- if field_name[0] == "-":
73
- field_name = field_name[1:]
74
- return field_name
75
- except (AttributeError, IndexError):
76
- return None
77
-
78
- def build_standard_field(self, field_name, model_field):
79
- field_class, field_kwargs = super().build_standard_field(
80
- field_name, model_field
81
- )
82
- if (
83
- isinstance(model_field, ORDERING_ACCEPTED_FIELDS)
84
- and field_name == self._get_ordering_field_name()
85
- ):
86
- field_kwargs["default"] = self.get_max_order(field_name) + 1
87
- return field_class, field_kwargs
88
-
89
-
90
- class JSONFieldPatchMixin:
91
- def is_json_field(self, attr, value, info):
92
- return (
93
- attr in info.fields
94
- and isinstance(info.fields[attr], DjangoJSONField)
95
- and isinstance(value, dict)
96
- )
97
-
98
- def update(self, instance, validated_data):
99
- if self.partial:
100
- if isinstance(self, TranslationsMixin):
101
- validated_data = self.merge_trans_jsonfields(instance, validated_data)
102
- info = model_meta.get_field_info(instance)
103
- for attr, value in validated_data.items():
104
- if self.is_json_field(attr, value, info):
105
- validated_data[attr] = dict_merge(
106
- getattr(instance, attr, {}), value
107
- )
108
- return super().update(instance, validated_data)
109
-
110
- def merge_trans_jsonfields(self, instance, validated_data):
111
- accessor = self.Meta.model._meta.translations_accessor
112
- if isinstance(validated_data.get(accessor), dict):
113
- translations = {
114
- trans.language_code: trans
115
- for trans in instance._meta.translations_model.objects.filter(
116
- master=instance,
117
- language_code__in=[
118
- lang
119
- for lang, val in validated_data[accessor].items()
120
- if isinstance(val, dict)
121
- ],
122
- )
123
- }
124
- for lang, trans in translations.items():
125
- info = model_meta.get_field_info(trans)
126
- for attr, value in validated_data[accessor][lang].items():
127
- if self.is_json_field(attr, value, info):
128
- validated_data[accessor][lang][attr] = dict_merge(
129
- getattr(trans, attr, {}), value
130
- )
131
- return validated_data
132
-
133
-
134
- DEFAULT_NESTING_DEPTH = getattr(settings, "CAMOMILLA_DRF_NESTING_DEPTH", 10)
135
-
136
-
137
- class NestMixin:
138
- def __init__(self, *args, **kwargs):
139
- self._depth = kwargs.pop("depth", None)
140
- return super().__init__(*args, **kwargs)
141
-
142
- def build_nested_field(self, field_name, relation_info, nested_depth):
143
- return self.build_relational_field(field_name, relation_info, nested_depth)
144
-
145
- def build_relational_field(
146
- self, field_name, relation_info, nested_depth=DEFAULT_NESTING_DEPTH + 1
147
- ):
148
- nested_depth = nested_depth if self._depth is None else self._depth
149
- field_class, field_kwargs = super().build_relational_field(
150
- field_name, relation_info
151
- )
152
- if (
153
- field_class is RelatedField and nested_depth > 1
154
- ): # stop recursion one step before the jump :P
155
- field_kwargs["serializer"] = self.build_standard_model_serializer(
156
- relation_info[1], nested_depth - 1
157
- )
158
- return field_class, field_kwargs
159
-
160
- def build_standard_model_serializer(self, model, depth):
161
- bases = (
162
- NestMixin,
163
- FieldsOverrideMixin,
164
- JSONFieldPatchMixin,
165
- OrderingMixin,
166
- SetupEagerLoadingMixin,
167
- serializers.ModelSerializer,
168
- )
169
- if issubclass(model, TranslatableModel):
170
- bases = (
171
- NestMixin,
172
- TranslatableFieldsOverrideMixin,
173
- JSONFieldPatchMixin,
174
- OrderingMixin,
175
- TranslatableModelSerializer,
176
- )
177
- return type(
178
- f"{model.__name__}StandardSerializer",
179
- bases,
180
- {
181
- "Meta": type(
182
- "Meta",
183
- (object,),
184
- {"model": model, "depth": depth, "fields": "__all__"},
185
- )
186
- },
187
- )
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
@@ -0,0 +1,57 @@
1
+ from rest_framework import serializers
2
+ from camomilla.serializers.fields.related import RelatedField
3
+ from collections import defaultdict
4
+
5
+
6
+ class FilterFieldsMixin(serializers.ModelSerializer):
7
+ """
8
+ Mixin to filter fields from a serializer, including handling nested fields.
9
+ """
10
+
11
+ def __init__(self, *args, **kwargs):
12
+ self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
13
+ return super().__init__(*args, **kwargs)
14
+
15
+ inherited_fields_filter = []
16
+
17
+ def get_default_field_names(self, *args):
18
+ field_names = super().get_default_field_names(*args)
19
+ request = self.context.get("request", None)
20
+
21
+ if request is not None and request.method == "GET":
22
+ fields = request.query_params.get("fields", "").split(",")
23
+ fields = [f for f in fields if f != ""]
24
+ if len(self.inherited_fields_filter) > 0:
25
+ fields = self.inherited_fields_filter
26
+
27
+ self.filtered_fields = set()
28
+ self.childs_fields = defaultdict(set)
29
+ for field in fields:
30
+ if "__" in field:
31
+ parent_field, child_field = field.split("__", 1)
32
+ if parent_field in field_names:
33
+ self.filtered_fields.add(parent_field)
34
+ self.childs_fields[parent_field].add(child_field)
35
+ else:
36
+ if field in field_names:
37
+ self.filtered_fields.add(field)
38
+
39
+ if len(self.filtered_fields) > 0:
40
+ return list(self.filtered_fields)
41
+ else:
42
+ return field_names
43
+
44
+ return field_names
45
+
46
+ def build_field(self, field_name, info, model_class, nested_depth):
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
+ )
55
+ if len(inherited_fields_filter) > 0 and issubclass(field_class, RelatedField):
56
+ field_kwargs["inherited_fields_filter"] = list(inherited_fields_filter)
57
+ return field_class, field_kwargs
@@ -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