django-camomilla-cms 6.0.0b16__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.
Files changed (62) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
  3. camomilla/dynamic_pages_urls.py +6 -2
  4. camomilla/managers/pages.py +87 -2
  5. camomilla/model_api.py +6 -4
  6. camomilla/models/menu.py +9 -4
  7. camomilla/models/page.py +178 -117
  8. camomilla/openapi/schema.py +15 -10
  9. camomilla/redirects.py +10 -0
  10. camomilla/serializers/base/__init__.py +4 -4
  11. camomilla/serializers/fields/__init__.py +5 -17
  12. camomilla/serializers/fields/related.py +5 -3
  13. camomilla/serializers/mixins/__init__.py +23 -240
  14. camomilla/serializers/mixins/fields.py +20 -0
  15. camomilla/serializers/mixins/filter_fields.py +9 -8
  16. camomilla/serializers/mixins/json.py +34 -0
  17. camomilla/serializers/mixins/language.py +32 -0
  18. camomilla/serializers/mixins/nesting.py +35 -0
  19. camomilla/serializers/mixins/optimize.py +91 -0
  20. camomilla/serializers/mixins/ordering.py +34 -0
  21. camomilla/serializers/mixins/page.py +58 -0
  22. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  23. camomilla/serializers/utils.py +3 -3
  24. camomilla/serializers/validators.py +6 -2
  25. camomilla/settings.py +10 -2
  26. camomilla/storages/default.py +7 -1
  27. camomilla/templates/defaults/parts/menu.html +1 -1
  28. camomilla/templatetags/menus.py +3 -0
  29. camomilla/theme/__init__.py +1 -1
  30. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  31. camomilla/theme/admin/pages.py +46 -0
  32. camomilla/theme/admin/translations.py +13 -0
  33. camomilla/theme/apps.py +1 -5
  34. camomilla/translation.py +7 -1
  35. camomilla/urls.py +2 -5
  36. camomilla/utils/query_parser.py +42 -23
  37. camomilla/utils/translation.py +47 -5
  38. camomilla/views/base/__init__.py +35 -5
  39. camomilla/views/medias.py +1 -1
  40. camomilla/views/mixins/__init__.py +17 -76
  41. camomilla/views/mixins/bulk_actions.py +22 -0
  42. camomilla/views/mixins/language.py +33 -0
  43. camomilla/views/mixins/optimize.py +18 -0
  44. camomilla/views/mixins/pagination.py +11 -8
  45. camomilla/views/mixins/permissions.py +6 -0
  46. camomilla/views/pages.py +12 -2
  47. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
  48. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/RECORD +60 -43
  49. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
  50. tests/test_camomilla_filters.py +1 -1
  51. tests/test_media.py +98 -65
  52. tests/test_menu.py +97 -0
  53. tests/test_model_api_register.py +393 -0
  54. tests/test_pages.py +343 -0
  55. tests/test_query_parser.py +1 -2
  56. tests/test_templates_context.py +111 -0
  57. tests/utils/api.py +0 -1
  58. tests/utils/media.py +9 -0
  59. camomilla/contrib/rest_framework/__init__.py +0 -0
  60. camomilla/serializers/fields/json.py +0 -48
  61. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
  62. {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.16"
1
+ __version__ = "6.0.0-beta.17"
2
2
 
3
3
 
4
4
  def get_core_apps():
@@ -56,12 +56,12 @@ class KeepTranslationsMixin:
56
56
  )
57
57
  )
58
58
  rows = cursor.fetchall()
59
- self._saved_data_from_plain[
60
- modelPath
61
- ] = self._saved_data_from_plain.get(modelPath, {})
62
- self._saved_data_from_plain[modelPath][
63
- master[0]
64
- ] = self._saved_data_from_plain[modelPath].get(master[0], [])
59
+ self._saved_data_from_plain[modelPath] = (
60
+ self._saved_data_from_plain.get(modelPath, {})
61
+ )
62
+ self._saved_data_from_plain[modelPath][master[0]] = (
63
+ self._saved_data_from_plain[modelPath].get(master[0], [])
64
+ )
65
65
  for row in rows:
66
66
  self._saved_data_from_plain[modelPath][master[0]].append(
67
67
  dict(zip(fields, row))
@@ -83,9 +83,9 @@ class KeepTranslationsMixin:
83
83
  )
84
84
  )
85
85
  rows = cursor.fetchall()
86
- self._saved_data_from_plain[
87
- modelPath
88
- ] = self._saved_data_from_plain.get(modelPath, [])
86
+ self._saved_data_from_plain[modelPath] = (
87
+ self._saved_data_from_plain.get(modelPath, [])
88
+ )
89
89
  for row in rows:
90
90
  row_data = dict(zip(("master_id", *fields), row))
91
91
  row_data.update({"language_code": lang})
@@ -3,15 +3,19 @@ from django.urls import path
3
3
 
4
4
  from camomilla import settings
5
5
  from django.conf import settings as django_settings
6
- from .models import Page
6
+ from .models import Page, UrlRedirect
7
7
 
8
8
 
9
9
  def fetch(request, *args, **kwargs):
10
10
  can_preview = request.user.is_staff or settings.DEBUG
11
11
  preview = can_preview and request.GET.get("preview", False)
12
12
  append_slash = getattr(django_settings, "APPEND_SLASH", True)
13
+ redirect_obj = UrlRedirect.find_redirect(request)
14
+ if redirect_obj:
15
+ return redirect_obj.redirect()
13
16
  if append_slash and not request.path.endswith("/"):
14
- return redirect(request.path + "/")
17
+ q_string = request.META.get("QUERY_STRING", "")
18
+ return redirect(request.path + "/" + ("?" + q_string if q_string else ""))
15
19
  if "permalink" in kwargs:
16
20
  page = Page.get_or_404(
17
21
  request, bypass_public_check=preview, bypass_type_check=True
@@ -1,6 +1,10 @@
1
1
  from django.db.models.query import QuerySet
2
2
  from django.core.exceptions import ObjectDoesNotExist
3
3
  from django.apps import apps
4
+ from django.db import models
5
+ from django.utils import timezone
6
+ from django.db.utils import ProgrammingError, OperationalError
7
+ from typing import Sequence, Tuple
4
8
 
5
9
  URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
6
10
 
@@ -16,16 +20,97 @@ class PageQuerySet(QuerySet):
16
20
  return self.__UrlNodeModel
17
21
 
18
22
  def get_permalink_kwargs(self, kwargs):
19
- return list(set(kwargs.keys()).intersection(set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])))
23
+ return list(
24
+ set(kwargs.keys()).intersection(
25
+ set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])
26
+ )
27
+ )
20
28
 
21
29
  def get(self, *args, **kwargs):
22
30
  permalink_args = self.get_permalink_kwargs(kwargs)
23
31
  if len(permalink_args):
24
32
  try:
25
- node = self.UrlNodeModel.objects.get(**{arg: kwargs.pop(arg) for arg in permalink_args})
33
+ node = self.UrlNodeModel.objects.get(
34
+ **{arg: kwargs.pop(arg) for arg in permalink_args}
35
+ )
26
36
  kwargs["url_node"] = node
27
37
  except ObjectDoesNotExist:
28
38
  raise self.model.DoesNotExist(
29
39
  "%s matching query does not exist." % self.model._meta.object_name
30
40
  )
31
41
  return super(PageQuerySet, self).get(*args, **kwargs)
42
+
43
+
44
+ class UrlNodeManager(models.Manager):
45
+ @property
46
+ def related_names(self):
47
+ self._related_names = getattr(
48
+ self,
49
+ "_related_names",
50
+ super().get_queryset().values_list("related_name", flat=True).distinct(),
51
+ )
52
+ return self._related_names
53
+
54
+ def _annotate_fields(
55
+ self,
56
+ qs: models.QuerySet,
57
+ field_names: Sequence[Tuple[str, models.Field, models.Value]],
58
+ ):
59
+ for field_name, output_field, default in field_names:
60
+ whens = [
61
+ models.When(
62
+ related_name=related_name,
63
+ then=models.F("__".join([related_name, field_name])),
64
+ )
65
+ for related_name in self.related_names
66
+ ]
67
+ qs = qs.annotate(
68
+ **{
69
+ field_name: models.Case(
70
+ *whens, output_field=output_field, default=default
71
+ )
72
+ }
73
+ )
74
+ return self._annotate_is_public(qs)
75
+
76
+ def _annotate_is_public(self, qs: models.QuerySet):
77
+ return qs.annotate(
78
+ is_public=models.Case(
79
+ models.When(status="PUB", then=True),
80
+ models.When(
81
+ status="PLA", publication_date__lte=timezone.now(), then=True
82
+ ),
83
+ default=False,
84
+ output_field=models.BooleanField(default=False),
85
+ )
86
+ )
87
+
88
+ def get_queryset(self):
89
+ try:
90
+ return self._annotate_fields(
91
+ super().get_queryset(),
92
+ [
93
+ (
94
+ "indexable",
95
+ models.BooleanField(),
96
+ models.Value(None, models.BooleanField()),
97
+ ),
98
+ (
99
+ "status",
100
+ models.CharField(),
101
+ models.Value("DRF", models.CharField()),
102
+ ),
103
+ (
104
+ "publication_date",
105
+ models.DateTimeField(),
106
+ models.Value(timezone.now(), models.DateTimeField()),
107
+ ),
108
+ (
109
+ "date_updated_at",
110
+ models.DateTimeField(),
111
+ models.Value(timezone.now(), models.DateTimeField()),
112
+ ),
113
+ ],
114
+ )
115
+ except (ProgrammingError, OperationalError):
116
+ return super().get_queryset()
camomilla/model_api.py CHANGED
@@ -47,7 +47,7 @@ def register(
47
47
  )
48
48
  },
49
49
  )
50
-
50
+
51
51
  def get_queryset(self, *args, **kwargs):
52
52
  qs = super(base_viewset, self).get_queryset(*args, **kwargs)
53
53
  return qs if filters is None else qs.filter(**filters)
@@ -66,9 +66,11 @@ def register(
66
66
 
67
67
  model_path = "".join(
68
68
  [
69
- "-" + character.lower()
70
- if character.isupper() and index > 0
71
- else character
69
+ (
70
+ "-" + character.lower()
71
+ if character.isupper() and index > 0
72
+ else character
73
+ )
72
74
  for index, character in enumerate(model.__name__)
73
75
  ]
74
76
  ).lstrip("-")
camomilla/models/menu.py CHANGED
@@ -17,7 +17,6 @@ from structured.fields import StructuredJSONField
17
17
  from camomilla.models.page import UrlNode, AbstractPage
18
18
  from typing import Optional, Union, Callable, List
19
19
  from django.db.models.base import Model as DjangoModel
20
- from django.conf import settings
21
20
 
22
21
 
23
22
  class LinkTypes(str, Enum):
@@ -37,7 +36,9 @@ class MenuNodeLink(BaseModel):
37
36
  if self.link_type == LinkTypes.relational:
38
37
  if self.content_type and self.page:
39
38
  if isinstance(self.page, DjangoModel) and not self.page._meta.abstract:
40
- self.content_type = ContentType.objects.get_for_model(self.page.__class__)
39
+ self.content_type = ContentType.objects.get_for_model(
40
+ self.page.__class__
41
+ )
41
42
  ctype_id = getattr(self.content_type, "pk", self.content_type)
42
43
  page_id = getattr(self.page, "pk", self.page)
43
44
  c_type = ContentType.objects.filter(pk=ctype_id).first()
@@ -47,7 +48,9 @@ class MenuNodeLink(BaseModel):
47
48
  elif self.url_node:
48
49
  url_node_id = getattr(self.url_node, "pk", self.url_node)
49
50
  self.page = UrlNode.objects.filter(pk=url_node_id).first().page
50
- self.content_type = ContentType.objects.get_for_model(self.page.__class__)
51
+ self.content_type = ContentType.objects.get_for_model(
52
+ self.page.__class__
53
+ )
51
54
  return handler(self)
52
55
 
53
56
  def get_url(self, request=None):
@@ -88,7 +91,9 @@ class Menu(models.Model):
88
91
  ):
89
92
  if isinstance(context, RequestContext):
90
93
  context = context.flatten()
91
- is_preview = bool(request.GET.get("preview", False))
94
+ is_preview = (
95
+ False if request is None else bool(request.GET.get("preview", False))
96
+ )
92
97
  context.update({"menu": self, "is_preview": is_preview})
93
98
  return mark_safe(render_to_string(template_path, context, request))
94
99