django-camomilla-cms 6.1.4__py2.py3-none-any.whl → 6.2.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 (34) hide show
  1. camomilla/__init__.py +1 -1
  2. camomilla/managers/pages.py +36 -5
  3. camomilla/models/mixins/__init__.py +1 -0
  4. camomilla/models/page.py +3 -2
  5. camomilla/serializers/mixins/translation.py +2 -3
  6. camomilla/settings.py +12 -1
  7. camomilla/templates/defaults/base.html +2 -0
  8. camomilla/templates/defaults/parts/langswitch.html +7 -0
  9. camomilla/theme/__init__.py +1 -1
  10. camomilla/theme/admin/pages.py +1 -1
  11. camomilla/translation.py +1 -0
  12. camomilla/utils/translation.py +7 -8
  13. camomilla/views/articles.py +0 -2
  14. camomilla/views/contents.py +0 -2
  15. camomilla/views/decorators.py +23 -3
  16. camomilla/views/medias.py +0 -3
  17. camomilla/views/menus.py +0 -2
  18. camomilla/views/pages.py +10 -5
  19. camomilla/views/tags.py +0 -2
  20. camomilla/views/users.py +1 -2
  21. {django_camomilla_cms-6.1.4.dist-info → django_camomilla_cms-6.2.0.dist-info}/METADATA +1 -1
  22. {django_camomilla_cms-6.1.4.dist-info → django_camomilla_cms-6.2.0.dist-info}/RECORD +34 -29
  23. tests/test_admin_page_form.py +2 -2
  24. tests/test_camomilla_filters.py +1 -1
  25. tests/test_model_api_permissions.py +6 -0
  26. tests/test_pages.py +18 -0
  27. tests/test_parsers.py +62 -0
  28. tests/test_sitemap.py +16 -0
  29. tests/test_utils_getters.py +41 -0
  30. tests/test_utils_normalization.py +37 -0
  31. tests/test_utils_setters.py +62 -0
  32. {django_camomilla_cms-6.1.4.dist-info → django_camomilla_cms-6.2.0.dist-info}/WHEEL +0 -0
  33. {django_camomilla_cms-6.1.4.dist-info → django_camomilla_cms-6.2.0.dist-info}/licenses/LICENSE +0 -0
  34. {django_camomilla_cms-6.1.4.dist-info → django_camomilla_cms-6.2.0.dist-info}/top_level.txt +0 -0
camomilla/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.1.4"
1
+ __version__ = "6.2.0"
2
2
 
3
3
 
4
4
  def get_core_apps():
@@ -42,13 +42,44 @@ class PageQuerySet(QuerySet):
42
42
 
43
43
 
44
44
  class UrlNodeManager(models.Manager):
45
+
46
+ def get_reverse_pages_relations(self):
47
+ """
48
+ Get all reverse relations coming from AbstractPages models.
49
+ This is used to annotate the UrlNode with the related page fields.
50
+ """
51
+ from camomilla.models.page import AbstractPage
52
+
53
+ relations = []
54
+
55
+ for field in self.model._meta.get_fields():
56
+ if not (hasattr(field, "related_model") and field.one_to_one):
57
+ continue
58
+
59
+ if not issubclass(field.related_model, AbstractPage):
60
+ continue
61
+
62
+ if field.remote_field.name != "url_node":
63
+ continue
64
+
65
+ related_name = field.get_accessor_name()
66
+ relations.append(
67
+ {
68
+ "name": related_name,
69
+ "model": field.related_model,
70
+ "field_name": field.remote_field.name,
71
+ "field": field,
72
+ }
73
+ )
74
+ return relations
75
+
45
76
  @property
46
77
  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
- )
78
+ self._related_names = getattr(self, "_related_names", None)
79
+ if self._related_names is None:
80
+ self._related_names = list(
81
+ set([rel["name"] for rel in self.get_reverse_pages_relations()])
82
+ )
52
83
  return self._related_names
53
84
 
54
85
  def _annotate_fields(
@@ -16,6 +16,7 @@ class SeoMixin(models.Model):
16
16
  on_delete=models.SET_NULL,
17
17
  related_name="%(app_label)s_%(class)s_og_images",
18
18
  )
19
+ keywords = models.JSONField(default=list)
19
20
 
20
21
  class Meta:
21
22
  abstract = True
camomilla/models/page.py CHANGED
@@ -99,7 +99,7 @@ class UrlNode(models.Model):
99
99
  LANG_PERMALINK_FIELDS = (
100
100
  [
101
101
  build_localized_fieldname("permalink", lang)
102
- for lang in settings.AVAILABLE_LANGUAGES
102
+ for lang in settings.LANGUAGE_CODES
103
103
  ]
104
104
  if settings.ENABLE_TRANSLATIONS
105
105
  else ["permalink"]
@@ -319,7 +319,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
319
319
  @property
320
320
  def breadcrumbs(self) -> Sequence[dict]:
321
321
  breadcrumb = {
322
- "permalink": self.permalink,
322
+ "permalink": self.routerlink,
323
323
  "title": self.breadcrumbs_title or self.title or "",
324
324
  }
325
325
  if self.parent:
@@ -481,6 +481,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
481
481
  )
482
482
  if preview:
483
483
  permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
484
+ permalinks.pop(get_language(), None)
484
485
  return permalinks
485
486
 
486
487
  class Meta:
@@ -1,6 +1,5 @@
1
1
  from functools import cached_property
2
2
  from typing import Iterable, List
3
- from modeltranslation import settings as mt_settings
4
3
  from modeltranslation.translator import NotRegistered, translator
5
4
  from modeltranslation.utils import build_localized_fieldname
6
5
  from rest_framework import serializers
@@ -8,7 +7,7 @@ from rest_framework.exceptions import ValidationError
8
7
  from camomilla.utils.getters import pointed_getter
9
8
  from camomilla.utils.translation import is_translatable
10
9
  from camomilla.utils.translation import nest_to_plain, plain_to_nest
11
- from camomilla.settings import API_TRANSLATION_ACCESSOR
10
+ from camomilla.settings import API_TRANSLATION_ACCESSOR, LANGUAGE_CODES
12
11
 
13
12
 
14
13
  class TranslationsMixin(serializers.ModelSerializer):
@@ -94,7 +93,7 @@ class RemoveTranslationsMixin(serializers.ModelSerializer):
94
93
  included_translations = []
95
94
 
96
95
  field_names = super().get_default_field_names(declared_fields, model_info)
97
- for lang in mt_settings.AVAILABLE_LANGUAGES:
96
+ for lang in LANGUAGE_CODES:
98
97
  if lang not in included_translations:
99
98
  for field in self.translation_fields:
100
99
  localized_fieldname = build_localized_fieldname(field, lang)
camomilla/settings.py CHANGED
@@ -105,6 +105,10 @@ INTEGRATIONS_ASTRO_URL = pointed_getter(
105
105
  django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.URL", ""
106
106
  )
107
107
 
108
+ PAGE_ROUTER_CACHE = pointed_getter(
109
+ django_settings, "CAMOMILLA.API.PAGES.ROUTER_CACHE", 60 * 15
110
+ )
111
+
108
112
  DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
109
113
 
110
114
  # camomilla settings example
@@ -133,6 +137,13 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
133
137
  # "URL": "http://localhost:4321"
134
138
  # }
135
139
  # }
136
- # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations", "PAGES": {"DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"}},
140
+ # "API": {
141
+ # "NESTING_DEPTH": 10,
142
+ # "TRANSLATION_ACCESSOR": "translations",
143
+ # "PAGES": {
144
+ # "DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"
145
+ # },
146
+ # "ROUTER_CACHE": 60 * 15
147
+ # },
137
148
  # "DEBUG": False
138
149
  # }
@@ -21,6 +21,8 @@
21
21
  justify-content: center;
22
22
  font-family: sans-serif;
23
23
  margin: 0;
24
+ width: 100vw;
25
+ height: 100vh;
24
26
  }
25
27
 
26
28
  .letter {
@@ -36,6 +36,13 @@
36
36
  {% get_language_info_list for LANGUAGES as languages %}
37
37
  {% get_language_info for LANGUAGE_CODE as current_lang %}
38
38
 
39
+ <div class="language-switch--btn" data-lang="{{current_lang.code}}"
40
+ {% if current_lang.code != LANGUAGE_CODE %}
41
+ onclick="submitLanguageWithoutRedirect('{{current_lang.code}}');"
42
+ {% endif %}
43
+ >
44
+ <a>{{ current_lang.name_translated }} {% if current_lang.code == LANGUAGE_CODE %} 👾 {% endif %}</a>
45
+ </div>
39
46
  {% for lang_code, redirect in page.alternate_urls.items %}
40
47
  <div class="language-switch--btn" data-lang="{{lang_code}}"
41
48
  {% if current_lang.code != lang_code %}
@@ -1 +1 @@
1
- __version__ = "6.1.4"
1
+ __version__ = "6.2.0"
@@ -29,7 +29,7 @@ class AbstractPageModelForm(
29
29
  super().__init__(*args, **kwargs)
30
30
  templates = [(t, t) for t in get_templates(request)]
31
31
  templates.insert(0, ("", "---------"))
32
- self.fields["template"] = forms.ChoiceField(choices=templates)
32
+ self.fields["template"] = forms.ChoiceField(choices=templates, required=False)
33
33
 
34
34
  def get_initial_for_field(self, field, field_name):
35
35
  if field_name in UrlNode.LANG_PERMALINK_FIELDS:
camomilla/translation.py CHANGED
@@ -12,6 +12,7 @@ class SeoMixinTranslationOptions(TranslationOptions):
12
12
  "og_type",
13
13
  "og_url",
14
14
  "canonical",
15
+ "keywords",
15
16
  )
16
17
 
17
18
 
@@ -3,13 +3,12 @@ from typing import Any, Sequence, Iterator, Union, List
3
3
 
4
4
  from django.db.models import Model, Q
5
5
  from django.utils.translation.trans_real import activate, get_language
6
- from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
7
6
  from modeltranslation.utils import build_localized_fieldname
8
- from camomilla.settings import BASE_URL
7
+ from camomilla.settings import BASE_URL, DEFAULT_LANGUAGE, LANGUAGE_CODES
9
8
  from django.http import QueryDict
10
9
 
11
10
 
12
- def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
11
+ def activate_languages(languages: Sequence[str] = LANGUAGE_CODES) -> Iterator[str]:
13
12
  old = get_language()
14
13
  for language in languages:
15
14
  activate(language)
@@ -37,7 +36,7 @@ def url_lang_decompose(url):
37
36
  if BASE_URL and url.startswith(BASE_URL):
38
37
  url = url[len(BASE_URL) :]
39
38
  data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
40
- result = re.match(rf"^/?({'|'.join(AVAILABLE_LANGUAGES)})?/(.*)", url) # noqa: W605
39
+ result = re.match(rf"^/?({'|'.join(LANGUAGE_CODES)})?/(.*)", url) # noqa: W605
41
40
  groups = result and result.groups()
42
41
  if groups and len(groups) == 2:
43
42
  data["language"] = groups[0]
@@ -48,14 +47,14 @@ def url_lang_decompose(url):
48
47
  def get_field_translations(instance: Model, field_name: str, *args, **kwargs):
49
48
  return {
50
49
  lang: get_nofallbacks(instance, field_name, language=lang, *args, **kwargs)
51
- for lang in AVAILABLE_LANGUAGES
50
+ for lang in LANGUAGE_CODES
52
51
  }
53
52
 
54
53
 
55
54
  def lang_fallback_query(**kwargs):
56
55
  current_lang = get_language()
57
56
  query = Q()
58
- for lang in AVAILABLE_LANGUAGES:
57
+ for lang in LANGUAGE_CODES:
59
58
  query |= Q(**{f"{key}_{lang}": value for key, value in kwargs.items()})
60
59
  if current_lang:
61
60
  query = query & Q(
@@ -77,7 +76,7 @@ def plain_to_nest(data, fields, accessor="translations"):
77
76
  into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
78
77
  """
79
78
  trans_data = {}
80
- for lang in AVAILABLE_LANGUAGES:
79
+ for lang in LANGUAGE_CODES:
81
80
  lang_data = {}
82
81
  for field in fields:
83
82
  trans_field_name = build_localized_fieldname(field, lang)
@@ -101,7 +100,7 @@ def nest_to_plain(
101
100
  if isinstance(data, QueryDict):
102
101
  data = data.dict()
103
102
  translations = data.pop(accessor, {})
104
- for lang in AVAILABLE_LANGUAGES:
103
+ for lang in LANGUAGE_CODES:
105
104
  nest_trans = translations.pop(lang, {})
106
105
  for k in fields:
107
106
  data.pop(k, None) # this removes all trans field without lang
@@ -1,5 +1,4 @@
1
1
  from camomilla.models import Article
2
- from camomilla.permissions import CamomillaBasePermissions
3
2
  from camomilla.serializers import ArticleSerializer
4
3
  from camomilla.views.base import BaseModelViewset
5
4
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -8,6 +7,5 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
8
7
  class ArticleViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
9
8
  queryset = Article.objects.all()
10
9
  serializer_class = ArticleSerializer
11
- permission_classes = (CamomillaBasePermissions,)
12
10
  search_fields = ["title", "identifier", "content", "permalink"]
13
11
  model = Article
@@ -4,7 +4,6 @@ from django.http import JsonResponse
4
4
  from rest_framework.decorators import action
5
5
 
6
6
  from camomilla.models import Content
7
- from camomilla.permissions import CamomillaBasePermissions
8
7
  from camomilla.serializers import ContentSerializer
9
8
  from camomilla.views.base import BaseModelViewset
10
9
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -14,7 +13,6 @@ class ContentViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
14
13
  queryset = Content.objects.all()
15
14
  serializer_class = ContentSerializer
16
15
  model = Content
17
- permission_classes = (CamomillaBasePermissions,)
18
16
 
19
17
  @action(detail=True, methods=["get", "patch"])
20
18
  def djsuperadmin(self, request, pk):
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  from django.utils.translation import activate
3
- from django.conf import settings
3
+ from django.views.decorators.cache import cache_page
4
+ from camomilla.settings import LANGUAGE_CODES, DEFAULT_LANGUAGE
4
5
 
5
6
 
6
7
  def active_lang(*args, **kwargs):
@@ -11,12 +12,12 @@ def active_lang(*args, **kwargs):
11
12
  request = args[0].request
12
13
  else:
13
14
  request = args[0] if len(args) else kwargs.get("request", None)
14
- lang = settings.LANGUAGE_CODE
15
+ lang = DEFAULT_LANGUAGE
15
16
  if request and hasattr(request, "GET"):
16
17
  lang = request.GET.get("lang", request.GET.get("language", lang))
17
18
  if request and hasattr(request, "data"):
18
19
  lang = request.data.pop("lang", request.data.pop("language", lang))
19
- if lang and lang in [lng[0] for lng in settings.LANGUAGES]:
20
+ if lang and lang in LANGUAGE_CODES:
20
21
  activate(lang)
21
22
  request.LANGUAGE_CODE = lang
22
23
  return func(*args, **kwargs)
@@ -24,3 +25,22 @@ def active_lang(*args, **kwargs):
24
25
  return wrapped_func
25
26
 
26
27
  return decorator
28
+
29
+
30
+ def staff_excluded_cache(timing=None):
31
+ def decorator(func):
32
+ if timing is None:
33
+ return func # No caching applied
34
+
35
+ @functools.wraps(func)
36
+ def wrapped_func(*args, **kwargs):
37
+ request = args[0] if len(args) else kwargs.get("request", None)
38
+ if request and hasattr(request, "user"):
39
+ user = request.user
40
+ if user.is_authenticated and user.is_staff:
41
+ return func(*args, **kwargs)
42
+ return cache_page(timing)(func)(*args, **kwargs)
43
+
44
+ return wrapped_func
45
+
46
+ return decorator
camomilla/views/medias.py CHANGED
@@ -3,7 +3,6 @@ from rest_framework.response import Response
3
3
 
4
4
  from camomilla.models import Media, MediaFolder
5
5
  from camomilla.parsers import MultipartJsonParser
6
- from camomilla.permissions import CamomillaBasePermissions
7
6
  from camomilla.serializers import (
8
7
  MediaFolderSerializer,
9
8
  MediaListSerializer,
@@ -33,7 +32,6 @@ class MediaFolderViewSet(
33
32
  ):
34
33
  model = MediaFolder
35
34
  serializer_class = MediaFolderSerializer
36
- permission_classes = (CamomillaBasePermissions,)
37
35
  items_per_page = 18
38
36
  search_fields = ["title", "alt_text", "file"]
39
37
 
@@ -85,6 +83,5 @@ class MediaViewSet(
85
83
  ):
86
84
  queryset = Media.objects.all()
87
85
  serializer_class = MediaSerializer
88
- permission_classes = (CamomillaBasePermissions,)
89
86
  model = Media
90
87
  parser_classes = [MultipartJsonParser, JSONParser]
camomilla/views/menus.py CHANGED
@@ -7,7 +7,6 @@ from rest_framework.response import Response
7
7
 
8
8
  from camomilla.models import AbstractPage, Menu
9
9
  from camomilla.models.page import UrlNode
10
- from camomilla.permissions import CamomillaBasePermissions
11
10
  from camomilla.serializers import ContentTypeSerializer, MenuSerializer
12
11
  from camomilla.serializers.page import UrlNodeSerializer
13
12
  from camomilla.views.base import BaseModelViewset
@@ -17,7 +16,6 @@ from camomilla.views.decorators import active_lang
17
16
  class MenuViewSet(BaseModelViewset):
18
17
  queryset = Menu.objects.all()
19
18
  serializer_class = MenuSerializer
20
- permission_classes = (CamomillaBasePermissions,)
21
19
  model = Menu
22
20
 
23
21
  lookup_field = "key"
camomilla/views/pages.py CHANGED
@@ -1,26 +1,27 @@
1
1
  from camomilla.models import Page
2
2
  from camomilla.models.page import UrlNode, UrlRedirect
3
- from camomilla.permissions import CamomillaBasePermissions
4
3
  from camomilla.serializers import PageSerializer
5
4
  from camomilla.serializers.page import RouteSerializer
5
+ from camomilla.utils.translation import url_lang_decompose
6
6
  from camomilla.views.base import BaseModelViewset
7
- from camomilla.views.decorators import active_lang
7
+ from camomilla.views.decorators import staff_excluded_cache
8
8
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
9
9
  from rest_framework.decorators import api_view, permission_classes
10
10
  from rest_framework.response import Response
11
11
  from rest_framework import permissions
12
+ from django.utils.translation.trans_real import activate as activate_language
12
13
  from django.shortcuts import get_object_or_404
14
+ from camomilla.settings import PAGE_ROUTER_CACHE
13
15
 
14
16
 
15
17
  class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
16
18
  queryset = Page.objects.all()
17
19
  serializer_class = PageSerializer
18
- permission_classes = (CamomillaBasePermissions,)
19
20
  model = Page
20
21
 
21
22
 
22
- @active_lang()
23
23
  @api_view(["GET"])
24
+ @staff_excluded_cache(PAGE_ROUTER_CACHE)
24
25
  @permission_classes(
25
26
  [
26
27
  permissions.AllowAny,
@@ -31,5 +32,9 @@ def pages_router(request, permalink=""):
31
32
  if redirect:
32
33
  redirect = redirect.redirect()
33
34
  return Response({"redirect": redirect.url, "status": redirect.status_code})
34
- node: UrlNode = get_object_or_404(UrlNode, permalink=f"/{permalink}")
35
+ url_decomposition = url_lang_decompose(permalink)
36
+ if not url_decomposition["permalink"].startswith("/"):
37
+ url_decomposition["permalink"] = f"/{url_decomposition['permalink']}"
38
+ activate_language(url_decomposition["language"])
39
+ node: UrlNode = get_object_or_404(UrlNode, permalink=url_decomposition["permalink"])
35
40
  return Response(RouteSerializer(node, context={"request": request}).data)
camomilla/views/tags.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from camomilla.models import Tag
2
- from camomilla.permissions import CamomillaBasePermissions
3
2
  from camomilla.serializers import TagSerializer
4
3
  from camomilla.views.base import BaseModelViewset
5
4
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -8,5 +7,4 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
8
7
  class TagViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
9
8
  queryset = Tag.objects.all()
10
9
  serializer_class = TagSerializer
11
- permission_classes = (CamomillaBasePermissions,)
12
10
  model = Tag
camomilla/views/users.py CHANGED
@@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
8
8
  from rest_framework.response import Response
9
9
  from rest_framework.views import APIView
10
10
 
11
- from camomilla.permissions import CamomillaBasePermissions, CamomillaSuperUser, ReadOnly
11
+ from camomilla.permissions import CamomillaSuperUser, ReadOnly
12
12
  from camomilla.serializers import (
13
13
  PermissionSerializer,
14
14
  UserProfileSerializer,
@@ -30,7 +30,6 @@ class UserViewSet(BaseModelViewset):
30
30
  queryset = get_user_model().objects.all()
31
31
  serializer_class = UserSerializer
32
32
  model = get_user_model()
33
- permission_classes = (CamomillaSuperUser | CamomillaBasePermissions,)
34
33
 
35
34
  @action(detail=False, methods=["get", "put"], permission_classes=(IsAuthenticated,))
36
35
  def current(self, request):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-camomilla-cms
3
- Version: 6.1.4
3
+ Version: 6.2.0
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- camomilla/__init__.py,sha256=_e972NJVuSoS9uhtizioaade6GDYyEUfJcitCrE429g,243
1
+ camomilla/__init__.py,sha256=jnnn2SfWq4nCc_c1cvq46CUVnXuYhwFEE7eQI9DkJ1s,243
2
2
  camomilla/apps.py,sha256=eUwb9ynyiRAc5OXgt7ZsAdhsCOnPCpNdIFYMheNeN-o,532
3
3
  camomilla/authentication.py,sha256=jz6tQT4PPEu-_JLox1LZrOy7EiWBb9MWaObK63MJGus,855
4
4
  camomilla/context_processors.py,sha256=cGowjDZ-oDGYn1j2Pj5QDGCqnzXAOdOwp5dmzin_FTc,165
@@ -9,9 +9,9 @@ camomilla/model_api.py,sha256=-7l3fc2eN1itCMzkWA8nFaQXMmz0vs7IlGlShF-gSuo,2487
9
9
  camomilla/parsers.py,sha256=fL8XGCGPxJIZNZkPdGtnPSbDP-6-yzGOCVMuLPjkx9Y,1975
10
10
  camomilla/permissions.py,sha256=9NlBO4JMmg36vXCUjPNyq6uZxhkdrnXyIbJVLtWhGWE,1813
11
11
  camomilla/redirects.py,sha256=ilcyHidb5Iw3jTrXMnPntr50kkl_WB3QOB0VNkIxP7A,263
12
- camomilla/settings.py,sha256=lEQPysEJZ_0c9Bkr1P38rO2csPrcNCdqeLqGp1aMS2I,4362
12
+ camomilla/settings.py,sha256=g8XZQ2hmqOeUhh4LmHggbPZsqPcf5hdFqsHhWnykiNc,4565
13
13
  camomilla/sitemap.py,sha256=U2t5TwhB_-sEscmQZ69PZ5st3bIap8NRxzWEvCgB130,786
14
- camomilla/translation.py,sha256=_QyfTlKG6hQ_ClRfxzeJ-3oI3Nu5peJN9xFkO9Ib3As,1316
14
+ camomilla/translation.py,sha256=8O9WlnkwboJZBWovS2ShWr3yA6--9HnoklLfmfX8ing,1336
15
15
  camomilla/urls.py,sha256=umWlVDJ_J4aPkqeSbHJznrGJi0marvWi8TfSmsRT5a0,2101
16
16
  camomilla/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  camomilla/contrib/modeltranslation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -22,14 +22,14 @@ camomilla/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
22
22
  camomilla/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  camomilla/management/commands/regenerate_thumbnails.py,sha256=pKToASR8p8TJezGpFfuylsAHtriNueJ7xqJJxq55adY,496
24
24
  camomilla/managers/__init__.py,sha256=Zwp28E1RKafIl0FqcUi1YNHxF19IsMIvhlhS-Njg9Mw,60
25
- camomilla/managers/pages.py,sha256=DYV1i5vPgxn1YzJlY3kSlsVCRgsiojLxjs2O0LylxEI,3925
25
+ camomilla/managers/pages.py,sha256=z0bLHDwbQby2PoHx_eTm1ClCdwFun-wVArncNPWxYgI,4947
26
26
  camomilla/models/__init__.py,sha256=y7Q34AGhQ1weJUKWb6XjiyUbwRnJeylOBGMErU0wqYg,147
27
27
  camomilla/models/article.py,sha256=LgkZgRsubtDV6NwBz8E2bIgKD6H3I-1QLAxEan5TYYs,1139
28
28
  camomilla/models/content.py,sha256=mIgtifb_WMIt58we5u6qWZemHvuDN1zZaBeCyzHL78A,956
29
29
  camomilla/models/media.py,sha256=pD-qldiHDOOHgux4lsivQLBcOJJrRx3a4Bg8ODNx7r0,6852
30
30
  camomilla/models/menu.py,sha256=SSIjnrGreDqN7sfh4LSu2Ek6d-DPv8Dt7fAOZoGIQN4,4268
31
- camomilla/models/page.py,sha256=ihxY4r_yKL_yE7DJM-U-BDuHzukMmXc2Bk8hTuVrwoA,20007
32
- camomilla/models/mixins/__init__.py,sha256=c2NixqvrIX4E9WGRqQbylXlqBWDXEqN9mzs_dpB0hFQ,1248
31
+ camomilla/models/page.py,sha256=4ofCugWL_RGuXRP4UCLtSxUtlM3q5LmF8QEWBOimqBw,20048
32
+ camomilla/models/mixins/__init__.py,sha256=sV8KTOSysmbbhXgiKE2NniZIjzovZoowjq5AFh6FNfQ,1294
33
33
  camomilla/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  camomilla/openapi/schema.py,sha256=C22dhKjaJ2DTK4KWFjyMJXiwe8NLy7ZTW5d-I1dqZ7g,2546
35
35
  camomilla/serializers/__init__.py,sha256=8v1GsJ_YZ6T72VnKBb5l-8K93oaLf4PIsMt-yFtK-Gk,176
@@ -54,16 +54,16 @@ camomilla/serializers/mixins/nesting.py,sha256=gCEU2UE_Y8e4VRnvT0AExFgwTfJm_jnSq
54
54
  camomilla/serializers/mixins/optimize.py,sha256=zAtbtk6kfGq9FnapqI8tVYOuMKd1IkHbAV6LffL61Z4,3845
55
55
  camomilla/serializers/mixins/ordering.py,sha256=rXQOz47_U4IsMT6IBhySghcmJWMZgpPWHovDcZQG88k,1172
56
56
  camomilla/serializers/mixins/page.py,sha256=-8f70L0yVJyR_06fOFlxKxKnwvJUkBJqW38-SjwYekI,2231
57
- camomilla/serializers/mixins/translation.py,sha256=Om2UT2EB4Xvp8SrIEvraY0kJXR9H54AsYBr9DKjsay8,4323
57
+ camomilla/serializers/mixins/translation.py,sha256=L4nSYWjLP8X0JLo44TUv9hcqtpsCqt5yHy7ZZB-XE9s,4269
58
58
  camomilla/storages/__init__.py,sha256=ytGgX59Ar8vFfYz7eV8z-j5yO_5FqxdZM25iyLnJuSA,131
59
59
  camomilla/storages/default.py,sha256=GNzvV_JZpXMcfTkyXjw5CfK8EIBi3o-NXYBO0KAxD5M,351
60
60
  camomilla/storages/optimize.py,sha256=VGSXZigzZC8LnPTqyTOpPA2Ba9EJB_KC5bcACoRs4GA,2762
61
61
  camomilla/storages/overwrite.py,sha256=jvW3zHvXNzH9dIjeZmmfXo_O3K1ZQmLQzmlSKAOE8ZA,360
62
62
  camomilla/templates/admin/camomilla/page/change_form.html,sha256=ig7rRUtylDZMINBQuVPpZLmeB4sOTV_VtqnTgzAyxEo,251
63
- camomilla/templates/defaults/base.html,sha256=C2gCnQP1AkERPv5w00nVhZdfg2h_8DMoIkmdf2I-VVY,8166
63
+ camomilla/templates/defaults/base.html,sha256=HjP__4XgzootLjbpTRXwQunHu3hwkzBRFpYfko87of0,8219
64
64
  camomilla/templates/defaults/articles/default.html,sha256=1f89jBvNtTa1mPAbC91yy8CzeAjTWO3hhQsTuQW5OKg,239
65
65
  camomilla/templates/defaults/pages/default.html,sha256=bP81Qb6M56I-fBJMywWwEu_cnERtWIX28UkGrUSRU6M,144
66
- camomilla/templates/defaults/parts/langswitch.html,sha256=AkaQzb2KNjRYCMLUn_jE31V36rwBIwp4MneirWPiBcI,3424
66
+ camomilla/templates/defaults/parts/langswitch.html,sha256=Cr-njWdtFxiJmWCq9_ZWToCFzJMKk7Iv2xw8BAHavrU,3808
67
67
  camomilla/templates/defaults/parts/menu.html,sha256=ReE-FfmfCNuNkJI33QqIfmMgLSBl3FTkWAhEa59aD3A,381
68
68
  camomilla/templates/defaults/widgets/media_select_multiple.html,sha256=k2XYou8KkPuFLnPMkPJAFJ-zGJj2Xvu6R3ZmiKa3g7Q,3727
69
69
  camomilla/templates_context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -73,10 +73,10 @@ camomilla/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
73
73
  camomilla/templatetags/camomilla_filters.py,sha256=35x0-cWrRHeLhqypSLlJzEFY_fQDcRHiwZQpFgIsspE,692
74
74
  camomilla/templatetags/menus.py,sha256=7fc4f9DDqtqG6wNb5_Q0km-fq0mqvGnbpR21qO1TJUw,960
75
75
  camomilla/templatetags/model_extras.py,sha256=6WfVDYP_OfuVJd3cNGNA55Wj9uWdrbfOZQ0ua9Xt_vc,2257
76
- camomilla/theme/__init__.py,sha256=EYwTVFPyFoL0qRFRa0edvPWfX2ajYnM9APdhVxj3rU4,22
76
+ camomilla/theme/__init__.py,sha256=5H_IrrHNTfYkLbC3nRLonK3TwkV0TTf0xGbx1a13Scw,22
77
77
  camomilla/theme/apps.py,sha256=Ue2H80fbFgxkQyHeU2H0fWs9Y6d-EnHYv4zz824FSRk,1066
78
78
  camomilla/theme/admin/__init__.py,sha256=TALAZaE-gWshSeGc6yy7VahdX5UfeCeoOE9Q5kJCEpM,2270
79
- camomilla/theme/admin/pages.py,sha256=HHi8dxjqffjMQGIRpOVNgQZhtKSSx3pFIctbqoZ9J2I,2614
79
+ camomilla/theme/admin/pages.py,sha256=VCsT5zTWFBba4LDWEJWPW5cLQTU3HUQyu6eUtJVwDoQ,2630
80
80
  camomilla/theme/admin/translations.py,sha256=iAjGM1A1aYrsz1FpeybROk6rn3Ddl_oUCwgU5oD8nSw,308
81
81
  camomilla/theme/static/admin/css/responsive.css,sha256=yGq6qXrr8xEVsXTnprIBgkX-sMGZrNf0Kkh-xDxf6yE,157
82
82
  camomilla/theme/static/admin/img/favicon.ico,sha256=qpKv_2MaGILvyihnD1Vq9Yk-ZXGkxWTW26ciMeBFMYU,15406
@@ -90,17 +90,17 @@ camomilla/utils/query_parser.py,sha256=TUScPzPVVJzaKdqy5NqtMOft3H5Bx6liXTVPM1yjH
90
90
  camomilla/utils/seo.py,sha256=8p_a_TGgohenpJb094tT4mMxbn2xzW0qDILuTnjNocM,3324
91
91
  camomilla/utils/setters.py,sha256=LV57SM65rL1_ZQkVzk9al_Q13lndVywXLkqgfIvgS0Y,915
92
92
  camomilla/utils/templates.py,sha256=NAvvuv-fwu9CIxQY5t0RKs4GiFClOBZa9pOtcH_YP6s,1576
93
- camomilla/utils/translation.py,sha256=w5tvTInDLegWBb1TnDWo09ckKY3K6hajuNNsngZIxPQ,4205
93
+ camomilla/utils/translation.py,sha256=i5F8MY7fUB9ImzxcgwyY-Eci-nlIHxXlerJIRbgFiXw,4133
94
94
  camomilla/views/__init__.py,sha256=94QuOnnbfMMb17mruO2ydUt286-8zBmDxEPWrJv5Wog,178
95
- camomilla/views/articles.py,sha256=qGxebOA5iTbGGe9PfbH40YBoDPKktH8FJongg6rh2R8,571
96
- camomilla/views/contents.py,sha256=JxvnmgeK8JEmCMLzVG8pVq2DwvmjXtgnIdsDnn74tA4,1205
97
- camomilla/views/decorators.py,sha256=hR--nTGQn2mMKDrWn-0Ildzbsvp11OfoWAtedKEzmiA,982
95
+ camomilla/views/articles.py,sha256=mHqNP9l5_C_7E_2mqK7HQsmb6UpD06LJRJ2udhG0CBI,459
96
+ camomilla/views/contents.py,sha256=TT8p1zj4RhY9PzougpBVhQX3IM6oxL3bTJ3MX5why_8,1093
97
+ camomilla/views/decorators.py,sha256=w8gjns515nPGsr9TJezKC3EFOURWyfxvgJEsRpmDai0,1636
98
98
  camomilla/views/languages.py,sha256=Rt_X7s3dbDBv4dxsQ9fnav_u0TAzzo8fGKBBx3esDsg,441
99
- camomilla/views/medias.py,sha256=XYa-NTLLQmSSynpfrFT3av-K_r59aRns3dTGfYMj-0Q,3002
100
- camomilla/views/menus.py,sha256=2uEeQc68wmmxiQ-mEG9wj1m18uF9f4oq-_5anAVOIbw,3449
101
- camomilla/views/pages.py,sha256=GwLRcyrLqL3g4BFcbGQ2b0yspA-osA092zTMgVys56M,1345
102
- camomilla/views/tags.py,sha256=XcYRlcBFSPPY32lt7POb6fWPJL_8HsTo5JcHcAOiOKw,479
103
- camomilla/views/users.py,sha256=_fvsKOEtep4SJLvMva2_q-HdLQT_1KlFNt4wcl3xCJk,3130
99
+ camomilla/views/medias.py,sha256=ljfzwdU-GNV1ybeGMr224qzXE8l012fgC9-aTpwKdyg,2837
100
+ camomilla/views/menus.py,sha256=K2r43fiezUggGn-MkclOH0VTdanCpGh4LBqb-YaHxQk,3337
101
+ camomilla/views/pages.py,sha256=QZdb6Z6QZzj7_nHkJGckywyg9-xlvl1hyG3JcZfLe7Y,1713
102
+ camomilla/views/tags.py,sha256=Knivz6dOwu54oas7SAwK5mFJdGo10fn8OTC3bVZgJk8,367
103
+ camomilla/views/users.py,sha256=ctdlIvNsE9wGpaB7CCcWl1zPk7tTjB6lvYEDQxg30mU,3030
104
104
  camomilla/views/base/__init__.py,sha256=bpbVBGXLTy7No95XyDNB2U8hVXmwQJrF1VjLAS5WH90,1232
105
105
  camomilla/views/mixins/__init__.py,sha256=Znv3fLYVy6lgu03Q_D8fTen4zMxI6VSRaLPDU8Cp7Ws,473
106
106
  camomilla/views/mixins/bulk_actions.py,sha256=i0duWW6wey9m7I_V8-gPcHsbJyPEfSdMdj4h2i-CbPw,787
@@ -109,28 +109,33 @@ camomilla/views/mixins/optimize.py,sha256=Y-jMYmScLeY7ZnDS1K9o8aXSgTGc5RrYUh66r5
109
109
  camomilla/views/mixins/ordering.py,sha256=mh7fqPyVCVJh84Nl2pYFQouzGxa-ANF3Wqv0pCb7OVU,4779
110
110
  camomilla/views/mixins/pagination.py,sha256=0haXRJNFrawCRSe4kNd4EWHsuhBAjdeqCklGJ3SD2gw,5950
111
111
  camomilla/views/mixins/permissions.py,sha256=TPmR3Hoa3BjeJu9rCE_7lpLOAupue4WI42C21HTo6X4,200
112
- django_camomilla_cms-6.1.4.dist-info/licenses/LICENSE,sha256=kVS7zDrNkav2hLLXbOJwVdonY2ToApTK3khyJagGQoQ,1063
112
+ django_camomilla_cms-6.2.0.dist-info/licenses/LICENSE,sha256=kVS7zDrNkav2hLLXbOJwVdonY2ToApTK3khyJagGQoQ,1063
113
113
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
- tests/test_admin_page_form.py,sha256=DRJxekEKeYMCf9q62ax7iBeJGhEP2NZ8o02krZBR_DA,1971
114
+ tests/test_admin_page_form.py,sha256=nvaSJjfy8yEZt6J0ycOrMKfMgy6bHHFrKdHtWLAEf2E,1971
115
115
  tests/test_api.py,sha256=t03EFDezGgm4UJl8RIVvnTUkAGTB6ptm0G2lHBQ7ljc,1833
116
- tests/test_camomilla_filters.py,sha256=rUK8iEyUNH28uTd8Ac13OikiH4Xi-V4wbtS-XjJoG24,1580
116
+ tests/test_camomilla_filters.py,sha256=SQR5Q_9ORoKGbuD8WRdBOvbyojQENNb72eP6TlrCefM,1564
117
117
  tests/test_media.py,sha256=n5PQmpMvw-a2epmq_60R0X-vVOa4BeyKMZkguGJzKa0,5243
118
118
  tests/test_menu.py,sha256=hrTikgXNnry1bS-t5K7UGYreEJ3m-FU0r4pduumwTd4,3791
119
119
  tests/test_model_api.py,sha256=ml3OlLuBfcnr2EMjwQLvVDPT2adSQ7WS4IxxXKD1InU,4121
120
- tests/test_model_api_permissions.py,sha256=lUlcYAOasFFQjMFnqhGXlXriCH-f6xdYSCn1Gqf1eSU,1838
120
+ tests/test_model_api_permissions.py,sha256=1HCagpyUWQMiAJ8L0w2N2EKDqlb1ZvpzGCtd03H0Je4,2114
121
121
  tests/test_model_api_register.py,sha256=txKaVTGt-DGrmI-6xcUEluPd7ArNi80VvlqBVXdH8zk,13555
122
122
  tests/test_models.py,sha256=WJs8lxWZWn1l7X3a_QFVc8fF5LHTsI8bc3uhQe6-o-Q,684
123
123
  tests/test_page_meta.py,sha256=QFmX97LBYSuHn9vJPM80MWZc3BwfGO5V-sCClk7ExOA,3142
124
124
  tests/test_page_relation_api.py,sha256=PrFY3vuuFal4og8MUq8ba02qITsNOtIP1eNmQ_f9jxk,2760
125
- tests/test_pages.py,sha256=Zo1dV6Ie2hPPfc2V7uPbVW9gEtuBWOGypglaVIbTG98,11885
125
+ tests/test_pages.py,sha256=bwAu-obgL_VZmA1PC3CnlcT2IvAeNuwUZfxm4pz9RYc,12503
126
+ tests/test_parsers.py,sha256=3dSBxHRAVLtM_GJdBsVCLaOjW1UDDDtQx9zLnHITmg0,2618
126
127
  tests/test_query_parser.py,sha256=R9l0L2QDEDcm2b6IFUhyf7wMXLzL9RySLkzKTWRtBkE,2097
128
+ tests/test_sitemap.py,sha256=MWkossQKdCM6s38ORHTWjPqWnbpXzrQ700indpiIwRU,663
127
129
  tests/test_templates_context.py,sha256=zGdmbQMGNXB2V_15BaQDIgqFMnVjBAw969n1tu3m7HY,5626
128
130
  tests/test_utils.py,sha256=ow4csGfU5WzMgAT5zWjZIxZwW1-BqnMduDt8hOzf9cE,2166
131
+ tests/test_utils_getters.py,sha256=oTgtmoxJBmm5sn76AP6fkgE-fDYwuHOVDwrTQeUYxwI,1263
132
+ tests/test_utils_normalization.py,sha256=gXHtGD6iKnk1Hqr3GmFfRFqGCfD5U7q2KIqVz4WPFnY,899
133
+ tests/test_utils_setters.py,sha256=0HimIaSz5IJES6Ya1Ep8lFo13CyVD_M52DANOf73nUY,1523
129
134
  tests/fixtures/__init__.py,sha256=ixyA6ZsmYbiKEsjQGOGoG4KyJmwWrf-qeoQjQG3J66U,426
130
135
  tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
136
  tests/utils/api.py,sha256=TYcDXeILHtBwzwG0acwPFmiqMZnlF9VnLB0Ydhg55vA,865
132
137
  tests/utils/media.py,sha256=-cnrQzzVuhNSb5rT5xMUs5f3yYpBnS0fVGDcjgsb8lw,291
133
- django_camomilla_cms-6.1.4.dist-info/METADATA,sha256=x30vUkA5zxm0eY9plcflZzK8e3LqBx7odwLC59UOMI4,5652
134
- django_camomilla_cms-6.1.4.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
135
- django_camomilla_cms-6.1.4.dist-info/top_level.txt,sha256=G9VIGBmMMqC7JEckoTgXKmC6T2BR75QRkqRnngw1_lo,16
136
- django_camomilla_cms-6.1.4.dist-info/RECORD,,
138
+ django_camomilla_cms-6.2.0.dist-info/METADATA,sha256=kasKQOklWZ6NosfiFKEUztsujYne0E-1aLzAqEuWi3Q,5652
139
+ django_camomilla_cms-6.2.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
140
+ django_camomilla_cms-6.2.0.dist-info/top_level.txt,sha256=G9VIGBmMMqC7JEckoTgXKmC6T2BR75QRkqRnngw1_lo,16
141
+ django_camomilla_cms-6.2.0.dist-info/RECORD,,
@@ -36,7 +36,7 @@ class AdminPageFormTestCase(TestCase):
36
36
 
37
37
  page_admin = PageAdmin(Page, AdminSite())
38
38
  form = page_admin.get_form(request)()
39
- self.assertEqual(len(list(form.fields)), 33)
39
+ self.assertEqual(len(list(form.fields)), 35)
40
40
  self.assertTrue("template" in list(form.fields))
41
41
  self.assertListEqual(
42
42
  form.fields["template"].widget.choices,
@@ -54,7 +54,7 @@ class AdminPageFormTestCase(TestCase):
54
54
 
55
55
  page_admin = PageAdmin(Page, AdminSite())
56
56
  form = page_admin.get_form(request_with_cookies)()
57
- self.assertEqual(len(list(form.fields)), 33)
57
+ self.assertEqual(len(list(form.fields)), 35)
58
58
  self.assertTrue("template" in list(form.fields))
59
59
  self.assertListEqual(
60
60
  form.fields["template"].widget.choices,
@@ -42,4 +42,4 @@ class CamomillaFiltersTestCase(TestCase):
42
42
  request.META["HTTP_HOST"] = "localhost"
43
43
  page = Page.get(request)
44
44
  alt_urls = dict(alternate_urls(page, request))
45
- self.assertEqual(alt_urls, {"it": None, "en": "/path/"})
45
+ self.assertEqual(alt_urls, {"it": None})
@@ -9,14 +9,20 @@ client = APIClient()
9
9
  def test_right_permissions():
10
10
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
11
11
  assert response.status_code == 401
12
+ response = client.get("/api/models/test-model/")
13
+ assert response.status_code == 401
12
14
  token = login_user()
13
15
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
14
16
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
15
17
  assert response.status_code == 403
18
+ response = client.get("/api/models/test-model/")
19
+ assert response.status_code == 200
16
20
  token = login_staff()
17
21
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
18
22
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
19
23
  assert response.status_code == 403
24
+ response = client.get("/api/models/test-model/")
25
+ assert response.status_code == 200
20
26
  token = login_superuser()
21
27
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
22
28
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
tests/test_pages.py CHANGED
@@ -347,3 +347,21 @@ class PagesTestCase(TestCase):
347
347
  response = self.client.get("/it/permalink_6_it/")
348
348
  assert response.status_code == 301
349
349
  assert response.url == "/it/permalink_6_it_changed/"
350
+
351
+ @pytest.mark.django_db
352
+ def test_page_keywords(self):
353
+ # Create page with keywords field and check it's given back as expected
354
+ response = self.client.post(
355
+ "/api/camomilla/pages/",
356
+ {
357
+ "og_description_it" : "Keywords Test",
358
+ "keywords_it" : ['key1', 'key2']
359
+ },
360
+ format="json",
361
+ )
362
+
363
+ assert response.status_code == 201
364
+ assert len(Page.objects.all()) == 1
365
+ page = Page.objects.first()
366
+ assert page.og_description_it == "Keywords Test"
367
+ assert page.keywords_it == ['key1', 'key2']
tests/test_parsers.py ADDED
@@ -0,0 +1,62 @@
1
+ import io
2
+ import json
3
+ import pytest
4
+ from django.conf import settings
5
+ from django.test import RequestFactory
6
+ from django.core.files.uploadedfile import SimpleUploadedFile
7
+ from camomilla.parsers import MultipartJsonParser
8
+
9
+ @pytest.mark.django_db
10
+ def test_multipart_json_parser_parses_json_and_files(monkeypatch):
11
+ # Prepare multipart data
12
+ json_data = json.dumps({"foo": "bar", "nested": {"baz": 1}, "nested_list": [{"qux": "quux"}, {"corge": "grault"}]})
13
+ file_content = b"filecontent"
14
+ upload = SimpleUploadedFile("test.txt", file_content, content_type="text/plain")
15
+
16
+ # Simulate multipart POST
17
+ factory = RequestFactory()
18
+ data = {
19
+ "data": json_data,
20
+ "nested.file": upload,
21
+ "nested_list.1.file": upload,
22
+ }
23
+ request = factory.post("/", data)
24
+ request.upload_handlers = []
25
+ request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
26
+
27
+ # Patch DjangoMultiPartParser to return our data
28
+ class DummyParser:
29
+ def __init__(self, *a, **kw): pass
30
+ def parse(self):
31
+ return ( {"data": json_data}, {"nested.file": upload, "nested_list.1.file": upload,},)
32
+ monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
33
+
34
+ parser = MultipartJsonParser()
35
+ parsed = parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
36
+ assert parsed["foo"] == "bar"
37
+ assert parsed["nested"]["baz"] == 1
38
+ assert isinstance(parsed["nested"]["file"], SimpleUploadedFile)
39
+ assert isinstance(parsed["nested_list"][1]["file"], SimpleUploadedFile)
40
+ parsed["nested"]["file"].seek(0)
41
+ assert parsed["nested"]["file"].read() == file_content
42
+ parsed["nested_list"][1]["file"].seek(0)
43
+ assert parsed["nested_list"][1]["file"].read() == file_content
44
+
45
+ @pytest.mark.django_db
46
+ def test_multipart_json_parser_handles_parse_error(monkeypatch):
47
+ factory = RequestFactory()
48
+ request = factory.post("/", {})
49
+ request.upload_handlers = []
50
+ request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
51
+
52
+ class DummyParser:
53
+ def __init__(self, *a, **kw): pass
54
+ def parse(self):
55
+ from django.http.multipartparser import MultiPartParserError
56
+ raise MultiPartParserError("fail")
57
+ monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
58
+ parser = MultipartJsonParser()
59
+ from rest_framework.exceptions import ParseError
60
+ with pytest.raises(ParseError) as exc:
61
+ parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
62
+ assert "Multipart form parse error" in str(exc.value)
tests/test_sitemap.py ADDED
@@ -0,0 +1,16 @@
1
+ import pytest
2
+ from django.urls import reverse
3
+ from django.test import Client
4
+ from camomilla.models.page import Page
5
+
6
+ @pytest.mark.django_db
7
+ def test_sitemap_xml_contains_pages():
8
+ Page.objects.create(title="Test Page 1", permalink="test-page-1", status="PUB", autopermalink=False)
9
+ Page.objects.create(title="Test Page 2", permalink="test-page-2", status="PUB", autopermalink=False)
10
+
11
+ client = Client()
12
+ response = client.get(reverse('django.contrib.sitemaps.views.sitemap'))
13
+ assert response.status_code == 200
14
+ assert b"<urlset" in response.content
15
+ assert b"/test-page-1" in response.content
16
+ assert b"/test-page-2" in response.content
@@ -0,0 +1,41 @@
1
+ from camomilla.utils.getters import safe_getter, pointed_getter, find_and_replace_dict
2
+
3
+ def test_safe_getter_dict():
4
+ d = {'a': 1}
5
+ assert safe_getter(d, 'a') == 1
6
+ assert safe_getter(d, 'b', 42) == 42
7
+
8
+ def test_safe_getter_object():
9
+ class Dummy:
10
+ foo = 'bar'
11
+ obj = Dummy()
12
+ assert safe_getter(obj, 'foo') == 'bar'
13
+ assert safe_getter(obj, 'baz', 99) == 99
14
+
15
+ def test_pointed_getter_dict():
16
+ d = {'a': {'b': {'c': 5}}}
17
+ assert pointed_getter(d, 'a.b.c') == 5
18
+ assert pointed_getter(d, 'a.b.x', 'nope') == 'nope'
19
+
20
+ def test_pointed_getter_object():
21
+ class Dummy:
22
+ pass
23
+ obj = Dummy()
24
+ obj.child = Dummy()
25
+ obj.child.value = 123
26
+ assert pointed_getter(obj, 'child.value') == 123
27
+ assert pointed_getter(obj, 'child.missing', 'default') == 'default'
28
+
29
+ def test_find_and_replace_dict_simple():
30
+ d = {'a': 1, 'b': 2}
31
+ def pred(key, value):
32
+ return value * 2
33
+ result = find_and_replace_dict(d, pred)
34
+ assert result == {'a': 2, 'b': 4}
35
+
36
+ def test_find_and_replace_dict_nested():
37
+ d = {'a': {'b': 2}, 'c': 3}
38
+ def pred(key, value):
39
+ return value if not isinstance(value, int) else value + 1
40
+ result = find_and_replace_dict(d, pred)
41
+ assert result == {'a': {'b': 3}, 'c': 4}
@@ -0,0 +1,37 @@
1
+ from camomilla.utils.normalization import dict_merge
2
+
3
+ def test_dict_merge_simple():
4
+ a = {'x': 1}
5
+ b = {'y': 2}
6
+ result = dict_merge(a.copy(), b)
7
+ assert result == {'x': 1, 'y': 2}
8
+
9
+ def test_dict_merge_overwrite():
10
+ a = {'x': 1}
11
+ b = {'x': 2}
12
+ result = dict_merge(a.copy(), b)
13
+ assert result == {'x': 2}
14
+
15
+ def test_dict_merge_nested():
16
+ a = {'x': {'y': 1}}
17
+ b = {'x': {'z': 2}}
18
+ result = dict_merge(a.copy(), b)
19
+ assert result == {'x': {'y': 1, 'z': 2}}
20
+
21
+ def test_dict_merge_nested_overwrite():
22
+ a = {'x': {'y': 1}}
23
+ b = {'x': {'y': 2}}
24
+ result = dict_merge(a.copy(), b)
25
+ assert result == {'x': {'y': 2}}
26
+
27
+ def test_dict_merge_empty():
28
+ a = {}
29
+ b = {'foo': 1}
30
+ result = dict_merge(a.copy(), b)
31
+ assert result == {'foo': 1}
32
+
33
+ def test_dict_merge_both_empty():
34
+ a = {}
35
+ b = {}
36
+ result = dict_merge(a.copy(), b)
37
+ assert result == {}
@@ -0,0 +1,62 @@
1
+ import pytest
2
+ from camomilla.utils import setters
3
+
4
+ def test_set_key_dict():
5
+ d = {'a': 1}
6
+ result = setters.set_key(d, 'b', 2)
7
+ assert result['b'] == 2
8
+ assert d['b'] == 2
9
+
10
+ def test_set_key_list():
11
+ l = [1, 2]
12
+ result = setters.set_key(l, 1, 3)
13
+ assert result[1] == 3
14
+ result = setters.set_key(l, 2, 4)
15
+ assert result[2] == 4
16
+
17
+ def test_set_key_object():
18
+ class Dummy:
19
+ pass
20
+ obj = Dummy()
21
+ setters.set_key(obj, 'foo', 'bar')
22
+ assert obj.foo == 'bar'
23
+
24
+ def test_get_key_list():
25
+ l = [10, 20]
26
+ assert setters.get_key(l, 1, 'x') == 20
27
+ assert setters.get_key(l, 5, 'x') == 'x'
28
+
29
+ def test_pointed_setter_dict():
30
+ d = {'a': {'b': 1}}
31
+ setters.pointed_setter(d, 'a.b', 2)
32
+ assert d['a']['b'] == 2
33
+
34
+ def test_pointed_setter_list():
35
+ l = [[0, 1], [2, 3]]
36
+ setters.pointed_setter(l, '1.1', 99)
37
+ assert l[1][1] == 99
38
+
39
+ def test_set_key_list_append():
40
+ l = [1]
41
+ # key out of range, should append
42
+ setters.set_key(l, 2, 42)
43
+ assert l == [1, 42]
44
+
45
+ def test_set_key_invalid_type():
46
+ class Dummy:
47
+ pass
48
+ obj = Dummy()
49
+ # Should set attribute if not dict or list
50
+ setters.set_key(obj, 'bar', 123)
51
+ assert obj.bar == 123
52
+
53
+ def test_get_key_invalid_index():
54
+ l = [1, 2]
55
+ # Out of range index returns default
56
+ assert setters.get_key(l, 10, 'default') == 'default'
57
+
58
+ def test_pointed_setter_new_path():
59
+ d = {}
60
+ # Should create nested dicts
61
+ setters.pointed_setter(d, 'foo.bar.baz', 7)
62
+ assert d['foo']['bar']['baz'] == 7