django-camomilla-cms 6.0.0b2__py2.py3-none-any.whl → 6.0.0b4__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/apps.py +3 -0
  3. camomilla/contrib/modeltranslation/hvad_migration.py +1 -2
  4. camomilla/contrib/rest_framework/serializer.py +31 -1
  5. camomilla/dynamic_pages_urls.py +7 -1
  6. camomilla/fields/json.py +12 -9
  7. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  8. camomilla/model_api.py +6 -4
  9. camomilla/models/__init__.py +5 -5
  10. camomilla/models/article.py +0 -1
  11. camomilla/models/media.py +1 -2
  12. camomilla/models/menu.py +44 -42
  13. camomilla/models/mixins/__init__.py +0 -1
  14. camomilla/models/page.py +66 -32
  15. camomilla/openapi/schema.py +27 -0
  16. camomilla/parsers.py +0 -1
  17. camomilla/serializers/fields/json.py +7 -75
  18. camomilla/serializers/mixins/__init__.py +16 -2
  19. camomilla/serializers/page.py +47 -0
  20. camomilla/serializers/user.py +2 -3
  21. camomilla/serializers/utils.py +22 -17
  22. camomilla/settings.py +21 -1
  23. camomilla/storages/optimize.py +1 -1
  24. camomilla/structured/__init__.py +90 -75
  25. camomilla/structured/cache.py +193 -0
  26. camomilla/structured/fields.py +132 -275
  27. camomilla/structured/models.py +45 -138
  28. camomilla/structured/utils.py +114 -0
  29. camomilla/templatetags/camomilla_filters.py +0 -1
  30. camomilla/theme/__init__.py +1 -1
  31. camomilla/theme/admin.py +96 -0
  32. camomilla/theme/apps.py +12 -1
  33. camomilla/translation.py +4 -2
  34. camomilla/urls.py +13 -6
  35. camomilla/utils/__init__.py +1 -1
  36. camomilla/utils/getters.py +11 -1
  37. camomilla/utils/templates.py +2 -2
  38. camomilla/utils/translation.py +9 -6
  39. camomilla/views/__init__.py +1 -1
  40. camomilla/views/articles.py +0 -1
  41. camomilla/views/contents.py +0 -1
  42. camomilla/views/decorators.py +26 -0
  43. camomilla/views/medias.py +1 -2
  44. camomilla/views/menus.py +45 -1
  45. camomilla/views/pages.py +13 -1
  46. camomilla/views/tags.py +0 -1
  47. camomilla/views/users.py +0 -2
  48. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/METADATA +4 -3
  49. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/RECORD +53 -49
  50. tests/test_api.py +1 -0
  51. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/LICENSE +0 -0
  52. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/WHEEL +0 -0
  53. {django_camomilla_cms-6.0.0b2.dist-info → django_camomilla_cms-6.0.0b4.dist-info}/top_level.txt +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.2"
1
+ __version__ = "6.0.0-beta.4"
2
2
 
3
3
 
4
4
  def get_core_apps():
camomilla/apps.py CHANGED
@@ -3,6 +3,8 @@ from __future__ import unicode_literals
3
3
  from django.apps import AppConfig
4
4
  from django.conf import settings
5
5
 
6
+ from camomilla.context.autodiscover import autodiscover_context_files
7
+
6
8
 
7
9
  class CamomillaConfig(AppConfig):
8
10
  default_auto_field = "django.db.models.AutoField"
@@ -13,3 +15,4 @@ class CamomillaConfig(AppConfig):
13
15
  if "camomilla" not in migration_modules:
14
16
  migration_modules["camomilla"] = "camomilla_migrations"
15
17
  setattr(settings, "MIGRATION_MODULES", migration_modules)
18
+ autodiscover_context_files()
@@ -3,7 +3,6 @@ from django.db import migrations, connection
3
3
 
4
4
 
5
5
  class KeepTranslationsMixin:
6
-
7
6
  _saved_data_from_plain = {}
8
7
  language_codes = dict(getattr(settings, "LANGUAGES", {})).keys()
9
8
 
@@ -30,7 +29,7 @@ class KeepTranslationsMixin:
30
29
  for modelPath, fields in self.keep_translations.items():
31
30
  Model = apps.get_model(*modelPath.split("."))
32
31
  table = Model._meta.db_table + "_translation"
33
- if not "language_code" in fields:
32
+ if "language_code" not in fields:
34
33
  fields = ("language_code",) + fields
35
34
  with connection.cursor() as cursor:
36
35
  cursor.execute("SELECT master_id FROM {0};".format(table))
@@ -52,7 +52,7 @@ class TranslationsMixin(serializers.ModelSerializer):
52
52
  @property
53
53
  def _writable_fields(self):
54
54
  for field in super()._writable_fields:
55
- if not field.field_name in self.translation_fields:
55
+ if field.field_name not in self.translation_fields:
56
56
  yield field
57
57
 
58
58
  def to_internal_value(self, data):
@@ -77,3 +77,33 @@ class TranslationsMixin(serializers.ModelSerializer):
77
77
  @property
78
78
  def is_translatable(self):
79
79
  return is_translatable(pointed_getter(self, "Meta.model"))
80
+
81
+
82
+ class RemoveTranslationsMixin(serializers.ModelSerializer):
83
+ @cached_property
84
+ def translation_fields(self):
85
+ try:
86
+ return translator.get_options_for_model(self.Meta.model).get_field_names()
87
+ except NotRegistered:
88
+ return []
89
+
90
+ def get_default_field_names(self, declared_fields, model_info):
91
+ request = self.context.get("request", False)
92
+ included_translations = request and request.GET.get(
93
+ "included_translations", False
94
+ )
95
+ if included_translations == "all":
96
+ return super().get_default_field_names(declared_fields, model_info)
97
+ elif included_translations is not False:
98
+ included_translations = included_translations.split(",")
99
+ else:
100
+ included_translations = []
101
+
102
+ field_names = super().get_default_field_names(declared_fields, model_info)
103
+ for lang in mt_settings.AVAILABLE_LANGUAGES:
104
+ if lang not in included_translations:
105
+ for field in self.translation_fields:
106
+ localized_fieldname = build_localized_fieldname(field, lang)
107
+ if localized_fieldname in field_names:
108
+ field_names.remove(localized_fieldname)
109
+ return field_names
@@ -1,13 +1,19 @@
1
1
  from django.shortcuts import render
2
2
  from django.urls import path
3
3
 
4
+ from camomilla import settings
5
+
4
6
  from .models import Page
5
7
 
6
8
 
7
9
  def fetch(request, *args, **kwargs):
8
10
  preview = request.user.is_staff and request.GET.get("preview", False)
9
11
  if "permalink" in kwargs:
10
- page = Page.get_or_404(request, bypass_public_check=preview, bypass_type_check=True)
12
+ page = Page.get_or_404(
13
+ request, bypass_public_check=preview, bypass_type_check=True
14
+ )
15
+ elif settings.AUTO_CREATE_HOMEPAGE is False:
16
+ page, _ = Page.get_or_404(permalink="/", bypass_type_check=True)
11
17
  else:
12
18
  page, _ = Page.get_or_create_homepage()
13
19
  return render(request, page.get_template_path(request), page.get_context(request))
camomilla/fields/json.py CHANGED
@@ -12,15 +12,7 @@ from django.contrib.postgres.fields import ArrayField as DjangoArrayField
12
12
  from django.db import models
13
13
 
14
14
 
15
- class JSONField(DjangoJSONField):
16
- pass
17
-
18
-
19
- class ArrayField(DjangoArrayField):
20
- pass
21
-
22
-
23
- if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
15
+ if "sqlite" in settings.DATABASES["default"]["ENGINE"]: # noqa: C901
24
16
 
25
17
  class JSONField(models.Field):
26
18
  def db_type(self, connection):
@@ -64,3 +56,14 @@ if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
64
56
  }
65
57
  )
66
58
  return name, path, args, kwargs
59
+
60
+ else:
61
+
62
+ class JSONField(DjangoJSONField):
63
+ pass
64
+
65
+ class ArrayField(DjangoArrayField):
66
+ pass
67
+
68
+
69
+ __all__ = [JSONField, ArrayField]
@@ -3,7 +3,6 @@ from camomilla.models import Media
3
3
 
4
4
 
5
5
  class Command(BaseCommand):
6
-
7
6
  help = "Regenerates all the thumbnail"
8
7
 
9
8
  def handle(self, *args, **options):
camomilla/model_api.py CHANGED
@@ -13,7 +13,7 @@ def register(
13
13
  base_viewset=BaseModelViewset,
14
14
  serializer_meta={},
15
15
  viewset_attrs={},
16
- filters = None
16
+ filters=None,
17
17
  ):
18
18
  """
19
19
  Register a model to the API.
@@ -30,8 +30,8 @@ def register(
30
30
  "model": model,
31
31
  "fields": "__all__",
32
32
  }
33
- if 'exclude' in serializer_meta:
34
- base_meta.pop('fields')
33
+ if "exclude" in serializer_meta:
34
+ base_meta.pop("fields")
35
35
  serializer = type(
36
36
  f"{model.__name__}Serializer",
37
37
  (base_serializer,),
@@ -51,7 +51,9 @@ def register(
51
51
  f"{model.__name__}ViewSet",
52
52
  (base_viewset,),
53
53
  {
54
- "get_queryset": lambda self: model.objects.all() if filters is None else model.objects.filter(**filters),
54
+ "get_queryset": lambda self: model.objects.all()
55
+ if filters is None
56
+ else model.objects.filter(**filters),
55
57
  "serializer_class": serializer,
56
58
  **viewset_attrs,
57
59
  },
@@ -1,5 +1,5 @@
1
- from .article import * # NOQA
2
- from .content import * # NOQA
3
- from .media import * # NOQA
4
- from .page import * # NOQA
5
- from .menu import * # NOQA
1
+ from .article import * # NOQA
2
+ from .content import * # NOQA
3
+ from .media import * # NOQA
4
+ from .page import * # NOQA
5
+ from .menu import * # NOQA
@@ -1,6 +1,5 @@
1
1
  from django.conf import settings as dj_settings
2
2
  from django.db import models
3
- from django.utils.translation import gettext_lazy as _
4
3
 
5
4
  from camomilla.models.page import AbstractPage
6
5
  from camomilla import settings
camomilla/models/media.py CHANGED
@@ -5,7 +5,6 @@ from io import BytesIO
5
5
  import magic
6
6
  from django.core.exceptions import ValidationError
7
7
  from django.core.files.base import ContentFile
8
- from django.core.files.storage import default_storage as storage
9
8
  from django.db import models
10
9
  from django.db.models.fields.related import ForeignObjectRel
11
10
  from django.db.models.signals import post_save, pre_delete
@@ -181,7 +180,7 @@ class Media(models.Model):
181
180
  def _get_file_size(self):
182
181
  try:
183
182
  return self.file.storage.size(self.file.name)
184
- except:
183
+ except Exception:
185
184
  return 0
186
185
 
187
186
  def __str__(self):
camomilla/models/menu.py CHANGED
@@ -1,68 +1,67 @@
1
+ from enum import Enum
2
+ from uuid import uuid4
1
3
  from django.contrib.contenttypes.models import ContentType
2
4
  from django.db import models
3
5
  from django.utils.translation import gettext_lazy as _
4
6
  from django.template.loader import render_to_string
5
7
  from django.template import RequestContext
6
8
  from django.utils.safestring import mark_safe
9
+ from pydantic import (
10
+ Field,
11
+ SerializationInfo,
12
+ computed_field,
13
+ model_serializer,
14
+ )
7
15
  from camomilla import structured
8
16
  from camomilla.models.page import UrlNode
9
- from typing import Union
17
+ from typing import Optional, Union, Callable
10
18
 
11
19
 
12
- class MenuNodeLink(structured.Model):
13
- link_type = structured.CharField()
14
- static = structured.CharField()
15
- url_node = structured.ForeignKey(UrlNode)
16
- content_type = structured.IntegerField()
17
- page_id = structured.IntegerField()
20
+ class LinkTypes(str, Enum):
21
+ relational = "RE"
22
+ static = "ST"
18
23
 
19
- @classmethod
20
- def to_db_transform(cls, data):
21
- if data.get("link_type", None) == "RE":
22
- ct_id = data.get("content_type", None)
23
- p_id = data.get("page_id", None)
24
- if ct_id and p_id:
25
- c_type = ContentType.objects.filter(pk=ct_id).first()
24
+
25
+ class MenuNodeLink(structured.BaseModel):
26
+ link_type: LinkTypes = LinkTypes.static
27
+ static: str = None
28
+ content_type: int = None
29
+ page_id: int = None
30
+ url_node: UrlNode = None
31
+
32
+ @model_serializer(mode="wrap", when_used="json")
33
+ def update_relational(self, handler: Callable, info: SerializationInfo):
34
+ 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()
26
37
  model = c_type and c_type.model_class()
27
- page = model and model.objects.filter(pk=p_id).first()
28
- if page:
29
- data["url_node"] = page.url_node.pk
30
- else:
31
- data["url_node"] = None
32
- return data
38
+ page = model and model.objects.filter(pk=self.page_id).first()
39
+ self.url_node = page and page.url_node
40
+ return handler(self)
33
41
 
34
42
  def get_url(self, request=None):
35
- if self.link_type == "RE":
36
- return self.url_node and self.url_node.routerlink
37
- elif self.link_type == "ST":
43
+ if self.link_type == LinkTypes.relational:
44
+ return isinstance(self.url_node, UrlNode) and self.url_node.routerlink
45
+ elif self.link_type == LinkTypes.static:
38
46
  return self.static
39
47
 
48
+ @computed_field
40
49
  @property
41
- def url(self):
50
+ def url(self) -> Optional[str]:
42
51
  return self.get_url()
43
52
 
44
53
 
45
- class MenuNode(structured.Model):
46
- id = structured.CharField()
47
- meta = structured.DictField()
48
- nodes = structured.ListField(items_types=("MenuNode",))
49
- title = structured.CharField()
50
- link = structured.EmbeddedField("MenuNodeLink")
51
-
52
- @classmethod
53
- def to_db_transform(cls, data):
54
- link = data.pop("link", {})
55
- nodes = data.pop("nodes", {})
56
- return {
57
- "link": MenuNodeLink.to_db_transform(link),
58
- "nodes": [cls.to_db_transform(n) for n in nodes],
59
- **data,
60
- }
54
+ class MenuNode(structured.BaseModel):
55
+ id: str = Field(default_factory=uuid4)
56
+ meta: dict = {}
57
+ nodes: list["MenuNode"] = []
58
+ title: str = ""
59
+ link: MenuNodeLink
61
60
 
62
61
 
63
62
  class Menu(models.Model):
64
- key = models.CharField(max_length=200, unique=True)
65
- available_classes = models.JSONField(default=dict)
63
+ key = models.CharField(max_length=200, unique=True, editable=False)
64
+ available_classes = models.JSONField(default=dict, editable=False)
66
65
  enabled = models.BooleanField(default=True)
67
66
  nodes = structured.StructuredJSONField(default=list, schema=MenuNode)
68
67
 
@@ -85,3 +84,6 @@ class Menu(models.Model):
85
84
  def __missing__(self, key):
86
85
  dict.__setitem__(self, key, Menu.objects.get_or_create(key=key)[0])
87
86
  return self[key]
87
+
88
+ def __str__(self) -> str:
89
+ return self.key
@@ -2,7 +2,6 @@ from django.db import models
2
2
 
3
3
 
4
4
  class SeoMixin(models.Model):
5
-
6
5
  title = models.CharField(max_length=200, null=True, blank=True)
7
6
  description = models.TextField(null=True, blank=True)
8
7
  og_description = models.TextField(blank=True, null=True)
camomilla/models/page.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Iterable, Tuple
1
+ from typing import Sequence, Tuple
2
2
  from uuid import uuid4
3
3
 
4
4
  from django.core.exceptions import ObjectDoesNotExist
@@ -20,13 +20,15 @@ from camomilla.utils import (
20
20
  lang_fallback_query,
21
21
  set_nofallbacks,
22
22
  url_lang_decompose,
23
- get_all_templates_files
23
+ get_all_templates_files,
24
24
  )
25
25
  from camomilla.utils.getters import pointed_getter
26
26
  from camomilla import settings
27
+ from camomilla.context.rendering import ctx_registry
28
+
27
29
 
28
30
  def GET_TEMPLATE_CHOICES():
29
- return [(t,t)for t in get_all_templates_files()]
31
+ return [(t, t) for t in get_all_templates_files()]
30
32
 
31
33
 
32
34
  class UrlNodeManager(models.Manager):
@@ -42,7 +44,7 @@ class UrlNodeManager(models.Manager):
42
44
  def _annotate_fields(
43
45
  self,
44
46
  qs: models.QuerySet,
45
- field_names: Iterable[Tuple[str, models.Field, models.Value]],
47
+ field_names: Sequence[Tuple[str, models.Field, models.Value]],
46
48
  ):
47
49
  for field_name, output_field, default in field_names:
48
50
  whens = [
@@ -66,9 +68,21 @@ class UrlNodeManager(models.Manager):
66
68
  return self._annotate_fields(
67
69
  super().get_queryset(),
68
70
  [
69
- ("indexable", models.BooleanField(), models.Value(None, models.BooleanField())),
70
- ("status", models.BooleanField(), models.Value(None, models.BooleanField())),
71
- ("pubblication_date", models.DateTimeField(), models.Value(timezone.now(), models.DateTimeField())),
71
+ (
72
+ "indexable",
73
+ models.BooleanField(),
74
+ models.Value(None, models.BooleanField()),
75
+ ),
76
+ (
77
+ "status",
78
+ models.BooleanField(),
79
+ models.Value(None, models.BooleanField()),
80
+ ),
81
+ (
82
+ "pubblication_date",
83
+ models.DateTimeField(),
84
+ models.Value(timezone.now(), models.DateTimeField()),
85
+ ),
72
86
  ],
73
87
  )
74
88
  except (ProgrammingError, OperationalError):
@@ -83,15 +97,19 @@ class UrlNode(models.Model):
83
97
  @property
84
98
  def page(self) -> "AbstractPage":
85
99
  return getattr(self, self.related_name)
86
-
87
- @property
88
- def routerlink(self) -> str:
100
+
101
+ @staticmethod
102
+ def reverse_url(permalink: str) -> str:
89
103
  try:
90
- if self.permalink == '/':
91
- return reverse('camomilla-homepage')
92
- return reverse('camomilla-permalink', args=(self.permalink.lstrip("/"),))
104
+ if permalink == "/":
105
+ return reverse("camomilla-homepage")
106
+ return reverse("camomilla-permalink", args=(permalink.lstrip("/"),))
93
107
  except NoReverseMatch:
94
- return self.permalink
108
+ return None
109
+
110
+ @property
111
+ def routerlink(self) -> str:
112
+ return self.reverse_url(self.permalink) or self.permalink
95
113
 
96
114
 
97
115
  PAGE_CHILD_RELATED_NAME = "%(app_label)s_%(class)s_child_pages"
@@ -123,7 +141,11 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
123
141
  date_created = models.DateTimeField(auto_now_add=True)
124
142
  date_updated_at = models.DateTimeField(auto_now=True)
125
143
  url_node = models.OneToOneField(
126
- UrlNode, on_delete=models.CASCADE, related_name=URL_NODE_RELATED_NAME, null=True, editable=False
144
+ UrlNode,
145
+ on_delete=models.CASCADE,
146
+ related_name=URL_NODE_RELATED_NAME,
147
+ null=True,
148
+ editable=False,
127
149
  )
128
150
  breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
129
151
  slug = models.SlugField(max_length=150, allow_unicode=True, null=True, blank=True)
@@ -145,16 +167,16 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
145
167
  blank=True,
146
168
  on_delete=models.CASCADE,
147
169
  )
148
-
170
+
149
171
  def __init__(self, *args, **kwargs):
150
172
  super(AbstractPage, self).__init__(*args, **kwargs)
151
- self._meta.get_field('template').choices = lazy(GET_TEMPLATE_CHOICES,list)()
173
+ self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
152
174
 
153
175
  def __str__(self) -> str:
154
176
  return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
155
177
 
156
178
  def get_context(self, request=None):
157
- context={
179
+ context = {
158
180
  "page": self,
159
181
  "page_model": {"class": self.__class__.__name__, "module": self.__module__},
160
182
  "request": request,
@@ -164,7 +186,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
164
186
  new_ctx = inject_func(request=request, super_ctx=context)
165
187
  if isinstance(new_ctx, dict):
166
188
  context.update(new_ctx)
167
- return context
189
+ return ctx_registry.get_context_for_page(self, request, super_ctx=context)
168
190
 
169
191
  @property
170
192
  def model_name(self) -> str:
@@ -177,13 +199,13 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
177
199
  @property
178
200
  def permalink(self) -> str:
179
201
  return self.url_node and self.url_node.permalink
180
-
202
+
181
203
  @property
182
204
  def routerlink(self) -> str:
183
205
  return self.url_node and self.url_node.routerlink
184
-
206
+
185
207
  @property
186
- def breadcrumbs(self) -> Iterable[dict]:
208
+ def breadcrumbs(self) -> Sequence[dict]:
187
209
  breadcrumb = {
188
210
  "permalink": self.permalink,
189
211
  "title": self.breadcrumbs_title or self.title or self.slug,
@@ -197,7 +219,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
197
219
  if self.status == "PUB":
198
220
  return True
199
221
  if self.status == "PLA":
200
- return bool(self.pubblication_date) and timezone.now() > self.pubblication_date
222
+ return (
223
+ bool(self.pubblication_date) and timezone.now() > self.pubblication_date
224
+ )
201
225
  return False
202
226
 
203
227
  def get_template_path(self, request=None) -> str:
@@ -222,7 +246,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
222
246
 
223
247
  def _update_url_node(self, force: bool = False) -> UrlNode:
224
248
  self.url_node = self._get_or_create_url_node()
225
- for _ in activate_languages():
249
+ for __ in activate_languages():
226
250
  old_permalink = self.permalink
227
251
  new_permalink = self.generate_permalink()
228
252
  force = force or old_permalink != new_permalink
@@ -232,7 +256,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
232
256
  self.update_childs()
233
257
  return self.url_node
234
258
 
235
- def generate_permalink(self, safe:bool=True) -> str:
259
+ def generate_permalink(self, safe: bool = True) -> str:
236
260
  slug = get_nofallbacks(self, "slug")
237
261
  if slug is None and not self.permalink:
238
262
  translations = get_field_translations(self, "slug").values()
@@ -277,14 +301,20 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
277
301
  ).first()
278
302
  page = node and node.page
279
303
  type_error = not bypass_type_check and not isinstance(page, cls)
280
- public_error = not bypass_public_check and not getattr(page or object, "is_public", False)
304
+ public_error = not bypass_public_check and not getattr(
305
+ page or object, "is_public", False
306
+ )
281
307
  if not page or type_error or public_error:
282
308
  bases = (UrlNode.DoesNotExist,)
283
309
  if hasattr(cls, "DoesNotExist"):
284
310
  bases += (cls.DoesNotExist,)
285
- raise type("PageDoesNotExist", bases, {})(
286
- "%s matching query does not exist." % cls._meta.object_name
287
- )
311
+ message = "%s matching query does not exist." % cls._meta.object_name
312
+ if public_error:
313
+ message = (
314
+ "Match found: %s.\nThe page appears not to be public.\nUse ?preview=true in the url to see it."
315
+ % page
316
+ )
317
+ raise type("PageDoesNotExist", bases, {})(message)
288
318
  return page
289
319
 
290
320
  @classmethod
@@ -311,11 +341,15 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
311
341
  def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
312
342
  try:
313
343
  return cls.get(request, *args, **kwargs)
314
- except ObjectDoesNotExist:
315
- raise Http404("No %s matches the given query." % cls._meta.object_name)
344
+ except ObjectDoesNotExist as ex:
345
+ raise Http404(ex)
316
346
 
317
347
  def alternate_urls(self, *args, **kwargs) -> dict:
318
- return get_field_translations(self.url_node or object, "permalink", None)
348
+ permalinks = get_field_translations(self.url_node or object, "permalink", None)
349
+ for lang in activate_languages():
350
+ if lang in permalinks:
351
+ permalinks[lang] = UrlNode.reverse_url(permalinks[lang])
352
+ return permalinks
319
353
 
320
354
  class Meta:
321
355
  abstract = True
@@ -7,9 +7,13 @@ from camomilla.contrib.rest_framework.serializer import (
7
7
  plain_to_nest,
8
8
  TRANS_ACCESSOR,
9
9
  )
10
+ from camomilla.serializers.fields.json import StructuredJSONField
11
+ from camomilla.utils.getters import find_and_replace_dict, pointed_getter
10
12
 
11
13
 
12
14
  class AutoSchema(DRFAutoSchema):
15
+ extra_components = {}
16
+
13
17
  def map_serializer(self, serializer):
14
18
  schema = super(AutoSchema, self).map_serializer(serializer)
15
19
  if isinstance(serializer, TranslationsMixin) and serializer.is_translatable:
@@ -23,6 +27,29 @@ class AutoSchema(DRFAutoSchema):
23
27
  }
24
28
  return schema
25
29
 
30
+ def get_components(self, path, method):
31
+ components = super().get_components(path, method)
32
+ if len(self.extra_components.keys()) > 0:
33
+ components = {**(components or {}), **self.extra_components}
34
+ return components
35
+
36
+ def map_field(self, field):
37
+ if isinstance(field, StructuredJSONField):
38
+
39
+ def replace(key, value):
40
+ if isinstance(value, str) and value.startswith("#/definitions"):
41
+ return value.replace("#/definitions", "#/components/schemas")
42
+ return value
43
+
44
+ self.extra_components.update(
45
+ **find_and_replace_dict(
46
+ field.json_schema.pop("definitions", {}), replace
47
+ )
48
+ )
49
+
50
+ return find_and_replace_dict(field.json_schema, replace)
51
+ return super().map_field(field)
52
+
26
53
 
27
54
  class SchemaGenerator(DRFSchemaGenerator):
28
55
  def create_view(self, callback, method, request=None):
camomilla/parsers.py CHANGED
@@ -38,7 +38,6 @@ def compile_payload(data, path, value):
38
38
 
39
39
 
40
40
  class MultipartJsonParser(parsers.BaseParser):
41
-
42
41
  media_type = "multipart/form-data"
43
42
 
44
43
  def parse(self, stream, media_type=None, parser_context=None):