Merge pull request #408 from sartography/jinja-complete-template-508
Jinja complete template #508
This commit is contained in:
commit
61e51e736f
|
@ -41,7 +41,7 @@ def render_docx():
|
|||
file = connexion.request.files['file']
|
||||
data = connexion.request.form['data']
|
||||
# TODO: This bypasses the Jinja service and uses complete_template script
|
||||
target_stream = CompleteTemplate().make_template(file, json.loads(data))
|
||||
target_stream = JinjaService().make_template(file, json.loads(data))
|
||||
return send_file(
|
||||
io.BytesIO(target_stream.read()),
|
||||
as_attachment=True,
|
||||
|
|
|
@ -12,6 +12,7 @@ from crc.models.file import CONTENT_TYPES, FileModel
|
|||
from crc.models.workflow import WorkflowModel
|
||||
from crc.scripts.script import Script
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.jinja_service import JinjaService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
||||
|
@ -77,7 +78,7 @@ Takes two arguments:
|
|||
else:
|
||||
image_file_data = None
|
||||
|
||||
return self.make_template(BytesIO(file_data_model.data), task.data, image_file_data)
|
||||
return JinjaService().make_template(BytesIO(file_data_model.data), task.data, image_file_data)
|
||||
|
||||
def get_image_file_data(self, fields_str, task):
|
||||
image_file_data = []
|
||||
|
@ -107,55 +108,3 @@ Takes two arguments:
|
|||
"be a comma-delimited list of File IDs")
|
||||
|
||||
return image_file_data
|
||||
|
||||
def make_template(self, binary_stream, context, image_file_data=None):
|
||||
# TODO: Move this into the jinja_service?
|
||||
doc = DocxTemplate(binary_stream)
|
||||
doc_context = copy.deepcopy(context)
|
||||
doc_context = self.rich_text_update(doc_context)
|
||||
doc_context = self.append_images(doc, doc_context, image_file_data)
|
||||
jinja_env = jinja2.Environment(autoescape=True)
|
||||
try:
|
||||
doc.render(doc_context, jinja_env)
|
||||
except Exception as e:
|
||||
print (e)
|
||||
target_stream = BytesIO()
|
||||
doc.save(target_stream)
|
||||
target_stream.seek(0) # move to the beginning of the stream.
|
||||
return target_stream
|
||||
|
||||
def append_images(self, template, context, image_file_data):
|
||||
context['images'] = {}
|
||||
if image_file_data is not None:
|
||||
for file_data_model in image_file_data:
|
||||
fm = file_data_model.file_model
|
||||
if fm is not None:
|
||||
context['images'][fm.id] = {
|
||||
'name': fm.name,
|
||||
'url': '/v1.0/file/%s/data' % fm.id,
|
||||
'image': self.make_image(file_data_model, template)
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def make_image(self, file_data_model, template):
|
||||
return InlineImage(template, BytesIO(file_data_model.data), width=Inches(6.5))
|
||||
|
||||
def rich_text_update(self, context):
|
||||
"""This is a bit of a hack. If we find that /n characters exist in the data, we want
|
||||
these to come out in the final document without requiring someone to predict it in the
|
||||
template. Ideally we would use the 'RichText' feature of the python-docx library, but
|
||||
that requires we both escape it here, and in the Docx template. There is a thing called
|
||||
a 'listing' in python-docx library that only requires we use it on the way in, and the
|
||||
template doesn't have to think about it. So running with that for now."""
|
||||
# loop through the content, identify anything that has a newline character in it, and
|
||||
# wrap that sucker in a 'listing' function.
|
||||
if isinstance(context, dict):
|
||||
for k, v in context.items():
|
||||
context[k] = self.rich_text_update(v)
|
||||
elif isinstance(context, list):
|
||||
for i in range(len(context)):
|
||||
context[i] = self.rich_text_update(context[i])
|
||||
elif isinstance(context, str) and '\n' in context:
|
||||
return Listing(context)
|
||||
return context
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
from docxtpl import DocxTemplate, Listing, InlineImage
|
||||
from jinja2 import Environment, DictLoader
|
||||
|
||||
from docx.shared import Inches
|
||||
from docxtpl import DocxTemplate, Listing, InlineImage
|
||||
from io import BytesIO
|
||||
from jinja2 import Environment, DictLoader
|
||||
|
||||
import copy
|
||||
|
||||
|
@ -37,3 +36,59 @@ Cool Right?
|
|||
template = jinja2_env.get_template('main_template')
|
||||
return template.render(**data)
|
||||
|
||||
#
|
||||
# The rest of this is for using Word documents as Jinja templates
|
||||
#
|
||||
def make_template(self, binary_stream, context, image_file_data=None):
|
||||
templates = context
|
||||
doc = DocxTemplate(binary_stream)
|
||||
doc_context = copy.deepcopy(context)
|
||||
doc_context = self.rich_text_update(doc_context)
|
||||
doc_context = self.append_images(doc, doc_context, image_file_data)
|
||||
jinja_env = Environment(loader=DictLoader(templates), autoescape=True)
|
||||
|
||||
try:
|
||||
doc.render(doc_context, jinja_env)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
target_stream = BytesIO()
|
||||
doc.save(target_stream)
|
||||
target_stream.seek(0) # move to the beginning of the stream.
|
||||
return target_stream
|
||||
|
||||
def rich_text_update(self, context):
|
||||
"""This is a bit of a hack. If we find that /n characters exist in the data, we want
|
||||
these to come out in the final document without requiring someone to predict it in the
|
||||
template. Ideally we would use the 'RichText' feature of the python-docx library, but
|
||||
that requires we both escape it here, and in the Docx template. There is a thing called
|
||||
a 'listing' in python-docx library that only requires we use it on the way in, and the
|
||||
template doesn't have to think about it. So running with that for now."""
|
||||
# loop through the content, identify anything that has a newline character in it, and
|
||||
# wrap that sucker in a 'listing' function.
|
||||
if isinstance(context, dict):
|
||||
for k, v in context.items():
|
||||
context[k] = self.rich_text_update(v)
|
||||
elif isinstance(context, list):
|
||||
for i in range(len(context)):
|
||||
context[i] = self.rich_text_update(context[i])
|
||||
elif isinstance(context, str) and '\n' in context:
|
||||
return Listing(context)
|
||||
return context
|
||||
|
||||
def append_images(self, template, context, image_file_data):
|
||||
context['images'] = {}
|
||||
if image_file_data is not None:
|
||||
for file_data_model in image_file_data:
|
||||
fm = file_data_model.file_model
|
||||
if fm is not None:
|
||||
context['images'][fm.id] = {
|
||||
'name': fm.name,
|
||||
'url': '/v1.0/file/%s/data' % fm.id,
|
||||
'image': self.make_image(file_data_model, template)
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def make_image(file_data_model, template):
|
||||
return InlineImage(template, BytesIO(file_data_model.data), width=Inches(6.5))
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0jhano7" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
|
||||
<bpmn:process id="Process_1wdyw8o" name="Test Complete Template Script" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1lthj06</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1lthj06" sourceRef="StartEvent_1" targetRef="Activity_GetData" />
|
||||
<bpmn:userTask id="Activity_GetData" name="Get Data" camunda:formKey="DataForm">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="file_name" label="File Name" type="string" />
|
||||
<camunda:formField id="irb_doc_code" label="IRB Doc Code" type="enum">
|
||||
<camunda:value id="Study_App_Doc" name="Study_App_Doc" />
|
||||
<camunda:value id="Study_Protocol" name="Study_Protocol" />
|
||||
</camunda:formField>
|
||||
<camunda:formField id="name" label="Name" type="string" defaultValue="World" />
|
||||
<camunda:formField id="include_me" type="string" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1lthj06</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1bfcgdx</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="Flow_1bfcgdx" sourceRef="Activity_GetData" targetRef="Activity_CompleteTemplate" />
|
||||
<bpmn:scriptTask id="Activity_CompleteTemplate" name="Complete Template">
|
||||
<bpmn:incoming>Flow_1bfcgdx</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ltznd4</bpmn:outgoing>
|
||||
<bpmn:script>print(f'name is {name}.')
|
||||
print(f'include_me is {include_me}')
|
||||
result = complete_template(file_name, irb_doc_code)</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_0ltznd4" sourceRef="Activity_CompleteTemplate" targetRef="Activity_DisplayData" />
|
||||
<bpmn:manualTask id="Activity_DisplayData" name="Display Data">
|
||||
<bpmn:documentation># Result
|
||||
{{ result }}</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_0ltznd4</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0472uor</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:endEvent id="Event_0edltir">
|
||||
<bpmn:incoming>Flow_0472uor</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0472uor" sourceRef="Activity_DisplayData" targetRef="Event_0edltir" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1wdyw8o">
|
||||
<bpmndi:BPMNEdge id="Flow_0472uor_di" bpmnElement="Flow_0472uor">
|
||||
<di:waypoint x="690" y="117" />
|
||||
<di:waypoint x="752" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ltznd4_di" bpmnElement="Flow_0ltznd4">
|
||||
<di:waypoint x="532" y="117" />
|
||||
<di:waypoint x="590" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1bfcgdx_di" bpmnElement="Flow_1bfcgdx">
|
||||
<di:waypoint x="373" y="117" />
|
||||
<di:waypoint x="432" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1lthj06_di" bpmnElement="Flow_1lthj06">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="273" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_14dp1gz_di" bpmnElement="Activity_DisplayData">
|
||||
<dc:Bounds x="590" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0edltir_di" bpmnElement="Event_0edltir">
|
||||
<dc:Bounds x="752" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1u6kbns_di" bpmnElement="Activity_CompleteTemplate">
|
||||
<dc:Bounds x="432" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_10trg2t_di" bpmnElement="Activity_GetData">
|
||||
<dc:Bounds x="273" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
Binary file not shown.
|
@ -1,22 +1,27 @@
|
|||
import unittest
|
||||
from tests.base_test import BaseTest
|
||||
import copy
|
||||
import docx
|
||||
|
||||
from docxtpl import Listing
|
||||
from io import BytesIO
|
||||
|
||||
from crc.scripts.complete_template import CompleteTemplate
|
||||
from crc import session
|
||||
from crc.models.file import FileModel, FileDataModel
|
||||
from crc.services.jinja_service import JinjaService
|
||||
|
||||
|
||||
class TestCompleteTemplate(unittest.TestCase):
|
||||
|
||||
def test_rich_text_update(self):
|
||||
script = CompleteTemplate()
|
||||
script = JinjaService()
|
||||
data = {"name": "Dan"}
|
||||
data_copy = copy.deepcopy(data)
|
||||
script.rich_text_update(data_copy)
|
||||
self.assertEqual(data, data_copy)
|
||||
|
||||
def test_rich_text_update_new_line(self):
|
||||
script = CompleteTemplate()
|
||||
script = JinjaService()
|
||||
data = {"name": "Dan\n Funk"}
|
||||
data_copy = copy.deepcopy(data)
|
||||
script.rich_text_update(data_copy)
|
||||
|
@ -24,9 +29,37 @@ class TestCompleteTemplate(unittest.TestCase):
|
|||
self.assertIsInstance(data_copy["name"], Listing)
|
||||
|
||||
def test_rich_text_nested_new_line(self):
|
||||
script = CompleteTemplate()
|
||||
script = JinjaService()
|
||||
data = {"names": [{"name": "Dan\n Funk"}]}
|
||||
data_copy = copy.deepcopy(data)
|
||||
script.rich_text_update(data_copy)
|
||||
self.assertNotEqual(data, data_copy)
|
||||
self.assertIsInstance(data_copy["names"][0]["name"], Listing)
|
||||
|
||||
|
||||
class TestEmbeddedTemplate(BaseTest):
|
||||
|
||||
def test_embedded_template(self):
|
||||
workflow = self.create_workflow('docx_embedded')
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
task = workflow_api.next_task
|
||||
|
||||
data = {'include_me': 'Hello {{ name }}!',
|
||||
'name': 'World',
|
||||
'file_name': 'simple.docx',
|
||||
'irb_doc_code': 'Study_App_Doc'}
|
||||
self.complete_form(workflow, task, data)
|
||||
|
||||
# Get the file data created for us in the workflow
|
||||
file_model = session.query(FileModel).\
|
||||
filter(FileModel.workflow_id == workflow.id).\
|
||||
filter(FileModel.irb_doc_code == 'Study_App_Doc').\
|
||||
first()
|
||||
file_data_model = session.query(FileDataModel). \
|
||||
filter(FileDataModel.file_model_id == file_model.id).\
|
||||
first()
|
||||
|
||||
# read the data as a word document
|
||||
document = docx.Document(BytesIO(file_data_model.data))
|
||||
# Make sure 'Hello World!' is there
|
||||
self.assertEqual('Hello World!', document.paragraphs[4].text)
|
||||
|
|
Loading…
Reference in New Issue