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.
Files changed (143) hide show
  1. camomilla/__init__.py +8 -2
  2. camomilla/apps.py +9 -1
  3. camomilla/context_processors.py +6 -0
  4. camomilla/contrib/modeltranslation/__init__.py +0 -0
  5. camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
  6. camomilla/dynamic_pages_urls.py +33 -0
  7. camomilla/fields/__init__.py +13 -0
  8. camomilla/{fields.py → fields/json.py} +15 -18
  9. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  10. camomilla/managers/__init__.py +3 -0
  11. camomilla/managers/pages.py +116 -0
  12. camomilla/model_api.py +86 -0
  13. camomilla/models/__init__.py +5 -6
  14. camomilla/models/article.py +26 -44
  15. camomilla/models/content.py +8 -15
  16. camomilla/models/media.py +70 -97
  17. camomilla/models/menu.py +106 -0
  18. camomilla/models/mixins/__init__.py +10 -48
  19. camomilla/models/page.py +521 -20
  20. camomilla/openapi/__init__.py +0 -0
  21. camomilla/openapi/schema.py +67 -0
  22. camomilla/parsers.py +0 -1
  23. camomilla/redirects.py +10 -0
  24. camomilla/serializers/__init__.py +2 -0
  25. camomilla/serializers/article.py +5 -10
  26. camomilla/serializers/base/__init__.py +21 -17
  27. camomilla/serializers/content_type.py +17 -0
  28. camomilla/serializers/fields/__init__.py +6 -20
  29. camomilla/serializers/fields/file.py +5 -0
  30. camomilla/serializers/fields/related.py +24 -4
  31. camomilla/serializers/media.py +6 -8
  32. camomilla/serializers/menu.py +17 -0
  33. camomilla/serializers/mixins/__init__.py +23 -187
  34. camomilla/serializers/mixins/fields.py +20 -0
  35. camomilla/serializers/mixins/filter_fields.py +57 -0
  36. camomilla/serializers/mixins/json.py +34 -0
  37. camomilla/serializers/mixins/language.py +32 -0
  38. camomilla/serializers/mixins/nesting.py +35 -0
  39. camomilla/serializers/mixins/optimize.py +91 -0
  40. camomilla/serializers/mixins/ordering.py +34 -0
  41. camomilla/serializers/mixins/page.py +58 -0
  42. camomilla/serializers/mixins/translation.py +103 -0
  43. camomilla/serializers/page.py +53 -4
  44. camomilla/serializers/user.py +5 -4
  45. camomilla/serializers/utils.py +38 -0
  46. camomilla/serializers/validators.py +51 -0
  47. camomilla/settings.py +118 -0
  48. camomilla/sitemap.py +30 -0
  49. camomilla/storages/__init__.py +4 -0
  50. camomilla/storages/default.py +12 -0
  51. camomilla/storages/optimize.py +71 -0
  52. camomilla/{storages.py → storages/overwrite.py} +2 -2
  53. camomilla/templates/admin/camomilla/page/change_form.html +10 -0
  54. camomilla/templates/defaults/articles/default.html +7 -0
  55. camomilla/templates/defaults/base.html +170 -0
  56. camomilla/templates/defaults/pages/default.html +3 -0
  57. camomilla/templates/defaults/parts/langswitch.html +83 -0
  58. camomilla/templates/defaults/parts/menu.html +15 -0
  59. camomilla/templates_context/__init__.py +0 -0
  60. camomilla/templates_context/autodiscover.py +51 -0
  61. camomilla/templates_context/rendering.py +89 -0
  62. camomilla/templatetags/camomilla_filters.py +6 -5
  63. camomilla/templatetags/menus.py +37 -0
  64. camomilla/templatetags/model_extras.py +77 -0
  65. camomilla/theme/__init__.py +1 -1
  66. camomilla/theme/admin/__init__.py +99 -0
  67. camomilla/theme/admin/pages.py +46 -0
  68. camomilla/theme/admin/translations.py +13 -0
  69. camomilla/theme/apps.py +38 -0
  70. camomilla/theme/static/admin/css/responsive.css +5 -1021
  71. camomilla/theme/static/admin/img/favicon.ico +0 -0
  72. camomilla/theme/static/admin/img/logo.svg +31 -0
  73. camomilla/theme/templates/admin/base.html +7 -0
  74. camomilla/theme/templates/rosetta/base.html +196 -0
  75. camomilla/translation.py +61 -0
  76. camomilla/urls.py +38 -17
  77. camomilla/utils/__init__.py +4 -0
  78. camomilla/utils/getters.py +27 -0
  79. camomilla/utils/normalization.py +7 -0
  80. camomilla/utils/query_parser.py +167 -0
  81. camomilla/{utils.py → utils/seo.py} +13 -15
  82. camomilla/utils/setters.py +37 -0
  83. camomilla/utils/templates.py +32 -0
  84. camomilla/utils/translation.py +114 -0
  85. camomilla/views/__init__.py +1 -1
  86. camomilla/views/articles.py +5 -7
  87. camomilla/views/base/__init__.py +35 -5
  88. camomilla/views/contents.py +6 -11
  89. camomilla/views/decorators.py +26 -0
  90. camomilla/views/medias.py +24 -19
  91. camomilla/views/menus.py +81 -0
  92. camomilla/views/mixins/__init__.py +17 -73
  93. camomilla/views/mixins/bulk_actions.py +22 -0
  94. camomilla/views/mixins/language.py +33 -0
  95. camomilla/views/mixins/optimize.py +18 -0
  96. camomilla/views/mixins/ordering.py +2 -2
  97. camomilla/views/mixins/pagination.py +12 -18
  98. camomilla/views/mixins/permissions.py +6 -0
  99. camomilla/views/pages.py +28 -6
  100. camomilla/views/tags.py +5 -6
  101. camomilla/views/users.py +7 -12
  102. django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
  103. django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
  104. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
  105. tests/fixtures/__init__.py +14 -0
  106. tests/test_api.py +22 -39
  107. tests/test_camomilla_filters.py +11 -13
  108. tests/test_media.py +152 -0
  109. tests/test_menu.py +112 -0
  110. tests/test_model_api.py +113 -0
  111. tests/test_model_api_permissions.py +44 -0
  112. tests/test_model_api_register.py +355 -0
  113. tests/test_pages.py +351 -0
  114. tests/test_query_parser.py +58 -0
  115. tests/test_templates_context.py +149 -0
  116. tests/test_utils.py +64 -64
  117. tests/utils/__init__.py +0 -0
  118. tests/utils/api.py +28 -0
  119. tests/utils/media.py +10 -0
  120. camomilla/admin.py +0 -98
  121. camomilla/migrations/0001_initial.py +0 -577
  122. camomilla/migrations/0002_auto_20200214_1127.py +0 -33
  123. camomilla/migrations/0003_auto_20210130_1610.py +0 -30
  124. camomilla/migrations/0004_auto_20210511_0937.py +0 -25
  125. camomilla/migrations/0005_media_image_props.py +0 -19
  126. camomilla/migrations/0006_auto_20220103_1845.py +0 -35
  127. camomilla/migrations/0007_auto_20220211_1622.py +0 -18
  128. camomilla/migrations/0008_auto_20220309_1616.py +0 -60
  129. camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
  130. camomilla/migrations/0010_auto_20220802_1406.py +0 -83
  131. camomilla/migrations/0011_auto_20220902_1000.py +0 -15
  132. camomilla/models/category.py +0 -25
  133. camomilla/models/tag.py +0 -19
  134. camomilla/theme/static/admin/img/logo.png +0 -0
  135. camomilla/theme/templates/admin/base_site.html +0 -18
  136. camomilla/views/categories.py +0 -13
  137. django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
  138. django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
  139. tests/urls.py +0 -21
  140. /camomilla/{migrations → contrib}/__init__.py +0 -0
  141. /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
  142. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
  143. {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 django.db import models
2
- from hvad.models import TranslatedFields
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
- class BasePage(SeoMixin, MetaMixin):
8
- identifier = models.CharField(max_length=200, unique=True)
9
- translations = TranslatedFields()
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
- verbose_name = "Page"
15
- verbose_name_plural = "Pages"
16
- ordering = ["ordering"]
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
- def alternate_urls(self, request):
23
- from djlotrek.context_processors import alternate_seo_url
497
+ __url_node_history__ = {}
24
498
 
25
- return alternate_seo_url(request)
26
499
 
27
- def __str__(self):
28
- return self.identifier
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
- class Page(BasePage):
32
- translations = TranslatedFields()
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
@@ -38,7 +38,6 @@ def compile_payload(data, path, value):
38
38
 
39
39
 
40
40
  class MultipartJsonParser(parsers.BaseParser):
41
-
42
41
  media_type = "multipart/form-data"
43
42
 
44
43
  def parse(self, stream, media_type=None, parser_context=None):
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
+ ]
@@ -4,3 +4,5 @@ from .page import *
4
4
  from .user import *
5
5
  from .fields import *
6
6
  from .mixins import *
7
+ from .menu import *
8
+ from .content_type import *
@@ -1,20 +1,15 @@
1
- from ..models import Article, Category, Tag
2
- from .base import BaseTranslatableModelSerializer
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(BaseTranslatableModelSerializer):
6
+ class TagSerializer(BaseModelSerializer):
6
7
  class Meta:
7
8
  model = Tag
8
9
  fields = "__all__"
9
10
 
10
11
 
11
- class CategorySerializer(BaseTranslatableModelSerializer):
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__"