django-camomilla-cms 5.8.5__py2.py3-none-any.whl → 6.0.0__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 +8 -2
- camomilla/apps.py +9 -1
- camomilla/context_processors.py +6 -0
- camomilla/contrib/modeltranslation/__init__.py +0 -0
- camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
- camomilla/dynamic_pages_urls.py +33 -0
- camomilla/fields/__init__.py +13 -0
- camomilla/{fields.py → fields/json.py} +15 -18
- camomilla/management/commands/regenerate_thumbnails.py +0 -1
- camomilla/managers/__init__.py +3 -0
- camomilla/managers/pages.py +116 -0
- camomilla/model_api.py +86 -0
- camomilla/models/__init__.py +5 -6
- camomilla/models/article.py +26 -44
- camomilla/models/content.py +8 -15
- camomilla/models/media.py +70 -97
- camomilla/models/menu.py +106 -0
- camomilla/models/mixins/__init__.py +10 -48
- camomilla/models/page.py +521 -20
- camomilla/openapi/__init__.py +0 -0
- camomilla/openapi/schema.py +67 -0
- camomilla/parsers.py +0 -1
- camomilla/redirects.py +10 -0
- camomilla/serializers/__init__.py +2 -0
- camomilla/serializers/article.py +5 -10
- camomilla/serializers/base/__init__.py +21 -17
- camomilla/serializers/content_type.py +17 -0
- camomilla/serializers/fields/__init__.py +6 -20
- camomilla/serializers/fields/file.py +5 -0
- camomilla/serializers/fields/related.py +24 -4
- camomilla/serializers/media.py +6 -8
- camomilla/serializers/menu.py +17 -0
- camomilla/serializers/mixins/__init__.py +23 -187
- 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/serializers/mixins/translation.py +103 -0
- camomilla/serializers/page.py +53 -4
- camomilla/serializers/user.py +5 -4
- camomilla/serializers/utils.py +38 -0
- camomilla/serializers/validators.py +51 -0
- camomilla/settings.py +118 -0
- camomilla/sitemap.py +30 -0
- camomilla/storages/__init__.py +4 -0
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +71 -0
- camomilla/{storages.py → storages/overwrite.py} +2 -2
- camomilla/templates/admin/camomilla/page/change_form.html +10 -0
- camomilla/templates/defaults/articles/default.html +7 -0
- camomilla/templates/defaults/base.html +170 -0
- camomilla/templates/defaults/pages/default.html +3 -0
- camomilla/templates/defaults/parts/langswitch.html +83 -0
- camomilla/templates/defaults/parts/menu.html +15 -0
- camomilla/templates_context/__init__.py +0 -0
- camomilla/templates_context/autodiscover.py +51 -0
- camomilla/templates_context/rendering.py +89 -0
- camomilla/templatetags/camomilla_filters.py +6 -5
- camomilla/templatetags/menus.py +37 -0
- camomilla/templatetags/model_extras.py +77 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin/__init__.py +99 -0
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +38 -0
- camomilla/theme/static/admin/css/responsive.css +5 -1021
- camomilla/theme/static/admin/img/favicon.ico +0 -0
- camomilla/theme/static/admin/img/logo.svg +31 -0
- camomilla/theme/templates/admin/base.html +7 -0
- camomilla/theme/templates/rosetta/base.html +196 -0
- camomilla/translation.py +61 -0
- camomilla/urls.py +38 -17
- camomilla/utils/__init__.py +4 -0
- camomilla/utils/getters.py +27 -0
- camomilla/utils/normalization.py +7 -0
- camomilla/utils/query_parser.py +167 -0
- camomilla/{utils.py → utils/seo.py} +13 -15
- camomilla/utils/setters.py +37 -0
- camomilla/utils/templates.py +32 -0
- camomilla/utils/translation.py +114 -0
- camomilla/views/__init__.py +1 -1
- camomilla/views/articles.py +5 -7
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/contents.py +6 -11
- camomilla/views/decorators.py +26 -0
- camomilla/views/medias.py +24 -19
- camomilla/views/menus.py +81 -0
- camomilla/views/mixins/__init__.py +17 -73
- 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/ordering.py +2 -2
- camomilla/views/mixins/pagination.py +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +28 -6
- camomilla/views/tags.py +5 -6
- camomilla/views/users.py +7 -12
- django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
- django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +14 -0
- tests/test_api.py +22 -39
- tests/test_camomilla_filters.py +11 -13
- tests/test_media.py +152 -0
- tests/test_menu.py +112 -0
- tests/test_model_api.py +113 -0
- tests/test_model_api_permissions.py +44 -0
- tests/test_model_api_register.py +355 -0
- tests/test_pages.py +351 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +149 -0
- tests/test_utils.py +64 -64
- tests/utils/__init__.py +0 -0
- tests/utils/api.py +28 -0
- tests/utils/media.py +10 -0
- camomilla/admin.py +0 -98
- camomilla/migrations/0001_initial.py +0 -577
- camomilla/migrations/0002_auto_20200214_1127.py +0 -33
- camomilla/migrations/0003_auto_20210130_1610.py +0 -30
- camomilla/migrations/0004_auto_20210511_0937.py +0 -25
- camomilla/migrations/0005_media_image_props.py +0 -19
- camomilla/migrations/0006_auto_20220103_1845.py +0 -35
- camomilla/migrations/0007_auto_20220211_1622.py +0 -18
- camomilla/migrations/0008_auto_20220309_1616.py +0 -60
- camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
- camomilla/migrations/0010_auto_20220802_1406.py +0 -83
- camomilla/migrations/0011_auto_20220902_1000.py +0 -15
- camomilla/models/category.py +0 -25
- camomilla/models/tag.py +0 -19
- camomilla/theme/static/admin/img/logo.png +0 -0
- camomilla/theme/templates/admin/base_site.html +0 -18
- camomilla/views/categories.py +0 -13
- django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
- django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
- tests/urls.py +0 -21
- /camomilla/{migrations → contrib}/__init__.py +0 -0
- /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
camomilla/models/page.py
CHANGED
@@ -1,32 +1,533 @@
|
|
1
|
-
from
|
2
|
-
from
|
3
|
-
from .mixins import SeoMixin, MetaMixin
|
4
|
-
from ..utils import get_page
|
1
|
+
from typing import Sequence, Tuple, Optional, Union
|
2
|
+
from uuid import uuid4
|
5
3
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
from django.db import models, transaction
|
7
|
+
from django.db.models.signals import post_delete, post_save, pre_save
|
8
|
+
from django.dispatch import receiver
|
9
|
+
from django.http import Http404, HttpRequest
|
10
|
+
from django.shortcuts import redirect
|
11
|
+
from django.urls import NoReverseMatch, reverse
|
12
|
+
from django.utils import timezone
|
13
|
+
from django.utils.functional import lazy
|
14
|
+
from django.utils.text import slugify
|
15
|
+
from django.utils.translation import gettext_lazy as _
|
16
|
+
from django.utils.translation import get_language
|
17
|
+
|
18
|
+
from camomilla.managers.pages import PageQuerySet, UrlNodeManager
|
19
|
+
from camomilla.models.mixins import MetaMixin, SeoMixin
|
20
|
+
from camomilla.utils import (
|
21
|
+
activate_languages,
|
22
|
+
get_field_translations,
|
23
|
+
get_nofallbacks,
|
24
|
+
lang_fallback_query,
|
25
|
+
set_nofallbacks,
|
26
|
+
url_lang_decompose,
|
27
|
+
get_all_templates_files,
|
28
|
+
)
|
29
|
+
from camomilla.utils.getters import pointed_getter
|
30
|
+
from camomilla import settings
|
31
|
+
from camomilla.templates_context.rendering import ctx_registry
|
32
|
+
from django.conf import settings as django_settings
|
33
|
+
from modeltranslation.utils import build_localized_fieldname
|
34
|
+
|
35
|
+
|
36
|
+
def GET_TEMPLATE_CHOICES():
|
37
|
+
return [(t, t) for t in get_all_templates_files()]
|
38
|
+
|
39
|
+
|
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()
|
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()
|
77
|
+
|
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
|
+
]
|
100
|
+
|
101
|
+
|
102
|
+
class UrlNode(models.Model):
|
103
|
+
|
104
|
+
LANG_PERMALINK_FIELDS = (
|
105
|
+
[
|
106
|
+
build_localized_fieldname("permalink", lang)
|
107
|
+
for lang in settings.AVAILABLE_LANGUAGES
|
108
|
+
]
|
109
|
+
if settings.ENABLE_TRANSLATIONS
|
110
|
+
else ["permalink"]
|
111
|
+
)
|
112
|
+
|
113
|
+
permalink = models.CharField(max_length=400, unique=True, null=True)
|
114
|
+
related_name = models.CharField(max_length=200)
|
115
|
+
objects = UrlNodeManager()
|
116
|
+
|
117
|
+
@property
|
118
|
+
def page(self) -> "AbstractPage":
|
119
|
+
return getattr(self, self.related_name)
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def reverse_url(permalink: str, request: Optional[HttpRequest] = None) -> str:
|
123
|
+
append_slash = getattr(django_settings, "APPEND_SLASH", True)
|
124
|
+
try:
|
125
|
+
if permalink == "/":
|
126
|
+
return reverse("camomilla-homepage")
|
127
|
+
url = reverse("camomilla-permalink", args=(permalink.lstrip("/"),))
|
128
|
+
if append_slash and not url.endswith("/"):
|
129
|
+
url += "/"
|
130
|
+
if request:
|
131
|
+
url = request.build_absolute_uri(url)
|
132
|
+
return url
|
133
|
+
except NoReverseMatch:
|
134
|
+
return None
|
135
|
+
|
136
|
+
@property
|
137
|
+
def routerlink(self) -> str:
|
138
|
+
return self.reverse_url(self.permalink) or self.permalink
|
139
|
+
|
140
|
+
def get_absolute_url(self) -> str:
|
141
|
+
if self.routerlink == "/":
|
142
|
+
return ""
|
143
|
+
return self.routerlink
|
144
|
+
|
145
|
+
@staticmethod
|
146
|
+
def sanitize_permalink(permalink):
|
147
|
+
if isinstance(permalink, str):
|
148
|
+
p_parts = permalink.split("/")
|
149
|
+
permalink = "/".join(
|
150
|
+
[slugify(p, allow_unicode=True).strip() for p in p_parts]
|
151
|
+
)
|
152
|
+
if not permalink.startswith("/"):
|
153
|
+
permalink = f"/{permalink}"
|
154
|
+
return permalink
|
155
|
+
|
156
|
+
def save(self, *args, **kwargs) -> None:
|
157
|
+
for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
|
158
|
+
setattr(
|
159
|
+
self,
|
160
|
+
lang_p_field,
|
161
|
+
UrlNode.sanitize_permalink(getattr(self, lang_p_field)),
|
162
|
+
)
|
163
|
+
super().save(*args, **kwargs)
|
164
|
+
|
165
|
+
def __str__(self) -> str:
|
166
|
+
return self.permalink
|
167
|
+
|
168
|
+
|
169
|
+
PAGE_CHILD_RELATED_NAME = "%(app_label)s_%(class)s_child_pages"
|
170
|
+
URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
|
171
|
+
|
172
|
+
PAGE_STATUS = (
|
173
|
+
("PUB", _("Published")),
|
174
|
+
("DRF", _("Draft")),
|
175
|
+
("TRS", _("Trash")),
|
176
|
+
("PLA", _("Planned")),
|
177
|
+
)
|
178
|
+
|
179
|
+
|
180
|
+
class PageBase(models.base.ModelBase):
|
181
|
+
"""
|
182
|
+
This models comes to implement a language based permalink logic
|
183
|
+
"""
|
184
|
+
|
185
|
+
def perm_prop_factory(permalink_field):
|
186
|
+
def getter(_self):
|
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
|
+
setattr(_self, f"__{permalink_field}", value)
|
195
|
+
|
196
|
+
return getter, setter
|
197
|
+
|
198
|
+
def __new__(cls, name, bases, attrs, **kwargs):
|
199
|
+
attr_meta = attrs.pop("PageMeta", None)
|
200
|
+
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
201
|
+
page_meta = attr_meta or getattr(new_class, "PageMeta", None)
|
202
|
+
base_page_meta = getattr(new_class, "_page_meta", None)
|
203
|
+
for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
|
204
|
+
computed_prop = property(*cls.perm_prop_factory(lang_p_field))
|
205
|
+
setattr(new_class, lang_p_field, computed_prop)
|
206
|
+
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
|
+
)
|
223
|
+
if page_meta:
|
224
|
+
for name, value in getattr(base_page_meta, "__dict__", {}).items():
|
225
|
+
if name not in page_meta.__dict__:
|
226
|
+
setattr(page_meta, name, value)
|
227
|
+
setattr(new_class, "_page_meta", page_meta)
|
228
|
+
return new_class
|
229
|
+
|
230
|
+
|
231
|
+
class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
232
|
+
identifier = models.CharField(max_length=200, null=True, unique=True, default=uuid4)
|
233
|
+
date_created = models.DateTimeField(auto_now_add=True)
|
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)
|
10
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
|
+
)
|
246
|
+
url_node = models.OneToOneField(
|
247
|
+
UrlNode,
|
248
|
+
on_delete=models.CASCADE,
|
249
|
+
related_name=URL_NODE_RELATED_NAME,
|
250
|
+
null=True,
|
251
|
+
editable=False,
|
252
|
+
)
|
253
|
+
publication_date = models.DateTimeField(null=True, blank=True)
|
254
|
+
status = models.CharField(
|
255
|
+
max_length=3,
|
256
|
+
choices=PAGE_STATUS,
|
257
|
+
default="DRF",
|
258
|
+
)
|
259
|
+
indexable = models.BooleanField(default=True)
|
260
|
+
autopermalink = models.BooleanField(default=True)
|
261
|
+
|
262
|
+
objects = PageQuerySet.as_manager()
|
263
|
+
|
264
|
+
__cached_db_instance: "AbstractPage" = None
|
265
|
+
|
266
|
+
@property
|
267
|
+
def db_instance(self):
|
268
|
+
if self.__cached_db_instance is None:
|
269
|
+
self.__cached_db_instance = self.get_db_instance()
|
270
|
+
return self.__cached_db_instance
|
271
|
+
|
272
|
+
def get_db_instance(self):
|
273
|
+
if self.pk:
|
274
|
+
return self.__class__.objects.get(pk=self.pk)
|
275
|
+
return None
|
276
|
+
|
277
|
+
def __init__(self, *args, **kwargs):
|
278
|
+
super(AbstractPage, self).__init__(*args, **kwargs)
|
279
|
+
self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
|
280
|
+
|
281
|
+
def __str__(self) -> str:
|
282
|
+
return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
|
283
|
+
|
284
|
+
def get_context(self, request: Optional[HttpRequest] = None):
|
285
|
+
context = {
|
286
|
+
"page": self,
|
287
|
+
"page_model": {"class": self.__class__.__name__, "module": self.__module__},
|
288
|
+
"request": request,
|
289
|
+
}
|
290
|
+
inject_func = pointed_getter(self, "_page_meta.inject_context_func")
|
291
|
+
if inject_func and callable(inject_func):
|
292
|
+
new_ctx = inject_func(request=request, super_ctx=context)
|
293
|
+
if isinstance(new_ctx, dict):
|
294
|
+
context.update(new_ctx)
|
295
|
+
return ctx_registry.get_context_for_page(self, request, super_ctx=context)
|
296
|
+
|
297
|
+
@property
|
298
|
+
def model_name(self) -> str:
|
299
|
+
return self._meta.app_label + "." + self._meta.model_name
|
300
|
+
|
301
|
+
@property
|
302
|
+
def model_info(self) -> dict:
|
303
|
+
return {"app_label": self._meta.app_label, "class": self._meta.model_name}
|
304
|
+
|
305
|
+
@property
|
306
|
+
def routerlink(self) -> str:
|
307
|
+
return self.url_node and self.url_node.routerlink
|
308
|
+
|
309
|
+
@property
|
310
|
+
def breadcrumbs(self) -> Sequence[dict]:
|
311
|
+
breadcrumb = {
|
312
|
+
"permalink": self.permalink,
|
313
|
+
"title": self.breadcrumbs_title or self.title or "",
|
314
|
+
}
|
315
|
+
if self.parent:
|
316
|
+
return self.parent.breadcrumbs + [breadcrumb]
|
317
|
+
return [breadcrumb]
|
318
|
+
|
319
|
+
@property
|
320
|
+
def is_public(self) -> bool:
|
321
|
+
status = get_nofallbacks(self, "status")
|
322
|
+
publication_date = get_nofallbacks(self, "publication_date")
|
323
|
+
if status == "PUB":
|
324
|
+
return True
|
325
|
+
if status == "PLA":
|
326
|
+
return bool(publication_date) and timezone.now() > publication_date
|
327
|
+
return False
|
328
|
+
|
329
|
+
def get_template_path(self, request: Optional[HttpRequest] = None) -> str:
|
330
|
+
return self.template or pointed_getter(self, "_page_meta.default_template")
|
331
|
+
|
332
|
+
@property
|
333
|
+
def childs(self) -> models.Manager:
|
334
|
+
if hasattr(self._page_meta, "child_page_field"):
|
335
|
+
return getattr(self, self._page_meta.child_page_field)
|
336
|
+
return getattr(self, PAGE_CHILD_RELATED_NAME % self.model_info)
|
337
|
+
|
338
|
+
@property
|
339
|
+
def parent(self) -> models.Model:
|
340
|
+
return getattr(self, self._page_meta.parent_page_field)
|
341
|
+
|
342
|
+
def _get_or_create_url_node(self) -> UrlNode:
|
343
|
+
if not self.url_node:
|
344
|
+
self.url_node = UrlNode.objects.create(
|
345
|
+
related_name=URL_NODE_RELATED_NAME % self.model_info
|
346
|
+
)
|
347
|
+
return self.url_node
|
348
|
+
|
349
|
+
def _update_url_node(self, force: bool = False) -> UrlNode:
|
350
|
+
self.url_node = self._get_or_create_url_node()
|
351
|
+
for __ in activate_languages():
|
352
|
+
old_permalink = self.db_instance and self.db_instance.permalink
|
353
|
+
new_permalink = self.permalink
|
354
|
+
if self.autopermalink:
|
355
|
+
new_permalink = self.generate_permalink()
|
356
|
+
force = force or old_permalink != new_permalink
|
357
|
+
set_nofallbacks(self.url_node, "permalink", new_permalink)
|
358
|
+
if force:
|
359
|
+
self.url_node.save()
|
360
|
+
self.update_childs()
|
361
|
+
return self.url_node
|
362
|
+
|
363
|
+
def generate_permalink(self, safe: bool = True) -> str:
|
364
|
+
permalink = f"/{slugify(self.title or '', allow_unicode=True)}"
|
365
|
+
if self.parent:
|
366
|
+
permalink = f"/{self.parent.permalink}{permalink}"
|
367
|
+
set_nofallbacks(self, "permalink", permalink)
|
368
|
+
qs = UrlNode.objects.exclude(pk=getattr(self.url_node or object, "pk", None))
|
369
|
+
if safe and qs.filter(permalink=permalink).exists():
|
370
|
+
permalink = "/".join(
|
371
|
+
permalink.split("/")[:-1] + [slugify(uuid4(), allow_unicode=True)]
|
372
|
+
)
|
373
|
+
return permalink
|
374
|
+
|
375
|
+
def update_childs(self) -> None:
|
376
|
+
# without pk, no childs there
|
377
|
+
if self.pk is not None:
|
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):
|
382
|
+
child.save()
|
383
|
+
|
384
|
+
def save(self, *args, **kwargs) -> None:
|
385
|
+
with transaction.atomic():
|
386
|
+
self._update_url_node()
|
387
|
+
super().save(*args, **kwargs)
|
388
|
+
self.__cached_db_instance = None
|
389
|
+
for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
|
390
|
+
hasattr(self, f"__{lang_p_field}") and delattr(
|
391
|
+
self, f"__{lang_p_field}"
|
392
|
+
)
|
393
|
+
|
394
|
+
@classmethod
|
395
|
+
def get(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
|
396
|
+
bypass_type_check = kwargs.pop("bypass_type_check", False)
|
397
|
+
bypass_public_check = kwargs.pop("bypass_public_check", False)
|
398
|
+
if len(kwargs.keys()) > 0:
|
399
|
+
page = cls.objects.get(**kwargs)
|
400
|
+
else:
|
401
|
+
if not request:
|
402
|
+
raise ValueError("request is required if no kwargs are passed")
|
403
|
+
path = request.path
|
404
|
+
if getattr(django_settings, "APPEND_SLASH", True):
|
405
|
+
path = path.rstrip("/")
|
406
|
+
node = UrlNode.objects.filter(
|
407
|
+
permalink=url_lang_decompose(path)["permalink"]
|
408
|
+
).first()
|
409
|
+
page = node and node.page
|
410
|
+
type_error = not bypass_type_check and not isinstance(page, cls)
|
411
|
+
public_error = not bypass_public_check and not getattr(
|
412
|
+
page or object, "is_public", False
|
413
|
+
)
|
414
|
+
if not page or type_error or public_error:
|
415
|
+
bases = (UrlNode.DoesNotExist,)
|
416
|
+
if hasattr(cls, "DoesNotExist"):
|
417
|
+
bases += (cls.DoesNotExist,)
|
418
|
+
message = "%s matching query does not exist." % cls._meta.object_name
|
419
|
+
if public_error:
|
420
|
+
message = (
|
421
|
+
"Match found: %s.\nThe page appears not to be public.\nUse ?preview=true in the url to see it."
|
422
|
+
% page
|
423
|
+
)
|
424
|
+
raise type("PageDoesNotExist", bases, {})(message)
|
425
|
+
return page
|
426
|
+
|
427
|
+
@classmethod
|
428
|
+
def get_or_create(
|
429
|
+
cls, request: HttpRequest, *args, **kwargs
|
430
|
+
) -> Tuple["AbstractPage", bool]:
|
431
|
+
try:
|
432
|
+
return cls.get(request, *args, **kwargs), False
|
433
|
+
except ObjectDoesNotExist:
|
434
|
+
if len(kwargs.keys()) > 0:
|
435
|
+
return cls.objects.get_or_create(**kwargs)
|
436
|
+
return (None, False)
|
437
|
+
|
438
|
+
@classmethod
|
439
|
+
def get_or_create_homepage(cls) -> Tuple["AbstractPage", bool]:
|
440
|
+
try:
|
441
|
+
if settings.ENABLE_TRANSLATIONS:
|
442
|
+
node = UrlNode.objects.get(lang_fallback_query(permalink="/"))
|
443
|
+
else:
|
444
|
+
node = UrlNode.objects.get(permalink="/")
|
445
|
+
return node.page, False
|
446
|
+
except UrlNode.DoesNotExist:
|
447
|
+
return cls.get_or_create(None, permalink="/")
|
448
|
+
|
449
|
+
@classmethod
|
450
|
+
def get_or_404(cls, request: HttpRequest, *args, **kwargs) -> "AbstractPage":
|
451
|
+
try:
|
452
|
+
return cls.get(request, *args, **kwargs)
|
453
|
+
except ObjectDoesNotExist as ex:
|
454
|
+
raise Http404(ex)
|
455
|
+
|
456
|
+
def alternate_urls(self, *args, **kwargs) -> dict:
|
457
|
+
request: Union[HttpRequest, bool] = False
|
458
|
+
if len(args) > 0:
|
459
|
+
request = args[0]
|
460
|
+
if "request" in kwargs:
|
461
|
+
request = kwargs["request"]
|
462
|
+
preview = request and getattr(request, "GET", {}).get("preview", False)
|
463
|
+
permalinks = get_field_translations(self.url_node or object, "permalink", None)
|
464
|
+
for lang in activate_languages():
|
465
|
+
if lang in permalinks:
|
466
|
+
permalinks[lang] = (
|
467
|
+
UrlNode.reverse_url(permalinks[lang])
|
468
|
+
if preview or self.is_public
|
469
|
+
else None
|
470
|
+
)
|
471
|
+
if preview:
|
472
|
+
permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
|
473
|
+
return permalinks
|
11
474
|
|
12
475
|
class Meta:
|
13
476
|
abstract = True
|
14
|
-
|
15
|
-
|
16
|
-
|
477
|
+
ordering = ("ordering",)
|
478
|
+
verbose_name = _("Page")
|
479
|
+
verbose_name_plural = _("Pages")
|
480
|
+
|
481
|
+
class PageMeta:
|
482
|
+
parent_page_field = "parent_page"
|
483
|
+
default_template = settings.PAGE_DEFAULT_TEMPLATE
|
484
|
+
inject_context_func = settings.PAGE_INJECT_CONTEXT_FUNC
|
485
|
+
|
486
|
+
|
487
|
+
class Page(AbstractPage):
|
488
|
+
pass
|
489
|
+
|
490
|
+
|
491
|
+
@receiver(post_delete)
|
492
|
+
def auto_delete_url_node(sender, instance, **kwargs):
|
493
|
+
if issubclass(sender, AbstractPage):
|
494
|
+
instance.url_node and instance.url_node.delete()
|
17
495
|
|
18
|
-
@classmethod
|
19
|
-
def get(model, request, **kwargs):
|
20
|
-
return get_page(request, **kwargs)
|
21
496
|
|
22
|
-
|
23
|
-
from djlotrek.context_processors import alternate_seo_url
|
497
|
+
__url_node_history__ = {}
|
24
498
|
|
25
|
-
return alternate_seo_url(request)
|
26
499
|
|
27
|
-
|
28
|
-
|
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()
|
29
506
|
|
30
507
|
|
31
|
-
|
32
|
-
|
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)
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from rest_framework.schemas.openapi import (
|
2
|
+
SchemaGenerator as DRFSchemaGenerator,
|
3
|
+
AutoSchema as DRFAutoSchema,
|
4
|
+
)
|
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
|
10
|
+
|
11
|
+
|
12
|
+
class AutoSchema(DRFAutoSchema):
|
13
|
+
extra_components = {}
|
14
|
+
|
15
|
+
def map_serializer(self, serializer):
|
16
|
+
schema = super(AutoSchema, self).map_serializer(serializer)
|
17
|
+
if isinstance(serializer, TranslationsMixin) and serializer.is_translatable:
|
18
|
+
schema = plain_to_nest(schema["properties"], serializer.translation_fields)
|
19
|
+
schema[API_TRANSLATION_ACCESSOR] = {
|
20
|
+
"type": "object",
|
21
|
+
"properties": {
|
22
|
+
k: {"type": "object", "properties": v}
|
23
|
+
for k, v in schema[API_TRANSLATION_ACCESSOR].items()
|
24
|
+
},
|
25
|
+
}
|
26
|
+
return schema
|
27
|
+
|
28
|
+
def get_components(self, path, method):
|
29
|
+
components = super().get_components(path, method)
|
30
|
+
if len(self.extra_components.keys()) > 0:
|
31
|
+
components = {**(components or {}), **self.extra_components}
|
32
|
+
return components
|
33
|
+
|
34
|
+
def map_field(self, field):
|
35
|
+
if isinstance(field, StructuredJSONField):
|
36
|
+
|
37
|
+
def replace(key, value):
|
38
|
+
if isinstance(value, str) and value.startswith("#/definitions"):
|
39
|
+
return value.replace("#/definitions", "#/components/schemas")
|
40
|
+
return value
|
41
|
+
|
42
|
+
self.extra_components.update(
|
43
|
+
**find_and_replace_dict(
|
44
|
+
field.json_schema.pop("definitions", {}), replace
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
return find_and_replace_dict(field.json_schema, replace)
|
49
|
+
return super().map_field(field)
|
50
|
+
|
51
|
+
|
52
|
+
class SchemaGenerator(DRFSchemaGenerator):
|
53
|
+
def create_view(self, callback, method, request=None):
|
54
|
+
view = super(SchemaGenerator, self).create_view(callback, method, request)
|
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
|
+
)
|
67
|
+
return view
|
camomilla/parsers.py
CHANGED
camomilla/redirects.py
ADDED
camomilla/serializers/article.py
CHANGED
@@ -1,20 +1,15 @@
|
|
1
|
-
from
|
2
|
-
from .base import
|
1
|
+
from camomilla.models import Article, Tag
|
2
|
+
from camomilla.serializers.base import BaseModelSerializer
|
3
|
+
from camomilla.serializers.mixins import AbstractPageMixin
|
3
4
|
|
4
5
|
|
5
|
-
class TagSerializer(
|
6
|
+
class TagSerializer(BaseModelSerializer):
|
6
7
|
class Meta:
|
7
8
|
model = Tag
|
8
9
|
fields = "__all__"
|
9
10
|
|
10
11
|
|
11
|
-
class
|
12
|
-
class Meta:
|
13
|
-
model = Category
|
14
|
-
fields = "__all__"
|
15
|
-
|
16
|
-
|
17
|
-
class ArticleSerializer(BaseTranslatableModelSerializer):
|
12
|
+
class ArticleSerializer(AbstractPageMixin, BaseModelSerializer):
|
18
13
|
class Meta:
|
19
14
|
model = Article
|
20
15
|
fields = "__all__"
|