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
tests/test_pages.py ADDED
@@ -0,0 +1,343 @@
1
+ import pytest
2
+ from django.test import TestCase
3
+ from rest_framework.test import APIClient
4
+ from .utils.api import login_superuser
5
+ from camomilla.models import Page
6
+ from camomilla.models.page import UrlRedirect
7
+ from datetime import datetime, timedelta
8
+
9
+
10
+ class PagesTestCase(TestCase):
11
+ def setUp(self):
12
+ self.client = APIClient()
13
+ token = login_superuser()
14
+ self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
15
+
16
+ @pytest.mark.django_db
17
+ def test_pages_api_no_access(self):
18
+ client = APIClient()
19
+ response = client.post("/api/camomilla/pages/")
20
+ assert response.status_code == 401
21
+
22
+ @pytest.mark.django_db
23
+ def test_pages_api_crud(self):
24
+ # Create page
25
+ response = self.client.post(
26
+ "/api/camomilla/pages/",
27
+ {
28
+ "translations": {
29
+ "it": {
30
+ "title": "title_page_1",
31
+ }
32
+ }
33
+ },
34
+ format='json'
35
+ )
36
+
37
+ assert response.status_code == 201
38
+ assert len(Page.objects.all()) == 1
39
+ page = Page.objects.first()
40
+ assert page.id == 1
41
+ assert page.title_it == "title_page_1"
42
+ assert page.url_node.id == 1
43
+
44
+ response = self.client.post(
45
+ "/api/camomilla/pages/",
46
+ {
47
+ "title_it": "title_page_2",
48
+ }
49
+ )
50
+
51
+ assert response.status_code == 201
52
+ assert len(Page.objects.all()) == 2
53
+ page = Page.objects.last()
54
+ assert page.id == 2
55
+ assert page.title_it == "title_page_2"
56
+ assert page.url_node.id == 2
57
+
58
+ # Update page
59
+ response = self.client.patch(
60
+ "/api/camomilla/pages/2/",
61
+ {
62
+ "translations": {
63
+ "it": {
64
+ "title": "title_page_2_updated",
65
+ }
66
+ }
67
+ },
68
+ format='json'
69
+ )
70
+
71
+ assert response.status_code == 200
72
+ assert len(Page.objects.all()) == 2
73
+ page = Page.objects.last()
74
+ assert page.id == 2
75
+ assert page.title_it == "title_page_2_updated"
76
+
77
+ # Read page
78
+ response = self.client.get("/api/camomilla/pages/2/")
79
+
80
+ assert response.status_code == 200
81
+ assert response.json()['id'] == 2
82
+ assert response.json()['title'] == "title_page_2_updated"
83
+
84
+ # Read pages
85
+ response = self.client.get("/api/camomilla/pages/")
86
+
87
+ assert response.status_code == 200
88
+ assert response.json()[0]['id'] == 1
89
+ assert response.json()[0]['title'] == "title_page_1"
90
+ assert response.json()[1]['id'] == 2
91
+ assert response.json()[1]['title'] == "title_page_2_updated"
92
+
93
+ # Delete page
94
+ response = self.client.delete("/api/camomilla/pages/2/")
95
+
96
+ assert response.status_code == 204
97
+ assert len(Page.objects.all()) == 1
98
+ page = Page.objects.last()
99
+ assert page.id == 1
100
+ assert page.title_it == "title_page_1"
101
+
102
+ @pytest.mark.django_db
103
+ def test_pages_url_nodes(self):
104
+ # Create page with automatic url creation
105
+ response = self.client.post(
106
+ "/api/camomilla/pages/",
107
+ {
108
+ "title_en": "title_page_1",
109
+ "title_it": "titolo_pagina_1",
110
+ }
111
+ )
112
+
113
+ assert response.status_code == 201
114
+
115
+ # EN automatic url creation
116
+ response = self.client.get("/api/camomilla/pages/1/?language=en")
117
+ assert response.json()['autopermalink'] == True
118
+ assert response.json()['permalink'] == "/title_page_1"
119
+ # IT automatic url creation
120
+ response = self.client.get("/api/camomilla/pages/1/?language=it")
121
+ assert response.json()['autopermalink'] == True
122
+ assert response.json()['permalink'] == "/titolo_pagina_1"
123
+
124
+ # Create page with manual url creation
125
+ response = self.client.post(
126
+ "/api/camomilla/pages/",
127
+ {
128
+ "translations": {
129
+ "it": {
130
+ "title": "titolo_pagina_2",
131
+ "permalink": "permalink_manuale_it_2",
132
+ "autopermalink": False
133
+ },
134
+ "en": {
135
+ "title": "title_page_2",
136
+ "permalink": "permalink_manual_en_2",
137
+ "autopermalink": False
138
+ }
139
+ }
140
+ },
141
+ format='json'
142
+ )
143
+ assert response.status_code == 201
144
+
145
+ # EN manual url creation
146
+ response = self.client.get("/api/camomilla/pages/2/?language=en")
147
+ assert response.json()['autopermalink'] == False
148
+ assert response.json()['permalink'] == "/permalink_manual_en_2"
149
+ # IT manual url creation
150
+ response = self.client.get("/api/camomilla/pages/2/?language=it")
151
+ assert response.json()['autopermalink'] == False
152
+ assert response.json()['permalink'] == "/permalink_manuale_it_2"
153
+
154
+ # Create page with a parent page with automatic url creation
155
+ response = self.client.post(
156
+ "/api/camomilla/pages/",
157
+ {
158
+ "title_en": "title_page_3",
159
+ "title_it": "titolo_pagina_3",
160
+ "parent_page": 2
161
+ }
162
+ )
163
+ assert response.status_code == 201
164
+
165
+ # EN parent page with automatic url creation
166
+ response = self.client.get("/api/camomilla/pages/3/?language=en")
167
+ assert response.json()['autopermalink'] == True
168
+ assert response.json()['permalink'] == "//permalink_manual_en_2/title_page_3"
169
+ # IT parent page with automatic url creation
170
+ response = self.client.get("/api/camomilla/pages/3/?language=it")
171
+ assert response.json()['autopermalink'] == True
172
+ assert response.json()['permalink'] == "//permalink_manuale_it_2/titolo_pagina_3"
173
+
174
+ # Check url uniqueness and consistency EN
175
+ response = self.client.post(
176
+ "/api/camomilla/pages/",
177
+ {
178
+ "autopermalink_en": False,
179
+ "permalink_en": "permalink_manual_en_2",
180
+ }
181
+ )
182
+
183
+ # Client error when url check uniqueness and consistency fail
184
+ assert response.status_code == 400
185
+ assert response.data['permalink_en'][0] == "There is an other page with same permalink."
186
+
187
+ # Check url uniqueness and consistency IT
188
+ response = self.client.post(
189
+ "/api/camomilla/pages/",
190
+ {
191
+ "translations": {
192
+ "it": {
193
+ "autopermalink": False,
194
+ "permalink": "permalink_manuale_it_2",
195
+ }
196
+ }
197
+ },
198
+ format='json'
199
+ )
200
+
201
+ # Client error when url check uniqueness and consistency fail
202
+ assert response.status_code == 400
203
+ assert response.data['permalink_it'][0] == "There is an other page with same permalink."
204
+
205
+ @pytest.mark.django_db
206
+ def test_pages_url_nodes_navigation(self):
207
+ #Test the camomilla.dynamic_pages_url handler for navigating and rendering UrlNodes
208
+ self.client.post(
209
+ "/api/camomilla/pages/",
210
+ {
211
+ "autopermalink_en": False,
212
+ "permalink_en": "permalink_4_en",
213
+ "status_en": "PUB",
214
+ "autopermalink_it": False,
215
+ "permalink_it": "permalink_4_it",
216
+ "status_it": "PUB",
217
+ }
218
+ )
219
+
220
+ response = self.client.get("/permalink_4_en/")
221
+ assert response.status_code == 200
222
+ response = self.client.get("/it/permalink_4_it/")
223
+ assert response.status_code == 200
224
+
225
+ #Test draft - published - planned and ?preview=true
226
+ self.client.post(
227
+ "/api/camomilla/pages/",
228
+ {
229
+ "translations": {
230
+ "it": {
231
+ "autopermalink": False,
232
+ "permalink": "permalink_5_it",
233
+ "status": "PLA",
234
+ },
235
+ "en": {
236
+ "autopermalink": False,
237
+ "permalink": "permalink_5_en",
238
+ "status": "DRF",
239
+ }
240
+ }
241
+ },
242
+ format='json'
243
+ )
244
+
245
+ response = self.client.get("/permalink_5_en/")
246
+ assert response.status_code == 404
247
+ response = self.client.get("/permalink_5_en/?preview=true")
248
+ assert response.status_code == 200
249
+ response = self.client.get("/it/permalink_5_it/")
250
+ assert response.status_code == 404
251
+
252
+ self.client.patch(
253
+ "/api/camomilla/pages/2/",
254
+ {
255
+ "publication_date": (datetime.now() - timedelta(1)).strftime('%Y-%m-%d') + " 00:00:00",
256
+ }
257
+ )
258
+
259
+ response = self.client.get("/it/permalink_5_it/")
260
+ assert response.status_code == 200
261
+
262
+ @pytest.mark.django_db
263
+ def test_pages_url_nodes_navigation_redirects(self):
264
+ #Test the camomilla.dynamic_pages_url handler for navigating and rendering UrlNodes
265
+ self.client.post(
266
+ "/api/camomilla/pages/",
267
+ {
268
+ "translations": {
269
+ "it": {
270
+ "autopermalink": False,
271
+ "permalink": "permalink_6_it",
272
+ "status": "PUB",
273
+ },
274
+ "en": {
275
+ "autopermalink": False,
276
+ "permalink": "permalink_6_en",
277
+ "status": "PUB",
278
+ }
279
+ }
280
+ },
281
+ format='json'
282
+ )
283
+
284
+ #EN Insert Moved Permanently Redirect
285
+ url_redirect = UrlRedirect.objects.create(
286
+ language_code='en',
287
+ from_url='/redirecting_1',
288
+ to_url='/redirected_1',
289
+ url_node_id=1
290
+ )
291
+
292
+ response = self.client.get("/redirecting_1/")
293
+ assert response.status_code == 301
294
+ assert response.url == '/redirected_1/'
295
+
296
+ #EN Change to Moved Temporarily Redirect
297
+ url_redirect.permanent = False
298
+ url_redirect.save()
299
+ response = self.client.get("/redirecting_1/")
300
+ assert response.status_code == 302
301
+
302
+
303
+ #IT Insert Moved Permanently Redirect
304
+ url_redirect = UrlRedirect.objects.create(
305
+ language_code='it',
306
+ from_url='/urlreindirizzamento_1',
307
+ to_url='/urlreindirizzato_1',
308
+ url_node_id=1
309
+ )
310
+
311
+ response = self.client.get("/it/urlreindirizzamento_1/")
312
+ assert response.status_code == 301
313
+ assert response.url == '/it/urlreindirizzato_1/'
314
+
315
+ #IT Change to Moved Temporarily Redirect
316
+ url_redirect.permanent = False
317
+ url_redirect.save()
318
+ response = self.client.get("/it/urlreindirizzamento_1/")
319
+ assert response.status_code == 302
320
+
321
+ # Test auto redirect after permalink change
322
+ self.client.patch(
323
+ "/api/camomilla/pages/1/",
324
+ {
325
+ "translations": {
326
+ "it": {
327
+ "permalink": "permalink_6_it_changed",
328
+ },
329
+ "en": {
330
+ "permalink": "permalink_6_en_changed",
331
+ }
332
+ }
333
+ },
334
+ format='json'
335
+ )
336
+
337
+ response = self.client.get("/permalink_6_en/")
338
+ assert response.status_code == 301
339
+ assert response.url == '/permalink_6_en_changed/'
340
+
341
+ response = self.client.get("/it/permalink_6_it/")
342
+ assert response.status_code == 301
343
+ assert response.url == '/it/permalink_6_it_changed/'
@@ -4,6 +4,7 @@ from camomilla.utils.query_parser import (
4
4
  ConditionParser,
5
5
  )
6
6
 
7
+
7
8
  @pytest.mark.parametrize(
8
9
  "query, expected_q",
9
10
  [
@@ -55,5 +56,3 @@ def test_condition_parser(query, expected_q):
55
56
  parser = ConditionParser(query)
56
57
  q_object = parser.parse_to_q()
57
58
  assert q_object.__str__() == expected_q.__str__()
58
-
59
-
@@ -0,0 +1,111 @@
1
+ import pytest
2
+ import json
3
+ import re
4
+ from django.test import TestCase
5
+ from rest_framework.test import APIClient
6
+ from .utils.api import login_superuser
7
+ from .utils.media import load_asset_and_remove_media
8
+
9
+ class TemoplateContextTestCase(TestCase):
10
+ def setUp(self):
11
+ self.client = APIClient()
12
+ token = login_superuser()
13
+ self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
14
+
15
+ @pytest.mark.django_db
16
+ def test_page_context_template_based(self):
17
+ # Create page with custom context template
18
+ response = self.client.post(
19
+ "/api/camomilla/pages/",
20
+ {
21
+ "title_en": "Page custom context template",
22
+ "autopermalink_en": False,
23
+ "permalink_en": "permalink_context_template",
24
+ "template": "website/page_context_template_based.html",
25
+ "status_en": "PUB",
26
+ },
27
+ format="multipart",
28
+ )
29
+ assert response.status_code == 201
30
+
31
+ # Create media for custom context
32
+ asset = load_asset_and_remove_media("10595073.png")
33
+ response = self.client.post(
34
+ "/api/camomilla/media/",
35
+ {
36
+ "file": asset,
37
+ "data": json.dumps({"translations": {"en": {"alt_text": "Test media", "title": "Test media", "description": "Description media"}}}),
38
+ },
39
+ format="multipart",
40
+ )
41
+ assert response.status_code == 201
42
+
43
+ response = self.client.get("/permalink_context_template/")
44
+ assert response.status_code == 200
45
+ assert re.sub(r'[\s+]', '', response.content.decode()) == '<!DOCTYPEhtml><html><body><h1>Titlepageforpagecontexttemplatebased</h1><p>Contentpageforpagecontexttemplatebased</p><ul><li>Testmedia</li></ul></body></html>'
46
+
47
+
48
+ @pytest.mark.django_db
49
+ def test_model_context_template_based(self):
50
+ # Create page with custom context template
51
+ response = self.client.post(
52
+ "/api/camomilla/pages/",
53
+ {
54
+ "title_en": "Page custom context template",
55
+ "autopermalink_en": False,
56
+ "permalink_en": "permalink_context_template",
57
+ "template": "website/page_context_model_based.html",
58
+ "status_en": "PUB",
59
+ },
60
+ format="multipart",
61
+ )
62
+ assert response.status_code == 201
63
+
64
+ # Create media for custom context
65
+ asset = load_asset_and_remove_media("10595073.png")
66
+ response = self.client.post(
67
+ "/api/camomilla/media/",
68
+ {
69
+ "file": asset,
70
+ "data": json.dumps({"translations": {"en": {"alt_text": "Test media", "title": "Test media", "description": "Description media"}}}),
71
+ },
72
+ format="multipart",
73
+ )
74
+ assert response.status_code == 201
75
+
76
+ response = self.client.get("/permalink_context_template/")
77
+ assert response.status_code == 200
78
+ assert re.sub(r'[\s+]', '', response.content.decode()) == '<!DOCTYPEhtml><html><body><h1>Titlepageforpagecontextmodelbased</h1><p>Contentpageforpagecontextmodelbased</p><ul><li>Testmedia</li></ul></body></html>'
79
+
80
+
81
+ @pytest.mark.django_db
82
+ def test_mixed_context_template(self):
83
+ # Create page with custom context template
84
+ response = self.client.post(
85
+ "/api/camomilla/pages/",
86
+ {
87
+ "title_en": "Page custom context template",
88
+ "autopermalink_en": False,
89
+ "permalink_en": "permalink_context_template",
90
+ "template": "website/page_context_mixed.html",
91
+ "status_en": "PUB",
92
+ },
93
+ format="multipart",
94
+ )
95
+ assert response.status_code == 201
96
+
97
+ # Create media for custom context
98
+ asset = load_asset_and_remove_media("10595073.png")
99
+ response = self.client.post(
100
+ "/api/camomilla/media/",
101
+ {
102
+ "file": asset,
103
+ "data": json.dumps({"translations": {"en": {"alt_text": "Test media", "title": "Test media", "description": "Description media"}}}),
104
+ },
105
+ format="multipart",
106
+ )
107
+ assert response.status_code == 201
108
+
109
+ response = self.client.get("/permalink_context_template/")
110
+ assert response.status_code == 200
111
+ assert re.sub(r'[\s+]', '', response.content.decode()) == '<!DOCTYPEhtml><html><body><!--Templatecontext--><h1>Titlepageforpagecontexttemplatebased</h1><p>Contentpageforpagecontexttemplatebased</p><ul><li>Testmedia</li></ul><!--Modelcontext--><h1>Titlepageforpagecontextmodelbased</h1><p>Contentpageforpagecontextmodelbased</p><ul><li>Testmedia</li></ul></body></html>'
tests/utils/api.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from django.contrib.auth.models import User
2
2
  from rest_framework.test import APIClient
3
- from example.website.models import TestModel
4
3
 
5
4
  client = APIClient()
6
5
 
tests/utils/media.py ADDED
@@ -0,0 +1,9 @@
1
+ import os
2
+ from tests.fixtures import load_asset
3
+ from django.conf import settings
4
+
5
+ def load_asset_and_remove_media(filename):
6
+ asset = load_asset(filename)
7
+ if os.path.exists(f"{settings.MEDIA_ROOT}/{filename}"):
8
+ os.remove(f"{settings.MEDIA_ROOT}/{filename}")
9
+ return asset
File without changes
@@ -1,48 +0,0 @@
1
- from pydantic import TypeAdapter, ValidationError
2
- from rest_framework import serializers
3
- from rest_framework.utils import model_meta
4
- from typing import TYPE_CHECKING, Any, Union, Dict, List
5
-
6
- from camomilla.utils.setters import pointed_setter
7
-
8
- if TYPE_CHECKING:
9
- from structured.pydantic.models import BaseModel
10
-
11
-
12
- class StructuredJSONField(serializers.JSONField):
13
- """
14
- This field allows to serialize and deserialize structured data.
15
- """
16
-
17
- schema: Union["BaseModel", TypeAdapter] = None
18
-
19
- def __init__(self, **kwargs):
20
- self.schema = kwargs.pop("schema", None)
21
- super().__init__(**kwargs)
22
-
23
- def bind(self, field_name, parent):
24
- if self.schema is None and isinstance(parent, serializers.ModelSerializer):
25
- info = model_meta.get_field_info(parent.Meta.model)
26
- field = info.fields[field_name]
27
- self.schema = field.schema
28
- self.many = field.many
29
- self.json_schema = field.schema.json_schema()
30
- super().bind(field_name, parent)
31
-
32
- def to_representation(self, instance: Union["BaseModel", List["BaseModel"]]):
33
- if isinstance(instance, list) and self.many:
34
- return super().to_representation(
35
- self.schema.dump_python(instance, exclude_unset=True)
36
- )
37
- return super().to_representation(instance.model_dump(exclude_unset=True))
38
-
39
- def to_internal_value(self, data: Union[list, dict]):
40
- try:
41
- return self.schema.validate_python(super().to_internal_value(data))
42
- except ValidationError as e:
43
- drf_error: Union[list, Dict[str, Any]] = [] if self.many else {}
44
- for error in e.errors():
45
- pointed_setter(
46
- drf_error, ".".join([str(x) for x in error["loc"]]), [error["msg"]]
47
- )
48
- raise serializers.ValidationError(drf_error)