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.
- camomilla/__init__.py +1 -1
- camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
- camomilla/dynamic_pages_urls.py +6 -2
- camomilla/managers/pages.py +87 -2
- camomilla/model_api.py +6 -4
- camomilla/models/menu.py +9 -4
- camomilla/models/page.py +178 -117
- camomilla/openapi/schema.py +15 -10
- camomilla/redirects.py +10 -0
- camomilla/serializers/base/__init__.py +4 -4
- camomilla/serializers/fields/__init__.py +5 -17
- camomilla/serializers/fields/related.py +5 -3
- camomilla/serializers/mixins/__init__.py +23 -240
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +9 -8
- camomilla/serializers/mixins/json.py +34 -0
- camomilla/serializers/mixins/language.py +32 -0
- camomilla/serializers/mixins/nesting.py +35 -0
- camomilla/serializers/mixins/optimize.py +91 -0
- camomilla/serializers/mixins/ordering.py +34 -0
- camomilla/serializers/mixins/page.py +58 -0
- camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
- camomilla/serializers/utils.py +3 -3
- camomilla/serializers/validators.py +6 -2
- camomilla/settings.py +16 -2
- camomilla/storages/default.py +7 -1
- camomilla/templates/defaults/base.html +60 -4
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/templatetags/menus.py +3 -0
- camomilla/templatetags/model_extras.py +73 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/{admin.py → admin/__init__.py} +22 -20
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +2 -5
- camomilla/translation.py +7 -1
- camomilla/urls.py +2 -5
- camomilla/utils/query_parser.py +42 -23
- camomilla/utils/templates.py +23 -10
- camomilla/utils/translation.py +47 -5
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/medias.py +1 -1
- camomilla/views/mixins/__init__.py +17 -76
- camomilla/views/mixins/bulk_actions.py +22 -0
- camomilla/views/mixins/language.py +33 -0
- camomilla/views/mixins/optimize.py +18 -0
- camomilla/views/mixins/pagination.py +11 -8
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +12 -2
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/METADATA +23 -16
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/RECORD +63 -45
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/WHEEL +1 -1
- tests/test_camomilla_filters.py +1 -1
- tests/test_media.py +98 -65
- tests/test_menu.py +97 -0
- tests/test_model_api_register.py +393 -0
- tests/test_pages.py +343 -0
- tests/test_query_parser.py +1 -2
- tests/test_templates_context.py +111 -0
- tests/utils/api.py +0 -1
- tests/utils/media.py +10 -0
- camomilla/contrib/rest_framework/__init__.py +0 -0
- camomilla/serializers/fields/json.py +0 -48
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info/licenses}/LICENSE +0 -0
- {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
|
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
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
123
|
-
|
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(
|
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(
|
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
|
-
|
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(
|
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
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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])
|
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)
|
camomilla/openapi/schema.py
CHANGED
@@ -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[
|
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[
|
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
|
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(
|
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
@@ -1,14 +1,14 @@
|
|
1
1
|
from rest_framework import serializers
|
2
2
|
|
3
|
-
from
|
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
|
-
|
12
|
-
""
|
13
|
-
|
14
|
-
""
|
15
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
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
|
}
|