django-camomilla-cms 6.1.3__py2.py3-none-any.whl → 6.2.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 +1 -1
- camomilla/managers/pages.py +36 -5
- camomilla/models/menu.py +13 -7
- camomilla/models/mixins/__init__.py +1 -0
- camomilla/models/page.py +3 -2
- camomilla/serializers/mixins/page.py +3 -1
- camomilla/serializers/mixins/translation.py +2 -3
- camomilla/settings.py +12 -1
- camomilla/templates/defaults/base.html +2 -0
- camomilla/templates/defaults/parts/langswitch.html +7 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin/pages.py +1 -1
- camomilla/translation.py +1 -0
- camomilla/utils/translation.py +7 -8
- camomilla/views/articles.py +0 -2
- camomilla/views/contents.py +0 -2
- camomilla/views/decorators.py +23 -3
- camomilla/views/medias.py +0 -3
- camomilla/views/menus.py +0 -2
- camomilla/views/pages.py +10 -5
- camomilla/views/tags.py +0 -2
- camomilla/views/users.py +1 -2
- {django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/METADATA +3 -3
- {django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/RECORD +36 -31
- tests/test_admin_page_form.py +2 -2
- tests/test_camomilla_filters.py +1 -1
- tests/test_model_api_permissions.py +6 -0
- tests/test_pages.py +18 -0
- tests/test_parsers.py +62 -0
- tests/test_sitemap.py +16 -0
- tests/test_utils_getters.py +41 -0
- tests/test_utils_normalization.py +37 -0
- tests/test_utils_setters.py +62 -0
- {django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/WHEEL +0 -0
- {django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/licenses/LICENSE +0 -0
- {django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/top_level.txt +0 -0
camomilla/__init__.py
CHANGED
camomilla/managers/pages.py
CHANGED
@@ -42,13 +42,44 @@ class PageQuerySet(QuerySet):
|
|
42
42
|
|
43
43
|
|
44
44
|
class UrlNodeManager(models.Manager):
|
45
|
+
|
46
|
+
def get_reverse_pages_relations(self):
|
47
|
+
"""
|
48
|
+
Get all reverse relations coming from AbstractPages models.
|
49
|
+
This is used to annotate the UrlNode with the related page fields.
|
50
|
+
"""
|
51
|
+
from camomilla.models.page import AbstractPage
|
52
|
+
|
53
|
+
relations = []
|
54
|
+
|
55
|
+
for field in self.model._meta.get_fields():
|
56
|
+
if not (hasattr(field, "related_model") and field.one_to_one):
|
57
|
+
continue
|
58
|
+
|
59
|
+
if not issubclass(field.related_model, AbstractPage):
|
60
|
+
continue
|
61
|
+
|
62
|
+
if field.remote_field.name != "url_node":
|
63
|
+
continue
|
64
|
+
|
65
|
+
related_name = field.get_accessor_name()
|
66
|
+
relations.append(
|
67
|
+
{
|
68
|
+
"name": related_name,
|
69
|
+
"model": field.related_model,
|
70
|
+
"field_name": field.remote_field.name,
|
71
|
+
"field": field,
|
72
|
+
}
|
73
|
+
)
|
74
|
+
return relations
|
75
|
+
|
45
76
|
@property
|
46
77
|
def related_names(self):
|
47
|
-
self._related_names = getattr(
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
78
|
+
self._related_names = getattr(self, "_related_names", None)
|
79
|
+
if self._related_names is None:
|
80
|
+
self._related_names = list(
|
81
|
+
set([rel["name"] for rel in self.get_reverse_pages_relations()])
|
82
|
+
)
|
52
83
|
return self._related_names
|
53
84
|
|
54
85
|
def _annotate_fields(
|
camomilla/models/menu.py
CHANGED
@@ -10,7 +10,6 @@ from pydantic import (
|
|
10
10
|
Field,
|
11
11
|
SerializationInfo,
|
12
12
|
computed_field,
|
13
|
-
field_serializer,
|
14
13
|
model_serializer,
|
15
14
|
)
|
16
15
|
from structured.pydantic.models import BaseModel
|
@@ -18,7 +17,18 @@ from structured.fields import StructuredJSONField
|
|
18
17
|
from camomilla.models.page import UrlNode, AbstractPage
|
19
18
|
from typing import Optional, Union, Callable, List
|
20
19
|
from django.db.models.base import Model as DjangoModel
|
21
|
-
from
|
20
|
+
from typing_extensions import Annotated
|
21
|
+
from structured.pydantic.fields.serializer import FieldSerializer
|
22
|
+
from rest_framework import serializers
|
23
|
+
|
24
|
+
|
25
|
+
class AbstractPageMinimalSerializer(serializers.Serializer):
|
26
|
+
def to_representation(self, instance):
|
27
|
+
return {
|
28
|
+
"id": instance.id,
|
29
|
+
"name": instance.__str__(),
|
30
|
+
"model": f"{instance._meta.app_label}.{instance._meta.model_name}",
|
31
|
+
}
|
22
32
|
|
23
33
|
|
24
34
|
class LinkTypes(str, Enum):
|
@@ -30,13 +40,9 @@ class MenuNodeLink(BaseModel):
|
|
30
40
|
link_type: LinkTypes = LinkTypes.static
|
31
41
|
static: str = None
|
32
42
|
content_type: ContentType = None
|
33
|
-
page: AbstractPage = None
|
43
|
+
page: Annotated[AbstractPage, FieldSerializer(AbstractPageMinimalSerializer)] = None
|
34
44
|
url_node: UrlNode = None
|
35
45
|
|
36
|
-
@field_serializer('page')
|
37
|
-
def serialize_value(self, v):
|
38
|
-
return minimal_serialization(v)
|
39
|
-
|
40
46
|
@model_serializer(mode="wrap", when_used="json")
|
41
47
|
def update_relational(self, handler: Callable, info: SerializationInfo):
|
42
48
|
if self.link_type == LinkTypes.relational:
|
camomilla/models/page.py
CHANGED
@@ -99,7 +99,7 @@ class UrlNode(models.Model):
|
|
99
99
|
LANG_PERMALINK_FIELDS = (
|
100
100
|
[
|
101
101
|
build_localized_fieldname("permalink", lang)
|
102
|
-
for lang in settings.
|
102
|
+
for lang in settings.LANGUAGE_CODES
|
103
103
|
]
|
104
104
|
if settings.ENABLE_TRANSLATIONS
|
105
105
|
else ["permalink"]
|
@@ -319,7 +319,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
319
319
|
@property
|
320
320
|
def breadcrumbs(self) -> Sequence[dict]:
|
321
321
|
breadcrumb = {
|
322
|
-
"permalink": self.
|
322
|
+
"permalink": self.routerlink,
|
323
323
|
"title": self.breadcrumbs_title or self.title or "",
|
324
324
|
}
|
325
325
|
if self.parent:
|
@@ -481,6 +481,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
481
481
|
)
|
482
482
|
if preview:
|
483
483
|
permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
|
484
|
+
permalinks.pop(get_language(), None)
|
484
485
|
return permalinks
|
485
486
|
|
486
487
|
class Meta:
|
@@ -2,12 +2,14 @@ from rest_framework import serializers
|
|
2
2
|
from camomilla.models import UrlNode
|
3
3
|
from camomilla.serializers.validators import UniquePermalinkValidator
|
4
4
|
from typing import TYPE_CHECKING
|
5
|
+
from structured.contrib.restframework import StructuredModelSerializer
|
6
|
+
|
5
7
|
|
6
8
|
if TYPE_CHECKING:
|
7
9
|
from camomilla.models.page import AbstractPage
|
8
10
|
|
9
11
|
|
10
|
-
class AbstractPageMixin(serializers.ModelSerializer):
|
12
|
+
class AbstractPageMixin(StructuredModelSerializer, serializers.ModelSerializer):
|
11
13
|
"""
|
12
14
|
This mixin is needed to serialize AbstractPage models.
|
13
15
|
It provides permalink validation and some extra fields serialization.
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from functools import cached_property
|
2
2
|
from typing import Iterable, List
|
3
|
-
from modeltranslation import settings as mt_settings
|
4
3
|
from modeltranslation.translator import NotRegistered, translator
|
5
4
|
from modeltranslation.utils import build_localized_fieldname
|
6
5
|
from rest_framework import serializers
|
@@ -8,7 +7,7 @@ from rest_framework.exceptions import ValidationError
|
|
8
7
|
from camomilla.utils.getters import pointed_getter
|
9
8
|
from camomilla.utils.translation import is_translatable
|
10
9
|
from camomilla.utils.translation import nest_to_plain, plain_to_nest
|
11
|
-
from camomilla.settings import API_TRANSLATION_ACCESSOR
|
10
|
+
from camomilla.settings import API_TRANSLATION_ACCESSOR, LANGUAGE_CODES
|
12
11
|
|
13
12
|
|
14
13
|
class TranslationsMixin(serializers.ModelSerializer):
|
@@ -94,7 +93,7 @@ class RemoveTranslationsMixin(serializers.ModelSerializer):
|
|
94
93
|
included_translations = []
|
95
94
|
|
96
95
|
field_names = super().get_default_field_names(declared_fields, model_info)
|
97
|
-
for lang in
|
96
|
+
for lang in LANGUAGE_CODES:
|
98
97
|
if lang not in included_translations:
|
99
98
|
for field in self.translation_fields:
|
100
99
|
localized_fieldname = build_localized_fieldname(field, lang)
|
camomilla/settings.py
CHANGED
@@ -105,6 +105,10 @@ INTEGRATIONS_ASTRO_URL = pointed_getter(
|
|
105
105
|
django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.URL", ""
|
106
106
|
)
|
107
107
|
|
108
|
+
PAGE_ROUTER_CACHE = pointed_getter(
|
109
|
+
django_settings, "CAMOMILLA.API.PAGES.ROUTER_CACHE", 60 * 15
|
110
|
+
)
|
111
|
+
|
108
112
|
DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
|
109
113
|
|
110
114
|
# camomilla settings example
|
@@ -133,6 +137,13 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
|
|
133
137
|
# "URL": "http://localhost:4321"
|
134
138
|
# }
|
135
139
|
# }
|
136
|
-
# "API": {
|
140
|
+
# "API": {
|
141
|
+
# "NESTING_DEPTH": 10,
|
142
|
+
# "TRANSLATION_ACCESSOR": "translations",
|
143
|
+
# "PAGES": {
|
144
|
+
# "DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"
|
145
|
+
# },
|
146
|
+
# "ROUTER_CACHE": 60 * 15
|
147
|
+
# },
|
137
148
|
# "DEBUG": False
|
138
149
|
# }
|
@@ -36,6 +36,13 @@
|
|
36
36
|
{% get_language_info_list for LANGUAGES as languages %}
|
37
37
|
{% get_language_info for LANGUAGE_CODE as current_lang %}
|
38
38
|
|
39
|
+
<div class="language-switch--btn" data-lang="{{current_lang.code}}"
|
40
|
+
{% if current_lang.code != LANGUAGE_CODE %}
|
41
|
+
onclick="submitLanguageWithoutRedirect('{{current_lang.code}}');"
|
42
|
+
{% endif %}
|
43
|
+
>
|
44
|
+
<a>{{ current_lang.name_translated }} {% if current_lang.code == LANGUAGE_CODE %} 👾 {% endif %}</a>
|
45
|
+
</div>
|
39
46
|
{% for lang_code, redirect in page.alternate_urls.items %}
|
40
47
|
<div class="language-switch--btn" data-lang="{{lang_code}}"
|
41
48
|
{% if current_lang.code != lang_code %}
|
camomilla/theme/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "6.
|
1
|
+
__version__ = "6.2.0"
|
camomilla/theme/admin/pages.py
CHANGED
@@ -29,7 +29,7 @@ class AbstractPageModelForm(
|
|
29
29
|
super().__init__(*args, **kwargs)
|
30
30
|
templates = [(t, t) for t in get_templates(request)]
|
31
31
|
templates.insert(0, ("", "---------"))
|
32
|
-
self.fields["template"] = forms.ChoiceField(choices=templates)
|
32
|
+
self.fields["template"] = forms.ChoiceField(choices=templates, required=False)
|
33
33
|
|
34
34
|
def get_initial_for_field(self, field, field_name):
|
35
35
|
if field_name in UrlNode.LANG_PERMALINK_FIELDS:
|
camomilla/translation.py
CHANGED
camomilla/utils/translation.py
CHANGED
@@ -3,13 +3,12 @@ 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
|
-
from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
|
7
6
|
from modeltranslation.utils import build_localized_fieldname
|
8
|
-
from camomilla.settings import BASE_URL
|
7
|
+
from camomilla.settings import BASE_URL, DEFAULT_LANGUAGE, LANGUAGE_CODES
|
9
8
|
from django.http import QueryDict
|
10
9
|
|
11
10
|
|
12
|
-
def activate_languages(languages: Sequence[str] =
|
11
|
+
def activate_languages(languages: Sequence[str] = LANGUAGE_CODES) -> Iterator[str]:
|
13
12
|
old = get_language()
|
14
13
|
for language in languages:
|
15
14
|
activate(language)
|
@@ -37,7 +36,7 @@ def url_lang_decompose(url):
|
|
37
36
|
if BASE_URL and url.startswith(BASE_URL):
|
38
37
|
url = url[len(BASE_URL) :]
|
39
38
|
data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
|
40
|
-
result = re.match(rf"^/?({'|'.join(
|
39
|
+
result = re.match(rf"^/?({'|'.join(LANGUAGE_CODES)})?/(.*)", url) # noqa: W605
|
41
40
|
groups = result and result.groups()
|
42
41
|
if groups and len(groups) == 2:
|
43
42
|
data["language"] = groups[0]
|
@@ -48,14 +47,14 @@ def url_lang_decompose(url):
|
|
48
47
|
def get_field_translations(instance: Model, field_name: str, *args, **kwargs):
|
49
48
|
return {
|
50
49
|
lang: get_nofallbacks(instance, field_name, language=lang, *args, **kwargs)
|
51
|
-
for lang in
|
50
|
+
for lang in LANGUAGE_CODES
|
52
51
|
}
|
53
52
|
|
54
53
|
|
55
54
|
def lang_fallback_query(**kwargs):
|
56
55
|
current_lang = get_language()
|
57
56
|
query = Q()
|
58
|
-
for lang in
|
57
|
+
for lang in LANGUAGE_CODES:
|
59
58
|
query |= Q(**{f"{key}_{lang}": value for key, value in kwargs.items()})
|
60
59
|
if current_lang:
|
61
60
|
query = query & Q(
|
@@ -77,7 +76,7 @@ def plain_to_nest(data, fields, accessor="translations"):
|
|
77
76
|
into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
|
78
77
|
"""
|
79
78
|
trans_data = {}
|
80
|
-
for lang in
|
79
|
+
for lang in LANGUAGE_CODES:
|
81
80
|
lang_data = {}
|
82
81
|
for field in fields:
|
83
82
|
trans_field_name = build_localized_fieldname(field, lang)
|
@@ -101,7 +100,7 @@ def nest_to_plain(
|
|
101
100
|
if isinstance(data, QueryDict):
|
102
101
|
data = data.dict()
|
103
102
|
translations = data.pop(accessor, {})
|
104
|
-
for lang in
|
103
|
+
for lang in LANGUAGE_CODES:
|
105
104
|
nest_trans = translations.pop(lang, {})
|
106
105
|
for k in fields:
|
107
106
|
data.pop(k, None) # this removes all trans field without lang
|
camomilla/views/articles.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from camomilla.models import Article
|
2
|
-
from camomilla.permissions import CamomillaBasePermissions
|
3
2
|
from camomilla.serializers import ArticleSerializer
|
4
3
|
from camomilla.views.base import BaseModelViewset
|
5
4
|
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
@@ -8,6 +7,5 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
|
8
7
|
class ArticleViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
9
8
|
queryset = Article.objects.all()
|
10
9
|
serializer_class = ArticleSerializer
|
11
|
-
permission_classes = (CamomillaBasePermissions,)
|
12
10
|
search_fields = ["title", "identifier", "content", "permalink"]
|
13
11
|
model = Article
|
camomilla/views/contents.py
CHANGED
@@ -4,7 +4,6 @@ from django.http import JsonResponse
|
|
4
4
|
from rest_framework.decorators import action
|
5
5
|
|
6
6
|
from camomilla.models import Content
|
7
|
-
from camomilla.permissions import CamomillaBasePermissions
|
8
7
|
from camomilla.serializers import ContentSerializer
|
9
8
|
from camomilla.views.base import BaseModelViewset
|
10
9
|
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
@@ -14,7 +13,6 @@ class ContentViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
|
14
13
|
queryset = Content.objects.all()
|
15
14
|
serializer_class = ContentSerializer
|
16
15
|
model = Content
|
17
|
-
permission_classes = (CamomillaBasePermissions,)
|
18
16
|
|
19
17
|
@action(detail=True, methods=["get", "patch"])
|
20
18
|
def djsuperadmin(self, request, pk):
|
camomilla/views/decorators.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
from django.utils.translation import activate
|
3
|
-
from django.
|
3
|
+
from django.views.decorators.cache import cache_page
|
4
|
+
from camomilla.settings import LANGUAGE_CODES, DEFAULT_LANGUAGE
|
4
5
|
|
5
6
|
|
6
7
|
def active_lang(*args, **kwargs):
|
@@ -11,12 +12,12 @@ def active_lang(*args, **kwargs):
|
|
11
12
|
request = args[0].request
|
12
13
|
else:
|
13
14
|
request = args[0] if len(args) else kwargs.get("request", None)
|
14
|
-
lang =
|
15
|
+
lang = DEFAULT_LANGUAGE
|
15
16
|
if request and hasattr(request, "GET"):
|
16
17
|
lang = request.GET.get("lang", request.GET.get("language", lang))
|
17
18
|
if request and hasattr(request, "data"):
|
18
19
|
lang = request.data.pop("lang", request.data.pop("language", lang))
|
19
|
-
if lang and lang in
|
20
|
+
if lang and lang in LANGUAGE_CODES:
|
20
21
|
activate(lang)
|
21
22
|
request.LANGUAGE_CODE = lang
|
22
23
|
return func(*args, **kwargs)
|
@@ -24,3 +25,22 @@ def active_lang(*args, **kwargs):
|
|
24
25
|
return wrapped_func
|
25
26
|
|
26
27
|
return decorator
|
28
|
+
|
29
|
+
|
30
|
+
def staff_excluded_cache(timing=None):
|
31
|
+
def decorator(func):
|
32
|
+
if timing is None:
|
33
|
+
return func # No caching applied
|
34
|
+
|
35
|
+
@functools.wraps(func)
|
36
|
+
def wrapped_func(*args, **kwargs):
|
37
|
+
request = args[0] if len(args) else kwargs.get("request", None)
|
38
|
+
if request and hasattr(request, "user"):
|
39
|
+
user = request.user
|
40
|
+
if user.is_authenticated and user.is_staff:
|
41
|
+
return func(*args, **kwargs)
|
42
|
+
return cache_page(timing)(func)(*args, **kwargs)
|
43
|
+
|
44
|
+
return wrapped_func
|
45
|
+
|
46
|
+
return decorator
|
camomilla/views/medias.py
CHANGED
@@ -3,7 +3,6 @@ from rest_framework.response import Response
|
|
3
3
|
|
4
4
|
from camomilla.models import Media, MediaFolder
|
5
5
|
from camomilla.parsers import MultipartJsonParser
|
6
|
-
from camomilla.permissions import CamomillaBasePermissions
|
7
6
|
from camomilla.serializers import (
|
8
7
|
MediaFolderSerializer,
|
9
8
|
MediaListSerializer,
|
@@ -33,7 +32,6 @@ class MediaFolderViewSet(
|
|
33
32
|
):
|
34
33
|
model = MediaFolder
|
35
34
|
serializer_class = MediaFolderSerializer
|
36
|
-
permission_classes = (CamomillaBasePermissions,)
|
37
35
|
items_per_page = 18
|
38
36
|
search_fields = ["title", "alt_text", "file"]
|
39
37
|
|
@@ -85,6 +83,5 @@ class MediaViewSet(
|
|
85
83
|
):
|
86
84
|
queryset = Media.objects.all()
|
87
85
|
serializer_class = MediaSerializer
|
88
|
-
permission_classes = (CamomillaBasePermissions,)
|
89
86
|
model = Media
|
90
87
|
parser_classes = [MultipartJsonParser, JSONParser]
|
camomilla/views/menus.py
CHANGED
@@ -7,7 +7,6 @@ from rest_framework.response import Response
|
|
7
7
|
|
8
8
|
from camomilla.models import AbstractPage, Menu
|
9
9
|
from camomilla.models.page import UrlNode
|
10
|
-
from camomilla.permissions import CamomillaBasePermissions
|
11
10
|
from camomilla.serializers import ContentTypeSerializer, MenuSerializer
|
12
11
|
from camomilla.serializers.page import UrlNodeSerializer
|
13
12
|
from camomilla.views.base import BaseModelViewset
|
@@ -17,7 +16,6 @@ from camomilla.views.decorators import active_lang
|
|
17
16
|
class MenuViewSet(BaseModelViewset):
|
18
17
|
queryset = Menu.objects.all()
|
19
18
|
serializer_class = MenuSerializer
|
20
|
-
permission_classes = (CamomillaBasePermissions,)
|
21
19
|
model = Menu
|
22
20
|
|
23
21
|
lookup_field = "key"
|
camomilla/views/pages.py
CHANGED
@@ -1,26 +1,27 @@
|
|
1
1
|
from camomilla.models import Page
|
2
2
|
from camomilla.models.page import UrlNode, UrlRedirect
|
3
|
-
from camomilla.permissions import CamomillaBasePermissions
|
4
3
|
from camomilla.serializers import PageSerializer
|
5
4
|
from camomilla.serializers.page import RouteSerializer
|
5
|
+
from camomilla.utils.translation import url_lang_decompose
|
6
6
|
from camomilla.views.base import BaseModelViewset
|
7
|
-
from camomilla.views.decorators import
|
7
|
+
from camomilla.views.decorators import staff_excluded_cache
|
8
8
|
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
9
9
|
from rest_framework.decorators import api_view, permission_classes
|
10
10
|
from rest_framework.response import Response
|
11
11
|
from rest_framework import permissions
|
12
|
+
from django.utils.translation.trans_real import activate as activate_language
|
12
13
|
from django.shortcuts import get_object_or_404
|
14
|
+
from camomilla.settings import PAGE_ROUTER_CACHE
|
13
15
|
|
14
16
|
|
15
17
|
class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
16
18
|
queryset = Page.objects.all()
|
17
19
|
serializer_class = PageSerializer
|
18
|
-
permission_classes = (CamomillaBasePermissions,)
|
19
20
|
model = Page
|
20
21
|
|
21
22
|
|
22
|
-
@active_lang()
|
23
23
|
@api_view(["GET"])
|
24
|
+
@staff_excluded_cache(PAGE_ROUTER_CACHE)
|
24
25
|
@permission_classes(
|
25
26
|
[
|
26
27
|
permissions.AllowAny,
|
@@ -31,5 +32,9 @@ def pages_router(request, permalink=""):
|
|
31
32
|
if redirect:
|
32
33
|
redirect = redirect.redirect()
|
33
34
|
return Response({"redirect": redirect.url, "status": redirect.status_code})
|
34
|
-
|
35
|
+
url_decomposition = url_lang_decompose(permalink)
|
36
|
+
if not url_decomposition["permalink"].startswith("/"):
|
37
|
+
url_decomposition["permalink"] = f"/{url_decomposition['permalink']}"
|
38
|
+
activate_language(url_decomposition["language"])
|
39
|
+
node: UrlNode = get_object_or_404(UrlNode, permalink=url_decomposition["permalink"])
|
35
40
|
return Response(RouteSerializer(node, context={"request": request}).data)
|
camomilla/views/tags.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from camomilla.models import Tag
|
2
|
-
from camomilla.permissions import CamomillaBasePermissions
|
3
2
|
from camomilla.serializers import TagSerializer
|
4
3
|
from camomilla.views.base import BaseModelViewset
|
5
4
|
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
@@ -8,5 +7,4 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
|
8
7
|
class TagViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
9
8
|
queryset = Tag.objects.all()
|
10
9
|
serializer_class = TagSerializer
|
11
|
-
permission_classes = (CamomillaBasePermissions,)
|
12
10
|
model = Tag
|
camomilla/views/users.py
CHANGED
@@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
|
|
8
8
|
from rest_framework.response import Response
|
9
9
|
from rest_framework.views import APIView
|
10
10
|
|
11
|
-
from camomilla.permissions import
|
11
|
+
from camomilla.permissions import CamomillaSuperUser, ReadOnly
|
12
12
|
from camomilla.serializers import (
|
13
13
|
PermissionSerializer,
|
14
14
|
UserProfileSerializer,
|
@@ -30,7 +30,6 @@ class UserViewSet(BaseModelViewset):
|
|
30
30
|
queryset = get_user_model().objects.all()
|
31
31
|
serializer_class = UserSerializer
|
32
32
|
model = get_user_model()
|
33
|
-
permission_classes = (CamomillaSuperUser | CamomillaBasePermissions,)
|
34
33
|
|
35
34
|
@action(detail=False, methods=["get", "put"], permission_classes=(IsAuthenticated,))
|
36
35
|
def current(self, request):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-camomilla-cms
|
3
|
-
Version: 6.
|
3
|
+
Version: 6.2.0
|
4
4
|
Summary: Django powered cms
|
5
5
|
Author-email: Lotrèk <dimmitutto@lotrek.it>
|
6
6
|
License: MIT
|
@@ -11,13 +11,13 @@ 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: <=3.13,>=3.
|
14
|
+
Requires-Python: <=3.13,>=3.9
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
17
|
Requires-Dist: django-modeltranslation<=0.18.12,>=0.18.7
|
18
18
|
Requires-Dist: djsuperadmin<1.0.0,>=0.9
|
19
19
|
Requires-Dist: djangorestframework<=3.14.0,>=3.10.0
|
20
|
-
Requires-Dist: django-structured-json-field>=
|
20
|
+
Requires-Dist: django-structured-json-field>=1.1.1
|
21
21
|
Requires-Dist: Pillow>=10.0.0
|
22
22
|
Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
|
23
23
|
Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
|
@@ -1,4 +1,4 @@
|
|
1
|
-
camomilla/__init__.py,sha256=
|
1
|
+
camomilla/__init__.py,sha256=jnnn2SfWq4nCc_c1cvq46CUVnXuYhwFEE7eQI9DkJ1s,243
|
2
2
|
camomilla/apps.py,sha256=eUwb9ynyiRAc5OXgt7ZsAdhsCOnPCpNdIFYMheNeN-o,532
|
3
3
|
camomilla/authentication.py,sha256=jz6tQT4PPEu-_JLox1LZrOy7EiWBb9MWaObK63MJGus,855
|
4
4
|
camomilla/context_processors.py,sha256=cGowjDZ-oDGYn1j2Pj5QDGCqnzXAOdOwp5dmzin_FTc,165
|
@@ -9,9 +9,9 @@ camomilla/model_api.py,sha256=-7l3fc2eN1itCMzkWA8nFaQXMmz0vs7IlGlShF-gSuo,2487
|
|
9
9
|
camomilla/parsers.py,sha256=fL8XGCGPxJIZNZkPdGtnPSbDP-6-yzGOCVMuLPjkx9Y,1975
|
10
10
|
camomilla/permissions.py,sha256=9NlBO4JMmg36vXCUjPNyq6uZxhkdrnXyIbJVLtWhGWE,1813
|
11
11
|
camomilla/redirects.py,sha256=ilcyHidb5Iw3jTrXMnPntr50kkl_WB3QOB0VNkIxP7A,263
|
12
|
-
camomilla/settings.py,sha256=
|
12
|
+
camomilla/settings.py,sha256=g8XZQ2hmqOeUhh4LmHggbPZsqPcf5hdFqsHhWnykiNc,4565
|
13
13
|
camomilla/sitemap.py,sha256=U2t5TwhB_-sEscmQZ69PZ5st3bIap8NRxzWEvCgB130,786
|
14
|
-
camomilla/translation.py,sha256=
|
14
|
+
camomilla/translation.py,sha256=8O9WlnkwboJZBWovS2ShWr3yA6--9HnoklLfmfX8ing,1336
|
15
15
|
camomilla/urls.py,sha256=umWlVDJ_J4aPkqeSbHJznrGJi0marvWi8TfSmsRT5a0,2101
|
16
16
|
camomilla/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
camomilla/contrib/modeltranslation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -22,14 +22,14 @@ camomilla/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
22
22
|
camomilla/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
23
|
camomilla/management/commands/regenerate_thumbnails.py,sha256=pKToASR8p8TJezGpFfuylsAHtriNueJ7xqJJxq55adY,496
|
24
24
|
camomilla/managers/__init__.py,sha256=Zwp28E1RKafIl0FqcUi1YNHxF19IsMIvhlhS-Njg9Mw,60
|
25
|
-
camomilla/managers/pages.py,sha256=
|
25
|
+
camomilla/managers/pages.py,sha256=z0bLHDwbQby2PoHx_eTm1ClCdwFun-wVArncNPWxYgI,4947
|
26
26
|
camomilla/models/__init__.py,sha256=y7Q34AGhQ1weJUKWb6XjiyUbwRnJeylOBGMErU0wqYg,147
|
27
27
|
camomilla/models/article.py,sha256=LgkZgRsubtDV6NwBz8E2bIgKD6H3I-1QLAxEan5TYYs,1139
|
28
28
|
camomilla/models/content.py,sha256=mIgtifb_WMIt58we5u6qWZemHvuDN1zZaBeCyzHL78A,956
|
29
29
|
camomilla/models/media.py,sha256=pD-qldiHDOOHgux4lsivQLBcOJJrRx3a4Bg8ODNx7r0,6852
|
30
|
-
camomilla/models/menu.py,sha256=
|
31
|
-
camomilla/models/page.py,sha256=
|
32
|
-
camomilla/models/mixins/__init__.py,sha256=
|
30
|
+
camomilla/models/menu.py,sha256=SSIjnrGreDqN7sfh4LSu2Ek6d-DPv8Dt7fAOZoGIQN4,4268
|
31
|
+
camomilla/models/page.py,sha256=4ofCugWL_RGuXRP4UCLtSxUtlM3q5LmF8QEWBOimqBw,20048
|
32
|
+
camomilla/models/mixins/__init__.py,sha256=sV8KTOSysmbbhXgiKE2NniZIjzovZoowjq5AFh6FNfQ,1294
|
33
33
|
camomilla/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
34
|
camomilla/openapi/schema.py,sha256=C22dhKjaJ2DTK4KWFjyMJXiwe8NLy7ZTW5d-I1dqZ7g,2546
|
35
35
|
camomilla/serializers/__init__.py,sha256=8v1GsJ_YZ6T72VnKBb5l-8K93oaLf4PIsMt-yFtK-Gk,176
|
@@ -53,17 +53,17 @@ camomilla/serializers/mixins/language.py,sha256=VukEvPzTpKQfwB-z_RtoNIJ43N3OEgfj
|
|
53
53
|
camomilla/serializers/mixins/nesting.py,sha256=gCEU2UE_Y8e4VRnvT0AExFgwTfJm_jnSqa6l2SwZ3Mg,1432
|
54
54
|
camomilla/serializers/mixins/optimize.py,sha256=zAtbtk6kfGq9FnapqI8tVYOuMKd1IkHbAV6LffL61Z4,3845
|
55
55
|
camomilla/serializers/mixins/ordering.py,sha256=rXQOz47_U4IsMT6IBhySghcmJWMZgpPWHovDcZQG88k,1172
|
56
|
-
camomilla/serializers/mixins/page.py,sha256
|
57
|
-
camomilla/serializers/mixins/translation.py,sha256=
|
56
|
+
camomilla/serializers/mixins/page.py,sha256=-8f70L0yVJyR_06fOFlxKxKnwvJUkBJqW38-SjwYekI,2231
|
57
|
+
camomilla/serializers/mixins/translation.py,sha256=L4nSYWjLP8X0JLo44TUv9hcqtpsCqt5yHy7ZZB-XE9s,4269
|
58
58
|
camomilla/storages/__init__.py,sha256=ytGgX59Ar8vFfYz7eV8z-j5yO_5FqxdZM25iyLnJuSA,131
|
59
59
|
camomilla/storages/default.py,sha256=GNzvV_JZpXMcfTkyXjw5CfK8EIBi3o-NXYBO0KAxD5M,351
|
60
60
|
camomilla/storages/optimize.py,sha256=VGSXZigzZC8LnPTqyTOpPA2Ba9EJB_KC5bcACoRs4GA,2762
|
61
61
|
camomilla/storages/overwrite.py,sha256=jvW3zHvXNzH9dIjeZmmfXo_O3K1ZQmLQzmlSKAOE8ZA,360
|
62
62
|
camomilla/templates/admin/camomilla/page/change_form.html,sha256=ig7rRUtylDZMINBQuVPpZLmeB4sOTV_VtqnTgzAyxEo,251
|
63
|
-
camomilla/templates/defaults/base.html,sha256=
|
63
|
+
camomilla/templates/defaults/base.html,sha256=HjP__4XgzootLjbpTRXwQunHu3hwkzBRFpYfko87of0,8219
|
64
64
|
camomilla/templates/defaults/articles/default.html,sha256=1f89jBvNtTa1mPAbC91yy8CzeAjTWO3hhQsTuQW5OKg,239
|
65
65
|
camomilla/templates/defaults/pages/default.html,sha256=bP81Qb6M56I-fBJMywWwEu_cnERtWIX28UkGrUSRU6M,144
|
66
|
-
camomilla/templates/defaults/parts/langswitch.html,sha256=
|
66
|
+
camomilla/templates/defaults/parts/langswitch.html,sha256=Cr-njWdtFxiJmWCq9_ZWToCFzJMKk7Iv2xw8BAHavrU,3808
|
67
67
|
camomilla/templates/defaults/parts/menu.html,sha256=ReE-FfmfCNuNkJI33QqIfmMgLSBl3FTkWAhEa59aD3A,381
|
68
68
|
camomilla/templates/defaults/widgets/media_select_multiple.html,sha256=k2XYou8KkPuFLnPMkPJAFJ-zGJj2Xvu6R3ZmiKa3g7Q,3727
|
69
69
|
camomilla/templates_context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -73,10 +73,10 @@ camomilla/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
73
73
|
camomilla/templatetags/camomilla_filters.py,sha256=35x0-cWrRHeLhqypSLlJzEFY_fQDcRHiwZQpFgIsspE,692
|
74
74
|
camomilla/templatetags/menus.py,sha256=7fc4f9DDqtqG6wNb5_Q0km-fq0mqvGnbpR21qO1TJUw,960
|
75
75
|
camomilla/templatetags/model_extras.py,sha256=6WfVDYP_OfuVJd3cNGNA55Wj9uWdrbfOZQ0ua9Xt_vc,2257
|
76
|
-
camomilla/theme/__init__.py,sha256=
|
76
|
+
camomilla/theme/__init__.py,sha256=5H_IrrHNTfYkLbC3nRLonK3TwkV0TTf0xGbx1a13Scw,22
|
77
77
|
camomilla/theme/apps.py,sha256=Ue2H80fbFgxkQyHeU2H0fWs9Y6d-EnHYv4zz824FSRk,1066
|
78
78
|
camomilla/theme/admin/__init__.py,sha256=TALAZaE-gWshSeGc6yy7VahdX5UfeCeoOE9Q5kJCEpM,2270
|
79
|
-
camomilla/theme/admin/pages.py,sha256=
|
79
|
+
camomilla/theme/admin/pages.py,sha256=VCsT5zTWFBba4LDWEJWPW5cLQTU3HUQyu6eUtJVwDoQ,2630
|
80
80
|
camomilla/theme/admin/translations.py,sha256=iAjGM1A1aYrsz1FpeybROk6rn3Ddl_oUCwgU5oD8nSw,308
|
81
81
|
camomilla/theme/static/admin/css/responsive.css,sha256=yGq6qXrr8xEVsXTnprIBgkX-sMGZrNf0Kkh-xDxf6yE,157
|
82
82
|
camomilla/theme/static/admin/img/favicon.ico,sha256=qpKv_2MaGILvyihnD1Vq9Yk-ZXGkxWTW26ciMeBFMYU,15406
|
@@ -90,17 +90,17 @@ camomilla/utils/query_parser.py,sha256=TUScPzPVVJzaKdqy5NqtMOft3H5Bx6liXTVPM1yjH
|
|
90
90
|
camomilla/utils/seo.py,sha256=8p_a_TGgohenpJb094tT4mMxbn2xzW0qDILuTnjNocM,3324
|
91
91
|
camomilla/utils/setters.py,sha256=LV57SM65rL1_ZQkVzk9al_Q13lndVywXLkqgfIvgS0Y,915
|
92
92
|
camomilla/utils/templates.py,sha256=NAvvuv-fwu9CIxQY5t0RKs4GiFClOBZa9pOtcH_YP6s,1576
|
93
|
-
camomilla/utils/translation.py,sha256=
|
93
|
+
camomilla/utils/translation.py,sha256=i5F8MY7fUB9ImzxcgwyY-Eci-nlIHxXlerJIRbgFiXw,4133
|
94
94
|
camomilla/views/__init__.py,sha256=94QuOnnbfMMb17mruO2ydUt286-8zBmDxEPWrJv5Wog,178
|
95
|
-
camomilla/views/articles.py,sha256=
|
96
|
-
camomilla/views/contents.py,sha256=
|
97
|
-
camomilla/views/decorators.py,sha256=
|
95
|
+
camomilla/views/articles.py,sha256=mHqNP9l5_C_7E_2mqK7HQsmb6UpD06LJRJ2udhG0CBI,459
|
96
|
+
camomilla/views/contents.py,sha256=TT8p1zj4RhY9PzougpBVhQX3IM6oxL3bTJ3MX5why_8,1093
|
97
|
+
camomilla/views/decorators.py,sha256=w8gjns515nPGsr9TJezKC3EFOURWyfxvgJEsRpmDai0,1636
|
98
98
|
camomilla/views/languages.py,sha256=Rt_X7s3dbDBv4dxsQ9fnav_u0TAzzo8fGKBBx3esDsg,441
|
99
|
-
camomilla/views/medias.py,sha256=
|
100
|
-
camomilla/views/menus.py,sha256=
|
101
|
-
camomilla/views/pages.py,sha256=
|
102
|
-
camomilla/views/tags.py,sha256=
|
103
|
-
camomilla/views/users.py,sha256=
|
99
|
+
camomilla/views/medias.py,sha256=ljfzwdU-GNV1ybeGMr224qzXE8l012fgC9-aTpwKdyg,2837
|
100
|
+
camomilla/views/menus.py,sha256=K2r43fiezUggGn-MkclOH0VTdanCpGh4LBqb-YaHxQk,3337
|
101
|
+
camomilla/views/pages.py,sha256=QZdb6Z6QZzj7_nHkJGckywyg9-xlvl1hyG3JcZfLe7Y,1713
|
102
|
+
camomilla/views/tags.py,sha256=Knivz6dOwu54oas7SAwK5mFJdGo10fn8OTC3bVZgJk8,367
|
103
|
+
camomilla/views/users.py,sha256=ctdlIvNsE9wGpaB7CCcWl1zPk7tTjB6lvYEDQxg30mU,3030
|
104
104
|
camomilla/views/base/__init__.py,sha256=bpbVBGXLTy7No95XyDNB2U8hVXmwQJrF1VjLAS5WH90,1232
|
105
105
|
camomilla/views/mixins/__init__.py,sha256=Znv3fLYVy6lgu03Q_D8fTen4zMxI6VSRaLPDU8Cp7Ws,473
|
106
106
|
camomilla/views/mixins/bulk_actions.py,sha256=i0duWW6wey9m7I_V8-gPcHsbJyPEfSdMdj4h2i-CbPw,787
|
@@ -109,28 +109,33 @@ camomilla/views/mixins/optimize.py,sha256=Y-jMYmScLeY7ZnDS1K9o8aXSgTGc5RrYUh66r5
|
|
109
109
|
camomilla/views/mixins/ordering.py,sha256=mh7fqPyVCVJh84Nl2pYFQouzGxa-ANF3Wqv0pCb7OVU,4779
|
110
110
|
camomilla/views/mixins/pagination.py,sha256=0haXRJNFrawCRSe4kNd4EWHsuhBAjdeqCklGJ3SD2gw,5950
|
111
111
|
camomilla/views/mixins/permissions.py,sha256=TPmR3Hoa3BjeJu9rCE_7lpLOAupue4WI42C21HTo6X4,200
|
112
|
-
django_camomilla_cms-6.
|
112
|
+
django_camomilla_cms-6.2.0.dist-info/licenses/LICENSE,sha256=kVS7zDrNkav2hLLXbOJwVdonY2ToApTK3khyJagGQoQ,1063
|
113
113
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
114
|
-
tests/test_admin_page_form.py,sha256=
|
114
|
+
tests/test_admin_page_form.py,sha256=nvaSJjfy8yEZt6J0ycOrMKfMgy6bHHFrKdHtWLAEf2E,1971
|
115
115
|
tests/test_api.py,sha256=t03EFDezGgm4UJl8RIVvnTUkAGTB6ptm0G2lHBQ7ljc,1833
|
116
|
-
tests/test_camomilla_filters.py,sha256=
|
116
|
+
tests/test_camomilla_filters.py,sha256=SQR5Q_9ORoKGbuD8WRdBOvbyojQENNb72eP6TlrCefM,1564
|
117
117
|
tests/test_media.py,sha256=n5PQmpMvw-a2epmq_60R0X-vVOa4BeyKMZkguGJzKa0,5243
|
118
118
|
tests/test_menu.py,sha256=hrTikgXNnry1bS-t5K7UGYreEJ3m-FU0r4pduumwTd4,3791
|
119
119
|
tests/test_model_api.py,sha256=ml3OlLuBfcnr2EMjwQLvVDPT2adSQ7WS4IxxXKD1InU,4121
|
120
|
-
tests/test_model_api_permissions.py,sha256=
|
120
|
+
tests/test_model_api_permissions.py,sha256=1HCagpyUWQMiAJ8L0w2N2EKDqlb1ZvpzGCtd03H0Je4,2114
|
121
121
|
tests/test_model_api_register.py,sha256=txKaVTGt-DGrmI-6xcUEluPd7ArNi80VvlqBVXdH8zk,13555
|
122
122
|
tests/test_models.py,sha256=WJs8lxWZWn1l7X3a_QFVc8fF5LHTsI8bc3uhQe6-o-Q,684
|
123
123
|
tests/test_page_meta.py,sha256=QFmX97LBYSuHn9vJPM80MWZc3BwfGO5V-sCClk7ExOA,3142
|
124
124
|
tests/test_page_relation_api.py,sha256=PrFY3vuuFal4og8MUq8ba02qITsNOtIP1eNmQ_f9jxk,2760
|
125
|
-
tests/test_pages.py,sha256=
|
125
|
+
tests/test_pages.py,sha256=bwAu-obgL_VZmA1PC3CnlcT2IvAeNuwUZfxm4pz9RYc,12503
|
126
|
+
tests/test_parsers.py,sha256=3dSBxHRAVLtM_GJdBsVCLaOjW1UDDDtQx9zLnHITmg0,2618
|
126
127
|
tests/test_query_parser.py,sha256=R9l0L2QDEDcm2b6IFUhyf7wMXLzL9RySLkzKTWRtBkE,2097
|
128
|
+
tests/test_sitemap.py,sha256=MWkossQKdCM6s38ORHTWjPqWnbpXzrQ700indpiIwRU,663
|
127
129
|
tests/test_templates_context.py,sha256=zGdmbQMGNXB2V_15BaQDIgqFMnVjBAw969n1tu3m7HY,5626
|
128
130
|
tests/test_utils.py,sha256=ow4csGfU5WzMgAT5zWjZIxZwW1-BqnMduDt8hOzf9cE,2166
|
131
|
+
tests/test_utils_getters.py,sha256=oTgtmoxJBmm5sn76AP6fkgE-fDYwuHOVDwrTQeUYxwI,1263
|
132
|
+
tests/test_utils_normalization.py,sha256=gXHtGD6iKnk1Hqr3GmFfRFqGCfD5U7q2KIqVz4WPFnY,899
|
133
|
+
tests/test_utils_setters.py,sha256=0HimIaSz5IJES6Ya1Ep8lFo13CyVD_M52DANOf73nUY,1523
|
129
134
|
tests/fixtures/__init__.py,sha256=ixyA6ZsmYbiKEsjQGOGoG4KyJmwWrf-qeoQjQG3J66U,426
|
130
135
|
tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
131
136
|
tests/utils/api.py,sha256=TYcDXeILHtBwzwG0acwPFmiqMZnlF9VnLB0Ydhg55vA,865
|
132
137
|
tests/utils/media.py,sha256=-cnrQzzVuhNSb5rT5xMUs5f3yYpBnS0fVGDcjgsb8lw,291
|
133
|
-
django_camomilla_cms-6.
|
134
|
-
django_camomilla_cms-6.
|
135
|
-
django_camomilla_cms-6.
|
136
|
-
django_camomilla_cms-6.
|
138
|
+
django_camomilla_cms-6.2.0.dist-info/METADATA,sha256=kasKQOklWZ6NosfiFKEUztsujYne0E-1aLzAqEuWi3Q,5652
|
139
|
+
django_camomilla_cms-6.2.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
140
|
+
django_camomilla_cms-6.2.0.dist-info/top_level.txt,sha256=G9VIGBmMMqC7JEckoTgXKmC6T2BR75QRkqRnngw1_lo,16
|
141
|
+
django_camomilla_cms-6.2.0.dist-info/RECORD,,
|
tests/test_admin_page_form.py
CHANGED
@@ -36,7 +36,7 @@ class AdminPageFormTestCase(TestCase):
|
|
36
36
|
|
37
37
|
page_admin = PageAdmin(Page, AdminSite())
|
38
38
|
form = page_admin.get_form(request)()
|
39
|
-
self.assertEqual(len(list(form.fields)),
|
39
|
+
self.assertEqual(len(list(form.fields)), 35)
|
40
40
|
self.assertTrue("template" in list(form.fields))
|
41
41
|
self.assertListEqual(
|
42
42
|
form.fields["template"].widget.choices,
|
@@ -54,7 +54,7 @@ class AdminPageFormTestCase(TestCase):
|
|
54
54
|
|
55
55
|
page_admin = PageAdmin(Page, AdminSite())
|
56
56
|
form = page_admin.get_form(request_with_cookies)()
|
57
|
-
self.assertEqual(len(list(form.fields)),
|
57
|
+
self.assertEqual(len(list(form.fields)), 35)
|
58
58
|
self.assertTrue("template" in list(form.fields))
|
59
59
|
self.assertListEqual(
|
60
60
|
form.fields["template"].widget.choices,
|
tests/test_camomilla_filters.py
CHANGED
@@ -9,14 +9,20 @@ client = APIClient()
|
|
9
9
|
def test_right_permissions():
|
10
10
|
response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
|
11
11
|
assert response.status_code == 401
|
12
|
+
response = client.get("/api/models/test-model/")
|
13
|
+
assert response.status_code == 401
|
12
14
|
token = login_user()
|
13
15
|
client.credentials(HTTP_AUTHORIZATION="Token " + token)
|
14
16
|
response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
|
15
17
|
assert response.status_code == 403
|
18
|
+
response = client.get("/api/models/test-model/")
|
19
|
+
assert response.status_code == 200
|
16
20
|
token = login_staff()
|
17
21
|
client.credentials(HTTP_AUTHORIZATION="Token " + token)
|
18
22
|
response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
|
19
23
|
assert response.status_code == 403
|
24
|
+
response = client.get("/api/models/test-model/")
|
25
|
+
assert response.status_code == 200
|
20
26
|
token = login_superuser()
|
21
27
|
client.credentials(HTTP_AUTHORIZATION="Token " + token)
|
22
28
|
response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
|
tests/test_pages.py
CHANGED
@@ -347,3 +347,21 @@ class PagesTestCase(TestCase):
|
|
347
347
|
response = self.client.get("/it/permalink_6_it/")
|
348
348
|
assert response.status_code == 301
|
349
349
|
assert response.url == "/it/permalink_6_it_changed/"
|
350
|
+
|
351
|
+
@pytest.mark.django_db
|
352
|
+
def test_page_keywords(self):
|
353
|
+
# Create page with keywords field and check it's given back as expected
|
354
|
+
response = self.client.post(
|
355
|
+
"/api/camomilla/pages/",
|
356
|
+
{
|
357
|
+
"og_description_it" : "Keywords Test",
|
358
|
+
"keywords_it" : ['key1', 'key2']
|
359
|
+
},
|
360
|
+
format="json",
|
361
|
+
)
|
362
|
+
|
363
|
+
assert response.status_code == 201
|
364
|
+
assert len(Page.objects.all()) == 1
|
365
|
+
page = Page.objects.first()
|
366
|
+
assert page.og_description_it == "Keywords Test"
|
367
|
+
assert page.keywords_it == ['key1', 'key2']
|
tests/test_parsers.py
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
import pytest
|
4
|
+
from django.conf import settings
|
5
|
+
from django.test import RequestFactory
|
6
|
+
from django.core.files.uploadedfile import SimpleUploadedFile
|
7
|
+
from camomilla.parsers import MultipartJsonParser
|
8
|
+
|
9
|
+
@pytest.mark.django_db
|
10
|
+
def test_multipart_json_parser_parses_json_and_files(monkeypatch):
|
11
|
+
# Prepare multipart data
|
12
|
+
json_data = json.dumps({"foo": "bar", "nested": {"baz": 1}, "nested_list": [{"qux": "quux"}, {"corge": "grault"}]})
|
13
|
+
file_content = b"filecontent"
|
14
|
+
upload = SimpleUploadedFile("test.txt", file_content, content_type="text/plain")
|
15
|
+
|
16
|
+
# Simulate multipart POST
|
17
|
+
factory = RequestFactory()
|
18
|
+
data = {
|
19
|
+
"data": json_data,
|
20
|
+
"nested.file": upload,
|
21
|
+
"nested_list.1.file": upload,
|
22
|
+
}
|
23
|
+
request = factory.post("/", data)
|
24
|
+
request.upload_handlers = []
|
25
|
+
request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
|
26
|
+
|
27
|
+
# Patch DjangoMultiPartParser to return our data
|
28
|
+
class DummyParser:
|
29
|
+
def __init__(self, *a, **kw): pass
|
30
|
+
def parse(self):
|
31
|
+
return ( {"data": json_data}, {"nested.file": upload, "nested_list.1.file": upload,},)
|
32
|
+
monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
|
33
|
+
|
34
|
+
parser = MultipartJsonParser()
|
35
|
+
parsed = parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
|
36
|
+
assert parsed["foo"] == "bar"
|
37
|
+
assert parsed["nested"]["baz"] == 1
|
38
|
+
assert isinstance(parsed["nested"]["file"], SimpleUploadedFile)
|
39
|
+
assert isinstance(parsed["nested_list"][1]["file"], SimpleUploadedFile)
|
40
|
+
parsed["nested"]["file"].seek(0)
|
41
|
+
assert parsed["nested"]["file"].read() == file_content
|
42
|
+
parsed["nested_list"][1]["file"].seek(0)
|
43
|
+
assert parsed["nested_list"][1]["file"].read() == file_content
|
44
|
+
|
45
|
+
@pytest.mark.django_db
|
46
|
+
def test_multipart_json_parser_handles_parse_error(monkeypatch):
|
47
|
+
factory = RequestFactory()
|
48
|
+
request = factory.post("/", {})
|
49
|
+
request.upload_handlers = []
|
50
|
+
request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
|
51
|
+
|
52
|
+
class DummyParser:
|
53
|
+
def __init__(self, *a, **kw): pass
|
54
|
+
def parse(self):
|
55
|
+
from django.http.multipartparser import MultiPartParserError
|
56
|
+
raise MultiPartParserError("fail")
|
57
|
+
monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
|
58
|
+
parser = MultipartJsonParser()
|
59
|
+
from rest_framework.exceptions import ParseError
|
60
|
+
with pytest.raises(ParseError) as exc:
|
61
|
+
parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
|
62
|
+
assert "Multipart form parse error" in str(exc.value)
|
tests/test_sitemap.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
import pytest
|
2
|
+
from django.urls import reverse
|
3
|
+
from django.test import Client
|
4
|
+
from camomilla.models.page import Page
|
5
|
+
|
6
|
+
@pytest.mark.django_db
|
7
|
+
def test_sitemap_xml_contains_pages():
|
8
|
+
Page.objects.create(title="Test Page 1", permalink="test-page-1", status="PUB", autopermalink=False)
|
9
|
+
Page.objects.create(title="Test Page 2", permalink="test-page-2", status="PUB", autopermalink=False)
|
10
|
+
|
11
|
+
client = Client()
|
12
|
+
response = client.get(reverse('django.contrib.sitemaps.views.sitemap'))
|
13
|
+
assert response.status_code == 200
|
14
|
+
assert b"<urlset" in response.content
|
15
|
+
assert b"/test-page-1" in response.content
|
16
|
+
assert b"/test-page-2" in response.content
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from camomilla.utils.getters import safe_getter, pointed_getter, find_and_replace_dict
|
2
|
+
|
3
|
+
def test_safe_getter_dict():
|
4
|
+
d = {'a': 1}
|
5
|
+
assert safe_getter(d, 'a') == 1
|
6
|
+
assert safe_getter(d, 'b', 42) == 42
|
7
|
+
|
8
|
+
def test_safe_getter_object():
|
9
|
+
class Dummy:
|
10
|
+
foo = 'bar'
|
11
|
+
obj = Dummy()
|
12
|
+
assert safe_getter(obj, 'foo') == 'bar'
|
13
|
+
assert safe_getter(obj, 'baz', 99) == 99
|
14
|
+
|
15
|
+
def test_pointed_getter_dict():
|
16
|
+
d = {'a': {'b': {'c': 5}}}
|
17
|
+
assert pointed_getter(d, 'a.b.c') == 5
|
18
|
+
assert pointed_getter(d, 'a.b.x', 'nope') == 'nope'
|
19
|
+
|
20
|
+
def test_pointed_getter_object():
|
21
|
+
class Dummy:
|
22
|
+
pass
|
23
|
+
obj = Dummy()
|
24
|
+
obj.child = Dummy()
|
25
|
+
obj.child.value = 123
|
26
|
+
assert pointed_getter(obj, 'child.value') == 123
|
27
|
+
assert pointed_getter(obj, 'child.missing', 'default') == 'default'
|
28
|
+
|
29
|
+
def test_find_and_replace_dict_simple():
|
30
|
+
d = {'a': 1, 'b': 2}
|
31
|
+
def pred(key, value):
|
32
|
+
return value * 2
|
33
|
+
result = find_and_replace_dict(d, pred)
|
34
|
+
assert result == {'a': 2, 'b': 4}
|
35
|
+
|
36
|
+
def test_find_and_replace_dict_nested():
|
37
|
+
d = {'a': {'b': 2}, 'c': 3}
|
38
|
+
def pred(key, value):
|
39
|
+
return value if not isinstance(value, int) else value + 1
|
40
|
+
result = find_and_replace_dict(d, pred)
|
41
|
+
assert result == {'a': {'b': 3}, 'c': 4}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from camomilla.utils.normalization import dict_merge
|
2
|
+
|
3
|
+
def test_dict_merge_simple():
|
4
|
+
a = {'x': 1}
|
5
|
+
b = {'y': 2}
|
6
|
+
result = dict_merge(a.copy(), b)
|
7
|
+
assert result == {'x': 1, 'y': 2}
|
8
|
+
|
9
|
+
def test_dict_merge_overwrite():
|
10
|
+
a = {'x': 1}
|
11
|
+
b = {'x': 2}
|
12
|
+
result = dict_merge(a.copy(), b)
|
13
|
+
assert result == {'x': 2}
|
14
|
+
|
15
|
+
def test_dict_merge_nested():
|
16
|
+
a = {'x': {'y': 1}}
|
17
|
+
b = {'x': {'z': 2}}
|
18
|
+
result = dict_merge(a.copy(), b)
|
19
|
+
assert result == {'x': {'y': 1, 'z': 2}}
|
20
|
+
|
21
|
+
def test_dict_merge_nested_overwrite():
|
22
|
+
a = {'x': {'y': 1}}
|
23
|
+
b = {'x': {'y': 2}}
|
24
|
+
result = dict_merge(a.copy(), b)
|
25
|
+
assert result == {'x': {'y': 2}}
|
26
|
+
|
27
|
+
def test_dict_merge_empty():
|
28
|
+
a = {}
|
29
|
+
b = {'foo': 1}
|
30
|
+
result = dict_merge(a.copy(), b)
|
31
|
+
assert result == {'foo': 1}
|
32
|
+
|
33
|
+
def test_dict_merge_both_empty():
|
34
|
+
a = {}
|
35
|
+
b = {}
|
36
|
+
result = dict_merge(a.copy(), b)
|
37
|
+
assert result == {}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import pytest
|
2
|
+
from camomilla.utils import setters
|
3
|
+
|
4
|
+
def test_set_key_dict():
|
5
|
+
d = {'a': 1}
|
6
|
+
result = setters.set_key(d, 'b', 2)
|
7
|
+
assert result['b'] == 2
|
8
|
+
assert d['b'] == 2
|
9
|
+
|
10
|
+
def test_set_key_list():
|
11
|
+
l = [1, 2]
|
12
|
+
result = setters.set_key(l, 1, 3)
|
13
|
+
assert result[1] == 3
|
14
|
+
result = setters.set_key(l, 2, 4)
|
15
|
+
assert result[2] == 4
|
16
|
+
|
17
|
+
def test_set_key_object():
|
18
|
+
class Dummy:
|
19
|
+
pass
|
20
|
+
obj = Dummy()
|
21
|
+
setters.set_key(obj, 'foo', 'bar')
|
22
|
+
assert obj.foo == 'bar'
|
23
|
+
|
24
|
+
def test_get_key_list():
|
25
|
+
l = [10, 20]
|
26
|
+
assert setters.get_key(l, 1, 'x') == 20
|
27
|
+
assert setters.get_key(l, 5, 'x') == 'x'
|
28
|
+
|
29
|
+
def test_pointed_setter_dict():
|
30
|
+
d = {'a': {'b': 1}}
|
31
|
+
setters.pointed_setter(d, 'a.b', 2)
|
32
|
+
assert d['a']['b'] == 2
|
33
|
+
|
34
|
+
def test_pointed_setter_list():
|
35
|
+
l = [[0, 1], [2, 3]]
|
36
|
+
setters.pointed_setter(l, '1.1', 99)
|
37
|
+
assert l[1][1] == 99
|
38
|
+
|
39
|
+
def test_set_key_list_append():
|
40
|
+
l = [1]
|
41
|
+
# key out of range, should append
|
42
|
+
setters.set_key(l, 2, 42)
|
43
|
+
assert l == [1, 42]
|
44
|
+
|
45
|
+
def test_set_key_invalid_type():
|
46
|
+
class Dummy:
|
47
|
+
pass
|
48
|
+
obj = Dummy()
|
49
|
+
# Should set attribute if not dict or list
|
50
|
+
setters.set_key(obj, 'bar', 123)
|
51
|
+
assert obj.bar == 123
|
52
|
+
|
53
|
+
def test_get_key_invalid_index():
|
54
|
+
l = [1, 2]
|
55
|
+
# Out of range index returns default
|
56
|
+
assert setters.get_key(l, 10, 'default') == 'default'
|
57
|
+
|
58
|
+
def test_pointed_setter_new_path():
|
59
|
+
d = {}
|
60
|
+
# Should create nested dicts
|
61
|
+
setters.pointed_setter(d, 'foo.bar.baz', 7)
|
62
|
+
assert d['foo']['bar']['baz'] == 7
|
File without changes
|
{django_camomilla_cms-6.1.3.dist-info → django_camomilla_cms-6.2.0.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|