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.
- camomilla/__init__.py +1 -1
- camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
- camomilla/dynamic_pages_urls.py +6 -2
- camomilla/managers/pages.py +93 -8
- camomilla/model_api.py +14 -7
- camomilla/models/media.py +1 -1
- camomilla/models/menu.py +10 -4
- camomilla/models/page.py +189 -127
- camomilla/openapi/schema.py +17 -8
- camomilla/redirects.py +10 -0
- camomilla/serializers/base/__init__.py +6 -4
- camomilla/serializers/fields/__init__.py +5 -17
- camomilla/serializers/fields/related.py +10 -4
- camomilla/serializers/mixins/__init__.py +23 -195
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +57 -0
- 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 +5 -3
- camomilla/serializers/validators.py +6 -2
- camomilla/settings.py +10 -2
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +2 -2
- camomilla/storages/overwrite.py +2 -2
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/templatetags/menus.py +3 -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 +1 -5
- camomilla/translation.py +7 -1
- camomilla/urls.py +2 -5
- camomilla/utils/query_parser.py +167 -0
- camomilla/utils/translation.py +47 -5
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/medias.py +1 -1
- camomilla/views/menus.py +0 -2
- camomilla/views/mixins/__init__.py +17 -69
- 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 +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +12 -2
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
- django_camomilla_cms-6.0.0b17.dist-info/RECORD +132 -0
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +17 -0
- tests/test_api.py +2 -11
- tests/test_camomilla_filters.py +7 -13
- tests/test_media.py +113 -0
- tests/test_menu.py +97 -0
- tests/test_model_api.py +68 -0
- tests/test_model_api_permissions.py +39 -0
- tests/test_model_api_register.py +393 -0
- tests/test_pages.py +343 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +111 -0
- tests/test_utils.py +64 -64
- tests/utils/api.py +28 -0
- tests/utils/media.py +9 -0
- camomilla/serializers/fields/json.py +0 -49
- django_camomilla_cms-6.0.0b15.dist-info/RECORD +0 -105
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
- {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
|
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
|
44
|
-
|
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
|
-
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
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
122
|
-
|
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(
|
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:
|
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
|
-
|
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(
|
193
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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])
|
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)
|
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,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
@@ -1,21 +1,23 @@
|
|
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
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
|
-
|
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
|
+
]
|
@@ -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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
}
|