Merge branch 'dev' of github.com:sartography/cr-connect-workflow into dev

This commit is contained in:
Dan 2021-05-14 14:10:19 -04:00
commit 7e6645db89
14 changed files with 281 additions and 7 deletions

View File

@ -17,7 +17,10 @@ API_TOKEN = environ.get('API_TOKEN', default = 'af95596f327c9ecc007b60414fc84b61
NAME = "CR Connect Workflow" NAME = "CR Connect Workflow"
DEFAULT_PORT = "5000" DEFAULT_PORT = "5000"
FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default=DEFAULT_PORT) FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default=DEFAULT_PORT)
CORS_ALLOW_ORIGINS = re.split(r',\s*', environ.get('CORS_ALLOW_ORIGINS', default="localhost:4200, localhost:5002")) FRONTEND = "localhost:4200"
BPMN = "localhost:5002"
CORS_DEFAULT = f'{FRONTEND}, {BPMN}'
CORS_ALLOW_ORIGINS = re.split(r',\s*', environ.get('CORS_ALLOW_ORIGINS', default=CORS_DEFAULT))
TESTING = environ.get('TESTING', default="false") == "true" TESTING = environ.get('TESTING', default="false") == "true"
PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") PRODUCTION = (environ.get('PRODUCTION', default="false") == "true")
TEST_UID = environ.get('TEST_UID', default="dhf8r") TEST_UID = environ.get('TEST_UID', default="dhf8r")
@ -50,7 +53,6 @@ SQLALCHEMY_DATABASE_URI = environ.get(
TOKEN_AUTH_TTL_HOURS = float(environ.get('TOKEN_AUTH_TTL_HOURS', default=24)) TOKEN_AUTH_TTL_HOURS = float(environ.get('TOKEN_AUTH_TTL_HOURS', default=24))
SECRET_KEY = environ.get('SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.") SECRET_KEY = environ.get('SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.")
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER") SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
# %s/%i placeholders expected for uva_id and study_id in various calls. # %s/%i placeholders expected for uva_id and study_id in various calls.
PB_ENABLED = environ.get('PB_ENABLED', default="false") == "true" PB_ENABLED = environ.get('PB_ENABLED', default="false") == "true"

View File

@ -740,6 +740,41 @@ paths:
type: string type: string
format: binary format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>' example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/file/{file_id}/download :
parameters :
- name : file_id
in : path
required : true
description : The id of the File requested
schema :
type : integer
- name : auth_token
in : query
required : true
description : User Auth Toeken
schema :
type : string
- name : version
in : query
required : false
description : The version of the file, or none for latest version
schema :
type : integer
get :
operationId : crc.api.file.get_file_data_link
summary : Returns only the file contents
security: []
tags :
- Files
responses :
'200' :
description : Returns the actual file
content :
application/octet-stream :
schema :
type : string
format : binary
example : '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/file/{file_id}/data: /file/{file_id}/data:
parameters: parameters:
- name: file_id - name: file_id
@ -1574,6 +1609,7 @@ components:
standalone: standalone:
type: boolean type: boolean
example: false example: false
default: false
workflow_spec_category: workflow_spec_category:
$ref: "#/components/schemas/WorkflowSpecCategory" $ref: "#/components/schemas/WorkflowSpecCategory"
is_status: is_status:

View File

@ -6,6 +6,7 @@ from flask import send_file
from crc import session from crc import session
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.api.user import verify_token
from crc.models.api_models import DocumentDirectory, DocumentDirectorySchema from crc.models.api_models import DocumentDirectory, DocumentDirectorySchema
from crc.models.file import FileSchema, FileModel, File, FileModelSchema, FileDataModel, FileType from crc.models.file import FileSchema, FileModel, File, FileModelSchema, FileDataModel, FileType
from crc.models.workflow import WorkflowSpecModel from crc.models.workflow import WorkflowSpecModel
@ -182,6 +183,22 @@ def get_file_data(file_id, version=None):
) )
def get_file_data_link(file_id, auth_token, version=None):
if not verify_token(auth_token):
raise ApiError('not_authenticated', 'You need to include an authorization token in the URL with this')
file_data = FileService.get_file_data(file_id, version)
if file_data is None:
raise ApiError('no_such_file', 'The file id you provided does not exist')
return send_file(
io.BytesIO(file_data.data),
attachment_filename=file_data.file_model.name,
mimetype=file_data.file_model.content_type,
cache_timeout=-1, # Don't cache these files on the browser.
last_modified=file_data.date_created,
as_attachment = True
)
def get_file_info(file_id): def get_file_info(file_id):
file_model = session.query(FileModel).filter_by(id=file_id).with_for_update().first() file_model = session.query(FileModel).filter_by(id=file_id).with_for_update().first()
if file_model is None: if file_model is None:

View File

@ -0,0 +1,16 @@
from crc.scripts.script import Script
from crc import app
class GetDashboardURL(Script):
def get_description(self):
"""Get the URL for the main dashboard. This should be system instance aware.
I.e., dev, testing, production, etc."""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.do_task(task, study_id, workflow_id, *args, **kwargs)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
frontend = app.config['FRONTEND']
return f'https://{frontend}'

View File

@ -1,7 +1,9 @@
import urllib
from copy import copy from copy import copy
from datetime import datetime from datetime import datetime
from typing import List from typing import List
import flask
import requests import requests
from SpiffWorkflow import WorkflowException from SpiffWorkflow import WorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskExecException from SpiffWorkflow.exceptions import WorkflowTaskExecException
@ -288,9 +290,19 @@ class StudyService(object):
doc_files = FileService.get_files_for_study(study_id=study_id, irb_doc_code=code) doc_files = FileService.get_files_for_study(study_id=study_id, irb_doc_code=code)
doc['count'] = len(doc_files) doc['count'] = len(doc_files)
doc['files'] = [] doc['files'] = []
# when we run tests - it doesn't look like the user is available
# so we return a bogus token
token = 'not_available'
if hasattr(flask.g,'user'):
token = flask.g.user.encode_auth_token()
for file in doc_files: for file in doc_files:
doc['files'].append({'file_id': file.id, doc['files'].append({'file_id': file.id,
'name': file.name, 'name': file.name,
'url': app.config['APPLICATION_ROOT']+
'file/' + str(file.id) +
'/download?auth_token='+
urllib.parse.quote_plus(token),
'workflow_id': file.workflow_id}) 'workflow_id': file.workflow_id})
# update the document status to match the status of the workflow it is in. # update the document status to match the status of the workflow it is in.

View File

@ -22,7 +22,7 @@ from jinja2 import Template
from crc import db, app from crc import db, app
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType, WorkflowApi from crc.models.api_models import Task, MultiInstanceType, WorkflowApi
from crc.models.file import LookupDataModel from crc.models.file import LookupDataModel, FileModel
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel
from crc.models.user import UserModel, UserModelSchema from crc.models.user import UserModel, UserModelSchema
@ -739,10 +739,7 @@ class WorkflowService(object):
if hasattr(task.task_spec, 'form'): if hasattr(task.task_spec, 'form'):
for field in task.task_spec.form.fields: for field in task.task_spec.form.fields:
if field.has_property(Task.FIELD_PROP_READ_ONLY) and \ if field.has_property(Task.FIELD_PROP_REPEAT):
field.get_property(Task.FIELD_PROP_READ_ONLY).lower().strip() == "true":
continue # Don't add read-only data
elif field.has_property(Task.FIELD_PROP_REPEAT):
group = field.get_property(Task.FIELD_PROP_REPEAT) group = field.get_property(Task.FIELD_PROP_REPEAT)
if group in latest_data: if group in latest_data:
data[group] = latest_data[group] data[group] = latest_data[group]
@ -811,3 +808,12 @@ class WorkflowService(object):
def get_standalone_workflow_specs(): def get_standalone_workflow_specs():
specs = db.session.query(WorkflowSpecModel).filter_by(standalone=True).all() specs = db.session.query(WorkflowSpecModel).filter_by(standalone=True).all()
return specs return specs
@staticmethod
def get_primary_workflow(workflow_spec_id):
# Returns the FileModel of the primary workflow for a workflow_spec
primary = None
file = db.session.query(FileModel).filter(FileModel.workflow_spec_id==workflow_spec_id, FileModel.primary==True).first()
if file:
primary = file
return primary

View File

@ -0,0 +1,6 @@
from crc.api.file import get_document_directory
def render_files(study_id,irb_codes):
files = get_document_directory(study_id)
print(files)

View File

@ -0,0 +1,53 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_024561a" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_1796d29" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0c51a4b</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0c51a4b" sourceRef="StartEvent_1" targetRef="Activity_GetURL" />
<bpmn:sequenceFlow id="Flow_1ker6ik" sourceRef="Activity_GetURL" targetRef="Activity_EmailURL" />
<bpmn:endEvent id="Event_17hmyob">
<bpmn:incoming>Flow_1rfvzi5</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1rfvzi5" sourceRef="Activity_EmailURL" targetRef="Event_17hmyob" />
<bpmn:scriptTask id="Activity_GetURL" name="Get Dashboard URL&#10;">
<bpmn:incoming>Flow_0c51a4b</bpmn:incoming>
<bpmn:outgoing>Flow_1ker6ik</bpmn:outgoing>
<bpmn:script>dashboard_url = get_dashboard_url()</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_EmailURL" name="Email Dashboard URL">
<bpmn:documentation>&lt;a href="{{dashboard_url}}"&gt;{{dashboard_url}}&lt;/a&gt;</bpmn:documentation>
<bpmn:incoming>Flow_1ker6ik</bpmn:incoming>
<bpmn:outgoing>Flow_1rfvzi5</bpmn:outgoing>
<bpmn:script>email(subject='My Email Subject', recipients="test@example.com")</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1796d29">
<bpmndi:BPMNEdge id="Flow_1rfvzi5_di" bpmnElement="Flow_1rfvzi5">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ker6ik_di" bpmnElement="Flow_1ker6ik">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c51a4b_di" bpmnElement="Flow_0c51a4b">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" 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="Event_17hmyob_di" bpmnElement="Event_17hmyob">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1n7b49v_di" bpmnElement="Activity_GetURL">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1dn6kw2_di" bpmnElement="Activity_EmailURL">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -18,6 +18,10 @@
fileid = documents['UVACompl_PRCAppr'].files[0]['file_id'] fileid = documents['UVACompl_PRCAppr'].files[0]['file_id']
fileurl = documents['UVACompl_PRCAppr'].files[0]['url']
filename = documents['UVACompl_PRCAppr'].files[0]['name']
file_data_set(file_id=fileid,key='test',value='me')</bpmn:script> file_data_set(file_id=fileid,key='test',value='me')</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:endEvent id="Event_1pdyoyv"> <bpmn:endEvent id="Event_1pdyoyv">

View File

@ -0,0 +1,77 @@
<?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_5e40639" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_ReadOnlyField" name="Test Read Only Field" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0to8etb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0to8etb" sourceRef="StartEvent_1" targetRef="Activity_SetData" />
<bpmn:sequenceFlow id="Flow_04r75ca" sourceRef="Activity_SetData" targetRef="Activity_DisplayOnlyField" />
<bpmn:sequenceFlow id="Flow_0g25v76" sourceRef="Activity_DisplayOnlyField" targetRef="Activity_CheckData" />
<bpmn:endEvent id="Event_0cfckhy">
<bpmn:incoming>Flow_0a95kns</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0a95kns" sourceRef="Activity_CheckData" targetRef="Event_0cfckhy" />
<bpmn:scriptTask id="Activity_SetData" name="Set Data">
<bpmn:incoming>Flow_0to8etb</bpmn:incoming>
<bpmn:outgoing>Flow_04r75ca</bpmn:outgoing>
<bpmn:script>string_value = 'asdf'</bpmn:script>
</bpmn:scriptTask>
<bpmn:userTask id="Activity_DisplayOnlyField" name="Display Only Field&#10;" camunda:formKey="ReadOnlyFormField">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="read_only_field" label="Read Only" type="string">
<camunda:properties>
<camunda:property id="read_only" value="True" />
<camunda:property id="value_expression" value="string_value" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_04r75ca</bpmn:incoming>
<bpmn:outgoing>Flow_0g25v76</bpmn:outgoing>
</bpmn:userTask>
<bpmn:manualTask id="Activity_CheckData" name="Check Data Persistence">
<bpmn:documentation>Read only is {{ read_only_field }}</bpmn:documentation>
<bpmn:incoming>Flow_0g25v76</bpmn:incoming>
<bpmn:outgoing>Flow_0a95kns</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ReadOnlyField">
<bpmndi:BPMNEdge id="Flow_0a95kns_di" bpmnElement="Flow_0a95kns">
<di:waypoint x="690" y="177" />
<di:waypoint x="752" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0g25v76_di" bpmnElement="Flow_0g25v76">
<di:waypoint x="530" y="177" />
<di:waypoint x="590" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_04r75ca_di" bpmnElement="Flow_04r75ca">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0to8etb_di" bpmnElement="Flow_0to8etb">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0cfckhy_di" bpmnElement="Event_0cfckhy">
<dc:Bounds x="752" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="733" y="202" width="76" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_10544ek_di" bpmnElement="Activity_SetData">
<dc:Bounds x="270" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ho1wsm_di" bpmnElement="Activity_DisplayOnlyField">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_10ds6m4_di" bpmnElement="Activity_CheckData">
<dc:Bounds x="590" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -27,6 +27,8 @@ class TestFileDatastore(BaseTest):
processor = WorkflowProcessor(workflow) processor = WorkflowProcessor(workflow)
processor.do_engine_steps() processor.do_engine_steps()
task_data = processor.bpmn_workflow.last_task.data task_data = processor.bpmn_workflow.last_task.data
self.assertTrue(str(task_data['fileid']) in task_data['fileurl'])
self.assertEqual(task_data['filename'],'anything.png')
self.assertEqual(task_data['output'], 'me') self.assertEqual(task_data['output'], 'me')
self.assertEqual(task_data['output2'], 'nope') self.assertEqual(task_data['output2'], 'nope')

View File

@ -0,0 +1,17 @@
from tests.base_test import BaseTest
from crc import app, mail
class TestGetDashboardURL(BaseTest):
def test_get_dashboard_url(self):
with mail.record_messages() as outbox:
dashboard_url = f'https://{app.config["FRONTEND"]}'
workflow = self.create_workflow('email_dashboard_url')
self.get_workflow_api(workflow)
self.assertEqual(1, len(outbox))
self.assertEqual('My Email Subject', outbox[0].subject)
self.assertEqual(['test@example.com'], outbox[0].recipients)
self.assertIn(dashboard_url, outbox[0].body)

View File

@ -0,0 +1,16 @@
from tests.base_test import BaseTest
class TestReadOnlyField(BaseTest):
def test_read_only(self):
workflow = self.create_workflow('read_only_field')
workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task
read_only_field = first_task.data['read_only_field']
self.complete_form(workflow, first_task, {'read_only_field': read_only_field})
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
self.assertEqual('Read only is asdf', task.documentation)

View File

@ -10,6 +10,7 @@ from example_data import ExampleDataLoader
from crc import db from crc import db
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel
from crc.models.api_models import Task from crc.models.api_models import Task
from crc.models.file import FileModel
from crc.api.common import ApiError from crc.api.common import ApiError
@ -114,3 +115,12 @@ class TestWorkflowService(BaseTest):
result2 = WorkflowService.get_dot_value(path, {"a.b.c":"garbage"}) result2 = WorkflowService.get_dot_value(path, {"a.b.c":"garbage"})
self.assertEqual("garbage", result2) self.assertEqual("garbage", result2)
def test_get_primary_workflow(self):
workflow = self.create_workflow('hello_world')
workflow_spec_id = workflow.workflow_spec.id
primary_workflow = WorkflowService.get_primary_workflow(workflow_spec_id)
self.assertIsInstance(primary_workflow, FileModel)
self.assertEqual(workflow_spec_id, primary_workflow.workflow_spec_id)
self.assertEqual('hello_world.bpmn', primary_workflow.name)