django-camomilla-cms 6.0.0b2__py2.py3-none-any.whl → 6.0.0b4__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/apps.py +3 -0
- camomilla/contrib/modeltranslation/hvad_migration.py +1 -2
- camomilla/contrib/rest_framework/serializer.py +31 -1
- camomilla/dynamic_pages_urls.py +7 -1
- camomilla/fields/json.py +12 -9
- camomilla/management/commands/regenerate_thumbnails.py +0 -1
- camomilla/model_api.py +6 -4
- camomilla/models/__init__.py +5 -5
- camomilla/models/article.py +0 -1
- camomilla/models/media.py +1 -2
- camomilla/models/menu.py +44 -42
- camomilla/models/mixins/__init__.py +0 -1
- camomilla/models/page.py +66 -32
- camomilla/openapi/schema.py +27 -0
- camomilla/parsers.py +0 -1
- camomilla/serializers/fields/json.py +7 -75
- camomilla/serializers/mixins/__init__.py +16 -2
- camomilla/serializers/page.py +47 -0
- camomilla/serializers/user.py +2 -3
- camomilla/serializers/utils.py +22 -17
- camomilla/settings.py +21 -1
- camomilla/storages/optimize.py +1 -1
- camomilla/structured/__init__.py +90 -75
- camomilla/structured/cache.py +193 -0
- camomilla/structured/fields.py +132 -275
- camomilla/structured/models.py +45 -138
- camomilla/structured/utils.py +114 -0
- camomilla/templatetags/camomilla_filters.py +0 -1
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin.py +96 -0
- camomilla/theme/apps.py +12 -1
- camomilla/translation.py +4 -2
- camomilla/urls.py +13 -6
- camomilla/utils/__init__.py +1 -1
- camomilla/utils/getters.py +11 -1
- camomilla/utils/templates.py +2 -2
- camomilla/utils/translation.py +9 -6
- camomilla/views/__init__.py +1 -1
- camomilla/views/articles.py +0 -1
- camomilla/views/contents.py +0 -1
- camomilla/views/decorators.py +26 -0
- camomilla/views/medias.py +1 -2
- camomilla/views/menus.py +45 -1
- camomilla/views/pages.py +13 -1
- camomilla/views/tags.py +0 -1
- camomilla/views/users.py +0 -2
- {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/METADATA +4 -3
- {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/RECORD +53 -49
- tests/test_api.py +1 -0
- {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/WHEEL +0 -0
- {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/top_level.txt +0 -0
camomilla/structured/models.py
CHANGED
@@ -1,140 +1,47 @@
|
|
1
|
-
from
|
2
|
-
from
|
3
|
-
|
4
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
from
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
def __init__(self, **kwargs):
|
44
|
-
self._cache = _Cache(self)
|
45
|
-
for _, field in self.iterate_over_fields():
|
46
|
-
field.bind(self)
|
47
|
-
super(Model, self).__init__(**kwargs)
|
48
|
-
|
49
|
-
def populate(self, **kwargs):
|
50
|
-
relations = self.prepopulate(**kwargs)
|
51
|
-
self.prefetch_related(kwargs)
|
52
|
-
super(Model, self).populate(**relations)
|
53
|
-
|
54
|
-
def prepopulate(self, **kwargs):
|
55
|
-
relations = {}
|
56
|
-
for _, struct_name, field in self.iterate_with_name():
|
57
|
-
if struct_name in kwargs and isinstance(
|
58
|
-
field, (ForeignKey, ForeignKeyList, EmbeddedField, ListField)
|
59
|
-
):
|
60
|
-
relations[struct_name] = kwargs.pop(struct_name)
|
61
|
-
super(Model, self).populate(**kwargs)
|
62
|
-
return relations
|
63
|
-
|
64
|
-
def bind(self, parent):
|
65
|
-
self._cache.parent = parent
|
66
|
-
|
67
|
-
@classmethod
|
68
|
-
def to_db_transform(cls, data):
|
69
|
-
return data
|
70
|
-
|
1
|
+
from inspect import isclass
|
2
|
+
from typing import Any, Dict, Tuple, get_origin
|
3
|
+
|
4
|
+
import pydantic._internal._model_construction
|
5
|
+
from django.db.models import Model as DjangoModel
|
6
|
+
from pydantic import BaseModel as PyDBaseModel
|
7
|
+
from pydantic import Field, model_validator
|
8
|
+
from typing_extensions import Annotated
|
9
|
+
|
10
|
+
from .fields import ForeignKey, QuerySet
|
11
|
+
from .utils import get_type, map_method_aliases
|
12
|
+
|
13
|
+
|
14
|
+
class BaseModelMeta(pydantic._internal._model_construction.ModelMetaclass):
|
15
|
+
def __new__(
|
16
|
+
mcs, name: str, bases: Tuple[type], namespaces: Dict[str, Any], **kwargs
|
17
|
+
):
|
18
|
+
annotations: dict = namespaces.get("__annotations__", {})
|
19
|
+
for base in bases:
|
20
|
+
for base_ in base.__mro__:
|
21
|
+
if base_ is PyDBaseModel:
|
22
|
+
break
|
23
|
+
annotations.update(base_.__annotations__)
|
24
|
+
|
25
|
+
for field in annotations:
|
26
|
+
annotation = annotations[field]
|
27
|
+
origin = get_origin(annotation)
|
28
|
+
if isclass(annotation) and issubclass(annotation, DjangoModel):
|
29
|
+
annotations[field] = ForeignKey[annotation]
|
30
|
+
elif isclass(origin) and issubclass(origin, QuerySet):
|
31
|
+
annotations[field] = Annotated[
|
32
|
+
annotation,
|
33
|
+
Field(default_factory=get_type(annotation)._default_manager.none),
|
34
|
+
]
|
35
|
+
namespaces["__annotations__"] = annotations
|
36
|
+
return map_method_aliases(
|
37
|
+
super().__new__(mcs, name, bases, namespaces, **kwargs)
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
class BaseModel(PyDBaseModel, metaclass=BaseModelMeta):
|
42
|
+
@model_validator(mode="before")
|
71
43
|
@classmethod
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
@classmethod
|
76
|
-
def get_all_relateds(cls, struct):
|
77
|
-
relateds = defaultdict(set)
|
78
|
-
for _, struct_name, field in cls.iterate_with_name():
|
79
|
-
if isinstance(field, ForeignKey):
|
80
|
-
value = struct.get(struct_name, None)
|
81
|
-
model = getattr(field, "model", None)
|
82
|
-
if value:
|
83
|
-
relateds[model].add(value)
|
84
|
-
elif isinstance(field, ForeignKeyList):
|
85
|
-
value = pointed_getter(struct, struct_name, [])
|
86
|
-
if isinstance(value, list):
|
87
|
-
model = getattr(field, "inner_model", None)
|
88
|
-
relateds[model].update([i for i in value if i])
|
89
|
-
elif isinstance(field, ListField):
|
90
|
-
values = pointed_getter(struct, struct_name, [])
|
91
|
-
if isinstance(values, list):
|
92
|
-
for val in values:
|
93
|
-
if isinstance(val, Model):
|
94
|
-
main_type = val.__class__
|
95
|
-
val = val.to_struct()
|
96
|
-
else:
|
97
|
-
main_type = field._get_main_type()
|
98
|
-
if isinstance(main_type, _LazyType):
|
99
|
-
main_type = main_type.evaluate(cls)
|
100
|
-
if issubclass(main_type, Model):
|
101
|
-
for model, pks in main_type.get_all_relateds(val).items():
|
102
|
-
relateds[model].update(pks)
|
103
|
-
elif isinstance(field, EmbeddedField):
|
104
|
-
value = struct.get(struct_name, None)
|
105
|
-
if isinstance(value, Model):
|
106
|
-
value = value.to_struct()
|
107
|
-
if not isinstance(value, dict):
|
108
|
-
continue
|
109
|
-
child_relateds = field._get_embed_type().get_all_relateds(value)
|
110
|
-
for model, pks in child_relateds.items():
|
111
|
-
relateds[model].update(pks)
|
112
|
-
return relateds
|
113
|
-
|
114
|
-
def prefetch_related(self, struct):
|
115
|
-
if struct.keys() and self._cache.is_root:
|
116
|
-
relateds = self.get_all_relateds(struct)
|
117
|
-
for model, pks in relateds.items():
|
118
|
-
self._cache.prefetched_data[model] = (
|
119
|
-
{obj.pk: obj for obj in build_model_cache(model, pks)}
|
120
|
-
if len(pks) > 0
|
121
|
-
else {}
|
122
|
-
)
|
123
|
-
|
124
|
-
def get_prefetched_data(self):
|
125
|
-
return self._cache.get_prefetched_data()
|
126
|
-
|
44
|
+
def build_cache(cls, data: Any) -> Any:
|
45
|
+
from camomilla.structured.cache import CacheBuilder
|
127
46
|
|
128
|
-
|
129
|
-
models = []
|
130
|
-
pks = []
|
131
|
-
for value in values:
|
132
|
-
if isinstance(value, model):
|
133
|
-
models.append(value)
|
134
|
-
else:
|
135
|
-
pks.append(value)
|
136
|
-
models_pks = [m.pk for m in models]
|
137
|
-
pks = [pk for pk in pks if pk not in models_pks]
|
138
|
-
if len(pks):
|
139
|
-
models += list(model.objects.filter(pk__in=pks))
|
140
|
-
return models
|
47
|
+
return CacheBuilder.from_model(cls).inject_cache(data)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
from typing import Any, Generic, Sequence
|
2
|
+
from typing_extensions import TypeVar, get_args
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
4
|
+
from camomilla.utils.getters import pointed_getter
|
5
|
+
|
6
|
+
|
7
|
+
T = TypeVar("T")
|
8
|
+
|
9
|
+
|
10
|
+
class _LazyType:
|
11
|
+
def __init__(self, path):
|
12
|
+
self.path = path
|
13
|
+
|
14
|
+
def evaluate(self, base_cls):
|
15
|
+
module, type_name = self._evaluate_path(self.path, base_cls)
|
16
|
+
return self._import(module, type_name)
|
17
|
+
|
18
|
+
def _evaluate_path(self, relative_path, base_cls):
|
19
|
+
base_module = base_cls.__module__
|
20
|
+
|
21
|
+
modules = self._get_modules(relative_path, base_module)
|
22
|
+
|
23
|
+
type_name = modules.pop()
|
24
|
+
module = ".".join(modules)
|
25
|
+
if not module:
|
26
|
+
module = base_module
|
27
|
+
return module, type_name
|
28
|
+
|
29
|
+
def _get_modules(self, relative_path, base_module):
|
30
|
+
canonical_path = relative_path.lstrip(".")
|
31
|
+
canonical_modules = canonical_path.split(".")
|
32
|
+
|
33
|
+
if not relative_path.startswith("."):
|
34
|
+
return canonical_modules
|
35
|
+
|
36
|
+
parents_amount = len(relative_path) - len(canonical_path)
|
37
|
+
parent_modules = base_module.split(".")
|
38
|
+
parents_amount = max(0, parents_amount - 1)
|
39
|
+
if parents_amount > len(parent_modules):
|
40
|
+
raise ValueError(f"Can't evaluate path '{relative_path}'")
|
41
|
+
return parent_modules[: parents_amount * -1] + canonical_modules
|
42
|
+
|
43
|
+
def _import(self, module_name, type_name):
|
44
|
+
module = __import__(module_name, fromlist=[type_name])
|
45
|
+
try:
|
46
|
+
return getattr(module, type_name)
|
47
|
+
except AttributeError:
|
48
|
+
raise ValueError(f"Can't find type '{module_name}.{type_name}'.")
|
49
|
+
|
50
|
+
|
51
|
+
def get_type(source: Generic[T], raise_exception=True) -> T:
|
52
|
+
try:
|
53
|
+
return get_args(source)[0]
|
54
|
+
except IndexError:
|
55
|
+
if raise_exception:
|
56
|
+
raise ImproperlyConfigured(
|
57
|
+
"Must provide a Model class for ForeignKey fields."
|
58
|
+
)
|
59
|
+
else:
|
60
|
+
return None
|
61
|
+
|
62
|
+
|
63
|
+
def get_type_eval(source: Generic[T], model: Any, raise_exception=True) -> T:
|
64
|
+
type = get_type(source, raise_exception)
|
65
|
+
if isinstance(type, str):
|
66
|
+
return _LazyType(type).evaluate(model)
|
67
|
+
|
68
|
+
|
69
|
+
def set_key(data, key, val):
|
70
|
+
if isinstance(data, Sequence):
|
71
|
+
key = int(key)
|
72
|
+
if key < len(data):
|
73
|
+
data[key] = val
|
74
|
+
else:
|
75
|
+
data.append(val)
|
76
|
+
return data
|
77
|
+
elif isinstance(data, dict):
|
78
|
+
data[key] = val
|
79
|
+
else:
|
80
|
+
setattr(data, key, val)
|
81
|
+
return data
|
82
|
+
|
83
|
+
|
84
|
+
def get_key(data, key, default):
|
85
|
+
if isinstance(data, Sequence):
|
86
|
+
try:
|
87
|
+
return data[int(key)]
|
88
|
+
except IndexError:
|
89
|
+
return default
|
90
|
+
return pointed_getter(data, key, default)
|
91
|
+
|
92
|
+
|
93
|
+
def pointed_setter(data, path, value):
|
94
|
+
path = path.split(".")
|
95
|
+
key = path.pop(0)
|
96
|
+
if not len(path):
|
97
|
+
return set_key(data, key, value)
|
98
|
+
default = [] if path[0].isdigit() else {}
|
99
|
+
return set_key(
|
100
|
+
data, key, pointed_setter(get_key(data, key, default), ".".join(path), value)
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
def map_method_aliases(new_cls):
|
105
|
+
method_aliases = {
|
106
|
+
"validate_python": "model_validate",
|
107
|
+
"validate_json": "model_validate_json",
|
108
|
+
# "dump_python": "model_dump",
|
109
|
+
# "dump_json": "model_dump_json",
|
110
|
+
"json_schema": "model_json_schema"
|
111
|
+
}
|
112
|
+
for alias_name, target_name in method_aliases.items():
|
113
|
+
setattr(new_cls, alias_name, getattr(new_cls, target_name))
|
114
|
+
return new_cls
|
camomilla/theme/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "6.0.0-beta.
|
1
|
+
__version__ = "6.0.0-beta.4"
|
camomilla/theme/admin.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
2
|
+
from django import forms
|
3
|
+
from django.contrib import admin
|
4
|
+
from django.http import HttpResponse
|
5
|
+
|
6
|
+
from camomilla import settings
|
7
|
+
|
8
|
+
if settings.ENABLE_TRANSLATIONS:
|
9
|
+
from modeltranslation.admin import (
|
10
|
+
TabbedTranslationAdmin as TranslationAwareModelAdmin,
|
11
|
+
)
|
12
|
+
else:
|
13
|
+
from django.contrib.admin import ModelAdmin as TranslationAwareModelAdmin
|
14
|
+
|
15
|
+
from camomilla.models import Article, Content, Media, MediaFolder, Page, Tag, Menu
|
16
|
+
|
17
|
+
|
18
|
+
class AbstractPageAdmin(TranslationAwareModelAdmin):
|
19
|
+
change_form_template = "admin/camomilla/page/change_form.html"
|
20
|
+
|
21
|
+
|
22
|
+
class UserProfileAdmin(admin.ModelAdmin):
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
class ArticleAdminForm(forms.ModelForm):
|
27
|
+
class Meta:
|
28
|
+
model = Article
|
29
|
+
exclude = ("slug",)
|
30
|
+
widgets = {"content": CKEditorUploadingWidget}
|
31
|
+
|
32
|
+
|
33
|
+
class ArticleAdmin(AbstractPageAdmin):
|
34
|
+
filter_horizontal = ("tags",)
|
35
|
+
form = ArticleAdminForm
|
36
|
+
|
37
|
+
|
38
|
+
class TagAdmin(TranslationAwareModelAdmin):
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
42
|
+
class MediaFolderAdmin(admin.ModelAdmin):
|
43
|
+
readonly_fields = ("path",)
|
44
|
+
|
45
|
+
|
46
|
+
class ContentAdminForm(forms.ModelForm):
|
47
|
+
class Meta:
|
48
|
+
model = Content
|
49
|
+
fields = "__all__"
|
50
|
+
widgets = {"content": CKEditorUploadingWidget}
|
51
|
+
|
52
|
+
|
53
|
+
class ContentAdmin(TranslationAwareModelAdmin):
|
54
|
+
form = ContentAdminForm
|
55
|
+
|
56
|
+
|
57
|
+
class MediaAdmin(TranslationAwareModelAdmin):
|
58
|
+
exclude = (
|
59
|
+
"thumbnail",
|
60
|
+
"size",
|
61
|
+
"image_props",
|
62
|
+
)
|
63
|
+
readonly_fields = ("image_preview", "image_thumb_preview", "mime_type")
|
64
|
+
list_display = (
|
65
|
+
"__str__",
|
66
|
+
"title",
|
67
|
+
"image_thumb_preview",
|
68
|
+
)
|
69
|
+
|
70
|
+
def response_add(self, request, obj):
|
71
|
+
if request.GET.get("_popup", ""):
|
72
|
+
return HttpResponse(
|
73
|
+
"""
|
74
|
+
<script type="text/javascript">
|
75
|
+
opener.dismissAddRelatedObjectPopup(window, %s, '%s');
|
76
|
+
</script>"""
|
77
|
+
% (obj.id, obj.json_repr)
|
78
|
+
)
|
79
|
+
else:
|
80
|
+
return super(MediaAdmin, self).response_add(request, obj)
|
81
|
+
|
82
|
+
|
83
|
+
class PageAdmin(AbstractPageAdmin):
|
84
|
+
readonly_fields = ("permalink",)
|
85
|
+
|
86
|
+
|
87
|
+
class MenuAdmin(TranslationAwareModelAdmin):
|
88
|
+
pass
|
89
|
+
|
90
|
+
admin.site.register(Article, ArticleAdmin)
|
91
|
+
admin.site.register(MediaFolder, MediaFolderAdmin)
|
92
|
+
admin.site.register(Tag, TagAdmin)
|
93
|
+
admin.site.register(Content, ContentAdmin)
|
94
|
+
admin.site.register(Media, MediaAdmin)
|
95
|
+
admin.site.register(Page, PageAdmin)
|
96
|
+
admin.site.register(Menu, MenuAdmin)
|
camomilla/theme/apps.py
CHANGED
@@ -11,6 +11,9 @@ class CamomillaThemeConfig(AppConfig):
|
|
11
11
|
def ready(self):
|
12
12
|
installed_apps = getattr(settings, "INSTALLED_APPS", [])
|
13
13
|
changed = False
|
14
|
+
if "django_jsonform" not in installed_apps:
|
15
|
+
changed = True
|
16
|
+
installed_apps = ["django_jsonform", *installed_apps]
|
14
17
|
if "admin_interface" not in installed_apps:
|
15
18
|
changed = True
|
16
19
|
installed_apps = ["admin_interface", *installed_apps]
|
@@ -23,4 +26,12 @@ class CamomillaThemeConfig(AppConfig):
|
|
23
26
|
apps.apps_ready = apps.models_ready = apps.loading = apps.ready = False
|
24
27
|
apps.clear_cache()
|
25
28
|
apps.populate(settings.INSTALLED_APPS)
|
26
|
-
setattr(
|
29
|
+
setattr(
|
30
|
+
settings,
|
31
|
+
"X_FRAME_OPTIONS",
|
32
|
+
getattr(
|
33
|
+
settings,
|
34
|
+
"X_FRAME_OPTIONS",
|
35
|
+
"SAMEORIGIN"
|
36
|
+
),
|
37
|
+
)
|
camomilla/translation.py
CHANGED
@@ -14,9 +14,10 @@ class SeoMixinTranslationOptions(TranslationOptions):
|
|
14
14
|
"canonical",
|
15
15
|
)
|
16
16
|
|
17
|
+
|
17
18
|
class AbstractPageTranslationOptions(SeoMixinTranslationOptions):
|
18
19
|
fields = ("breadcrumbs_title", "slug", "status", "indexable", "template_data")
|
19
|
-
|
20
|
+
|
20
21
|
|
21
22
|
@register(Article)
|
22
23
|
class ArticleTranslationOptions(AbstractPageTranslationOptions):
|
@@ -48,6 +49,7 @@ class PageTranslationOptions(AbstractPageTranslationOptions):
|
|
48
49
|
class UrlNodeTranslationOptions(TranslationOptions):
|
49
50
|
fields = ("permalink",)
|
50
51
|
|
52
|
+
|
51
53
|
@register(Menu)
|
52
54
|
class MenuTranslationOptions(TranslationOptions):
|
53
|
-
fields = ("nodes",)
|
55
|
+
fields = ("nodes",)
|
camomilla/urls.py
CHANGED
@@ -20,6 +20,7 @@ from camomilla.views import (
|
|
20
20
|
UserViewSet,
|
21
21
|
MenuViewSet,
|
22
22
|
)
|
23
|
+
from camomilla.views.pages import fetch_page
|
23
24
|
|
24
25
|
router = routers.DefaultRouter()
|
25
26
|
|
@@ -36,6 +37,8 @@ router.register(r"menus", MenuViewSet, "camomilla-menus")
|
|
36
37
|
|
37
38
|
urlpatterns = [
|
38
39
|
path("", include(router.urls)),
|
40
|
+
path("pages-router/", fetch_page),
|
41
|
+
path("pages-router/<path:permalink>", fetch_page),
|
39
42
|
path(
|
40
43
|
"profiles/me/", lambda _: redirect("../../users/current/"), name="profiles-me"
|
41
44
|
),
|
@@ -43,12 +46,16 @@ urlpatterns = [
|
|
43
46
|
path("auth/login/", CamomillaAuthLogin.as_view(), name="login"),
|
44
47
|
path("auth/logout/", CamomillaAuthLogout.as_view(), name="logout"),
|
45
48
|
path("languages/", LanguageViewSet.as_view(), name="get_languages"),
|
46
|
-
path(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
path(
|
50
|
+
"openapi",
|
51
|
+
get_schema_view(
|
52
|
+
title="Camomilla",
|
53
|
+
description="API for all things …",
|
54
|
+
version="1.0.0",
|
55
|
+
generator_class=SchemaGenerator,
|
56
|
+
),
|
57
|
+
name="openapi-schema",
|
58
|
+
),
|
52
59
|
]
|
53
60
|
|
54
61
|
if find_spec("djsuperadmin.urls") is not None:
|
camomilla/utils/__init__.py
CHANGED
camomilla/utils/getters.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any, Union
|
1
|
+
from typing import Any, Callable, Union
|
2
2
|
|
3
3
|
|
4
4
|
def safe_getter(instance: Union[dict, object], key: str, default: Any = None) -> Any:
|
@@ -15,3 +15,13 @@ def pointed_getter(
|
|
15
15
|
if len(attrs) == 2:
|
16
16
|
data = pointed_getter(data, attrs[1], default)
|
17
17
|
return data
|
18
|
+
|
19
|
+
|
20
|
+
def find_and_replace_dict(obj: dict, predicate: Callable):
|
21
|
+
result = {}
|
22
|
+
for k, v in obj.items():
|
23
|
+
v = predicate(key=k, value=v)
|
24
|
+
if isinstance(v, dict):
|
25
|
+
v = find_and_replace_dict(v, predicate)
|
26
|
+
result[k] = v
|
27
|
+
return result
|
camomilla/utils/templates.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from pathlib import Path
|
2
|
-
from typing import
|
2
|
+
from typing import Sequence
|
3
3
|
|
4
4
|
from django import template as django_template
|
5
5
|
from os.path import relpath
|
6
6
|
|
7
7
|
|
8
|
-
def get_all_templates_files() ->
|
8
|
+
def get_all_templates_files() -> Sequence[str]:
|
9
9
|
dirs = []
|
10
10
|
for engine in django_template.loader.engines.all():
|
11
11
|
# Exclude pip installed site package template dirs
|
camomilla/utils/translation.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import re
|
2
|
-
from typing import Any,
|
2
|
+
from typing import Any, Sequence, Iterator
|
3
3
|
|
4
4
|
from django.db.models import Model, Q
|
5
5
|
from django.utils.translation.trans_real import activate, get_language
|
@@ -8,7 +8,7 @@ from modeltranslation.utils import build_localized_fieldname
|
|
8
8
|
from camomilla.settings import BASE_URL
|
9
9
|
|
10
10
|
|
11
|
-
def activate_languages(languages:
|
11
|
+
def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
|
12
12
|
old = get_language()
|
13
13
|
for language in languages:
|
14
14
|
activate(language)
|
@@ -36,7 +36,7 @@ def url_lang_decompose(url):
|
|
36
36
|
if BASE_URL and url.startswith(BASE_URL):
|
37
37
|
url = url[len(BASE_URL):]
|
38
38
|
data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
|
39
|
-
result = re.match(f"^\/?({'|'.join(AVAILABLE_LANGUAGES)})?\/(.*)", url)
|
39
|
+
result = re.match(f"^\/?({'|'.join(AVAILABLE_LANGUAGES)})?\/(.*)", url) # noqa: W605
|
40
40
|
groups = result and result.groups()
|
41
41
|
if groups and len(groups) == 2:
|
42
42
|
data["language"] = groups[0]
|
@@ -57,11 +57,14 @@ def lang_fallback_query(**kwargs):
|
|
57
57
|
for lang in AVAILABLE_LANGUAGES:
|
58
58
|
query |= Q(**{f"{key}_{lang}": value for key, value in kwargs.items()})
|
59
59
|
if current_lang:
|
60
|
-
query =
|
60
|
+
query = query & Q(
|
61
|
+
**{f"{key}_{current_lang}__isnull": True for key in kwargs.keys()}
|
62
|
+
)
|
61
63
|
query |= Q(**{f"{key}_{current_lang}": value for key, value in kwargs.items()})
|
62
64
|
return query
|
63
65
|
|
64
66
|
|
65
|
-
def is_translatable(model:Model) -> bool:
|
67
|
+
def is_translatable(model: Model) -> bool:
|
66
68
|
from modeltranslation.translator import translator
|
67
|
-
|
69
|
+
|
70
|
+
return model in translator.get_registered_models()
|
camomilla/views/__init__.py
CHANGED
camomilla/views/articles.py
CHANGED
@@ -6,7 +6,6 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
|
|
6
6
|
|
7
7
|
|
8
8
|
class ArticleViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
|
9
|
-
|
10
9
|
queryset = Article.objects.all()
|
11
10
|
serializer_class = ArticleSerializer
|
12
11
|
permission_classes = (CamomillaBasePermissions,)
|
camomilla/views/contents.py
CHANGED
@@ -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 [l[0] for l 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
@@ -45,7 +45,7 @@ class MediaFolderViewSet(
|
|
45
45
|
|
46
46
|
def get_mixed_response(self, request, *args, **kwargs):
|
47
47
|
search = self.request.GET.get("search", None)
|
48
|
-
all = self.request.GET.get("all", "false").lower() ==
|
48
|
+
all = self.request.GET.get("all", "false").lower() == "true"
|
49
49
|
updir = None if all else kwargs.get("pk", None)
|
50
50
|
if not search and all:
|
51
51
|
self.search_fields = []
|
@@ -83,7 +83,6 @@ class MediaViewSet(
|
|
83
83
|
TrigramSearchMixin,
|
84
84
|
BaseModelViewset,
|
85
85
|
):
|
86
|
-
|
87
86
|
queryset = Media.objects.all()
|
88
87
|
serializer_class = MediaSerializer
|
89
88
|
permission_classes = (CamomillaBasePermissions,)
|