django-camomilla-cms 5.8.5__py2.py3-none-any.whl → 6.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- camomilla/__init__.py +8 -2
- camomilla/apps.py +9 -1
- camomilla/context_processors.py +6 -0
- camomilla/contrib/modeltranslation/__init__.py +0 -0
- camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
- camomilla/dynamic_pages_urls.py +33 -0
- camomilla/fields/__init__.py +13 -0
- camomilla/{fields.py → fields/json.py} +15 -18
- camomilla/management/commands/regenerate_thumbnails.py +0 -1
- camomilla/managers/__init__.py +3 -0
- camomilla/managers/pages.py +116 -0
- camomilla/model_api.py +86 -0
- camomilla/models/__init__.py +5 -6
- camomilla/models/article.py +26 -44
- camomilla/models/content.py +8 -15
- camomilla/models/media.py +70 -97
- camomilla/models/menu.py +106 -0
- camomilla/models/mixins/__init__.py +10 -48
- camomilla/models/page.py +521 -20
- camomilla/openapi/__init__.py +0 -0
- camomilla/openapi/schema.py +67 -0
- camomilla/parsers.py +0 -1
- camomilla/redirects.py +10 -0
- camomilla/serializers/__init__.py +2 -0
- camomilla/serializers/article.py +5 -10
- camomilla/serializers/base/__init__.py +21 -17
- camomilla/serializers/content_type.py +17 -0
- camomilla/serializers/fields/__init__.py +6 -20
- camomilla/serializers/fields/file.py +5 -0
- camomilla/serializers/fields/related.py +24 -4
- camomilla/serializers/media.py +6 -8
- camomilla/serializers/menu.py +17 -0
- camomilla/serializers/mixins/__init__.py +23 -187
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +57 -0
- camomilla/serializers/mixins/json.py +34 -0
- camomilla/serializers/mixins/language.py +32 -0
- camomilla/serializers/mixins/nesting.py +35 -0
- camomilla/serializers/mixins/optimize.py +91 -0
- camomilla/serializers/mixins/ordering.py +34 -0
- camomilla/serializers/mixins/page.py +58 -0
- camomilla/serializers/mixins/translation.py +103 -0
- camomilla/serializers/page.py +53 -4
- camomilla/serializers/user.py +5 -4
- camomilla/serializers/utils.py +38 -0
- camomilla/serializers/validators.py +51 -0
- camomilla/settings.py +118 -0
- camomilla/sitemap.py +30 -0
- camomilla/storages/__init__.py +4 -0
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +71 -0
- camomilla/{storages.py → storages/overwrite.py} +2 -2
- camomilla/templates/admin/camomilla/page/change_form.html +10 -0
- camomilla/templates/defaults/articles/default.html +7 -0
- camomilla/templates/defaults/base.html +170 -0
- camomilla/templates/defaults/pages/default.html +3 -0
- camomilla/templates/defaults/parts/langswitch.html +83 -0
- camomilla/templates/defaults/parts/menu.html +15 -0
- camomilla/templates_context/__init__.py +0 -0
- camomilla/templates_context/autodiscover.py +51 -0
- camomilla/templates_context/rendering.py +89 -0
- camomilla/templatetags/camomilla_filters.py +6 -5
- camomilla/templatetags/menus.py +37 -0
- camomilla/templatetags/model_extras.py +77 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin/__init__.py +99 -0
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +38 -0
- camomilla/theme/static/admin/css/responsive.css +5 -1021
- camomilla/theme/static/admin/img/favicon.ico +0 -0
- camomilla/theme/static/admin/img/logo.svg +31 -0
- camomilla/theme/templates/admin/base.html +7 -0
- camomilla/theme/templates/rosetta/base.html +196 -0
- camomilla/translation.py +61 -0
- camomilla/urls.py +38 -17
- camomilla/utils/__init__.py +4 -0
- camomilla/utils/getters.py +27 -0
- camomilla/utils/normalization.py +7 -0
- camomilla/utils/query_parser.py +167 -0
- camomilla/{utils.py → utils/seo.py} +13 -15
- camomilla/utils/setters.py +37 -0
- camomilla/utils/templates.py +32 -0
- camomilla/utils/translation.py +114 -0
- camomilla/views/__init__.py +1 -1
- camomilla/views/articles.py +5 -7
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/contents.py +6 -11
- camomilla/views/decorators.py +26 -0
- camomilla/views/medias.py +24 -19
- camomilla/views/menus.py +81 -0
- camomilla/views/mixins/__init__.py +17 -73
- camomilla/views/mixins/bulk_actions.py +22 -0
- camomilla/views/mixins/language.py +33 -0
- camomilla/views/mixins/optimize.py +18 -0
- camomilla/views/mixins/ordering.py +2 -2
- camomilla/views/mixins/pagination.py +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +28 -6
- camomilla/views/tags.py +5 -6
- camomilla/views/users.py +7 -12
- django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
- django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +14 -0
- tests/test_api.py +22 -39
- tests/test_camomilla_filters.py +11 -13
- tests/test_media.py +152 -0
- tests/test_menu.py +112 -0
- tests/test_model_api.py +113 -0
- tests/test_model_api_permissions.py +44 -0
- tests/test_model_api_register.py +355 -0
- tests/test_pages.py +351 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +149 -0
- tests/test_utils.py +64 -64
- tests/utils/__init__.py +0 -0
- tests/utils/api.py +28 -0
- tests/utils/media.py +10 -0
- camomilla/admin.py +0 -98
- camomilla/migrations/0001_initial.py +0 -577
- camomilla/migrations/0002_auto_20200214_1127.py +0 -33
- camomilla/migrations/0003_auto_20210130_1610.py +0 -30
- camomilla/migrations/0004_auto_20210511_0937.py +0 -25
- camomilla/migrations/0005_media_image_props.py +0 -19
- camomilla/migrations/0006_auto_20220103_1845.py +0 -35
- camomilla/migrations/0007_auto_20220211_1622.py +0 -18
- camomilla/migrations/0008_auto_20220309_1616.py +0 -60
- camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
- camomilla/migrations/0010_auto_20220802_1406.py +0 -83
- camomilla/migrations/0011_auto_20220902_1000.py +0 -15
- camomilla/models/category.py +0 -25
- camomilla/models/tag.py +0 -19
- camomilla/theme/static/admin/img/logo.png +0 -0
- camomilla/theme/templates/admin/base_site.html +0 -18
- camomilla/views/categories.py +0 -13
- django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
- django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
- tests/urls.py +0 -21
- /camomilla/{migrations → contrib}/__init__.py +0 -0
- /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
from typing import Sequence
|
2
|
+
from .getters import pointed_getter
|
3
|
+
|
4
|
+
|
5
|
+
def set_key(data, key, val):
|
6
|
+
if isinstance(data, Sequence):
|
7
|
+
key = int(key)
|
8
|
+
if key < len(data):
|
9
|
+
data[key] = val
|
10
|
+
else:
|
11
|
+
data.append(val)
|
12
|
+
return data
|
13
|
+
elif isinstance(data, dict):
|
14
|
+
data[key] = val
|
15
|
+
else:
|
16
|
+
setattr(data, key, val)
|
17
|
+
return data
|
18
|
+
|
19
|
+
|
20
|
+
def get_key(data, key, default):
|
21
|
+
if isinstance(data, Sequence):
|
22
|
+
try:
|
23
|
+
return data[int(key)]
|
24
|
+
except IndexError:
|
25
|
+
return default
|
26
|
+
return pointed_getter(data, key, default)
|
27
|
+
|
28
|
+
|
29
|
+
def pointed_setter(data, path, value):
|
30
|
+
path = path.split(".")
|
31
|
+
key = path.pop(0)
|
32
|
+
if not len(path):
|
33
|
+
return set_key(data, key, value)
|
34
|
+
default = [] if path[0].isdigit() else {}
|
35
|
+
return set_key(
|
36
|
+
data, key, pointed_setter(get_key(data, key, default), ".".join(path), value)
|
37
|
+
)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Sequence
|
3
|
+
|
4
|
+
from django import template as django_template
|
5
|
+
from os.path import relpath
|
6
|
+
from camomilla.settings import REGISTERED_TEMPLATES_APPS
|
7
|
+
|
8
|
+
|
9
|
+
def get_all_templates_files() -> Sequence[str]:
|
10
|
+
files = []
|
11
|
+
|
12
|
+
for engine in django_template.loader.engines.all():
|
13
|
+
|
14
|
+
if REGISTERED_TEMPLATES_APPS:
|
15
|
+
dirs = [
|
16
|
+
d
|
17
|
+
for d in engine.template_dirs
|
18
|
+
if any(app in str(d) for app in REGISTERED_TEMPLATES_APPS)
|
19
|
+
]
|
20
|
+
else:
|
21
|
+
# Exclude pip installed site package template dirs
|
22
|
+
dirs = [
|
23
|
+
d
|
24
|
+
for d in engine.template_dirs
|
25
|
+
if "site-packages" not in str(d) or "camomilla" in str(d)
|
26
|
+
]
|
27
|
+
|
28
|
+
for d in dirs:
|
29
|
+
base = Path(d)
|
30
|
+
files.extend(relpath(f, d) for f in base.rglob("*.html"))
|
31
|
+
|
32
|
+
return files
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Any, Sequence, Iterator, Union, List
|
3
|
+
|
4
|
+
from django.db.models import Model, Q
|
5
|
+
from django.utils.translation.trans_real import activate, get_language
|
6
|
+
from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
|
7
|
+
from modeltranslation.utils import build_localized_fieldname
|
8
|
+
from camomilla.settings import BASE_URL
|
9
|
+
from django.http import QueryDict
|
10
|
+
|
11
|
+
|
12
|
+
def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
|
13
|
+
old = get_language()
|
14
|
+
for language in languages:
|
15
|
+
activate(language)
|
16
|
+
yield language
|
17
|
+
activate(old)
|
18
|
+
|
19
|
+
|
20
|
+
def set_nofallbacks(instance: Model, attr: str, value: Any, **kwargs) -> None:
|
21
|
+
language = kwargs.pop("language", get_language())
|
22
|
+
local_fieldname = build_localized_fieldname(attr, language)
|
23
|
+
if hasattr(instance, local_fieldname):
|
24
|
+
attr = local_fieldname
|
25
|
+
return setattr(instance, attr, value)
|
26
|
+
|
27
|
+
|
28
|
+
def get_nofallbacks(instance: Model, attr: str, *args, **kwargs) -> Any:
|
29
|
+
language = kwargs.pop("language", get_language())
|
30
|
+
local_fieldname = build_localized_fieldname(attr, language)
|
31
|
+
if hasattr(instance, local_fieldname):
|
32
|
+
attr = local_fieldname
|
33
|
+
return getattr(instance, attr, *args, **kwargs)
|
34
|
+
|
35
|
+
|
36
|
+
def url_lang_decompose(url):
|
37
|
+
if BASE_URL and url.startswith(BASE_URL):
|
38
|
+
url = url[len(BASE_URL) :]
|
39
|
+
data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
|
40
|
+
result = re.match(rf"^/?({'|'.join(AVAILABLE_LANGUAGES)})?/(.*)", url) # noqa: W605
|
41
|
+
groups = result and result.groups()
|
42
|
+
if groups and len(groups) == 2:
|
43
|
+
data["language"] = groups[0]
|
44
|
+
data["permalink"] = "/%s" % groups[1]
|
45
|
+
return data
|
46
|
+
|
47
|
+
|
48
|
+
def get_field_translations(instance: Model, field_name: str, *args, **kwargs):
|
49
|
+
return {
|
50
|
+
lang: get_nofallbacks(instance, field_name, language=lang, *args, **kwargs)
|
51
|
+
for lang in AVAILABLE_LANGUAGES
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
def lang_fallback_query(**kwargs):
|
56
|
+
current_lang = get_language()
|
57
|
+
query = Q()
|
58
|
+
for lang in AVAILABLE_LANGUAGES:
|
59
|
+
query |= Q(**{f"{key}_{lang}": value for key, value in kwargs.items()})
|
60
|
+
if current_lang:
|
61
|
+
query = query & Q(
|
62
|
+
**{f"{key}_{current_lang}__isnull": True for key in kwargs.keys()}
|
63
|
+
)
|
64
|
+
query |= Q(**{f"{key}_{current_lang}": value for key, value in kwargs.items()})
|
65
|
+
return query
|
66
|
+
|
67
|
+
|
68
|
+
def is_translatable(model: Model) -> bool:
|
69
|
+
from modeltranslation.translator import translator
|
70
|
+
|
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/__init__.py
CHANGED
camomilla/views/articles.py
CHANGED
@@ -1,13 +1,11 @@
|
|
1
|
-
from .
|
2
|
-
from .
|
3
|
-
|
4
|
-
from
|
5
|
-
from
|
6
|
-
from ..permissions import CamomillaBasePermissions
|
1
|
+
from camomilla.models import Article
|
2
|
+
from camomilla.permissions import CamomillaBasePermissions
|
3
|
+
from camomilla.serializers import ArticleSerializer
|
4
|
+
from camomilla.views.base import BaseModelViewset
|
5
|
+
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
7
6
|
|
8
7
|
|
9
8
|
class ArticleViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
10
|
-
|
11
9
|
queryset = Article.objects.all()
|
12
10
|
serializer_class = ArticleSerializer
|
13
11
|
permission_classes = (CamomillaBasePermissions,)
|
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
|
-
|
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/contents.py
CHANGED
@@ -1,18 +1,16 @@
|
|
1
|
-
from .base import BaseModelViewset
|
2
1
|
import json
|
3
2
|
|
4
3
|
from django.http import JsonResponse
|
5
|
-
from django.utils.translation import get_language
|
6
4
|
from rest_framework.decorators import action
|
7
5
|
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from .
|
6
|
+
from camomilla.models import Content
|
7
|
+
from camomilla.permissions import CamomillaBasePermissions
|
8
|
+
from camomilla.serializers import ContentSerializer
|
9
|
+
from camomilla.views.base import BaseModelViewset
|
10
|
+
from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
12
11
|
|
13
12
|
|
14
13
|
class ContentViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
15
|
-
|
16
14
|
queryset = Content.objects.all()
|
17
15
|
serializer_class = ContentSerializer
|
18
16
|
model = Content
|
@@ -20,13 +18,10 @@ class ContentViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
|
20
18
|
|
21
19
|
@action(detail=True, methods=["get", "patch"])
|
22
20
|
def djsuperadmin(self, request, pk):
|
23
|
-
# content, _ = Content.objects.get_or_create(pk=pk)
|
24
|
-
# content.translate(get_language())
|
25
21
|
try:
|
26
|
-
content = Content.objects.
|
22
|
+
content = Content.objects.get(pk=pk)
|
27
23
|
except Content.DoesNotExist:
|
28
24
|
content, _ = Content.objects.get_or_create(pk=pk)
|
29
|
-
content.translate(get_language())
|
30
25
|
if request.method == "GET":
|
31
26
|
return JsonResponse({"content": content.content})
|
32
27
|
if request.method == "PATCH":
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import functools
|
2
|
+
from django.utils.translation import activate
|
3
|
+
from django.conf import settings
|
4
|
+
|
5
|
+
|
6
|
+
def active_lang(*args, **kwargs):
|
7
|
+
def decorator(func):
|
8
|
+
@functools.wraps(func)
|
9
|
+
def wrapped_func(*args, **kwargs):
|
10
|
+
if len(args) and hasattr(args[0], "request"):
|
11
|
+
request = args[0].request
|
12
|
+
else:
|
13
|
+
request = args[0] if len(args) else kwargs.get("request", None)
|
14
|
+
lang = settings.LANGUAGE_CODE
|
15
|
+
if request and hasattr(request, "GET"):
|
16
|
+
lang = request.GET.get("lang", request.GET.get("language", lang))
|
17
|
+
if request and hasattr(request, "data"):
|
18
|
+
lang = request.data.pop("lang", request.data.pop("language", lang))
|
19
|
+
if lang and lang in [lng[0] for lng in settings.LANGUAGES]:
|
20
|
+
activate(lang)
|
21
|
+
request.LANGUAGE_CODE = lang
|
22
|
+
return func(*args, **kwargs)
|
23
|
+
|
24
|
+
return wrapped_func
|
25
|
+
|
26
|
+
return decorator
|
camomilla/views/medias.py
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
-
from .base import BaseModelViewset
|
2
|
-
from .mixins import BulkDeleteMixin, GetUserLanguageMixin, TrigramSearchMixin
|
3
|
-
from ..parsers import MultipartJsonParser
|
4
|
-
from ..permissions import CamomillaBasePermissions
|
5
|
-
|
6
|
-
|
7
|
-
from rest_framework.response import Response
|
8
1
|
from rest_framework.parsers import JSONParser
|
2
|
+
from rest_framework.response import Response
|
9
3
|
|
10
|
-
from
|
11
|
-
from
|
4
|
+
from camomilla.models import Media, MediaFolder
|
5
|
+
from camomilla.parsers import MultipartJsonParser
|
6
|
+
from camomilla.permissions import CamomillaBasePermissions
|
7
|
+
from camomilla.serializers import (
|
8
|
+
MediaFolderSerializer,
|
9
|
+
MediaListSerializer,
|
10
|
+
MediaSerializer,
|
11
|
+
)
|
12
|
+
from camomilla.views.base import BaseModelViewset
|
13
|
+
from camomilla.views.mixins import (
|
14
|
+
BulkDeleteMixin,
|
15
|
+
GetUserLanguageMixin,
|
16
|
+
TrigramSearchMixin,
|
17
|
+
)
|
12
18
|
|
13
19
|
|
14
20
|
class ParseMimeMixin(object):
|
@@ -29,7 +35,7 @@ class MediaFolderViewSet(
|
|
29
35
|
serializer_class = MediaFolderSerializer
|
30
36
|
permission_classes = (CamomillaBasePermissions,)
|
31
37
|
items_per_page = 18
|
32
|
-
search_fields = ["
|
38
|
+
search_fields = ["title", "alt_text", "file"]
|
33
39
|
|
34
40
|
def get_queryset(self):
|
35
41
|
return self.model.objects.all()
|
@@ -38,24 +44,24 @@ class MediaFolderViewSet(
|
|
38
44
|
return {**super().get_serializer_context(), "action": "list"}
|
39
45
|
|
40
46
|
def get_mixed_response(self, request, *args, **kwargs):
|
41
|
-
|
47
|
+
search = self.request.GET.get("search", None)
|
48
|
+
all = self.request.GET.get("all", "false").lower() == "true"
|
49
|
+
updir = None if all else kwargs.get("pk", None)
|
50
|
+
if not search and all:
|
51
|
+
self.search_fields = []
|
42
52
|
|
43
53
|
parent_folder = MediaFolderSerializer(
|
44
54
|
self.model.objects.filter(pk=updir).first()
|
45
55
|
).data
|
46
|
-
folder_queryset = self.model.objects.
|
47
|
-
media_queryset = (
|
48
|
-
Media.objects.language(self.active_language).fallbacks()
|
49
|
-
if request.GET.get("search", None)
|
50
|
-
else Media.objects.all()
|
51
|
-
).filter(folder__pk=updir)
|
56
|
+
folder_queryset = self.model.objects.filter(updir__pk=updir)
|
57
|
+
media_queryset = Media.objects.filter(**({} if all else {"folder__pk": updir}))
|
52
58
|
|
53
59
|
folder_data = MediaFolderSerializer(
|
54
60
|
folder_queryset, many=True, context={"request": request}
|
55
61
|
).data
|
56
62
|
media_data = self.format_output(
|
57
63
|
*self.handle_pagination_stack(media_queryset),
|
58
|
-
SerializerClass=MediaListSerializer
|
64
|
+
SerializerClass=MediaListSerializer,
|
59
65
|
)
|
60
66
|
return {
|
61
67
|
"folders": folder_data,
|
@@ -77,7 +83,6 @@ class MediaViewSet(
|
|
77
83
|
TrigramSearchMixin,
|
78
84
|
BaseModelViewset,
|
79
85
|
):
|
80
|
-
|
81
86
|
queryset = Media.objects.all()
|
82
87
|
serializer_class = MediaSerializer
|
83
88
|
permission_classes = (CamomillaBasePermissions,)
|
camomilla/views/menus.py
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
2
|
+
from django.db.models import Q
|
3
|
+
from django.http import Http404
|
4
|
+
from django.shortcuts import get_object_or_404
|
5
|
+
from rest_framework.decorators import action
|
6
|
+
from rest_framework.response import Response
|
7
|
+
|
8
|
+
from camomilla.models import AbstractPage, Menu
|
9
|
+
from camomilla.models.page import UrlNode
|
10
|
+
from camomilla.permissions import CamomillaBasePermissions
|
11
|
+
from camomilla.serializers import ContentTypeSerializer, MenuSerializer
|
12
|
+
from camomilla.serializers.page import BasicUrlNodeSerializer
|
13
|
+
from camomilla.views.base import BaseModelViewset
|
14
|
+
from camomilla.views.decorators import active_lang
|
15
|
+
|
16
|
+
|
17
|
+
class MenuViewSet(BaseModelViewset):
|
18
|
+
queryset = Menu.objects.all()
|
19
|
+
serializer_class = MenuSerializer
|
20
|
+
permission_classes = (CamomillaBasePermissions,)
|
21
|
+
model = Menu
|
22
|
+
|
23
|
+
lookup_field = "key"
|
24
|
+
|
25
|
+
def get_object(self):
|
26
|
+
queryset = self.filter_queryset(self.get_queryset())
|
27
|
+
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
28
|
+
assert lookup_url_kwarg in self.kwargs, (
|
29
|
+
"Expected view %s to be called with a URL keyword argument "
|
30
|
+
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
31
|
+
"attribute on the view correctly."
|
32
|
+
% (self.__class__.__name__, lookup_url_kwarg)
|
33
|
+
)
|
34
|
+
filter = Q(**{self.lookup_field: self.kwargs[lookup_url_kwarg]})
|
35
|
+
if (
|
36
|
+
isinstance(self.kwargs[lookup_url_kwarg], str)
|
37
|
+
and self.kwargs[lookup_url_kwarg].isnumeric()
|
38
|
+
or isinstance(self.kwargs[lookup_url_kwarg], int)
|
39
|
+
):
|
40
|
+
filter |= Q(pk=self.kwargs[lookup_url_kwarg])
|
41
|
+
|
42
|
+
obj = get_object_or_404(queryset, filter)
|
43
|
+
# May raise a permission denied
|
44
|
+
self.check_object_permissions(self.request, obj)
|
45
|
+
|
46
|
+
return obj
|
47
|
+
|
48
|
+
@action(detail=False, methods=["get"], url_path="page_types")
|
49
|
+
def page_types(self, request, *args, **kwargs):
|
50
|
+
return Response(
|
51
|
+
ContentTypeSerializer(
|
52
|
+
[
|
53
|
+
model
|
54
|
+
for model in ContentType.objects.all()
|
55
|
+
if model.model_class()
|
56
|
+
and issubclass(model.model_class(), AbstractPage)
|
57
|
+
],
|
58
|
+
many=True,
|
59
|
+
).data
|
60
|
+
)
|
61
|
+
|
62
|
+
@action(detail=False, methods=["get"], url_path=r"page_types/(?P<content_id>\w+)")
|
63
|
+
def page_type_instances(self, request, content_id, *args, **kwargs):
|
64
|
+
content_type = ContentType.objects.filter(pk=content_id).first()
|
65
|
+
if not content_type or not issubclass(content_type.model_class(), AbstractPage):
|
66
|
+
raise Http404("No object matches the given query.")
|
67
|
+
return Response(
|
68
|
+
[
|
69
|
+
{"id": obj.pk, "name": str(obj), "url_node_id": obj.url_node.pk}
|
70
|
+
for obj in content_type.model_class().objects.exclude(
|
71
|
+
url_node__isnull=True
|
72
|
+
)
|
73
|
+
]
|
74
|
+
)
|
75
|
+
|
76
|
+
@active_lang()
|
77
|
+
@action(detail=False, methods=["get"], url_path="search_urlnode")
|
78
|
+
def search_urlnode(self, request, *args, **kwargs):
|
79
|
+
url_node = request.GET.get("q", "")
|
80
|
+
qs = UrlNode.objects.filter(permalink__icontains=url_node).order_by("permalink")
|
81
|
+
return Response(BasicUrlNodeSerializer(qs, many=True).data)
|
@@ -1,73 +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
|
-
return (
|
34
|
-
self.model.objects.language(self.active_language).fallbacks().all()
|
35
|
-
if self.language_fallbacks
|
36
|
-
else self.model.objects.language(self.active_language).all()
|
37
|
-
)
|
38
|
-
|
39
|
-
|
40
|
-
class OptimViewMixin:
|
41
|
-
def get_serializer_class(self):
|
42
|
-
if hasattr(self, "action_serializers"):
|
43
|
-
if self.action in self.action_serializers:
|
44
|
-
return self.action_serializers[self.action]
|
45
|
-
return super().get_serializer_class()
|
46
|
-
|
47
|
-
def get_serializer_context(self):
|
48
|
-
return {"request": self.request, "action": self.action}
|
49
|
-
|
50
|
-
def get_queryset(self):
|
51
|
-
queryset = super().get_queryset()
|
52
|
-
serializer = self.get_serializer_class()
|
53
|
-
if hasattr(serializer, "setup_eager_loading"):
|
54
|
-
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
55
|
-
return queryset
|
56
|
-
|
57
|
-
|
58
|
-
class BulkDeleteMixin(object):
|
59
|
-
@action(
|
60
|
-
detail=False, methods=["post"], permission_classes=(CamomillaBasePermissions,)
|
61
|
-
)
|
62
|
-
def bulk_delete(self, request):
|
63
|
-
try:
|
64
|
-
self.model.objects.filter(pk__in=request.data).delete()
|
65
|
-
return Response(
|
66
|
-
{"detail": "Eliminazione multipla andata a buon fine"},
|
67
|
-
status=status.HTTP_200_OK,
|
68
|
-
)
|
69
|
-
except Exception:
|
70
|
-
return Response(
|
71
|
-
{"detail": "Eliminazione multipla non riuscita"},
|
72
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
73
|
-
)
|
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
|
@@ -1,12 +1,12 @@
|
|
1
1
|
from rest_framework.decorators import action
|
2
2
|
from rest_framework.response import Response
|
3
|
-
from
|
3
|
+
from camomilla.permissions import CamomillaBasePermissions
|
4
4
|
from django.core.exceptions import ImproperlyConfigured
|
5
5
|
from django.db import transaction, router
|
6
6
|
from django.db.models.signals import post_save, pre_save
|
7
7
|
from itertools import chain
|
8
8
|
from django.db.models.expressions import F
|
9
|
-
from
|
9
|
+
from camomilla.fields import ORDERING_ACCEPTED_FIELDS
|
10
10
|
|
11
11
|
|
12
12
|
class OrderingMixin:
|