django-camomilla-cms 6.0.0b15__py2.py3-none-any.whl → 6.0.0b17__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.
Files changed (72) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
  3. camomilla/dynamic_pages_urls.py +6 -2
  4. camomilla/managers/pages.py +93 -8
  5. camomilla/model_api.py +14 -7
  6. camomilla/models/media.py +1 -1
  7. camomilla/models/menu.py +10 -4
  8. camomilla/models/page.py +189 -127
  9. camomilla/openapi/schema.py +17 -8
  10. camomilla/redirects.py +10 -0
  11. camomilla/serializers/base/__init__.py +6 -4
  12. camomilla/serializers/fields/__init__.py +5 -17
  13. camomilla/serializers/fields/related.py +10 -4
  14. camomilla/serializers/mixins/__init__.py +23 -195
  15. camomilla/serializers/mixins/fields.py +20 -0
  16. camomilla/serializers/mixins/filter_fields.py +57 -0
  17. camomilla/serializers/mixins/json.py +34 -0
  18. camomilla/serializers/mixins/language.py +32 -0
  19. camomilla/serializers/mixins/nesting.py +35 -0
  20. camomilla/serializers/mixins/optimize.py +91 -0
  21. camomilla/serializers/mixins/ordering.py +34 -0
  22. camomilla/serializers/mixins/page.py +58 -0
  23. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  24. camomilla/serializers/utils.py +5 -3
  25. camomilla/serializers/validators.py +6 -2
  26. camomilla/settings.py +10 -2
  27. camomilla/storages/default.py +12 -0
  28. camomilla/storages/optimize.py +2 -2
  29. camomilla/storages/overwrite.py +2 -2
  30. camomilla/templates/defaults/parts/menu.html +1 -1
  31. camomilla/templatetags/menus.py +3 -0
  32. camomilla/theme/__init__.py +1 -1
  33. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  34. camomilla/theme/admin/pages.py +46 -0
  35. camomilla/theme/admin/translations.py +13 -0
  36. camomilla/theme/apps.py +1 -5
  37. camomilla/translation.py +7 -1
  38. camomilla/urls.py +2 -5
  39. camomilla/utils/query_parser.py +167 -0
  40. camomilla/utils/translation.py +47 -5
  41. camomilla/views/base/__init__.py +35 -5
  42. camomilla/views/medias.py +1 -1
  43. camomilla/views/menus.py +0 -2
  44. camomilla/views/mixins/__init__.py +17 -69
  45. camomilla/views/mixins/bulk_actions.py +22 -0
  46. camomilla/views/mixins/language.py +33 -0
  47. camomilla/views/mixins/optimize.py +18 -0
  48. camomilla/views/mixins/pagination.py +12 -18
  49. camomilla/views/mixins/permissions.py +6 -0
  50. camomilla/views/pages.py +12 -2
  51. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
  52. django_camomilla_cms-6.0.0b17.dist-info/RECORD +132 -0
  53. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
  54. tests/fixtures/__init__.py +17 -0
  55. tests/test_api.py +2 -11
  56. tests/test_camomilla_filters.py +7 -13
  57. tests/test_media.py +113 -0
  58. tests/test_menu.py +97 -0
  59. tests/test_model_api.py +68 -0
  60. tests/test_model_api_permissions.py +39 -0
  61. tests/test_model_api_register.py +393 -0
  62. tests/test_pages.py +343 -0
  63. tests/test_query_parser.py +58 -0
  64. tests/test_templates_context.py +111 -0
  65. tests/test_utils.py +64 -64
  66. tests/utils/api.py +28 -0
  67. tests/utils/media.py +9 -0
  68. camomilla/serializers/fields/json.py +0 -49
  69. django_camomilla_cms-6.0.0b15.dist-info/RECORD +0 -105
  70. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
  71. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
  72. {camomilla/contrib/rest_framework → tests/utils}/__init__.py +0 -0
@@ -1,11 +1,12 @@
1
1
  import re
2
- from typing import Any, Sequence, Iterator
2
+ from typing import Any, Sequence, Iterator, Union, List
3
3
 
4
4
  from django.db.models import Model, Q
5
5
  from django.utils.translation.trans_real import activate, get_language
6
6
  from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
7
7
  from modeltranslation.utils import build_localized_fieldname
8
8
  from camomilla.settings import BASE_URL
9
+ from django.http import QueryDict
9
10
 
10
11
 
11
12
  def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
@@ -34,11 +35,9 @@ def get_nofallbacks(instance: Model, attr: str, *args, **kwargs) -> Any:
34
35
 
35
36
  def url_lang_decompose(url):
36
37
  if BASE_URL and url.startswith(BASE_URL):
37
- url = url[len(BASE_URL):]
38
+ url = url[len(BASE_URL) :]
38
39
  data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
39
- result = re.match(
40
- f"^\/?({'|'.join(AVAILABLE_LANGUAGES)})?\/(.*)", url
41
- ) # noqa: W605
40
+ result = re.match(rf"^/?({'|'.join(AVAILABLE_LANGUAGES)})?/(.*)", url) # noqa: W605
42
41
  groups = result and result.groups()
43
42
  if groups and len(groups) == 2:
44
43
  data["language"] = groups[0]
@@ -70,3 +69,46 @@ def is_translatable(model: Model) -> bool:
70
69
  from modeltranslation.translator import translator
71
70
 
72
71
  return model in translator.get_registered_models()
72
+
73
+
74
+ def plain_to_nest(data, fields, accessor="translations"):
75
+ """
76
+ This function transforms a plain dictionary with translations fields (es. {"title_en": "Hello"})
77
+ into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
78
+ """
79
+ trans_data = {}
80
+ for lang in AVAILABLE_LANGUAGES:
81
+ lang_data = {}
82
+ for field in fields:
83
+ trans_field_name = build_localized_fieldname(field, lang)
84
+ if trans_field_name in data:
85
+ lang_data[field] = data.pop(trans_field_name)
86
+ if lang_data.keys():
87
+ trans_data[lang] = lang_data
88
+ if trans_data.keys():
89
+ data[accessor] = trans_data
90
+ return data
91
+
92
+
93
+ def nest_to_plain(
94
+ data: Union[dict, QueryDict], fields: List[str], accessor="translations"
95
+ ):
96
+ """
97
+ This function is the inverse of plain_to_nest.
98
+ It transforms a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}})
99
+ into a plain dictionary with translations fields (es. {"title_en": "Hello"}).
100
+ """
101
+ if isinstance(data, QueryDict):
102
+ data = data.dict()
103
+ translations = data.pop(accessor, {})
104
+ for lang in AVAILABLE_LANGUAGES:
105
+ nest_trans = translations.pop(lang, {})
106
+ for k in fields:
107
+ data.pop(k, None) # this removes all trans field without lang
108
+ if k in nest_trans:
109
+ # this saves on the default field the default language value
110
+ if lang == DEFAULT_LANGUAGE:
111
+ data[k] = nest_trans[k]
112
+ key = build_localized_fieldname(k, lang)
113
+ data[key] = data.get(key, nest_trans[k])
114
+ return data
@@ -1,8 +1,38 @@
1
- from ..mixins import OptimViewMixin, PaginateStackMixin, OrderingMixin
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
- class BaseModelViewset(
6
- OptimViewMixin, OrderingMixin, PaginateStackMixin, viewsets.ModelViewSet
7
- ):
8
- pass
12
+ base_viewset_classes = [
13
+ CamomillaBasePermissionMixin,
14
+ OptimViewMixin,
15
+ OrderingMixin,
16
+ PaginateStackMixin,
17
+ viewsets.ModelViewSet,
18
+ ]
19
+
20
+
21
+ class BaseViewMetadata(SimpleMetadata):
22
+
23
+ def get_field_info(self, field):
24
+ field_info = super().get_field_info(field)
25
+ if isinstance(field, StructuredJSONField):
26
+ field_info["schema"] = field.schema.json_schema()
27
+ field_info["type"] = "structured-json"
28
+ return field_info
29
+
30
+ def get_serializer_info(self, serializer):
31
+ info = super().get_serializer_info(serializer)
32
+ if hasattr(serializer, "plain_to_nest"):
33
+ info.update(serializer.plain_to_nest(info))
34
+ return info
35
+
36
+
37
+ class BaseModelViewset(*base_viewset_classes):
38
+ metadata_class = BaseViewMetadata
camomilla/views/medias.py CHANGED
@@ -61,7 +61,7 @@ class MediaFolderViewSet(
61
61
  ).data
62
62
  media_data = self.format_output(
63
63
  *self.handle_pagination_stack(media_queryset),
64
- SerializerClass=MediaListSerializer
64
+ SerializerClass=MediaListSerializer,
65
65
  )
66
66
  return {
67
67
  "folders": folder_data,
camomilla/views/menus.py CHANGED
@@ -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()
@@ -1,69 +1,17 @@
1
- from rest_framework import status
2
- from rest_framework.decorators import action
3
- from rest_framework.response import Response
4
- from .pagination import *
5
- from .ordering import *
6
- from ...permissions import CamomillaBasePermissions
7
- from django.utils import translation
8
-
9
-
10
- class GetUserLanguageMixin(object):
11
- def _get_user_language(self, request):
12
- self.active_language = request.GET.get(
13
- "language",
14
- request.GET.get(
15
- "language_code", translation.get_language_from_request(request)
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 self.model.objects.all()
34
-
35
-
36
- class OptimViewMixin:
37
- def get_serializer_class(self):
38
- if hasattr(self, "action_serializers"):
39
- if self.action in self.action_serializers:
40
- return self.action_serializers[self.action]
41
- return super().get_serializer_class()
42
-
43
- def get_serializer_context(self):
44
- return {"request": self.request, "action": self.action}
45
-
46
- def get_queryset(self):
47
- queryset = super().get_queryset()
48
- serializer = self.get_serializer_class()
49
- if hasattr(serializer, "setup_eager_loading"):
50
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
51
- return queryset
52
-
53
-
54
- class BulkDeleteMixin(object):
55
- @action(
56
- detail=False, methods=["post"], permission_classes=(CamomillaBasePermissions,)
57
- )
58
- def bulk_delete(self, request):
59
- try:
60
- self.model.objects.filter(pk__in=request.data).delete()
61
- return Response(
62
- {"detail": "Eliminazione multipla andata a buon fine"},
63
- status=status.HTTP_200_OK,
64
- )
65
- except Exception:
66
- return Response(
67
- {"detail": "Eliminazione multipla non riuscita"},
68
- status=status.HTTP_500_INTERNAL_SERVER_ERROR,
69
- )
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
@@ -2,6 +2,8 @@ from rest_framework.response import Response
2
2
  from django.db.models import Q
3
3
  from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
4
4
  from django.contrib.postgres.search import SearchVector, SearchQuery, TrigramSimilarity
5
+ from camomilla.utils.query_parser import ConditionParser
6
+ from django.conf import settings
5
7
 
6
8
 
7
9
  class TrigramSearchMixin:
@@ -32,19 +34,6 @@ class PaginateStackMixin:
32
34
  list_handler, "shared_model", getattr(list_handler, "model", None)
33
35
  )
34
36
 
35
- def parse_qs_value(self, string: str):
36
- if string and string.startswith("[") and string.endswith("]"):
37
- string = [self.parse_qs_value(substr) for substr in string[1:-1].split(",")]
38
- elif string and string.lower() in ["true", "false"]:
39
- string = string.lower() == "true"
40
- elif string and string.isdigit():
41
- string = int(string)
42
- return string
43
-
44
- def parse_filter(self, filter):
45
- filter_name, value = filter.split("=")
46
- return filter_name, self.parse_qs_value(value)
47
-
48
37
  def handle_pagination(self, list_handler=None, items_per_page=None):
49
38
  list_handler = list_handler if list_handler is not None else self.get_queryset()
50
39
  items_per_page = int(
@@ -84,8 +73,7 @@ class PaginateStackMixin:
84
73
  filters = dict(self.request.GET).get("fltr", [])
85
74
  for filter in filters:
86
75
  try:
87
- filter_name, value = self.parse_filter(filter)
88
- list_handler = list_handler.filter(**{filter_name: value})
76
+ list_handler = list_handler.filter(ConditionParser(filter).db_query)
89
77
  except Exception:
90
78
  pass
91
79
  return list_handler
@@ -95,9 +83,15 @@ class PaginateStackMixin:
95
83
  search_string = self.request.GET.get("search", None)
96
84
  search_fields = search_fields or getattr(self, "search_fields", [])
97
85
  if search_string and len(search_fields) > 0:
98
- return list_handler.annotate(
99
- search=SearchVector(*search_fields),
100
- ).filter(search=SearchQuery(search_string))
86
+ if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
87
+ filter_statement = Q()
88
+ for field in search_fields:
89
+ filter_statement |= Q(**{field + '__icontains': search_string})
90
+ return list_handler.filter(filter_statement)
91
+ else:
92
+ return list_handler.annotate(
93
+ search=SearchVector(*search_fields),
94
+ ).filter(search=SearchQuery(search_string))
101
95
 
102
96
  return list_handler
103
97
 
@@ -0,0 +1,6 @@
1
+ from camomilla.permissions import CamomillaBasePermissions
2
+
3
+
4
+ class CamomillaBasePermissionMixin:
5
+ def get_permissions(self):
6
+ return [*super().get_permissions(), CamomillaBasePermissions()]
camomilla/views/pages.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from camomilla.models import Page
2
- from camomilla.models.page import UrlNode
2
+ from camomilla.models.page import UrlNode, UrlRedirect
3
3
  from camomilla.permissions import CamomillaBasePermissions
4
4
  from camomilla.serializers import PageSerializer
5
5
  from camomilla.serializers.page import UrlNodeSerializer
6
6
  from camomilla.views.base import BaseModelViewset
7
7
  from camomilla.views.decorators import active_lang
8
8
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
9
- from rest_framework.decorators import api_view
9
+ from rest_framework.decorators import api_view, permission_classes
10
10
  from rest_framework.response import Response
11
+ from rest_framework import permissions
11
12
  from django.shortcuts import get_object_or_404
12
13
 
13
14
 
@@ -20,6 +21,15 @@ class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
20
21
 
21
22
  @active_lang()
22
23
  @api_view(["GET"])
24
+ @permission_classes(
25
+ [
26
+ permissions.AllowAny,
27
+ ]
28
+ )
23
29
  def fetch_page(request, permalink=""):
30
+ redirect = UrlRedirect.find_redirect_from_url(f"/{permalink}")
31
+ if redirect:
32
+ redirect = redirect.redirect()
33
+ return Response({"redirect": redirect.url, "status": redirect.status_code})
24
34
  node = get_object_or_404(UrlNode, permalink=f"/{permalink}")
25
35
  return Response(UrlNodeSerializer(node, context={"request": request}).data)
@@ -1,30 +1,33 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-camomilla-cms
3
- Version: 6.0.0b15
3
+ Version: 6.0.0b17
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/lotrekagency/camomilla
7
+ Project-URL: Homepage, https://github.com/camomillacms/camomilla-core
8
8
  Keywords: cms,django,api cms
9
9
  Classifier: Environment :: Web Environment
10
10
  Classifier: Framework :: Django
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python
13
13
  Classifier: Programming Language :: Python :: 3
14
- Requires-Python: >=3.6
14
+ Requires-Python: <=3.13,>=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: django-modeltranslation ~=0.18.11
18
- Requires-Dist: djsuperadmin <1.0.0,>=0.9
19
- Requires-Dist: djangorestframework <4.0.0,>=3.10.0
20
- Requires-Dist: django-admin-interface <1.0.0,>=0.26.0
21
- Requires-Dist: Pillow <10.0.0,>=6.2.0
22
- Requires-Dist: django-ckeditor <7.0.0,>=5.7.1
23
- Requires-Dist: django-structured-json-field ==0.2.0
24
- Requires-Dist: python-magic <0.5,>=0.4
25
- Requires-Dist: Django >=3.2
26
-
27
- # camomilla django cms [![PyPI](https://img.shields.io/pypi/v/django-camomilla-cms?style=flat-square)](https://pypi.org/project/django-camomilla-cms) ![Codecov](https://img.shields.io/codecov/c/github/lotrekagency/camomilla?style=flat-square) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/lotrekagency/camomilla/Test,%20Coverage%20and%20Release?style=flat-square) [![GitHub](https://img.shields.io/github/license/lotrekagency/camomilla?style=flat-square)](./LICENSE)
17
+ Requires-Dist: django-modeltranslation<=0.18.12,>=0.18.7
18
+ Requires-Dist: djsuperadmin<1.0.0,>=0.9
19
+ Requires-Dist: djangorestframework<=3.14.0,>=3.10.0
20
+ Requires-Dist: django-structured-json-field>=0.4.1
21
+ Requires-Dist: Pillow>=10.0.0
22
+ Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
23
+ Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
24
+ Requires-Dist: django-tinymce<5.0.0,>=4.1.0
25
+ Requires-Dist: python-magic<0.5,>=0.4
26
+ Requires-Dist: Django<6,>=3.2
27
+ Requires-Dist: django_jsonform>=2.23
28
+ Dynamic: license-file
29
+
30
+ # camomilla django cms [![PyPI](https://img.shields.io/pypi/v/django-camomilla-cms?style=flat-square)](https://pypi.org/project/django-camomilla-cms) ![Codecov](https://img.shields.io/codecov/c/github/camomillacms/camomilla-core?style=flat-square) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/camomillacms/camomilla-core/Test,%20Coverage%20and%20Release?style=flat-square) [![GitHub](https://img.shields.io/github/license/camomillacms/camomilla-core?style=flat-square)](./LICENSE)
28
31
 
29
32
  ## Install
30
33
 
@@ -35,7 +38,7 @@ $ pip install django-camomilla-cms
35
38
  ## Setup
36
39
  ```shell
37
40
  $ mkdir -p camomilla_migrations
38
- $ touch camomilla_migrations.__init__.py
41
+ $ touch camomilla_migrations/__init__.py
39
42
  $ python manage.py makemigrations camomilla
40
43
  $ python manage.py migrate camomilla
41
44
  ```
@@ -70,3 +73,7 @@ INSTALLED_APPS = [
70
73
 
71
74
  pip install -r requirements-dev.txt
72
75
  make test
76
+
77
+ ## Run format with black
78
+
79
+ black camomilla
@@ -0,0 +1,132 @@
1
+ camomilla/__init__.py,sha256=xH1i4zwYfJScQn-ySoem00poi295diBqrVChxqcmkW8,251
2
+ camomilla/apps.py,sha256=eUwb9ynyiRAc5OXgt7ZsAdhsCOnPCpNdIFYMheNeN-o,532
3
+ camomilla/authentication.py,sha256=jz6tQT4PPEu-_JLox1LZrOy7EiWBb9MWaObK63MJGus,855
4
+ camomilla/context_processors.py,sha256=cGowjDZ-oDGYn1j2Pj5QDGCqnzXAOdOwp5dmzin_FTc,165
5
+ camomilla/defaults.py,sha256=VNQ_sbxu09AyFGNpUUYypIAyhlBhEORD36BBNj7e73I,1220
6
+ camomilla/dynamic_pages_urls.py,sha256=wd52ktpY_LH24jTW77vII7XZ25p_Kz5MSjes8s_94-A,1278
7
+ camomilla/exceptions.py,sha256=gLniAsK_pmsNNKGMv5Z384LXVbM8oeHcOwz4F91u1LY,111
8
+ camomilla/model_api.py,sha256=-7l3fc2eN1itCMzkWA8nFaQXMmz0vs7IlGlShF-gSuo,2487
9
+ camomilla/parsers.py,sha256=fL8XGCGPxJIZNZkPdGtnPSbDP-6-yzGOCVMuLPjkx9Y,1975
10
+ camomilla/permissions.py,sha256=9NlBO4JMmg36vXCUjPNyq6uZxhkdrnXyIbJVLtWhGWE,1813
11
+ camomilla/redirects.py,sha256=ilcyHidb5Iw3jTrXMnPntr50kkl_WB3QOB0VNkIxP7A,263
12
+ camomilla/settings.py,sha256=nY-a1PRhbQ_edvNG5WyndPLWxwsRb_h4eFAjmOHvKYM,3599
13
+ camomilla/sitemap.py,sha256=U2t5TwhB_-sEscmQZ69PZ5st3bIap8NRxzWEvCgB130,786
14
+ camomilla/translation.py,sha256=_QyfTlKG6hQ_ClRfxzeJ-3oI3Nu5peJN9xFkO9Ib3As,1316
15
+ camomilla/urls.py,sha256=XgaeFoG2eXlJQve3KmFKlD-74CMLW1ziaY1mq-lrAiA,2095
16
+ camomilla/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ camomilla/contrib/modeltranslation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ camomilla/contrib/modeltranslation/hvad_migration.py,sha256=3j_q_Q85eF4iHbU4LG1Zr3LOmfmGmFiVSL-C8KvPsJQ,5409
19
+ camomilla/fields/__init__.py,sha256=gKrJwHvUA3q_wu-OVV0hrRZmFkT4znMHrmZtpriDumw,323
20
+ camomilla/fields/json.py,sha256=tWEDn6kwTP6pNB53djxuVPu2d57m9cIDc4ccCEfUbDQ,1938
21
+ camomilla/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ camomilla/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ camomilla/management/commands/regenerate_thumbnails.py,sha256=pKToASR8p8TJezGpFfuylsAHtriNueJ7xqJJxq55adY,496
24
+ camomilla/managers/__init__.py,sha256=Zwp28E1RKafIl0FqcUi1YNHxF19IsMIvhlhS-Njg9Mw,60
25
+ camomilla/managers/pages.py,sha256=DYV1i5vPgxn1YzJlY3kSlsVCRgsiojLxjs2O0LylxEI,3925
26
+ camomilla/models/__init__.py,sha256=y7Q34AGhQ1weJUKWb6XjiyUbwRnJeylOBGMErU0wqYg,147
27
+ camomilla/models/article.py,sha256=LgkZgRsubtDV6NwBz8E2bIgKD6H3I-1QLAxEan5TYYs,1139
28
+ camomilla/models/content.py,sha256=mIgtifb_WMIt58we5u6qWZemHvuDN1zZaBeCyzHL78A,956
29
+ camomilla/models/media.py,sha256=pD-qldiHDOOHgux4lsivQLBcOJJrRx3a4Bg8ODNx7r0,6852
30
+ camomilla/models/menu.py,sha256=hUszPcn1prWCDhk4RPvbITmyhsB2CjFkaerx9t1GWnc,3766
31
+ camomilla/models/page.py,sha256=lkZPdPkl8Yxpz_NfYrpr7Myp8hB3bEAGtdJwKBR7_cg,19385
32
+ camomilla/models/mixins/__init__.py,sha256=c2NixqvrIX4E9WGRqQbylXlqBWDXEqN9mzs_dpB0hFQ,1248
33
+ camomilla/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ camomilla/openapi/schema.py,sha256=C22dhKjaJ2DTK4KWFjyMJXiwe8NLy7ZTW5d-I1dqZ7g,2546
35
+ camomilla/serializers/__init__.py,sha256=8v1GsJ_YZ6T72VnKBb5l-8K93oaLf4PIsMt-yFtK-Gk,176
36
+ camomilla/serializers/article.py,sha256=pYVcS0KztzjzSqgruElQMMEZcqTzmQUqXrdv_Sx5Az4,401
37
+ camomilla/serializers/content_type.py,sha256=qB2wkmkvQI6LHxfSI6auEh6M9cJRFBaHnpmkBCCzeYo,557
38
+ camomilla/serializers/media.py,sha256=H4JVpRVxXVmn_BiqrjihKXpfLm9fLmHDFIICRDGJU4s,1940
39
+ camomilla/serializers/menu.py,sha256=TdoyXs40PqxNevnRbBbYOOX9rUv9zQGiHFNduspaZnw,552
40
+ camomilla/serializers/page.py,sha256=NNjEypVYu_9iKqdHV_-61ea37gxiHlDP5gsloV_i6yg,1834
41
+ camomilla/serializers/user.py,sha256=CzrHiVRvYYWNE4eNpCNKtJB7DjVqHHwIcP4NUBXMHSo,3706
42
+ camomilla/serializers/utils.py,sha256=XRL4CNwQDBNpX8xT7365Dw2Cyx8Kvh18GaadgjS9awk,968
43
+ camomilla/serializers/validators.py,sha256=X2uBlh348nJjUWHPtiu9XKCD7Etsdg0811a4xHLAUzU,2103
44
+ camomilla/serializers/base/__init__.py,sha256=maaL3y6tvc5Ph9y07KVcMvZVYTkzh_3aBiBnGCoi1EA,799
45
+ camomilla/serializers/fields/__init__.py,sha256=0I_E9oMtlC0H48LjStMQZiZ-Ycoy49nWK9GvA5JWxN8,145
46
+ camomilla/serializers/fields/file.py,sha256=yjKMho2ti9TIAzo6nwyLnNPJ6GVUumL2wxhegvYqI2o,800
47
+ camomilla/serializers/fields/related.py,sha256=qQQeUxIZSNqnVRHsXocLGmBNHjAvrlr0eDY9K2uCjWk,5069
48
+ camomilla/serializers/mixins/__init__.py,sha256=gMyFpSWHLtLTlKucP1Xk-GtBNX48CwwyY6_wEN3L9vA,640
49
+ camomilla/serializers/mixins/fields.py,sha256=h-YBHBITLGHsrXRgSYP3NBoUzJqmQ_5HhhftoFFNsKs,711
50
+ camomilla/serializers/mixins/filter_fields.py,sha256=sqnCG1hi_qfISFJvwgh37BLyQoNDifR0pU7zmJn_MW8,2236
51
+ camomilla/serializers/mixins/json.py,sha256=Iq5S7mUg5_Y4bsm6XN3ECJR07NGjCAQ21_wdxj6RgyY,1193
52
+ camomilla/serializers/mixins/language.py,sha256=VukEvPzTpKQfwB-z_RtoNIJ43N3OEgfjLpHvn9KuZDU,1205
53
+ camomilla/serializers/mixins/nesting.py,sha256=gCEU2UE_Y8e4VRnvT0AExFgwTfJm_jnSqa6l2SwZ3Mg,1432
54
+ camomilla/serializers/mixins/optimize.py,sha256=zAtbtk6kfGq9FnapqI8tVYOuMKd1IkHbAV6LffL61Z4,3845
55
+ camomilla/serializers/mixins/ordering.py,sha256=rXQOz47_U4IsMT6IBhySghcmJWMZgpPWHovDcZQG88k,1172
56
+ camomilla/serializers/mixins/page.py,sha256=Ida7dY9MQTv747_dpPGpo6u3iL5GX87z4zeVct-C36s,2132
57
+ camomilla/serializers/mixins/translation.py,sha256=Om2UT2EB4Xvp8SrIEvraY0kJXR9H54AsYBr9DKjsay8,4323
58
+ camomilla/storages/__init__.py,sha256=ytGgX59Ar8vFfYz7eV8z-j5yO_5FqxdZM25iyLnJuSA,131
59
+ camomilla/storages/default.py,sha256=GNzvV_JZpXMcfTkyXjw5CfK8EIBi3o-NXYBO0KAxD5M,351
60
+ camomilla/storages/optimize.py,sha256=VGSXZigzZC8LnPTqyTOpPA2Ba9EJB_KC5bcACoRs4GA,2762
61
+ camomilla/storages/overwrite.py,sha256=jvW3zHvXNzH9dIjeZmmfXo_O3K1ZQmLQzmlSKAOE8ZA,360
62
+ camomilla/templates/admin/camomilla/page/change_form.html,sha256=ig7rRUtylDZMINBQuVPpZLmeB4sOTV_VtqnTgzAyxEo,251
63
+ camomilla/templates/defaults/base.html,sha256=pklt7Pif3g9d7gwgRxCQj7gniJaHD14ZqZID_xIlC0A,6638
64
+ camomilla/templates/defaults/articles/default.html,sha256=1f89jBvNtTa1mPAbC91yy8CzeAjTWO3hhQsTuQW5OKg,239
65
+ camomilla/templates/defaults/pages/default.html,sha256=bP81Qb6M56I-fBJMywWwEu_cnERtWIX28UkGrUSRU6M,144
66
+ camomilla/templates/defaults/parts/langswitch.html,sha256=AkaQzb2KNjRYCMLUn_jE31V36rwBIwp4MneirWPiBcI,3424
67
+ camomilla/templates/defaults/parts/menu.html,sha256=ReE-FfmfCNuNkJI33QqIfmMgLSBl3FTkWAhEa59aD3A,381
68
+ camomilla/templates/defaults/widgets/media_select_multiple.html,sha256=k2XYou8KkPuFLnPMkPJAFJ-zGJj2Xvu6R3ZmiKa3g7Q,3727
69
+ camomilla/templates_context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
+ camomilla/templates_context/autodiscover.py,sha256=td7SCsqg3iNPnv1HDEDEpwWgWLjy5Zmc8Nbze1_J46I,1907
71
+ camomilla/templates_context/rendering.py,sha256=GfTR45_gC7WT7zTKPVXkBDwe22uF63A-DfZUW31woAU,3194
72
+ camomilla/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
+ camomilla/templatetags/camomilla_filters.py,sha256=35x0-cWrRHeLhqypSLlJzEFY_fQDcRHiwZQpFgIsspE,692
74
+ camomilla/templatetags/menus.py,sha256=7fc4f9DDqtqG6wNb5_Q0km-fq0mqvGnbpR21qO1TJUw,960
75
+ camomilla/theme/__init__.py,sha256=3ahSYJ-HBLHrW2VZ45mAM6mlZvpqDKPffxhE0EkD9as,30
76
+ camomilla/theme/apps.py,sha256=3nCSZ6d4tkx2aMNNnQQoqX8PGrrMNf4VxCxBV_JXNrA,1040
77
+ camomilla/theme/admin/__init__.py,sha256=TALAZaE-gWshSeGc6yy7VahdX5UfeCeoOE9Q5kJCEpM,2270
78
+ camomilla/theme/admin/pages.py,sha256=y3rL1nwZlytyD-YR_qqLiBAmjCAjkBY3v56V6JdhBvY,1908
79
+ camomilla/theme/admin/translations.py,sha256=iAjGM1A1aYrsz1FpeybROk6rn3Ddl_oUCwgU5oD8nSw,308
80
+ camomilla/theme/static/admin/css/responsive.css,sha256=yGq6qXrr8xEVsXTnprIBgkX-sMGZrNf0Kkh-xDxf6yE,157
81
+ camomilla/theme/static/admin/img/favicon.ico,sha256=qpKv_2MaGILvyihnD1Vq9Yk-ZXGkxWTW26ciMeBFMYU,15406
82
+ camomilla/theme/static/admin/img/logo.svg,sha256=WBAORIV_LzEOdcpq7iU6d0Nu3omgk2gn05f6LnPjfWg,3238
83
+ camomilla/theme/templates/admin/base.html,sha256=veKZ6KlnUYILeY4I2MYI18BbZMDec9MgzVzUqCHnjDc,254
84
+ camomilla/theme/templates/rosetta/base.html,sha256=s9Ijf1nZx8um30R5Gk3g-w-RwFGv2QGViHufyLLiiDY,16731
85
+ camomilla/utils/__init__.py,sha256=Ui7nzSh45UMMFtCGF1xFHKyJMNWflG_Nx72HAJHJHFM,100
86
+ camomilla/utils/getters.py,sha256=6j18grFAZ8BC70SriycFDTQFxTnudGn0uKGA83_Rclk,798
87
+ camomilla/utils/normalization.py,sha256=RDCZtjwpEEwjvfUjQl2bEWFKw7NxTzkXco72VeO2M9w,255
88
+ camomilla/utils/query_parser.py,sha256=TUScPzPVVJzaKdqy5NqtMOft3H5Bx6liXTVPM1yjH24,6303
89
+ camomilla/utils/seo.py,sha256=8p_a_TGgohenpJb094tT4mMxbn2xzW0qDILuTnjNocM,3324
90
+ camomilla/utils/setters.py,sha256=LV57SM65rL1_ZQkVzk9al_Q13lndVywXLkqgfIvgS0Y,915
91
+ camomilla/utils/templates.py,sha256=Lv4-5019cnM30HmdZnYWiU5gxry-eFZVAhwOofGQRDs,598
92
+ camomilla/utils/translation.py,sha256=w5tvTInDLegWBb1TnDWo09ckKY3K6hajuNNsngZIxPQ,4205
93
+ camomilla/views/__init__.py,sha256=94QuOnnbfMMb17mruO2ydUt286-8zBmDxEPWrJv5Wog,178
94
+ camomilla/views/articles.py,sha256=qGxebOA5iTbGGe9PfbH40YBoDPKktH8FJongg6rh2R8,571
95
+ camomilla/views/contents.py,sha256=JxvnmgeK8JEmCMLzVG8pVq2DwvmjXtgnIdsDnn74tA4,1205
96
+ camomilla/views/decorators.py,sha256=hR--nTGQn2mMKDrWn-0Ildzbsvp11OfoWAtedKEzmiA,982
97
+ camomilla/views/languages.py,sha256=Rt_X7s3dbDBv4dxsQ9fnav_u0TAzzo8fGKBBx3esDsg,441
98
+ camomilla/views/medias.py,sha256=XYa-NTLLQmSSynpfrFT3av-K_r59aRns3dTGfYMj-0Q,3002
99
+ camomilla/views/menus.py,sha256=Kpygnf3tMKJ30gcblUES2NW83A37Vy75ecSGSvExGKM,3301
100
+ camomilla/views/pages.py,sha256=UL74_u-18QdAkjVl74AVWZbRarEdIPrANTzdcM4iqmE,1338
101
+ camomilla/views/tags.py,sha256=XcYRlcBFSPPY32lt7POb6fWPJL_8HsTo5JcHcAOiOKw,479
102
+ camomilla/views/users.py,sha256=_fvsKOEtep4SJLvMva2_q-HdLQT_1KlFNt4wcl3xCJk,3130
103
+ camomilla/views/base/__init__.py,sha256=t-7tqY_ep4Xi8YgB1sXDgNWQ5oh2YEUlfQWU5pltwJ0,1063
104
+ camomilla/views/mixins/__init__.py,sha256=Znv3fLYVy6lgu03Q_D8fTen4zMxI6VSRaLPDU8Cp7Ws,473
105
+ camomilla/views/mixins/bulk_actions.py,sha256=i0duWW6wey9m7I_V8-gPcHsbJyPEfSdMdj4h2i-CbPw,787
106
+ camomilla/views/mixins/language.py,sha256=hfnYznlVMrMLBdJ_f8dChJWENg7Kpt9m1yqavrdLm7E,1299
107
+ camomilla/views/mixins/optimize.py,sha256=iRPNkoeIIlJugk7DjJhDPaqeX7Opi7TxnUoMDnxJxUk,686
108
+ camomilla/views/mixins/ordering.py,sha256=mh7fqPyVCVJh84Nl2pYFQouzGxa-ANF3Wqv0pCb7OVU,4779
109
+ camomilla/views/mixins/pagination.py,sha256=NWerBdMyBt4Kswig4fbANqGTzsll8SJdE6a8_UIoueU,5772
110
+ camomilla/views/mixins/permissions.py,sha256=TPmR3Hoa3BjeJu9rCE_7lpLOAupue4WI42C21HTo6X4,200
111
+ django_camomilla_cms-6.0.0b17.dist-info/licenses/LICENSE,sha256=kVS7zDrNkav2hLLXbOJwVdonY2ToApTK3khyJagGQoQ,1063
112
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
+ tests/test_api.py,sha256=t03EFDezGgm4UJl8RIVvnTUkAGTB6ptm0G2lHBQ7ljc,1833
114
+ tests/test_camomilla_filters.py,sha256=5LlR3tctGu6qxVmOrY52AGh_ACvEzdAvkwH2v7medpo,1536
115
+ tests/test_media.py,sha256=IG-cigWxDnsq54DRVaPO2O0lcsnutfKRsqPTTR8W1E8,4183
116
+ tests/test_menu.py,sha256=HyEdDzTDsLtNzsTNGjjZ6SCrgTXRU3FkKviUOacnYRg,3582
117
+ tests/test_model_api.py,sha256=Ne8YlXTH2cqP5gzOc8UKjJuh0t-NaKHh5Ol9krpVHQg,3768
118
+ tests/test_model_api_permissions.py,sha256=7CSb4-yIOfycAL_vXvh1dE2whx7k0gNkWl9LO0yzy4I,1801
119
+ tests/test_model_api_register.py,sha256=9pqf7fvtniw63ZD4P2JItfDC0brD55vIrqjZ4phm_qs,14016
120
+ tests/test_models.py,sha256=WJs8lxWZWn1l7X3a_QFVc8fF5LHTsI8bc3uhQe6-o-Q,684
121
+ tests/test_pages.py,sha256=qIVdfmbtx7GKHyNWHhirR58gGr9zjfrrzXXkvYlLusc,11469
122
+ tests/test_query_parser.py,sha256=R9l0L2QDEDcm2b6IFUhyf7wMXLzL9RySLkzKTWRtBkE,2097
123
+ tests/test_templates_context.py,sha256=D72ufRqCGjInGGXHSNVhlJ1HcWG0zMqrAiTuiaU057k,4694
124
+ tests/test_utils.py,sha256=o_FG7XOxLePOBfwBr4sk09gej0onWNw9t2-gSjGmgNg,3741
125
+ tests/fixtures/__init__.py,sha256=NGj22kLV65v56IpOrOVqSkPhJePTXD4QjuuZhZSMwfQ,460
126
+ tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
+ tests/utils/api.py,sha256=TYcDXeILHtBwzwG0acwPFmiqMZnlF9VnLB0Ydhg55vA,865
128
+ tests/utils/media.py,sha256=ChsoHqwWmVYHE7teFsV9swTqBEFD0zcvQSBuKsK9G_s,298
129
+ django_camomilla_cms-6.0.0b17.dist-info/METADATA,sha256=oiU3jK3ZCjQurWBdq2vaUZgBr2OMim4RLEjSPtIVm28,2565
130
+ django_camomilla_cms-6.0.0b17.dist-info/WHEEL,sha256=oSJJyWjO7Z2XSScFQUpXG1HL-N0sFMqqeKVVbZTPkWc,109
131
+ django_camomilla_cms-6.0.0b17.dist-info/top_level.txt,sha256=G9VIGBmMMqC7JEckoTgXKmC6T2BR75QRkqRnngw1_lo,16
132
+ django_camomilla_cms-6.0.0b17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -0,0 +1,17 @@
1
+ import json
2
+ import os
3
+ from django.core.files.uploadedfile import SimpleUploadedFile
4
+
5
+
6
+ def load_json_fixture(filename):
7
+ with open(os.path.join(os.path.dirname(__file__), 'json', filename), "r") as f:
8
+ return json.load(f)
9
+
10
+
11
+ def load_asset(filename):
12
+ with open(os.path.join(os.path.dirname(__file__), 'assets', filename), "rb") as f:
13
+ up_file = SimpleUploadedFile(
14
+ filename,
15
+ f.read()
16
+ )
17
+ return up_file