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"
DEFAULT_PORT = "5000"
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"
PRODUCTION = (environ.get('PRODUCTION', default="false") == "true")
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))
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")
# %s/%i placeholders expected for uva_id and study_id in various calls.
PB_ENABLED = environ.get('PB_ENABLED', default="false") == "true"

View File

@ -740,6 +740,41 @@ paths:
type: string
format: binary
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:
parameters:
- name: file_id
@ -1574,6 +1609,7 @@ components:
standalone:
type: boolean
example: false
default: false
workflow_spec_category:
$ref: "#/components/schemas/WorkflowSpecCategory"
is_status:

View File

@ -6,6 +6,7 @@ from flask import send_file
from crc import session
from crc.api.common import ApiError
from crc.api.user import verify_token
from crc.models.api_models import DocumentDirectory, DocumentDirectorySchema
from crc.models.file import FileSchema, FileModel, File, FileModelSchema, FileDataModel, FileType
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):
file_model = session.query(FileModel).filter_by(id=file_id).with_for_update().first()
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 datetime import datetime
from typing import List
import flask
import requests
from SpiffWorkflow import WorkflowException
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['count'] = len(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:
doc['files'].append({'file_id': file.id,
'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})
# 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.api.common import ApiError
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.task_event import TaskEventModel
from crc.models.user import UserModel, UserModelSchema
@ -739,10 +739,7 @@ class WorkflowService(object):
if hasattr(task.task_spec, 'form'):
for field in task.task_spec.form.fields:
if field.has_property(Task.FIELD_PROP_READ_ONLY) and \
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):
if field.has_property(Task.FIELD_PROP_REPEAT):
group = field.get_property(Task.FIELD_PROP_REPEAT)
if group in latest_data:
data[group] = latest_data[group]
@ -811,3 +808,12 @@ class WorkflowService(object):
def get_standalone_workflow_specs():
specs = db.session.query(WorkflowSpecModel).filter_by(standalone=True).all()
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']
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>
</bpmn:scriptTask>
<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.do_engine_steps()
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['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.models.task_event import TaskEventModel
from crc.models.api_models import Task
from crc.models.file import FileModel
from crc.api.common import ApiError
@ -114,3 +115,12 @@ class TestWorkflowService(BaseTest):
result2 = WorkflowService.get_dot_value(path, {"a.b.c":"garbage"})
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)