django-camomilla-cms 6.0.0b15__tar.gz → 6.0.0b16__tar.gz
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.
- {django_camomilla_cms-6.0.0b15/django_camomilla_cms.egg-info → django_camomilla_cms-6.0.0b16}/PKG-INFO +1 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/__init__.py +1 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/managers/pages.py +7 -7
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/model_api.py +9 -4
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/media.py +1 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/menu.py +3 -2
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/page.py +12 -11
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/openapi/schema.py +4 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/base/__init__.py +3 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/fields/json.py +0 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/fields/related.py +5 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/mixins/__init__.py +51 -6
- django_camomilla_cms-6.0.0b16/camomilla/serializers/mixins/filter_fields.py +56 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/utils.py +3 -1
- django_camomilla_cms-6.0.0b16/camomilla/storages/default.py +6 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/storages/optimize.py +2 -2
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/storages/overwrite.py +2 -2
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/parts/menu.html +1 -1
- django_camomilla_cms-6.0.0b16/camomilla/theme/__init__.py +1 -0
- django_camomilla_cms-6.0.0b16/camomilla/utils/query_parser.py +148 -0
- django_camomilla_cms-6.0.0b16/camomilla/views/base/__init__.py +8 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/menus.py +0 -2
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/mixins/__init__.py +9 -2
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/mixins/pagination.py +4 -13
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16/django_camomilla_cms.egg-info}/PKG-INFO +1 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/django_camomilla_cms.egg-info/SOURCES.txt +11 -1
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/setup.py +1 -1
- django_camomilla_cms-6.0.0b16/tests/fixtures/__init__.py +17 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/tests/test_api.py +2 -11
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/tests/test_camomilla_filters.py +7 -13
- django_camomilla_cms-6.0.0b16/tests/test_media.py +80 -0
- django_camomilla_cms-6.0.0b16/tests/test_model_api.py +68 -0
- django_camomilla_cms-6.0.0b16/tests/test_model_api_permissions.py +39 -0
- django_camomilla_cms-6.0.0b16/tests/test_query_parser.py +59 -0
- django_camomilla_cms-6.0.0b16/tests/test_utils.py +86 -0
- django_camomilla_cms-6.0.0b16/tests/utils/__init__.py +0 -0
- django_camomilla_cms-6.0.0b16/tests/utils/api.py +29 -0
- django_camomilla_cms-6.0.0b15/camomilla/theme/__init__.py +0 -1
- django_camomilla_cms-6.0.0b15/camomilla/views/base/__init__.py +0 -8
- django_camomilla_cms-6.0.0b15/tests/test_utils.py +0 -86
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/MANIFEST.in +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/README.md +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/apps.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/authentication.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/context_processors.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/contrib/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/contrib/modeltranslation/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/contrib/modeltranslation/hvad_migration.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/contrib/rest_framework/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/contrib/rest_framework/serializer.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/defaults.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/dynamic_pages_urls.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/exceptions.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/fields/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/fields/json.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/management/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/management/commands/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/management/commands/regenerate_thumbnails.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/managers/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/article.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/content.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/models/mixins/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/openapi/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/parsers.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/permissions.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/article.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/content_type.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/fields/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/fields/file.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/media.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/menu.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/page.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/user.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/validators.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/settings.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/sitemap.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/storages/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/admin/camomilla/page/change_form.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/articles/default.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/base.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/pages/default.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/parts/langswitch.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates/defaults/widgets/media_select_multiple.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates_context/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates_context/autodiscover.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templates_context/rendering.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templatetags/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templatetags/camomilla_filters.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/templatetags/menus.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/admin.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/apps.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/static/admin/css/responsive.css +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/static/admin/img/favicon.ico +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/static/admin/img/logo.svg +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/templates/admin/base.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/theme/templates/rosetta/base.html +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/translation.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/urls.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/getters.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/normalization.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/seo.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/setters.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/templates.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/utils/translation.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/articles.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/contents.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/decorators.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/languages.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/medias.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/mixins/ordering.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/pages.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/tags.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/views/users.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/django_camomilla_cms.egg-info/dependency_links.txt +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/django_camomilla_cms.egg-info/requires.txt +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/django_camomilla_cms.egg-info/top_level.txt +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/pyproject.toml +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/setup.cfg +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/tests/__init__.py +0 -0
- {django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/tests/test_models.py +0 -0
@@ -6,26 +6,26 @@ URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
|
|
6
6
|
|
7
7
|
|
8
8
|
class PageQuerySet(QuerySet):
|
9
|
-
|
9
|
+
|
10
10
|
__UrlNodeModel = None
|
11
|
-
|
11
|
+
|
12
12
|
@property
|
13
13
|
def UrlNodeModel(self):
|
14
14
|
if not self.__UrlNodeModel:
|
15
15
|
self.__UrlNodeModel = apps.get_model("camomilla", "UrlNode")
|
16
|
-
return self.__UrlNodeModel
|
17
|
-
|
16
|
+
return self.__UrlNodeModel
|
17
|
+
|
18
18
|
def get_permalink_kwargs(self, kwargs):
|
19
19
|
return list(set(kwargs.keys()).intersection(set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])))
|
20
|
-
|
20
|
+
|
21
21
|
def get(self, *args, **kwargs):
|
22
22
|
permalink_args = self.get_permalink_kwargs(kwargs)
|
23
23
|
if len(permalink_args):
|
24
24
|
try:
|
25
|
-
node = self.UrlNodeModel.objects.get(**{arg:kwargs.pop(arg) for arg in permalink_args})
|
25
|
+
node = self.UrlNodeModel.objects.get(**{arg: kwargs.pop(arg) for arg in permalink_args})
|
26
26
|
kwargs["url_node"] = node
|
27
27
|
except ObjectDoesNotExist:
|
28
28
|
raise self.model.DoesNotExist(
|
29
29
|
"%s matching query does not exist." % self.model._meta.object_name
|
30
30
|
)
|
31
|
-
return super(PageQuerySet, self).get(*args, **kwargs)
|
31
|
+
return super(PageQuerySet, self).get(*args, **kwargs)
|
@@ -26,6 +26,7 @@ def register(
|
|
26
26
|
"""
|
27
27
|
|
28
28
|
def inner(model):
|
29
|
+
global urlpatterns
|
29
30
|
base_meta = {
|
30
31
|
"model": model,
|
31
32
|
"fields": "__all__",
|
@@ -46,14 +47,18 @@ def register(
|
|
46
47
|
)
|
47
48
|
},
|
48
49
|
)
|
50
|
+
|
51
|
+
def get_queryset(self, *args, **kwargs):
|
52
|
+
qs = super(base_viewset, self).get_queryset(*args, **kwargs)
|
53
|
+
return qs if filters is None else qs.filter(**filters)
|
49
54
|
|
50
55
|
viewset = type(
|
51
56
|
f"{model.__name__}ViewSet",
|
52
57
|
(base_viewset,),
|
53
58
|
{
|
54
|
-
"
|
55
|
-
|
56
|
-
|
59
|
+
"queryset": model.objects.all(),
|
60
|
+
"model": model,
|
61
|
+
"get_queryset": get_queryset,
|
57
62
|
"serializer_class": serializer,
|
58
63
|
**viewset_attrs,
|
59
64
|
},
|
@@ -73,7 +78,7 @@ def register(
|
|
73
78
|
viewset,
|
74
79
|
f"{model.__name__.lower()}_api",
|
75
80
|
)
|
76
|
-
urlpatterns
|
81
|
+
urlpatterns = [path("", include(router.urls))]
|
77
82
|
return model
|
78
83
|
|
79
84
|
return inner
|
@@ -150,7 +150,7 @@ class Media(models.Model):
|
|
150
150
|
img_bytes = self.file.storage.open(self.file.name, "rb")
|
151
151
|
with Image.open(img_bytes) as orig_image:
|
152
152
|
image = orig_image.copy()
|
153
|
-
image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.
|
153
|
+
image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.LANCZOS)
|
154
154
|
|
155
155
|
# Path to save to, name, and extension
|
156
156
|
thumb_name, thumb_extension = os.path.splitext(self.file.name)
|
@@ -14,10 +14,10 @@ from pydantic import (
|
|
14
14
|
)
|
15
15
|
from structured.pydantic.models import BaseModel
|
16
16
|
from structured.fields import StructuredJSONField
|
17
|
-
from structured.pydantic.fields import QuerySet
|
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
|
20
|
+
from django.conf import settings
|
21
21
|
|
22
22
|
|
23
23
|
class LinkTypes(str, Enum):
|
@@ -88,7 +88,8 @@ class Menu(models.Model):
|
|
88
88
|
):
|
89
89
|
if isinstance(context, RequestContext):
|
90
90
|
context = context.flatten()
|
91
|
-
|
91
|
+
is_preview = bool(request.GET.get("preview", False))
|
92
|
+
context.update({"menu": self, "is_preview": is_preview})
|
92
93
|
return mark_safe(render_to_string(template_path, context, request))
|
93
94
|
|
94
95
|
class defaultdict(dict):
|
@@ -36,6 +36,7 @@ from modeltranslation.utils import build_localized_fieldname
|
|
36
36
|
class UrlPathValidator():
|
37
37
|
pass
|
38
38
|
|
39
|
+
|
39
40
|
def GET_TEMPLATE_CHOICES():
|
40
41
|
return [(t, t) for t in get_all_templates_files()]
|
41
42
|
|
@@ -116,14 +117,13 @@ class UrlNodeManager(models.Manager):
|
|
116
117
|
|
117
118
|
|
118
119
|
class UrlNode(models.Model):
|
119
|
-
|
120
|
+
|
120
121
|
LANG_PERMALINK_FIELDS = [
|
121
122
|
build_localized_fieldname("permalink", lang)
|
122
123
|
for lang in AVAILABLE_LANGUAGES
|
123
124
|
if settings.ENABLE_TRANSLATIONS
|
124
125
|
]
|
125
|
-
|
126
|
-
|
126
|
+
|
127
127
|
permalink = models.CharField(max_length=400, unique=True, null=True)
|
128
128
|
related_name = models.CharField(max_length=200)
|
129
129
|
objects = UrlNodeManager()
|
@@ -153,7 +153,7 @@ class UrlNode(models.Model):
|
|
153
153
|
if self.routerlink == "/":
|
154
154
|
return ""
|
155
155
|
return self.routerlink
|
156
|
-
|
156
|
+
|
157
157
|
@staticmethod
|
158
158
|
def sanitize_permalink(permalink):
|
159
159
|
if isinstance(permalink, str):
|
@@ -162,12 +162,12 @@ class UrlNode(models.Model):
|
|
162
162
|
if not permalink.startswith("/"):
|
163
163
|
permalink = f"/{permalink}"
|
164
164
|
return permalink
|
165
|
-
|
165
|
+
|
166
166
|
def save(self, *args, **kwargs) -> None:
|
167
167
|
for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
|
168
168
|
setattr(self, lang_p_field, UrlNode.sanitize_permalink(getattr(self, lang_p_field)))
|
169
169
|
super().save(*args, **kwargs)
|
170
|
-
|
170
|
+
|
171
171
|
def __str__(self) -> str:
|
172
172
|
return self.permalink
|
173
173
|
|
@@ -190,10 +190,11 @@ class PageBase(models.base.ModelBase):
|
|
190
190
|
def perm_prop_factory(permalink_field):
|
191
191
|
def getter(_self):
|
192
192
|
return getattr(_self, f"__{permalink_field}", getattr(_self.url_node or object(), permalink_field, None))
|
193
|
-
|
193
|
+
|
194
|
+
def setter(_self, value: str):
|
194
195
|
setattr(_self, f"__{permalink_field}", value)
|
195
196
|
return getter, setter
|
196
|
-
|
197
|
+
|
197
198
|
def __new__(cls, name, bases, attrs, **kwargs):
|
198
199
|
attr_meta = attrs.pop("PageMeta", None)
|
199
200
|
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
@@ -246,7 +247,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
246
247
|
)
|
247
248
|
|
248
249
|
objects = PageQuerySet.as_manager()
|
249
|
-
|
250
|
+
|
250
251
|
__cached_db_instance: "AbstractPage" = None
|
251
252
|
|
252
253
|
@property
|
@@ -254,7 +255,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
254
255
|
if self.__cached_db_instance is None:
|
255
256
|
self.__cached_db_instance = self.get_db_instance()
|
256
257
|
return self.__cached_db_instance
|
257
|
-
|
258
|
+
|
258
259
|
def get_db_instance(self):
|
259
260
|
if self.pk:
|
260
261
|
return self.__class__.objects.get(pk=self.pk)
|
@@ -337,7 +338,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
337
338
|
for __ in activate_languages():
|
338
339
|
old_permalink = self.db_instance and self.db_instance.permalink
|
339
340
|
new_permalink = self.permalink
|
340
|
-
if self.autopermalink:
|
341
|
+
if self.autopermalink:
|
341
342
|
new_permalink = self.generate_permalink()
|
342
343
|
force = force or old_permalink != new_permalink
|
343
344
|
set_nofallbacks(self.url_node, "permalink", new_permalink)
|
@@ -55,4 +55,8 @@ class SchemaGenerator(DRFSchemaGenerator):
|
|
55
55
|
def create_view(self, callback, method, request=None):
|
56
56
|
view = super(SchemaGenerator, self).create_view(callback, method, request)
|
57
57
|
view.schema = AutoSchema()
|
58
|
+
if not hasattr(view, 'get_queryset') and getattr(view, 'queryset', None) is None:
|
59
|
+
attname = "permission_classes"
|
60
|
+
cname = "DjangoModelPermissions"
|
61
|
+
setattr(view, attname, [p for p in getattr(view, attname, []) if cname not in p.__name__])
|
58
62
|
return view
|
@@ -8,14 +8,16 @@ from ..mixins import (
|
|
8
8
|
OrderingMixin,
|
9
9
|
SetupEagerLoadingMixin,
|
10
10
|
)
|
11
|
+
from ..mixins.filter_fields import FilterFieldsMixin
|
11
12
|
|
12
13
|
|
13
14
|
class BaseModelSerializer(
|
15
|
+
SetupEagerLoadingMixin,
|
14
16
|
NestMixin,
|
17
|
+
FilterFieldsMixin,
|
15
18
|
FieldsOverrideMixin,
|
16
19
|
JSONFieldPatchMixin,
|
17
20
|
OrderingMixin,
|
18
|
-
SetupEagerLoadingMixin,
|
19
21
|
TranslationsMixin,
|
20
22
|
serializers.ModelSerializer,
|
21
23
|
):
|
@@ -18,6 +18,7 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
|
|
18
18
|
"""
|
19
19
|
|
20
20
|
def __init__(self, **kwargs):
|
21
|
+
self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
|
21
22
|
self.serializer = kwargs.pop("serializer", None)
|
22
23
|
self.lookup = kwargs.pop("lookup", "id")
|
23
24
|
if self.serializer is not None:
|
@@ -42,7 +43,10 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
|
|
42
43
|
|
43
44
|
def to_representation(self, instance):
|
44
45
|
if self.serializer:
|
45
|
-
|
46
|
+
kwargs = {"context": self.context}
|
47
|
+
if self.inherited_fields_filter:
|
48
|
+
kwargs["inherited_fields_filter"] = self.inherited_fields_filter
|
49
|
+
return self.serializer(instance, **kwargs).data
|
46
50
|
return super().to_representation(instance)
|
47
51
|
|
48
52
|
def to_internal_value(self, data):
|
@@ -57,8 +57,50 @@ class SetupEagerLoadingMixin:
|
|
57
57
|
"""
|
58
58
|
This mixin allows to use the setup_eager_loading method to optimize the queries.
|
59
59
|
"""
|
60
|
-
|
61
|
-
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def optimize_qs(cls, queryset, context=None):
|
63
|
+
if hasattr(cls, "setup_eager_loading"):
|
64
|
+
queryset = cls.setup_eager_loading(queryset, context=context)
|
65
|
+
return cls.auto_optimize_queryset(queryset, context=context)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def auto_optimize_queryset(cls, queryset, context=None):
|
69
|
+
request = context.get("request", None)
|
70
|
+
if request and request.method == "GET":
|
71
|
+
model = getattr(cls.Meta, "model", None)
|
72
|
+
info = model_meta.get_field_info(model)
|
73
|
+
only = set()
|
74
|
+
prefetch_related = set()
|
75
|
+
select_related = set()
|
76
|
+
serializer_fields = cls(context=context).fields.keys()
|
77
|
+
filtered_fields = set()
|
78
|
+
for field in request.query_params.get("fields", "").split(","):
|
79
|
+
if "__" in field:
|
80
|
+
field, _ = field.split("__", 1)
|
81
|
+
if field in serializer_fields:
|
82
|
+
filtered_fields.add(field)
|
83
|
+
if len(filtered_fields) == 0:
|
84
|
+
filtered_fields = serializer_fields
|
85
|
+
for field in filtered_fields:
|
86
|
+
complete_field = field
|
87
|
+
if "__" in field:
|
88
|
+
field, sub_field = field.split("__", 1)
|
89
|
+
complete_field = f"{field}__{sub_field}"
|
90
|
+
if field in info.forward_relations and not info.forward_relations[field].to_many:
|
91
|
+
select_related.add(field)
|
92
|
+
only.add(complete_field)
|
93
|
+
elif field in info.reverse_relations or field in info.forward_relations and info.forward_relations[field].to_many:
|
94
|
+
prefetch_related.add(field)
|
95
|
+
only.add(complete_field)
|
96
|
+
elif field in info.fields or field == info.pk.name:
|
97
|
+
only.add(complete_field)
|
98
|
+
if len(only) > 0:
|
99
|
+
queryset = queryset.only(*only)
|
100
|
+
if len(select_related) > 0:
|
101
|
+
queryset = queryset.select_related(*select_related)
|
102
|
+
if len(prefetch_related) > 0:
|
103
|
+
queryset = queryset.prefetch_related(*prefetch_related)
|
62
104
|
return queryset
|
63
105
|
|
64
106
|
|
@@ -174,13 +216,16 @@ class AbstractPageMixin(serializers.ModelSerializer):
|
|
174
216
|
|
175
217
|
def get_default_field_names(self, *args):
|
176
218
|
from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
|
177
|
-
|
219
|
+
default_fields = super().get_default_field_names(*args)
|
220
|
+
filtered_fields = getattr(self, "filtered_fields", [])
|
221
|
+
if len(filtered_fields) > 0:
|
222
|
+
return filtered_fields
|
178
223
|
if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
|
179
|
-
return
|
224
|
+
return default_fields
|
180
225
|
return (
|
181
|
-
[f for f in
|
226
|
+
[f for f in default_fields if f != "url_node"]
|
182
227
|
+ UrlNode.LANG_PERMALINK_FIELDS
|
183
|
-
+ ["permalink"]
|
228
|
+
+ ["permalink"]
|
184
229
|
)
|
185
230
|
|
186
231
|
def build_field(self, field_name, info, model_class, nested_depth):
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from rest_framework import serializers
|
2
|
+
from camomilla.serializers.fields.related import RelatedField
|
3
|
+
from collections import defaultdict
|
4
|
+
|
5
|
+
|
6
|
+
class FilterFieldsMixin(serializers.ModelSerializer):
|
7
|
+
"""
|
8
|
+
Mixin to filter fields from a serializer, including handling nested fields.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, *args, **kwargs):
|
12
|
+
self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
|
13
|
+
return super().__init__(*args, **kwargs)
|
14
|
+
|
15
|
+
|
16
|
+
inherited_fields_filter = []
|
17
|
+
|
18
|
+
|
19
|
+
def get_default_field_names(self, *args):
|
20
|
+
field_names = super().get_default_field_names(*args)
|
21
|
+
request = self.context.get("request", None)
|
22
|
+
|
23
|
+
if request is not None and request.method == "GET":
|
24
|
+
fields = request.query_params.get("fields", "").split(",")
|
25
|
+
fields = [f for f in fields if f != ""]
|
26
|
+
if len(self.inherited_fields_filter) > 0:
|
27
|
+
fields = self.inherited_fields_filter
|
28
|
+
|
29
|
+
self.filtered_fields = set()
|
30
|
+
self.childs_fields = defaultdict(set)
|
31
|
+
for field in fields:
|
32
|
+
if "__" in field:
|
33
|
+
parent_field, child_field = field.split("__", 1)
|
34
|
+
if parent_field in field_names:
|
35
|
+
self.filtered_fields.add(parent_field)
|
36
|
+
self.childs_fields[parent_field].add(child_field)
|
37
|
+
|
38
|
+
|
39
|
+
else:
|
40
|
+
if field in field_names:
|
41
|
+
self.filtered_fields.add(field)
|
42
|
+
|
43
|
+
if len(self.filtered_fields) > 0:
|
44
|
+
return list(self.filtered_fields)
|
45
|
+
else:
|
46
|
+
return field_names
|
47
|
+
|
48
|
+
return field_names
|
49
|
+
|
50
|
+
def build_field(self, field_name, info, model_class, nested_depth):
|
51
|
+
field_class, field_kwargs = super().build_field(field_name, info, model_class, nested_depth)
|
52
|
+
inherited_fields_filter = self.childs_fields.get(field_name, []) if hasattr(self, "childs_fields") else []
|
53
|
+
if len(inherited_fields_filter) > 0 and issubclass(field_class, RelatedField):
|
54
|
+
field_kwargs["inherited_fields_filter"] = list(inherited_fields_filter)
|
55
|
+
return field_class, field_kwargs
|
56
|
+
|
{django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/serializers/utils.py
RENAMED
@@ -1,6 +1,7 @@
|
|
1
1
|
def get_standard_bases() -> tuple:
|
2
2
|
from rest_framework.serializers import ModelSerializer
|
3
3
|
from camomilla.serializers.fields import FieldsOverrideMixin
|
4
|
+
from camomilla.serializers.mixins.filter_fields import FilterFieldsMixin
|
4
5
|
from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
|
5
6
|
from camomilla.serializers.mixins import (
|
6
7
|
JSONFieldPatchMixin,
|
@@ -10,12 +11,13 @@ def get_standard_bases() -> tuple:
|
|
10
11
|
)
|
11
12
|
|
12
13
|
return (
|
14
|
+
SetupEagerLoadingMixin,
|
15
|
+
FilterFieldsMixin,
|
13
16
|
NestMixin,
|
14
17
|
FieldsOverrideMixin,
|
15
18
|
JSONFieldPatchMixin,
|
16
19
|
OrderingMixin,
|
17
20
|
RemoveTranslationsMixin,
|
18
|
-
SetupEagerLoadingMixin,
|
19
21
|
ModelSerializer,
|
20
22
|
)
|
21
23
|
|
{django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/storages/optimize.py
RENAMED
@@ -2,13 +2,13 @@ import traceback
|
|
2
2
|
from io import BytesIO
|
3
3
|
|
4
4
|
from django.core.files.base import ContentFile
|
5
|
-
from django.core.files.storage import get_storage_class
|
6
5
|
from PIL import Image
|
7
6
|
|
8
7
|
from camomilla import settings
|
8
|
+
from camomilla.storages.default import get_default_storage_class
|
9
9
|
|
10
10
|
|
11
|
-
class OptimizedStorage(
|
11
|
+
class OptimizedStorage(get_default_storage_class()):
|
12
12
|
MEDIA_MAX_WIDTH = settings.MEDIA_OPTIMIZE_MAX_WIDTH
|
13
13
|
MEDIA_MAX_HEIGHT = settings.MEDIA_OPTIMIZE_MAX_HEIGHT
|
14
14
|
MEDIA_DPI = settings.MEDIA_OPTIMIZE_DPI
|
{django_camomilla_cms-6.0.0b15 → django_camomilla_cms-6.0.0b16}/camomilla/storages/overwrite.py
RENAMED
@@ -1,7 +1,7 @@
|
|
1
|
-
from
|
1
|
+
from camomilla.storages.default import get_default_storage_class
|
2
2
|
|
3
3
|
|
4
|
-
class OverwriteStorage(
|
4
|
+
class OverwriteStorage(get_default_storage_class()):
|
5
5
|
def _save(self, name, content):
|
6
6
|
if self.exists(name):
|
7
7
|
self.delete(name)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
{% for item in menu.nodes %}
|
5
5
|
<li>
|
6
6
|
{% if item.link.url %}
|
7
|
-
<a href="{{ item.link.url }}">{{ item.title }}</a>
|
7
|
+
<a href="{{ item.link.url }}{% if is_preview %}?preview=true{% endif %}">{{ item.title }}</a>
|
8
8
|
{% else %}
|
9
9
|
<span>{{item.title}}</span>
|
10
10
|
{% endif %}
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "6.0.0-beta.16"
|
@@ -0,0 +1,148 @@
|
|
1
|
+
import re
|
2
|
+
from django.db.models import Q
|
3
|
+
|
4
|
+
CONDITION_PATTERN = re.compile(r"(\w+__\w+='[^']+'|\w+__\w+=\S+)") # Updated regex to handle quoted values
|
5
|
+
LOGICAL_OPERATORS = {"AND", "OR"}
|
6
|
+
|
7
|
+
class ConditionParser:
|
8
|
+
def __init__(self, query):
|
9
|
+
self.query = query
|
10
|
+
|
11
|
+
def parse(self, query=None):
|
12
|
+
"""Parse the query or subquery. If no query is provided, use the instance's query."""
|
13
|
+
if query is None:
|
14
|
+
query = self.query
|
15
|
+
|
16
|
+
tokens = self.tokenize(query)
|
17
|
+
# If there's just one token and it's a dictionary (single condition), return it
|
18
|
+
if len(tokens) == 1 and isinstance(tokens[0], dict):
|
19
|
+
return tokens[0]
|
20
|
+
return self.build_tree(tokens)
|
21
|
+
|
22
|
+
def tokenize(self, query):
|
23
|
+
tokens = []
|
24
|
+
i = 0
|
25
|
+
while i < len(query):
|
26
|
+
if query[i] == '(':
|
27
|
+
j = i + 1
|
28
|
+
open_parens = 1
|
29
|
+
while j < len(query) and open_parens > 0:
|
30
|
+
if query[j] == '(':
|
31
|
+
open_parens += 1
|
32
|
+
elif query[j] == ')':
|
33
|
+
open_parens -= 1
|
34
|
+
j += 1
|
35
|
+
if open_parens == 0:
|
36
|
+
subquery = query[i + 1:j - 1]
|
37
|
+
tokens.append(self.parse(subquery)) # Pass the subquery here
|
38
|
+
i = j
|
39
|
+
else:
|
40
|
+
raise ValueError("Mismatched parentheses")
|
41
|
+
elif query[i:i+3] == 'AND' or query[i:i+2] == 'OR':
|
42
|
+
operator = 'AND' if query[i:i+3] == 'AND' else 'OR'
|
43
|
+
tokens.append(operator)
|
44
|
+
i += 3 if operator == 'AND' else 2
|
45
|
+
else:
|
46
|
+
match = CONDITION_PATTERN.match(query[i:])
|
47
|
+
if match:
|
48
|
+
condition = self.parse_condition(match.group())
|
49
|
+
tokens.append(condition)
|
50
|
+
i += match.end()
|
51
|
+
else:
|
52
|
+
i += 1
|
53
|
+
return tokens
|
54
|
+
|
55
|
+
def parse_condition(self, condition_str):
|
56
|
+
"""Parse a single condition into field lookup and value."""
|
57
|
+
if '=' in condition_str:
|
58
|
+
field_lookup, value = condition_str.split("=")
|
59
|
+
value = value.strip("'").strip('"') # Remove single or double quotes
|
60
|
+
value = self.parse_value(value) # Parse the value
|
61
|
+
return {"field_lookup": field_lookup, "value": value}
|
62
|
+
return None
|
63
|
+
|
64
|
+
def parse_value(self, string: str):
|
65
|
+
"""Parse single condition values based on specific rules."""
|
66
|
+
if string and string.startswith("[") and string.endswith("]"):
|
67
|
+
string = [self.parse_value(substr) for substr in string[1:-1].split(",")]
|
68
|
+
elif string and string.lower() in ["true", "false"]:
|
69
|
+
string = string.lower() == "true"
|
70
|
+
elif string and string.isdigit():
|
71
|
+
string = int(string)
|
72
|
+
return string
|
73
|
+
|
74
|
+
def build_tree(self, tokens):
|
75
|
+
"""Build a tree-like structure with operators and conditions."""
|
76
|
+
if not tokens:
|
77
|
+
return None
|
78
|
+
|
79
|
+
output_stack = []
|
80
|
+
operator_stack = []
|
81
|
+
|
82
|
+
# Process each token in the query
|
83
|
+
for token in tokens:
|
84
|
+
if isinstance(token, dict):
|
85
|
+
# Handle a single condition
|
86
|
+
if operator_stack:
|
87
|
+
operator = operator_stack.pop()
|
88
|
+
if isinstance(output_stack[-1], dict):
|
89
|
+
output_stack[-1] = {
|
90
|
+
"operator": operator,
|
91
|
+
"conditions": [output_stack[-1], token]
|
92
|
+
}
|
93
|
+
else:
|
94
|
+
output_stack[-1]["conditions"].append(token)
|
95
|
+
else:
|
96
|
+
output_stack.append(token)
|
97
|
+
|
98
|
+
elif token in LOGICAL_OPERATORS:
|
99
|
+
# Operator found (AND/OR), handle precedence
|
100
|
+
operator_stack.append(token)
|
101
|
+
|
102
|
+
# If only one item in output_stack, return it directly
|
103
|
+
if len(output_stack) == 1:
|
104
|
+
return output_stack[0]
|
105
|
+
return {"operator": "AND", "conditions": output_stack} # Default to AND if no operators
|
106
|
+
|
107
|
+
def to_q(self, parsed_tree):
|
108
|
+
"""Convert parsed tree structure into Q objects."""
|
109
|
+
if isinstance(parsed_tree, list):
|
110
|
+
# If parsed_tree is a list, combine all conditions with AND by default
|
111
|
+
q_objects = [self.to_q(cond) for cond in parsed_tree]
|
112
|
+
combined_q = Q()
|
113
|
+
for q_obj in q_objects:
|
114
|
+
combined_q &= q_obj
|
115
|
+
return combined_q
|
116
|
+
|
117
|
+
if isinstance(parsed_tree, dict):
|
118
|
+
if "field_lookup" in parsed_tree:
|
119
|
+
# Base case: a single condition
|
120
|
+
return Q(**{parsed_tree["field_lookup"]: parsed_tree["value"]})
|
121
|
+
|
122
|
+
elif "operator" in parsed_tree and "conditions" in parsed_tree:
|
123
|
+
operator = parsed_tree["operator"]
|
124
|
+
conditions = parsed_tree["conditions"]
|
125
|
+
|
126
|
+
q_objects = [self.to_q(cond) for cond in conditions]
|
127
|
+
|
128
|
+
if operator == "AND":
|
129
|
+
combined_q = Q()
|
130
|
+
for q_obj in q_objects:
|
131
|
+
combined_q &= q_obj
|
132
|
+
return combined_q
|
133
|
+
elif operator == "OR":
|
134
|
+
combined_q = Q()
|
135
|
+
for q_obj in q_objects:
|
136
|
+
combined_q |= q_obj
|
137
|
+
return combined_q
|
138
|
+
else:
|
139
|
+
raise ValueError(f"Unknown operator: {operator}")
|
140
|
+
|
141
|
+
raise ValueError("Parsed tree structure is invalid")
|
142
|
+
|
143
|
+
def parse_to_q(self):
|
144
|
+
"""Parse the query and convert to Q object."""
|
145
|
+
parsed_tree = self.parse()
|
146
|
+
if not parsed_tree:
|
147
|
+
return Q() # Return an empty Q if parsing fails
|
148
|
+
return self.to_q(parsed_tree)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from ..mixins import OptimViewMixin, PaginateStackMixin, OrderingMixin, CamomillaBasePermissionMixin
|
2
|
+
from rest_framework import viewsets
|
3
|
+
|
4
|
+
|
5
|
+
class BaseModelViewset(
|
6
|
+
CamomillaBasePermissionMixin, OptimViewMixin, OrderingMixin, PaginateStackMixin, viewsets.ModelViewSet
|
7
|
+
):
|
8
|
+
pass
|
@@ -13,8 +13,6 @@ from camomilla.serializers.page import BasicUrlNodeSerializer
|
|
13
13
|
from camomilla.views.base import BaseModelViewset
|
14
14
|
from camomilla.views.decorators import active_lang
|
15
15
|
|
16
|
-
from django.utils.translation import get_language
|
17
|
-
|
18
16
|
|
19
17
|
class MenuViewSet(BaseModelViewset):
|
20
18
|
queryset = Menu.objects.all()
|