karrio-server-documents 2025.5.2__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.
- karrio/server/documents/__init__.py +0 -0
- karrio/server/documents/admin.py +93 -0
- karrio/server/documents/apps.py +13 -0
- karrio/server/documents/filters.py +14 -0
- karrio/server/documents/generator.py +298 -0
- karrio/server/documents/migrations/0001_initial.py +69 -0
- karrio/server/documents/migrations/0002_alter_documenttemplate_related_objects.py +18 -0
- karrio/server/documents/migrations/0003_rename_related_objects_documenttemplate_related_object.py +18 -0
- karrio/server/documents/migrations/0004_documenttemplate_active.py +18 -0
- karrio/server/documents/migrations/0005_alter_documenttemplate_description_and_more.py +23 -0
- karrio/server/documents/migrations/0006_documenttemplate_metadata.py +25 -0
- karrio/server/documents/migrations/0007_alter_documenttemplate_related_object.py +18 -0
- karrio/server/documents/migrations/0008_documenttemplate_options.py +26 -0
- karrio/server/documents/migrations/__init__.py +0 -0
- karrio/server/documents/models.py +61 -0
- karrio/server/documents/serializers/__init__.py +4 -0
- karrio/server/documents/serializers/base.py +89 -0
- karrio/server/documents/serializers/documents.py +9 -0
- karrio/server/documents/signals.py +29 -0
- karrio/server/documents/tests/__init__.py +6 -0
- karrio/server/documents/tests/test_generator.py +474 -0
- karrio/server/documents/tests/test_templates.py +318 -0
- karrio/server/documents/tests.py +182 -0
- karrio/server/documents/urls.py +11 -0
- karrio/server/documents/utils.py +2486 -0
- karrio/server/documents/views/__init__.py +0 -0
- karrio/server/documents/views/printers.py +223 -0
- karrio/server/documents/views/templates.py +255 -0
- karrio/server/graph/schemas/__init__.py +1 -0
- karrio/server/graph/schemas/documents/__init__.py +46 -0
- karrio/server/graph/schemas/documents/inputs.py +41 -0
- karrio/server/graph/schemas/documents/mutations.py +51 -0
- karrio/server/graph/schemas/documents/types.py +45 -0
- karrio_server_documents-2025.5.2.dist-info/METADATA +19 -0
- karrio_server_documents-2025.5.2.dist-info/RECORD +37 -0
- karrio_server_documents-2025.5.2.dist-info/WHEEL +5 -0
- karrio_server_documents-2025.5.2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import ANY
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
from rest_framework import status
|
|
5
|
+
from karrio.server.core.tests import APITestCase
|
|
6
|
+
from karrio.server.graph.tests.base import GraphTestCase
|
|
7
|
+
from karrio.server.documents.models import DocumentTemplate
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestDocumentTemplatesREST(APITestCase):
|
|
11
|
+
def test_create_document_template(self):
|
|
12
|
+
url = reverse("karrio.server.documents:document-template-list")
|
|
13
|
+
data = DOCUMENT_TEMPLATE_DATA
|
|
14
|
+
|
|
15
|
+
response = self.client.post(url, data)
|
|
16
|
+
response_data = json.loads(response.content)
|
|
17
|
+
|
|
18
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
19
|
+
|
|
20
|
+
# Check individual fields instead of strict dictionary comparison
|
|
21
|
+
self.assertEqual(response_data["name"], "Test Invoice Template")
|
|
22
|
+
self.assertEqual(response_data["slug"], "test_invoice")
|
|
23
|
+
self.assertEqual(response_data["description"], "A test invoice template")
|
|
24
|
+
self.assertEqual(response_data["object_type"], "document-template")
|
|
25
|
+
self.assertEqual(response_data["related_object"], "shipment")
|
|
26
|
+
self.assertEqual(response_data["active"], True)
|
|
27
|
+
self.assertEqual(response_data["metadata"], {"doc_type": "invoice", "version": "1.0"})
|
|
28
|
+
self.assertEqual(response_data["options"], {"page_size": "A4", "orientation": "portrait"})
|
|
29
|
+
|
|
30
|
+
# Check that ID field exists
|
|
31
|
+
self.assertIn("id", response_data)
|
|
32
|
+
|
|
33
|
+
def test_list_document_templates(self):
|
|
34
|
+
# Create a template first
|
|
35
|
+
DocumentTemplate.objects.create(
|
|
36
|
+
**{
|
|
37
|
+
"name": "Test Template",
|
|
38
|
+
"slug": "test_template",
|
|
39
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
40
|
+
"description": "A test template",
|
|
41
|
+
"related_object": "shipment",
|
|
42
|
+
"created_by": self.user,
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
url = reverse("karrio.server.documents:document-template-list")
|
|
47
|
+
response = self.client.get(url)
|
|
48
|
+
response_data = json.loads(response.content)
|
|
49
|
+
|
|
50
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
51
|
+
self.assertIn("results", response_data)
|
|
52
|
+
self.assertEqual(len(response_data["results"]), 1)
|
|
53
|
+
self.assertEqual(response_data["results"][0]["name"], "Test Template")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestDocumentTemplateDetailsREST(APITestCase):
|
|
57
|
+
def setUp(self) -> None:
|
|
58
|
+
super().setUp()
|
|
59
|
+
self.template: DocumentTemplate = DocumentTemplate.objects.create(
|
|
60
|
+
**{
|
|
61
|
+
"name": "Test Template",
|
|
62
|
+
"slug": "test_template",
|
|
63
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
64
|
+
"description": "A test template",
|
|
65
|
+
"related_object": "shipment",
|
|
66
|
+
"active": True,
|
|
67
|
+
"metadata": {"doc_type": "invoice"},
|
|
68
|
+
"options": {"page_size": "A4"},
|
|
69
|
+
"created_by": self.user,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_retrieve_document_template(self):
|
|
74
|
+
url = reverse(
|
|
75
|
+
"karrio.server.documents:document-template-details",
|
|
76
|
+
kwargs=dict(pk=self.template.pk),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
response = self.client.get(url)
|
|
80
|
+
response_data = json.loads(response.content)
|
|
81
|
+
|
|
82
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
83
|
+
|
|
84
|
+
# Check individual fields instead of strict dictionary comparison
|
|
85
|
+
self.assertEqual(response_data["name"], "Test Template")
|
|
86
|
+
self.assertEqual(response_data["slug"], "test_template")
|
|
87
|
+
self.assertEqual(response_data["description"], "A test template")
|
|
88
|
+
self.assertEqual(response_data["object_type"], "document-template")
|
|
89
|
+
self.assertEqual(response_data["related_object"], "shipment")
|
|
90
|
+
self.assertEqual(response_data["active"], True)
|
|
91
|
+
self.assertEqual(response_data["metadata"], {"doc_type": "invoice"})
|
|
92
|
+
self.assertEqual(response_data["options"], {"page_size": "A4"})
|
|
93
|
+
self.assertEqual(response_data["template"], SAMPLE_HTML_TEMPLATE)
|
|
94
|
+
|
|
95
|
+
# Check that ID field exists
|
|
96
|
+
self.assertIn("id", response_data)
|
|
97
|
+
|
|
98
|
+
def test_update_document_template(self):
|
|
99
|
+
url = reverse(
|
|
100
|
+
"karrio.server.documents:document-template-details",
|
|
101
|
+
kwargs=dict(pk=self.template.pk),
|
|
102
|
+
)
|
|
103
|
+
data = DOCUMENT_TEMPLATE_UPDATE_DATA
|
|
104
|
+
|
|
105
|
+
response = self.client.patch(url, data)
|
|
106
|
+
response_data = json.loads(response.content)
|
|
107
|
+
|
|
108
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
109
|
+
self.assertDictEqual(response_data, DOCUMENT_TEMPLATE_UPDATE_RESPONSE)
|
|
110
|
+
|
|
111
|
+
def test_delete_document_template(self):
|
|
112
|
+
url = reverse(
|
|
113
|
+
"karrio.server.documents:document-template-details",
|
|
114
|
+
kwargs=dict(pk=self.template.pk),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
response = self.client.delete(url)
|
|
118
|
+
response_data = json.loads(response.content)
|
|
119
|
+
|
|
120
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
121
|
+
# Verify template data is returned (soft delete behavior)
|
|
122
|
+
self.assertEqual(response_data.get("name"), "Test Template")
|
|
123
|
+
self.assertEqual(response_data.get("slug"), "test_template")
|
|
124
|
+
# The template should still be active (or check if it's marked as inactive)
|
|
125
|
+
self.assertIsNotNone(response_data.get("object_type"))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestDocumentTemplatesGraphQL(GraphTestCase):
|
|
129
|
+
def test_query_document_templates(self):
|
|
130
|
+
# Create a template first
|
|
131
|
+
DocumentTemplate.objects.create(
|
|
132
|
+
**{
|
|
133
|
+
"name": "GraphQL Test Template",
|
|
134
|
+
"slug": "graphql_test",
|
|
135
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
136
|
+
"description": "A GraphQL test template",
|
|
137
|
+
"related_object": "order",
|
|
138
|
+
"created_by": self.user,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
query = """
|
|
143
|
+
query {
|
|
144
|
+
document_templates {
|
|
145
|
+
edges {
|
|
146
|
+
node {
|
|
147
|
+
id
|
|
148
|
+
name
|
|
149
|
+
slug
|
|
150
|
+
description
|
|
151
|
+
related_object
|
|
152
|
+
active
|
|
153
|
+
object_type
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
result = self.query(query)
|
|
161
|
+
self.assertResponseNoErrors(result)
|
|
162
|
+
|
|
163
|
+
templates = result.data["data"]["document_templates"]["edges"]
|
|
164
|
+
self.assertEqual(len(templates), 1)
|
|
165
|
+
self.assertEqual(templates[0]["node"]["name"], "GraphQL Test Template")
|
|
166
|
+
self.assertEqual(templates[0]["node"]["slug"], "graphql_test")
|
|
167
|
+
|
|
168
|
+
def test_create_document_template_mutation(self):
|
|
169
|
+
mutation = """
|
|
170
|
+
mutation CreateDocumentTemplate($input: CreateDocumentTemplateMutationInput!) {
|
|
171
|
+
create_document_template(input: $input) {
|
|
172
|
+
template {
|
|
173
|
+
id
|
|
174
|
+
name
|
|
175
|
+
slug
|
|
176
|
+
description
|
|
177
|
+
related_object
|
|
178
|
+
active
|
|
179
|
+
}
|
|
180
|
+
errors {
|
|
181
|
+
field
|
|
182
|
+
messages
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
variables = {
|
|
189
|
+
"input": {
|
|
190
|
+
"name": "GraphQL Created Template",
|
|
191
|
+
"slug": "graphql_created",
|
|
192
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
193
|
+
"description": "Created via GraphQL",
|
|
194
|
+
"related_object": "shipment",
|
|
195
|
+
"active": True,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
result = self.query(mutation, variables=variables)
|
|
200
|
+
self.assertResponseNoErrors(result)
|
|
201
|
+
|
|
202
|
+
created_template = result.data["data"]["create_document_template"]["template"]
|
|
203
|
+
self.assertEqual(created_template["name"], "GraphQL Created Template")
|
|
204
|
+
self.assertEqual(created_template["slug"], "graphql_created")
|
|
205
|
+
self.assertTrue(created_template["active"])
|
|
206
|
+
|
|
207
|
+
def test_update_document_template_mutation(self):
|
|
208
|
+
# Create a template first
|
|
209
|
+
template = DocumentTemplate.objects.create(
|
|
210
|
+
**{
|
|
211
|
+
"name": "Original Template",
|
|
212
|
+
"slug": "original",
|
|
213
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
214
|
+
"description": "Original description",
|
|
215
|
+
"related_object": "shipment",
|
|
216
|
+
"created_by": self.user,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
mutation = """
|
|
221
|
+
mutation UpdateDocumentTemplate($input: UpdateDocumentTemplateMutationInput!) {
|
|
222
|
+
update_document_template(input: $input) {
|
|
223
|
+
template {
|
|
224
|
+
id
|
|
225
|
+
name
|
|
226
|
+
description
|
|
227
|
+
active
|
|
228
|
+
}
|
|
229
|
+
errors {
|
|
230
|
+
field
|
|
231
|
+
messages
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
variables = {
|
|
238
|
+
"input": {
|
|
239
|
+
"id": template.id,
|
|
240
|
+
"name": "Updated Template",
|
|
241
|
+
"description": "Updated description",
|
|
242
|
+
"active": False,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
result = self.query(mutation, variables=variables)
|
|
247
|
+
self.assertResponseNoErrors(result)
|
|
248
|
+
|
|
249
|
+
updated_template = result.data["data"]["update_document_template"]["template"]
|
|
250
|
+
self.assertEqual(updated_template["name"], "Updated Template")
|
|
251
|
+
self.assertEqual(updated_template["description"], "Updated description")
|
|
252
|
+
self.assertFalse(updated_template["active"])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# Test Data and Fixtures
|
|
256
|
+
SAMPLE_HTML_TEMPLATE = """
|
|
257
|
+
<title>{{ title | default('Test Document') }}</title>
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
DOCUMENT_TEMPLATE_DATA = {
|
|
261
|
+
"name": "Test Invoice Template",
|
|
262
|
+
"slug": "test_invoice",
|
|
263
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
264
|
+
"description": "A test invoice template",
|
|
265
|
+
"related_object": "shipment",
|
|
266
|
+
"active": True,
|
|
267
|
+
"metadata": {"doc_type": "invoice", "version": "1.0"},
|
|
268
|
+
"options": {"page_size": "A4", "orientation": "portrait"},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
DOCUMENT_TEMPLATE_RESPONSE = {
|
|
272
|
+
"id": ANY,
|
|
273
|
+
"object_type": "document-template",
|
|
274
|
+
"name": "Test Invoice Template",
|
|
275
|
+
"slug": "test_invoice",
|
|
276
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
277
|
+
"description": "A test invoice template",
|
|
278
|
+
"related_object": "shipment",
|
|
279
|
+
"active": True,
|
|
280
|
+
"metadata": {"doc_type": "invoice", "version": "1.0"},
|
|
281
|
+
"options": {"page_size": "A4", "orientation": "portrait"},
|
|
282
|
+
"preview_url": ANY,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
DOCUMENT_TEMPLATE_DETAIL_RESPONSE = {
|
|
286
|
+
"active": True,
|
|
287
|
+
"description": "A test template",
|
|
288
|
+
"id": ANY,
|
|
289
|
+
"metadata": {"doc_type": "invoice"},
|
|
290
|
+
"name": "Test Template",
|
|
291
|
+
"object_type": "document-template",
|
|
292
|
+
"options": {"page_size": "A4"},
|
|
293
|
+
"related_object": "shipment",
|
|
294
|
+
"slug": "test_template",
|
|
295
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
296
|
+
"preview_url": ANY,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
DOCUMENT_TEMPLATE_UPDATE_DATA = {
|
|
300
|
+
"name": "Updated Test Template",
|
|
301
|
+
"description": "An updated test template",
|
|
302
|
+
"active": False,
|
|
303
|
+
"metadata": {"doc_type": "commercial_invoice", "version": "2.0"},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
DOCUMENT_TEMPLATE_UPDATE_RESPONSE = {
|
|
307
|
+
"id": ANY,
|
|
308
|
+
"object_type": "document-template",
|
|
309
|
+
"name": "Updated Test Template",
|
|
310
|
+
"slug": "test_template",
|
|
311
|
+
"template": SAMPLE_HTML_TEMPLATE,
|
|
312
|
+
"description": "An updated test template",
|
|
313
|
+
"related_object": "shipment",
|
|
314
|
+
"active": False,
|
|
315
|
+
"metadata": {"doc_type": "commercial_invoice", "version": "2.0"},
|
|
316
|
+
"options": {"page_size": "A4"},
|
|
317
|
+
"preview_url": ANY,
|
|
318
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from django.test import TestCase
|
|
3
|
+
from rest_framework.test import APIClient
|
|
4
|
+
from rest_framework import status
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from karrio.server.documents.generator import Documents, TemplateRenderingError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocumentGenerationErrorHandlingTest(TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.client = APIClient()
|
|
13
|
+
# Mock authentication for testing
|
|
14
|
+
with patch('karrio.server.core.views.api.APIView.permission_classes', []):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def test_template_with_undefined_variable_returns_error(self):
|
|
18
|
+
"""Test that template with undefined variables returns proper error message"""
|
|
19
|
+
template = "<div>{{ shipment.shipper.address_line1 }}</div>"
|
|
20
|
+
data = {
|
|
21
|
+
"template": template,
|
|
22
|
+
"doc_format": "html",
|
|
23
|
+
"doc_name": "Test Document",
|
|
24
|
+
"data": {
|
|
25
|
+
"object": {"id": "test123"},
|
|
26
|
+
# Note: no shipment data provided, so shipment.shipper.address_line1 will be undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Mock the permission check
|
|
31
|
+
with patch('karrio.server.core.views.api.APIView.check_permissions'):
|
|
32
|
+
response = self.client.post(
|
|
33
|
+
'/v1/documents/generate',
|
|
34
|
+
data=json.dumps(data),
|
|
35
|
+
content_type='application/json'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
39
|
+
self.assertIn('errors', response.json())
|
|
40
|
+
self.assertIn('Template variable error', response.json()['errors'][0]['message'])
|
|
41
|
+
self.assertIn('shipment', response.json()['errors'][0]['message'])
|
|
42
|
+
|
|
43
|
+
def test_template_syntax_error_returns_error(self):
|
|
44
|
+
"""Test that template with syntax errors returns proper error message"""
|
|
45
|
+
template = "<div>{{ unclosed_tag</div>" # Missing closing }}
|
|
46
|
+
data = {
|
|
47
|
+
"template": template,
|
|
48
|
+
"doc_format": "html",
|
|
49
|
+
"doc_name": "Test Document",
|
|
50
|
+
"data": {"test": "data"}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
with patch('karrio.server.core.views.api.APIView.check_permissions'):
|
|
54
|
+
response = self.client.post(
|
|
55
|
+
'/v1/documents/generate',
|
|
56
|
+
data=json.dumps(data),
|
|
57
|
+
content_type='application/json'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
61
|
+
self.assertIn('errors', response.json())
|
|
62
|
+
self.assertIn('Template syntax error', response.json()['errors'][0]['message'])
|
|
63
|
+
|
|
64
|
+
def test_missing_template_returns_error(self):
|
|
65
|
+
"""Test that missing template returns proper error message"""
|
|
66
|
+
data = {
|
|
67
|
+
"doc_format": "html",
|
|
68
|
+
"doc_name": "Test Document",
|
|
69
|
+
"data": {"test": "data"}
|
|
70
|
+
# Note: no template or template_id provided
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
with patch('karrio.server.core.views.api.APIView.check_permissions'):
|
|
74
|
+
response = self.client.post(
|
|
75
|
+
'/v1/documents/generate',
|
|
76
|
+
data=json.dumps(data),
|
|
77
|
+
content_type='application/json'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
81
|
+
self.assertIn('errors', response.json())
|
|
82
|
+
self.assertEqual(
|
|
83
|
+
response.json()['errors'][0]['message'],
|
|
84
|
+
'template or template_id is required'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_valid_template_with_data_succeeds(self):
|
|
88
|
+
"""Test that valid template with proper data succeeds"""
|
|
89
|
+
template = "<div>Hello {{ name }}!</div>"
|
|
90
|
+
data = {
|
|
91
|
+
"template": template,
|
|
92
|
+
"doc_format": "html",
|
|
93
|
+
"doc_name": "Test Document",
|
|
94
|
+
"data": {
|
|
95
|
+
"name": "World",
|
|
96
|
+
"shipment": {
|
|
97
|
+
"shipper": {"address_line1": "123 Test St"}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Mock PDF generation to avoid WeasyPrint dependency in tests
|
|
103
|
+
with patch('karrio.server.core.views.api.APIView.check_permissions'), \
|
|
104
|
+
patch('karrio.server.documents.generator.weasyprint.HTML') as mock_html:
|
|
105
|
+
|
|
106
|
+
# Mock the PDF generation
|
|
107
|
+
mock_buffer = b"fake pdf content"
|
|
108
|
+
mock_html.return_value.write_pdf.return_value = None
|
|
109
|
+
|
|
110
|
+
# Patch the buffer creation
|
|
111
|
+
with patch('karrio.server.documents.generator.io.BytesIO') as mock_bytesio:
|
|
112
|
+
mock_bytesio.return_value.getvalue.return_value = mock_buffer
|
|
113
|
+
|
|
114
|
+
response = self.client.post(
|
|
115
|
+
'/v1/documents/generate',
|
|
116
|
+
data=json.dumps(data),
|
|
117
|
+
content_type='application/json'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Should succeed (201) or fail gracefully with a proper error message
|
|
121
|
+
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST])
|
|
122
|
+
|
|
123
|
+
if response.status_code == status.HTTP_400_BAD_REQUEST:
|
|
124
|
+
# If it fails, it should be with a proper error message, not a 500
|
|
125
|
+
self.assertIn('errors', response.json())
|
|
126
|
+
error_message = response.json()['errors'][0]['message']
|
|
127
|
+
# Should not be a generic "internal server error"
|
|
128
|
+
self.assertNotIn('Internal server error', error_message)
|
|
129
|
+
|
|
130
|
+
def test_template_rendering_error_direct(self):
|
|
131
|
+
"""Test TemplateRenderingError class directly"""
|
|
132
|
+
try:
|
|
133
|
+
# Test with a template that has undefined variables
|
|
134
|
+
Documents.generate(
|
|
135
|
+
template="<div>{{ undefined_var.nested.property }}</div>",
|
|
136
|
+
data={"some_data": "test"}
|
|
137
|
+
)
|
|
138
|
+
self.fail("Should have raised TemplateRenderingError")
|
|
139
|
+
except TemplateRenderingError as e:
|
|
140
|
+
self.assertIn("Template variable error", e.message)
|
|
141
|
+
self.assertIn("undefined_var", e.message)
|
|
142
|
+
self.assertIn("Available variables", e.message)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.fail(f"Should have raised TemplateRenderingError, got {type(e).__name__}: {e}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DocumentGenerationContextTest(TestCase):
|
|
148
|
+
"""Test that data contexts are properly handled"""
|
|
149
|
+
|
|
150
|
+
def test_shipment_context_properly_passed(self):
|
|
151
|
+
"""Test that shipment data is properly passed to template context"""
|
|
152
|
+
template = "<div>{{ shipment.shipper.address_line1 }}</div>"
|
|
153
|
+
data = {
|
|
154
|
+
"shipment": {
|
|
155
|
+
"shipper": {"address_line1": "123 Test Street"}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# This should work without errors
|
|
161
|
+
result = Documents.generate(template, data=data)
|
|
162
|
+
# If we get here, the context was properly passed
|
|
163
|
+
self.assertIsNotNone(result)
|
|
164
|
+
except TemplateRenderingError as e:
|
|
165
|
+
# If it fails, it should be due to PDF generation, not template rendering
|
|
166
|
+
if "PDF generation error" not in e.message:
|
|
167
|
+
self.fail(f"Template rendering failed: {e.message}")
|
|
168
|
+
|
|
169
|
+
def test_generic_context_fallback(self):
|
|
170
|
+
"""Test that generic context fallback works for arbitrary data"""
|
|
171
|
+
template = "<div>{{ custom_field }}</div>"
|
|
172
|
+
data = {
|
|
173
|
+
"custom_field": "test value"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
result = Documents.generate(template, data=data)
|
|
178
|
+
self.assertIsNotNone(result)
|
|
179
|
+
except TemplateRenderingError as e:
|
|
180
|
+
# Should work for simple templates
|
|
181
|
+
if "PDF generation error" not in e.message:
|
|
182
|
+
self.fail(f"Template rendering failed: {e.message}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
karrio server documents module urls
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.urls import include, path
|
|
6
|
+
|
|
7
|
+
app_name = "karrio.server.documents"
|
|
8
|
+
urlpatterns = [
|
|
9
|
+
path("", include("karrio.server.documents.views.printers")),
|
|
10
|
+
path("v1/", include("karrio.server.documents.views.templates")),
|
|
11
|
+
]
|