django-camomilla-cms 5.8.6__py2.py3-none-any.whl → 6.0.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 (143) hide show
  1. camomilla/__init__.py +8 -2
  2. camomilla/apps.py +9 -1
  3. camomilla/context_processors.py +6 -0
  4. camomilla/contrib/modeltranslation/__init__.py +0 -0
  5. camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
  6. camomilla/dynamic_pages_urls.py +33 -0
  7. camomilla/fields/__init__.py +13 -0
  8. camomilla/{fields.py → fields/json.py} +15 -18
  9. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  10. camomilla/managers/__init__.py +3 -0
  11. camomilla/managers/pages.py +116 -0
  12. camomilla/model_api.py +86 -0
  13. camomilla/models/__init__.py +5 -6
  14. camomilla/models/article.py +26 -44
  15. camomilla/models/content.py +8 -15
  16. camomilla/models/media.py +70 -97
  17. camomilla/models/menu.py +106 -0
  18. camomilla/models/mixins/__init__.py +10 -48
  19. camomilla/models/page.py +521 -20
  20. camomilla/openapi/__init__.py +0 -0
  21. camomilla/openapi/schema.py +67 -0
  22. camomilla/parsers.py +0 -1
  23. camomilla/redirects.py +10 -0
  24. camomilla/serializers/__init__.py +2 -0
  25. camomilla/serializers/article.py +5 -10
  26. camomilla/serializers/base/__init__.py +21 -17
  27. camomilla/serializers/content_type.py +17 -0
  28. camomilla/serializers/fields/__init__.py +6 -20
  29. camomilla/serializers/fields/file.py +5 -0
  30. camomilla/serializers/fields/related.py +24 -4
  31. camomilla/serializers/media.py +6 -8
  32. camomilla/serializers/menu.py +17 -0
  33. camomilla/serializers/mixins/__init__.py +23 -187
  34. camomilla/serializers/mixins/fields.py +20 -0
  35. camomilla/serializers/mixins/filter_fields.py +57 -0
  36. camomilla/serializers/mixins/json.py +34 -0
  37. camomilla/serializers/mixins/language.py +32 -0
  38. camomilla/serializers/mixins/nesting.py +35 -0
  39. camomilla/serializers/mixins/optimize.py +91 -0
  40. camomilla/serializers/mixins/ordering.py +34 -0
  41. camomilla/serializers/mixins/page.py +58 -0
  42. camomilla/serializers/mixins/translation.py +103 -0
  43. camomilla/serializers/page.py +53 -4
  44. camomilla/serializers/user.py +5 -4
  45. camomilla/serializers/utils.py +38 -0
  46. camomilla/serializers/validators.py +51 -0
  47. camomilla/settings.py +118 -0
  48. camomilla/sitemap.py +30 -0
  49. camomilla/storages/__init__.py +4 -0
  50. camomilla/storages/default.py +12 -0
  51. camomilla/storages/optimize.py +71 -0
  52. camomilla/{storages.py → storages/overwrite.py} +2 -2
  53. camomilla/templates/admin/camomilla/page/change_form.html +10 -0
  54. camomilla/templates/defaults/articles/default.html +7 -0
  55. camomilla/templates/defaults/base.html +170 -0
  56. camomilla/templates/defaults/pages/default.html +3 -0
  57. camomilla/templates/defaults/parts/langswitch.html +83 -0
  58. camomilla/templates/defaults/parts/menu.html +15 -0
  59. camomilla/templates_context/__init__.py +0 -0
  60. camomilla/templates_context/autodiscover.py +51 -0
  61. camomilla/templates_context/rendering.py +89 -0
  62. camomilla/templatetags/camomilla_filters.py +6 -5
  63. camomilla/templatetags/menus.py +37 -0
  64. camomilla/templatetags/model_extras.py +77 -0
  65. camomilla/theme/__init__.py +1 -1
  66. camomilla/theme/admin/__init__.py +99 -0
  67. camomilla/theme/admin/pages.py +46 -0
  68. camomilla/theme/admin/translations.py +13 -0
  69. camomilla/theme/apps.py +38 -0
  70. camomilla/theme/static/admin/css/responsive.css +5 -1021
  71. camomilla/theme/static/admin/img/favicon.ico +0 -0
  72. camomilla/theme/static/admin/img/logo.svg +31 -0
  73. camomilla/theme/templates/admin/base.html +7 -0
  74. camomilla/theme/templates/rosetta/base.html +196 -0
  75. camomilla/translation.py +61 -0
  76. camomilla/urls.py +38 -17
  77. camomilla/utils/__init__.py +4 -0
  78. camomilla/utils/getters.py +27 -0
  79. camomilla/utils/normalization.py +7 -0
  80. camomilla/utils/query_parser.py +167 -0
  81. camomilla/{utils.py → utils/seo.py} +13 -15
  82. camomilla/utils/setters.py +37 -0
  83. camomilla/utils/templates.py +32 -0
  84. camomilla/utils/translation.py +114 -0
  85. camomilla/views/__init__.py +1 -1
  86. camomilla/views/articles.py +5 -7
  87. camomilla/views/base/__init__.py +35 -5
  88. camomilla/views/contents.py +6 -11
  89. camomilla/views/decorators.py +26 -0
  90. camomilla/views/medias.py +24 -19
  91. camomilla/views/menus.py +81 -0
  92. camomilla/views/mixins/__init__.py +17 -73
  93. camomilla/views/mixins/bulk_actions.py +22 -0
  94. camomilla/views/mixins/language.py +33 -0
  95. camomilla/views/mixins/optimize.py +18 -0
  96. camomilla/views/mixins/ordering.py +2 -2
  97. camomilla/views/mixins/pagination.py +12 -18
  98. camomilla/views/mixins/permissions.py +6 -0
  99. camomilla/views/pages.py +28 -6
  100. camomilla/views/tags.py +5 -6
  101. camomilla/views/users.py +7 -12
  102. django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
  103. django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
  104. {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
  105. tests/fixtures/__init__.py +14 -0
  106. tests/test_api.py +22 -39
  107. tests/test_camomilla_filters.py +11 -13
  108. tests/test_media.py +152 -0
  109. tests/test_menu.py +112 -0
  110. tests/test_model_api.py +113 -0
  111. tests/test_model_api_permissions.py +44 -0
  112. tests/test_model_api_register.py +355 -0
  113. tests/test_pages.py +351 -0
  114. tests/test_query_parser.py +58 -0
  115. tests/test_templates_context.py +149 -0
  116. tests/test_utils.py +64 -64
  117. tests/utils/__init__.py +0 -0
  118. tests/utils/api.py +28 -0
  119. tests/utils/media.py +10 -0
  120. camomilla/admin.py +0 -98
  121. camomilla/migrations/0001_initial.py +0 -577
  122. camomilla/migrations/0002_auto_20200214_1127.py +0 -33
  123. camomilla/migrations/0003_auto_20210130_1610.py +0 -30
  124. camomilla/migrations/0004_auto_20210511_0937.py +0 -25
  125. camomilla/migrations/0005_media_image_props.py +0 -19
  126. camomilla/migrations/0006_auto_20220103_1845.py +0 -35
  127. camomilla/migrations/0007_auto_20220211_1622.py +0 -18
  128. camomilla/migrations/0008_auto_20220309_1616.py +0 -60
  129. camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
  130. camomilla/migrations/0010_auto_20220802_1406.py +0 -83
  131. camomilla/migrations/0011_auto_20220902_1000.py +0 -15
  132. camomilla/models/category.py +0 -25
  133. camomilla/models/tag.py +0 -19
  134. camomilla/theme/static/admin/img/logo.png +0 -0
  135. camomilla/theme/templates/admin/base_site.html +0 -18
  136. camomilla/views/categories.py +0 -13
  137. django_camomilla_cms-5.8.6.dist-info/METADATA +0 -63
  138. django_camomilla_cms-5.8.6.dist-info/RECORD +0 -76
  139. tests/urls.py +0 -21
  140. /camomilla/{migrations → contrib}/__init__.py +0 -0
  141. /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
  142. {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
  143. {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
tests/test_media.py ADDED
@@ -0,0 +1,152 @@
1
+ import pytest
2
+ import json
3
+ from django.test import TestCase
4
+ from camomilla.models import Media
5
+ from .utils.api import login_superuser
6
+ from .utils.media import load_asset_and_remove_media
7
+ from rest_framework.test import APIClient
8
+
9
+ client = APIClient()
10
+
11
+
12
+ class MediaTestCase(TestCase):
13
+ def setUp(self):
14
+ self.client = APIClient()
15
+ token = login_superuser()
16
+ self.client.credentials(HTTP_AUTHORIZATION="Token " + token)
17
+
18
+ @pytest.mark.django_db
19
+ def test_media_api_crud(self):
20
+ # Create media 1
21
+ asset = load_asset_and_remove_media("10595073.png")
22
+ response = self.client.post(
23
+ "/api/camomilla/media/",
24
+ {
25
+ "file": asset,
26
+ "data": json.dumps(
27
+ {
28
+ "translations": {
29
+ "en": {
30
+ "alt_text": "Test 1",
31
+ "title": "Test 1",
32
+ "description": "Test 1",
33
+ }
34
+ }
35
+ }
36
+ ),
37
+ },
38
+ format="multipart",
39
+ )
40
+ assert response.status_code == 201
41
+ assert Media.objects.count() == 1
42
+ media = Media.objects.first()
43
+ assert media.alt_text == "Test 1"
44
+ assert media.title == "Test 1"
45
+ assert media.description == "Test 1"
46
+ assert media.file.name == "10595073.png"
47
+
48
+ # Create media 2
49
+ asset = load_asset_and_remove_media("37059501.png")
50
+ response = self.client.post(
51
+ "/api/camomilla/media/",
52
+ {
53
+ "file": asset,
54
+ "data": json.dumps(
55
+ {
56
+ "translations": {
57
+ "en": {
58
+ "alt_text": "Test 2",
59
+ "title": "Test 2",
60
+ "description": "Test 2",
61
+ }
62
+ }
63
+ }
64
+ ),
65
+ },
66
+ format="multipart",
67
+ )
68
+ assert response.status_code == 201
69
+ assert Media.objects.count() == 2
70
+ media = Media.objects.first() # Ordering in model is descending -pk
71
+ assert media.alt_text == "Test 2"
72
+ assert media.title == "Test 2"
73
+ assert media.description == "Test 2"
74
+ assert media.file.name == "37059501.png"
75
+
76
+ # Read media
77
+ response = self.client.get("/api/camomilla/media/2/")
78
+ assert response.status_code == 200
79
+ assert response.json()["id"] == 2
80
+ assert response.json()["title"] == "Test 2"
81
+ assert response.json()["file"] == "http://testserver/media/37059501.png"
82
+
83
+ # Read medias
84
+ response = self.client.get("/api/camomilla/media/")
85
+ assert response.status_code == 200
86
+ assert response.json()[0]["id"] == 2 # Ordering in model is descending -pk
87
+ assert response.json()[0]["title"] == "Test 2"
88
+ assert response.json()[1]["id"] == 1
89
+ assert response.json()[1]["title"] == "Test 1"
90
+
91
+ # Delete media
92
+ response = self.client.delete("/api/camomilla/media/2/")
93
+ assert response.status_code == 204
94
+ assert len(Media.objects.all()) == 1
95
+ media = Media.objects.last()
96
+ assert media.id == 1
97
+ assert media.title == "Test 1"
98
+
99
+ @pytest.mark.django_db
100
+ def test_media_compression(self):
101
+ asset = load_asset_and_remove_media("Sample-jpg-image-10mb.jpg")
102
+ asset_size = asset.size
103
+ response = self.client.post(
104
+ "/api/camomilla/media/",
105
+ {
106
+ "file": asset,
107
+ "data": json.dumps(
108
+ {
109
+ "translations": {
110
+ "en": {
111
+ "alt_text": "Test",
112
+ "title": "Test",
113
+ "description": "Test",
114
+ }
115
+ }
116
+ }
117
+ ),
118
+ },
119
+ format="multipart",
120
+ )
121
+ assert response.status_code == 201
122
+ assert Media.objects.count() == 1
123
+ media = Media.objects.first()
124
+ assert media.file.size < asset_size
125
+ assert media.file.size < 1000000 # 1MB
126
+
127
+ @pytest.mark.django_db
128
+ def test_inflating_prevent(self):
129
+ asset = load_asset_and_remove_media("optimized.jpg")
130
+ asset_size = asset.size
131
+ response = self.client.post(
132
+ "/api/camomilla/media/",
133
+ {
134
+ "file": asset,
135
+ "data": json.dumps(
136
+ {
137
+ "translations": {
138
+ "en": {
139
+ "alt_text": "Test",
140
+ "title": "Test",
141
+ "description": "Test",
142
+ }
143
+ }
144
+ }
145
+ ),
146
+ },
147
+ format="multipart",
148
+ )
149
+ assert response.status_code == 201
150
+ assert Media.objects.count() == 1
151
+ media = Media.objects.first()
152
+ assert media.file.size < asset_size
tests/test_menu.py ADDED
@@ -0,0 +1,112 @@
1
+ import pytest
2
+ import html
3
+ from django.test import TestCase
4
+ from rest_framework.test import APIClient
5
+ from .utils.api import login_superuser
6
+ from django.template import Template, Context
7
+ from camomilla.models import Menu
8
+
9
+
10
+ class MenuTestCase(TestCase):
11
+ def setUp(self):
12
+ self.client = APIClient()
13
+ token = login_superuser()
14
+ self.client.credentials(HTTP_AUTHORIZATION="Token " + token)
15
+
16
+ def renderTemplate(self, template, context=None):
17
+ return Template("{% load menus %}" + template).render(Context(context))
18
+
19
+ @pytest.mark.django_db
20
+ def test_template_render_menu(self):
21
+ assert self.renderTemplate('{% render_menu "key_1" %}') == "\n\n"
22
+ assert len(Menu.objects.all()) == 1
23
+ menu = Menu.objects.first()
24
+ assert menu.id == 1
25
+ assert menu.key == "key_1"
26
+
27
+ assert self.renderTemplate('{% render_menu "key_2" %}') == "\n\n"
28
+ assert len(Menu.objects.all()) == 2
29
+ menu = Menu.objects.last()
30
+ assert menu.id == 2
31
+ assert menu.key == "key_2"
32
+
33
+ @pytest.mark.django_db
34
+ def test_template_get_menus(self):
35
+ self.renderTemplate('{% render_menu "key_3" %}')
36
+ self.renderTemplate('{% render_menu "key_4" %}')
37
+
38
+ rendered = html.unescape(self.renderTemplate("{% get_menus %}"))
39
+ assert rendered == "{'key_3': <Menu: key_3>, 'key_4': <Menu: key_4>}"
40
+
41
+ rendered = html.unescape(self.renderTemplate('{% get_menus "arg" %}'))
42
+ assert rendered == "{}"
43
+
44
+ rendered = html.unescape(self.renderTemplate('{% get_menus "key_3" %}'))
45
+ assert rendered == "{'key_3': <Menu: key_3>}"
46
+
47
+ menus = 'test "menus" in context'
48
+ rendered = html.unescape(
49
+ self.renderTemplate("{% get_menus %}", {"menus": menus})
50
+ )
51
+ assert rendered == menus
52
+
53
+ @pytest.mark.django_db
54
+ def test_template_get_menu_node_url(self):
55
+ self.renderTemplate('{% render_menu "key_5" %}')
56
+
57
+ menu = Menu.objects.first()
58
+ menu.nodes = [
59
+ {"title": "key_5_node_title", "link": {"static": "key_5_url_static"}}
60
+ ]
61
+ menu.save()
62
+
63
+ rendered = html.unescape(self.renderTemplate('{% render_menu "key_5" %}'))
64
+ assert {'<a href="key_5_url_static">key_5_node_title</a>' in rendered}
65
+
66
+ @pytest.mark.django_db
67
+ def test_menu_custom_template(self):
68
+ self.renderTemplate('{% render_menu "key_6_custom" %}')
69
+
70
+ menu = Menu.objects.first()
71
+ menu.nodes = [
72
+ {"title": "key_6_node_title", "link": {"static": "key_6_url_static"}}
73
+ ]
74
+ menu.save()
75
+
76
+ rendered = html.unescape(
77
+ self.renderTemplate(
78
+ '{% render_menu "key_6_custom" "website/menu_custom.html" %}'
79
+ )
80
+ )
81
+ assert {"This is custom menu: key_6_node_title" in rendered}
82
+
83
+ @pytest.mark.django_db
84
+ def test_menu_in_page_template(self):
85
+ self.renderTemplate('{% render_menu "key_7" %}')
86
+
87
+ response = self.client.post(
88
+ "/api/camomilla/pages/",
89
+ {
90
+ "translations": {
91
+ "en": {
92
+ "title": "title_page_menu_1",
93
+ "permalink": "permalink_page_menu_en_1",
94
+ "autopermalink": False,
95
+ }
96
+ }
97
+ },
98
+ format="json",
99
+ )
100
+ assert response.status_code == 201
101
+
102
+ menu = Menu.objects.first()
103
+ menu.nodes = [
104
+ {
105
+ "title": "key_7_node_title",
106
+ "link": {"page": {"id": 1, "model": "camomilla.page"}},
107
+ }
108
+ ]
109
+ menu.save()
110
+
111
+ rendered = html.unescape(self.renderTemplate('{% render_menu "key_7" %}'))
112
+ assert {'href="permalink_page_menu_en_1"' in rendered}
@@ -0,0 +1,113 @@
1
+ import pytest
2
+ from rest_framework.test import APIClient
3
+ from .fixtures import load_json_fixture
4
+ from .utils.api import login_superuser
5
+ from example.website.models import SimpleRelationModel
6
+
7
+
8
+ client = APIClient()
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def init_test():
13
+ token = login_superuser()
14
+ client.credentials(HTTP_AUTHORIZATION="Token " + token)
15
+ SimpleRelationModel.objects.bulk_create(
16
+ [SimpleRelationModel(name=f"test{i}") for i in range(1, 10)]
17
+ )
18
+
19
+
20
+ @pytest.mark.django_db
21
+ def test_simple_relation_model_api_endpoint():
22
+ response = client.get("/api/models/simple-relation-model/")
23
+ assert response.status_code == 200
24
+ assert len(response.json()) == 9
25
+ response = client.get("/api/models/simple-relation-model/1/")
26
+ assert response.status_code == 200
27
+ assert response.json()["name"] == "test1"
28
+ response = client.patch(
29
+ "/api/models/simple-relation-model/1/", {"name": "updated"}, format="json"
30
+ )
31
+ assert response.status_code == 200
32
+ assert response.json()["name"] == "updated"
33
+ response = client.delete("/api/models/simple-relation-model/1/")
34
+ assert response.status_code == 204
35
+ response = client.get("/api/models/simple-relation-model/")
36
+ assert response.status_code == 200
37
+ assert len(response.json()) == 8
38
+ response = client.get("/api/models/simple-relation-model/1/")
39
+ assert response.status_code == 404
40
+ assert response.json() in [
41
+ {"detail": "Not found."},
42
+ {"detail": "No SimpleRelationModel matches the given query."},
43
+ ]
44
+
45
+
46
+ @pytest.mark.django_db
47
+ def test_test_model_api_endpoint():
48
+ response = client.get("/api/models/test-model/")
49
+ assert response.status_code == 200
50
+ assert response.json() == []
51
+ test_model_data = load_json_fixture("test-model-api.json")
52
+ response = client.post("/api/models/test-model/", test_model_data, format="json")
53
+ assert response.status_code == 201
54
+ assert response.json()["title"] == test_model_data["title"]
55
+ assert (
56
+ response.json()["structured_data"]["name"]
57
+ == test_model_data["structured_data"]["name"]
58
+ )
59
+ assert (
60
+ response.json()["structured_data"]["age"]
61
+ == test_model_data["structured_data"]["age"]
62
+ )
63
+ assert (
64
+ response.json()["structured_data"]["child"]["name"]
65
+ == test_model_data["structured_data"]["child"]["name"]
66
+ )
67
+ assert (
68
+ response.json()["structured_data"]["childs"][0]["name"]
69
+ == test_model_data["structured_data"]["childs"][0]["name"]
70
+ )
71
+ assert (
72
+ response.json()["structured_data"]["fk_field"]["id"]
73
+ == test_model_data["structured_data"]["fk_field"]["id"]
74
+ )
75
+ assert (
76
+ response.json()["structured_data"]["qs_field"][0]["id"]
77
+ == test_model_data["structured_data"]["qs_field"][0]["id"]
78
+ )
79
+ response = client.get("/api/models/test-model/")
80
+ assert response.status_code == 200
81
+ assert len(response.json()) == 1
82
+ response = client.get("/api/models/test-model/1/")
83
+ assert response.status_code == 200
84
+ assert response.json()["title"] == test_model_data["title"]
85
+ assert (
86
+ response.json()["structured_data"]["name"]
87
+ == test_model_data["structured_data"]["name"]
88
+ )
89
+ assert (
90
+ response.json()["structured_data"]["age"]
91
+ == test_model_data["structured_data"]["age"]
92
+ )
93
+ assert (
94
+ response.json()["structured_data"]["child"]["name"]
95
+ == test_model_data["structured_data"]["child"]["name"]
96
+ )
97
+ assert (
98
+ response.json()["structured_data"]["childs"][0]["name"]
99
+ == test_model_data["structured_data"]["childs"][0]["name"]
100
+ )
101
+ assert (
102
+ response.json()["structured_data"]["fk_field"]["id"]
103
+ == test_model_data["structured_data"]["fk_field"]["id"]
104
+ )
105
+ assert (
106
+ response.json()["structured_data"]["qs_field"][0]["id"]
107
+ == test_model_data["structured_data"]["qs_field"][0]["id"]
108
+ )
109
+ response = client.patch(
110
+ "/api/models/test-model/1/", {"title": "updated"}, format="json"
111
+ )
112
+ assert response.status_code == 200
113
+ assert response.json()["title"] == "updated"
@@ -0,0 +1,44 @@
1
+ import pytest
2
+ from rest_framework.test import APIClient
3
+ from .utils.api import login_user, login_superuser, login_staff
4
+
5
+ client = APIClient()
6
+
7
+
8
+ @pytest.mark.django_db
9
+ def test_right_permissions():
10
+ response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
11
+ assert response.status_code == 401
12
+ token = login_user()
13
+ client.credentials(HTTP_AUTHORIZATION="Token " + token)
14
+ response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
15
+ assert response.status_code == 403
16
+ token = login_staff()
17
+ client.credentials(HTTP_AUTHORIZATION="Token " + token)
18
+ response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
19
+ assert response.status_code == 403
20
+ token = login_superuser()
21
+ client.credentials(HTTP_AUTHORIZATION="Token " + token)
22
+ response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
23
+ assert response.status_code == 201
24
+ response = client.get("/api/models/test-model/")
25
+ assert response.status_code == 200
26
+ assert len(response.json()) == 1
27
+ response = client.get("/api/models/test-model/1/")
28
+ assert response.status_code == 200
29
+ response = client.patch(
30
+ "/api/models/test-model/1/", {"title": "updated"}, format="json"
31
+ )
32
+ assert response.status_code == 200
33
+ assert response.json()["title"] == "updated"
34
+ response = client.delete("/api/models/test-model/1/")
35
+ assert response.status_code == 204
36
+ response = client.get("/api/models/test-model/")
37
+ assert response.status_code == 200
38
+ assert len(response.json()) == 0
39
+ response = client.get("/api/models/test-model/1/")
40
+ assert response.status_code == 404
41
+ assert response.json() in [
42
+ {"detail": "Not found."},
43
+ {"detail": "No TestModel matches the given query."},
44
+ ]