django-camomilla-cms 6.0.0b14__tar.gz → 6.0.0b15__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.
Files changed (119) hide show
  1. {django-camomilla-cms-6.0.0b14/django_camomilla_cms.egg-info → django_camomilla_cms-6.0.0b15}/PKG-INFO +2 -3
  2. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/__init__.py +1 -1
  3. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/dynamic_pages_urls.py +2 -1
  4. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/fields/__init__.py +1 -3
  5. django_camomilla_cms-6.0.0b15/camomilla/managers/pages.py +31 -0
  6. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/menu.py +21 -10
  7. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/page.py +75 -38
  8. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/fields/__init__.py +2 -2
  9. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/fields/json.py +3 -2
  10. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/mixins/__init__.py +5 -12
  11. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/validators.py +9 -5
  12. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/settings.py +0 -4
  13. django_camomilla_cms-6.0.0b15/camomilla/theme/__init__.py +1 -0
  14. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/admin.py +1 -1
  15. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/translation.py +1 -1
  16. django_camomilla_cms-6.0.0b15/camomilla/utils/setters.py +37 -0
  17. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/menus.py +0 -1
  18. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15/django_camomilla_cms.egg-info}/PKG-INFO +2 -3
  19. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/django_camomilla_cms.egg-info/SOURCES.txt +1 -5
  20. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/django_camomilla_cms.egg-info/requires.txt +1 -2
  21. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/pyproject.toml +1 -2
  22. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/setup.py +1 -1
  23. django-camomilla-cms-6.0.0b14/camomilla/managers/pages.py +0 -42
  24. django-camomilla-cms-6.0.0b14/camomilla/structured/__init__.py +0 -125
  25. django-camomilla-cms-6.0.0b14/camomilla/structured/cache.py +0 -202
  26. django-camomilla-cms-6.0.0b14/camomilla/structured/fields.py +0 -150
  27. django-camomilla-cms-6.0.0b14/camomilla/structured/models.py +0 -47
  28. django-camomilla-cms-6.0.0b14/camomilla/structured/utils.py +0 -114
  29. django-camomilla-cms-6.0.0b14/camomilla/theme/__init__.py +0 -1
  30. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/LICENSE +0 -0
  31. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/MANIFEST.in +0 -0
  32. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/README.md +0 -0
  33. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/apps.py +0 -0
  34. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/authentication.py +0 -0
  35. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/context_processors.py +0 -0
  36. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/contrib/__init__.py +0 -0
  37. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/contrib/modeltranslation/__init__.py +0 -0
  38. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/contrib/modeltranslation/hvad_migration.py +0 -0
  39. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/contrib/rest_framework/__init__.py +0 -0
  40. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/contrib/rest_framework/serializer.py +0 -0
  41. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/defaults.py +0 -0
  42. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/exceptions.py +0 -0
  43. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/fields/json.py +0 -0
  44. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/management/__init__.py +0 -0
  45. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/management/commands/__init__.py +0 -0
  46. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/management/commands/regenerate_thumbnails.py +0 -0
  47. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/managers/__init__.py +0 -0
  48. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/model_api.py +0 -0
  49. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/__init__.py +0 -0
  50. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/article.py +0 -0
  51. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/content.py +0 -0
  52. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/media.py +0 -0
  53. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/models/mixins/__init__.py +0 -0
  54. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/openapi/__init__.py +0 -0
  55. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/openapi/schema.py +0 -0
  56. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/parsers.py +0 -0
  57. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/permissions.py +0 -0
  58. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/__init__.py +0 -0
  59. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/article.py +0 -0
  60. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/base/__init__.py +0 -0
  61. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/content_type.py +0 -0
  62. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/fields/file.py +0 -0
  63. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/fields/related.py +0 -0
  64. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/media.py +0 -0
  65. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/menu.py +0 -0
  66. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/page.py +0 -0
  67. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/user.py +0 -0
  68. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/serializers/utils.py +0 -0
  69. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/sitemap.py +0 -0
  70. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/storages/__init__.py +0 -0
  71. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/storages/optimize.py +0 -0
  72. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/storages/overwrite.py +0 -0
  73. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/admin/camomilla/page/change_form.html +0 -0
  74. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/articles/default.html +0 -0
  75. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/base.html +0 -0
  76. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/pages/default.html +0 -0
  77. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/parts/langswitch.html +0 -0
  78. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/parts/menu.html +0 -0
  79. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates/defaults/widgets/media_select_multiple.html +0 -0
  80. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates_context/__init__.py +0 -0
  81. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates_context/autodiscover.py +0 -0
  82. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templates_context/rendering.py +0 -0
  83. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templatetags/__init__.py +0 -0
  84. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templatetags/camomilla_filters.py +0 -0
  85. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/templatetags/menus.py +0 -0
  86. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/apps.py +0 -0
  87. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/static/admin/css/responsive.css +0 -0
  88. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/static/admin/img/favicon.ico +0 -0
  89. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/static/admin/img/logo.svg +0 -0
  90. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/templates/admin/base.html +0 -0
  91. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/theme/templates/rosetta/base.html +0 -0
  92. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/urls.py +0 -0
  93. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/__init__.py +0 -0
  94. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/getters.py +0 -0
  95. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/normalization.py +0 -0
  96. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/seo.py +0 -0
  97. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/templates.py +0 -0
  98. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/utils/translation.py +0 -0
  99. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/__init__.py +0 -0
  100. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/articles.py +0 -0
  101. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/base/__init__.py +0 -0
  102. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/contents.py +0 -0
  103. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/decorators.py +0 -0
  104. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/languages.py +0 -0
  105. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/medias.py +0 -0
  106. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/mixins/__init__.py +0 -0
  107. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/mixins/ordering.py +0 -0
  108. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/mixins/pagination.py +0 -0
  109. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/pages.py +0 -0
  110. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/tags.py +0 -0
  111. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/camomilla/views/users.py +0 -0
  112. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/django_camomilla_cms.egg-info/dependency_links.txt +0 -0
  113. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/django_camomilla_cms.egg-info/top_level.txt +0 -0
  114. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/setup.cfg +0 -0
  115. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/tests/__init__.py +0 -0
  116. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/tests/test_api.py +0 -0
  117. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/tests/test_camomilla_filters.py +0 -0
  118. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/tests/test_models.py +0 -0
  119. {django-camomilla-cms-6.0.0b14 → django_camomilla_cms-6.0.0b15}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-camomilla-cms
3
- Version: 6.0.0b14
3
+ Version: 6.0.0b15
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
@@ -20,10 +20,9 @@ Requires-Dist: djangorestframework<4.0.0,>=3.10.0
20
20
  Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
21
21
  Requires-Dist: Pillow<10.0.0,>=6.2.0
22
22
  Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
23
+ Requires-Dist: django-structured-json-field==0.2.0
23
24
  Requires-Dist: python-magic<0.5,>=0.4
24
- Requires-Dist: django-jsonform~=2.19.0
25
25
  Requires-Dist: Django>=3.2
26
- Requires-Dist: pydantic~=2.2.1
27
26
 
28
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)
29
28
 
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.14"
1
+ __version__ = "6.0.0-beta.15"
2
2
 
3
3
 
4
4
  def get_core_apps():
@@ -7,7 +7,8 @@ from .models import Page
7
7
 
8
8
 
9
9
  def fetch(request, *args, **kwargs):
10
- preview = request.user.is_staff and request.GET.get("preview", False)
10
+ can_preview = request.user.is_staff or settings.DEBUG
11
+ preview = can_preview and request.GET.get("preview", False)
11
12
  append_slash = getattr(django_settings, "APPEND_SLASH", True)
12
13
  if append_slash and not request.path.endswith("/"):
13
14
  return redirect(request.path + "/")
@@ -1,7 +1,5 @@
1
1
  from django.db import models
2
2
 
3
- from camomilla.structured import StructuredJSONField
4
-
5
3
  from .json import ArrayField, JSONField
6
4
 
7
5
  ORDERING_ACCEPTED_FIELDS = (
@@ -12,4 +10,4 @@ ORDERING_ACCEPTED_FIELDS = (
12
10
  models.SmallIntegerField,
13
11
  )
14
12
 
15
- __all__ = ["StructuredJSONField", "JSONField", "ArrayField", "ORDERING_ACCEPTED_FIELDS"]
13
+ __all__ = ["JSONField", "ArrayField", "ORDERING_ACCEPTED_FIELDS"]
@@ -0,0 +1,31 @@
1
+ from django.db.models.query import QuerySet
2
+ from django.core.exceptions import ObjectDoesNotExist
3
+ from django.apps import apps
4
+
5
+ URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
6
+
7
+
8
+ class PageQuerySet(QuerySet):
9
+
10
+ __UrlNodeModel = None
11
+
12
+ @property
13
+ def UrlNodeModel(self):
14
+ if not self.__UrlNodeModel:
15
+ self.__UrlNodeModel = apps.get_model("camomilla", "UrlNode")
16
+ return self.__UrlNodeModel
17
+
18
+ def get_permalink_kwargs(self, kwargs):
19
+ return list(set(kwargs.keys()).intersection(set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])))
20
+
21
+ def get(self, *args, **kwargs):
22
+ permalink_args = self.get_permalink_kwargs(kwargs)
23
+ if len(permalink_args):
24
+ try:
25
+ node = self.UrlNodeModel.objects.get(**{arg:kwargs.pop(arg) for arg in permalink_args})
26
+ kwargs["url_node"] = node
27
+ except ObjectDoesNotExist:
28
+ raise self.model.DoesNotExist(
29
+ "%s matching query does not exist." % self.model._meta.object_name
30
+ )
31
+ return super(PageQuerySet, self).get(*args, **kwargs)
@@ -12,9 +12,12 @@ from pydantic import (
12
12
  computed_field,
13
13
  model_serializer,
14
14
  )
15
- from camomilla import structured
16
- from camomilla.models.page import UrlNode
15
+ from structured.pydantic.models import BaseModel
16
+ from structured.fields import StructuredJSONField
17
+ from structured.pydantic.fields import QuerySet
18
+ from camomilla.models.page import UrlNode, AbstractPage
17
19
  from typing import Optional, Union, Callable, List
20
+ from django.db.models.base import Model as DjangoModel
18
21
 
19
22
 
20
23
  class LinkTypes(str, Enum):
@@ -22,21 +25,29 @@ class LinkTypes(str, Enum):
22
25
  static = "ST"
23
26
 
24
27
 
25
- class MenuNodeLink(structured.BaseModel):
28
+ class MenuNodeLink(BaseModel):
26
29
  link_type: LinkTypes = LinkTypes.static
27
30
  static: str = None
28
- content_type: int = None
29
- page_id: int = None
31
+ content_type: ContentType = None
32
+ page: AbstractPage = None
30
33
  url_node: UrlNode = None
31
34
 
32
35
  @model_serializer(mode="wrap", when_used="json")
33
36
  def update_relational(self, handler: Callable, info: SerializationInfo):
34
37
  if self.link_type == LinkTypes.relational:
35
- if self.content_type and self.page_id:
36
- c_type = ContentType.objects.filter(pk=self.content_type).first()
38
+ if self.content_type and self.page:
39
+ if isinstance(self.page, DjangoModel) and not self.page._meta.abstract:
40
+ self.content_type = ContentType.objects.get_for_model(self.page.__class__)
41
+ ctype_id = getattr(self.content_type, "pk", self.content_type)
42
+ page_id = getattr(self.page, "pk", self.page)
43
+ c_type = ContentType.objects.filter(pk=ctype_id).first()
37
44
  model = c_type and c_type.model_class()
38
- page = model and model.objects.filter(pk=self.page_id).first()
45
+ page = model and model.objects.filter(pk=page_id).first()
39
46
  self.url_node = page and page.url_node
47
+ elif self.url_node:
48
+ url_node_id = getattr(self.url_node, "pk", self.url_node)
49
+ self.page = UrlNode.objects.filter(pk=url_node_id).first().page
50
+ self.content_type = ContentType.objects.get_for_model(self.page.__class__)
40
51
  return handler(self)
41
52
 
42
53
  def get_url(self, request=None):
@@ -51,7 +62,7 @@ class MenuNodeLink(structured.BaseModel):
51
62
  return self.get_url()
52
63
 
53
64
 
54
- class MenuNode(structured.BaseModel):
65
+ class MenuNode(BaseModel):
55
66
  id: str = Field(default_factory=uuid4)
56
67
  meta: dict = {}
57
68
  nodes: List["MenuNode"] = []
@@ -63,7 +74,7 @@ class Menu(models.Model):
63
74
  key = models.CharField(max_length=200, unique=True, editable=False)
64
75
  available_classes = models.JSONField(default=dict, editable=False)
65
76
  enabled = models.BooleanField(default=True)
66
- nodes = structured.StructuredJSONField(default=list, schema=MenuNode)
77
+ nodes = StructuredJSONField(default=list, schema=MenuNode)
67
78
 
68
79
  class Meta:
69
80
  verbose_name = _("menu")
@@ -2,7 +2,6 @@ from typing import Sequence, Tuple
2
2
  from uuid import uuid4
3
3
 
4
4
  from django.core.exceptions import ObjectDoesNotExist
5
- from django.core.validators import RegexValidator
6
5
 
7
6
  from django.db import ProgrammingError, OperationalError, models, transaction
8
7
  from django.db.models.signals import post_delete
@@ -13,6 +12,7 @@ from django.utils import timezone
13
12
  from django.utils.functional import lazy
14
13
  from django.utils.text import slugify
15
14
  from django.utils.translation import gettext_lazy as _
15
+ from django.utils.translation import get_language
16
16
 
17
17
  from camomilla.managers.pages import PageQuerySet
18
18
  from camomilla.models.mixins import MetaMixin, SeoMixin
@@ -29,8 +29,13 @@ from camomilla.utils.getters import pointed_getter
29
29
  from camomilla import settings
30
30
  from camomilla.templates_context.rendering import ctx_registry
31
31
  from django.conf import settings as django_settings
32
+ from modeltranslation.settings import AVAILABLE_LANGUAGES
33
+ from modeltranslation.utils import build_localized_fieldname
32
34
 
33
35
 
36
+ class UrlPathValidator():
37
+ pass
38
+
34
39
  def GET_TEMPLATE_CHOICES():
35
40
  return [(t, t) for t in get_all_templates_files()]
36
41
 
@@ -111,6 +116,14 @@ class UrlNodeManager(models.Manager):
111
116
 
112
117
 
113
118
  class UrlNode(models.Model):
119
+
120
+ LANG_PERMALINK_FIELDS = [
121
+ build_localized_fieldname("permalink", lang)
122
+ for lang in AVAILABLE_LANGUAGES
123
+ if settings.ENABLE_TRANSLATIONS
124
+ ]
125
+
126
+
114
127
  permalink = models.CharField(max_length=400, unique=True, null=True)
115
128
  related_name = models.CharField(max_length=200)
116
129
  objects = UrlNodeManager()
@@ -140,6 +153,23 @@ class UrlNode(models.Model):
140
153
  if self.routerlink == "/":
141
154
  return ""
142
155
  return self.routerlink
156
+
157
+ @staticmethod
158
+ def sanitize_permalink(permalink):
159
+ if isinstance(permalink, str):
160
+ p_parts = permalink.split("/")
161
+ permalink = "/".join([slugify(p, allow_unicode=True).strip() for p in p_parts])
162
+ if not permalink.startswith("/"):
163
+ permalink = f"/{permalink}"
164
+ return permalink
165
+
166
+ def save(self, *args, **kwargs) -> None:
167
+ for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
168
+ setattr(self, lang_p_field, UrlNode.sanitize_permalink(getattr(self, lang_p_field)))
169
+ super().save(*args, **kwargs)
170
+
171
+ def __str__(self) -> str:
172
+ return self.permalink
143
173
 
144
174
 
145
175
  PAGE_CHILD_RELATED_NAME = "%(app_label)s_%(class)s_child_pages"
@@ -154,11 +184,28 @@ PAGE_STATUS = (
154
184
 
155
185
 
156
186
  class PageBase(models.base.ModelBase):
187
+ """
188
+ This models comes to implement a language based permalink logic
189
+ """
190
+ def perm_prop_factory(permalink_field):
191
+ def getter(_self):
192
+ return getattr(_self, f"__{permalink_field}", getattr(_self.url_node or object(), permalink_field, None))
193
+ def setter(_self, value:str):
194
+ setattr(_self, f"__{permalink_field}", value)
195
+ return getter, setter
196
+
157
197
  def __new__(cls, name, bases, attrs, **kwargs):
158
198
  attr_meta = attrs.pop("PageMeta", None)
159
199
  new_class = super().__new__(cls, name, bases, attrs, **kwargs)
160
200
  page_meta = attr_meta or getattr(new_class, "PageMeta", None)
161
201
  base_page_meta = getattr(new_class, "_page_meta", None)
202
+ for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
203
+ computed_prop = property(*cls.perm_prop_factory(lang_p_field))
204
+ setattr(new_class, lang_p_field, computed_prop)
205
+ setattr(new_class, "permalink", property(
206
+ lambda _self: getattr(_self, build_localized_fieldname("permalink", get_language()), None),
207
+ lambda _self, value: setattr(_self, f"__{build_localized_fieldname('permalink', get_language())}", value)
208
+ ))
162
209
  if page_meta:
163
210
  for name, value in getattr(base_page_meta, "__dict__", {}).items():
164
211
  if name not in page_meta.__dict__:
@@ -167,16 +214,6 @@ class PageBase(models.base.ModelBase):
167
214
  return new_class
168
215
 
169
216
 
170
- class UrlPathValidator(RegexValidator):
171
-
172
- regex = r"^[a-zA-Z0-9_\-\/]+[^\/]$"
173
- message = _(
174
- "Enter a valid 'slug' consisting of lowercase letters, numbers, "
175
- "underscores, hyphens and slashes."
176
- )
177
- flags = 0
178
-
179
-
180
217
  class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
181
218
  date_created = models.DateTimeField(auto_now_add=True)
182
219
  date_updated_at = models.DateTimeField(auto_now=True)
@@ -188,9 +225,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
188
225
  editable=False,
189
226
  )
190
227
  breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
191
- slug = models.CharField(
192
- max_length=150, null=True, blank=True, validators=[UrlPathValidator()]
193
- )
228
+ autopermalink = models.BooleanField(default=True)
194
229
  status = models.CharField(
195
230
  max_length=3,
196
231
  choices=PAGE_STATUS,
@@ -211,6 +246,19 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
211
246
  )
212
247
 
213
248
  objects = PageQuerySet.as_manager()
249
+
250
+ __cached_db_instance: "AbstractPage" = None
251
+
252
+ @property
253
+ def db_instance(self):
254
+ if self.__cached_db_instance is None:
255
+ self.__cached_db_instance = self.get_db_instance()
256
+ return self.__cached_db_instance
257
+
258
+ def get_db_instance(self):
259
+ if self.pk:
260
+ return self.__class__.objects.get(pk=self.pk)
261
+ return None
214
262
 
215
263
  def __init__(self, *args, **kwargs):
216
264
  super(AbstractPage, self).__init__(*args, **kwargs)
@@ -240,10 +288,6 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
240
288
  def model_info(self) -> dict:
241
289
  return {"app_label": self._meta.app_label, "class": self._meta.model_name}
242
290
 
243
- @property
244
- def permalink(self) -> str:
245
- return self.url_node and self.url_node.permalink
246
-
247
291
  @property
248
292
  def routerlink(self) -> str:
249
293
  return self.url_node and self.url_node.routerlink
@@ -252,7 +296,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
252
296
  def breadcrumbs(self) -> Sequence[dict]:
253
297
  breadcrumb = {
254
298
  "permalink": self.permalink,
255
- "title": self.breadcrumbs_title or self.title or self.slug,
299
+ "title": self.breadcrumbs_title or self.title or "",
256
300
  }
257
301
  if self.parent:
258
302
  return self.parent.breadcrumbs + [breadcrumb]
@@ -291,8 +335,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
291
335
  def _update_url_node(self, force: bool = False) -> UrlNode:
292
336
  self.url_node = self._get_or_create_url_node()
293
337
  for __ in activate_languages():
294
- old_permalink = self.permalink
295
- new_permalink = self.generate_permalink()
338
+ old_permalink = self.db_instance and self.db_instance.permalink
339
+ new_permalink = self.permalink
340
+ if self.autopermalink:
341
+ new_permalink = self.generate_permalink()
296
342
  force = force or old_permalink != new_permalink
297
343
  set_nofallbacks(self.url_node, "permalink", new_permalink)
298
344
  if force:
@@ -301,22 +347,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
301
347
  return self.url_node
302
348
 
303
349
  def generate_permalink(self, safe: bool = True) -> str:
304
- slug = get_nofallbacks(self, "slug")
305
- if slug is None and not self.permalink:
306
- translations = get_field_translations(self, "slug").values()
307
- fallback_slug = next((t for t in translations if t is not None), None)
308
- slug = (
309
- slugify(self.title or uuid4(), allow_unicode=True)
310
- if fallback_slug is None
311
- else fallback_slug
312
- )
313
- set_nofallbacks(self, "slug", slug)
314
- slug_parts = (slug or "").split("/")
315
- for i, part in enumerate(slug_parts):
316
- slug_parts[i] = slugify(part, allow_unicode=True)
317
- permalink = "/%s" % "/".join(slug_parts)
350
+ permalink = f"/{slugify(self.title or '', allow_unicode=True)}"
318
351
  if self.parent:
319
- permalink = f"{self.parent.permalink}{permalink}"
352
+ permalink = f"/{self.parent.permalink}{permalink}"
353
+ set_nofallbacks(self, "permalink", permalink)
320
354
  qs = UrlNode.objects.exclude(pk=getattr(self.url_node or object, "pk", None))
321
355
  if safe and qs.filter(permalink=permalink).exists():
322
356
  permalink = "/".join(
@@ -333,7 +367,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
333
367
  def save(self, *args, **kwargs) -> None:
334
368
  with transaction.atomic():
335
369
  self._update_url_node()
336
- return super().save(*args, **kwargs)
370
+ super().save(*args, **kwargs)
371
+ self.__cached_db_instance = None
372
+ for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
373
+ hasattr(self, f"__{lang_p_field}") and delattr(self, f"__{lang_p_field}")
337
374
 
338
375
  @classmethod
339
376
  def get(cls, request, *args, **kwargs) -> "AbstractPage":
@@ -386,7 +423,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
386
423
  node = UrlNode.objects.get(permalink="/")
387
424
  return node.page, False
388
425
  except UrlNode.DoesNotExist:
389
- return cls.get_or_create(None, slug="")
426
+ return cls.get_or_create(None, permalink="/")
390
427
 
391
428
  @classmethod
392
429
  def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
@@ -1,7 +1,7 @@
1
1
  from django.db import models
2
2
  from rest_framework import serializers
3
3
 
4
- from camomilla import structured
4
+ from structured.fields import StructuredJSONField as ModelStructuredJSONField
5
5
 
6
6
  from .json import StructuredJSONField
7
7
  from .file import FileField, ImageField
@@ -16,6 +16,6 @@ class FieldsOverrideMixin:
16
16
  **serializers.ModelSerializer.serializer_field_mapping,
17
17
  models.FileField: FileField,
18
18
  models.ImageField: ImageField,
19
- structured.StructuredJSONField: StructuredJSONField,
19
+ ModelStructuredJSONField: StructuredJSONField,
20
20
  }
21
21
  serializer_related_field = RelatedField
@@ -3,10 +3,11 @@ from rest_framework import serializers
3
3
  from rest_framework.utils import model_meta
4
4
  from typing import TYPE_CHECKING, Any, Union, Dict, List
5
5
 
6
- from camomilla.structured.utils import pointed_setter
6
+ from camomilla.utils.setters import pointed_setter
7
7
 
8
8
  if TYPE_CHECKING:
9
- from camomilla.structured import BaseModel
9
+ from structured.pydantic.models import BaseModel
10
+
10
11
 
11
12
 
12
13
  class StructuredJSONField(serializers.JSONField):
@@ -3,11 +3,10 @@ from django.conf import settings as django_settings
3
3
  from django.db.models.aggregates import Max
4
4
  from django.db.models.functions import Coalesce
5
5
  from django.utils import translation
6
- from modeltranslation.settings import AVAILABLE_LANGUAGES
7
- from modeltranslation.utils import build_localized_fieldname
8
6
  from rest_framework import serializers
9
7
  from rest_framework.utils import model_meta
10
8
 
9
+ from camomilla.models import UrlNode
11
10
  from camomilla.fields import ORDERING_ACCEPTED_FIELDS
12
11
  from camomilla.serializers.fields.related import RelatedField
13
12
  from camomilla.serializers.utils import build_standard_model_serializer
@@ -169,12 +168,6 @@ class AbstractPageMixin(serializers.ModelSerializer):
169
168
  def get_breadcrumbs(self, instance: "AbstractPage"):
170
169
  return instance.breadcrumbs
171
170
 
172
- LANG_PERMALINK_FIELDS = [
173
- build_localized_fieldname("permalink", lang)
174
- for lang in AVAILABLE_LANGUAGES
175
- if settings.ENABLE_TRANSLATIONS
176
- ]
177
-
178
171
  @property
179
172
  def translation_fields(self):
180
173
  return super().translation_fields + ["permalink"]
@@ -186,15 +179,15 @@ class AbstractPageMixin(serializers.ModelSerializer):
186
179
  return super().get_default_field_names(*args)
187
180
  return (
188
181
  [f for f in super().get_default_field_names(*args) if f != "url_node"]
189
- + self.LANG_PERMALINK_FIELDS
182
+ + UrlNode.LANG_PERMALINK_FIELDS
190
183
  + ["permalink"]
191
184
  )
192
185
 
193
186
  def build_field(self, field_name, info, model_class, nested_depth):
194
- if field_name in self.LANG_PERMALINK_FIELDS + ["permalink"]:
187
+ if field_name in UrlNode.LANG_PERMALINK_FIELDS + ["permalink"]:
195
188
  return serializers.CharField, {
196
- "source": "url_node.%s" % field_name,
197
- "read_only": True,
189
+ "required": False,
190
+ "allow_blank": True,
198
191
  }
199
192
  return super().build_field(field_name, info, model_class, nested_depth)
200
193
 
@@ -8,7 +8,7 @@ from camomilla.utils.translation import get_nofallbacks, set_nofallbacks
8
8
 
9
9
 
10
10
  class UniquePermalinkValidator:
11
- message = _("This slug generates a non-unique permalink.")
11
+ message = _("There is an other page with same permalink.")
12
12
 
13
13
  requires_context = True
14
14
 
@@ -29,13 +29,17 @@ class UniquePermalinkValidator:
29
29
  instance, parent_page_field, None
30
30
  )
31
31
  for language in activate_languages():
32
- f_name = build_localized_fieldname("slug", language)
33
- slug = value.get(f_name, instance and get_nofallbacks(instance, "slug"))
32
+ autopermalink_f = build_localized_fieldname("autopermalink", language)
33
+ f_name = build_localized_fieldname("permalink", language)
34
+ permalink = value.get(f_name, instance and get_nofallbacks(instance, "permalink"))
35
+ permalink = UrlNode.sanitize_permalink(permalink)
36
+ autopermalink = value.get(autopermalink_f, instance and get_nofallbacks(instance, "autopermalink"))
37
+ if autopermalink:
38
+ continue
34
39
  fake_instance = serializer.Meta.model()
35
- set_nofallbacks(fake_instance, "slug", slug)
40
+ set_nofallbacks(fake_instance, "permalink", permalink)
36
41
  if parent_page:
37
42
  set_nofallbacks(fake_instance, parent_page_field, parent_page)
38
- permalink = fake_instance.generate_permalink(safe=False)
39
43
  qs = UrlNode.objects.exclude(**exclude_kwargs)
40
44
  if qs.filter(permalink=permalink).exists():
41
45
  errors[f_name] = self.message
@@ -79,10 +79,6 @@ TEMPLATE_CONTEXT_FILES = pointed_getter(
79
79
  django_settings, "CAMOMILLA.RENDER.TEMPLATE_CONTEXT_FILES", []
80
80
  )
81
81
 
82
- STRUCTURED_FIELD_CACHE_ENABLED = pointed_getter(
83
- django_settings, "CAMOMILLA.STRUCTURED_FIELD.CACHE_ENABLED", True
84
- )
85
-
86
82
  DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
87
83
 
88
84
  # camomilla settings example
@@ -0,0 +1 @@
1
+ __version__ = "6.0.0-beta.15"
@@ -26,7 +26,7 @@ class UserProfileAdmin(admin.ModelAdmin):
26
26
  class ArticleAdminForm(forms.ModelForm):
27
27
  class Meta:
28
28
  model = Article
29
- exclude = ("slug",)
29
+ fields = "__all__"
30
30
  widgets = {"content": CKEditorUploadingWidget}
31
31
 
32
32
 
@@ -16,7 +16,7 @@ class SeoMixinTranslationOptions(TranslationOptions):
16
16
 
17
17
 
18
18
  class AbstractPageTranslationOptions(SeoMixinTranslationOptions):
19
- fields = ("breadcrumbs_title", "slug", "status", "indexable", "template_data")
19
+ fields = ("breadcrumbs_title", "autopermalink", "status", "indexable", "template_data")
20
20
 
21
21
 
22
22
  @register(Article)
@@ -0,0 +1,37 @@
1
+ from typing import Sequence
2
+ from .getters import pointed_getter
3
+
4
+
5
+ def set_key(data, key, val):
6
+ if isinstance(data, Sequence):
7
+ key = int(key)
8
+ if key < len(data):
9
+ data[key] = val
10
+ else:
11
+ data.append(val)
12
+ return data
13
+ elif isinstance(data, dict):
14
+ data[key] = val
15
+ else:
16
+ setattr(data, key, val)
17
+ return data
18
+
19
+
20
+ def get_key(data, key, default):
21
+ if isinstance(data, Sequence):
22
+ try:
23
+ return data[int(key)]
24
+ except IndexError:
25
+ return default
26
+ return pointed_getter(data, key, default)
27
+
28
+
29
+ def pointed_setter(data, path, value):
30
+ path = path.split(".")
31
+ key = path.pop(0)
32
+ if not len(path):
33
+ return set_key(data, key, value)
34
+ default = [] if path[0].isdigit() else {}
35
+ return set_key(
36
+ data, key, pointed_setter(get_key(data, key, default), ".".join(path), value)
37
+ )
@@ -80,5 +80,4 @@ class MenuViewSet(BaseModelViewset):
80
80
  def search_urlnode(self, request, *args, **kwargs):
81
81
  url_node = request.GET.get("q", "")
82
82
  qs = UrlNode.objects.filter(permalink__icontains=url_node).order_by("permalink")
83
- print(get_language())
84
83
  return Response(BasicUrlNodeSerializer(qs, many=True).data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-camomilla-cms
3
- Version: 6.0.0b14
3
+ Version: 6.0.0b15
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
@@ -20,10 +20,9 @@ Requires-Dist: djangorestframework<4.0.0,>=3.10.0
20
20
  Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
21
21
  Requires-Dist: Pillow<10.0.0,>=6.2.0
22
22
  Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
23
+ Requires-Dist: django-structured-json-field==0.2.0
23
24
  Requires-Dist: python-magic<0.5,>=0.4
24
- Requires-Dist: django-jsonform~=2.19.0
25
25
  Requires-Dist: Django>=3.2
26
- Requires-Dist: pydantic~=2.2.1
27
26
 
28
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)
29
28
 
@@ -56,11 +56,6 @@ camomilla/serializers/mixins/__init__.py
56
56
  camomilla/storages/__init__.py
57
57
  camomilla/storages/optimize.py
58
58
  camomilla/storages/overwrite.py
59
- camomilla/structured/__init__.py
60
- camomilla/structured/cache.py
61
- camomilla/structured/fields.py
62
- camomilla/structured/models.py
63
- camomilla/structured/utils.py
64
59
  camomilla/templates/admin/camomilla/page/change_form.html
65
60
  camomilla/templates/defaults/base.html
66
61
  camomilla/templates/defaults/articles/default.html
@@ -86,6 +81,7 @@ camomilla/utils/__init__.py
86
81
  camomilla/utils/getters.py
87
82
  camomilla/utils/normalization.py
88
83
  camomilla/utils/seo.py
84
+ camomilla/utils/setters.py
89
85
  camomilla/utils/templates.py
90
86
  camomilla/utils/translation.py
91
87
  camomilla/views/__init__.py
@@ -4,7 +4,6 @@ djangorestframework<4.0.0,>=3.10.0
4
4
  django-admin-interface<1.0.0,>=0.26.0
5
5
  Pillow<10.0.0,>=6.2.0
6
6
  django-ckeditor<7.0.0,>=5.7.1
7
+ django-structured-json-field==0.2.0
7
8
  python-magic<0.5,>=0.4
8
- django-jsonform~=2.19.0
9
9
  Django>=3.2
10
- pydantic~=2.2.1
@@ -18,10 +18,9 @@ dependencies = [
18
18
  "django-admin-interface>=0.26.0,<1.0.0",
19
19
  "Pillow>=6.2.0,<10.0.0",
20
20
  "django-ckeditor>=5.7.1,<7.0.0",
21
+ "django-structured-json-field==0.2.0",
21
22
  "python-magic>=0.4,<0.5",
22
- "django-jsonform~=2.19.0",
23
23
  "Django>=3.2",
24
- "pydantic~=2.2.1",
25
24
  ]
26
25
  description = "Django powered cms"
27
26
  dynamic = ["version"]
@@ -1,5 +1,5 @@
1
1
  from setuptools import setup
2
2
 
3
- __version__ = "6.0.0-beta.14"
3
+ __version__ = "6.0.0-beta.15"
4
4
 
5
5
  setup(version=__version__)