Merge pull request #408 from sartography/jinja-complete-template-508

Jinja complete template #508
This commit is contained in:
Dan Funk 2021-10-21 14:22:00 -04:00 committed by GitHub
commit 61e51e736f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 176 additions and 61 deletions

View File

@ -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,

View File

@ -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

View File

@ -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))

View File

@ -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.

View File

@ -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)