Merge branch 'dev' into dmn-from-spreadsheet-395
This commit is contained in:
commit
0c31b091ee
|
@ -14,7 +14,7 @@ local.properties
|
|||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
.vscode/
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
|
|
|
@ -979,7 +979,7 @@
|
|||
},
|
||||
"spiffworkflow": {
|
||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||
"ref": "0b4a878f9b6d4f7fc320c26f59ca5e458a6130e8"
|
||||
"ref": "07990f4af2d89587c74613db9db036ab09967780"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
|
|
10
crc/api.yml
10
crc/api.yml
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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." %
|
||||
|
|
|
@ -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))
|
|
@ -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')
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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" )
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" )
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue