django-camomilla-cms 6.0.0b15__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.
- camomilla/__init__.py +1 -1
- camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
- camomilla/dynamic_pages_urls.py +6 -2
- camomilla/managers/pages.py +93 -8
- camomilla/model_api.py +14 -7
- camomilla/models/media.py +1 -1
- camomilla/models/menu.py +10 -4
- camomilla/models/page.py +189 -127
- camomilla/openapi/schema.py +17 -8
- camomilla/redirects.py +10 -0
- camomilla/serializers/base/__init__.py +6 -4
- camomilla/serializers/fields/__init__.py +5 -17
- camomilla/serializers/fields/related.py +10 -4
- camomilla/serializers/mixins/__init__.py +23 -195
- 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/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
- camomilla/serializers/utils.py +5 -3
- camomilla/serializers/validators.py +6 -2
- camomilla/settings.py +10 -2
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +2 -2
- camomilla/storages/overwrite.py +2 -2
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/templatetags/menus.py +3 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/{admin.py → admin/__init__.py} +22 -20
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +1 -5
- camomilla/translation.py +7 -1
- camomilla/urls.py +2 -5
- camomilla/utils/query_parser.py +167 -0
- camomilla/utils/translation.py +47 -5
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/medias.py +1 -1
- camomilla/views/menus.py +0 -2
- camomilla/views/mixins/__init__.py +17 -69
- 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/pagination.py +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +12 -2
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
- django_camomilla_cms-6.0.0b17.dist-info/RECORD +132 -0
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +17 -0
- tests/test_api.py +2 -11
- tests/test_camomilla_filters.py +7 -13
- tests/test_media.py +113 -0
- tests/test_menu.py +97 -0
- tests/test_model_api.py +68 -0
- tests/test_model_api_permissions.py +39 -0
- tests/test_model_api_register.py +393 -0
- tests/test_pages.py +343 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +111 -0
- tests/test_utils.py +64 -64
- tests/utils/api.py +28 -0
- tests/utils/media.py +9 -0
- camomilla/serializers/fields/json.py +0 -49
- django_camomilla_cms-6.0.0b15.dist-info/RECORD +0 -105
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
- {camomilla/contrib/rest_framework → tests/utils}/__init__.py +0 -0
@@ -1,7 +1,5 @@
|
|
1
1
|
from functools import cached_property
|
2
|
-
from typing import Iterable,
|
3
|
-
from django.http import QueryDict
|
4
|
-
|
2
|
+
from typing import Iterable, List
|
5
3
|
from modeltranslation import settings as mt_settings
|
6
4
|
from modeltranslation.translator import NotRegistered, translator
|
7
5
|
from modeltranslation.utils import build_localized_fieldname
|
@@ -9,50 +7,8 @@ from rest_framework import serializers
|
|
9
7
|
from rest_framework.exceptions import ValidationError
|
10
8
|
from camomilla.utils.getters import pointed_getter
|
11
9
|
from camomilla.utils.translation import is_translatable
|
12
|
-
|
13
|
-
|
14
|
-
TRANS_ACCESSOR = "translations"
|
15
|
-
|
16
|
-
|
17
|
-
def plain_to_nest(data, fields, accessor=TRANS_ACCESSOR):
|
18
|
-
"""
|
19
|
-
This function transforms a plain dictionary with translations fields (es. {"title_en": "Hello"})
|
20
|
-
into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
|
21
|
-
"""
|
22
|
-
trans_data = {}
|
23
|
-
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
24
|
-
lang_data = {}
|
25
|
-
for field in fields:
|
26
|
-
trans_field_name = build_localized_fieldname(field, lang)
|
27
|
-
if trans_field_name in data:
|
28
|
-
lang_data[field] = data.pop(trans_field_name)
|
29
|
-
if lang_data.keys():
|
30
|
-
trans_data[lang] = lang_data
|
31
|
-
if trans_data.keys():
|
32
|
-
data[accessor] = trans_data
|
33
|
-
return data
|
34
|
-
|
35
|
-
|
36
|
-
def nest_to_plain(data: Union[dict, QueryDict], fields: List[str], accessor=TRANS_ACCESSOR):
|
37
|
-
"""
|
38
|
-
This function is the inverse of plain_to_nest.
|
39
|
-
It transforms a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}})
|
40
|
-
into a plain dictionary with translations fields (es. {"title_en": "Hello"}).
|
41
|
-
"""
|
42
|
-
if isinstance(data, QueryDict):
|
43
|
-
data = data.dict()
|
44
|
-
translations = data.pop(accessor, {})
|
45
|
-
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
46
|
-
nest_trans = translations.pop(lang, {})
|
47
|
-
for k in fields:
|
48
|
-
data.pop(k, None) # this removes all trans field without lang
|
49
|
-
if k in nest_trans:
|
50
|
-
# this saves on the default field the default language value
|
51
|
-
if lang == mt_settings.DEFAULT_LANGUAGE:
|
52
|
-
data[k] = nest_trans[k]
|
53
|
-
key = build_localized_fieldname(k, lang)
|
54
|
-
data[key] = data.get(key, nest_trans[k])
|
55
|
-
return data
|
10
|
+
from camomilla.utils.translation import nest_to_plain, plain_to_nest
|
11
|
+
from camomilla.settings import API_TRANSLATION_ACCESSOR
|
56
12
|
|
57
13
|
|
58
14
|
class TranslationsMixin(serializers.ModelSerializer):
|
@@ -67,6 +23,16 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
67
23
|
`{"translations": {"en": {"title": "Hello"}, "it": {"title": "Ciao"}}` -> `{"title_en": "Hello", "title_it": "Ciao"}`
|
68
24
|
"""
|
69
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
|
+
|
70
36
|
@cached_property
|
71
37
|
def translation_fields(self) -> List[str]:
|
72
38
|
try:
|
@@ -81,22 +47,16 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
81
47
|
yield field
|
82
48
|
|
83
49
|
def to_internal_value(self, data):
|
84
|
-
|
85
|
-
nest_to_plain(data, self.translation_fields)
|
86
|
-
return super().to_internal_value(data)
|
50
|
+
return super().to_internal_value(self._transform_input(data))
|
87
51
|
|
88
52
|
def to_representation(self, instance):
|
89
|
-
|
90
|
-
if self.translation_fields:
|
91
|
-
plain_to_nest(representation, self.translation_fields)
|
92
|
-
return representation
|
53
|
+
return self._transform_output(super().to_representation(instance))
|
93
54
|
|
94
55
|
def run_validation(self, *args, **kwargs):
|
95
56
|
try:
|
96
57
|
return super().run_validation(*args, **kwargs)
|
97
58
|
except ValidationError as ex:
|
98
|
-
|
99
|
-
plain_to_nest(ex.detail, self.translation_fields)
|
59
|
+
ex.detail.update(self._transform_input(ex.detail))
|
100
60
|
raise ValidationError(detail=ex.detail)
|
101
61
|
|
102
62
|
@property
|
camomilla/serializers/utils.py
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
def get_standard_bases() -> tuple:
|
2
2
|
from rest_framework.serializers import ModelSerializer
|
3
|
-
from camomilla.serializers.fields import FieldsOverrideMixin
|
4
|
-
from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
|
5
3
|
from camomilla.serializers.mixins import (
|
6
4
|
JSONFieldPatchMixin,
|
7
5
|
NestMixin,
|
8
6
|
OrderingMixin,
|
9
7
|
SetupEagerLoadingMixin,
|
8
|
+
FieldsOverrideMixin,
|
9
|
+
FilterFieldsMixin,
|
10
|
+
RemoveTranslationsMixin,
|
10
11
|
)
|
11
12
|
|
12
13
|
return (
|
14
|
+
SetupEagerLoadingMixin,
|
15
|
+
FilterFieldsMixin,
|
13
16
|
NestMixin,
|
14
17
|
FieldsOverrideMixin,
|
15
18
|
JSONFieldPatchMixin,
|
16
19
|
OrderingMixin,
|
17
20
|
RemoveTranslationsMixin,
|
18
|
-
SetupEagerLoadingMixin,
|
19
21
|
ModelSerializer,
|
20
22
|
)
|
21
23
|
|
@@ -31,9 +31,13 @@ class UniquePermalinkValidator:
|
|
31
31
|
for language in activate_languages():
|
32
32
|
autopermalink_f = build_localized_fieldname("autopermalink", language)
|
33
33
|
f_name = build_localized_fieldname("permalink", language)
|
34
|
-
permalink = value.get(
|
34
|
+
permalink = value.get(
|
35
|
+
f_name, instance and get_nofallbacks(instance, "permalink")
|
36
|
+
)
|
35
37
|
permalink = UrlNode.sanitize_permalink(permalink)
|
36
|
-
autopermalink = value.get(
|
38
|
+
autopermalink = value.get(
|
39
|
+
autopermalink_f, instance and get_nofallbacks(instance, "autopermalink")
|
40
|
+
)
|
37
41
|
if autopermalink:
|
38
42
|
continue
|
39
43
|
fake_instance = serializer.Meta.model()
|
camomilla/settings.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from django.conf import settings as django_settings
|
2
|
-
from modeltranslation.settings import ENABLE_REGISTRATIONS
|
2
|
+
from modeltranslation.settings import ENABLE_REGISTRATIONS, AVAILABLE_LANGUAGES
|
3
3
|
|
4
4
|
from camomilla.utils.getters import pointed_getter
|
5
5
|
|
@@ -53,6 +53,10 @@ ENABLE_TRANSLATIONS = (
|
|
53
53
|
ENABLE_REGISTRATIONS and "modeltranslation" in django_settings.INSTALLED_APPS
|
54
54
|
)
|
55
55
|
|
56
|
+
DEFAULT_LANGUAGE = pointed_getter(django_settings, "LANGUAGE_CODE", "en")
|
57
|
+
|
58
|
+
LANGUAGE_CODES = AVAILABLE_LANGUAGES
|
59
|
+
|
56
60
|
MEDIA_OPTIMIZE_MAX_WIDTH = pointed_getter(
|
57
61
|
django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.MAX_WIDTH", 1980
|
58
62
|
)
|
@@ -79,6 +83,10 @@ TEMPLATE_CONTEXT_FILES = pointed_getter(
|
|
79
83
|
django_settings, "CAMOMILLA.RENDER.TEMPLATE_CONTEXT_FILES", []
|
80
84
|
)
|
81
85
|
|
86
|
+
API_TRANSLATION_ACCESSOR = pointed_getter(
|
87
|
+
django_settings, "CAMOMILLA.API.TRANSLATION_ACCESSOR", "translations"
|
88
|
+
)
|
89
|
+
|
82
90
|
DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
|
83
91
|
|
84
92
|
# camomilla settings example
|
@@ -100,6 +108,6 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
|
|
100
108
|
# "STRUCTURED_FIELD": {
|
101
109
|
# "CACHE_ENABLED": True
|
102
110
|
# }
|
103
|
-
# "API": {"NESTING_DEPTH": 10 },
|
111
|
+
# "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations"},
|
104
112
|
# "DEBUG": False
|
105
113
|
# }
|
@@ -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)
|
camomilla/storages/optimize.py
CHANGED
@@ -2,13 +2,13 @@ import traceback
|
|
2
2
|
from io import BytesIO
|
3
3
|
|
4
4
|
from django.core.files.base import ContentFile
|
5
|
-
from django.core.files.storage import get_storage_class
|
6
5
|
from PIL import Image
|
7
6
|
|
8
7
|
from camomilla import settings
|
8
|
+
from camomilla.storages.default import get_default_storage_class
|
9
9
|
|
10
10
|
|
11
|
-
class OptimizedStorage(
|
11
|
+
class OptimizedStorage(get_default_storage_class()):
|
12
12
|
MEDIA_MAX_WIDTH = settings.MEDIA_OPTIMIZE_MAX_WIDTH
|
13
13
|
MEDIA_MAX_HEIGHT = settings.MEDIA_OPTIMIZE_MAX_HEIGHT
|
14
14
|
MEDIA_DPI = settings.MEDIA_OPTIMIZE_DPI
|
camomilla/storages/overwrite.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
from
|
1
|
+
from camomilla.storages.default import get_default_storage_class
|
2
2
|
|
3
3
|
|
4
|
-
class OverwriteStorage(
|
4
|
+
class OverwriteStorage(get_default_storage_class()):
|
5
5
|
def _save(self, name, content):
|
6
6
|
if self.exists(name):
|
7
7
|
self.delete(name)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
{% for item in menu.nodes %}
|
5
5
|
<li>
|
6
6
|
{% if item.link.url %}
|
7
|
-
<a href="{{ item
|
7
|
+
<a href="{{ item|node_url }}{% if is_preview %}?preview=true{% endif %}">{{ item.title }}</a>
|
8
8
|
{% else %}
|
9
9
|
<span>{{item.title}}</span>
|
10
10
|
{% endif %}
|
camomilla/templatetags/menus.py
CHANGED
@@ -22,6 +22,9 @@ def render_menu(
|
|
22
22
|
menu_key: str,
|
23
23
|
template_path: str = "defaults/parts/menu.html",
|
24
24
|
):
|
25
|
+
if context is not None and not isinstance(context, dict):
|
26
|
+
context = context.__dict__
|
27
|
+
|
25
28
|
return context.get("menus", Menu.defaultdict())[menu_key].render(
|
26
29
|
template_path=template_path,
|
27
30
|
context=context,
|
camomilla/theme/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "6.0.0-beta.
|
1
|
+
__version__ = "6.0.0-beta.17"
|
@@ -1,33 +1,30 @@
|
|
1
|
-
from
|
1
|
+
from tinymce.widgets import TinyMCE
|
2
2
|
from django import forms
|
3
3
|
from django.contrib import admin
|
4
4
|
from django.http import HttpResponse
|
5
|
-
|
6
|
-
from
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
class AbstractPageAdmin(TranslationAwareModelAdmin):
|
19
|
-
change_form_template = "admin/camomilla/page/change_form.html"
|
5
|
+
from .pages import AbstractPageModelForm, AbstractPageAdmin
|
6
|
+
from .translations import TranslationAwareModelAdmin
|
7
|
+
from camomilla.models import (
|
8
|
+
Article,
|
9
|
+
Content,
|
10
|
+
Media,
|
11
|
+
MediaFolder,
|
12
|
+
Page,
|
13
|
+
Tag,
|
14
|
+
Menu,
|
15
|
+
UrlRedirect,
|
16
|
+
)
|
20
17
|
|
21
18
|
|
22
19
|
class UserProfileAdmin(admin.ModelAdmin):
|
23
20
|
pass
|
24
21
|
|
25
22
|
|
26
|
-
class ArticleAdminForm(
|
23
|
+
class ArticleAdminForm(AbstractPageModelForm):
|
27
24
|
class Meta:
|
28
25
|
model = Article
|
29
26
|
fields = "__all__"
|
30
|
-
widgets = {"content":
|
27
|
+
widgets = {"content": TinyMCE()}
|
31
28
|
|
32
29
|
|
33
30
|
class ArticleAdmin(AbstractPageAdmin):
|
@@ -47,7 +44,7 @@ class ContentAdminForm(forms.ModelForm):
|
|
47
44
|
class Meta:
|
48
45
|
model = Content
|
49
46
|
fields = "__all__"
|
50
|
-
widgets = {"content":
|
47
|
+
widgets = {"content": TinyMCE()}
|
51
48
|
|
52
49
|
|
53
50
|
class ContentAdmin(TranslationAwareModelAdmin):
|
@@ -81,13 +78,17 @@ class MediaAdmin(TranslationAwareModelAdmin):
|
|
81
78
|
|
82
79
|
|
83
80
|
class PageAdmin(AbstractPageAdmin):
|
84
|
-
|
81
|
+
pass
|
85
82
|
|
86
83
|
|
87
84
|
class MenuAdmin(TranslationAwareModelAdmin):
|
88
85
|
pass
|
89
86
|
|
90
87
|
|
88
|
+
class UrlRedirectAdmin(admin.ModelAdmin):
|
89
|
+
pass
|
90
|
+
|
91
|
+
|
91
92
|
admin.site.register(Article, ArticleAdmin)
|
92
93
|
admin.site.register(MediaFolder, MediaFolderAdmin)
|
93
94
|
admin.site.register(Tag, TagAdmin)
|
@@ -95,3 +96,4 @@ admin.site.register(Content, ContentAdmin)
|
|
95
96
|
admin.site.register(Media, MediaAdmin)
|
96
97
|
admin.site.register(Page, PageAdmin)
|
97
98
|
admin.site.register(Menu, MenuAdmin)
|
99
|
+
admin.site.register(UrlRedirect, UrlRedirectAdmin)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from django import forms
|
2
|
+
from camomilla import settings
|
3
|
+
from .translations import TranslationAwareModelAdmin
|
4
|
+
from camomilla.models import UrlNode
|
5
|
+
|
6
|
+
|
7
|
+
class AbstractPageModelFormMeta(forms.models.ModelFormMetaclass):
|
8
|
+
def __new__(mcs, name, bases, attrs):
|
9
|
+
new_class = super().__new__(mcs, name, bases, attrs)
|
10
|
+
fields_to_add = forms.fields_for_model(UrlNode, UrlNode.LANG_PERMALINK_FIELDS)
|
11
|
+
if settings.ENABLE_TRANSLATIONS:
|
12
|
+
for i, field_name in enumerate(fields_to_add.keys()):
|
13
|
+
field_classes = ["mt", f"mt-field-{field_name.replace('_', '-')}"]
|
14
|
+
i == 0 and field_classes.append("mt-default")
|
15
|
+
fields_to_add[field_name].widget.attrs.update(
|
16
|
+
{"class": " ".join(field_classes)}
|
17
|
+
)
|
18
|
+
new_class.base_fields.update(fields_to_add)
|
19
|
+
return new_class
|
20
|
+
|
21
|
+
|
22
|
+
class AbstractPageModelForm(
|
23
|
+
forms.models.BaseModelForm, metaclass=AbstractPageModelFormMeta
|
24
|
+
):
|
25
|
+
|
26
|
+
def get_initial_for_field(self, field, field_name):
|
27
|
+
if field_name in UrlNode.LANG_PERMALINK_FIELDS:
|
28
|
+
return getattr(self.instance, field_name)
|
29
|
+
return super().get_initial_for_field(field, field_name)
|
30
|
+
|
31
|
+
def save(self, commit: bool = True):
|
32
|
+
model = super().save(commit=False)
|
33
|
+
for field_name in UrlNode.LANG_PERMALINK_FIELDS:
|
34
|
+
if field_name in self.cleaned_data:
|
35
|
+
if getattr(model, field_name) != self.cleaned_data[field_name]:
|
36
|
+
# sets autopermalink to False if permalink is manually set
|
37
|
+
setattr(model, f"auto{field_name}", False)
|
38
|
+
setattr(model, field_name, self.cleaned_data[field_name])
|
39
|
+
if commit:
|
40
|
+
model.save()
|
41
|
+
return model
|
42
|
+
|
43
|
+
|
44
|
+
class AbstractPageAdmin(TranslationAwareModelAdmin):
|
45
|
+
form = AbstractPageModelForm
|
46
|
+
change_form_template = "admin/camomilla/page/change_form.html"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from camomilla import settings
|
2
|
+
|
3
|
+
if settings.ENABLE_TRANSLATIONS:
|
4
|
+
from modeltranslation.admin import (
|
5
|
+
TabbedTranslationAdmin as TranslationAwareModelAdmin,
|
6
|
+
)
|
7
|
+
else:
|
8
|
+
from django.contrib.admin import ModelAdmin as TranslationAwareModelAdmin
|
9
|
+
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"TranslationAwareModelAdmin",
|
13
|
+
]
|
camomilla/theme/apps.py
CHANGED
@@ -29,12 +29,8 @@ class CamomillaThemeConfig(AppConfig):
|
|
29
29
|
name = "camomilla.theme"
|
30
30
|
|
31
31
|
def ready(self):
|
32
|
-
set_default_settings(
|
33
|
-
CKEDITOR_UPLOAD_PATH="editor-uploads/", X_FRAME_OPTIONS="SAMEORIGIN"
|
34
|
-
)
|
35
32
|
add_apps(
|
36
|
-
"
|
37
|
-
"ckeditor",
|
33
|
+
"tinymce",
|
38
34
|
"django_jsonform",
|
39
35
|
"admin_interface",
|
40
36
|
"colorfield",
|
camomilla/translation.py
CHANGED
@@ -16,7 +16,13 @@ class SeoMixinTranslationOptions(TranslationOptions):
|
|
16
16
|
|
17
17
|
|
18
18
|
class AbstractPageTranslationOptions(SeoMixinTranslationOptions):
|
19
|
-
fields = (
|
19
|
+
fields = (
|
20
|
+
"breadcrumbs_title",
|
21
|
+
"autopermalink",
|
22
|
+
"status",
|
23
|
+
"indexable",
|
24
|
+
"template_data",
|
25
|
+
)
|
20
26
|
|
21
27
|
|
22
28
|
@register(Article)
|
camomilla/urls.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
from django.shortcuts import redirect
|
2
1
|
from django.urls import include, path
|
3
2
|
from rest_framework import routers
|
4
3
|
from importlib.util import find_spec
|
@@ -21,6 +20,7 @@ from camomilla.views import (
|
|
21
20
|
MenuViewSet,
|
22
21
|
)
|
23
22
|
from camomilla.views.pages import fetch_page
|
23
|
+
from camomilla.redirects import url_patterns as old_redirects
|
24
24
|
|
25
25
|
router = routers.DefaultRouter()
|
26
26
|
|
@@ -30,18 +30,15 @@ router.register(r"contents", ContentViewSet, "camomilla-content")
|
|
30
30
|
router.register(r"media", MediaViewSet, "camomilla-media")
|
31
31
|
router.register(r"media-folders", MediaFolderViewSet, "camomilla-media_folders")
|
32
32
|
router.register(r"pages", PageViewSet, "camomilla-pages")
|
33
|
-
router.register(r"sitemap", PageViewSet, "camomilla-sitemap")
|
34
33
|
router.register(r"users", UserViewSet, "camomilla-users")
|
35
34
|
router.register(r"permissions", PermissionViewSet, "camomilla-permissions")
|
36
35
|
router.register(r"menus", MenuViewSet, "camomilla-menus")
|
37
36
|
|
38
37
|
urlpatterns = [
|
38
|
+
*old_redirects,
|
39
39
|
path("", include(router.urls)),
|
40
40
|
path("pages-router/", fetch_page),
|
41
41
|
path("pages-router/<path:permalink>", fetch_page),
|
42
|
-
path(
|
43
|
-
"profiles/me/", lambda _: redirect("../../users/current/"), name="profiles-me"
|
44
|
-
),
|
45
42
|
path("token-auth/", CamomillaObtainAuthToken.as_view(), name="api_token"),
|
46
43
|
path("auth/login/", CamomillaAuthLogin.as_view(), name="login"),
|
47
44
|
path("auth/logout/", CamomillaAuthLogout.as_view(), name="logout"),
|
@@ -0,0 +1,167 @@
|
|
1
|
+
import re
|
2
|
+
from django.db.models import Q
|
3
|
+
from typing import List, Dict, Optional
|
4
|
+
|
5
|
+
|
6
|
+
class ConditionParser:
|
7
|
+
CONDITION_PATTERN = re.compile(
|
8
|
+
r"(\w+__\w+='[^']+'|\w+__\w+=\S+|\w+='[^']+'|\w+=\S+)"
|
9
|
+
)
|
10
|
+
LOGICAL_OPERATORS = {"AND", "OR"}
|
11
|
+
|
12
|
+
__db_query: Optional[Q] = None
|
13
|
+
|
14
|
+
def __init__(self, query: str):
|
15
|
+
self.__db_query = None
|
16
|
+
self.query = query
|
17
|
+
|
18
|
+
def parse(self, query: str = None) -> Dict:
|
19
|
+
"""Parse the query or subquery. If no query is provided, use the instance's query."""
|
20
|
+
if query is None:
|
21
|
+
query = self.query
|
22
|
+
|
23
|
+
tokens = self.tokenize(query)
|
24
|
+
# If there's just one token and it's a dictionary (single condition), return it
|
25
|
+
if len(tokens) == 1 and isinstance(tokens[0], dict):
|
26
|
+
return tokens[0]
|
27
|
+
return self.build_tree(tokens)
|
28
|
+
|
29
|
+
def tokenize(self, query: str) -> List:
|
30
|
+
tokens = []
|
31
|
+
i = 0
|
32
|
+
while i < len(query):
|
33
|
+
if query[i] == "(":
|
34
|
+
j = i + 1
|
35
|
+
open_parens = 1
|
36
|
+
while j < len(query) and open_parens > 0:
|
37
|
+
if query[j] == "(":
|
38
|
+
open_parens += 1
|
39
|
+
elif query[j] == ")":
|
40
|
+
open_parens -= 1
|
41
|
+
j += 1
|
42
|
+
if open_parens == 0:
|
43
|
+
subquery = query[i + 1 : j - 1]
|
44
|
+
tokens.append(self.parse(subquery)) # Pass the subquery here
|
45
|
+
i = j
|
46
|
+
else:
|
47
|
+
raise ValueError("Mismatched parentheses")
|
48
|
+
elif query[i : i + 3] == "AND" or query[i : i + 2] == "OR":
|
49
|
+
operator = "AND" if query[i : i + 3] == "AND" else "OR"
|
50
|
+
tokens.append(operator)
|
51
|
+
i += 3 if operator == "AND" else 2
|
52
|
+
else:
|
53
|
+
match = self.CONDITION_PATTERN.match(query[i:])
|
54
|
+
if match:
|
55
|
+
condition = self.parse_condition(match.group())
|
56
|
+
tokens.append(condition)
|
57
|
+
i += match.end()
|
58
|
+
else:
|
59
|
+
i += 1
|
60
|
+
return tokens
|
61
|
+
|
62
|
+
def parse_condition(self, condition: str) -> Optional[Dict]:
|
63
|
+
"""Parse a single condition into field lookup and value."""
|
64
|
+
if "=" in condition:
|
65
|
+
field_lookup, value = condition.split("=")
|
66
|
+
value = value.strip("'").strip('"') # Remove single or double quotes
|
67
|
+
value = self.parse_value(value) # Parse the value
|
68
|
+
return {"field_lookup": field_lookup, "value": value}
|
69
|
+
return None
|
70
|
+
|
71
|
+
def parse_value(self, string: str):
|
72
|
+
"""Parse single condition values based on specific rules."""
|
73
|
+
if string and string.startswith("[") and string.endswith("]"):
|
74
|
+
string = [self.parse_value(substr) for substr in string[1:-1].split(",")]
|
75
|
+
elif string and string.lower() in ["true", "false"]:
|
76
|
+
string = string.lower() == "true"
|
77
|
+
elif string and string.isdigit():
|
78
|
+
string = int(string)
|
79
|
+
return string
|
80
|
+
|
81
|
+
def build_tree(self, tokens: List[str]) -> Dict:
|
82
|
+
"""Build a tree-like structure with operators and conditions."""
|
83
|
+
if not tokens:
|
84
|
+
return None
|
85
|
+
|
86
|
+
output_stack = []
|
87
|
+
operator_stack = []
|
88
|
+
|
89
|
+
# Process each token in the query
|
90
|
+
for token in tokens:
|
91
|
+
if isinstance(token, dict):
|
92
|
+
# Handle a single condition
|
93
|
+
if operator_stack:
|
94
|
+
operator = operator_stack.pop()
|
95
|
+
if isinstance(output_stack[-1], dict):
|
96
|
+
output_stack[-1] = {
|
97
|
+
"operator": operator,
|
98
|
+
"conditions": [output_stack[-1], token],
|
99
|
+
}
|
100
|
+
else:
|
101
|
+
output_stack[-1]["conditions"].append(token)
|
102
|
+
else:
|
103
|
+
output_stack.append(token)
|
104
|
+
|
105
|
+
elif token in self.LOGICAL_OPERATORS:
|
106
|
+
# Operator found (AND/OR), handle precedence
|
107
|
+
operator_stack.append(token)
|
108
|
+
|
109
|
+
# If only one item in output_stack, return it directly
|
110
|
+
if len(output_stack) == 1:
|
111
|
+
return output_stack[0]
|
112
|
+
return {
|
113
|
+
"operator": "AND",
|
114
|
+
"conditions": output_stack,
|
115
|
+
} # Default to AND if no operators
|
116
|
+
|
117
|
+
def to_q(self, parsed_tree: Dict) -> Q:
|
118
|
+
"""Convert parsed tree structure into Q objects."""
|
119
|
+
if isinstance(parsed_tree, list):
|
120
|
+
# If parsed_tree is a list, combine all conditions with AND by default
|
121
|
+
q_objects = [self.to_q(cond) for cond in parsed_tree]
|
122
|
+
combined_q = Q()
|
123
|
+
for q_obj in q_objects:
|
124
|
+
combined_q &= q_obj
|
125
|
+
return combined_q
|
126
|
+
|
127
|
+
if isinstance(parsed_tree, dict):
|
128
|
+
if "field_lookup" in parsed_tree:
|
129
|
+
# Base case: a single condition
|
130
|
+
return Q(**{parsed_tree["field_lookup"]: parsed_tree["value"]})
|
131
|
+
|
132
|
+
elif "operator" in parsed_tree and "conditions" in parsed_tree:
|
133
|
+
operator = parsed_tree["operator"]
|
134
|
+
conditions = parsed_tree["conditions"]
|
135
|
+
|
136
|
+
q_objects = [self.to_q(cond) for cond in conditions]
|
137
|
+
|
138
|
+
if operator == "AND":
|
139
|
+
combined_q = Q()
|
140
|
+
for q_obj in q_objects:
|
141
|
+
combined_q &= q_obj
|
142
|
+
return combined_q
|
143
|
+
elif operator == "OR":
|
144
|
+
combined_q = Q()
|
145
|
+
for q_obj in q_objects:
|
146
|
+
combined_q |= q_obj
|
147
|
+
return combined_q
|
148
|
+
else:
|
149
|
+
raise ValueError(f"Unknown operator: {operator}")
|
150
|
+
|
151
|
+
raise ValueError("Parsed tree structure is invalid")
|
152
|
+
|
153
|
+
def parse_to_q(self) -> Q:
|
154
|
+
"""Parse the query and convert to Q object."""
|
155
|
+
parsed_tree = self.parse()
|
156
|
+
if not parsed_tree:
|
157
|
+
return Q() # Return an empty Q if parsing fails
|
158
|
+
return self.to_q(parsed_tree)
|
159
|
+
|
160
|
+
@property
|
161
|
+
def db_query(self) -> Q:
|
162
|
+
if self.__db_query is None:
|
163
|
+
self.__db_query = self.parse_to_q()
|
164
|
+
return self.__db_query
|
165
|
+
|
166
|
+
def __str__(self) -> str:
|
167
|
+
return f"ConditionParser({self.db_query})"
|