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.
Files changed (72) 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 +93 -8
  5. camomilla/model_api.py +14 -7
  6. camomilla/models/media.py +1 -1
  7. camomilla/models/menu.py +10 -4
  8. camomilla/models/page.py +189 -127
  9. camomilla/openapi/schema.py +17 -8
  10. camomilla/redirects.py +10 -0
  11. camomilla/serializers/base/__init__.py +6 -4
  12. camomilla/serializers/fields/__init__.py +5 -17
  13. camomilla/serializers/fields/related.py +10 -4
  14. camomilla/serializers/mixins/__init__.py +23 -195
  15. camomilla/serializers/mixins/fields.py +20 -0
  16. camomilla/serializers/mixins/filter_fields.py +57 -0
  17. camomilla/serializers/mixins/json.py +34 -0
  18. camomilla/serializers/mixins/language.py +32 -0
  19. camomilla/serializers/mixins/nesting.py +35 -0
  20. camomilla/serializers/mixins/optimize.py +91 -0
  21. camomilla/serializers/mixins/ordering.py +34 -0
  22. camomilla/serializers/mixins/page.py +58 -0
  23. camomilla/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
  24. camomilla/serializers/utils.py +5 -3
  25. camomilla/serializers/validators.py +6 -2
  26. camomilla/settings.py +10 -2
  27. camomilla/storages/default.py +12 -0
  28. camomilla/storages/optimize.py +2 -2
  29. camomilla/storages/overwrite.py +2 -2
  30. camomilla/templates/defaults/parts/menu.html +1 -1
  31. camomilla/templatetags/menus.py +3 -0
  32. camomilla/theme/__init__.py +1 -1
  33. camomilla/theme/{admin.py → admin/__init__.py} +22 -20
  34. camomilla/theme/admin/pages.py +46 -0
  35. camomilla/theme/admin/translations.py +13 -0
  36. camomilla/theme/apps.py +1 -5
  37. camomilla/translation.py +7 -1
  38. camomilla/urls.py +2 -5
  39. camomilla/utils/query_parser.py +167 -0
  40. camomilla/utils/translation.py +47 -5
  41. camomilla/views/base/__init__.py +35 -5
  42. camomilla/views/medias.py +1 -1
  43. camomilla/views/menus.py +0 -2
  44. camomilla/views/mixins/__init__.py +17 -69
  45. camomilla/views/mixins/bulk_actions.py +22 -0
  46. camomilla/views/mixins/language.py +33 -0
  47. camomilla/views/mixins/optimize.py +18 -0
  48. camomilla/views/mixins/pagination.py +12 -18
  49. camomilla/views/mixins/permissions.py +6 -0
  50. camomilla/views/pages.py +12 -2
  51. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/METADATA +23 -16
  52. django_camomilla_cms-6.0.0b17.dist-info/RECORD +132 -0
  53. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/WHEEL +1 -1
  54. tests/fixtures/__init__.py +17 -0
  55. tests/test_api.py +2 -11
  56. tests/test_camomilla_filters.py +7 -13
  57. tests/test_media.py +113 -0
  58. tests/test_menu.py +97 -0
  59. tests/test_model_api.py +68 -0
  60. tests/test_model_api_permissions.py +39 -0
  61. tests/test_model_api_register.py +393 -0
  62. tests/test_pages.py +343 -0
  63. tests/test_query_parser.py +58 -0
  64. tests/test_templates_context.py +111 -0
  65. tests/test_utils.py +64 -64
  66. tests/utils/api.py +28 -0
  67. tests/utils/media.py +9 -0
  68. camomilla/serializers/fields/json.py +0 -49
  69. django_camomilla_cms-6.0.0b15.dist-info/RECORD +0 -105
  70. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info/licenses}/LICENSE +0 -0
  71. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b17.dist-info}/top_level.txt +0 -0
  72. {camomilla/contrib/rest_framework → tests/utils}/__init__.py +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.15"
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,31 +1,116 @@
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
 
7
11
 
8
12
  class PageQuerySet(QuerySet):
9
-
13
+
10
14
  __UrlNodeModel = None
11
-
15
+
12
16
  @property
13
17
  def UrlNodeModel(self):
14
18
  if not self.__UrlNodeModel:
15
19
  self.__UrlNodeModel = apps.get_model("camomilla", "UrlNode")
16
- return self.__UrlNodeModel
17
-
20
+ return self.__UrlNodeModel
21
+
18
22
  def get_permalink_kwargs(self, kwargs):
19
- return list(set(kwargs.keys()).intersection(set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])))
20
-
23
+ return list(
24
+ set(kwargs.keys()).intersection(
25
+ set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])
26
+ )
27
+ )
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
- return super(PageQuerySet, self).get(*args, **kwargs)
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
@@ -26,6 +26,7 @@ def register(
26
26
  """
27
27
 
28
28
  def inner(model):
29
+ global urlpatterns
29
30
  base_meta = {
30
31
  "model": model,
31
32
  "fields": "__all__",
@@ -47,13 +48,17 @@ def register(
47
48
  },
48
49
  )
49
50
 
51
+ def get_queryset(self, *args, **kwargs):
52
+ qs = super(base_viewset, self).get_queryset(*args, **kwargs)
53
+ return qs if filters is None else qs.filter(**filters)
54
+
50
55
  viewset = type(
51
56
  f"{model.__name__}ViewSet",
52
57
  (base_viewset,),
53
58
  {
54
- "get_queryset": lambda self: model.objects.all()
55
- if filters is None
56
- else model.objects.filter(**filters),
59
+ "queryset": model.objects.all(),
60
+ "model": model,
61
+ "get_queryset": get_queryset,
57
62
  "serializer_class": serializer,
58
63
  **viewset_attrs,
59
64
  },
@@ -61,9 +66,11 @@ def register(
61
66
 
62
67
  model_path = "".join(
63
68
  [
64
- "-" + character.lower()
65
- if character.isupper() and index > 0
66
- else character
69
+ (
70
+ "-" + character.lower()
71
+ if character.isupper() and index > 0
72
+ else character
73
+ )
67
74
  for index, character in enumerate(model.__name__)
68
75
  ]
69
76
  ).lstrip("-")
@@ -73,7 +80,7 @@ def register(
73
80
  viewset,
74
81
  f"{model.__name__.lower()}_api",
75
82
  )
76
- urlpatterns.append(path("", include(router.urls)))
83
+ urlpatterns = [path("", include(router.urls))]
77
84
  return model
78
85
 
79
86
  return inner
camomilla/models/media.py CHANGED
@@ -150,7 +150,7 @@ class Media(models.Model):
150
150
  img_bytes = self.file.storage.open(self.file.name, "rb")
151
151
  with Image.open(img_bytes) as orig_image:
152
152
  image = orig_image.copy()
153
- image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.ANTIALIAS)
153
+ image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.LANCZOS)
154
154
 
155
155
  # Path to save to, name, and extension
156
156
  thumb_name, thumb_extension = os.path.splitext(self.file.name)
camomilla/models/menu.py CHANGED
@@ -14,7 +14,6 @@ from pydantic import (
14
14
  )
15
15
  from structured.pydantic.models import BaseModel
16
16
  from structured.fields import StructuredJSONField
17
- from structured.pydantic.fields import QuerySet
18
17
  from camomilla.models.page import UrlNode, AbstractPage
19
18
  from typing import Optional, Union, Callable, List
20
19
  from django.db.models.base import Model as DjangoModel
@@ -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,10 @@ class Menu(models.Model):
88
91
  ):
89
92
  if isinstance(context, RequestContext):
90
93
  context = context.flatten()
91
- context.update({"menu": self})
94
+ is_preview = (
95
+ False if request is None else bool(request.GET.get("preview", False))
96
+ )
97
+ context.update({"menu": self, "is_preview": is_preview})
92
98
  return mark_safe(render_to_string(template_path, context, request))
93
99
 
94
100
  class defaultdict(dict):