From 0122029e30c361100781fb0f2d21c986d67d8952 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Mon, 18 Oct 2021 16:56:03 -0400 Subject: [PATCH] *** WIP *** Migrating the complete_template script stuff to JinjaService. Having trouble with the tools stuff. Pulled back to spot where test pass using CompleteTemplate --- crc/api/tools.py | 8 +++- crc/scripts/complete_template.py | 14 +++++- crc/services/jinja_service.py | 61 +++++++++++++++++++++---- tests/data/jinja_email/jinja_email.bpmn | 36 ++++++++------- tests/test_complete_template_script.py | 11 +++-- tests/test_jinja_service.py | 10 +++- 6 files changed, 106 insertions(+), 34 deletions(-) diff --git a/crc/api/tools.py b/crc/api/tools.py index edca98fb..684b1fee 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -24,9 +24,7 @@ def render_markdown(data, template): data structure. Useful for folks that are building these markdown templates. """ try: - # template = Template(template) data = json.loads(data) - # return template.render(**data) return JinjaService.get_content(template, data) except UndefinedError as ue: raise ApiError(code="undefined_field", message=ue.message) @@ -42,6 +40,12 @@ def render_docx(): try: file = connexion.request.files['file'] data = connexion.request.form['data'] + # TODO: This bypasses the Jinja service and uses complete_template script + # content = JinjaService().get_word_document_content(file, json.loads(data), image_file_data=None) + # + # target_stream = io.BytesIO() + # content.save(target_stream) + # target_stream.seek(0) # move to the beginning of the stream. target_stream = CompleteTemplate().make_template(file, json.loads(data)) return send_file( io.BytesIO(target_stream.read()), diff --git a/crc/scripts/complete_template.py b/crc/scripts/complete_template.py index d946f35e..24153b63 100644 --- a/crc/scripts/complete_template.py +++ b/crc/scripts/complete_template.py @@ -12,6 +12,7 @@ from crc.models.file import CONTENT_TYPES, FileModel, FileDataModel 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 @@ -108,10 +109,10 @@ Takes two arguments: return image_file_data - def make_template(self, binary_stream, context, image_file_data=None): + def make_template(self, binary_stream, task_data, image_file_data=None): # TODO: Move this into the jinja_service? doc = DocxTemplate(binary_stream) - doc_context = copy.deepcopy(context) + doc_context = copy.deepcopy(task_data) 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) @@ -119,6 +120,15 @@ Takes two arguments: doc.render(doc_context, jinja_env) except Exception as e: print (e) + # try: + # doc = JinjaService.get_word_document_content(binary_stream, task_data, image_file_data) + # except jinja2.exceptions.TemplateError as te: + # # raise ApiError.from_task(code="bad_template", + # # message="There was a problem compiling your template.", + # # task=self.task) + # print(te) + # except TypeError as te: + # print(te) target_stream = BytesIO() doc.save(target_stream) target_stream.seek(0) # move to the beginning of the stream. diff --git a/crc/services/jinja_service.py b/crc/services/jinja_service.py index 17671d48..7bde0ee8 100644 --- a/crc/services/jinja_service.py +++ b/crc/services/jinja_service.py @@ -1,5 +1,11 @@ +from docxtpl import DocxTemplate, Listing, InlineImage from jinja2 import Environment, DictLoader +from docx.shared import Inches +from io import BytesIO + +import copy + class JinjaService: """Service for Jinja2 templates. @@ -27,13 +33,52 @@ Cool Right? templates['main_template'] = input_template jinja2_env = Environment(loader=DictLoader(templates)) - try: - template = jinja2_env.get_template('main_template') + # We just make a call here and let any errors percolate up to the calling method + template = jinja2_env.get_template('main_template') + return template.render(**data) - except Exception: - # TODO: Should we deal w/ specific exceptions here? - # i.e., the ones in workflow_service._process_documentation - raise + @staticmethod + def get_word_document_content(binary_stream, task_data, image_file_data=None): + doc = DocxTemplate(binary_stream) + doc_context = copy.deepcopy(task_data) + doc_context = JinjaService().rich_text_update(doc_context) + # doc_context = JinjaService().append_images(doc, doc_context, image_file_data) + jinja2_env = Environment(autoescape=True) + return doc.render(doc_context, jinja2_env) - else: - return template.render(**data) + 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)) diff --git a/tests/data/jinja_email/jinja_email.bpmn b/tests/data/jinja_email/jinja_email.bpmn index d7568e86..2d72f9f0 100644 --- a/tests/data/jinja_email/jinja_email.bpmn +++ b/tests/data/jinja_email/jinja_email.bpmn @@ -16,9 +16,16 @@ - + + + + + + + + Flow_0yh2coe Flow_0ovknlg @@ -33,38 +40,35 @@ Thank you for supporting the cause! {% include 'include_me' %} -Did we include the other template? - - +Did we include the other template? Flow_0ovknlg Flow_103ct5a email_model = email(subject=subject, recipients=recipients) # Email Model -{{ email_model }} - +{{ email_model }} Flow_103ct5a Flow_1nd1ian - - - - - - - + + + - - - + + + + + + + diff --git a/tests/test_complete_template_script.py b/tests/test_complete_template_script.py index 985c2a87..d5593da9 100644 --- a/tests/test_complete_template_script.py +++ b/tests/test_complete_template_script.py @@ -1,26 +1,27 @@ +import unittest +from tests.base_test import BaseTest import json import os -import unittest import copy from docxtpl import Listing from crc import app from crc.scripts.complete_template import CompleteTemplate -from tests.base_test import BaseTest +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) @@ -28,7 +29,7 @@ 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) diff --git a/tests/test_jinja_service.py b/tests/test_jinja_service.py index f7a60fa2..93b5d339 100644 --- a/tests/test_jinja_service.py +++ b/tests/test_jinja_service.py @@ -4,6 +4,8 @@ from crc.services.workflow_service import WorkflowService from crc import mail +import json + class TestJinjaService(BaseTest): @@ -38,7 +40,13 @@ class TestJinjaService(BaseTest): print(f'test_jinja_service_email: {workflow_api.next_task.data}') def test_jinja_service_tools(self): - pass + template = "This is my template. {% include 'include_me' %} Was something included?" + data = {"name": "World", + "include_me": "Hello {{name}}!"} + rv = self.app.get('/v1.0/render_markdown?template=%s&data=%s' % + (template, json.dumps(data))) + self.assert_success(rv) + self.assertIn("Hello World", rv.get_data(as_text=True)) def test_jinja_service_documents(self): pass