django-camomilla-cms 6.0.0b14__py2.py3-none-any.whl → 6.0.0b16__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 (51) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/dynamic_pages_urls.py +2 -1
  3. camomilla/fields/__init__.py +1 -3
  4. camomilla/managers/pages.py +22 -33
  5. camomilla/model_api.py +9 -4
  6. camomilla/models/media.py +1 -1
  7. camomilla/models/menu.py +23 -11
  8. camomilla/models/page.py +76 -38
  9. camomilla/openapi/schema.py +4 -0
  10. camomilla/serializers/base/__init__.py +3 -1
  11. camomilla/serializers/fields/__init__.py +2 -2
  12. camomilla/serializers/fields/json.py +2 -2
  13. camomilla/serializers/fields/related.py +5 -1
  14. camomilla/serializers/mixins/__init__.py +56 -18
  15. camomilla/serializers/mixins/filter_fields.py +56 -0
  16. camomilla/serializers/utils.py +3 -1
  17. camomilla/serializers/validators.py +9 -5
  18. camomilla/settings.py +0 -4
  19. camomilla/storages/default.py +6 -0
  20. camomilla/storages/optimize.py +2 -2
  21. camomilla/storages/overwrite.py +2 -2
  22. camomilla/templates/defaults/parts/menu.html +1 -1
  23. camomilla/theme/__init__.py +1 -1
  24. camomilla/theme/admin.py +1 -1
  25. camomilla/translation.py +1 -1
  26. camomilla/utils/query_parser.py +148 -0
  27. camomilla/utils/setters.py +37 -0
  28. camomilla/views/base/__init__.py +2 -2
  29. camomilla/views/menus.py +0 -3
  30. camomilla/views/mixins/__init__.py +9 -2
  31. camomilla/views/mixins/pagination.py +4 -13
  32. {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/METADATA +2 -3
  33. {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/RECORD +46 -40
  34. {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/WHEEL +1 -1
  35. tests/fixtures/__init__.py +17 -0
  36. tests/test_api.py +2 -11
  37. tests/test_camomilla_filters.py +7 -13
  38. tests/test_media.py +80 -0
  39. tests/test_model_api.py +68 -0
  40. tests/test_model_api_permissions.py +39 -0
  41. tests/test_query_parser.py +59 -0
  42. tests/test_utils.py +64 -64
  43. tests/utils/__init__.py +0 -0
  44. tests/utils/api.py +29 -0
  45. camomilla/structured/__init__.py +0 -125
  46. camomilla/structured/cache.py +0 -202
  47. camomilla/structured/fields.py +0 -150
  48. camomilla/structured/models.py +0 -47
  49. camomilla/structured/utils.py +0 -114
  50. {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/LICENSE +0 -0
  51. {django_camomilla_cms-6.0.0b14.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/top_level.txt +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.14"
1
+ __version__ = "6.0.0-beta.16"
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"]
@@ -1,42 +1,31 @@
1
- from typing import Any, Tuple
2
1
  from django.db.models.query import QuerySet
3
- from django.db import transaction
2
+ from django.core.exceptions import ObjectDoesNotExist
4
3
  from django.apps import apps
5
4
 
6
5
  URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
7
6
 
8
7
 
9
8
  class PageQuerySet(QuerySet):
10
- def get_or_create(self, defaults=None, **kwargs) -> Tuple[Any, bool]:
11
- if "permalink" in kwargs and isinstance(kwargs["permalink"], str):
12
- if kwargs["permalink"].startswith("/"):
13
- kwargs["permalink"] = kwargs["permalink"][1:]
14
- with transaction.atomic():
15
- UrlNode = apps.get_model("camomilla", "UrlNode")
16
- url_node, created = UrlNode.objects.get_or_create(
17
- defaults={
18
- "related_name": URL_NODE_RELATED_NAME
19
- % {
20
- "app_label": self.model._meta.app_label,
21
- "class": self.model._meta.model_name,
22
- }
23
- },
24
- permalink=kwargs["permalink"],
25
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
26
30
  )
27
- if created is False and url_node.page is not None:
28
- page = self.get(**kwargs)
29
- if page.pk != url_node.page.pk:
30
- raise self.model.MultipleObjectsReturned(
31
- "got more than one %s object for the same permalink: %s"
32
- % (
33
- self.model._meta.object_name,
34
- kwargs["permalink"],
35
- )
36
- )
37
- return url_node.page, False
38
- kwargs["url_node"] = url_node
39
- kwargs["slug"] = kwargs["permalink"]
40
- kwargs.pop("permalink")
41
- return super().get_or_create(defaults, **kwargs)
42
- return super().get_or_create(defaults, **kwargs)
31
+ return super(PageQuerySet, self).get(*args, **kwargs)
camomilla/model_api.py CHANGED
@@ -26,6 +26,7 @@ def register(
26
26
  """
27
27
 
28
28
  def inner(model):
29
+ global urlpatterns
29
30
  base_meta = {
30
31
  "model": model,
31
32
  "fields": "__all__",
@@ -46,14 +47,18 @@ def register(
46
47
  )
47
48
  },
48
49
  )
50
+
51
+ def get_queryset(self, *args, **kwargs):
52
+ qs = super(base_viewset, self).get_queryset(*args, **kwargs)
53
+ return qs if filters is None else qs.filter(**filters)
49
54
 
50
55
  viewset = type(
51
56
  f"{model.__name__}ViewSet",
52
57
  (base_viewset,),
53
58
  {
54
- "get_queryset": lambda self: model.objects.all()
55
- if filters is None
56
- else model.objects.filter(**filters),
59
+ "queryset": model.objects.all(),
60
+ "model": model,
61
+ "get_queryset": get_queryset,
57
62
  "serializer_class": serializer,
58
63
  **viewset_attrs,
59
64
  },
@@ -73,7 +78,7 @@ def register(
73
78
  viewset,
74
79
  f"{model.__name__.lower()}_api",
75
80
  )
76
- urlpatterns.append(path("", include(router.urls)))
81
+ urlpatterns = [path("", include(router.urls))]
77
82
  return model
78
83
 
79
84
  return inner
camomilla/models/media.py CHANGED
@@ -150,7 +150,7 @@ class Media(models.Model):
150
150
  img_bytes = self.file.storage.open(self.file.name, "rb")
151
151
  with Image.open(img_bytes) as orig_image:
152
152
  image = orig_image.copy()
153
- image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.ANTIALIAS)
153
+ image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.LANCZOS)
154
154
 
155
155
  # Path to save to, name, and extension
156
156
  thumb_name, thumb_extension = os.path.splitext(self.file.name)
camomilla/models/menu.py CHANGED
@@ -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 camomilla.models.page import UrlNode, AbstractPage
17
18
  from typing import Optional, Union, Callable, List
19
+ from django.db.models.base import Model as DjangoModel
20
+ from django.conf import settings
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")
@@ -77,7 +88,8 @@ class Menu(models.Model):
77
88
  ):
78
89
  if isinstance(context, RequestContext):
79
90
  context = context.flatten()
80
- context.update({"menu": self})
91
+ is_preview = bool(request.GET.get("preview", False))
92
+ context.update({"menu": self, "is_preview": is_preview})
81
93
  return mark_safe(render_to_string(template_path, context, request))
82
94
 
83
95
  class defaultdict(dict):
camomilla/models/page.py CHANGED
@@ -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,6 +29,12 @@ 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
34
+
35
+
36
+ class UrlPathValidator():
37
+ pass
32
38
 
33
39
 
34
40
  def GET_TEMPLATE_CHOICES():
@@ -111,6 +117,13 @@ class UrlNodeManager(models.Manager):
111
117
 
112
118
 
113
119
  class UrlNode(models.Model):
120
+
121
+ LANG_PERMALINK_FIELDS = [
122
+ build_localized_fieldname("permalink", lang)
123
+ for lang in AVAILABLE_LANGUAGES
124
+ if settings.ENABLE_TRANSLATIONS
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()
@@ -141,6 +154,23 @@ class UrlNode(models.Model):
141
154
  return ""
142
155
  return self.routerlink
143
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
173
+
144
174
 
145
175
  PAGE_CHILD_RELATED_NAME = "%(app_label)s_%(class)s_child_pages"
146
176
  URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
@@ -154,11 +184,29 @@ 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
+
194
+ def setter(_self, value: str):
195
+ setattr(_self, f"__{permalink_field}", value)
196
+ return getter, setter
197
+
157
198
  def __new__(cls, name, bases, attrs, **kwargs):
158
199
  attr_meta = attrs.pop("PageMeta", None)
159
200
  new_class = super().__new__(cls, name, bases, attrs, **kwargs)
160
201
  page_meta = attr_meta or getattr(new_class, "PageMeta", None)
161
202
  base_page_meta = getattr(new_class, "_page_meta", None)
203
+ for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
204
+ computed_prop = property(*cls.perm_prop_factory(lang_p_field))
205
+ setattr(new_class, lang_p_field, computed_prop)
206
+ setattr(new_class, "permalink", property(
207
+ lambda _self: getattr(_self, build_localized_fieldname("permalink", get_language()), None),
208
+ lambda _self, value: setattr(_self, f"__{build_localized_fieldname('permalink', get_language())}", value)
209
+ ))
162
210
  if page_meta:
163
211
  for name, value in getattr(base_page_meta, "__dict__", {}).items():
164
212
  if name not in page_meta.__dict__:
@@ -167,16 +215,6 @@ class PageBase(models.base.ModelBase):
167
215
  return new_class
168
216
 
169
217
 
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
218
  class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
181
219
  date_created = models.DateTimeField(auto_now_add=True)
182
220
  date_updated_at = models.DateTimeField(auto_now=True)
@@ -188,9 +226,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
188
226
  editable=False,
189
227
  )
190
228
  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
- )
229
+ autopermalink = models.BooleanField(default=True)
194
230
  status = models.CharField(
195
231
  max_length=3,
196
232
  choices=PAGE_STATUS,
@@ -212,6 +248,19 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
212
248
 
213
249
  objects = PageQuerySet.as_manager()
214
250
 
251
+ __cached_db_instance: "AbstractPage" = None
252
+
253
+ @property
254
+ def db_instance(self):
255
+ if self.__cached_db_instance is None:
256
+ self.__cached_db_instance = self.get_db_instance()
257
+ return self.__cached_db_instance
258
+
259
+ def get_db_instance(self):
260
+ if self.pk:
261
+ return self.__class__.objects.get(pk=self.pk)
262
+ return None
263
+
215
264
  def __init__(self, *args, **kwargs):
216
265
  super(AbstractPage, self).__init__(*args, **kwargs)
217
266
  self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
@@ -240,10 +289,6 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
240
289
  def model_info(self) -> dict:
241
290
  return {"app_label": self._meta.app_label, "class": self._meta.model_name}
242
291
 
243
- @property
244
- def permalink(self) -> str:
245
- return self.url_node and self.url_node.permalink
246
-
247
292
  @property
248
293
  def routerlink(self) -> str:
249
294
  return self.url_node and self.url_node.routerlink
@@ -252,7 +297,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
252
297
  def breadcrumbs(self) -> Sequence[dict]:
253
298
  breadcrumb = {
254
299
  "permalink": self.permalink,
255
- "title": self.breadcrumbs_title or self.title or self.slug,
300
+ "title": self.breadcrumbs_title or self.title or "",
256
301
  }
257
302
  if self.parent:
258
303
  return self.parent.breadcrumbs + [breadcrumb]
@@ -291,8 +336,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
291
336
  def _update_url_node(self, force: bool = False) -> UrlNode:
292
337
  self.url_node = self._get_or_create_url_node()
293
338
  for __ in activate_languages():
294
- old_permalink = self.permalink
295
- new_permalink = self.generate_permalink()
339
+ old_permalink = self.db_instance and self.db_instance.permalink
340
+ new_permalink = self.permalink
341
+ if self.autopermalink:
342
+ new_permalink = self.generate_permalink()
296
343
  force = force or old_permalink != new_permalink
297
344
  set_nofallbacks(self.url_node, "permalink", new_permalink)
298
345
  if force:
@@ -301,22 +348,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
301
348
  return self.url_node
302
349
 
303
350
  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)
351
+ permalink = f"/{slugify(self.title or '', allow_unicode=True)}"
318
352
  if self.parent:
319
- permalink = f"{self.parent.permalink}{permalink}"
353
+ permalink = f"/{self.parent.permalink}{permalink}"
354
+ set_nofallbacks(self, "permalink", permalink)
320
355
  qs = UrlNode.objects.exclude(pk=getattr(self.url_node or object, "pk", None))
321
356
  if safe and qs.filter(permalink=permalink).exists():
322
357
  permalink = "/".join(
@@ -333,7 +368,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
333
368
  def save(self, *args, **kwargs) -> None:
334
369
  with transaction.atomic():
335
370
  self._update_url_node()
336
- return super().save(*args, **kwargs)
371
+ super().save(*args, **kwargs)
372
+ self.__cached_db_instance = None
373
+ for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
374
+ hasattr(self, f"__{lang_p_field}") and delattr(self, f"__{lang_p_field}")
337
375
 
338
376
  @classmethod
339
377
  def get(cls, request, *args, **kwargs) -> "AbstractPage":
@@ -386,7 +424,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
386
424
  node = UrlNode.objects.get(permalink="/")
387
425
  return node.page, False
388
426
  except UrlNode.DoesNotExist:
389
- return cls.get_or_create(None, slug="")
427
+ return cls.get_or_create(None, permalink="/")
390
428
 
391
429
  @classmethod
392
430
  def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
@@ -55,4 +55,8 @@ class SchemaGenerator(DRFSchemaGenerator):
55
55
  def create_view(self, callback, method, request=None):
56
56
  view = super(SchemaGenerator, self).create_view(callback, method, request)
57
57
  view.schema = AutoSchema()
58
+ if not hasattr(view, 'get_queryset') and getattr(view, 'queryset', None) is None:
59
+ attname = "permission_classes"
60
+ cname = "DjangoModelPermissions"
61
+ setattr(view, attname, [p for p in getattr(view, attname, []) if cname not in p.__name__])
58
62
  return view
@@ -8,14 +8,16 @@ from ..mixins import (
8
8
  OrderingMixin,
9
9
  SetupEagerLoadingMixin,
10
10
  )
11
+ from ..mixins.filter_fields import FilterFieldsMixin
11
12
 
12
13
 
13
14
  class BaseModelSerializer(
15
+ SetupEagerLoadingMixin,
14
16
  NestMixin,
17
+ FilterFieldsMixin,
15
18
  FieldsOverrideMixin,
16
19
  JSONFieldPatchMixin,
17
20
  OrderingMixin,
18
- SetupEagerLoadingMixin,
19
21
  TranslationsMixin,
20
22
  serializers.ModelSerializer,
21
23
  ):
@@ -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,10 @@ 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
  class StructuredJSONField(serializers.JSONField):
@@ -18,6 +18,7 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
18
18
  """
19
19
 
20
20
  def __init__(self, **kwargs):
21
+ self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
21
22
  self.serializer = kwargs.pop("serializer", None)
22
23
  self.lookup = kwargs.pop("lookup", "id")
23
24
  if self.serializer is not None:
@@ -42,7 +43,10 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
42
43
 
43
44
  def to_representation(self, instance):
44
45
  if self.serializer:
45
- return self.serializer(instance, context=self.context).data
46
+ kwargs = {"context": self.context}
47
+ if self.inherited_fields_filter:
48
+ kwargs["inherited_fields_filter"] = self.inherited_fields_filter
49
+ return self.serializer(instance, **kwargs).data
46
50
  return super().to_representation(instance)
47
51
 
48
52
  def to_internal_value(self, data):
@@ -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
@@ -58,8 +57,50 @@ class SetupEagerLoadingMixin:
58
57
  """
59
58
  This mixin allows to use the setup_eager_loading method to optimize the queries.
60
59
  """
61
- @staticmethod
62
- def setup_eager_loading(queryset):
60
+
61
+ @classmethod
62
+ def optimize_qs(cls, queryset, context=None):
63
+ if hasattr(cls, "setup_eager_loading"):
64
+ queryset = cls.setup_eager_loading(queryset, context=context)
65
+ return cls.auto_optimize_queryset(queryset, context=context)
66
+
67
+ @classmethod
68
+ def auto_optimize_queryset(cls, queryset, context=None):
69
+ request = context.get("request", None)
70
+ if request and request.method == "GET":
71
+ model = getattr(cls.Meta, "model", None)
72
+ info = model_meta.get_field_info(model)
73
+ only = set()
74
+ prefetch_related = set()
75
+ select_related = set()
76
+ serializer_fields = cls(context=context).fields.keys()
77
+ filtered_fields = set()
78
+ for field in request.query_params.get("fields", "").split(","):
79
+ if "__" in field:
80
+ field, _ = field.split("__", 1)
81
+ if field in serializer_fields:
82
+ filtered_fields.add(field)
83
+ if len(filtered_fields) == 0:
84
+ filtered_fields = serializer_fields
85
+ for field in filtered_fields:
86
+ complete_field = field
87
+ if "__" in field:
88
+ field, sub_field = field.split("__", 1)
89
+ complete_field = f"{field}__{sub_field}"
90
+ if field in info.forward_relations and not info.forward_relations[field].to_many:
91
+ select_related.add(field)
92
+ only.add(complete_field)
93
+ elif field in info.reverse_relations or field in info.forward_relations and info.forward_relations[field].to_many:
94
+ prefetch_related.add(field)
95
+ only.add(complete_field)
96
+ elif field in info.fields or field == info.pk.name:
97
+ only.add(complete_field)
98
+ if len(only) > 0:
99
+ queryset = queryset.only(*only)
100
+ if len(select_related) > 0:
101
+ queryset = queryset.select_related(*select_related)
102
+ if len(prefetch_related) > 0:
103
+ queryset = queryset.prefetch_related(*prefetch_related)
63
104
  return queryset
64
105
 
65
106
 
@@ -169,32 +210,29 @@ class AbstractPageMixin(serializers.ModelSerializer):
169
210
  def get_breadcrumbs(self, instance: "AbstractPage"):
170
211
  return instance.breadcrumbs
171
212
 
172
- LANG_PERMALINK_FIELDS = [
173
- build_localized_fieldname("permalink", lang)
174
- for lang in AVAILABLE_LANGUAGES
175
- if settings.ENABLE_TRANSLATIONS
176
- ]
177
-
178
213
  @property
179
214
  def translation_fields(self):
180
215
  return super().translation_fields + ["permalink"]
181
216
 
182
217
  def get_default_field_names(self, *args):
183
218
  from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
184
-
219
+ default_fields = super().get_default_field_names(*args)
220
+ filtered_fields = getattr(self, "filtered_fields", [])
221
+ if len(filtered_fields) > 0:
222
+ return filtered_fields
185
223
  if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
186
- return super().get_default_field_names(*args)
224
+ return default_fields
187
225
  return (
188
- [f for f in super().get_default_field_names(*args) if f != "url_node"]
189
- + self.LANG_PERMALINK_FIELDS
190
- + ["permalink"]
226
+ [f for f in default_fields if f != "url_node"]
227
+ + UrlNode.LANG_PERMALINK_FIELDS
228
+ + ["permalink"]
191
229
  )
192
230
 
193
231
  def build_field(self, field_name, info, model_class, nested_depth):
194
- if field_name in self.LANG_PERMALINK_FIELDS + ["permalink"]:
232
+ if field_name in UrlNode.LANG_PERMALINK_FIELDS + ["permalink"]:
195
233
  return serializers.CharField, {
196
- "source": "url_node.%s" % field_name,
197
- "read_only": True,
234
+ "required": False,
235
+ "allow_blank": True,
198
236
  }
199
237
  return super().build_field(field_name, info, model_class, nested_depth)
200
238