Merge branch 'dev' into dmn-from-spreadsheet-395

This commit is contained in:
mike cullerton 2021-09-01 09:38:03 -04:00
commit 0c31b091ee
41 changed files with 713 additions and 147 deletions

2
.gitignore vendored Normal file → Executable file
View File

@ -14,7 +14,7 @@ local.properties
.settings/
.loadpath
.recommenders
.vscode/
# External tool builders
.externalToolBuilders/

2
Pipfile.lock generated
View File

@ -979,7 +979,7 @@
},
"spiffworkflow": {
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "0b4a878f9b6d4f7fc320c26f59ca5e458a6130e8"
"ref": "07990f4af2d89587c74613db9db036ab09967780"
},
"sqlalchemy": {
"hashes": [

View File

@ -699,6 +699,12 @@ paths:
description: The unique id of a workflow instance
schema:
type: integer
- name: task_spec_name
in: query
required: false
description: The name of the task spec
schema:
type: string
- name: form_field_key
in: query
required: false
@ -1656,10 +1662,6 @@ components:
type: string
x-nullable: true
example: "27b-6-42"
hsr_number:
type: string
x-nullable: true
example: "27b-6-1212"
StudyAssociate:
properties:
uid:

View File

@ -51,7 +51,15 @@ class ApiError(Exception):
if "task" in task.data:
task.data.pop("task")
# In the unlikely event that the API error can't be serialized, try removing the task_data, as it may
# contain some invalid data that we can't return, so we can at least get the erro rmessage.
instance.task_data = task.data
try:
json.dumps(ApiErrorSchema().dump(instance))
except TypeError as te:
instance.task_data = {
'task_data_hidden': 'We were unable to serialize the task data when reporting this error'}
app.logger.error(message, exc_info=True)
return instance
@ -90,15 +98,6 @@ class ApiErrorSchema(ma.Schema):
@app.errorhandler(ApiError)
def handle_invalid_usage(error):
response = ApiErrorSchema().dump(error)
# In the unlikely event that the API error can't be serialized, try removing the task_data, as it may
# contain some invalid data that we can't return, so we can at least get the erro rmessage.
try:
json_output = json.dumps(response)
except TypeError as te:
error.task_data = {'task_data_hidden':'We were unable to serialize the task data when reporting this error'}
response = ApiErrorSchema().dump(error)
return response, error.status_code

View File

@ -41,13 +41,17 @@ def get_reference_files():
return FileSchema(many=True).dump(files)
def add_file(workflow_spec_id=None, workflow_id=None, form_field_key=None):
def add_file(workflow_spec_id=None, workflow_id=None, task_spec_name=None, form_field_key=None):
file = connexion.request.files['file']
if workflow_id:
if form_field_key is None:
raise ApiError('invalid_workflow_file',
'When adding a workflow related file, you must specify a form_field_key')
if task_spec_name is None:
raise ApiError('invalid_workflow_file',
'When adding a workflow related file, you must specify a task_spec_name')
file_model = FileService.add_workflow_file(workflow_id=workflow_id, irb_doc_code=form_field_key,
task_spec_name=task_spec_name,
name=file.filename, content_type=file.content_type,
binary_data=file.stream.read())
elif workflow_spec_id:

View File

@ -99,6 +99,12 @@ def user_studies():
user = UserService.current_user(allow_admin_impersonate=True)
StudyService.synch_with_protocol_builder_if_enabled(user)
studies = StudyService().get_studies_for_user(user)
if len(studies) == 0:
studies = StudyService().get_studies_for_user(user, include_invalid=True)
if len(studies) > 0:
message = f"All studies associated with User: {user.display_name} failed study validation"
raise ApiError(code="study_integrity_error", message=message)
results = StudySchema(many=True).dump(studies)
return results

View File

@ -138,7 +138,9 @@ def delete_workflow_specification(spec_id):
# Delete all events and workflow models related to this specification
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow.id)
session.query(WorkflowSpecModel).filter_by(id=spec_id).delete()
# .delete() doesn't work when we need a cascade. Must grab the record, and explicitly delete
deleteSpec = session.query(WorkflowSpecModel).filter_by(id=spec_id).first()
session.delete(deleteSpec)
session.commit()

View File

@ -66,6 +66,7 @@ class Task(object):
# Additional properties
FIELD_PROP_ENUM_TYPE = "enum_type"
FIELD_PROP_BOOLEAN_TYPE = "boolean_type"
FIELD_PROP_TEXT_AREA_ROWS = "rows"
FIELD_PROP_TEXT_AREA_COLS = "cols"
FIELD_PROP_TEXT_AREA_AUTO = "autosize"

View File

@ -11,7 +11,7 @@ class DataStoreModel(db.Model):
key = db.Column(db.String, nullable=False)
workflow_id = db.Column(db.Integer)
study_id = db.Column(db.Integer, nullable=True)
task_id = db.Column(db.String)
task_spec = db.Column(db.String)
spec_id = db.Column(db.String)
user_id = db.Column(db.String, nullable=True)
file_id = db.Column(db.Integer, db.ForeignKey('file.id'), nullable=True)

View File

@ -83,6 +83,7 @@ class FileModel(db.Model):
primary_process_id = db.Column(db.String, nullable=True) # An id in the xml of BPMN documents, for primary BPMN.
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'), nullable=True)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=True)
task_spec = db.Column(db.String, nullable=True)
irb_doc_code = db.Column(db.String, nullable=True) # Code reference to the irb_documents.xlsx reference file.
# A request was made to delete the file, but we can't because there are
# active approvals or running workflows that depend on it. So we archive

View File

@ -19,32 +19,31 @@ class ProtocolBuilderInvestigatorType(enum.Enum):
# Deprecated: Marked for removal
class ProtocolBuilderStatus(enum.Enum):
# • Active: found in PB and no HSR number and not hold
# • Active: found in PB and not hold
# • Hold: store boolean value in CR Connect (add to Study Model)
# • Open To Enrollment: has start date and HSR number?
# • Open To Enrollment: has start date?
# • Abandoned: deleted in PB
incomplete = 'incomplete' # Found in PB but not ready to start (not q_complete)
active = 'active' # found in PB, marked as "q_complete" and no HSR number and not hold
active = 'active' # found in PB, marked as "q_complete" and not hold
hold = 'hold' # CR Connect side, if the Study ias marked as "hold".
open = 'open' # Open To Enrollment: has start date and HSR number?
open = 'open' # Open To Enrollment: has start date?
abandoned = 'abandoned' # Not found in PB
#DRAFT = 'draft', # !Q_COMPLETE
#IN_PROCESS = 'in_process', # Q_COMPLETE && !UPLOAD_COMPLETE && !HSRNUMBER
#IN_REVIEW = 'in_review', # Q_COMPLETE && (!UPLOAD_COMPLETE || !HSRNUMBER)
#REVIEW_COMPLETE = 'review_complete', # Q_COMPLETE && UPLOAD_COMPLETE && HSRNUMBER
#IN_PROCESS = 'in_process', # Q_COMPLETE && !UPLOAD_COMPLETE
#IN_REVIEW = 'in_review', # Q_COMPLETE && (!UPLOAD_COMPLETE)
#REVIEW_COMPLETE = 'review_complete', # Q_COMPLETE && UPLOAD_COMPLETE
#INACTIVE = 'inactive', # Not found in PB
class ProtocolBuilderStudy(object):
def __init__(
self, STUDYID: int, HSRNUMBER: str, TITLE: str, NETBADGEID: str,
self, STUDYID: int, TITLE: str, NETBADGEID: str,
DATE_MODIFIED: str
):
self.STUDYID = STUDYID
self.HSRNUMBER = HSRNUMBER
self.TITLE = TITLE
self.NETBADGEID = NETBADGEID
self.DATE_MODIFIED = DATE_MODIFIED
@ -54,7 +53,7 @@ class ProtocolBuilderStudySchema(ma.Schema):
class Meta:
model = ProtocolBuilderStudy
unknown = INCLUDE
fields = ["STUDYID", "HSRNUMBER", "TITLE", "NETBADGEID",
fields = ["STUDYID", "TITLE", "NETBADGEID",
"DATE_MODIFIED"]
@post_load

View File

@ -27,7 +27,6 @@ class StudyStatus(enum.Enum):
class IrbStatus(enum.Enum):
incomplete_in_protocol_builder = 'incomplete in protocol builder'
completed_in_protocol_builder = 'completed in protocol builder'
hsr_assigned = 'hsr number assigned'
class StudyEventType(enum.Enum):
@ -46,7 +45,6 @@ class StudyModel(db.Model):
irb_status = db.Column(db.Enum(IrbStatus))
primary_investigator_id = db.Column(db.String, nullable=True)
sponsor = db.Column(db.String, nullable=True)
hsr_number = db.Column(db.String, nullable=True)
ind_number = db.Column(db.String, nullable=True)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
investigator_uids = db.Column(db.ARRAY(db.String), nullable=True)
@ -57,7 +55,6 @@ class StudyModel(db.Model):
events_history = db.relationship("StudyEvent", cascade="all, delete, delete-orphan")
def update_from_protocol_builder(self, pbs: ProtocolBuilderStudy):
self.hsr_number = pbs.HSRNUMBER
self.title = pbs.TITLE
self.user_uid = pbs.NETBADGEID
self.last_updated = pbs.DATE_MODIFIED
@ -172,7 +169,7 @@ class Study(object):
def __init__(self, title, short_title, last_updated, primary_investigator_id, user_uid,
id=None, status=None, irb_status=None, comment="",
sponsor="", hsr_number="", ind_number="", categories=[],
sponsor="", ind_number="", categories=[],
files=[], approvals=[], enrollment_date=None, events_history=[],
last_activity_user="",last_activity_date =None,create_user_display="", **argsv):
self.id = id
@ -188,7 +185,6 @@ class Study(object):
self.comment = comment
self.primary_investigator_id = primary_investigator_id
self.sponsor = sponsor
self.hsr_number = hsr_number
self.ind_number = ind_number
self.categories = categories
self.approvals = approvals
@ -220,7 +216,6 @@ class StudyForUpdateSchema(ma.Schema):
id = fields.Integer(required=False, allow_none=True)
status = EnumField(StudyStatus, by_value=True)
hsr_number = fields.String(allow_none=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)
enrollment_date = fields.DateTime(allow_none=True)
@ -252,7 +247,6 @@ class StudySchema(ma.Schema):
warnings = fields.List(fields.Nested(ApiErrorSchema), dump_only=True)
protocol_builder_status = EnumField(StudyStatus, by_value=True)
status = EnumField(StudyStatus, by_value=True)
hsr_number = fields.String(allow_none=True)
short_title = fields.String(allow_none=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)

View File

@ -4,6 +4,7 @@ import marshmallow
from marshmallow import EXCLUDE,fields
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from sqlalchemy import func
from sqlalchemy.orm import backref
from crc import db
from crc.models.file import FileModel, FileDataModel
@ -46,9 +47,9 @@ class WorkflowLibraryModel(db.Model):
library_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'), nullable=True)
parent = db.relationship(WorkflowSpecModel,
primaryjoin=workflow_spec_id==WorkflowSpecModel.id,
backref='libraries')
backref=backref('libraries',cascade='all, delete'))
library = db.relationship(WorkflowSpecModel,primaryjoin=library_spec_id==WorkflowSpecModel.id,
backref='parents')
backref=backref('parents',cascade='all, delete'))
class WorkflowSpecModelSchema(SQLAlchemyAutoSchema):

View File

@ -38,6 +38,7 @@ Takes two arguments:
file_name = args[0]
irb_doc_code = args[1]
FileService.add_workflow_file(workflow_id=workflow_id,
task_spec_name=task.get_name(),
name=file_name,
content_type=CONTENT_TYPES['docx'],
binary_data=final_document_stream.read(),

View File

@ -0,0 +1,51 @@
from crc import session
from crc.api.common import ApiError
from crc.models.data_store import DataStoreModel
from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel
from crc.scripts.script import Script
from crc.services.document_service import DocumentService
from crc.services.file_service import FileService
from crc.services.workflow_service import WorkflowService
class DeleteTaskData(Script):
def get_description(self):
return """Delete IRB Documents and task data from a workflow, for a given task"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if 'task_id' in kwargs:
return True
elif len(args) == 1:
return True
return False
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
# fixme: using task_id is confusing, this is actually the name of the task_spec
# make sure we have a task_id
if 'task_id' in kwargs:
task_spec_name = kwargs['task_id']
elif len(args) == 1:
task_spec_name = args[0]
else:
raise ApiError(code='missing_task_id',
message='The delete_task_data requires task_id. This is the ID of the task used to upload the file(s)')
# delete task events
session.query(TaskEventModel).filter(TaskEventModel.workflow_id == workflow_id).filter(
TaskEventModel.study_id == study_id).filter(TaskEventModel.task_name == task_spec_name).filter_by(
action=WorkflowService.TASK_ACTION_COMPLETE).delete()
files_to_delete = session.query(FileModel). \
filter(FileModel.workflow_id == workflow_id). \
filter(FileModel.task_spec == task_spec_name).all()
# delete files
for file in files_to_delete:
FileService().delete_file(file.id)
# delete the data store
session.query(DataStoreModel). \
filter(DataStoreModel.file_id == file.id).delete()

View File

@ -1,29 +1,42 @@
import sys
import traceback
from crc import app
from crc import app, session
from crc.api.common import ApiError
from crc.models.file import FileModel, CONTENT_TYPES
from crc.models.workflow import WorkflowModel
from crc.services.document_service import DocumentService
from crc.scripts.script import Script
from crc.services.ldap_service import LdapService
from crc.services.email_service import EmailService
from crc.services.ldap_service import LdapService
from crc.services.study_service import StudyService
class Email(Script):
"""This Script allows to be introduced as part of a workflow and called from there, specifying
recipients and content """
"""Send an email from a script task, as part of a workflow.
You must specify recipients and content.
You can also specify cc, bcc, reply_to, and attachments"""
def get_description(self):
return """
Creates an email, using the provided `subject`, `recipients`, and `cc` arguments.
The recipients and cc arguments can contain an email address or list of email addresses.
Creates an email, using the provided `subject` and `recipients` arguments, which are required.
The `Element Documentation` field in the script task must contain markdown that becomes the body of the email message.
You can also provide `cc`, `bcc`, `reply_to` and `attachments` arguments.
The cc, bcc, reply_to, and attachments arguments are not required.
The recipients, cc, and bcc arguments can contain an email address or list of email addresses.
In place of an email address, we accept the string 'associated', in which case we
look up the users associated with the study who have send_email set to True.
The cc argument is not required.
The "documentation" should contain markdown that will become the body of the email message.
The reply_to argument can contain an email address.
The attachments arguments can contain a doc_code or list of doc_codes.
Examples:
email (subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email, 'associated'])
email (subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email], cc='associated')
email(subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email, 'associated'])
email(subject="My Subject", recipients="user@example.com", cc='associated', bcc='test_user@example.com)
email(subject="My Subject", recipients="user@example.com", reply_to="reply_to@example.com")
email(subject="My Subject", recipients="user@example.com", attachments='Study_App_Doc')
email(subject="My Subject", recipients="user@example.com", attachments=['Study_App_Doc','Study_Protocol_Document'])
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
@ -37,8 +50,17 @@ email (subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email], cc='as
subject = self.get_subject(kwargs['subject'])
recipients = self.get_email_addresses(kwargs['recipients'], study_id)
cc = []
bcc = []
reply_to = None
files = None
if 'cc' in kwargs and kwargs['cc'] is not None:
cc = self.get_email_addresses(kwargs['cc'], study_id)
if 'bcc' in kwargs and kwargs['bcc'] is not None:
bcc = self.get_email_addresses(kwargs['bcc'], study_id)
if 'reply_to' in kwargs:
reply_to = kwargs['reply_to']
if 'attachments' in kwargs:
files = self.get_files(kwargs['attachments'], study_id)
else:
raise ApiError(code="missing_argument",
@ -56,7 +78,10 @@ email (subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email], cc='as
content=content,
content_html=content_html,
cc=cc,
study_id=study_id
bcc=bcc,
study_id=study_id,
reply_to=reply_to,
attachment_files=files
)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
@ -118,3 +143,31 @@ email (subject="My Subject", recipients=["dhf8r@virginia.edu", pi.email], cc='as
user_info = LdapService.user_info(associate.uid)
associated_emails.append(user_info.email_address)
return associated_emails
@staticmethod
def get_files(attachments, study_id):
files = []
codes = None
if isinstance(attachments, str):
codes = [attachments]
elif isinstance(attachments, list):
codes = attachments
if codes is not None:
for code in codes:
if DocumentService.is_allowed_document(code):
workflows = session.query(WorkflowModel).filter(WorkflowModel.study_id==study_id).all()
for workflow in workflows:
workflow_files = session.query(FileModel).\
filter(FileModel.workflow_id == workflow.id).\
filter(FileModel.irb_doc_code == code).all()
for file in workflow_files:
files.append({'id': file.id, 'name': file.name, 'type': CONTENT_TYPES[file.type.value]})
else:
raise ApiError(code='bad_doc_code',
message=f'The doc_code {code} is not valid.')
else:
raise ApiError(code='bad_argument_type',
message='The attachments argument must be a string or list of strings')
return files

View File

@ -43,7 +43,7 @@ class DataStoreBase(object):
return study
def set_data_common(self,
task_id,
task_spec,
study_id,
user_id,
workflow_id,
@ -67,7 +67,7 @@ class DataStoreBase(object):
prev_value = None
study = DataStoreModel(key=args[0], value=args[1],
study_id=study_id,
task_id=task_id,
task_spec=task_spec,
user_id=user_id, # Make this available to any User
file_id=file_id,
workflow_id=workflow_id,

View File

@ -1,25 +1,23 @@
import markdown
import re
from datetime import datetime
from flask import render_template, request
from flask import render_template
from flask_mail import Message
from jinja2 import Template
from sqlalchemy import desc
from crc import app, db, mail, session
from crc.api.common import ApiError
from crc.models.study import StudyModel
from crc.models.email import EmailModel
from crc.models.file import FileDataModel
from crc.models.study import StudyModel
class EmailService(object):
"""Provides common tools for working with an Email"""
@staticmethod
def add_email(subject, sender, recipients, content, content_html, cc=None, study_id=None):
def add_email(subject, sender, recipients, content, content_html,
cc=None, bcc=None, study_id=None, reply_to=None, attachment_files=None):
"""We will receive all data related to an email and store it"""
# Find corresponding study - if any
@ -35,11 +33,17 @@ class EmailService(object):
try:
msg = Message(subject,
sender=sender,
recipients=recipients)
recipients=recipients,
body=content,
html=content_html,
cc=cc,
bcc=bcc,
reply_to=reply_to)
msg.body = content
msg.html = content_html
msg.cc = cc
if attachment_files is not None:
for file in attachment_files:
file_data = session.query(FileDataModel).filter(FileDataModel.file_model_id==file['id']).first()
msg.attach(file['name'], file['type'], file_data.data)
mail.send(msg)
except Exception as e:

View File

@ -85,16 +85,18 @@ class FileService(object):
@staticmethod
def add_workflow_file(workflow_id, irb_doc_code, name, content_type, binary_data):
def add_workflow_file(workflow_id, irb_doc_code, task_spec_name, name, content_type, binary_data):
file_model = session.query(FileModel)\
.filter(FileModel.workflow_id == workflow_id)\
.filter(FileModel.name == name)\
.filter(FileModel.name == name) \
.filter(FileModel.task_spec == task_spec_name) \
.filter(FileModel.irb_doc_code == irb_doc_code).first()
if not file_model:
file_model = FileModel(
workflow_id=workflow_id,
name=name,
task_spec=task_spec_name,
irb_doc_code=irb_doc_code
)
return FileService.update_file(file_model, binary_data, content_type)
@ -175,7 +177,7 @@ class FileService(object):
user_uid = None
new_file_data_model = FileDataModel(
data=binary_data, file_model_id=file_model.id, file_model=file_model,
version=version, md5_hash=md5_checksum, date_created=datetime.utcnow(),
version=version, md5_hash=md5_checksum,
size=size, user_uid=user_uid
)
session.add_all([file_model, new_file_data_model])

13
crc/services/study_service.py Normal file → Executable file
View File

@ -42,7 +42,7 @@ class StudyService(object):
return True
return False
def get_studies_for_user(self, user):
def get_studies_for_user(self, user, include_invalid=False):
"""Returns a list of all studies for the given user."""
associated = session.query(StudyAssociated).filter_by(uid=user.uid, access=True).all()
associated_studies = [x.study_id for x in associated]
@ -51,7 +51,7 @@ class StudyService(object):
studies = []
for study_model in db_studies:
if self._is_valid_study(study_model.id):
if include_invalid or self._is_valid_study(study_model.id):
studies.append(StudyService.get_study(study_model.id, study_model, do_status=False))
return studies
@ -130,7 +130,7 @@ class StudyService(object):
return people
else:
raise ApiError('uid_not_associated_with_study', "user id %s was not associated with study number %d" % (uid,
study_id))
study_id))
@staticmethod
def get_study_associates(study_id):
@ -398,11 +398,6 @@ class StudyService(object):
session.add(db_study)
db_studies.append(db_study)
if pb_study.HSRNUMBER:
db_study.irb_status = IrbStatus.hsr_assigned
if db_study.status != StudyStatus.open_for_enrollment:
new_status = StudyStatus.open_for_enrollment
db_study.update_from_protocol_builder(pb_study)
StudyService._add_all_workflow_specs_to_study(db_study)
@ -469,6 +464,8 @@ class StudyService(object):
workflow_models = db.session.query(WorkflowModel). \
join(WorkflowSpecModel). \
filter(WorkflowSpecModel.is_master_spec == False). \
filter((WorkflowSpecModel.library == False) | \
(WorkflowSpecModel.library == None)). \
filter(WorkflowModel.study_id == study_id). \
all()
workflow_metas = []

14
crc/services/workflow_service.py Normal file → Executable file
View File

@ -244,6 +244,12 @@ class WorkflowService(object):
if field.has_property(Task.FIELD_PROP_REPEAT):
group = field.get_property(Task.FIELD_PROP_REPEAT)
if group in form_data and not(isinstance(form_data[group], list)):
raise ApiError.from_task("invalid_group",
f'You are grouping form fields inside a variable that is defined '
f'elsewhere: {group}. Be sure that you use a unique name for the '
f'for repeat and group expressions that is not also used for a field name.'
, task=task)
if field.has_property(Task.FIELD_PROP_REPEAT_HIDE_EXPRESSION):
result = WorkflowService.evaluate_property(Task.FIELD_PROP_REPEAT_HIDE_EXPRESSION, field, task)
if not result:
@ -289,7 +295,8 @@ class WorkflowService(object):
if not hasattr(task.task_spec, 'form'): return
for field in task.task_spec.form.fields:
data = task.data
if field.has_property(Task.FIELD_PROP_REPEAT):
# If we have a repeat field, make sure it is used before processing it
if field.has_property(Task.FIELD_PROP_REPEAT) and field.get_property(Task.FIELD_PROP_REPEAT) in task.data.keys():
repeat_array = task.data[field.get_property(Task.FIELD_PROP_REPEAT)]
for repeat_data in repeat_array:
WorkflowService.__post_process_field(task, field, repeat_data)
@ -329,7 +336,7 @@ class WorkflowService(object):
# Then you must evaluate the expression based on the data within the group, if that data exists.
# There may not be data available in the group, if no groups where added
group = field.get_property(Task.FIELD_PROP_REPEAT)
if group in task.data:
if group in task.data and len(task.data[group]) > 0:
# Here we must make the current group data top level (as it would be in a repeat section) but
# make all other top level task data available as well.
new_data = copy.deepcopy(task.data)
@ -781,6 +788,7 @@ class WorkflowService(object):
if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None:
associated = StudyService.get_study_associates(processor.workflow_model.study.id)
return [user.uid for user in associated if user.access]
if spiff_task.task_spec.lane not in spiff_task.data:
return [] # No users are assignable to the task at this moment
lane_users = spiff_task.data[spiff_task.task_spec.lane]
@ -790,7 +798,7 @@ class WorkflowService(object):
lane_uids = []
for user in lane_users:
if isinstance(user, dict):
if 'value' in user and user['value'] is not None:
if user.get("value"):
lane_uids.append(user['value'])
else:
raise ApiError.from_task(code="task_lane_user_error", message="Spiff Task %s lane user dict must have a key called 'value' with the user's uid in it." %

View File

@ -0,0 +1,40 @@
"""Remove HSR Number
Revision ID: 3d9ae7cfc231
Revises: 2a6f7ea00e5f
Create Date: 2021-08-16 11:28:40.027495
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3d9ae7cfc231'
down_revision = '2a6f7ea00e5f'
branch_labels = None
depends_on = None
def upgrade():
# Remove `hsr_assigned` values from Study table
op.execute("UPDATE study SET irb_status = 'incomplete_in_protocol_builder' where irb_status = 'hsr_assigned'")
# Remove `hsr_assigned` from IRB Status Enum
op.execute("ALTER TYPE irbstatus RENAME TO irbstatus_old")
op.execute("CREATE TYPE irbstatus AS ENUM('incomplete_in_protocol_builder', 'completed_in_protocol_builder')")
op.execute("ALTER TABLE study ALTER COLUMN irb_status TYPE irbstatus USING irb_status::text::irbstatus")
op.execute("DROP TYPE irbstatus_old")
# Remove hsr_number column from Study table
op.drop_column('study', 'hsr_number')
def downgrade():
op.execute("ALTER TYPE irbstatus RENAME TO irbstatus_old")
op.execute("CREATE TYPE irbstatus AS ENUM('incomplete_in_protocol_builder', 'completed_in_protocol_builder', 'hsr_assigned')")
op.execute("ALTER TABLE study ALTER COLUMN irb_status TYPE irbstatus USING irb_status::text::irbstatus")
op.execute("DROP TYPE irbstatus_old")
op.add_column('study', sa.Column('hsr_number', sa.String(), nullable=True))

View File

@ -0,0 +1,28 @@
"""Delete file stuff - add task_spec to file and data_store
Revision ID: 981156283cb9
Revises: 3d9ae7cfc231
Create Date: 2021-08-26 09:51:58.422819
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '981156283cb9'
down_revision = '3d9ae7cfc231'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('data_store', sa.Column('task_spec', sa.String(), nullable=True))
op.drop_column('data_store', 'task_id')
op.add_column('file', sa.Column('task_spec', sa.String(), nullable=True))
def downgrade():
op.drop_column('file', 'task_spec')
op.add_column('data_store', sa.Column('task_id', sa.VARCHAR(), autoincrement=False, nullable=True))
op.drop_column('data_store', 'task_spec')

View File

@ -0,0 +1,140 @@
<?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_08a4c34" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_DeleteTaskData" name="Delete Task Data" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_12ulmn8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:endEvent id="Event_19ssfc0">
<bpmn:incoming>SequenceFlow_1fgwvz0</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="Activity_UploadSingle" name="Upload Single" camunda:formKey="SingleFile">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="Study_Protocol_Document" label="Select File" type="file" />
<camunda:formField id="ShortDesc" label="Short Description" type="textarea">
<camunda:properties>
<camunda:property id="file_data" value="Study_Protocol_Document" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="VerDate" label="Version Date" type="date">
<camunda:properties>
<camunda:property id="file_data" value="Study_Protocol_Document" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_12ulmn8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_06786ls</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Activity_UploadRepeat" name="Upload Repeat" camunda:formKey="RepeatFile">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="Study_App_Doc" label="Select File" type="file">
<camunda:properties>
<camunda:property id="repeat" value="StudyAppDoc" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="ShortDesc" label="Short Description" type="textarea">
<camunda:properties>
<camunda:property id="repeat" value="StudyAppDoc" />
<camunda:property id="file_data" value="Study_App_Doc" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="VerDate" label="Version Date" type="date">
<camunda:properties>
<camunda:property id="repeat" value="StudyAppDoc" />
<camunda:property id="file_data" value="Study_App_Doc" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_06786ls</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0xo2jve</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Activity_DeleteSingle" name="Delete Single">
<bpmn:incoming>SequenceFlow_17j00uv</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0oa0av1</bpmn:outgoing>
<bpmn:script>delete_task_data(task_id='Activity_UploadSingle')</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_DeleteRepeat" name="Delete Repeat">
<bpmn:incoming>SequenceFlow_0oa0av1</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0hxvd1d</bpmn:outgoing>
<bpmn:script>delete_task_data(task_id='Activity_UploadRepeat')</bpmn:script>
</bpmn:scriptTask>
<bpmn:manualTask id="Activity_FilesUploaded" name="Files Uploaded">
<bpmn:documentation>## Files Uploaded</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0xo2jve</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17j00uv</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="Activity_FilesDeleted" name="Files Deleted">
<bpmn:documentation>## Files Deleted</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0hxvd1d</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1fgwvz0</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="SequenceFlow_12ulmn8" sourceRef="StartEvent_1" targetRef="Activity_UploadSingle" />
<bpmn:sequenceFlow id="SequenceFlow_06786ls" sourceRef="Activity_UploadSingle" targetRef="Activity_UploadRepeat" />
<bpmn:sequenceFlow id="SequenceFlow_0xo2jve" sourceRef="Activity_UploadRepeat" targetRef="Activity_FilesUploaded" />
<bpmn:sequenceFlow id="SequenceFlow_0oa0av1" sourceRef="Activity_DeleteSingle" targetRef="Activity_DeleteRepeat" />
<bpmn:sequenceFlow id="SequenceFlow_0hxvd1d" sourceRef="Activity_DeleteRepeat" targetRef="Activity_FilesDeleted" />
<bpmn:sequenceFlow id="SequenceFlow_1fgwvz0" sourceRef="Activity_FilesDeleted" targetRef="Event_19ssfc0" />
<bpmn:sequenceFlow id="SequenceFlow_17j00uv" sourceRef="Activity_FilesUploaded" targetRef="Activity_DeleteSingle" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_DeleteTaskData">
<bpmndi:BPMNEdge id="SequenceFlow_17j00uv_di" bpmnElement="SequenceFlow_17j00uv">
<di:waypoint x="661" y="160" />
<di:waypoint x="661" y="240" />
<di:waypoint x="327" y="240" />
<di:waypoint x="327" y="310" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1fgwvz0_di" bpmnElement="SequenceFlow_1fgwvz0">
<di:waypoint x="711" y="350" />
<di:waypoint x="772" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0hxvd1d_di" bpmnElement="SequenceFlow_0hxvd1d">
<di:waypoint x="544" y="350" />
<di:waypoint x="611" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0oa0av1_di" bpmnElement="SequenceFlow_0oa0av1">
<di:waypoint x="377" y="350" />
<di:waypoint x="444" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0xo2jve_di" bpmnElement="SequenceFlow_0xo2jve">
<di:waypoint x="544" y="120" />
<di:waypoint x="611" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_06786ls_di" bpmnElement="SequenceFlow_06786ls">
<di:waypoint x="377" y="120" />
<di:waypoint x="444" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_12ulmn8_di" bpmnElement="SequenceFlow_12ulmn8">
<di:waypoint x="215" y="120" />
<di:waypoint x="277" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_19ssfc0_di" bpmnElement="Event_19ssfc0">
<dc:Bounds x="772" y="332" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1hrox53_di" bpmnElement="Activity_UploadSingle">
<dc:Bounds x="277" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_04ypnfq_di" bpmnElement="Activity_UploadRepeat">
<dc:Bounds x="444" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1b1g8ix_di" bpmnElement="Activity_DeleteSingle">
<dc:Bounds x="277" y="310" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_17by4en_di" bpmnElement="Activity_DeleteRepeat">
<dc:Bounds x="444" y="310" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1pqjcpj_di" bpmnElement="Activity_FilesUploaded">
<dc:Bounds x="611" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_07qeio1_di" bpmnElement="Activity_FilesDeleted">
<dc:Bounds x="611" y="310" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -19,6 +19,13 @@
</camunda:validation>
</camunda:formField>
<camunda:formField id="cc" label="CC" type="string" />
<camunda:formField id="bcc" label="Bcc" type="string" />
<camunda:formField id="reply_to" label="Reply To" type="string" />
<camunda:formField id="doc_code" label="Doc Code" type="string">
<camunda:properties>
<camunda:property id="repeat" value="doc_codes" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0scd96e</bpmn:incoming>
@ -26,9 +33,8 @@
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0c60gne" sourceRef="Activity_EmailForm" targetRef="Activity_SendEmail" />
<bpmn:endEvent id="Event_EndEvent">
<bpmn:incoming>Flow_19fqvhc</bpmn:incoming>
<bpmn:incoming>Flow_0wv0swo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_19fqvhc" sourceRef="Activity_SendEmail" targetRef="Event_EndEvent" />
<bpmn:scriptTask id="Activity_SendEmail" name="Send Email">
<bpmn:documentation>Dear Person,
@ -42,18 +48,29 @@ Yours faithfully,
Dan</bpmn:documentation>
<bpmn:incoming>Flow_0c60gne</bpmn:incoming>
<bpmn:outgoing>Flow_19fqvhc</bpmn:outgoing>
<bpmn:outgoing>Flow_0xrm7iw</bpmn:outgoing>
<bpmn:script>if not 'cc' in globals():
cc=None
email(subject=subject, recipients=recipients, cc=cc)</bpmn:script>
if not 'bcc' in globals():
bcc=None
if not 'reply_to' in globals():
reply_to=None
attachments = []
if 'doc_codes' in globals():
for item in globals()['doc_codes']:
attachments.append(item['doc_code'])
email(subject=subject, recipients=recipients, cc=cc, bcc=bcc, reply_to=reply_to, attachments=attachments)</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0xrm7iw" sourceRef="Activity_SendEmail" targetRef="Activity_1lnjeej" />
<bpmn:sequenceFlow id="Flow_0wv0swo" sourceRef="Activity_1lnjeej" targetRef="Event_EndEvent" />
<bpmn:manualTask id="Activity_1lnjeej" name="Display Data">
<bpmn:incoming>Flow_0xrm7iw</bpmn:incoming>
<bpmn:outgoing>Flow_0wv0swo</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_fe6205f">
<bpmndi:BPMNEdge id="Flow_19fqvhc_di" bpmnElement="Flow_19fqvhc">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c60gne_di" bpmnElement="Flow_0c60gne">
<di:waypoint x="374" y="117" />
<di:waypoint x="430" y="117" />
@ -62,18 +79,29 @@ email(subject=subject, recipients=recipients, cc=cc)</bpmn:script>
<di:waypoint x="215" y="117" />
<di:waypoint x="274" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xrm7iw_di" bpmnElement="Flow_0xrm7iw">
<di:waypoint x="530" y="117" />
<di:waypoint x="580" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0wv0swo_di" bpmnElement="Flow_0wv0swo">
<di:waypoint x="680" y="117" />
<di:waypoint x="722" 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="Activity_0wqsfcj_di" bpmnElement="Activity_EmailForm">
<dc:Bounds x="274" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1wh1xsj_di" bpmnElement="Event_EndEvent">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ajacra_di" bpmnElement="Activity_SendEmail">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1wh1xsj_di" bpmnElement="Event_EndEvent">
<dc:Bounds x="722" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0eb7isk_di" bpmnElement="Activity_1lnjeej">
<dc:Bounds x="580" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,21 +1,18 @@
[
{
"DATE_MODIFIED": "2020-02-19T14:26:49.127756",
"HSRNUMBER": "12345",
"NETBADGEID": "dhf8r",
"STUDYID": 54321,
"TITLE": "Another study about the effect of a naked mannequin on software productivity"
},
{
"DATE_MODIFIED": "2020-02-19T14:24:55.101695",
"HSRNUMBER": "",
"NETBADGEID": "dhf8r",
"STUDYID": 65432,
"TITLE": "Peanut butter consumption among quiet dogs"
},
{
"DATE_MODIFIED": "2020-02-19T14:24:55.101695",
"HSRNUMBER": "",
"NETBADGEID": "dhf8r",
"STUDYID": 1,
"TITLE": "Efficacy of xenomorph bio-augmented circuits on dexterity of cybernetic prostheses"

View File

@ -0,0 +1,128 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.data_store import DataStoreModel
from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel
from crc.services.workflow_service import WorkflowService
from io import BytesIO
class TestDeleteTaskData(BaseTest):
def test_delete_task_data_validation(self):
self.load_example_data()
spec_model = self.load_test_spec('delete_task_data')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
# Make sure we don't get json returned. This would indicate an error.
self.assertEqual([], rv.json)
def test_delete_task_data(self):
self.load_example_data()
doc_code_1 = 'Study_Protocol_Document'
doc_code_2 = 'Study_App_Doc'
workflow = self.create_workflow('delete_task_data')
# Make sure there are no files uploaded for workflow yet
files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
self.assertEqual(0, len(files))
workflow_api = self.get_workflow_api(workflow)
first_task = workflow_api.next_task
# Upload Single File
data = {'file': (BytesIO(b"abcdef"), 'test_file.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, first_task.name, doc_code_1), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id = rv.json['id']
self.complete_form(workflow, first_task, {doc_code_1: {'id': file_id},
'VerDate': '20210721'})
# Make sure we have 1 file
files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
self.assertEqual(1, len(files))
# Make sure data store is set
data_store = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id).all()
self.assertEqual('VerDate', data_store[0].key)
self.assertEqual('20210721', data_store[0].value)
workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task
# Upload 2 Files
data = {'file': (BytesIO(b"abcdef"), 'test_file_1.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, first_task.name, doc_code_2), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id_1 = rv.json['id']
data = {'file': (BytesIO(b"ghijk"), 'test_file_2.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, first_task.name, doc_code_2), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id_2 = rv.json['id']
self.complete_form(workflow, second_task, {'StudyAppDoc': [{'Study_App_Doc': {'id': file_id_1},
'VerDate': '20210701',
'ShortDesc': 'Short Description 1'},
{'Study_App_Doc': {'id': file_id_2},
'VerDate': '20210702',
'ShortDesc': 'Short Description 2'}
]})
# Make sure we have 2 more files
files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
self.assertEqual(3, len(files))
# Make sure data stores are set for new files
data_stores_1 = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id_1).all()
for data_store in data_stores_1:
if data_store.key == 'VerDate':
self.assertEqual('20210701', data_store.value)
elif data_store.key == 'ShortDesc':
self.assertEqual('Short Description 1', data_store.value)
data_stores_2 = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id_2).all()
for data_store in data_stores_2:
if data_store.key == 'VerDate':
self.assertEqual('20210702', data_store.value)
elif data_store.key == 'ShortDesc':
self.assertEqual('Short Description 2', data_store.value)
# Make sure we have something in task_events
task_events = session.query(TaskEventModel).\
filter(TaskEventModel.workflow_id == workflow.id).\
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_COMPLETE).all()
for task_event in task_events:
self.assertNotEqual({}, task_event.form_data)
workflow_api = self.get_workflow_api(workflow)
third_task = workflow_api.next_task
# This calls the delete task file data script
self.complete_form(workflow, third_task, {})
self.get_workflow_api(workflow)
# Make sure files, data_stores, and task_events are deleted
data_stores = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id).all()
data_stores_1 = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id_1).all()
data_stores_2 = session.query(DataStoreModel).filter(DataStoreModel.file_id == file_id_2).all()
files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
task_events = session.query(TaskEventModel).\
filter(TaskEventModel.workflow_id == workflow.id).\
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_COMPLETE).all()
self.assertEqual(0, len(data_stores))
self.assertEqual(0, len(data_stores_1))
self.assertEqual(0, len(data_stores_2))
self.assertEqual(0, len(files))
for task_event in task_events:
self.assertEqual({}, task_event.form_data)

View File

@ -59,10 +59,12 @@ class TestFileService(BaseTest):
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
# Add the file again with different data
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code=irb_code)
@ -83,11 +85,13 @@ class TestFileService(BaseTest):
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
# Add the file again with different data
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'5678')
@ -100,6 +104,7 @@ class TestFileService(BaseTest):
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
@ -117,6 +122,7 @@ class TestFileService(BaseTest):
# Add the file again with different data
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'5678')
@ -138,11 +144,13 @@ class TestFileService(BaseTest):
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
# Add the file again with different data
FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="a_different_thing.png", content_type="text",
binary_data=b'5678')
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
@ -159,9 +167,10 @@ class TestFileService(BaseTest):
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file_model = FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
name="anything.png", content_type="text",
binary_data=b'1234')
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
FileService.update_from_github([file_model.id])
file_model_data = FileDataModel.query.filter_by(
@ -182,9 +191,10 @@ class TestFileService(BaseTest):
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file_model = FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
name="anything.png", content_type="text",
binary_data=b'1234')
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
result = FileService.publish_to_github([file_model.id])
self.assertEqual(result['created'], True)
@ -200,9 +210,10 @@ class TestFileService(BaseTest):
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file_model = FileService.add_workflow_file(workflow_id=workflow.id,
irb_doc_code=irb_code,
name="anything.png", content_type="text",
binary_data=b'1234')
irb_doc_code=irb_code,
task_spec_name=task.get_name(),
name="anything.png", content_type="text",
binary_data=b'1234')
result = FileService.publish_to_github([file_model.id])
self.assertEqual(result['updated'], True)

View File

@ -77,8 +77,8 @@ class TestFilesApi(BaseTest):
correct_name = task.task_spec.form.fields[0].id
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%i&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, correct_name), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.get_name(), correct_name), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
@ -93,8 +93,8 @@ class TestFilesApi(BaseTest):
correct_name = task.task_spec.form.fields[0].id
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%i&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, correct_name), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.get_name(), correct_name), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
@ -261,8 +261,8 @@ class TestFilesApi(BaseTest):
correct_name = task.task_spec.form.fields[0].id
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%i&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, correct_name), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.get_name(), correct_name), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
@ -275,10 +275,10 @@ class TestFilesApi(BaseTest):
self.assertEqual(len(json_data), 1)
# Add another file for a different document type
FileService().add_workflow_file(workflow.id, 'Study_App_Doc', 'otherdoc.docx',
FileService().add_workflow_file(workflow.id, 'Study_App_Doc', task.get_name(), 'otherdoc.docx',
'application/xcode', b"asdfasdf")
# Note: this call can be made WITHOUT the task id.
# Note: this call can be made WITHOUT the task spec name.
rv = self.app.get('/v1.0/file?study_id=%i&workflow_id=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, correct_name), follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
@ -295,8 +295,8 @@ class TestFilesApi(BaseTest):
correct_name = task.task_spec.form.fields[0].id
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%i&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, correct_name), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.get_name(), correct_name), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))

View File

@ -87,6 +87,7 @@ class TestStudyApi(BaseTest):
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=task.get_name(),
name="anything.png", content_type="png",
binary_data=b'1234', irb_doc_code=irb_code)
@ -190,17 +191,20 @@ class TestStudyApi(BaseTest):
num_abandoned += 1
if study['status'] == 'in_progress': # One study is marked complete without HSR Number
num_in_progress += 1
if study['status'] == 'open_for_enrollment': # One study is marked complete and has an HSR Number
if study['status'] == 'open_for_enrollment': # Currently, we don't automatically set studies to open for enrollment
num_open += 1
db_studies_after = session.query(StudyModel).all()
num_db_studies_after = len(db_studies_after)
self.assertGreater(num_db_studies_after, num_db_studies_before)
self.assertEqual(num_abandoned, 1)
self.assertEqual(num_open, 1)
self.assertEqual(num_open, 0) # Currently, we don't automatically set studies to open for enrollment
self.assertEqual(num_in_progress, 2)
self.assertEqual(len(json_data), num_db_studies_after)
self.assertEqual(num_open + num_in_progress + num_abandoned, num_db_studies_after)
# The sum below is off, since we don't automatically set studies to Open for Enrollment
# Leaving the test here because we will need it again
# when we implement a new way to set Open for Enrollment
# self.assertEqual(num_open + num_in_progress + num_abandoned, num_db_studies_after)
# Automatic events check
in_progress_events = session.query(StudyEvent).filter_by(status=StudyStatus.in_progress)
@ -209,8 +213,11 @@ class TestStudyApi(BaseTest):
abandoned_events = session.query(StudyEvent).filter_by(status=StudyStatus.abandoned)
self.assertEqual(abandoned_events.count(), 1) # 1 study has been abandoned
open_for_enrollment_events = session.query(StudyEvent).filter_by(status=StudyStatus.open_for_enrollment)
self.assertEqual(open_for_enrollment_events.count(), 1) # 1 study was moved to open for enrollment
# We don't currently set any studies to Open for Enrollment automatically
# Leaving the test here because we will need it again
# when we implement a new way to set Open for Enrollment
# open_for_enrollment_events = session.query(StudyEvent).filter_by(status=StudyStatus.open_for_enrollment)
# self.assertEqual(open_for_enrollment_events.count(), 1) # 1 study was moved to open for enrollment
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
@ -252,7 +259,7 @@ class TestStudyApi(BaseTest):
def test_delete_workflow(self):
self.load_example_data()
workflow = session.query(WorkflowModel).first()
FileService.add_workflow_file(workflow_id=workflow.id,
FileService.add_workflow_file(workflow_id=workflow.id, task_spec_name='TaskSpec01',
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )

View File

@ -56,7 +56,6 @@ class TestStudyDetailsDocumentsScript(BaseTest):
@patch('crc.services.protocol_builder.requests.get')
def test_no_validation_error_when_correct_file_exists(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
@ -105,8 +104,9 @@ class TestStudyDetailsDocumentsScript(BaseTest):
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file = FileService.add_workflow_file(workflow_id=workflow_model.id,
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
task_spec_name='Acitivity01',
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
processor = WorkflowProcessor(workflow_model)
task = processor.next_task()
FileDataSet().do_task(task, study.id, workflow_model.id, key="ginger", value="doodle", file_id=file.id)
@ -126,8 +126,9 @@ class TestStudyDetailsDocumentsScript(BaseTest):
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file = FileService.add_workflow_file(workflow_id=workflow_model.id,
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
task_spec_name='TaskSpec01',
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
processor = WorkflowProcessor(workflow_model)
task = processor.next_task()
FileDataSet().do_task(task, study.id, workflow_model.id, key="irb_code", value="Study_App_Doc", file_id=file.id)
@ -148,10 +149,11 @@ class TestStudyDetailsDocumentsScript(BaseTest):
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
file = FileService.add_workflow_file(workflow_id=workflow_model.id,
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
task_spec_name='Activity01',
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
processor = WorkflowProcessor(workflow_model)
task = processor.next_task()
with self.assertRaises(ApiError):
FileDataSet().do_task(task, study.id, workflow_model.id, key="irb_code", value="My_Pretty_Pony",
file_id=file.id)
file_id=file.id)

View File

@ -147,6 +147,7 @@ class TestStudyService(BaseTest):
workflow = self.create_workflow('docx')
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name='t1',
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
@ -169,12 +170,15 @@ class TestStudyService(BaseTest):
# Add files to both workflows.
FileService.add_workflow_file(workflow_id=workflow1.id,
task_spec_name="t1",
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code="UVACompl_PRCAppr" )
FileService.add_workflow_file(workflow_id=workflow1.id,
task_spec_name="t1",
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code="AD_Consent_Model")
FileService.add_workflow_file(workflow_id=workflow2.id,
task_spec_name="t1",
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code="UVACompl_PRCAppr" )

View File

@ -16,10 +16,12 @@ class TestDocumentDirectories(BaseTest):
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="something.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_1)
# Add second file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code=irb_code_2)

View File

@ -1,8 +1,7 @@
from tests.base_test import BaseTest
from crc import mail, session
from crc.models.study import StudyModel
from crc import mail
from crc.services.file_service import FileService
from crc.services.study_service import StudyService
import json
class TestEmailScript(BaseTest):
@ -20,17 +19,21 @@ class TestEmailScript(BaseTest):
with mail.record_messages() as outbox:
workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task
self.complete_form(workflow, first_task, {'subject': 'My Email Subject', 'recipients': 'test@example.com'})
self.complete_form(workflow, first_task, {'subject': 'My Email Subject', 'recipients': 'test@example.com',
'cc': 'cc@example.com', 'bcc': 'bcc@example.com',
'reply_to': 'reply_to@example.com'})
self.assertEqual(1, len(outbox))
self.assertEqual('My Email Subject', outbox[0].subject)
self.assertEqual(['test@example.com'], outbox[0].recipients)
self.assertEqual(['cc@example.com'], outbox[0].cc)
self.assertEqual(['bcc@example.com'], outbox[0].bcc)
self.assertEqual('reply_to@example.com', outbox[0].reply_to)
self.assertIn('Thank you for using this email example', outbox[0].body)
def test_email_script_multiple(self):
self.load_example_data()
with mail.record_messages() as outbox:
workflow = self.create_workflow('email_script')
@ -80,19 +83,33 @@ class TestEmailScript(BaseTest):
self.assertIn(outbox[0].recipients[1], ['user@example.com', 'dhf8r@virginia.edu', 'lb3dp@virginia.edu'])
self.assertIn(outbox[0].recipients[2], ['user@example.com', 'dhf8r@virginia.edu', 'lb3dp@virginia.edu'])
def test_email_script_cc(self):
def test_email_script_attachments(self):
self.load_example_data()
irb_code_1 = 'Study_App_Doc'
irb_code_2 = 'Study_Protocol_Document'
workflow = self.create_workflow('email_script')
workflow_api = self.get_workflow_api(workflow)
self.create_user(uid='lb3dp', email='lb3dp@virginia.edu', display_name='Laura Barnes')
StudyService.update_study_associates(workflow.study_id,
[{'uid': 'dhf8r', 'role': 'Chief Bee Keeper', 'send_email': True, 'access': True},
{'uid': 'lb3dp', 'role': 'Chief Cat Herder', 'send_email': True, 'access': True}])
first_task = workflow_api.next_task
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="something.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_1)
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="another.png", content_type="text",
binary_data=b'67890', irb_doc_code=irb_code_1)
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code=irb_code_2)
with mail.record_messages() as outbox:
self.complete_form(workflow, first_task, {'subject': 'My Test Subject', 'recipients': 'user@example.com', 'cc': 'associated'})
self.complete_form(workflow, first_task, {'subject': 'My Test Subject', 'recipients': 'user@example.com',
'doc_codes': [{'doc_code': irb_code_1}, {'doc_code': irb_code_2}]})
self.assertEqual(1, len(outbox))
self.assertEqual('user@example.com', outbox[0].recipients[0])
self.assertIn(outbox[0].cc[0], ['dhf8r@virginia.edu', 'lb3dp@virginia.edu'])
self.assertIn(outbox[0].cc[1], ['dhf8r@virginia.edu', 'lb3dp@virginia.edu'])
self.assertEqual(3, len(outbox[0].attachments))
self.assertEqual('image/png', outbox[0].attachments[0].content_type)
self.assertEqual('something.png', outbox[0].attachments[0].filename)
self.assertEqual(b'1234', outbox[0].attachments[0].data)

View File

@ -20,6 +20,7 @@ class TestFileDatastore(BaseTest):
workflow = self.create_workflow('file_data_store')
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name='task1',
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
@ -40,8 +41,8 @@ class TestFileDatastore(BaseTest):
# upload the file
correct_name = task.form['fields'][1]['id']
data = {'file': (BytesIO(b"abcdef"), 'test_file.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, correct_name), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.name, correct_name), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id = json.loads(rv.get_data())['id']

View File

@ -21,6 +21,7 @@ class TestIsFileUploaded(BaseTest):
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="something.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_1)
@ -31,6 +32,7 @@ class TestIsFileUploaded(BaseTest):
# Add second file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code=irb_code_2)

View File

@ -40,7 +40,7 @@ class TestTimerEvent(BaseTest):
with self.assertLogs('crc', level='ERROR') as cm:
WorkflowService.do_waiting()
self.assertEqual(1, len(cm.output))
self.assertRegexpMatches(cm.output[0], f"workflow #%i" % workflow.id)
self.assertRegexpMatches(cm.output[0], f"study #%i" % workflow.study_id)
self.assertRegex(cm.output[0], f"workflow #%i" % workflow.id)
self.assertRegex(cm.output[0], f"study #%i" % workflow.study_id)
self.assertTrue(wf.status == WorkflowStatus.waiting)

View File

@ -1,3 +1,4 @@
from crc.models.workflow import WorkflowLibraryModel
from tests.base_test import BaseTest
from crc import session
@ -60,5 +61,34 @@ class TestWorkflowApi(BaseTest):
self.assertIsNotNone(returned.get('libraries'))
self.assertEqual(len(returned['libraries']),0)
def test_library_cleanup(self):
self.load_example_data()
spec1 = ExampleDataLoader().create_spec('hello_world', 'Hello World', category_id=0, library=False,
from_tests=True)
spec2 = ExampleDataLoader().create_spec('hello_world_lib', 'Hello World Library', category_id=0, library=True,
from_tests=True)
user = session.query(UserModel).first()
self.assertIsNotNone(user)
rv = self.app.post(f'/v1.0/workflow-specification/%s/library/%s'%(spec1.id,spec2.id),
follow_redirects=True,
content_type="application/json",
headers=self.logged_in_headers())
self.assert_success(rv)
rv = self.app.get(f'/v1.0/workflow-specification/%s'%spec1.id,follow_redirects=True,
content_type="application/json",
headers=self.logged_in_headers())
returned=rv.json
lib = session.query(WorkflowLibraryModel).filter(WorkflowLibraryModel.library_spec_id==spec2.id).first()
self.assertIsNotNone(lib)
rv = self.app.delete(f'/v1.0/workflow-specification/%s'%(spec1.id),follow_redirects=True,
content_type="application/json",
headers=self.logged_in_headers())
lib = session.query(WorkflowLibraryModel).filter(WorkflowLibraryModel.library_spec_id==spec2.id).first()
self.assertIsNone(lib)

View File

@ -24,6 +24,7 @@ class TestDeleteIRBDocument(BaseTest):
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="filename.txt", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
# Assert we have the file
@ -61,10 +62,12 @@ class TestDeleteIRBDocument(BaseTest):
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="filename.txt", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_1)
# Add another file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="filename.txt", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_2)
self.assertEqual(True, IsFileUploaded.do_task(

View File

@ -17,8 +17,8 @@ class TestHiddenFileDataField(BaseTest):
task = workflow_api.next_task
data = {'file': (BytesIO(b"abcdef"), 'test_file.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, 'Study_App_Doc'), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.name, 'Study_App_Doc'), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id = json.loads(rv.get_data())['id']
@ -43,8 +43,8 @@ class TestHiddenFileDataField(BaseTest):
task = workflow_api.next_task
data = {'file': (BytesIO(b"abcdef"), 'test_file.txt')}
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_id=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.id, 'Study_App_Doc'), data=data, follow_redirects=True,
rv = self.app.post('/v1.0/file?study_id=%i&workflow_id=%s&task_spec_name=%s&form_field_key=%s' %
(workflow.study_id, workflow.id, task.name, 'Study_App_Doc'), data=data, follow_redirects=True,
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
file_id = json.loads(rv.get_data())['id']

View File

@ -54,6 +54,7 @@ class TestWorkflowRestart(BaseTest):
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=first_task.name,
name="filename.txt", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
# Assert we have the file