django-camomilla-cms 5.8.6__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.6.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.6.dist-info/METADATA +0 -63
  138. django_camomilla_cms-5.8.6.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.6.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
  143. {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
@@ -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()]
@@ -0,0 +1,103 @@
1
+ from functools import cached_property
2
+ from typing import Iterable, List
3
+ from modeltranslation import settings as mt_settings
4
+ from modeltranslation.translator import NotRegistered, translator
5
+ from modeltranslation.utils import build_localized_fieldname
6
+ from rest_framework import serializers
7
+ from rest_framework.exceptions import ValidationError
8
+ from camomilla.utils.getters import pointed_getter
9
+ from camomilla.utils.translation import is_translatable
10
+ from camomilla.utils.translation import nest_to_plain, plain_to_nest
11
+ from camomilla.settings import API_TRANSLATION_ACCESSOR
12
+
13
+
14
+ class TranslationsMixin(serializers.ModelSerializer):
15
+ """
16
+ This mixin adds support for modeltranslation fields.
17
+ It automatically nests all translations fields (es. title_en) under a "translations" field.
18
+
19
+ This means that, in representation, the serializer will transform:
20
+ `{"title_en": "Hello", "title_it": "Ciao"}` -> `{"translations": {"en": {"title": "Hello"}, "it": {"title": "Ciao"}}`
21
+
22
+ While in deserialization, the serializer will transform:
23
+ `{"translations": {"en": {"title": "Hello"}, "it": {"title": "Ciao"}}` -> `{"title_en": "Hello", "title_it": "Ciao"}`
24
+ """
25
+
26
+ def _transform_input(self, data):
27
+ return nest_to_plain(
28
+ data, self.translation_fields or [], API_TRANSLATION_ACCESSOR
29
+ )
30
+
31
+ def _transform_output(self, data):
32
+ return plain_to_nest(
33
+ data, self.translation_fields or [], API_TRANSLATION_ACCESSOR
34
+ )
35
+
36
+ @cached_property
37
+ def translation_fields(self) -> List[str]:
38
+ try:
39
+ return translator.get_options_for_model(self.Meta.model).get_field_names()
40
+ except NotRegistered:
41
+ return []
42
+
43
+ @property
44
+ def _writable_fields(self) -> Iterable[serializers.Field]:
45
+ for field in super()._writable_fields:
46
+ if field.field_name not in self.translation_fields:
47
+ yield field
48
+
49
+ def to_internal_value(self, data):
50
+ return super().to_internal_value(self._transform_input(data))
51
+
52
+ def to_representation(self, instance):
53
+ return self._transform_output(super().to_representation(instance))
54
+
55
+ def run_validation(self, *args, **kwargs):
56
+ try:
57
+ return super().run_validation(*args, **kwargs)
58
+ except ValidationError as ex:
59
+ ex.detail.update(self._transform_input(ex.detail))
60
+ raise ValidationError(detail=ex.detail)
61
+
62
+ @property
63
+ def is_translatable(self):
64
+ return is_translatable(pointed_getter(self, "Meta.model"))
65
+
66
+
67
+ class RemoveTranslationsMixin(serializers.ModelSerializer):
68
+ """
69
+ This mixin removes all translations fields (es. title_en) from the serializer.
70
+ It's useful when you want to create a serializer that doesn't need to include all translations fields.
71
+
72
+ If request is passed in context, this serializer becomes aware of the query parameter "included_translations".
73
+ If the value is "all", all translations fields are included.
74
+ If the value is a comma separated list of languages (es. "en,it"), only the specified translations fields are included.
75
+ """
76
+
77
+ @cached_property
78
+ def translation_fields(self):
79
+ try:
80
+ return translator.get_options_for_model(self.Meta.model).get_field_names()
81
+ except NotRegistered:
82
+ return []
83
+
84
+ def get_default_field_names(self, declared_fields, model_info):
85
+ request = self.context.get("request", False)
86
+ included_translations = request and request.GET.get(
87
+ "included_translations", False
88
+ )
89
+ if included_translations == "all":
90
+ return super().get_default_field_names(declared_fields, model_info)
91
+ elif included_translations is not False:
92
+ included_translations = included_translations.split(",")
93
+ else:
94
+ included_translations = []
95
+
96
+ field_names = super().get_default_field_names(declared_fields, model_info)
97
+ for lang in mt_settings.AVAILABLE_LANGUAGES:
98
+ if lang not in included_translations:
99
+ for field in self.translation_fields:
100
+ localized_fieldname = build_localized_fieldname(field, lang)
101
+ if localized_fieldname in field_names:
102
+ field_names.remove(localized_fieldname)
103
+ return field_names
@@ -1,14 +1,63 @@
1
- from ..models import Content, Page
2
- from .base import BaseTranslatableModelSerializer
1
+ from camomilla.models.page import UrlNode
2
+ from camomilla.serializers.mixins import AbstractPageMixin
3
+ from camomilla.models import Content, Page
4
+ from camomilla.serializers.base import BaseModelSerializer
5
+ from rest_framework import serializers
3
6
 
7
+ from camomilla.serializers.utils import (
8
+ build_standard_model_serializer,
9
+ get_standard_bases,
10
+ )
4
11
 
5
- class ContentSerializer(BaseTranslatableModelSerializer):
12
+
13
+ class ContentSerializer(BaseModelSerializer):
6
14
  class Meta:
7
15
  model = Content
8
16
  fields = "__all__"
9
17
 
10
18
 
11
- class PageSerializer(BaseTranslatableModelSerializer):
19
+ class PageSerializer(AbstractPageMixin, BaseModelSerializer):
12
20
  class Meta:
13
21
  model = Page
14
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__"
@@ -1,9 +1,10 @@
1
1
  from django.forms import ValidationError
2
- from .base import BaseModelSerializer
2
+ from camomilla.serializers.base import BaseModelSerializer
3
3
  from django.contrib.auth import get_user_model, password_validation
4
4
  from django.contrib.auth.models import Permission
5
5
  from rest_framework import serializers
6
6
  from django.utils.translation import gettext_lazy as _
7
+ from django.db.models import Q
7
8
 
8
9
 
9
10
  class PermissionSerializer(BaseModelSerializer):
@@ -39,8 +40,9 @@ class UserProfileSerializer(BaseModelSerializer):
39
40
  def get_all_permissions(self, instance):
40
41
  return PermissionSerializer(
41
42
  Permission.objects.filter(
42
- group__pk__in=instance.groups.values_list("pk", flat=True)
43
- ).union(instance.user_permissions.all()),
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
+ ),
44
46
  context=self.context,
45
47
  many=True,
46
48
  ).data
@@ -66,7 +68,6 @@ class UserProfileSerializer(BaseModelSerializer):
66
68
 
67
69
 
68
70
  class UserSerializer(BaseModelSerializer):
69
-
70
71
  id = serializers.IntegerField(read_only=True)
71
72
  password = serializers.CharField(
72
73
  write_only=True, required=False, allow_null=True, allow_blank=True
@@ -0,0 +1,38 @@
1
+ def get_standard_bases() -> tuple:
2
+ from rest_framework.serializers import ModelSerializer
3
+ from camomilla.serializers.mixins import (
4
+ JSONFieldPatchMixin,
5
+ NestMixin,
6
+ OrderingMixin,
7
+ SetupEagerLoadingMixin,
8
+ FieldsOverrideMixin,
9
+ FilterFieldsMixin,
10
+ RemoveTranslationsMixin,
11
+ )
12
+
13
+ return (
14
+ SetupEagerLoadingMixin,
15
+ FilterFieldsMixin,
16
+ NestMixin,
17
+ FieldsOverrideMixin,
18
+ JSONFieldPatchMixin,
19
+ OrderingMixin,
20
+ RemoveTranslationsMixin,
21
+ ModelSerializer,
22
+ )
23
+
24
+
25
+ def build_standard_model_serializer(model, depth, bases=None):
26
+ if bases is None:
27
+ bases = get_standard_bases()
28
+ return type(
29
+ f"{model.__name__}StandardSerializer",
30
+ bases,
31
+ {
32
+ "Meta": type(
33
+ "Meta",
34
+ (object,),
35
+ {"model": model, "depth": depth, "fields": "__all__"},
36
+ )
37
+ },
38
+ )
@@ -0,0 +1,51 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from modeltranslation.utils import build_localized_fieldname
3
+ from rest_framework.exceptions import ValidationError
4
+
5
+ from camomilla.models.page import UrlNode
6
+ from camomilla.utils import activate_languages, is_page
7
+ from camomilla.utils.translation import get_nofallbacks, set_nofallbacks
8
+
9
+
10
+ class UniquePermalinkValidator:
11
+ message = _("There is an other page with same permalink.")
12
+
13
+ requires_context = True
14
+
15
+ def __call__(self, value, serializer):
16
+ if not is_page(serializer.Meta.model):
17
+ return
18
+ errors = {}
19
+ instance = serializer.instance
20
+ exclude_kwargs = {}
21
+ if instance and instance.url_node:
22
+ exclude_kwargs["pk"] = instance.url_node.pk
23
+ parent_page_field = getattr(
24
+ getattr(serializer.Meta.model, "PageMeta", object),
25
+ "parent_page_field",
26
+ "parent_page",
27
+ )
28
+ parent_page = value.get(parent_page_field, None) or getattr(
29
+ instance, parent_page_field, None
30
+ )
31
+ for language in activate_languages():
32
+ autopermalink_f = build_localized_fieldname("autopermalink", language)
33
+ f_name = build_localized_fieldname("permalink", language)
34
+ permalink = value.get(
35
+ f_name, instance and get_nofallbacks(instance, "permalink")
36
+ )
37
+ permalink = UrlNode.sanitize_permalink(permalink)
38
+ autopermalink = value.get(
39
+ autopermalink_f, instance and get_nofallbacks(instance, "autopermalink")
40
+ )
41
+ if autopermalink:
42
+ continue
43
+ fake_instance = serializer.Meta.model()
44
+ set_nofallbacks(fake_instance, "permalink", permalink)
45
+ if parent_page:
46
+ set_nofallbacks(fake_instance, parent_page_field, parent_page)
47
+ qs = UrlNode.objects.exclude(**exclude_kwargs)
48
+ if qs.filter(permalink=permalink).exists():
49
+ errors[f_name] = self.message
50
+ if len(errors.keys()):
51
+ raise ValidationError(errors)
camomilla/settings.py ADDED
@@ -0,0 +1,118 @@
1
+ from django.conf import settings as django_settings
2
+ from modeltranslation.settings import ENABLE_REGISTRATIONS, AVAILABLE_LANGUAGES
3
+
4
+ from camomilla.utils.getters import pointed_getter
5
+
6
+ PROJECT_TITLE = pointed_getter(
7
+ django_settings,
8
+ "CAMOMILLA.PROJECT_TITLE",
9
+ pointed_getter(django_settings, "CAMOMILLA_PROJECT_TITLE", "Camomilla"),
10
+ )
11
+
12
+ THUMBNAIL_FOLDER = pointed_getter(
13
+ django_settings,
14
+ "CAMOMILLA.MEDIA.THUMBNAIL.FOLDER",
15
+ pointed_getter(django_settings, "CAMOMILLA_THUMBTHUMBNAIL_FOLDER", "thumbnails"),
16
+ )
17
+ THUMBNAIL_WIDTH = pointed_getter(
18
+ django_settings,
19
+ "CAMOMILLA.MEDIA.THUMBNAIL.WIDTH",
20
+ pointed_getter(django_settings, "CAMOMILLA_THUMBNAIL_WIDTH", 50),
21
+ )
22
+ THUMBNAIL_HEIGHT = pointed_getter(
23
+ django_settings,
24
+ "CAMOMILLA.MEDIA.THUMBNAIL.HEIGHT",
25
+ pointed_getter(django_settings, "CAMOMILLA_THUMBNAIL_HEIGHT", 50),
26
+ )
27
+ BASE_URL = pointed_getter(
28
+ django_settings,
29
+ "CAMOMILLA.ROUTER.BASE_URL",
30
+ pointed_getter(django_settings, "FORCE_SCRIPT_NAME", None),
31
+ )
32
+ BASE_URL = BASE_URL and "/" + BASE_URL.strip("/")
33
+
34
+
35
+ ARTICLE_DEFAULT_TEMPLATE = pointed_getter(
36
+ django_settings,
37
+ "CAMOMILLA.RENDER.ARTICLE.DEFAULT_TEMPLATE",
38
+ "defaults/articles/default.html",
39
+ )
40
+ PAGE_DEFAULT_TEMPLATE = pointed_getter(
41
+ django_settings,
42
+ "CAMOMILLA.RENDER.PAGE.DEFAULT_TEMPLATE",
43
+ "defaults/pages/default.html",
44
+ )
45
+ ARTICLE_INJECT_CONTEXT_FUNC = pointed_getter(
46
+ django_settings, "CAMOMILLA.RENDER.ARTICLE.INJECT_CONTEXT", None
47
+ )
48
+ PAGE_INJECT_CONTEXT_FUNC = pointed_getter(
49
+ django_settings, "CAMOMILLA.RENDER.PAGE.INJECT_CONTEXT", None
50
+ )
51
+
52
+ ENABLE_TRANSLATIONS = (
53
+ ENABLE_REGISTRATIONS and "modeltranslation" in django_settings.INSTALLED_APPS
54
+ )
55
+
56
+ DEFAULT_LANGUAGE = pointed_getter(django_settings, "LANGUAGE_CODE", "en")
57
+
58
+ LANGUAGE_CODES = AVAILABLE_LANGUAGES
59
+
60
+ MEDIA_OPTIMIZE_MAX_WIDTH = pointed_getter(
61
+ django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.MAX_WIDTH", 1980
62
+ )
63
+ MEDIA_OPTIMIZE_MAX_HEIGHT = pointed_getter(
64
+ django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.MAX_HEIGHT", 1400
65
+ )
66
+ MEDIA_OPTIMIZE_DPI = pointed_getter(django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.DPI", 30)
67
+
68
+ MEDIA_OPTIMIZE_JPEG_QUALITY = pointed_getter(
69
+ django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.JPEG_QUALITY", 85
70
+ )
71
+
72
+ ENABLE_MEDIA_OPTIMIZATION = pointed_getter(
73
+ django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.ENABLE", True
74
+ )
75
+
76
+ API_NESTING_DEPTH = pointed_getter(django_settings, "CAMOMILLA.API.NESTING_DEPTH", 10)
77
+
78
+ AUTO_CREATE_HOMEPAGE = pointed_getter(
79
+ django_settings, "CAMOMILLA.RENDER.AUTO_CREATE_HOMEPAGE", True
80
+ )
81
+
82
+ TEMPLATE_CONTEXT_FILES = pointed_getter(
83
+ django_settings, "CAMOMILLA.RENDER.TEMPLATE_CONTEXT_FILES", []
84
+ )
85
+
86
+ API_TRANSLATION_ACCESSOR = pointed_getter(
87
+ django_settings, "CAMOMILLA.API.TRANSLATION_ACCESSOR", "translations"
88
+ )
89
+
90
+ REGISTERED_TEMPLATES_APPS = pointed_getter(
91
+ django_settings, "CAMOMILLA.RENDER.REGISTERED_TEMPLATES_APPS", None
92
+ )
93
+
94
+ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
95
+
96
+ # camomilla settings example
97
+ # CAMOMILLA = {
98
+ # "PROJECT_TITLE": "",
99
+ # "ROUTER": {
100
+ # "BASE_URL": ""
101
+ # },
102
+ # "MEDIA": {
103
+ # "OPTIMIZE": {"MAX_WIDTH": 1980, "MAX_HEIGHT": 1400, "DPI": 30, "JPEG_QUALITY": 85, "ENABLE": True},
104
+ # "THUMBNAIL": {"FOLDER": "", "WIDTH": 50, "HEIGHT": 50}
105
+ # },
106
+ # "RENDER": {
107
+ # "TEMPLATE_CONTEXT_FILES": [],
108
+ # "AUTO_CREATE_HOMEPAGE": True,
109
+ # "ARTICLE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None },
110
+ # "PAGE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None }
111
+ # "REGISTERED_TEMPLATE_APPS": []
112
+ # },
113
+ # "STRUCTURED_FIELD": {
114
+ # "CACHE_ENABLED": True
115
+ # }
116
+ # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations"},
117
+ # "DEBUG": False
118
+ # }
camomilla/sitemap.py ADDED
@@ -0,0 +1,30 @@
1
+ from django.contrib.sitemaps import Sitemap
2
+ from camomilla.models import UrlNode
3
+
4
+
5
+ class CamomillaPagesSitemap(Sitemap):
6
+ changefreq_default = "daily"
7
+ priority_default = 0.5
8
+
9
+ def changefreq(self, obj):
10
+ if hasattr(obj.page.__class__, "changefreq"):
11
+ return obj.page.changefreq
12
+ return self.changefreq_default
13
+
14
+ def priority(self, obj):
15
+ if hasattr(obj.page.__class__, "priority"):
16
+ return obj.priority
17
+ return self.priority_default
18
+
19
+ def items(self):
20
+ return UrlNode.objects.filter(is_public=True)
21
+
22
+ def lastmod(self, obj):
23
+ if hasattr(obj.page.__class__, "lastmod"):
24
+ return obj.page.lastmod
25
+ return obj.date_updated_at
26
+
27
+
28
+ camomilla_sitemaps = {
29
+ "pages": CamomillaPagesSitemap,
30
+ }
@@ -0,0 +1,4 @@
1
+ from .optimize import OptimizedStorage
2
+ from .overwrite import OverwriteStorage
3
+
4
+ __all__ = ["OptimizedStorage", "OverwriteStorage"]
@@ -0,0 +1,12 @@
1
+ from django.utils.module_loading import import_string
2
+ from django.conf import settings
3
+
4
+ from django import VERSION as DJANGO_VERSION
5
+
6
+
7
+ def get_default_storage_class():
8
+ if DJANGO_VERSION >= (4, 2):
9
+ storage = settings.STORAGES["default"]["BACKEND"]
10
+ else:
11
+ storage = settings.DEFAULT_FILE_STORAGE
12
+ return import_string(storage)