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.
- camomilla/__init__.py +8 -2
- camomilla/apps.py +9 -1
- camomilla/context_processors.py +6 -0
- camomilla/contrib/modeltranslation/__init__.py +0 -0
- camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
- camomilla/dynamic_pages_urls.py +33 -0
- camomilla/fields/__init__.py +13 -0
- camomilla/{fields.py → fields/json.py} +15 -18
- camomilla/management/commands/regenerate_thumbnails.py +0 -1
- camomilla/managers/__init__.py +3 -0
- camomilla/managers/pages.py +116 -0
- camomilla/model_api.py +86 -0
- camomilla/models/__init__.py +5 -6
- camomilla/models/article.py +26 -44
- camomilla/models/content.py +8 -15
- camomilla/models/media.py +70 -97
- camomilla/models/menu.py +106 -0
- camomilla/models/mixins/__init__.py +10 -48
- camomilla/models/page.py +521 -20
- camomilla/openapi/__init__.py +0 -0
- camomilla/openapi/schema.py +67 -0
- camomilla/parsers.py +0 -1
- camomilla/redirects.py +10 -0
- camomilla/serializers/__init__.py +2 -0
- camomilla/serializers/article.py +5 -10
- camomilla/serializers/base/__init__.py +21 -17
- camomilla/serializers/content_type.py +17 -0
- camomilla/serializers/fields/__init__.py +6 -20
- camomilla/serializers/fields/file.py +5 -0
- camomilla/serializers/fields/related.py +24 -4
- camomilla/serializers/media.py +6 -8
- camomilla/serializers/menu.py +17 -0
- camomilla/serializers/mixins/__init__.py +23 -187
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +57 -0
- camomilla/serializers/mixins/json.py +34 -0
- camomilla/serializers/mixins/language.py +32 -0
- camomilla/serializers/mixins/nesting.py +35 -0
- camomilla/serializers/mixins/optimize.py +91 -0
- camomilla/serializers/mixins/ordering.py +34 -0
- camomilla/serializers/mixins/page.py +58 -0
- camomilla/serializers/mixins/translation.py +103 -0
- camomilla/serializers/page.py +53 -4
- camomilla/serializers/user.py +5 -4
- camomilla/serializers/utils.py +38 -0
- camomilla/serializers/validators.py +51 -0
- camomilla/settings.py +118 -0
- camomilla/sitemap.py +30 -0
- camomilla/storages/__init__.py +4 -0
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +71 -0
- camomilla/{storages.py → storages/overwrite.py} +2 -2
- camomilla/templates/admin/camomilla/page/change_form.html +10 -0
- camomilla/templates/defaults/articles/default.html +7 -0
- camomilla/templates/defaults/base.html +170 -0
- camomilla/templates/defaults/pages/default.html +3 -0
- camomilla/templates/defaults/parts/langswitch.html +83 -0
- camomilla/templates/defaults/parts/menu.html +15 -0
- camomilla/templates_context/__init__.py +0 -0
- camomilla/templates_context/autodiscover.py +51 -0
- camomilla/templates_context/rendering.py +89 -0
- camomilla/templatetags/camomilla_filters.py +6 -5
- camomilla/templatetags/menus.py +37 -0
- camomilla/templatetags/model_extras.py +77 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin/__init__.py +99 -0
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +38 -0
- camomilla/theme/static/admin/css/responsive.css +5 -1021
- camomilla/theme/static/admin/img/favicon.ico +0 -0
- camomilla/theme/static/admin/img/logo.svg +31 -0
- camomilla/theme/templates/admin/base.html +7 -0
- camomilla/theme/templates/rosetta/base.html +196 -0
- camomilla/translation.py +61 -0
- camomilla/urls.py +38 -17
- camomilla/utils/__init__.py +4 -0
- camomilla/utils/getters.py +27 -0
- camomilla/utils/normalization.py +7 -0
- camomilla/utils/query_parser.py +167 -0
- camomilla/{utils.py → utils/seo.py} +13 -15
- camomilla/utils/setters.py +37 -0
- camomilla/utils/templates.py +32 -0
- camomilla/utils/translation.py +114 -0
- camomilla/views/__init__.py +1 -1
- camomilla/views/articles.py +5 -7
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/contents.py +6 -11
- camomilla/views/decorators.py +26 -0
- camomilla/views/medias.py +24 -19
- camomilla/views/menus.py +81 -0
- camomilla/views/mixins/__init__.py +17 -73
- camomilla/views/mixins/bulk_actions.py +22 -0
- camomilla/views/mixins/language.py +33 -0
- camomilla/views/mixins/optimize.py +18 -0
- camomilla/views/mixins/ordering.py +2 -2
- camomilla/views/mixins/pagination.py +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +28 -6
- camomilla/views/tags.py +5 -6
- camomilla/views/users.py +7 -12
- django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
- django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +14 -0
- tests/test_api.py +22 -39
- tests/test_camomilla_filters.py +11 -13
- tests/test_media.py +152 -0
- tests/test_menu.py +112 -0
- tests/test_model_api.py +113 -0
- tests/test_model_api_permissions.py +44 -0
- tests/test_model_api_register.py +355 -0
- tests/test_pages.py +351 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +149 -0
- tests/test_utils.py +64 -64
- tests/utils/__init__.py +0 -0
- tests/utils/api.py +28 -0
- tests/utils/media.py +10 -0
- camomilla/admin.py +0 -98
- camomilla/migrations/0001_initial.py +0 -577
- camomilla/migrations/0002_auto_20200214_1127.py +0 -33
- camomilla/migrations/0003_auto_20210130_1610.py +0 -30
- camomilla/migrations/0004_auto_20210511_0937.py +0 -25
- camomilla/migrations/0005_media_image_props.py +0 -19
- camomilla/migrations/0006_auto_20220103_1845.py +0 -35
- camomilla/migrations/0007_auto_20220211_1622.py +0 -18
- camomilla/migrations/0008_auto_20220309_1616.py +0 -60
- camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
- camomilla/migrations/0010_auto_20220802_1406.py +0 -83
- camomilla/migrations/0011_auto_20220902_1000.py +0 -15
- camomilla/models/category.py +0 -25
- camomilla/models/tag.py +0 -19
- camomilla/theme/static/admin/img/logo.png +0 -0
- camomilla/theme/templates/admin/base_site.html +0 -18
- camomilla/views/categories.py +0 -13
- django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
- django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
- tests/urls.py +0 -21
- /camomilla/{migrations → contrib}/__init__.py +0 -0
- /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-5.8.5.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
|
camomilla/serializers/page.py
CHANGED
@@ -1,14 +1,63 @@
|
|
1
|
-
from
|
2
|
-
from .
|
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
|
-
|
12
|
+
|
13
|
+
class ContentSerializer(BaseModelSerializer):
|
6
14
|
class Meta:
|
7
15
|
model = Content
|
8
16
|
fields = "__all__"
|
9
17
|
|
10
18
|
|
11
|
-
class PageSerializer(
|
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__"
|
camomilla/serializers/user.py
CHANGED
@@ -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
|
-
|
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,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)
|