2020-06-22 20:07:57 +00:00
|
|
|
import json
|
|
|
|
import pickle
|
|
|
|
from base64 import b64decode
|
2020-06-22 13:14:00 +00:00
|
|
|
from datetime import datetime, timedelta
|
2020-05-24 05:53:48 +00:00
|
|
|
|
2020-06-22 13:14:00 +00:00
|
|
|
from sqlalchemy import desc, func
|
2020-05-24 20:13:15 +00:00
|
|
|
|
2020-06-05 02:37:28 +00:00
|
|
|
from crc import app, db, session
|
2020-05-29 00:03:50 +00:00
|
|
|
from crc.api.common import ApiError
|
2020-05-24 20:13:15 +00:00
|
|
|
|
2020-06-02 22:17:00 +00:00
|
|
|
from crc.models.approval import ApprovalModel, ApprovalStatus, ApprovalFile, Approval
|
|
|
|
from crc.models.study import StudyModel
|
2020-05-29 00:03:50 +00:00
|
|
|
from crc.models.workflow import WorkflowModel
|
2020-05-24 20:13:15 +00:00
|
|
|
from crc.services.file_service import FileService
|
2020-06-05 02:37:28 +00:00
|
|
|
from crc.services.ldap_service import LdapService
|
|
|
|
from crc.services.mails import (
|
|
|
|
send_ramp_up_submission_email,
|
2020-06-05 18:08:31 +00:00
|
|
|
send_ramp_up_approval_request_email,
|
2020-06-05 02:37:28 +00:00
|
|
|
send_ramp_up_approval_request_first_review_email,
|
2020-06-05 18:08:31 +00:00
|
|
|
send_ramp_up_approved_email,
|
|
|
|
send_ramp_up_denied_email,
|
|
|
|
send_ramp_up_denied_email_to_approver
|
2020-06-05 02:37:28 +00:00
|
|
|
)
|
2020-05-24 05:53:48 +00:00
|
|
|
|
|
|
|
class ApprovalService(object):
|
|
|
|
"""Provides common tools for working with an Approval"""
|
|
|
|
|
2020-06-02 22:17:00 +00:00
|
|
|
@staticmethod
|
2020-06-05 21:49:55 +00:00
|
|
|
def __one_approval_from_study(study, approver_uid = None, status=None,
|
|
|
|
include_cancelled=True):
|
2020-06-02 22:17:00 +00:00
|
|
|
"""Returns one approval, with all additional approvals as 'related_approvals',
|
|
|
|
the main approval can be pinned to an approver with an optional argument.
|
|
|
|
Will return null if no approvals exist on the study."""
|
|
|
|
main_approval = None
|
|
|
|
related_approvals = []
|
2020-06-05 21:49:55 +00:00
|
|
|
query = db.session.query(ApprovalModel).filter(ApprovalModel.study_id == study.id)
|
2020-06-03 21:34:27 +00:00
|
|
|
if not include_cancelled:
|
2020-06-03 11:58:48 +00:00
|
|
|
query=query.filter(ApprovalModel.status != ApprovalStatus.CANCELED.value)
|
2020-06-05 19:54:53 +00:00
|
|
|
approvals = query.all() # All non-cancelled approvals.
|
2020-06-03 11:58:48 +00:00
|
|
|
|
2020-06-02 22:17:00 +00:00
|
|
|
for approval_model in approvals:
|
|
|
|
if approval_model.approver_uid == approver_uid:
|
2020-06-05 21:49:55 +00:00
|
|
|
main_approval = approval_model
|
2020-06-02 22:17:00 +00:00
|
|
|
else:
|
2020-06-05 21:49:55 +00:00
|
|
|
related_approvals.append(approval_model)
|
2020-06-05 18:33:00 +00:00
|
|
|
|
|
|
|
# IF WE ARE JUST RETURNING ALL OF THE APPROVALS PER STUDY
|
2020-06-02 22:17:00 +00:00
|
|
|
if not main_approval and len(related_approvals) > 0:
|
|
|
|
main_approval = related_approvals[0]
|
|
|
|
related_approvals = related_approvals[1:]
|
2020-06-05 18:33:00 +00:00
|
|
|
|
2020-06-05 19:54:53 +00:00
|
|
|
if main_approval is not None: # May be null if the study has no approvals.
|
2020-06-05 21:49:55 +00:00
|
|
|
final_status = ApprovalService.__calculate_overall_approval_status(main_approval, related_approvals)
|
|
|
|
if status and final_status != status: return # Now that we are certain of the status, filter on it.
|
|
|
|
|
|
|
|
main_approval = Approval.from_model(main_approval)
|
|
|
|
main_approval.status = final_status
|
|
|
|
for ra in related_approvals:
|
|
|
|
main_approval.related_approvals.append(Approval.from_model(ra))
|
2020-06-05 19:54:53 +00:00
|
|
|
|
2020-06-02 22:17:00 +00:00
|
|
|
return main_approval
|
|
|
|
|
2020-05-24 05:53:48 +00:00
|
|
|
@staticmethod
|
2020-06-05 21:49:55 +00:00
|
|
|
def __calculate_overall_approval_status(approval, related):
|
2020-06-05 18:33:00 +00:00
|
|
|
# In the case of pending approvals, check to see if there is a related approval
|
|
|
|
# that proceeds this approval - and if it is declined, or still pending, then change
|
2020-06-05 19:54:53 +00:00
|
|
|
# the state of the approval to be Declined, or Waiting respectively.
|
2020-06-05 18:33:00 +00:00
|
|
|
if approval.status == ApprovalStatus.PENDING.value:
|
2020-06-05 21:49:55 +00:00
|
|
|
for ra in related:
|
2020-06-05 18:33:00 +00:00
|
|
|
if ra.id < approval.id:
|
|
|
|
if ra.status == ApprovalStatus.DECLINED.value or ra.status == ApprovalStatus.CANCELED.value:
|
|
|
|
return ra.status # If any prior approval id declined or cancelled so is this approval.
|
|
|
|
elif ra.status == ApprovalStatus.PENDING.value:
|
|
|
|
return ApprovalStatus.AWAITING.value # if any prior approval is pending, then this is waiting.
|
2020-06-05 18:55:49 +00:00
|
|
|
return approval.status
|
2020-06-05 18:33:00 +00:00
|
|
|
else:
|
|
|
|
return approval.status
|
|
|
|
|
2020-05-24 05:53:48 +00:00
|
|
|
@staticmethod
|
2020-06-05 21:49:55 +00:00
|
|
|
def get_approvals_per_user(approver_uid, status=None, include_cancelled=False):
|
2020-06-02 22:17:00 +00:00
|
|
|
"""Returns a list of approval objects (not db models) for the given
|
|
|
|
approver. """
|
|
|
|
studies = db.session.query(StudyModel).join(ApprovalModel).\
|
|
|
|
filter(ApprovalModel.approver_uid == approver_uid).all()
|
|
|
|
approvals = []
|
|
|
|
for study in studies:
|
2020-06-05 21:49:55 +00:00
|
|
|
approval = ApprovalService.__one_approval_from_study(study, approver_uid,
|
|
|
|
status, include_cancelled)
|
2020-06-02 22:17:00 +00:00
|
|
|
if approval:
|
|
|
|
approvals.append(approval)
|
|
|
|
return approvals
|
|
|
|
|
|
|
|
@staticmethod
|
2020-06-03 21:34:27 +00:00
|
|
|
def get_all_approvals(include_cancelled=True):
|
2020-06-02 22:17:00 +00:00
|
|
|
"""Returns a list of all approval objects (not db models), one record
|
|
|
|
per study, with any associated approvals grouped under the first approval."""
|
|
|
|
studies = db.session.query(StudyModel).all()
|
|
|
|
approvals = []
|
|
|
|
for study in studies:
|
2020-06-03 21:34:27 +00:00
|
|
|
approval = ApprovalService.__one_approval_from_study(study, include_cancelled=include_cancelled)
|
2020-06-02 22:17:00 +00:00
|
|
|
if approval:
|
|
|
|
approvals.append(approval)
|
|
|
|
return approvals
|
2020-05-24 05:53:48 +00:00
|
|
|
|
2020-06-01 02:46:17 +00:00
|
|
|
@staticmethod
|
2020-06-03 21:34:27 +00:00
|
|
|
def get_approvals_for_study(study_id, include_cancelled=True):
|
2020-06-02 22:17:00 +00:00
|
|
|
"""Returns an array of Approval objects for the study, it does not
|
|
|
|
compute the related approvals."""
|
2020-06-03 21:34:27 +00:00
|
|
|
query = session.query(ApprovalModel).filter_by(study_id=study_id)
|
|
|
|
if not include_cancelled:
|
|
|
|
query = query.filter(ApprovalModel.status != ApprovalStatus.CANCELED.value)
|
|
|
|
db_approvals = query.all()
|
2020-06-02 22:17:00 +00:00
|
|
|
return [Approval.from_model(approval_model) for approval_model in db_approvals]
|
2020-06-01 02:46:17 +00:00
|
|
|
|
2020-06-22 13:14:00 +00:00
|
|
|
@staticmethod
|
2020-06-22 20:07:57 +00:00
|
|
|
def get_approval_details(approval):
|
|
|
|
"""Returns a list of packed approval details, obtained from
|
|
|
|
the task data sent during the workflow """
|
|
|
|
def extract_value(task, key):
|
|
|
|
if key in task['data']:
|
|
|
|
return pickle.loads(b64decode(task['data'][key]['__bytes__']))
|
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
def find_task(uuid, task):
|
|
|
|
if task['id']['__uuid__'] == uuid:
|
|
|
|
return task
|
|
|
|
for child in task['children']:
|
|
|
|
task = find_task(uuid, child)
|
|
|
|
if task:
|
|
|
|
return task
|
|
|
|
|
|
|
|
if approval.status != ApprovalStatus.APPROVED.value:
|
|
|
|
return {}
|
|
|
|
for related_approval in approval.related_approvals:
|
|
|
|
if related_approval.status != ApprovalStatus.APPROVED.value:
|
|
|
|
continue
|
|
|
|
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
|
|
|
|
data = json.loads(workflow.bpmn_workflow_json)
|
|
|
|
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
|
|
|
|
personnel = extract_value(last_task, 'personnel')
|
|
|
|
training_val = extract_value(last_task, 'RequiredTraining')
|
|
|
|
pi_supervisor = extract_value(last_task, 'PISupervisor')['value']
|
|
|
|
review_complete = 'AllRequiredTraining' in training_val
|
|
|
|
pi_uid = workflow.study.primary_investigator_id
|
|
|
|
pi_details = LdapService.user_info(pi_uid)
|
|
|
|
details = {'Supervisor': pi_supervisor}
|
|
|
|
details['person_details'] = []
|
|
|
|
details['person_details'].append(pi_details)
|
|
|
|
for person in personnel:
|
|
|
|
uid = person['PersonnelComputingID']['value']
|
|
|
|
details['person_details'].append(LdapService.user_info(uid))
|
|
|
|
|
|
|
|
return details
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_health_attesting_records():
|
|
|
|
"""Return a list with prepared information related to all approvals """
|
|
|
|
|
|
|
|
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
|
2020-06-22 13:14:00 +00:00
|
|
|
|
|
|
|
health_attesting_rows = [
|
2020-06-22 15:24:58 +00:00
|
|
|
['university_computing_id',
|
|
|
|
'last_name',
|
|
|
|
'first_name',
|
|
|
|
'department',
|
|
|
|
'job_title',
|
|
|
|
'supervisor_university_computing_id']
|
2020-06-22 13:14:00 +00:00
|
|
|
]
|
2020-06-22 20:07:57 +00:00
|
|
|
|
2020-06-22 13:14:00 +00:00
|
|
|
for approval in approvals:
|
2020-06-22 20:07:57 +00:00
|
|
|
try:
|
|
|
|
details = ApprovalService.get_approval_details(approval)
|
|
|
|
if not details:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for person in details['person_details']:
|
|
|
|
first_name = person.given_name
|
|
|
|
last_name = person.display_name.replace(first_name, '').strip()
|
|
|
|
record = [
|
|
|
|
person.uid,
|
|
|
|
last_name,
|
|
|
|
first_name,
|
|
|
|
'',
|
|
|
|
'Academic Researcher',
|
|
|
|
details['Supervisor'] if person.uid == details['person_details'][0].uid else 'askresearch'
|
|
|
|
]
|
|
|
|
|
|
|
|
if record not in health_attesting_rows:
|
|
|
|
health_attesting_rows.append(record)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
app.logger.error("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
2020-06-22 13:14:00 +00:00
|
|
|
|
|
|
|
return health_attesting_rows
|
2020-05-24 05:53:48 +00:00
|
|
|
|
2020-06-22 20:07:57 +00:00
|
|
|
@staticmethod
|
|
|
|
def get_not_really_csv_content():
|
|
|
|
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
|
|
|
|
output = []
|
|
|
|
errors = []
|
|
|
|
for approval in approvals:
|
|
|
|
try:
|
|
|
|
details = ApprovalService.get_approval_details(approval)
|
|
|
|
|
|
|
|
for person in details['person_details']:
|
|
|
|
record = {
|
|
|
|
"study_id": approval.study_id,
|
|
|
|
"pi_uid": pi_details.uid,
|
|
|
|
"pi": pi_details.display_name,
|
|
|
|
"name": person.display_name,
|
|
|
|
"uid": person.uid,
|
|
|
|
"email": person.email_address,
|
|
|
|
"supervisor": details['Supervisor'] if person.uid == details['person_details'][0].uid else "",
|
|
|
|
"review_complete": review_complete,
|
|
|
|
}
|
|
|
|
|
|
|
|
output.append(record)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
|
|
|
return {"results": output, "errors": errors }
|
|
|
|
|
2020-05-24 05:53:48 +00:00
|
|
|
@staticmethod
|
2020-06-05 19:39:52 +00:00
|
|
|
def update_approval(approval_id, approver_uid):
|
2020-06-22 15:24:58 +00:00
|
|
|
"""Update a specific approval
|
|
|
|
NOTE: Actual update happens in the API layer, this
|
|
|
|
funtion is currently in charge of only sending
|
|
|
|
corresponding emails
|
|
|
|
"""
|
2020-05-24 05:53:48 +00:00
|
|
|
db_approval = session.query(ApprovalModel).get(approval_id)
|
2020-06-05 19:39:52 +00:00
|
|
|
status = db_approval.status
|
2020-05-24 05:53:48 +00:00
|
|
|
if db_approval:
|
2020-06-04 17:43:10 +00:00
|
|
|
if status == ApprovalStatus.APPROVED.value:
|
2020-06-05 18:08:31 +00:00
|
|
|
# second_approval = ApprovalModel().query.filter_by(
|
|
|
|
# study_id=db_approval.study_id, workflow_id=db_approval.workflow_id,
|
|
|
|
# status=ApprovalStatus.PENDING.value, version=db_approval.version).first()
|
|
|
|
# if second_approval:
|
2020-06-04 17:43:10 +00:00
|
|
|
# send rrp approval request for second approver
|
2020-06-05 18:08:31 +00:00
|
|
|
ldap_service = LdapService()
|
2020-06-05 19:39:52 +00:00
|
|
|
pi_user_info = ldap_service.user_info(db_approval.study.primary_investigator_id)
|
2020-06-05 18:08:31 +00:00
|
|
|
approver_info = ldap_service.user_info(approver_uid)
|
|
|
|
# send rrp submission
|
2020-06-08 15:16:26 +00:00
|
|
|
mail_result = send_ramp_up_approved_email(
|
2020-06-05 18:08:31 +00:00
|
|
|
'askresearch@virginia.edu',
|
|
|
|
[pi_user_info.email_address],
|
|
|
|
f'{approver_info.display_name} - ({approver_info.uid})'
|
|
|
|
)
|
2020-06-08 15:16:26 +00:00
|
|
|
if mail_result:
|
|
|
|
app.logger.error(mail_result)
|
2020-06-04 17:43:10 +00:00
|
|
|
elif status == ApprovalStatus.DECLINED.value:
|
2020-06-05 18:08:31 +00:00
|
|
|
ldap_service = LdapService()
|
2020-06-05 19:39:52 +00:00
|
|
|
pi_user_info = ldap_service.user_info(db_approval.study.primary_investigator_id)
|
2020-06-05 18:08:31 +00:00
|
|
|
approver_info = ldap_service.user_info(approver_uid)
|
|
|
|
# send rrp submission
|
2020-06-08 15:16:26 +00:00
|
|
|
mail_result = send_ramp_up_denied_email(
|
2020-06-05 18:08:31 +00:00
|
|
|
'askresearch@virginia.edu',
|
|
|
|
[pi_user_info.email_address],
|
|
|
|
f'{approver_info.display_name} - ({approver_info.uid})'
|
|
|
|
)
|
2020-06-08 15:16:26 +00:00
|
|
|
if mail_result:
|
|
|
|
app.logger.error(mail_result)
|
2020-06-04 17:43:10 +00:00
|
|
|
first_approval = ApprovalModel().query.filter_by(
|
|
|
|
study_id=db_approval.study_id, workflow_id=db_approval.workflow_id,
|
|
|
|
status=ApprovalStatus.APPROVED.value, version=db_approval.version).first()
|
|
|
|
if first_approval:
|
|
|
|
# Second approver denies
|
2020-06-05 18:08:31 +00:00
|
|
|
first_approver_info = ldap_service.user_info(first_approval.approver_uid)
|
|
|
|
approver_email = [first_approver_info.email_address] if first_approver_info.email_address else app.config['FALLBACK_EMAILS']
|
2020-06-04 17:43:10 +00:00
|
|
|
# send rrp denied by second approver email to first approver
|
2020-06-08 15:16:26 +00:00
|
|
|
mail_result = send_ramp_up_denied_email_to_approver(
|
2020-06-05 18:08:31 +00:00
|
|
|
'askresearch@virginia.edu',
|
|
|
|
approver_email,
|
|
|
|
f'{pi_user_info.display_name} - ({pi_user_info.uid})',
|
|
|
|
f'{approver_info.display_name} - ({approver_info.uid})'
|
|
|
|
)
|
2020-06-08 15:16:26 +00:00
|
|
|
if mail_result:
|
|
|
|
app.logger.error(mail_result)
|
2020-05-24 05:53:48 +00:00
|
|
|
# TODO: Log update action by approver_uid - maybe ?
|
|
|
|
return db_approval
|
2020-05-24 20:13:15 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def add_approval(study_id, workflow_id, approver_uid):
|
|
|
|
"""we might have multiple approvals for a workflow, so I would expect this
|
|
|
|
method to get called multiple times for the same workflow. This will
|
|
|
|
only add a new approval if no approval already exists for the approver_uid,
|
|
|
|
unless the workflow has changed, at which point, it will CANCEL any
|
|
|
|
pending approvals and create a new approval for the latest version
|
|
|
|
of the workflow."""
|
|
|
|
|
|
|
|
# Find any existing approvals for this workflow and approver.
|
|
|
|
latest_approval_request = db.session.query(ApprovalModel). \
|
|
|
|
filter(ApprovalModel.workflow_id == workflow_id). \
|
|
|
|
filter(ApprovalModel.approver_uid == approver_uid). \
|
|
|
|
order_by(desc(ApprovalModel.version)).first()
|
|
|
|
|
|
|
|
# Construct as hash of the latest files to see if things have changed since
|
|
|
|
# the last approval.
|
2020-05-29 00:03:50 +00:00
|
|
|
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
|
|
|
|
workflow_data_files = FileService.get_workflow_data_files(workflow_id)
|
|
|
|
current_data_file_ids = list(data_file.id for data_file in workflow_data_files)
|
|
|
|
|
|
|
|
if len(current_data_file_ids) == 0:
|
|
|
|
raise ApiError("invalid_workflow_approval", "You can't create an approval for a workflow that has"
|
|
|
|
"no files to approve in it.")
|
2020-05-24 20:13:15 +00:00
|
|
|
|
|
|
|
# If an existing approval request exists and no changes were made, do nothing.
|
|
|
|
# If there is an existing approval request for a previous version of the workflow
|
|
|
|
# then add a new request, and cancel any waiting/pending requests.
|
|
|
|
if latest_approval_request:
|
2020-05-29 00:03:50 +00:00
|
|
|
request_file_ids = list(file.file_data_id for file in latest_approval_request.approval_files)
|
|
|
|
current_data_file_ids.sort()
|
|
|
|
request_file_ids.sort()
|
|
|
|
if current_data_file_ids == request_file_ids:
|
2020-05-24 20:13:15 +00:00
|
|
|
return # This approval already exists.
|
|
|
|
else:
|
|
|
|
latest_approval_request.status = ApprovalStatus.CANCELED.value
|
|
|
|
db.session.add(latest_approval_request)
|
|
|
|
version = latest_approval_request.version + 1
|
|
|
|
else:
|
|
|
|
version = 1
|
|
|
|
|
|
|
|
model = ApprovalModel(study_id=study_id, workflow_id=workflow_id,
|
2020-05-31 19:35:42 +00:00
|
|
|
approver_uid=approver_uid, status=ApprovalStatus.PENDING.value,
|
2020-05-24 20:13:15 +00:00
|
|
|
message="", date_created=datetime.now(),
|
2020-05-29 00:03:50 +00:00
|
|
|
version=version)
|
|
|
|
approval_files = ApprovalService._create_approval_files(workflow_data_files, model)
|
2020-06-04 17:43:10 +00:00
|
|
|
|
|
|
|
# Check approvals count
|
|
|
|
approvals_count = ApprovalModel().query.filter_by(study_id=study_id, workflow_id=workflow_id,
|
|
|
|
version=version).count()
|
|
|
|
|
2020-05-24 20:13:15 +00:00
|
|
|
db.session.add(model)
|
|
|
|
db.session.add_all(approval_files)
|
|
|
|
db.session.commit()
|
|
|
|
|
2020-06-05 02:37:28 +00:00
|
|
|
# Send first email
|
|
|
|
if approvals_count == 0:
|
|
|
|
ldap_service = LdapService()
|
|
|
|
pi_user_info = ldap_service.user_info(model.study.primary_investigator_id)
|
|
|
|
approver_info = ldap_service.user_info(approver_uid)
|
|
|
|
# send rrp submission
|
2020-06-08 15:16:26 +00:00
|
|
|
mail_result = send_ramp_up_submission_email(
|
2020-06-05 02:37:28 +00:00
|
|
|
'askresearch@virginia.edu',
|
|
|
|
[pi_user_info.email_address],
|
|
|
|
f'{approver_info.display_name} - ({approver_info.uid})'
|
|
|
|
)
|
2020-06-08 15:16:26 +00:00
|
|
|
if mail_result:
|
|
|
|
app.logger.error(mail_result)
|
2020-06-05 02:37:28 +00:00
|
|
|
# send rrp approval request for first approver
|
|
|
|
# enhance the second part in case it bombs
|
|
|
|
approver_email = [approver_info.email_address] if approver_info.email_address else app.config['FALLBACK_EMAILS']
|
2020-06-08 15:16:26 +00:00
|
|
|
mail_result = send_ramp_up_approval_request_first_review_email(
|
2020-06-05 02:37:28 +00:00
|
|
|
'askresearch@virginia.edu',
|
|
|
|
approver_email,
|
|
|
|
f'{pi_user_info.display_name} - ({pi_user_info.uid})'
|
|
|
|
)
|
2020-06-08 15:16:26 +00:00
|
|
|
if mail_result:
|
|
|
|
app.logger.error(mail_result)
|
2020-06-05 02:37:28 +00:00
|
|
|
|
2020-05-24 20:13:15 +00:00
|
|
|
@staticmethod
|
2020-05-29 00:03:50 +00:00
|
|
|
def _create_approval_files(workflow_data_files, approval):
|
2020-05-24 20:13:15 +00:00
|
|
|
"""Currently based exclusively on the status of files associated with a workflow."""
|
|
|
|
file_approval_models = []
|
2020-05-29 00:03:50 +00:00
|
|
|
for file_data in workflow_data_files:
|
|
|
|
file_approval_models.append(ApprovalFile(file_data_id=file_data.id,
|
|
|
|
approval=approval))
|
2020-05-24 20:13:15 +00:00
|
|
|
return file_approval_models
|
|
|
|
|