Merge remote-tracking branch 'origin/master'

This commit is contained in:
Aaron Louie 2020-03-02 13:16:43 -05:00
commit f7416065da
9 changed files with 170 additions and 16 deletions

2
Pipfile.lock generated
View File

@ -727,7 +727,7 @@
"spiffworkflow": {
"editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "0da57a83bfa0edaf1cfd5500f87757553621c412"
"ref": "d8dbbb4e0575e8346e3342f314b76a0bce140530"
},
"sqlalchemy": {
"hashes": [

View File

@ -585,12 +585,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Task"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/workflow/{workflow_id}/task/{task_id}/data:
parameters:
- name: workflow_id
@ -629,6 +623,63 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/render_markdown:
parameters:
- name: data
in: query
required: true
description: The json data to use in populating the template
schema:
type: string
- name: template
in: query
required: true
description: The markdown template to process.
schema:
type: string
get:
operationId: crc.api.tools.render_markdown
summary: Processes the markdown template using the data provided.
tags:
- Configurator Tools
responses:
'201':
description: Returns the updated workflow with the task completed.
content:
text/plain:
schema:
type: string
/render_docx:
parameters:
- name: data
in: query
required: true
description: The json data to use in populating the template
schema:
type: string
put:
operationId: crc.api.tools.render_docx
summary: Renders a docx template with embedded jinja2 markup.
tags:
- Configurator Tools
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'200':
description: Returns the generated document.
content:
application/octet-stream:
schema:
type: string
format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
components:
schemas:

View File

@ -1,8 +1,9 @@
from crc import ma
from crc import ma, app
class ApiError(Exception):
def __init__(self, code, message):
def __init__(self, code, message, status_code=400):
self.status_code = status_code
self.code = code
self.message = message
@ -11,3 +12,8 @@ class ApiErrorSchema(ma.Schema):
class Meta:
fields = ("code", "message")
@app.errorhandler(ApiError)
def handle_invalid_usage(error):
response = ApiErrorSchema().dump(error)
return response, error.status_code

50
crc/api/tools.py Normal file
View File

@ -0,0 +1,50 @@
import io
import json
import uuid
from io import BytesIO
import connexion
import jinja2
from docxtpl import DocxTemplate
from flask import send_file
from jinja2 import Template, UndefinedError
from crc.api.common import ApiError, ApiErrorSchema
from crc.scripts.CompleteTemplate import CompleteTemplate
from crc.services.file_service import FileService
def render_markdown(data, template):
"""
Provides a quick way to very that a Jinja markdown template will work properly on a given json
data structure. Useful for folks that are building these markdown templates.
"""
try:
template = Template(template)
data = json.loads(data)
return template.render(**data)
except UndefinedError as ue:
raise ApiError(code="undefined field", message=ue.message)
except Exception as e:
raise ApiError(code="invalid", message=str(e))
def render_docx(data):
"""
Provides a quick way to verify that a Jinja docx template will work properly on a given json
data structure. Useful for folks that are building these templates.
"""
try:
file = connexion.request.files['file']
target_stream = CompleteTemplate().make_template(file, json.loads(data))
return send_file(
io.BytesIO(target_stream.read()),
as_attachment=True,
attachment_filename="output.docx",
mimetype="application/octet-stream",
cache_timeout=-1 # Don't cache these files on the browser.
)
except ValueError as e:
raise ApiError(code="invalid", message=str(e))
except Exception as e:
raise ApiError(code="invalid", message=str(e))

View File

@ -1,7 +1,7 @@
import enum
import marshmallow
from jinja2 import Environment, BaseLoader
from jinja2 import Environment, BaseLoader, Undefined, Template
from marshmallow import INCLUDE
from marshmallow_enum import EnumField
from marshmallow_sqlalchemy import ModelSchema
@ -38,7 +38,6 @@ class WorkflowModel(db.Model):
study_id = db.Column(db.Integer, db.ForeignKey('study.id'))
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
class Task(object):
def __init__(self, id, name, title, type, state, form, documentation, data):
self.id = id
@ -64,14 +63,16 @@ class Task(object):
if hasattr(spiff_task.task_spec, "form"):
instance.form = spiff_task.task_spec.form
if documentation != "" and documentation is not None:
instance.process_documentation(documentation)
return instance
def process_documentation(self, documentation):
'''Runs markdown documentation through the Jinja2 processor to inject data
create loops, etc...'''
rtemplate = Environment(autoescape=True, loader=BaseLoader).from_string(documentation)
self.documentation = rtemplate.render(**self.data)
template = Template(documentation)
self.documentation = template.render(**self.data)
class OptionSchema(ma.Schema):

View File

@ -42,7 +42,7 @@ class CompleteTemplate(object):
message="Can not find a file called '%s' "
"within workflow specification '%s'") % (args[0], workflow_spec_model.id)
final_document_stream = self.make_template(file_data_model, task.data)
final_document_stream = self.make_template(BytesIO(file_data_model.data), task.data)
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
FileService.add_task_file(study_id=study_id, workflow_id=workflow_id, task_id=task.id,
@ -52,8 +52,8 @@ class CompleteTemplate(object):
print("Complete Task was called with %s" % str(args))
def make_template(self, file_data_model, context):
doc = DocxTemplate(BytesIO(file_data_model.data))
def make_template(self, binary_stream, context):
doc = DocxTemplate(binary_stream)
jinja_env = jinja2.Environment(autoescape=True)
doc.render(context, jinja_env)
target_stream = BytesIO()

BIN
tests/data/table.docx Normal file

Binary file not shown.

View File

@ -175,6 +175,19 @@ class TestTasksApi(BaseTest):
task.process_documentation(documentation)
self.assertEqual(expected, task.documentation)
def test_documentation_processing_handles_conditionals(self):
docs = "This test {% if works == 'yes' %}works{% endif %}"
task = Task(1, "bill", "bill", "", "started", {}, docs, {})
task.process_documentation(docs)
self.assertEqual("This test ", task.documentation)
task.data = {"works": 'yes'}
task.process_documentation(docs)
self.assertEqual("This test works", task.documentation)
def test_get_documentation_populated_in_end(self):
self.load_example_data()
workflow = self.create_workflow('random_fact')

33
tests/test_tools_api.py Normal file
View File

@ -0,0 +1,33 @@
import json
import os
from datetime import datetime, timezone
from crc import session, app
from crc.models.study import StudyModel, StudyModelSchema
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
WorkflowApiSchema
from tests.base_test import BaseTest
class TestStudyApi(BaseTest):
def test_render_markdown(self):
template = "My name is {{name}}"
data = {"name": "Dan"}
rv = self.app.get('/v1.0/render_markdown?template=%s&data=%s' %
(template, json.dumps(data)))
self.assert_success(rv)
self.assertEqual("My name is Dan", rv.get_data(as_text=True))
def test_render_docx(self):
filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'table.docx')
template_data = {"hippa": [{"option": "Name", "selected": True, "stored": ["Record at UVA", "Stored Long Term"]},
{"option": "Address", "selected": False},
{"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]}
with open(filepath, 'rb') as f:
file_data = {'file': (f, 'my_new_file.bpmn')}
rv = self.app.put('/v1.0/render_docx?data=%s' % json.dumps(template_data),
data=file_data, follow_redirects=True,
content_type='multipart/form-data')
self.assert_success(rv)