django-camomilla-cms 6.0.0b16__py2.py3-none-any.whl → 6.0.0b18__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 (65) 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 +87 -2
  5. camomilla/model_api.py +6 -4
  6. camomilla/models/menu.py +9 -4
  7. camomilla/models/page.py +178 -117
  8. camomilla/openapi/schema.py +15 -10
  9. camomilla/redirects.py +10 -0
  10. camomilla/serializers/base/__init__.py +4 -4
  11. camomilla/serializers/fields/__init__.py +5 -17
  12. camomilla/serializers/fields/related.py +5 -3
  13. camomilla/serializers/mixins/__init__.py +23 -240
  14. camomilla/serializers/mixins/fields.py +20 -0
  15. camomilla/serializers/mixins/filter_fields.py +9 -8
  16. camomilla/serializers/mixins/json.py +34 -0
  17. camomilla/serializers/mixins/language.py +32 -0
  18. camomilla/serializers/mixins/nesting.py +35 -0
  19. camomilla/serializers/mixins/optimize.py +91 -0
  20. camomilla/serializers/mixins/ordering.py +34 -0
  21. camomilla/serializers/mixins/page.py +58 -0
  22. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  23. camomilla/serializers/utils.py +3 -3
  24. camomilla/serializers/validators.py +6 -2
  25. camomilla/settings.py +16 -2
  26. camomilla/storages/default.py +7 -1
  27. camomilla/templates/defaults/base.html +60 -4
  28. camomilla/templates/defaults/parts/menu.html +1 -1
  29. camomilla/templatetags/menus.py +3 -0
  30. camomilla/templatetags/model_extras.py +73 -0
  31. camomilla/theme/__init__.py +1 -1
  32. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  33. camomilla/theme/admin/pages.py +46 -0
  34. camomilla/theme/admin/translations.py +13 -0
  35. camomilla/theme/apps.py +2 -5
  36. camomilla/translation.py +7 -1
  37. camomilla/urls.py +2 -5
  38. camomilla/utils/query_parser.py +42 -23
  39. camomilla/utils/templates.py +23 -10
  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/mixins/__init__.py +17 -76
  44. camomilla/views/mixins/bulk_actions.py +22 -0
  45. camomilla/views/mixins/language.py +33 -0
  46. camomilla/views/mixins/optimize.py +18 -0
  47. camomilla/views/mixins/pagination.py +11 -8
  48. camomilla/views/mixins/permissions.py +6 -0
  49. camomilla/views/pages.py +12 -2
  50. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/METADATA +23 -16
  51. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/RECORD +63 -45
  52. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/WHEEL +1 -1
  53. tests/test_camomilla_filters.py +1 -1
  54. tests/test_media.py +98 -65
  55. tests/test_menu.py +97 -0
  56. tests/test_model_api_register.py +393 -0
  57. tests/test_pages.py +343 -0
  58. tests/test_query_parser.py +1 -2
  59. tests/test_templates_context.py +111 -0
  60. tests/utils/api.py +0 -1
  61. tests/utils/media.py +10 -0
  62. camomilla/contrib/rest_framework/__init__.py +0 -0
  63. camomilla/serializers/fields/json.py +0 -48
  64. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info/licenses}/LICENSE +0 -0
  65. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/top_level.txt +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,100 +30,85 @@ 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
-
40
36
  def GET_TEMPLATE_CHOICES():
41
37
  return [(t, t) for t in get_all_templates_files()]
42
38
 
43
39
 
44
- class UrlNodeManager(models.Manager):
45
- @property
46
- def related_names(self):
47
- self._related_names = getattr(
48
- self,
49
- "_related_names",
50
- super().get_queryset().values_list("related_name", flat=True).distinct(),
51
- )
52
- return self._related_names
53
-
54
- def _annotate_fields(
55
- self,
56
- qs: models.QuerySet,
57
- field_names: Sequence[Tuple[str, models.Field, models.Value]],
58
- ):
59
- for field_name, output_field, default in field_names:
60
- whens = [
61
- models.When(
62
- related_name=related_name,
63
- then=models.F("__".join([related_name, field_name])),
64
- )
65
- for related_name in self.related_names
66
- ]
67
- qs = qs.annotate(
68
- **{
69
- field_name: models.Case(
70
- *whens, output_field=output_field, default=default
71
- )
72
- }
73
- )
74
- return self._annotate_is_public(qs)
75
-
76
- def _annotate_is_public(self, qs: models.QuerySet):
77
- return qs.annotate(
78
- is_public=models.Case(
79
- models.When(status="PUB", then=True),
80
- models.When(
81
- status="PLA", publication_date__lte=timezone.now(), then=True
82
- ),
83
- default=False,
84
- output_field=models.BooleanField(default=False),
85
- )
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()
86
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()
87
77
 
88
- def get_queryset(self):
89
- try:
90
- return self._annotate_fields(
91
- super().get_queryset(),
92
- [
93
- (
94
- "indexable",
95
- models.BooleanField(),
96
- models.Value(None, models.BooleanField()),
97
- ),
98
- (
99
- "status",
100
- models.CharField(),
101
- models.Value("DRF", models.CharField()),
102
- ),
103
- (
104
- "publication_date",
105
- models.DateTimeField(),
106
- models.Value(timezone.now(), models.DateTimeField()),
107
- ),
108
- (
109
- "date_updated_at",
110
- models.DateTimeField(),
111
- models.Value(timezone.now(), models.DateTimeField()),
112
- ),
113
- ],
114
- )
115
- except (ProgrammingError, OperationalError):
116
- 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
+ ]
117
100
 
118
101
 
119
102
  class UrlNode(models.Model):
120
103
 
121
- LANG_PERMALINK_FIELDS = [
122
- build_localized_fieldname("permalink", lang)
123
- for lang in AVAILABLE_LANGUAGES
104
+ LANG_PERMALINK_FIELDS = (
105
+ [
106
+ build_localized_fieldname("permalink", lang)
107
+ for lang in settings.AVAILABLE_LANGUAGES
108
+ ]
124
109
  if settings.ENABLE_TRANSLATIONS
125
- ]
110
+ else ["permalink"]
111
+ )
126
112
 
127
113
  permalink = models.CharField(max_length=400, unique=True, null=True)
128
114
  related_name = models.CharField(max_length=200)
@@ -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
@@ -158,14 +146,20 @@ class UrlNode(models.Model):
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:
@@ -185,14 +179,20 @@ 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))
187
+ return getattr(
188
+ _self,
189
+ f"__{permalink_field}",
190
+ getattr(_self.url_node or object(), permalink_field, None),
191
+ )
193
192
 
194
193
  def setter(_self, value: str):
195
194
  setattr(_self, f"__{permalink_field}", value)
195
+
196
196
  return getter, setter
197
197
 
198
198
  def __new__(cls, name, bases, attrs, **kwargs):
@@ -203,10 +203,23 @@ class PageBase(models.base.ModelBase):
203
203
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
204
204
  computed_prop = property(*cls.perm_prop_factory(lang_p_field))
205
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
- ))
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
+ )
210
223
  if page_meta:
211
224
  for name, value in getattr(base_page_meta, "__dict__", {}).items():
212
225
  if name not in page_meta.__dict__:
@@ -216,8 +229,20 @@ class PageBase(models.base.ModelBase):
216
229
 
217
230
 
218
231
  class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
232
+ identifier = models.CharField(max_length=200, null=True, unique=True, default=uuid4)
219
233
  date_created = models.DateTimeField(auto_now_add=True)
220
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
+ )
221
246
  url_node = models.OneToOneField(
222
247
  UrlNode,
223
248
  on_delete=models.CASCADE,
@@ -225,26 +250,14 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
225
250
  null=True,
226
251
  editable=False,
227
252
  )
228
- breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
229
- autopermalink = models.BooleanField(default=True)
253
+ publication_date = models.DateTimeField(null=True, blank=True)
230
254
  status = models.CharField(
231
255
  max_length=3,
232
256
  choices=PAGE_STATUS,
233
257
  default="DRF",
234
258
  )
235
- template = models.CharField(max_length=500, null=True, blank=True, choices=[])
236
- template_data = models.JSONField(default=dict, null=False, blank=True)
237
- identifier = models.CharField(max_length=200, null=True, unique=True, default=uuid4)
238
- publication_date = models.DateTimeField(null=True, blank=True)
239
259
  indexable = models.BooleanField(default=True)
240
- ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
241
- parent_page = models.ForeignKey(
242
- "self",
243
- related_name=PAGE_CHILD_RELATED_NAME,
244
- null=True,
245
- blank=True,
246
- on_delete=models.CASCADE,
247
- )
260
+ autopermalink = models.BooleanField(default=True)
248
261
 
249
262
  objects = PageQuerySet.as_manager()
250
263
 
@@ -268,7 +281,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
268
281
  def __str__(self) -> str:
269
282
  return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
270
283
 
271
- def get_context(self, request=None):
284
+ def get_context(self, request: Optional[HttpRequest] = None):
272
285
  context = {
273
286
  "page": self,
274
287
  "page_model": {"class": self.__class__.__name__, "module": self.__module__},
@@ -313,7 +326,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
313
326
  return bool(publication_date) and timezone.now() > publication_date
314
327
  return False
315
328
 
316
- def get_template_path(self, request=None) -> str:
329
+ def get_template_path(self, request: Optional[HttpRequest] = None) -> str:
317
330
  return self.template or pointed_getter(self, "_page_meta.default_template")
318
331
 
319
332
  @property
@@ -362,7 +375,10 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
362
375
  def update_childs(self) -> None:
363
376
  # without pk, no childs there
364
377
  if self.pk is not None:
365
- 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):
366
382
  child.save()
367
383
 
368
384
  def save(self, *args, **kwargs) -> None:
@@ -371,10 +387,12 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
371
387
  super().save(*args, **kwargs)
372
388
  self.__cached_db_instance = None
373
389
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
374
- 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
+ )
375
393
 
376
394
  @classmethod
377
- def get(cls, request, *args, **kwargs) -> "AbstractPage":
395
+ def get(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
378
396
  bypass_type_check = kwargs.pop("bypass_type_check", False)
379
397
  bypass_public_check = kwargs.pop("bypass_public_check", False)
380
398
  if len(kwargs.keys()) > 0:
@@ -407,7 +425,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
407
425
  return page
408
426
 
409
427
  @classmethod
410
- 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]:
411
431
  try:
412
432
  return cls.get(request, *args, **kwargs), False
413
433
  except ObjectDoesNotExist:
@@ -427,14 +447,14 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
427
447
  return cls.get_or_create(None, permalink="/")
428
448
 
429
449
  @classmethod
430
- def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
450
+ def get_or_404(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
431
451
  try:
432
452
  return cls.get(request, *args, **kwargs)
433
453
  except ObjectDoesNotExist as ex:
434
454
  raise Http404(ex)
435
455
 
436
456
  def alternate_urls(self, *args, **kwargs) -> dict:
437
- request = False
457
+ request: Union[HttpRequest, bool] = False
438
458
  if len(args) > 0:
439
459
  request = args[0]
440
460
  if "request" in kwargs:
@@ -444,7 +464,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
444
464
  for lang in activate_languages():
445
465
  if lang in permalinks:
446
466
  permalinks[lang] = (
447
- 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
448
470
  )
449
471
  if preview:
450
472
  permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
@@ -470,3 +492,42 @@ class Page(AbstractPage):
470
492
  def auto_delete_url_node(sender, instance, **kwargs):
471
493
  if issubclass(sender, AbstractPage):
472
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,8 +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()
58
- if not hasattr(view, 'get_queryset') and getattr(view, 'queryset', None) is None:
56
+ if (
57
+ not hasattr(view, "get_queryset")
58
+ and getattr(view, "queryset", None) is None
59
+ ):
59
60
  attname = "permission_classes"
60
61
  cname = "DjangoModelPermissions"
61
- setattr(view, attname, [p for p in getattr(view, attname, []) if cname not in p.__name__])
62
+ setattr(
63
+ view,
64
+ attname,
65
+ [p for p in getattr(view, attname, []) if cname not in p.__name__],
66
+ )
62
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,14 +1,14 @@
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
- from ..mixins.filter_fields import FilterFieldsMixin
12
12
 
13
13
 
14
14
  class BaseModelSerializer(
@@ -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
+ ]
@@ -97,9 +97,11 @@ class RelatedField(serializers.PrimaryKeyRelatedField):
97
97
  for item in child.get_queryset().filter(
98
98
  **{
99
99
  f"{child.lookup}__in": [
100
- item.get(child.lookup, None)
101
- if isinstance(item, dict)
102
- else item
100
+ (
101
+ item.get(child.lookup, None)
102
+ if isinstance(item, dict)
103
+ else item
104
+ )
103
105
  for item in data
104
106
  ]
105
107
  }