django-camomilla-cms 6.0.0b15__py2.py3-none-any.whl → 6.0.0b16__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 (38) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/managers/pages.py +7 -7
  3. camomilla/model_api.py +9 -4
  4. camomilla/models/media.py +1 -1
  5. camomilla/models/menu.py +3 -2
  6. camomilla/models/page.py +12 -11
  7. camomilla/openapi/schema.py +4 -0
  8. camomilla/serializers/base/__init__.py +3 -1
  9. camomilla/serializers/fields/json.py +0 -1
  10. camomilla/serializers/fields/related.py +5 -1
  11. camomilla/serializers/mixins/__init__.py +51 -6
  12. camomilla/serializers/mixins/filter_fields.py +56 -0
  13. camomilla/serializers/utils.py +3 -1
  14. camomilla/storages/default.py +6 -0
  15. camomilla/storages/optimize.py +2 -2
  16. camomilla/storages/overwrite.py +2 -2
  17. camomilla/templates/defaults/parts/menu.html +1 -1
  18. camomilla/theme/__init__.py +1 -1
  19. camomilla/utils/query_parser.py +148 -0
  20. camomilla/views/base/__init__.py +2 -2
  21. camomilla/views/menus.py +0 -2
  22. camomilla/views/mixins/__init__.py +9 -2
  23. camomilla/views/mixins/pagination.py +4 -13
  24. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/METADATA +1 -1
  25. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/RECORD +38 -28
  26. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/WHEEL +1 -1
  27. tests/fixtures/__init__.py +17 -0
  28. tests/test_api.py +2 -11
  29. tests/test_camomilla_filters.py +7 -13
  30. tests/test_media.py +80 -0
  31. tests/test_model_api.py +68 -0
  32. tests/test_model_api_permissions.py +39 -0
  33. tests/test_query_parser.py +59 -0
  34. tests/test_utils.py +64 -64
  35. tests/utils/__init__.py +0 -0
  36. tests/utils/api.py +29 -0
  37. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/LICENSE +0 -0
  38. {django_camomilla_cms-6.0.0b15.dist-info → django_camomilla_cms-6.0.0b16.dist-info}/top_level.txt +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.15"
1
+ __version__ = "6.0.0-beta.16"
2
2
 
3
3
 
4
4
  def get_core_apps():
@@ -6,26 +6,26 @@ URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
6
6
 
7
7
 
8
8
  class PageQuerySet(QuerySet):
9
-
9
+
10
10
  __UrlNodeModel = None
11
-
11
+
12
12
  @property
13
13
  def UrlNodeModel(self):
14
14
  if not self.__UrlNodeModel:
15
15
  self.__UrlNodeModel = apps.get_model("camomilla", "UrlNode")
16
- return self.__UrlNodeModel
17
-
16
+ return self.__UrlNodeModel
17
+
18
18
  def get_permalink_kwargs(self, kwargs):
19
19
  return list(set(kwargs.keys()).intersection(set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])))
20
-
20
+
21
21
  def get(self, *args, **kwargs):
22
22
  permalink_args = self.get_permalink_kwargs(kwargs)
23
23
  if len(permalink_args):
24
24
  try:
25
- node = self.UrlNodeModel.objects.get(**{arg:kwargs.pop(arg) for arg in permalink_args})
25
+ node = self.UrlNodeModel.objects.get(**{arg: kwargs.pop(arg) for arg in permalink_args})
26
26
  kwargs["url_node"] = node
27
27
  except ObjectDoesNotExist:
28
28
  raise self.model.DoesNotExist(
29
29
  "%s matching query does not exist." % self.model._meta.object_name
30
30
  )
31
- return super(PageQuerySet, self).get(*args, **kwargs)
31
+ return super(PageQuerySet, self).get(*args, **kwargs)
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__",
@@ -46,14 +47,18 @@ def register(
46
47
  )
47
48
  },
48
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)
49
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
  },
@@ -73,7 +78,7 @@ def register(
73
78
  viewset,
74
79
  f"{model.__name__.lower()}_api",
75
80
  )
76
- urlpatterns.append(path("", include(router.urls)))
81
+ urlpatterns = [path("", include(router.urls))]
77
82
  return model
78
83
 
79
84
  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,10 +14,10 @@ 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
20
+ from django.conf import settings
21
21
 
22
22
 
23
23
  class LinkTypes(str, Enum):
@@ -88,7 +88,8 @@ class Menu(models.Model):
88
88
  ):
89
89
  if isinstance(context, RequestContext):
90
90
  context = context.flatten()
91
- context.update({"menu": self})
91
+ is_preview = bool(request.GET.get("preview", False))
92
+ context.update({"menu": self, "is_preview": is_preview})
92
93
  return mark_safe(render_to_string(template_path, context, request))
93
94
 
94
95
  class defaultdict(dict):
camomilla/models/page.py CHANGED
@@ -36,6 +36,7 @@ from modeltranslation.utils import build_localized_fieldname
36
36
  class UrlPathValidator():
37
37
  pass
38
38
 
39
+
39
40
  def GET_TEMPLATE_CHOICES():
40
41
  return [(t, t) for t in get_all_templates_files()]
41
42
 
@@ -116,14 +117,13 @@ class UrlNodeManager(models.Manager):
116
117
 
117
118
 
118
119
  class UrlNode(models.Model):
119
-
120
+
120
121
  LANG_PERMALINK_FIELDS = [
121
122
  build_localized_fieldname("permalink", lang)
122
123
  for lang in AVAILABLE_LANGUAGES
123
124
  if settings.ENABLE_TRANSLATIONS
124
125
  ]
125
-
126
-
126
+
127
127
  permalink = models.CharField(max_length=400, unique=True, null=True)
128
128
  related_name = models.CharField(max_length=200)
129
129
  objects = UrlNodeManager()
@@ -153,7 +153,7 @@ class UrlNode(models.Model):
153
153
  if self.routerlink == "/":
154
154
  return ""
155
155
  return self.routerlink
156
-
156
+
157
157
  @staticmethod
158
158
  def sanitize_permalink(permalink):
159
159
  if isinstance(permalink, str):
@@ -162,12 +162,12 @@ class UrlNode(models.Model):
162
162
  if not permalink.startswith("/"):
163
163
  permalink = f"/{permalink}"
164
164
  return permalink
165
-
165
+
166
166
  def save(self, *args, **kwargs) -> None:
167
167
  for lang_p_field in UrlNode.LANG_PERMALINK_FIELDS:
168
168
  setattr(self, lang_p_field, UrlNode.sanitize_permalink(getattr(self, lang_p_field)))
169
169
  super().save(*args, **kwargs)
170
-
170
+
171
171
  def __str__(self) -> str:
172
172
  return self.permalink
173
173
 
@@ -190,10 +190,11 @@ class PageBase(models.base.ModelBase):
190
190
  def perm_prop_factory(permalink_field):
191
191
  def getter(_self):
192
192
  return getattr(_self, f"__{permalink_field}", getattr(_self.url_node or object(), permalink_field, None))
193
- def setter(_self, value:str):
193
+
194
+ def setter(_self, value: str):
194
195
  setattr(_self, f"__{permalink_field}", value)
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)
@@ -246,7 +247,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
246
247
  )
247
248
 
248
249
  objects = PageQuerySet.as_manager()
249
-
250
+
250
251
  __cached_db_instance: "AbstractPage" = None
251
252
 
252
253
  @property
@@ -254,7 +255,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
254
255
  if self.__cached_db_instance is None:
255
256
  self.__cached_db_instance = self.get_db_instance()
256
257
  return self.__cached_db_instance
257
-
258
+
258
259
  def get_db_instance(self):
259
260
  if self.pk:
260
261
  return self.__class__.objects.get(pk=self.pk)
@@ -337,7 +338,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
337
338
  for __ in activate_languages():
338
339
  old_permalink = self.db_instance and self.db_instance.permalink
339
340
  new_permalink = self.permalink
340
- if self.autopermalink:
341
+ if self.autopermalink:
341
342
  new_permalink = self.generate_permalink()
342
343
  force = force or old_permalink != new_permalink
343
344
  set_nofallbacks(self.url_node, "permalink", new_permalink)
@@ -55,4 +55,8 @@ class SchemaGenerator(DRFSchemaGenerator):
55
55
  def create_view(self, callback, method, request=None):
56
56
  view = super(SchemaGenerator, self).create_view(callback, method, request)
57
57
  view.schema = AutoSchema()
58
+ if not hasattr(view, 'get_queryset') and getattr(view, 'queryset', None) is None:
59
+ attname = "permission_classes"
60
+ cname = "DjangoModelPermissions"
61
+ setattr(view, attname, [p for p in getattr(view, attname, []) if cname not in p.__name__])
58
62
  return view
@@ -8,14 +8,16 @@ from ..mixins import (
8
8
  OrderingMixin,
9
9
  SetupEagerLoadingMixin,
10
10
  )
11
+ from ..mixins.filter_fields import FilterFieldsMixin
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
  ):
@@ -9,7 +9,6 @@ if TYPE_CHECKING:
9
9
  from structured.pydantic.models import BaseModel
10
10
 
11
11
 
12
-
13
12
  class StructuredJSONField(serializers.JSONField):
14
13
  """
15
14
  This field allows to serialize and deserialize structured data.
@@ -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
- return self.serializer(instance, context=self.context).data
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):
@@ -57,8 +57,50 @@ class SetupEagerLoadingMixin:
57
57
  """
58
58
  This mixin allows to use the setup_eager_loading method to optimize the queries.
59
59
  """
60
- @staticmethod
61
- def setup_eager_loading(queryset):
60
+
61
+ @classmethod
62
+ def optimize_qs(cls, queryset, context=None):
63
+ if hasattr(cls, "setup_eager_loading"):
64
+ queryset = cls.setup_eager_loading(queryset, context=context)
65
+ return cls.auto_optimize_queryset(queryset, context=context)
66
+
67
+ @classmethod
68
+ def auto_optimize_queryset(cls, queryset, context=None):
69
+ request = context.get("request", None)
70
+ if request and request.method == "GET":
71
+ model = getattr(cls.Meta, "model", None)
72
+ info = model_meta.get_field_info(model)
73
+ only = set()
74
+ prefetch_related = set()
75
+ select_related = set()
76
+ serializer_fields = cls(context=context).fields.keys()
77
+ filtered_fields = set()
78
+ for field in request.query_params.get("fields", "").split(","):
79
+ if "__" in field:
80
+ field, _ = field.split("__", 1)
81
+ if field in serializer_fields:
82
+ filtered_fields.add(field)
83
+ if len(filtered_fields) == 0:
84
+ filtered_fields = serializer_fields
85
+ for field in filtered_fields:
86
+ complete_field = field
87
+ if "__" in field:
88
+ field, sub_field = field.split("__", 1)
89
+ complete_field = f"{field}__{sub_field}"
90
+ if field in info.forward_relations and not info.forward_relations[field].to_many:
91
+ select_related.add(field)
92
+ only.add(complete_field)
93
+ elif field in info.reverse_relations or field in info.forward_relations and info.forward_relations[field].to_many:
94
+ prefetch_related.add(field)
95
+ only.add(complete_field)
96
+ elif field in info.fields or field == info.pk.name:
97
+ only.add(complete_field)
98
+ if len(only) > 0:
99
+ queryset = queryset.only(*only)
100
+ if len(select_related) > 0:
101
+ queryset = queryset.select_related(*select_related)
102
+ if len(prefetch_related) > 0:
103
+ queryset = queryset.prefetch_related(*prefetch_related)
62
104
  return queryset
63
105
 
64
106
 
@@ -174,13 +216,16 @@ class AbstractPageMixin(serializers.ModelSerializer):
174
216
 
175
217
  def get_default_field_names(self, *args):
176
218
  from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
177
-
219
+ default_fields = super().get_default_field_names(*args)
220
+ filtered_fields = getattr(self, "filtered_fields", [])
221
+ if len(filtered_fields) > 0:
222
+ return filtered_fields
178
223
  if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
179
- return super().get_default_field_names(*args)
224
+ return default_fields
180
225
  return (
181
- [f for f in super().get_default_field_names(*args) if f != "url_node"]
226
+ [f for f in default_fields if f != "url_node"]
182
227
  + UrlNode.LANG_PERMALINK_FIELDS
183
- + ["permalink"]
228
+ + ["permalink"]
184
229
  )
185
230
 
186
231
  def build_field(self, field_name, info, model_class, nested_depth):
@@ -0,0 +1,56 @@
1
+ from rest_framework import serializers
2
+ from camomilla.serializers.fields.related import RelatedField
3
+ from collections import defaultdict
4
+
5
+
6
+ class FilterFieldsMixin(serializers.ModelSerializer):
7
+ """
8
+ Mixin to filter fields from a serializer, including handling nested fields.
9
+ """
10
+
11
+ def __init__(self, *args, **kwargs):
12
+ self.inherited_fields_filter = kwargs.pop("inherited_fields_filter", [])
13
+ return super().__init__(*args, **kwargs)
14
+
15
+
16
+ inherited_fields_filter = []
17
+
18
+
19
+ def get_default_field_names(self, *args):
20
+ field_names = super().get_default_field_names(*args)
21
+ request = self.context.get("request", None)
22
+
23
+ if request is not None and request.method == "GET":
24
+ fields = request.query_params.get("fields", "").split(",")
25
+ fields = [f for f in fields if f != ""]
26
+ if len(self.inherited_fields_filter) > 0:
27
+ fields = self.inherited_fields_filter
28
+
29
+ self.filtered_fields = set()
30
+ self.childs_fields = defaultdict(set)
31
+ for field in fields:
32
+ if "__" in field:
33
+ parent_field, child_field = field.split("__", 1)
34
+ if parent_field in field_names:
35
+ self.filtered_fields.add(parent_field)
36
+ self.childs_fields[parent_field].add(child_field)
37
+
38
+
39
+ else:
40
+ if field in field_names:
41
+ self.filtered_fields.add(field)
42
+
43
+ if len(self.filtered_fields) > 0:
44
+ return list(self.filtered_fields)
45
+ else:
46
+ return field_names
47
+
48
+ return field_names
49
+
50
+ def build_field(self, field_name, info, model_class, nested_depth):
51
+ field_class, field_kwargs = super().build_field(field_name, info, model_class, nested_depth)
52
+ inherited_fields_filter = self.childs_fields.get(field_name, []) if hasattr(self, "childs_fields") else []
53
+ if len(inherited_fields_filter) > 0 and issubclass(field_class, RelatedField):
54
+ field_kwargs["inherited_fields_filter"] = list(inherited_fields_filter)
55
+ return field_class, field_kwargs
56
+
@@ -1,6 +1,7 @@
1
1
  def get_standard_bases() -> tuple:
2
2
  from rest_framework.serializers import ModelSerializer
3
3
  from camomilla.serializers.fields import FieldsOverrideMixin
4
+ from camomilla.serializers.mixins.filter_fields import FilterFieldsMixin
4
5
  from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
5
6
  from camomilla.serializers.mixins import (
6
7
  JSONFieldPatchMixin,
@@ -10,12 +11,13 @@ def get_standard_bases() -> tuple:
10
11
  )
11
12
 
12
13
  return (
14
+ SetupEagerLoadingMixin,
15
+ FilterFieldsMixin,
13
16
  NestMixin,
14
17
  FieldsOverrideMixin,
15
18
  JSONFieldPatchMixin,
16
19
  OrderingMixin,
17
20
  RemoveTranslationsMixin,
18
- SetupEagerLoadingMixin,
19
21
  ModelSerializer,
20
22
  )
21
23
 
@@ -0,0 +1,6 @@
1
+ from django.utils.module_loading import import_string
2
+ from django.conf import settings
3
+
4
+
5
+ def get_default_storage_class():
6
+ return import_string(settings.STORAGES["default"]["BACKEND"])
@@ -2,13 +2,13 @@ import traceback
2
2
  from io import BytesIO
3
3
 
4
4
  from django.core.files.base import ContentFile
5
- from django.core.files.storage import get_storage_class
6
5
  from PIL import Image
7
6
 
8
7
  from camomilla import settings
8
+ from camomilla.storages.default import get_default_storage_class
9
9
 
10
10
 
11
- class OptimizedStorage(get_storage_class()):
11
+ class OptimizedStorage(get_default_storage_class()):
12
12
  MEDIA_MAX_WIDTH = settings.MEDIA_OPTIMIZE_MAX_WIDTH
13
13
  MEDIA_MAX_HEIGHT = settings.MEDIA_OPTIMIZE_MAX_HEIGHT
14
14
  MEDIA_DPI = settings.MEDIA_OPTIMIZE_DPI
@@ -1,7 +1,7 @@
1
- from django.core.files.storage import get_storage_class
1
+ from camomilla.storages.default import get_default_storage_class
2
2
 
3
3
 
4
- class OverwriteStorage(get_storage_class()):
4
+ class OverwriteStorage(get_default_storage_class()):
5
5
  def _save(self, name, content):
6
6
  if self.exists(name):
7
7
  self.delete(name)
@@ -4,7 +4,7 @@
4
4
  {% for item in menu.nodes %}
5
5
  <li>
6
6
  {% if item.link.url %}
7
- <a href="{{ item.link.url }}">{{ item.title }}</a>
7
+ <a href="{{ item.link.url }}{% if is_preview %}?preview=true{% endif %}">{{ item.title }}</a>
8
8
  {% else %}
9
9
  <span>{{item.title}}</span>
10
10
  {% endif %}
@@ -1 +1 @@
1
- __version__ = "6.0.0-beta.15"
1
+ __version__ = "6.0.0-beta.16"
@@ -0,0 +1,148 @@
1
+ import re
2
+ from django.db.models import Q
3
+
4
+ CONDITION_PATTERN = re.compile(r"(\w+__\w+='[^']+'|\w+__\w+=\S+)") # Updated regex to handle quoted values
5
+ LOGICAL_OPERATORS = {"AND", "OR"}
6
+
7
+ class ConditionParser:
8
+ def __init__(self, query):
9
+ self.query = query
10
+
11
+ def parse(self, query=None):
12
+ """Parse the query or subquery. If no query is provided, use the instance's query."""
13
+ if query is None:
14
+ query = self.query
15
+
16
+ tokens = self.tokenize(query)
17
+ # If there's just one token and it's a dictionary (single condition), return it
18
+ if len(tokens) == 1 and isinstance(tokens[0], dict):
19
+ return tokens[0]
20
+ return self.build_tree(tokens)
21
+
22
+ def tokenize(self, query):
23
+ tokens = []
24
+ i = 0
25
+ while i < len(query):
26
+ if query[i] == '(':
27
+ j = i + 1
28
+ open_parens = 1
29
+ while j < len(query) and open_parens > 0:
30
+ if query[j] == '(':
31
+ open_parens += 1
32
+ elif query[j] == ')':
33
+ open_parens -= 1
34
+ j += 1
35
+ if open_parens == 0:
36
+ subquery = query[i + 1:j - 1]
37
+ tokens.append(self.parse(subquery)) # Pass the subquery here
38
+ i = j
39
+ else:
40
+ raise ValueError("Mismatched parentheses")
41
+ elif query[i:i+3] == 'AND' or query[i:i+2] == 'OR':
42
+ operator = 'AND' if query[i:i+3] == 'AND' else 'OR'
43
+ tokens.append(operator)
44
+ i += 3 if operator == 'AND' else 2
45
+ else:
46
+ match = CONDITION_PATTERN.match(query[i:])
47
+ if match:
48
+ condition = self.parse_condition(match.group())
49
+ tokens.append(condition)
50
+ i += match.end()
51
+ else:
52
+ i += 1
53
+ return tokens
54
+
55
+ def parse_condition(self, condition_str):
56
+ """Parse a single condition into field lookup and value."""
57
+ if '=' in condition_str:
58
+ field_lookup, value = condition_str.split("=")
59
+ value = value.strip("'").strip('"') # Remove single or double quotes
60
+ value = self.parse_value(value) # Parse the value
61
+ return {"field_lookup": field_lookup, "value": value}
62
+ return None
63
+
64
+ def parse_value(self, string: str):
65
+ """Parse single condition values based on specific rules."""
66
+ if string and string.startswith("[") and string.endswith("]"):
67
+ string = [self.parse_value(substr) for substr in string[1:-1].split(",")]
68
+ elif string and string.lower() in ["true", "false"]:
69
+ string = string.lower() == "true"
70
+ elif string and string.isdigit():
71
+ string = int(string)
72
+ return string
73
+
74
+ def build_tree(self, tokens):
75
+ """Build a tree-like structure with operators and conditions."""
76
+ if not tokens:
77
+ return None
78
+
79
+ output_stack = []
80
+ operator_stack = []
81
+
82
+ # Process each token in the query
83
+ for token in tokens:
84
+ if isinstance(token, dict):
85
+ # Handle a single condition
86
+ if operator_stack:
87
+ operator = operator_stack.pop()
88
+ if isinstance(output_stack[-1], dict):
89
+ output_stack[-1] = {
90
+ "operator": operator,
91
+ "conditions": [output_stack[-1], token]
92
+ }
93
+ else:
94
+ output_stack[-1]["conditions"].append(token)
95
+ else:
96
+ output_stack.append(token)
97
+
98
+ elif token in LOGICAL_OPERATORS:
99
+ # Operator found (AND/OR), handle precedence
100
+ operator_stack.append(token)
101
+
102
+ # If only one item in output_stack, return it directly
103
+ if len(output_stack) == 1:
104
+ return output_stack[0]
105
+ return {"operator": "AND", "conditions": output_stack} # Default to AND if no operators
106
+
107
+ def to_q(self, parsed_tree):
108
+ """Convert parsed tree structure into Q objects."""
109
+ if isinstance(parsed_tree, list):
110
+ # If parsed_tree is a list, combine all conditions with AND by default
111
+ q_objects = [self.to_q(cond) for cond in parsed_tree]
112
+ combined_q = Q()
113
+ for q_obj in q_objects:
114
+ combined_q &= q_obj
115
+ return combined_q
116
+
117
+ if isinstance(parsed_tree, dict):
118
+ if "field_lookup" in parsed_tree:
119
+ # Base case: a single condition
120
+ return Q(**{parsed_tree["field_lookup"]: parsed_tree["value"]})
121
+
122
+ elif "operator" in parsed_tree and "conditions" in parsed_tree:
123
+ operator = parsed_tree["operator"]
124
+ conditions = parsed_tree["conditions"]
125
+
126
+ q_objects = [self.to_q(cond) for cond in conditions]
127
+
128
+ if operator == "AND":
129
+ combined_q = Q()
130
+ for q_obj in q_objects:
131
+ combined_q &= q_obj
132
+ return combined_q
133
+ elif operator == "OR":
134
+ combined_q = Q()
135
+ for q_obj in q_objects:
136
+ combined_q |= q_obj
137
+ return combined_q
138
+ else:
139
+ raise ValueError(f"Unknown operator: {operator}")
140
+
141
+ raise ValueError("Parsed tree structure is invalid")
142
+
143
+ def parse_to_q(self):
144
+ """Parse the query and convert to Q object."""
145
+ parsed_tree = self.parse()
146
+ if not parsed_tree:
147
+ return Q() # Return an empty Q if parsing fails
148
+ return self.to_q(parsed_tree)
@@ -1,8 +1,8 @@
1
- from ..mixins import OptimViewMixin, PaginateStackMixin, OrderingMixin
1
+ from ..mixins import OptimViewMixin, PaginateStackMixin, OrderingMixin, CamomillaBasePermissionMixin
2
2
  from rest_framework import viewsets
3
3
 
4
4
 
5
5
  class BaseModelViewset(
6
- OptimViewMixin, OrderingMixin, PaginateStackMixin, viewsets.ModelViewSet
6
+ CamomillaBasePermissionMixin, OptimViewMixin, OrderingMixin, PaginateStackMixin, viewsets.ModelViewSet
7
7
  ):
8
8
  pass
camomilla/views/menus.py CHANGED
@@ -13,8 +13,6 @@ from camomilla.serializers.page import BasicUrlNodeSerializer
13
13
  from camomilla.views.base import BaseModelViewset
14
14
  from camomilla.views.decorators import active_lang
15
15
 
16
- from django.utils.translation import get_language
17
-
18
16
 
19
17
  class MenuViewSet(BaseModelViewset):
20
18
  queryset = Menu.objects.all()
@@ -30,9 +30,16 @@ class GetUserLanguageMixin(object):
30
30
  return super().initialize_request(request, *args, **kwargs)
31
31
 
32
32
  def get_queryset(self):
33
+ if hasattr(super(), "get_queryset"):
34
+ return super().get_queryset()
33
35
  return self.model.objects.all()
34
36
 
35
37
 
38
+ class CamomillaBasePermissionMixin:
39
+ def get_permissions(self):
40
+ return [*super().get_permissions(), CamomillaBasePermissions()]
41
+
42
+
36
43
  class OptimViewMixin:
37
44
  def get_serializer_class(self):
38
45
  if hasattr(self, "action_serializers"):
@@ -46,8 +53,8 @@ class OptimViewMixin:
46
53
  def get_queryset(self):
47
54
  queryset = super().get_queryset()
48
55
  serializer = self.get_serializer_class()
49
- if hasattr(serializer, "setup_eager_loading"):
50
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
56
+ if hasattr(serializer, "optimize_qs"):
57
+ queryset = serializer.optimize_qs(queryset, context=self.get_serializer_context())
51
58
  return queryset
52
59
 
53
60