django-camomilla-cms 6.0.0b16__py2.py3-none-any.whl → 6.0.0b18__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 +87 -2
- camomilla/model_api.py +6 -4
- camomilla/models/menu.py +9 -4
- camomilla/models/page.py +178 -117
- camomilla/openapi/schema.py +15 -10
- camomilla/redirects.py +10 -0
- camomilla/serializers/base/__init__.py +4 -4
- camomilla/serializers/fields/__init__.py +5 -17
- camomilla/serializers/fields/related.py +5 -3
- camomilla/serializers/mixins/__init__.py +23 -240
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +9 -8
- 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 +3 -3
- camomilla/serializers/validators.py +6 -2
- camomilla/settings.py +16 -2
- camomilla/storages/default.py +7 -1
- camomilla/templates/defaults/base.html +60 -4
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/templatetags/menus.py +3 -0
- camomilla/templatetags/model_extras.py +73 -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 +2 -5
- camomilla/translation.py +7 -1
- camomilla/urls.py +2 -5
- camomilla/utils/query_parser.py +42 -23
- camomilla/utils/templates.py +23 -10
- camomilla/utils/translation.py +47 -5
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/medias.py +1 -1
- camomilla/views/mixins/__init__.py +17 -76
- 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 +11 -8
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +12 -2
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/METADATA +23 -16
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/RECORD +63 -45
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/WHEEL +1 -1
- tests/test_camomilla_filters.py +1 -1
- tests/test_media.py +98 -65
- tests/test_menu.py +97 -0
- tests/test_model_api_register.py +393 -0
- tests/test_pages.py +343 -0
- tests/test_query_parser.py +1 -2
- tests/test_templates_context.py +111 -0
- tests/utils/api.py +0 -1
- tests/utils/media.py +10 -0
- camomilla/contrib/rest_framework/__init__.py +0 -0
- camomilla/serializers/fields/json.py +0 -48
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/top_level.txt +0 -0
camomilla/utils/query_parser.py
CHANGED
@@ -1,14 +1,21 @@
|
|
1
1
|
import re
|
2
2
|
from django.db.models import Q
|
3
|
+
from typing import List, Dict, Optional
|
3
4
|
|
4
|
-
CONDITION_PATTERN = re.compile(r"(\w+__\w+='[^']+'|\w+__\w+=\S+)") # Updated regex to handle quoted values
|
5
|
-
LOGICAL_OPERATORS = {"AND", "OR"}
|
6
5
|
|
7
6
|
class ConditionParser:
|
8
|
-
|
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
|
9
16
|
self.query = query
|
10
|
-
|
11
|
-
def parse(self, query=None):
|
17
|
+
|
18
|
+
def parse(self, query: str = None) -> Dict:
|
12
19
|
"""Parse the query or subquery. If no query is provided, use the instance's query."""
|
13
20
|
if query is None:
|
14
21
|
query = self.query
|
@@ -19,31 +26,31 @@ class ConditionParser:
|
|
19
26
|
return tokens[0]
|
20
27
|
return self.build_tree(tokens)
|
21
28
|
|
22
|
-
def tokenize(self, query):
|
29
|
+
def tokenize(self, query: str) -> List:
|
23
30
|
tokens = []
|
24
31
|
i = 0
|
25
32
|
while i < len(query):
|
26
|
-
if query[i] ==
|
33
|
+
if query[i] == "(":
|
27
34
|
j = i + 1
|
28
35
|
open_parens = 1
|
29
36
|
while j < len(query) and open_parens > 0:
|
30
|
-
if query[j] ==
|
37
|
+
if query[j] == "(":
|
31
38
|
open_parens += 1
|
32
|
-
elif query[j] ==
|
39
|
+
elif query[j] == ")":
|
33
40
|
open_parens -= 1
|
34
41
|
j += 1
|
35
42
|
if open_parens == 0:
|
36
|
-
subquery = query[i + 1:j - 1]
|
43
|
+
subquery = query[i + 1 : j - 1]
|
37
44
|
tokens.append(self.parse(subquery)) # Pass the subquery here
|
38
45
|
i = j
|
39
46
|
else:
|
40
47
|
raise ValueError("Mismatched parentheses")
|
41
|
-
elif query[i:i+3] ==
|
42
|
-
operator =
|
48
|
+
elif query[i : i + 3] == "AND" or query[i : i + 2] == "OR":
|
49
|
+
operator = "AND" if query[i : i + 3] == "AND" else "OR"
|
43
50
|
tokens.append(operator)
|
44
|
-
i += 3 if operator ==
|
51
|
+
i += 3 if operator == "AND" else 2
|
45
52
|
else:
|
46
|
-
match = CONDITION_PATTERN.match(query[i:])
|
53
|
+
match = self.CONDITION_PATTERN.match(query[i:])
|
47
54
|
if match:
|
48
55
|
condition = self.parse_condition(match.group())
|
49
56
|
tokens.append(condition)
|
@@ -52,10 +59,10 @@ class ConditionParser:
|
|
52
59
|
i += 1
|
53
60
|
return tokens
|
54
61
|
|
55
|
-
def parse_condition(self,
|
62
|
+
def parse_condition(self, condition: str) -> Optional[Dict]:
|
56
63
|
"""Parse a single condition into field lookup and value."""
|
57
|
-
if
|
58
|
-
field_lookup, value =
|
64
|
+
if "=" in condition:
|
65
|
+
field_lookup, value = condition.split("=")
|
59
66
|
value = value.strip("'").strip('"') # Remove single or double quotes
|
60
67
|
value = self.parse_value(value) # Parse the value
|
61
68
|
return {"field_lookup": field_lookup, "value": value}
|
@@ -71,7 +78,7 @@ class ConditionParser:
|
|
71
78
|
string = int(string)
|
72
79
|
return string
|
73
80
|
|
74
|
-
def build_tree(self, tokens):
|
81
|
+
def build_tree(self, tokens: List[str]) -> Dict:
|
75
82
|
"""Build a tree-like structure with operators and conditions."""
|
76
83
|
if not tokens:
|
77
84
|
return None
|
@@ -88,23 +95,26 @@ class ConditionParser:
|
|
88
95
|
if isinstance(output_stack[-1], dict):
|
89
96
|
output_stack[-1] = {
|
90
97
|
"operator": operator,
|
91
|
-
"conditions": [output_stack[-1], token]
|
98
|
+
"conditions": [output_stack[-1], token],
|
92
99
|
}
|
93
100
|
else:
|
94
101
|
output_stack[-1]["conditions"].append(token)
|
95
102
|
else:
|
96
103
|
output_stack.append(token)
|
97
104
|
|
98
|
-
elif token in LOGICAL_OPERATORS:
|
105
|
+
elif token in self.LOGICAL_OPERATORS:
|
99
106
|
# Operator found (AND/OR), handle precedence
|
100
107
|
operator_stack.append(token)
|
101
108
|
|
102
109
|
# If only one item in output_stack, return it directly
|
103
110
|
if len(output_stack) == 1:
|
104
111
|
return output_stack[0]
|
105
|
-
return {
|
112
|
+
return {
|
113
|
+
"operator": "AND",
|
114
|
+
"conditions": output_stack,
|
115
|
+
} # Default to AND if no operators
|
106
116
|
|
107
|
-
def to_q(self, parsed_tree):
|
117
|
+
def to_q(self, parsed_tree: Dict) -> Q:
|
108
118
|
"""Convert parsed tree structure into Q objects."""
|
109
119
|
if isinstance(parsed_tree, list):
|
110
120
|
# If parsed_tree is a list, combine all conditions with AND by default
|
@@ -140,9 +150,18 @@ class ConditionParser:
|
|
140
150
|
|
141
151
|
raise ValueError("Parsed tree structure is invalid")
|
142
152
|
|
143
|
-
def parse_to_q(self):
|
153
|
+
def parse_to_q(self) -> Q:
|
144
154
|
"""Parse the query and convert to Q object."""
|
145
155
|
parsed_tree = self.parse()
|
146
156
|
if not parsed_tree:
|
147
157
|
return Q() # Return an empty Q if parsing fails
|
148
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})"
|
camomilla/utils/templates.py
CHANGED
@@ -3,18 +3,31 @@ from typing import Sequence
|
|
3
3
|
|
4
4
|
from django import template as django_template
|
5
5
|
from os.path import relpath
|
6
|
+
from camomilla.settings import REGISTERED_TEMPLATES_APPS
|
6
7
|
|
7
8
|
|
8
9
|
def get_all_templates_files() -> Sequence[str]:
|
9
|
-
dirs = []
|
10
|
-
for engine in django_template.loader.engines.all():
|
11
|
-
# Exclude pip installed site package template dirs
|
12
|
-
dirs.extend(
|
13
|
-
x
|
14
|
-
for x in engine.template_dirs
|
15
|
-
if "site-packages" not in str(x) or "camomilla" in str(x)
|
16
|
-
)
|
17
10
|
files = []
|
18
|
-
|
19
|
-
|
11
|
+
|
12
|
+
for engine in django_template.loader.engines.all():
|
13
|
+
|
14
|
+
if REGISTERED_TEMPLATES_APPS:
|
15
|
+
dirs = [
|
16
|
+
d for d in engine.template_dirs
|
17
|
+
if any(app in str(d) for app in REGISTERED_TEMPLATES_APPS)
|
18
|
+
]
|
19
|
+
else:
|
20
|
+
# Exclude pip installed site package template dirs
|
21
|
+
dirs = [
|
22
|
+
d for d in engine.template_dirs
|
23
|
+
if "site-packages" not in str(d) or "camomilla" in str(d)
|
24
|
+
]
|
25
|
+
|
26
|
+
for d in dirs:
|
27
|
+
base = Path(d)
|
28
|
+
files.extend(
|
29
|
+
relpath(f, d)
|
30
|
+
for f in base.rglob("*.html")
|
31
|
+
)
|
32
|
+
|
20
33
|
return files
|
camomilla/utils/translation.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
import re
|
2
|
-
from typing import Any, Sequence, Iterator
|
2
|
+
from typing import Any, Sequence, Iterator, Union, List
|
3
3
|
|
4
4
|
from django.db.models import Model, Q
|
5
5
|
from django.utils.translation.trans_real import activate, get_language
|
6
6
|
from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
|
7
7
|
from modeltranslation.utils import build_localized_fieldname
|
8
8
|
from camomilla.settings import BASE_URL
|
9
|
+
from django.http import QueryDict
|
9
10
|
|
10
11
|
|
11
12
|
def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
|
@@ -34,11 +35,9 @@ def get_nofallbacks(instance: Model, attr: str, *args, **kwargs) -> Any:
|
|
34
35
|
|
35
36
|
def url_lang_decompose(url):
|
36
37
|
if BASE_URL and url.startswith(BASE_URL):
|
37
|
-
url = url[len(BASE_URL):]
|
38
|
+
url = url[len(BASE_URL) :]
|
38
39
|
data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
|
39
|
-
result = re.match(
|
40
|
-
f"^\/?({'|'.join(AVAILABLE_LANGUAGES)})?\/(.*)", url
|
41
|
-
) # noqa: W605
|
40
|
+
result = re.match(rf"^/?({'|'.join(AVAILABLE_LANGUAGES)})?/(.*)", url) # noqa: W605
|
42
41
|
groups = result and result.groups()
|
43
42
|
if groups and len(groups) == 2:
|
44
43
|
data["language"] = groups[0]
|
@@ -70,3 +69,46 @@ def is_translatable(model: Model) -> bool:
|
|
70
69
|
from modeltranslation.translator import translator
|
71
70
|
|
72
71
|
return model in translator.get_registered_models()
|
72
|
+
|
73
|
+
|
74
|
+
def plain_to_nest(data, fields, accessor="translations"):
|
75
|
+
"""
|
76
|
+
This function transforms a plain dictionary with translations fields (es. {"title_en": "Hello"})
|
77
|
+
into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
|
78
|
+
"""
|
79
|
+
trans_data = {}
|
80
|
+
for lang in AVAILABLE_LANGUAGES:
|
81
|
+
lang_data = {}
|
82
|
+
for field in fields:
|
83
|
+
trans_field_name = build_localized_fieldname(field, lang)
|
84
|
+
if trans_field_name in data:
|
85
|
+
lang_data[field] = data.pop(trans_field_name)
|
86
|
+
if lang_data.keys():
|
87
|
+
trans_data[lang] = lang_data
|
88
|
+
if trans_data.keys():
|
89
|
+
data[accessor] = trans_data
|
90
|
+
return data
|
91
|
+
|
92
|
+
|
93
|
+
def nest_to_plain(
|
94
|
+
data: Union[dict, QueryDict], fields: List[str], accessor="translations"
|
95
|
+
):
|
96
|
+
"""
|
97
|
+
This function is the inverse of plain_to_nest.
|
98
|
+
It transforms a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}})
|
99
|
+
into a plain dictionary with translations fields (es. {"title_en": "Hello"}).
|
100
|
+
"""
|
101
|
+
if isinstance(data, QueryDict):
|
102
|
+
data = data.dict()
|
103
|
+
translations = data.pop(accessor, {})
|
104
|
+
for lang in AVAILABLE_LANGUAGES:
|
105
|
+
nest_trans = translations.pop(lang, {})
|
106
|
+
for k in fields:
|
107
|
+
data.pop(k, None) # this removes all trans field without lang
|
108
|
+
if k in nest_trans:
|
109
|
+
# this saves on the default field the default language value
|
110
|
+
if lang == DEFAULT_LANGUAGE:
|
111
|
+
data[k] = nest_trans[k]
|
112
|
+
key = build_localized_fieldname(k, lang)
|
113
|
+
data[key] = data.get(key, nest_trans[k])
|
114
|
+
return data
|
camomilla/views/base/__init__.py
CHANGED
@@ -1,8 +1,38 @@
|
|
1
|
-
from ..mixins import
|
1
|
+
from ..mixins import (
|
2
|
+
OptimViewMixin,
|
3
|
+
PaginateStackMixin,
|
4
|
+
OrderingMixin,
|
5
|
+
CamomillaBasePermissionMixin,
|
6
|
+
)
|
2
7
|
from rest_framework import viewsets
|
8
|
+
from rest_framework.metadata import SimpleMetadata
|
9
|
+
from structured.contrib.restframework import StructuredJSONField
|
3
10
|
|
4
11
|
|
5
|
-
|
6
|
-
CamomillaBasePermissionMixin,
|
7
|
-
|
8
|
-
|
12
|
+
base_viewset_classes = [
|
13
|
+
CamomillaBasePermissionMixin,
|
14
|
+
OptimViewMixin,
|
15
|
+
OrderingMixin,
|
16
|
+
PaginateStackMixin,
|
17
|
+
viewsets.ModelViewSet,
|
18
|
+
]
|
19
|
+
|
20
|
+
|
21
|
+
class BaseViewMetadata(SimpleMetadata):
|
22
|
+
|
23
|
+
def get_field_info(self, field):
|
24
|
+
field_info = super().get_field_info(field)
|
25
|
+
if isinstance(field, StructuredJSONField):
|
26
|
+
field_info["schema"] = field.schema.json_schema()
|
27
|
+
field_info["type"] = "structured-json"
|
28
|
+
return field_info
|
29
|
+
|
30
|
+
def get_serializer_info(self, serializer):
|
31
|
+
info = super().get_serializer_info(serializer)
|
32
|
+
if hasattr(serializer, "plain_to_nest"):
|
33
|
+
info.update(serializer.plain_to_nest(info))
|
34
|
+
return info
|
35
|
+
|
36
|
+
|
37
|
+
class BaseModelViewset(*base_viewset_classes):
|
38
|
+
metadata_class = BaseViewMetadata
|
camomilla/views/medias.py
CHANGED
@@ -1,76 +1,17 @@
|
|
1
|
-
from
|
2
|
-
from
|
3
|
-
from
|
4
|
-
from .
|
5
|
-
from .
|
6
|
-
from
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
self.language_fallbacks = True
|
19
|
-
if (
|
20
|
-
len(self.active_language.split("-")) == 2
|
21
|
-
and self.active_language.split("-")[0] == "nofallbacks"
|
22
|
-
):
|
23
|
-
self.language_fallbacks = False
|
24
|
-
self.active_language = self.active_language.split("-")[1]
|
25
|
-
translation.activate(self.active_language)
|
26
|
-
return self.active_language
|
27
|
-
|
28
|
-
def initialize_request(self, request, *args, **kwargs):
|
29
|
-
self._get_user_language(request)
|
30
|
-
return super().initialize_request(request, *args, **kwargs)
|
31
|
-
|
32
|
-
def get_queryset(self):
|
33
|
-
if hasattr(super(), "get_queryset"):
|
34
|
-
return super().get_queryset()
|
35
|
-
return self.model.objects.all()
|
36
|
-
|
37
|
-
|
38
|
-
class CamomillaBasePermissionMixin:
|
39
|
-
def get_permissions(self):
|
40
|
-
return [*super().get_permissions(), CamomillaBasePermissions()]
|
41
|
-
|
42
|
-
|
43
|
-
class OptimViewMixin:
|
44
|
-
def get_serializer_class(self):
|
45
|
-
if hasattr(self, "action_serializers"):
|
46
|
-
if self.action in self.action_serializers:
|
47
|
-
return self.action_serializers[self.action]
|
48
|
-
return super().get_serializer_class()
|
49
|
-
|
50
|
-
def get_serializer_context(self):
|
51
|
-
return {"request": self.request, "action": self.action}
|
52
|
-
|
53
|
-
def get_queryset(self):
|
54
|
-
queryset = super().get_queryset()
|
55
|
-
serializer = self.get_serializer_class()
|
56
|
-
if hasattr(serializer, "optimize_qs"):
|
57
|
-
queryset = serializer.optimize_qs(queryset, context=self.get_serializer_context())
|
58
|
-
return queryset
|
59
|
-
|
60
|
-
|
61
|
-
class BulkDeleteMixin(object):
|
62
|
-
@action(
|
63
|
-
detail=False, methods=["post"], permission_classes=(CamomillaBasePermissions,)
|
64
|
-
)
|
65
|
-
def bulk_delete(self, request):
|
66
|
-
try:
|
67
|
-
self.model.objects.filter(pk__in=request.data).delete()
|
68
|
-
return Response(
|
69
|
-
{"detail": "Eliminazione multipla andata a buon fine"},
|
70
|
-
status=status.HTTP_200_OK,
|
71
|
-
)
|
72
|
-
except Exception:
|
73
|
-
return Response(
|
74
|
-
{"detail": "Eliminazione multipla non riuscita"},
|
75
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
76
|
-
)
|
1
|
+
from .bulk_actions import BulkDeleteMixin
|
2
|
+
from .language import GetUserLanguageMixin
|
3
|
+
from .optimize import OptimViewMixin
|
4
|
+
from .ordering import OrderingMixin
|
5
|
+
from .pagination import PaginateStackMixin, TrigramSearchMixin
|
6
|
+
from .permissions import CamomillaBasePermissionMixin
|
7
|
+
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"BulkDeleteMixin",
|
11
|
+
"GetUserLanguageMixin",
|
12
|
+
"OptimViewMixin",
|
13
|
+
"OrderingMixin",
|
14
|
+
"PaginateStackMixin",
|
15
|
+
"TrigramSearchMixin",
|
16
|
+
"CamomillaBasePermissionMixin",
|
17
|
+
]
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from rest_framework import status
|
2
|
+
from rest_framework.decorators import action
|
3
|
+
from rest_framework.response import Response
|
4
|
+
from ...permissions import CamomillaBasePermissions
|
5
|
+
|
6
|
+
|
7
|
+
class BulkDeleteMixin(object):
|
8
|
+
@action(
|
9
|
+
detail=False, methods=["post"], permission_classes=(CamomillaBasePermissions,)
|
10
|
+
)
|
11
|
+
def bulk_delete(self, request):
|
12
|
+
try:
|
13
|
+
self.model.objects.filter(pk__in=request.data).delete()
|
14
|
+
return Response(
|
15
|
+
{"detail": "Eliminazione multipla andata a buon fine"},
|
16
|
+
status=status.HTTP_200_OK,
|
17
|
+
)
|
18
|
+
except Exception:
|
19
|
+
return Response(
|
20
|
+
{"detail": "Eliminazione multipla non riuscita"},
|
21
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
22
|
+
)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from django.utils import translation
|
2
|
+
from camomilla import settings
|
3
|
+
|
4
|
+
|
5
|
+
class GetUserLanguageMixin:
|
6
|
+
def _get_user_language(self, request):
|
7
|
+
active_language_from_request = translation.get_language_from_request(request)
|
8
|
+
active_language = (
|
9
|
+
active_language_from_request
|
10
|
+
if active_language_from_request
|
11
|
+
else settings.DEFAULT_LANGUAGE
|
12
|
+
)
|
13
|
+
active_language = request.GET.get("language_code", active_language)
|
14
|
+
active_language = request.GET.get("language", active_language)
|
15
|
+
self.active_language = active_language
|
16
|
+
self.language_fallbacks = True
|
17
|
+
if (
|
18
|
+
len(self.active_language.split("-")) == 2
|
19
|
+
and self.active_language.split("-")[0] == "nofallbacks"
|
20
|
+
):
|
21
|
+
self.language_fallbacks = False
|
22
|
+
self.active_language = self.active_language.split("-")[1]
|
23
|
+
translation.activate(self.active_language)
|
24
|
+
return self.active_language
|
25
|
+
|
26
|
+
def initialize_request(self, request, *args, **kwargs):
|
27
|
+
self._get_user_language(request)
|
28
|
+
return super().initialize_request(request, *args, **kwargs)
|
29
|
+
|
30
|
+
def get_queryset(self):
|
31
|
+
if hasattr(super(), "get_queryset"):
|
32
|
+
return super().get_queryset()
|
33
|
+
return self.model.objects.all()
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class OptimViewMixin:
|
2
|
+
def get_serializer_class(self):
|
3
|
+
if hasattr(self, "action_serializers"):
|
4
|
+
if self.action in self.action_serializers:
|
5
|
+
return self.action_serializers[self.action]
|
6
|
+
return super().get_serializer_class()
|
7
|
+
|
8
|
+
def get_serializer_context(self):
|
9
|
+
return {"request": self.request, "action": self.action}
|
10
|
+
|
11
|
+
def get_queryset(self):
|
12
|
+
queryset = super().get_queryset()
|
13
|
+
serializer = self.get_serializer_class()
|
14
|
+
if hasattr(serializer, "optimize_qs"):
|
15
|
+
queryset = serializer.optimize_qs(
|
16
|
+
queryset, context=self.get_serializer_context()
|
17
|
+
)
|
18
|
+
return queryset
|
@@ -3,6 +3,7 @@ from django.db.models import Q
|
|
3
3
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
4
4
|
from django.contrib.postgres.search import SearchVector, SearchQuery, TrigramSimilarity
|
5
5
|
from camomilla.utils.query_parser import ConditionParser
|
6
|
+
from django.conf import settings
|
6
7
|
|
7
8
|
|
8
9
|
class TrigramSearchMixin:
|
@@ -33,10 +34,6 @@ class PaginateStackMixin:
|
|
33
34
|
list_handler, "shared_model", getattr(list_handler, "model", None)
|
34
35
|
)
|
35
36
|
|
36
|
-
def parse_filter(self, filter):
|
37
|
-
parser = ConditionParser(filter)
|
38
|
-
return parser.parse_to_q()
|
39
|
-
|
40
37
|
def handle_pagination(self, list_handler=None, items_per_page=None):
|
41
38
|
list_handler = list_handler if list_handler is not None else self.get_queryset()
|
42
39
|
items_per_page = int(
|
@@ -76,7 +73,7 @@ class PaginateStackMixin:
|
|
76
73
|
filters = dict(self.request.GET).get("fltr", [])
|
77
74
|
for filter in filters:
|
78
75
|
try:
|
79
|
-
list_handler = list_handler.filter(
|
76
|
+
list_handler = list_handler.filter(ConditionParser(filter).db_query)
|
80
77
|
except Exception:
|
81
78
|
pass
|
82
79
|
return list_handler
|
@@ -86,9 +83,15 @@ class PaginateStackMixin:
|
|
86
83
|
search_string = self.request.GET.get("search", None)
|
87
84
|
search_fields = search_fields or getattr(self, "search_fields", [])
|
88
85
|
if search_string and len(search_fields) > 0:
|
89
|
-
|
90
|
-
|
91
|
-
|
86
|
+
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
87
|
+
filter_statement = Q()
|
88
|
+
for field in search_fields:
|
89
|
+
filter_statement |= Q(**{field + '__icontains': search_string})
|
90
|
+
return list_handler.filter(filter_statement)
|
91
|
+
else:
|
92
|
+
return list_handler.annotate(
|
93
|
+
search=SearchVector(*search_fields),
|
94
|
+
).filter(search=SearchQuery(search_string))
|
92
95
|
|
93
96
|
return list_handler
|
94
97
|
|
camomilla/views/pages.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
from camomilla.models import Page
|
2
|
-
from camomilla.models.page import UrlNode
|
2
|
+
from camomilla.models.page import UrlNode, UrlRedirect
|
3
3
|
from camomilla.permissions import CamomillaBasePermissions
|
4
4
|
from camomilla.serializers import PageSerializer
|
5
5
|
from camomilla.serializers.page import UrlNodeSerializer
|
6
6
|
from camomilla.views.base import BaseModelViewset
|
7
7
|
from camomilla.views.decorators import active_lang
|
8
8
|
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
9
|
-
from rest_framework.decorators import api_view
|
9
|
+
from rest_framework.decorators import api_view, permission_classes
|
10
10
|
from rest_framework.response import Response
|
11
|
+
from rest_framework import permissions
|
11
12
|
from django.shortcuts import get_object_or_404
|
12
13
|
|
13
14
|
|
@@ -20,6 +21,15 @@ class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
|
20
21
|
|
21
22
|
@active_lang()
|
22
23
|
@api_view(["GET"])
|
24
|
+
@permission_classes(
|
25
|
+
[
|
26
|
+
permissions.AllowAny,
|
27
|
+
]
|
28
|
+
)
|
23
29
|
def fetch_page(request, permalink=""):
|
30
|
+
redirect = UrlRedirect.find_redirect_from_url(f"/{permalink}")
|
31
|
+
if redirect:
|
32
|
+
redirect = redirect.redirect()
|
33
|
+
return Response({"redirect": redirect.url, "status": redirect.status_code})
|
24
34
|
node = get_object_or_404(UrlNode, permalink=f"/{permalink}")
|
25
35
|
return Response(UrlNodeSerializer(node, context={"request": request}).data)
|
{django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/METADATA
RENAMED
@@ -1,30 +1,33 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: django-camomilla-cms
|
3
|
-
Version: 6.0.
|
3
|
+
Version: 6.0.0b18
|
4
4
|
Summary: Django powered cms
|
5
5
|
Author-email: Lotrèk <dimmitutto@lotrek.it>
|
6
6
|
License: MIT
|
7
|
-
Project-URL: Homepage, https://github.com/
|
7
|
+
Project-URL: Homepage, https://github.com/camomillacms/camomilla-core
|
8
8
|
Keywords: cms,django,api cms
|
9
9
|
Classifier: Environment :: Web Environment
|
10
10
|
Classifier: Framework :: Django
|
11
11
|
Classifier: Intended Audience :: Developers
|
12
12
|
Classifier: Programming Language :: Python
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
14
|
-
Requires-Python:
|
14
|
+
Requires-Python: <=3.13,>=3.8
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
|
-
Requires-Dist: django-modeltranslation
|
18
|
-
Requires-Dist: djsuperadmin
|
19
|
-
Requires-Dist: djangorestframework
|
20
|
-
Requires-Dist: django-
|
21
|
-
Requires-Dist: Pillow
|
22
|
-
Requires-Dist: django-
|
23
|
-
Requires-Dist: django-
|
24
|
-
Requires-Dist:
|
25
|
-
Requires-Dist:
|
26
|
-
|
27
|
-
|
17
|
+
Requires-Dist: django-modeltranslation<=0.18.12,>=0.18.7
|
18
|
+
Requires-Dist: djsuperadmin<1.0.0,>=0.9
|
19
|
+
Requires-Dist: djangorestframework<=3.14.0,>=3.10.0
|
20
|
+
Requires-Dist: django-structured-json-field>=0.4.1
|
21
|
+
Requires-Dist: Pillow>=10.0.0
|
22
|
+
Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
|
23
|
+
Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
|
24
|
+
Requires-Dist: django-tinymce<5.0.0,>=4.1.0
|
25
|
+
Requires-Dist: python-magic<0.5,>=0.4
|
26
|
+
Requires-Dist: Django<6,>=3.2
|
27
|
+
Requires-Dist: django_jsonform>=2.23
|
28
|
+
Dynamic: license-file
|
29
|
+
|
30
|
+
# camomilla django cms [](https://pypi.org/project/django-camomilla-cms)   [](./LICENSE)
|
28
31
|
|
29
32
|
## Install
|
30
33
|
|
@@ -35,7 +38,7 @@ $ pip install django-camomilla-cms
|
|
35
38
|
## Setup
|
36
39
|
```shell
|
37
40
|
$ mkdir -p camomilla_migrations
|
38
|
-
$ touch camomilla_migrations
|
41
|
+
$ touch camomilla_migrations/__init__.py
|
39
42
|
$ python manage.py makemigrations camomilla
|
40
43
|
$ python manage.py migrate camomilla
|
41
44
|
```
|
@@ -70,3 +73,7 @@ INSTALLED_APPS = [
|
|
70
73
|
|
71
74
|
pip install -r requirements-dev.txt
|
72
75
|
make test
|
76
|
+
|
77
|
+
## Run format with black
|
78
|
+
|
79
|
+
black camomilla
|