diff --git a/config/default.py b/config/default.py index de512b38..464b4ef7 100644 --- a/config/default.py +++ b/config/default.py @@ -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" diff --git a/crc/api.yml b/crc/api.yml index 2e36b93a..6501cebb 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -740,6 +740,41 @@ paths: type: string format: binary example: '' + /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 : '' /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: diff --git a/crc/api/file.py b/crc/api/file.py index b03b3778..911b2996 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -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: diff --git a/crc/scripts/get_dashboard_url.py b/crc/scripts/get_dashboard_url.py new file mode 100644 index 00000000..7730dab0 --- /dev/null +++ b/crc/scripts/get_dashboard_url.py @@ -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}' diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 4f3a8ca2..b01c42b8 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -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. diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index 101929d4..ee37c688 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -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 diff --git a/crc/static/jinja_extensions.py b/crc/static/jinja_extensions.py new file mode 100644 index 00000000..fcbc7b55 --- /dev/null +++ b/crc/static/jinja_extensions.py @@ -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) \ No newline at end of file diff --git a/tests/data/email_dashboard_url/email_dashboard_url.bpmn b/tests/data/email_dashboard_url/email_dashboard_url.bpmn new file mode 100644 index 00000000..ce75e589 --- /dev/null +++ b/tests/data/email_dashboard_url/email_dashboard_url.bpmn @@ -0,0 +1,53 @@ + + + + + Flow_0c51a4b + + + + + Flow_1rfvzi5 + + + + Flow_0c51a4b + Flow_1ker6ik + dashboard_url = get_dashboard_url() + + + <a href="{{dashboard_url}}">{{dashboard_url}}</a> + Flow_1ker6ik + Flow_1rfvzi5 + email(subject='My Email Subject', recipients="test@example.com") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/file_data_store/file_data_store.bpmn b/tests/data/file_data_store/file_data_store.bpmn index 9af77bb0..ddc9e987 100644 --- a/tests/data/file_data_store/file_data_store.bpmn +++ b/tests/data/file_data_store/file_data_store.bpmn @@ -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') diff --git a/tests/data/read_only_field/read_only_field.bpmn b/tests/data/read_only_field/read_only_field.bpmn new file mode 100644 index 00000000..3620afdf --- /dev/null +++ b/tests/data/read_only_field/read_only_field.bpmn @@ -0,0 +1,77 @@ + + + + + Flow_0to8etb + + + + + + Flow_0a95kns + + + + Flow_0to8etb + Flow_04r75ca + string_value = 'asdf' + + + + + + + + + + + + + Flow_04r75ca + Flow_0g25v76 + + + Read only is {{ read_only_field }} + Flow_0g25v76 + Flow_0a95kns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_file_datastore.py b/tests/test_file_datastore.py index 2b95519e..1a10eea7 100644 --- a/tests/test_file_datastore.py +++ b/tests/test_file_datastore.py @@ -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') diff --git a/tests/test_get_dashboard_url_script.py b/tests/test_get_dashboard_url_script.py new file mode 100644 index 00000000..217942f6 --- /dev/null +++ b/tests/test_get_dashboard_url_script.py @@ -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) diff --git a/tests/workflow/test_workflow_read_only_field.py b/tests/workflow/test_workflow_read_only_field.py new file mode 100644 index 00000000..804650d8 --- /dev/null +++ b/tests/workflow/test_workflow_read_only_field.py @@ -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) \ No newline at end of file diff --git a/tests/workflow/test_workflow_service.py b/tests/workflow/test_workflow_service.py index a4af4edc..cb4da9c2 100644 --- a/tests/workflow/test_workflow_service.py +++ b/tests/workflow/test_workflow_service.py @@ -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)