django-camomilla-cms 6.0.0b15__py2.py3-none-any.whl → 6.0.0b17__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
  3. camomilla/dynamic_pages_urls.py +6 -2
  4. camomilla/managers/pages.py +93 -8
  5. camomilla/model_api.py +14 -7
  6. camomilla/models/media.py +1 -1
  7. camomilla/models/menu.py +10 -4
  8. camomilla/models/page.py +189 -127
  9. camomilla/openapi/schema.py +17 -8
  10. camomilla/redirects.py +10 -0
  11. camomilla/serializers/base/__init__.py +6 -4
  12. camomilla/serializers/fields/__init__.py +5 -17
  13. camomilla/serializers/fields/related.py +10 -4
  14. camomilla/serializers/mixins/__init__.py +23 -195
  15. camomilla/serializers/mixins/fields.py +20 -0
  16. camomilla/serializers/mixins/filter_fields.py +57 -0
  17. camomilla/serializers/mixins/json.py +34 -0
  18. camomilla/serializers/mixins/language.py +32 -0
  19. camomilla/serializers/mixins/nesting.py +35 -0
  20. camomilla/serializers/mixins/optimize.py +91 -0
  21. camomilla/serializers/mixins/ordering.py +34 -0
  22. camomilla/serializers/mixins/page.py +58 -0
  23. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  24. camomilla/serializers/utils.py +5 -3
  25. camomilla/serializers/validators.py +6 -2
  26. camomilla/settings.py +10 -2
  27. camomilla/storages/default.py +12 -0
  28. camomilla/storages/optimize.py +2 -2
  29. camomilla/storages/overwrite.py +2 -2
  30. camomilla/templates/defaults/parts/menu.html +1 -1
  31. camomilla/templatetags/menus.py +3 -0
  32. camomilla/theme/__init__.py +1 -1
  33. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  34. camomilla/theme/admin/pages.py +46 -0
  35. camomilla/theme/admin/translations.py +13 -0
  36. camomilla/theme/apps.py +1 -5
  37. camomilla/translation.py +7 -1
  38. camomilla/urls.py +2 -5
  39. camomilla/utils/query_parser.py +167 -0
  40. camomilla/utils/translation.py +47 -5
  41. camomilla/views/base/__init__.py +35 -5
  42. camomilla/views/medias.py +1 -1
  43. camomilla/views/menus.py +0 -2
  44. camomilla/views/mixins/__init__.py +17 -69
  45. camomilla/views/mixins/bulk_actions.py +22 -0
  46. camomilla/views/mixins/language.py +33 -0
  47. camomilla/views/mixins/optimize.py +18 -0
  48. camomilla/views/mixins/pagination.py +12 -18
  49. camomilla/views/mixins/permissions.py +6 -0
  50. camomilla/views/pages.py +12 -2
  51. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
  52. django_camomilla_cms-6.0.0b17.dist-info/RECORD +132 -0
  53. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
  54. tests/fixtures/__init__.py +17 -0
  55. tests/test_api.py +2 -11
  56. tests/test_camomilla_filters.py +7 -13
  57. tests/test_media.py +113 -0
  58. tests/test_menu.py +97 -0
  59. tests/test_model_api.py +68 -0
  60. tests/test_model_api_permissions.py +39 -0
  61. tests/test_model_api_register.py +393 -0
  62. tests/test_pages.py +343 -0
  63. tests/test_query_parser.py +58 -0
  64. tests/test_templates_context.py +111 -0
  65. tests/test_utils.py +64 -64
  66. tests/utils/api.py +28 -0
  67. tests/utils/media.py +9 -0
  68. camomilla/serializers/fields/json.py +0 -49
  69. django_camomilla_cms-6.0.0b15.dist-info/RECORD +0 -105
  70. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
  71. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
  72. {camomilla/contrib/rest_framework → tests/utils}/__init__.py +0 -0
camomilla/models/page.py CHANGED
@@ -1,12 +1,13 @@
1
- from typing import Sequence, Tuple
1
+ from typing import Sequence, Tuple, Optional, Union
2
2
  from uuid import uuid4
3
3
 
4
4
  from django.core.exceptions import ObjectDoesNotExist
5
5
 
6
- from django.db import ProgrammingError, OperationalError, models, transaction
7
- from django.db.models.signals import post_delete
6
+ from django.db import models, transaction
7
+ from django.db.models.signals import post_delete, post_save, pre_save
8
8
  from django.dispatch import receiver
9
- from django.http import Http404
9
+ from django.http import Http404, HttpRequest
10
+ from django.shortcuts import redirect
10
11
  from django.urls import NoReverseMatch, reverse
11
12
  from django.utils import timezone
12
13
  from django.utils.functional import lazy
@@ -14,7 +15,7 @@ from django.utils.text import slugify
14
15
  from django.utils.translation import gettext_lazy as _
15
16
  from django.utils.translation import get_language
16
17
 
17
- from camomilla.managers.pages import PageQuerySet
18
+ from camomilla.managers.pages import PageQuerySet, UrlNodeManager
18
19
  from camomilla.models.mixins import MetaMixin, SeoMixin
19
20
  from camomilla.utils import (
20
21
  activate_languages,
@@ -29,101 +30,86 @@ from camomilla.utils.getters import pointed_getter
29
30
  from camomilla import settings
30
31
  from camomilla.templates_context.rendering import ctx_registry
31
32
  from django.conf import settings as django_settings
32
- from modeltranslation.settings import AVAILABLE_LANGUAGES
33
33
  from modeltranslation.utils import build_localized_fieldname
34
34
 
35
35
 
36
- class UrlPathValidator():
37
- pass
38
-
39
36
  def GET_TEMPLATE_CHOICES():
40
37
  return [(t, t) for t in get_all_templates_files()]
41
38
 
42
39
 
43
- class UrlNodeManager(models.Manager):
44
- @property
45
- def related_names(self):
46
- self._related_names = getattr(
47
- self,
48
- "_related_names",
49
- super().get_queryset().values_list("related_name", flat=True).distinct(),
50
- )
51
- return self._related_names
52
-
53
- def _annotate_fields(
54
- self,
55
- qs: models.QuerySet,
56
- field_names: Sequence[Tuple[str, models.Field, models.Value]],
57
- ):
58
- for field_name, output_field, default in field_names:
59
- whens = [
60
- models.When(
61
- related_name=related_name,
62
- then=models.F("__".join([related_name, field_name])),
63
- )
64
- for related_name in self.related_names
65
- ]
66
- qs = qs.annotate(
67
- **{
68
- field_name: models.Case(
69
- *whens, output_field=output_field, default=default
70
- )
71
- }
72
- )
73
- return self._annotate_is_public(qs)
74
-
75
- def _annotate_is_public(self, qs: models.QuerySet):
76
- return qs.annotate(
77
- is_public=models.Case(
78
- models.When(status="PUB", then=True),
79
- models.When(
80
- status="PLA", publication_date__lte=timezone.now(), then=True
81
- ),
82
- default=False,
83
- output_field=models.BooleanField(default=False),
84
- )
40
+ class UrlRedirect(models.Model):
41
+ language_code = models.CharField(max_length=10, null=True)
42
+ from_url = models.CharField(max_length=400)
43
+ to_url = models.CharField(max_length=400)
44
+ url_node = models.ForeignKey(
45
+ "UrlNode", on_delete=models.CASCADE, related_name="redirects"
46
+ )
47
+ permanent = models.BooleanField(default=True)
48
+ date_created = models.DateTimeField(auto_now_add=True)
49
+ date_updated_at = models.DateTimeField(auto_now=True)
50
+
51
+ __q_string = ""
52
+
53
+ def __str__(self) -> str:
54
+ return f"[{self.language_code}] {self.from_url} -> {self.to_url}"
55
+
56
+ @classmethod
57
+ def find_redirect(
58
+ cls, request: HttpRequest, language_code: Optional[str] = None
59
+ ) -> Optional["UrlRedirect"]:
60
+ instance = cls.find_redirect_from_url(request.path, language_code)
61
+ if instance:
62
+ instance.__q_string = request.META.get("QUERY_STRING", "")
63
+ return instance
64
+
65
+ @classmethod
66
+ def find_redirect_from_url(
67
+ cls, from_url: str, language_code: Optional[str] = None
68
+ ) -> Optional["UrlRedirect"]:
69
+ path_decomposition = url_lang_decompose(from_url)
70
+ language_code = (
71
+ language_code or path_decomposition["language"] or get_language()
85
72
  )
73
+ from_url = path_decomposition["permalink"]
74
+ return cls.objects.filter(
75
+ from_url=from_url.rstrip("/"), language_code=language_code or get_language()
76
+ ).first()
86
77
 
87
- def get_queryset(self):
88
- try:
89
- return self._annotate_fields(
90
- super().get_queryset(),
91
- [
92
- (
93
- "indexable",
94
- models.BooleanField(),
95
- models.Value(None, models.BooleanField()),
96
- ),
97
- (
98
- "status",
99
- models.CharField(),
100
- models.Value("DRF", models.CharField()),
101
- ),
102
- (
103
- "publication_date",
104
- models.DateTimeField(),
105
- models.Value(timezone.now(), models.DateTimeField()),
106
- ),
107
- (
108
- "date_updated_at",
109
- models.DateTimeField(),
110
- models.Value(timezone.now(), models.DateTimeField()),
111
- ),
112
- ],
113
- )
114
- except (ProgrammingError, OperationalError):
115
- return super().get_queryset()
78
+ def redirect(self) -> str:
79
+ return redirect(self.redirect_to, permanent=self.permanent)
80
+
81
+ @property
82
+ def redirect_to(self) -> str:
83
+ url_to = "/" + self.to_url.lstrip("/")
84
+ if getattr(django_settings, "APPEND_SLASH", True) and not url_to.endswith("/"):
85
+ url_to += "/"
86
+ if (
87
+ self.language_code != settings.DEFAULT_LANGUAGE
88
+ and settings.ENABLE_TRANSLATIONS
89
+ ):
90
+ url_to = "/" + self.language_code + url_to
91
+ return url_to + ("?" + self.__q_string if self.__q_string else "")
92
+
93
+ class Meta:
94
+ verbose_name = _("Redirect")
95
+ verbose_name_plural = _("Redirects")
96
+ unique_together = ("from_url", "language_code")
97
+ indexes = [
98
+ models.Index(fields=["from_url", "language_code"]),
99
+ ]
116
100
 
117
101
 
118
102
  class UrlNode(models.Model):
119
-
120
- LANG_PERMALINK_FIELDS = [
121
- build_localized_fieldname("permalink", lang)
122
- for lang in AVAILABLE_LANGUAGES
103
+
104
+ LANG_PERMALINK_FIELDS = (
105
+ [
106
+ build_localized_fieldname("permalink", lang)
107
+ for lang in settings.AVAILABLE_LANGUAGES
108
+ ]
123
109
  if settings.ENABLE_TRANSLATIONS
124
- ]
125
-
126
-
110
+ else ["permalink"]
111
+ )
112
+
127
113
  permalink = models.CharField(max_length=400, unique=True, null=True)
128
114
  related_name = models.CharField(max_length=200)
129
115
  objects = UrlNodeManager()
@@ -133,7 +119,7 @@ class UrlNode(models.Model):
133
119
  return getattr(self, self.related_name)
134
120
 
135
121
  @staticmethod
136
- def reverse_url(permalink: str) -> str:
122
+ def reverse_url(permalink: str, request: Optional[HttpRequest] = None) -> str:
137
123
  append_slash = getattr(django_settings, "APPEND_SLASH", True)
138
124
  try:
139
125
  if permalink == "/":
@@ -141,6 +127,8 @@ class UrlNode(models.Model):
141
127
  url = reverse("camomilla-permalink", args=(permalink.lstrip("/"),))
142
128
  if append_slash and not url.endswith("/"):
143
129
  url += "/"
130
+ if request:
131
+ url = request.build_absolute_uri(url)
144
132
  return url
145
133
  except NoReverseMatch:
146
134
  return None
@@ -153,21 +141,27 @@ class UrlNode(models.Model):
153
141
  if self.routerlink == "/":
154
142
  return ""
155
143
  return self.routerlink
156
-
144
+
157
145
  @staticmethod
158
146
  def sanitize_permalink(permalink):
159
147
  if isinstance(permalink, str):
160
148
  p_parts = permalink.split("/")
161
- permalink = "/".join([slugify(p, allow_unicode=True).strip() for p in p_parts])
149
+ permalink = "/".join(
150
+ [slugify(p, allow_unicode=True).strip() for p in p_parts]
151
+ )
162
152
  if not permalink.startswith("/"):
163
153
  permalink = f"/{permalink}"
164
154
  return permalink
165
-
155
+
166
156
  def save(self, *args, **kwargs) -> None:
167
157
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
168
- setattr(self, lang_p_field, UrlNode.sanitize_permalink(getattr(self, lang_p_field)))
158
+ setattr(
159
+ self,
160
+ lang_p_field,
161
+ UrlNode.sanitize_permalink(getattr(self, lang_p_field)),
162
+ )
169
163
  super().save(*args, **kwargs)
170
-
164
+
171
165
  def __str__(self) -> str:
172
166
  return self.permalink
173
167
 
@@ -185,15 +179,22 @@ PAGE_STATUS = (
185
179
 
186
180
  class PageBase(models.base.ModelBase):
187
181
  """
188
- This models comes to implement a language based permalink logic
182
+ This models comes to implement a language based permalink logic
189
183
  """
184
+
190
185
  def perm_prop_factory(permalink_field):
191
186
  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):
187
+ return getattr(
188
+ _self,
189
+ f"__{permalink_field}",
190
+ getattr(_self.url_node or object(), permalink_field, None),
191
+ )
192
+
193
+ def setter(_self, value: str):
194
194
  setattr(_self, f"__{permalink_field}", value)
195
+
195
196
  return getter, setter
196
-
197
+
197
198
  def __new__(cls, name, bases, attrs, **kwargs):
198
199
  attr_meta = attrs.pop("PageMeta", None)
199
200
  new_class = super().__new__(cls, name, bases, attrs, **kwargs)
@@ -202,10 +203,23 @@ class PageBase(models.base.ModelBase):
202
203
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
203
204
  computed_prop = property(*cls.perm_prop_factory(lang_p_field))
204
205
  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
- ))
206
+ if settings.ENABLE_TRANSLATIONS:
207
+ setattr(
208
+ new_class,
209
+ "permalink",
210
+ property(
211
+ lambda _self: getattr(
212
+ _self,
213
+ build_localized_fieldname("permalink", get_language()),
214
+ None,
215
+ ),
216
+ lambda _self, value: setattr(
217
+ _self,
218
+ f"__{build_localized_fieldname('permalink', get_language())}",
219
+ value,
220
+ ),
221
+ ),
222
+ )
209
223
  if page_meta:
210
224
  for name, value in getattr(base_page_meta, "__dict__", {}).items():
211
225
  if name not in page_meta.__dict__:
@@ -215,8 +229,20 @@ class PageBase(models.base.ModelBase):
215
229
 
216
230
 
217
231
  class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
232
+ identifier = models.CharField(max_length=200, null=True, unique=True, default=uuid4)
218
233
  date_created = models.DateTimeField(auto_now_add=True)
219
234
  date_updated_at = models.DateTimeField(auto_now=True)
235
+ breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
236
+ template = models.CharField(max_length=500, null=True, blank=True, choices=[])
237
+ template_data = models.JSONField(default=dict, null=False, blank=True)
238
+ ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
239
+ parent_page = models.ForeignKey(
240
+ "self",
241
+ related_name=PAGE_CHILD_RELATED_NAME,
242
+ null=True,
243
+ blank=True,
244
+ on_delete=models.CASCADE,
245
+ )
220
246
  url_node = models.OneToOneField(
221
247
  UrlNode,
222
248
  on_delete=models.CASCADE,
@@ -224,29 +250,17 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
224
250
  null=True,
225
251
  editable=False,
226
252
  )
227
- breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
228
- autopermalink = models.BooleanField(default=True)
253
+ publication_date = models.DateTimeField(null=True, blank=True)
229
254
  status = models.CharField(
230
255
  max_length=3,
231
256
  choices=PAGE_STATUS,
232
257
  default="DRF",
233
258
  )
234
- template = models.CharField(max_length=500, null=True, blank=True, choices=[])
235
- template_data = models.JSONField(default=dict, null=False, blank=True)
236
- identifier = models.CharField(max_length=200, null=True, unique=True, default=uuid4)
237
- publication_date = models.DateTimeField(null=True, blank=True)
238
259
  indexable = models.BooleanField(default=True)
239
- ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
240
- parent_page = models.ForeignKey(
241
- "self",
242
- related_name=PAGE_CHILD_RELATED_NAME,
243
- null=True,
244
- blank=True,
245
- on_delete=models.CASCADE,
246
- )
260
+ autopermalink = models.BooleanField(default=True)
247
261
 
248
262
  objects = PageQuerySet.as_manager()
249
-
263
+
250
264
  __cached_db_instance: "AbstractPage" = None
251
265
 
252
266
  @property
@@ -254,7 +268,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
254
268
  if self.__cached_db_instance is None:
255
269
  self.__cached_db_instance = self.get_db_instance()
256
270
  return self.__cached_db_instance
257
-
271
+
258
272
  def get_db_instance(self):
259
273
  if self.pk:
260
274
  return self.__class__.objects.get(pk=self.pk)
@@ -267,7 +281,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
267
281
  def __str__(self) -> str:
268
282
  return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
269
283
 
270
- def get_context(self, request=None):
284
+ def get_context(self, request: Optional[HttpRequest] = None):
271
285
  context = {
272
286
  "page": self,
273
287
  "page_model": {"class": self.__class__.__name__, "module": self.__module__},
@@ -312,7 +326,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
312
326
  return bool(publication_date) and timezone.now() > publication_date
313
327
  return False
314
328
 
315
- def get_template_path(self, request=None) -> str:
329
+ def get_template_path(self, request: Optional[HttpRequest] = None) -> str:
316
330
  return self.template or pointed_getter(self, "_page_meta.default_template")
317
331
 
318
332
  @property
@@ -337,7 +351,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
337
351
  for __ in activate_languages():
338
352
  old_permalink = self.db_instance and self.db_instance.permalink
339
353
  new_permalink = self.permalink
340
- if self.autopermalink:
354
+ if self.autopermalink:
341
355
  new_permalink = self.generate_permalink()
342
356
  force = force or old_permalink != new_permalink
343
357
  set_nofallbacks(self.url_node, "permalink", new_permalink)
@@ -361,7 +375,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
361
375
  def update_childs(self) -> None:
362
376
  # without pk, no childs there
363
377
  if self.pk is not None:
364
- for child in self.childs.all():
378
+ exclude_kwargs = {}
379
+ if self.childs.model == self.__class__:
380
+ exclude_kwargs["pk"] = self.pk
381
+ for child in self.childs.exclude(**exclude_kwargs):
365
382
  child.save()
366
383
 
367
384
  def save(self, *args, **kwargs) -> None:
@@ -370,10 +387,12 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
370
387
  super().save(*args, **kwargs)
371
388
  self.__cached_db_instance = None
372
389
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
373
- hasattr(self, f"__{lang_p_field}") and delattr(self, f"__{lang_p_field}")
390
+ hasattr(self, f"__{lang_p_field}") and delattr(
391
+ self, f"__{lang_p_field}"
392
+ )
374
393
 
375
394
  @classmethod
376
- def get(cls, request, *args, **kwargs) -> "AbstractPage":
395
+ def get(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
377
396
  bypass_type_check = kwargs.pop("bypass_type_check", False)
378
397
  bypass_public_check = kwargs.pop("bypass_public_check", False)
379
398
  if len(kwargs.keys()) > 0:
@@ -406,7 +425,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
406
425
  return page
407
426
 
408
427
  @classmethod
409
- def get_or_create(cls, request, *args, **kwargs) -> Tuple["AbstractPage", bool]:
428
+ def get_or_create(
429
+ cls, request: HttpRequest, *args, **kwargs
430
+ ) -> Tuple["AbstractPage", bool]:
410
431
  try:
411
432
  return cls.get(request, *args, **kwargs), False
412
433
  except ObjectDoesNotExist:
@@ -426,14 +447,14 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
426
447
  return cls.get_or_create(None, permalink="/")
427
448
 
428
449
  @classmethod
429
- def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
450
+ def get_or_404(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
430
451
  try:
431
452
  return cls.get(request, *args, **kwargs)
432
453
  except ObjectDoesNotExist as ex:
433
454
  raise Http404(ex)
434
455
 
435
456
  def alternate_urls(self, *args, **kwargs) -> dict:
436
- request = False
457
+ request: Union[HttpRequest, bool] = False
437
458
  if len(args) > 0:
438
459
  request = args[0]
439
460
  if "request" in kwargs:
@@ -443,7 +464,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
443
464
  for lang in activate_languages():
444
465
  if lang in permalinks:
445
466
  permalinks[lang] = (
446
- UrlNode.reverse_url(permalinks[lang]) if preview or self.is_public else None
467
+ UrlNode.reverse_url(permalinks[lang])
468
+ if preview or self.is_public
469
+ else None
447
470
  )
448
471
  if preview:
449
472
  permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
@@ -469,3 +492,42 @@ class Page(AbstractPage):
469
492
  def auto_delete_url_node(sender, instance, **kwargs):
470
493
  if issubclass(sender, AbstractPage):
471
494
  instance.url_node and instance.url_node.delete()
495
+
496
+
497
+ __url_node_history__ = {}
498
+
499
+
500
+ @receiver(pre_save, sender=UrlNode)
501
+ def cache_url_node(sender, instance, **kwargs):
502
+ if instance.pk:
503
+ __url_node_history__[instance.pk] = sender.objects.filter(
504
+ pk=instance.pk
505
+ ).first()
506
+
507
+
508
+ @receiver(post_save, sender=UrlNode)
509
+ def generate_redirects(sender, instance, **kwargs):
510
+ previous = __url_node_history__.pop(instance.pk, None)
511
+ if previous:
512
+ redirects = []
513
+ with transaction.atomic():
514
+ for lang in activate_languages():
515
+ new_permalink = get_nofallbacks(instance, "permalink")
516
+ old_permalink = get_nofallbacks(previous, "permalink")
517
+ UrlRedirect.objects.filter(
518
+ from_url=new_permalink, language_code=lang
519
+ ).delete()
520
+ if old_permalink and old_permalink != new_permalink:
521
+ redirects.append(
522
+ UrlRedirect(
523
+ from_url=old_permalink,
524
+ to_url=new_permalink,
525
+ url_node=instance,
526
+ language_code=lang,
527
+ )
528
+ )
529
+ UrlRedirect.objects.filter(
530
+ to_url=old_permalink, language_code=lang
531
+ ).update(to_url=new_permalink)
532
+ if len(redirects) > 0:
533
+ UrlRedirect.objects.bulk_create(redirects)
@@ -2,13 +2,11 @@ from rest_framework.schemas.openapi import (
2
2
  SchemaGenerator as DRFSchemaGenerator,
3
3
  AutoSchema as DRFAutoSchema,
4
4
  )
5
- from camomilla.contrib.rest_framework.serializer import (
6
- TranslationsMixin,
7
- plain_to_nest,
8
- TRANS_ACCESSOR,
9
- )
10
- from camomilla.serializers.fields.json import StructuredJSONField
11
5
  from camomilla.utils.getters import find_and_replace_dict
6
+ from camomilla.utils.translation import plain_to_nest
7
+ from camomilla.settings import API_TRANSLATION_ACCESSOR
8
+ from camomilla.serializers.mixins import TranslationsMixin
9
+ from structured.contrib.restframework import StructuredJSONField
12
10
 
13
11
 
14
12
  class AutoSchema(DRFAutoSchema):
@@ -18,11 +16,11 @@ class AutoSchema(DRFAutoSchema):
18
16
  schema = super(AutoSchema, self).map_serializer(serializer)
19
17
  if isinstance(serializer, TranslationsMixin) and serializer.is_translatable:
20
18
  schema = plain_to_nest(schema["properties"], serializer.translation_fields)
21
- schema[TRANS_ACCESSOR] = {
19
+ schema[API_TRANSLATION_ACCESSOR] = {
22
20
  "type": "object",
23
21
  "properties": {
24
22
  k: {"type": "object", "properties": v}
25
- for k, v in schema[TRANS_ACCESSOR].items()
23
+ for k, v in schema[API_TRANSLATION_ACCESSOR].items()
26
24
  },
27
25
  }
28
26
  return schema
@@ -55,4 +53,15 @@ class SchemaGenerator(DRFSchemaGenerator):
55
53
  def create_view(self, callback, method, request=None):
56
54
  view = super(SchemaGenerator, self).create_view(callback, method, request)
57
55
  view.schema = AutoSchema()
56
+ if (
57
+ not hasattr(view, "get_queryset")
58
+ and getattr(view, "queryset", None) is None
59
+ ):
60
+ attname = "permission_classes"
61
+ cname = "DjangoModelPermissions"
62
+ setattr(
63
+ view,
64
+ attname,
65
+ [p for p in getattr(view, attname, []) if cname not in p.__name__],
66
+ )
58
67
  return view
camomilla/redirects.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.urls import path
2
+ from django.shortcuts import redirect
3
+
4
+
5
+ url_patterns = [
6
+ path(
7
+ "profiles/me/", lambda _: redirect("../../users/current/"), name="profiles-me"
8
+ ),
9
+ path("sitemap/", lambda _: redirect("../pages/"), name="sitemap"),
10
+ ]
@@ -1,21 +1,23 @@
1
1
  from rest_framework import serializers
2
2
 
3
- from ...contrib.rest_framework.serializer import TranslationsMixin
4
- from ..fields import FieldsOverrideMixin
5
- from ..mixins import (
3
+ from camomilla.serializers.mixins import (
6
4
  JSONFieldPatchMixin,
7
5
  NestMixin,
8
6
  OrderingMixin,
9
7
  SetupEagerLoadingMixin,
8
+ FilterFieldsMixin,
9
+ FieldsOverrideMixin,
10
+ TranslationsMixin,
10
11
  )
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,21 +1,9 @@
1
- from django.db import models
2
- from rest_framework import serializers
3
-
4
- from structured.fields import StructuredJSONField as ModelStructuredJSONField
5
-
6
- from .json import StructuredJSONField
7
1
  from .file import FileField, ImageField
8
2
  from .related import RelatedField
9
3
 
10
4
 
11
- class FieldsOverrideMixin:
12
- """
13
- This mixin automatically overrides the fields of the serializer with camomilla's backed ones.
14
- """
15
- serializer_field_mapping = {
16
- **serializers.ModelSerializer.serializer_field_mapping,
17
- models.FileField: FileField,
18
- models.ImageField: ImageField,
19
- ModelStructuredJSONField: StructuredJSONField,
20
- }
21
- serializer_related_field = RelatedField
5
+ __all__ = [
6
+ "FileField",
7
+ "ImageField",
8
+ "RelatedField",
9
+ ]
@@ -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):
@@ -93,9 +97,11 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
93
97
  for item in child.get_queryset().filter(
94
98
  **{
95
99
  f"{child.lookup}__in": [
96
- item.get(child.lookup, None)
97
- if isinstance(item, dict)
98
- else item
100
+ (
101
+ item.get(child.lookup, None)
102
+ if isinstance(item, dict)
103
+ else item
104
+ )
99
105
  for item in data
100
106
  ]
101
107
  }