Merge branch 'rrt/testing' into rrt/staging

This commit is contained in:
Aaron Louie 2020-06-07 19:57:25 -04:00
commit ebb37b4555
40 changed files with 667 additions and 232 deletions

View File

@ -39,6 +39,7 @@ ldap3 = "*"
gunicorn = "*"
werkzeug = "*"
sentry-sdk = {extras = ["flask"],version = "==0.14.4"}
flask-mail = "*"
[requires]
python_version = "3.7"

95
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "54d9d51360f54762138a3acc7696badd1d711e7b1dde9e2d82aa706e40c17102"
"sha256": "6c89585086260ebcb41918b8ef3b1d9e189e1b492208d3ff000a138bc2f2fcee"
},
"pipfile-spec": 6,
"requires": {
@ -104,10 +104,10 @@
},
"celery": {
"hashes": [
"sha256:5147662e23dc6bc39c17a2cbc9a148debe08ecfb128b0eded14a0d9c81fc5742",
"sha256:df2937b7536a2a9b18024776a3a46fd281721813636c03a5177fa02fe66078f6"
"sha256:9ae2e73b93cc7d6b48b56aaf49a68c91752d0ffd7dfdcc47f842ca79a6f13eae",
"sha256:c2037b6a8463da43b19969a0fc13f9023ceca6352b4dd51be01c66fbbb13647e"
],
"version": "==4.4.3"
"version": "==4.4.4"
},
"certifi": {
"hashes": [
@ -276,6 +276,13 @@
"index": "pypi",
"version": "==3.0.8"
},
"flask-mail": {
"hashes": [
"sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"
],
"index": "pypi",
"version": "==0.9.1"
},
"flask-marshmallow": {
"hashes": [
"sha256:6e6aec171b8e092e0eafaf035ff5b8637bf3a58ab46f568c4c1bab02f2a3c196",
@ -344,11 +351,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
"sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
"version": "==1.6.1"
},
"inflection": {
"hashes": [
@ -387,10 +394,10 @@
},
"kombu": {
"hashes": [
"sha256:ab0afaa5388dd2979cbc439d3623b86a4f7a58d41f621096bef7767c37bc2505",
"sha256:aece08f48706743aaa1b9d607fee300559481eafcc5ee56451aa0ef867a3be07"
"sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a",
"sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3"
],
"version": "==4.6.9"
"version": "==4.6.10"
},
"ldap3": {
"hashes": [
@ -479,11 +486,11 @@
},
"marshmallow": {
"hashes": [
"sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab",
"sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7"
"sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5",
"sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f"
],
"index": "pypi",
"version": "==3.6.0"
"version": "==3.6.1"
},
"marshmallow-enum": {
"hashes": [
@ -503,29 +510,29 @@
},
"numpy": {
"hashes": [
"sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d",
"sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897",
"sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88",
"sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6",
"sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7",
"sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26",
"sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a",
"sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d",
"sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961",
"sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5",
"sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2",
"sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032",
"sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba",
"sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085",
"sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509",
"sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170",
"sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae",
"sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d",
"sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c",
"sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720",
"sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec"
"sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233",
"sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b",
"sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7",
"sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f",
"sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5",
"sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb",
"sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583",
"sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1",
"sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a",
"sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271",
"sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824",
"sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3",
"sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc",
"sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161",
"sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f",
"sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f",
"sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf",
"sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b",
"sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0",
"sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675",
"sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"
],
"version": "==1.18.4"
"version": "==1.18.5"
},
"openapi-spec-validator": {
"hashes": [
@ -917,11 +924,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
"sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
"version": "==1.6.1"
},
"more-itertools": {
"hashes": [
@ -968,11 +975,11 @@
},
"pytest": {
"hashes": [
"sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3",
"sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"
"sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1",
"sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"
],
"index": "pypi",
"version": "==5.4.2"
"version": "==5.4.3"
},
"six": {
"hashes": [
@ -983,10 +990,10 @@
},
"wcwidth": {
"hashes": [
"sha256:3de2e41158cb650b91f9654cbf9a3e053cee0719c9df4ddc11e4b568669e9829",
"sha256:b651b6b081476420e4e9ae61239ac4c1b49d0c5ace42b2e81dc2ff49ed50c566"
"sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6",
"sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"
],
"version": "==0.2.2"
"version": "==0.2.3"
},
"zipp": {
"hashes": [

View File

@ -42,6 +42,13 @@ PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL +
PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i")
LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No trailing slash or http://
LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=3))
LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=1))
# Email configuration
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')
MAIL_PORT = environ.get('MAIL_PORT', default=2525)
MAIL_USE_SSL = environ.get('MAIL_USE_SSL', default=False)
MAIL_USE_TLS = environ.get('MAIL_USE_TLS', default=True)
MAIL_USERNAME = environ.get('MAIL_USERNAME', default='5f012d0108d374')
MAIL_PASSWORD = environ.get('MAIL_PASSWORD', default='08442c04e98d50')

View File

@ -3,8 +3,10 @@ import os
import sentry_sdk
import connexion
from jinja2 import Environment, FileSystemLoader
from flask_cors import CORS
from flask_marshmallow import Marshmallow
from flask_mail import Mail
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sentry_sdk.integrations.flask import FlaskIntegration
@ -48,6 +50,12 @@ if app.config['ENABLE_SENTRY']:
integrations=[FlaskIntegration()]
)
# Jinja environment definition, used to render mail templates
template_dir = os.getcwd() + '/crc/static/templates/mails'
env = Environment(loader=FileSystemLoader(template_dir))
# Mail settings
mail = Mail(app)
print('=== USING THESE CONFIG SETTINGS: ===')
print('DB_HOST = ', )
print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS'])
@ -73,3 +81,9 @@ def load_example_rrt_data():
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
ExampleDataLoader().load_rrt()
@app.cli.command()
def clear_db():
"""Load example data into the database."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()

View File

@ -806,14 +806,42 @@ paths:
type: array
items:
$ref: "#/components/schemas/Script"
/approval:
/approval-counts:
parameters:
- name: everything
- name: as_user
in: query
required: false
description: If set to true, returns all the approvals known to the system.
description: If provided, returns the approval counts for that user.
schema:
type: boolean
type: string
get:
operationId: crc.api.approval.get_approval_counts
summary: Provides counts for approvals by status for the given user, or all users if no user is provided
tags:
- Approvals
responses:
'200':
description: An dictionary of Approval Statuses and the counts for each
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ApprovalCounts"
/approval:
parameters:
- name: status
in: query
required: false
description: If provided, returns just approvals for the given status.
schema:
type: string
- name: as_user
in: query
required: false
description: If provided, returns the approval results as they would appear for that user.
schema:
type: string
get:
operationId: crc.api.approval.get_approvals
summary: Provides a list of workflows approvals
@ -1280,4 +1308,26 @@ components:
type: number
format: integer
example: 5
ApprovalCounts:
properties:
PENDING:
type: number
format: integer
example: 5
APPROVED:
type: number
format: integer
example: 5
DECLINED:
type: number
format: integer
example: 5
CANCELED:
type: number
format: integer
example: 5
AWAITING:
type: number
format: integer
example: 5

View File

@ -13,11 +13,63 @@ from crc.services.approval_service import ApprovalService
from crc.services.ldap_service import LdapService
def get_approvals(everything=False):
if everything:
approvals = ApprovalService.get_all_approvals(include_cancelled=True)
else:
approvals = ApprovalService.get_approvals_per_user(g.user.uid, include_cancelled=False)
# Returns counts of approvals in each status group assigned to the given user.
# The goal is to return results as quickly as possible.
def get_approval_counts(as_user=None):
uid = as_user or g.user.uid
db_user_approvals = db.session.query(ApprovalModel)\
.filter_by(approver_uid=uid)\
.filter(ApprovalModel.status != ApprovalStatus.CANCELED.name)\
.all()
study_ids = [a.study_id for a in db_user_approvals]
print('study_ids', study_ids)
db_other_approvals = db.session.query(ApprovalModel)\
.filter(ApprovalModel.study_id.in_(study_ids))\
.filter(ApprovalModel.approver_uid != uid)\
.filter(ApprovalModel.status != ApprovalStatus.CANCELED.name)\
.all()
# Make a dict of the other approvals where the key is the study id and the value is the approval
# TODO: This won't work if there are more than 2 approvals with the same study_id
other_approvals = {}
for approval in db_other_approvals:
other_approvals[approval.study_id] = approval
counts = {}
for status in ApprovalStatus:
counts[status.name] = 0
for approval in db_user_approvals:
# Check if another approval has the same study id
if approval.study_id in other_approvals:
other_approval = other_approvals[approval.study_id]
# Other approval takes precedence over this one
if other_approval.id < approval.id:
if other_approval.status == ApprovalStatus.PENDING.name:
counts[ApprovalStatus.AWAITING.name] += 1
elif other_approval.status == ApprovalStatus.DECLINED.name:
counts[ApprovalStatus.DECLINED.name] += 1
elif other_approval.status == ApprovalStatus.CANCELED.name:
counts[ApprovalStatus.CANCELED.name] += 1
elif other_approval.status == ApprovalStatus.APPROVED.name:
counts[approval.status] += 1
else:
counts[approval.status] += 1
return counts
def get_approvals(status=None, as_user=None):
#status = ApprovalStatus.PENDING.value
user = g.user.uid
if as_user:
user = as_user
approvals = ApprovalService.get_approvals_per_user(user, status,
include_cancelled=False)
results = ApprovalSchema(many=True).dump(approvals)
return results
@ -29,7 +81,7 @@ def get_approvals_for_study(study_id=None):
return results
# ----- Being decent into madness ---- #
# ----- Begin descent into madness ---- #
def get_csv():
"""A damn lie, it's a json file. A huge bit of a one-off for RRT, but 3 weeks of midnight work can convince a
man to do just about anything"""
@ -79,12 +131,14 @@ def get_csv():
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
return {"results": output, "errors": errors }
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
@ -94,6 +148,7 @@ def find_task(uuid, task):
return task
# ----- come back to the world of the living ---- #
def update_approval(approval_id, body):
if approval_id is None:
raise ApiError('unknown_approval', 'Please provide a valid Approval ID.')
@ -111,5 +166,9 @@ def update_approval(approval_id, body):
session.add(approval_model)
session.commit()
# Called only to send emails
approver = body['approver']['uid']
ApprovalService.update_approval(approval_id, approver)
result = ApprovalSchema().dump(approval_model)
return result

View File

@ -20,6 +20,9 @@ class ApprovalStatus(enum.Enum):
DECLINED = "DECLINED" # rejected by the reviewer
CANCELED = "CANCELED" # The document was replaced with a new version and this review is no longer needed.
# Used for overall status only, never set on a task.
AWAITING = "AWAITING" # awaiting another approval
class ApprovalFile(db.Model):
file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True)
@ -81,13 +84,13 @@ class Approval(object):
instance.associated_files = []
for approval_file in model.approval_files:
try:
# fixme: This is slow because we are doing a ton of queries to find the irb code.
extra_info = doc_dictionary[approval_file.file_data.file_model.irb_doc_code]
except:
extra_info = None
associated_file = {}
associated_file['id'] = approval_file.file_data.file_model.id
if extra_info:
irb_doc_code = approval_file.file_data.file_model.irb_doc_code
associated_file['name'] = '_'.join((extra_info['category1'],
approval_file.file_data.file_model.name))
associated_file['description'] = extra_info['description']

View File

@ -11,7 +11,8 @@ class RequestApproval(Script):
return """
Creates an approval request on this workflow, by the given approver_uid(s),"
Takes multiple arguments, which should point to data located in current task
or be quoted strings.
or be quoted strings. The order is important. Approvals will be processed
in this order.
Example:
RequestApproval approver1 "dhf8r"

View File

@ -2,7 +2,7 @@ from datetime import datetime
from sqlalchemy import desc
from crc import db, session
from crc import app, db, session
from crc.api.common import ApiError
from crc.models.approval import ApprovalModel, ApprovalStatus, ApprovalFile, Approval
@ -10,45 +10,79 @@ from crc.models.study import StudyModel
from crc.models.workflow import WorkflowModel
from crc.services.file_service import FileService
from crc.services.ldap_service import LdapService
from crc.services.mails import (
send_ramp_up_submission_email,
send_ramp_up_approval_request_email,
send_ramp_up_approval_request_first_review_email,
send_ramp_up_approved_email,
send_ramp_up_denied_email,
send_ramp_up_denied_email_to_approver
)
class ApprovalService(object):
"""Provides common tools for working with an Approval"""
@staticmethod
def __one_approval_from_study(study, approver_uid = None, include_cancelled=True):
def __one_approval_from_study(study, approver_uid = None, status=None,
include_cancelled=True):
"""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 = []
query = db.session.query(ApprovalModel).\
filter(ApprovalModel.study_id == study.id)
query = db.session.query(ApprovalModel).filter(ApprovalModel.study_id == study.id)
if not include_cancelled:
query=query.filter(ApprovalModel.status != ApprovalStatus.CANCELED.value)
approvals = query.all() # All non-cancelled approvals.
approvals = query.all()
for approval_model in approvals:
if approval_model.approver_uid == approver_uid:
main_approval = Approval.from_model(approval_model)
main_approval = approval_model
else:
related_approvals.append(Approval.from_model(approval_model))
related_approvals.append(approval_model)
# IF WE ARE JUST RETURNING ALL OF THE APPROVALS PER STUDY
if not main_approval and len(related_approvals) > 0:
main_approval = related_approvals[0]
related_approvals = related_approvals[1:]
if len(related_approvals) > 0:
main_approval.related_approvals = related_approvals
if main_approval is not None: # May be null if the study has no approvals.
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))
return main_approval
@staticmethod
def get_approvals_per_user(approver_uid, include_cancelled=False):
def __calculate_overall_approval_status(approval, related):
# 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
# the state of the approval to be Declined, or Waiting respectively.
if approval.status == ApprovalStatus.PENDING.value:
for ra in related:
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.
return approval.status
else:
return approval.status
@staticmethod
def get_approvals_per_user(approver_uid, status=None, include_cancelled=False):
"""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:
approval = ApprovalService.__one_approval_from_study(study, approver_uid, include_cancelled)
approval = ApprovalService.__one_approval_from_study(study, approver_uid,
status, include_cancelled)
if approval:
approvals.append(approval)
return approvals
@ -77,13 +111,53 @@ class ApprovalService(object):
@staticmethod
def update_approval(approval_id, approver_uid, status):
def update_approval(approval_id, approver_uid):
"""Update a specific approval"""
db_approval = session.query(ApprovalModel).get(approval_id)
status = db_approval.status
if db_approval:
db_approval.status = status
session.add(db_approval)
session.commit()
# db_approval.status = status
# session.add(db_approval)
# session.commit()
if status == ApprovalStatus.APPROVED.value:
# 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:
# send rrp approval request for second approver
ldap_service = LdapService()
pi_user_info = ldap_service.user_info(db_approval.study.primary_investigator_id)
approver_info = ldap_service.user_info(approver_uid)
# send rrp submission
send_ramp_up_approved_email(
'askresearch@virginia.edu',
[pi_user_info.email_address],
f'{approver_info.display_name} - ({approver_info.uid})'
)
elif status == ApprovalStatus.DECLINED.value:
ldap_service = LdapService()
pi_user_info = ldap_service.user_info(db_approval.study.primary_investigator_id)
approver_info = ldap_service.user_info(approver_uid)
# send rrp submission
send_ramp_up_denied_email(
'askresearch@virginia.edu',
[pi_user_info.email_address],
f'{approver_info.display_name} - ({approver_info.uid})'
)
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
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']
# send rrp denied by second approver email to first approver
send_ramp_up_denied_email_to_approver(
'askresearch@virginia.edu',
approver_email,
f'{pi_user_info.display_name} - ({pi_user_info.uid})',
f'{approver_info.display_name} - ({approver_info.uid})'
)
# TODO: Log update action by approver_uid - maybe ?
return db_approval
@ -133,10 +207,35 @@ class ApprovalService(object):
message="", date_created=datetime.now(),
version=version)
approval_files = ApprovalService._create_approval_files(workflow_data_files, model)
# Check approvals count
approvals_count = ApprovalModel().query.filter_by(study_id=study_id, workflow_id=workflow_id,
version=version).count()
db.session.add(model)
db.session.add_all(approval_files)
db.session.commit()
# 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
send_ramp_up_submission_email(
'askresearch@virginia.edu',
[pi_user_info.email_address],
f'{approver_info.display_name} - ({approver_info.uid})'
)
# 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']
send_ramp_up_approval_request_first_review_email(
'askresearch@virginia.edu',
approver_email,
f'{pi_user_info.display_name} - ({pi_user_info.uid})'
)
@staticmethod
def _create_approval_files(workflow_data_files, approval):
"""Currently based exclusively on the status of files associated with a workflow."""

View File

@ -18,7 +18,7 @@ class LdapService(object):
user_or_last_name_search = "(&(objectclass=person)(|(uid=%s*)(sn=%s*)))"
cn_single_search = '(&(objectclass=person)(cn=%s*))'
cn_double_search = '(&(objectclass=person)(&(cn=%s*)(cn=*%s*)))'
temp_cache = {}
conn = None
@staticmethod
@ -43,6 +43,7 @@ class LdapService(object):
def user_info(uva_uid):
user_info = db.session.query(LdapModel).filter(LdapModel.uid == uva_uid).first()
if not user_info:
app.logger.info("No cache for " + uva_uid)
search_string = LdapService.uid_search_string % uva_uid
conn = LdapService.__get_conn()
conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes)
@ -51,6 +52,7 @@ class LdapService(object):
entry = conn.entries[0]
user_info = LdapModel.from_entry(entry)
db.session.add(user_info)
db.session.commit()
return user_info
@staticmethod
@ -69,7 +71,7 @@ class LdapService(object):
# Search by user_id or last name
search_string = LdapService.user_or_last_name_search % (query, query)
results = []
print(search_string)
app.logger.info(search_string)
try:
conn = LdapService.__get_conn()
conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes)

106
crc/services/mails.py Normal file
View File

@ -0,0 +1,106 @@
import os
from flask import render_template, render_template_string
from flask_mail import Message
# TODO: Extract common mailing code into its own function
def send_ramp_up_submission_email(sender, recipients, approver_1, approver_2=None):
try:
msg = Message('Research Ramp-up Plan Submitted',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_submission.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_submission.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approval_request_email(sender, recipients, primary_investigator):
try:
msg = Message('Research Ramp-up Plan Approval Request',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_approval_request.txt')
template_vars = {'primary_investigator': primary_investigator}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approval_request.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approval_request_first_review_email(sender, recipients, primary_investigator):
try:
msg = Message('Research Ramp-up Plan Approval Request',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_approval_request_first_review.txt')
template_vars = {'primary_investigator': primary_investigator}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approval_request_first_review.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approved_email(sender, recipients, approver_1, approver_2=None):
try:
msg = Message('Research Ramp-up Plan Approved',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_approved.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approved.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_denied_email(sender, recipients, approver):
try:
msg = Message('Research Ramp-up Plan Denied',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_denied.txt')
template_vars = {'approver': approver}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_denied.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_denied_email_to_approver(sender, recipients, primary_investigator, approver_2):
try:
msg = Message('Research Ramp-up Plan Denied',
sender=sender,
recipients=recipients)
from crc import env, mail
template = env.get_template('ramp_up_denied_first_approver.txt')
template_vars = {'primary_investigator': primary_investigator, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_denied_first_approver.html')
msg.html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)

View File

@ -0,0 +1 @@
<p>A Research Ramp-up approval request from {{ primary_investigator }} is now available for your review in your [Research Ramp-up Toolkit](https://rrt.uvadcos.io/app/approvals).</p>

View File

@ -0,0 +1 @@
A Research Ramp-up approval request from {{ primary_investigator }} is now available for your review in your [Research Ramp-up Toolkit](https://rrt.uvadcos.io/app/approvals).

View File

@ -0,0 +1 @@
<p>A Research Ramp-up approval request from {{ primary_investigator }} and is now available for your review in your [Research Ramp-up Toolkit](https://rrt.uvadcos.io/app/approvals).</p>

View File

@ -0,0 +1 @@
A Research Ramp-up approval request from {{ primary_investigator }} is now available for your review in your [Research Ramp-up Toolkit](https://rrt.uvadcos.io/app/approvals).

View File

@ -0,0 +1 @@
<p>Your Research Ramp-up Plan has been approved by {{ approver_1 }} {% if approver_2 %}and {{ approver_2 }} {% endif %}</p>

View File

@ -0,0 +1 @@
Your Research Ramp-up Plan has been approved by {{ approver_1 }} {% if approver_2 %}and {{ approver_2 }} {% endif %}

View File

@ -0,0 +1 @@
<p>Your Research Ramp-up Plan has been denied by {{ approver }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.</p>

View File

@ -0,0 +1 @@
Your Research Ramp-up Plan has been denied by {{ approver_1 }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver_1 }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.

View File

@ -0,0 +1 @@
<p>The Research Ramp-up Plan submitted by {{ primary_investigator }} was denied by {{ approver_2 }} and returned for requested updates. You may see comments related to this denial in on your Research Ramp-up Toolkit Approval dashboard.</p>

View File

@ -0,0 +1 @@
The Research Ramp-up Plan submitted by {{ primary_investigator }} was denied by {{ approver_2 }} and returned for requested updates. You may see comments related to this denial in on your Research Ramp-up Toolkit Approval dashboard.

View File

@ -0,0 +1,5 @@
<p>Your Research Ramp-up Plan (RRP) has been submitted for review by {{ approver_1 }} {% if approver_2 %}and {{ approver_2 }} {% endif %}. After completion of the review step you will receive email notification of its approval or if additional information and/or modifications are required, along with instructions on how to proceed. Return to the Research Ramp-up Plan application to proceed as instructed.</p>
<p>In the meantime, please make sure all required training has been completed and needed supplies secured. You will be asked to confirm that both of these requirements have been met before reopening the research space approved in your RRP.</p>
<p>Additionally, if there are any unknown Area Monitors for the spaces listed in your RRP, please contact your approvers to determine either who they are or how you can find out. Missing Area Monitors will need to be entered before proceeding as well.</p>

View File

@ -0,0 +1,5 @@
Your Research Ramp-up Plan (RRP) has been submitted for review by {{ approver_1 }} {% if approver_2 %}and {{ approver_2 }} {% endif %}. After completion of the review step you will receive email notification of its approval or if additional information and/or modifications are required, along with instructions on how to proceed. Return to the Research Ramp-up Plan application to proceed as instructed.
In the meantime, please make sure all required training has been completed and needed supplies secured. You will be asked to confirm that both of these requirements have been met before reopening the research space approved in your RRP.
Additionally, if there are any unknown Area Monitors for the spaces listed in your RRP, please contact your approvers to determine either who they are or how you can find out. Missing Area Monitors will need to be entered before proceeding as well.

View File

@ -119,7 +119,6 @@ class BaseTest(unittest.TestCase):
"""use_crc_data will cause this to load the mammoth collection of documents
we built up developing crc, use_rrt_data will do the same for hte rrt project,
otherwise it depends on a small setup for running tests."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
if use_crc_data:
@ -228,7 +227,7 @@ class BaseTest(unittest.TestCase):
if study is None:
user = self.create_user(uid=uid)
study = StudyModel(title=title, protocol_builder_status=ProtocolBuilderStatus.ACTIVE,
user_uid=user.uid)
user_uid=user.uid, primary_investigator_id='lb3dp')
db.session.add(study)
db.session.commit()
return study

View File

@ -68,16 +68,15 @@ class TestApprovals(BaseTest):
approval = response[0]
self.assertEqual(approval['approver']['uid'], approver_uid)
def test_list_approvals_per_admin(self):
"""All approvals will be returned"""
rv = self.app.get('/v1.0/approval?everything=true', headers=self.logged_in_headers())
def test_list_approvals_as_user(self):
"""All approvals as different user"""
rv = self.app.get('/v1.0/approval?as_user=lb3dp', headers=self.logged_in_headers())
self.assert_success(rv)
response = json.loads(rv.get_data(as_text=True))
# Returned approvals should match what's in the db, we should get one approval back
# per study (2 studies), and that approval should have one related approval.
approvals_count = ApprovalModel.query.count()
# Returned approvals should match what's in the db for user ld3dp, we should get one
# approval back per study (2 studies), and that approval should have one related approval.
response_count = len(response)
self.assertEqual(2, response_count)
@ -106,7 +105,7 @@ class TestApprovals(BaseTest):
def test_accept_approval(self):
approval = session.query(ApprovalModel).filter_by(approver_uid='dhf8r').first()
data = {'id': approval.id,
"approver_uid": "dhf8r",
"approver": {"uid": "dhf8r"},
'message': "Approved. I like the cut of your jib.",
'status': ApprovalStatus.APPROVED.value}
@ -127,7 +126,7 @@ class TestApprovals(BaseTest):
def test_decline_approval(self):
approval = session.query(ApprovalModel).filter_by(approver_uid='dhf8r').first()
data = {'id': approval.id,
"approver_uid": "dhf8r",
"approver": {"uid": "dhf8r"},
'message': "Approved. I find the cut of your jib lacking.",
'status': ApprovalStatus.DECLINED.value}

View File

@ -15,13 +15,14 @@ class TestApprovalsService(BaseTest):
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
self.assertEquals(1, db.session.query(ApprovalModel).count())
self.assertEqual(1, db.session.query(ApprovalModel).count())
model = db.session.query(ApprovalModel).first()
self.assertEquals(workflow.study_id, model.study_id)
self.assertEquals(workflow.id, model.workflow_id)
self.assertEquals("dhf8r", model.approver_uid)
self.assertEquals(1, model.version)
self.assertEqual(workflow.study_id, model.study_id)
self.assertEqual(workflow.id, model.workflow_id)
self.assertEqual("dhf8r", model.approver_uid)
self.assertEqual(1, model.version)
def test_new_requests_dont_add_if_approval_exists_for_current_workflow(self):
self.create_reference_document()
@ -32,9 +33,9 @@ class TestApprovalsService(BaseTest):
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
self.assertEquals(1, db.session.query(ApprovalModel).count())
self.assertEqual(1, db.session.query(ApprovalModel).count())
model = db.session.query(ApprovalModel).first()
self.assertEquals(1, model.version)
self.assertEqual(1, model.version)
def test_new_approval_requests_after_file_modification_create_new_requests(self):
self.load_example_data()
@ -51,9 +52,20 @@ class TestApprovalsService(BaseTest):
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr")
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
self.assertEquals(2, db.session.query(ApprovalModel).count())
self.assertEqual(2, db.session.query(ApprovalModel).count())
models = db.session.query(ApprovalModel).order_by(ApprovalModel.version).all()
self.assertEquals(1, models[0].version)
self.assertEquals(2, models[1].version)
self.assertEqual(1, models[0].version)
self.assertEqual(2, models[1].version)
def test_new_approval_sends_proper_emails(self):
self.assertEqual(1, 1)
def test_new_approval_failed_ldap_lookup(self):
# failed lookup should send email to sartographysupport@googlegroups.com + Cheryl
self.assertEqual(1, 1)
def test_approve_approval_sends_proper_emails(self):
self.assertEqual(1, 1)
def test_deny_approval_sends_proper_emails(self):
self.assertEqual(1, 1)

View File

@ -49,10 +49,10 @@ class TestAuthentication(BaseTest):
self.assert_success(rv)
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
self.assertIsNotNone(user)
self.assertEquals(new_uid, user.uid)
self.assertEquals("Laura Barnes", user.display_name)
self.assertEquals("lb3dp@virginia.edu", user.email_address)
self.assertEquals("E0:Associate Professor of Systems and Information Engineering", user.title)
self.assertEqual(new_uid, user.uid)
self.assertEqual("Laura Barnes", user.display_name)
self.assertEqual("lb3dp@virginia.edu", user.email_address)
self.assertEqual("E0:Associate Professor of Systems and Information Engineering", user.title)
def test_current_user_status(self):

View File

@ -17,7 +17,7 @@ class TestCompleteTemplate(unittest.TestCase):
data = {"name": "Dan"}
data_copy = copy.deepcopy(data)
script.rich_text_update(data_copy)
self.assertEquals(data, data_copy)
self.assertEqual(data, data_copy)
def test_rich_text_update_new_line(self):
script = CompleteTemplate()

View File

@ -23,11 +23,11 @@ class TestFileService(BaseTest):
binary_data=b'5678', irb_doc_code=irb_code)
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
self.assertEquals(1, len(file_models))
self.assertEqual(1, len(file_models))
file_data = FileService.get_workflow_data_files(workflow_id=workflow.id)
self.assertEquals(1, len(file_data))
self.assertEquals(2, file_data[0].version)
self.assertEqual(1, len(file_data))
self.assertEqual(2, file_data[0].version)
def test_add_file_from_form_increments_version_and_replaces_on_subsequent_add_with_same_name(self):
@ -77,12 +77,13 @@ class TestFileService(BaseTest):
binary_data=b'5678')
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
self.assertEquals(1, len(file_models))
self.assertEqual(1, len(file_models))
file_data = FileService.get_workflow_data_files(workflow_id=workflow.id)
self.assertEquals(1, len(file_data))
self.assertEquals(2, file_data[0].version)
self.assertEquals(b'5678', file_data[0].data)
self.assertEqual(1, len(file_data))
self.assertEqual(2, file_data[0].version)
self.assertEqual(b'5678', file_data[0].data)
def test_add_file_from_form_allows_multiple_files_with_different_names(self):
self.load_example_data()
@ -101,4 +102,4 @@ class TestFileService(BaseTest):
name="a_different_thing.png", content_type="text",
binary_data=b'5678')
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
self.assertEquals(2, len(file_models))
self.assertEqual(2, len(file_models))

View File

@ -30,4 +30,4 @@ class TestLdapService(BaseTest):
user_info = LdapService.user_info("nosuch")
self.assertFalse(True, "An API error should be raised.")
except ApiError as ae:
self.assertEquals("missing_ldap_record", ae.code)
self.assertEqual("missing_ldap_record", ae.code)

View File

@ -31,7 +31,7 @@ class TestLookupService(BaseTest):
self.assertEqual(1, len(lookup_records))
lookup_record = lookup_records[0]
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEquals(28, len(lookup_data))
self.assertEqual(28, len(lookup_data))
def test_updates_to_file_cause_lookup_rebuild(self):
spec = BaseTest.load_test_spec('enum_options_with_search')
@ -43,7 +43,7 @@ class TestLookupService(BaseTest):
self.assertEqual(1, len(lookup_records))
lookup_record = lookup_records[0]
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEquals(28, len(lookup_data))
self.assertEqual(28, len(lookup_data))
# Update the workflow specification file.
file_path = os.path.join(app.root_path, '..', 'tests', 'data',
@ -59,7 +59,7 @@ class TestLookupService(BaseTest):
lookup_records = session.query(LookupFileModel).all()
lookup_record = lookup_records[0]
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEquals(4, len(lookup_data))
self.assertEqual(4, len(lookup_data))
@ -70,49 +70,49 @@ class TestLookupService(BaseTest):
processor.do_engine_steps()
results = LookupService.lookup(workflow, "AllTheNames", "", limit=10)
self.assertEquals(10, len(results), "Blank queries return everything, to the limit")
self.assertEqual(10, len(results), "Blank queries return everything, to the limit")
results = LookupService.lookup(workflow, "AllTheNames", "medicines", limit=10)
self.assertEquals(1, len(results), "words in the middle of label are detected.")
self.assertEquals("The Medicines Company", results[0].label)
self.assertEqual(1, len(results), "words in the middle of label are detected.")
self.assertEqual("The Medicines Company", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "UVA", limit=10)
self.assertEquals(1, len(results), "Beginning of label is found.")
self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label)
self.assertEqual(1, len(results), "Beginning of label is found.")
self.assertEqual("UVA - INTERNAL - GM USE ONLY", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "uva", limit=10)
self.assertEquals(1, len(results), "case does not matter.")
self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label)
self.assertEqual(1, len(results), "case does not matter.")
self.assertEqual("UVA - INTERNAL - GM USE ONLY", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "medici", limit=10)
self.assertEquals(1, len(results), "partial words are picked up.")
self.assertEquals("The Medicines Company", results[0].label)
self.assertEqual(1, len(results), "partial words are picked up.")
self.assertEqual("The Medicines Company", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "Genetics Savings", limit=10)
self.assertEquals(1, len(results), "multiple terms are picked up..")
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
self.assertEqual(1, len(results), "multiple terms are picked up..")
self.assertEqual("Genetics Savings & Clone, Inc.", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "Genetics Sav", limit=10)
self.assertEquals(1, len(results), "prefix queries still work with partial terms")
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
self.assertEqual(1, len(results), "prefix queries still work with partial terms")
self.assertEqual("Genetics Savings & Clone, Inc.", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "Gen Sav", limit=10)
self.assertEquals(1, len(results), "prefix queries still work with ALL the partial terms")
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
self.assertEqual(1, len(results), "prefix queries still work with ALL the partial terms")
self.assertEqual("Genetics Savings & Clone, Inc.", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "Inc", limit=10)
self.assertEquals(7, len(results), "short terms get multiple correct results.")
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
self.assertEqual(7, len(results), "short terms get multiple correct results.")
self.assertEqual("Genetics Savings & Clone, Inc.", results[0].label)
results = LookupService.lookup(workflow, "AllTheNames", "reaction design", limit=10)
self.assertEquals(5, len(results), "all results come back for two terms.")
self.assertEquals("Reaction Design", results[0].label, "Exact matches come first.")
self.assertEqual(5, len(results), "all results come back for two terms.")
self.assertEqual("Reaction Design", results[0].label, "Exact matches come first.")
results = LookupService.lookup(workflow, "AllTheNames", "1 Something", limit=10)
self.assertEquals("1 Something", results[0].label, "Exact matches are prefered")
self.assertEqual("1 Something", results[0].label, "Exact matches are prefered")
results = LookupService.lookup(workflow, "AllTheNames", "1 (!-Something", limit=10)
self.assertEquals("1 Something", results[0].label, "special characters don't flake out")
self.assertEqual("1 Something", results[0].label, "special characters don't flake out")
@ -124,6 +124,6 @@ class TestLookupService(BaseTest):
# Fixme: Stop words are taken into account on the query side, and haven't found a fix yet.
#results = WorkflowService.run_lookup_query(lookup_table.id, "in", limit=10)
#self.assertEquals(7, len(results), "stop words are not removed.")
#self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
#self.assertEqual(7, len(results), "stop words are not removed.")
#self.assertEqual("Genetics Savings & Clone, Inc.", results[0].label)

55
tests/test_mails.py Normal file
View File

@ -0,0 +1,55 @@
from tests.base_test import BaseTest
from crc.services.mails import (
send_ramp_up_submission_email,
send_ramp_up_approval_request_email,
send_ramp_up_approval_request_first_review_email,
send_ramp_up_approved_email,
send_ramp_up_denied_email,
send_ramp_up_denied_email_to_approver
)
class TestMails(BaseTest):
def setUp(self):
self.sender = 'sender@sartography.com'
self.recipients = ['recipient@sartography.com']
self.primary_investigator = 'Dr. Bartlett'
self.approver_1 = 'Max Approver'
self.approver_2 = 'Close Reviewer'
def test_send_ramp_up_submission_email(self):
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertTrue(True)
def test_send_ramp_up_approval_request_email(self):
send_ramp_up_approval_request_email(self.sender, self.recipients, self.primary_investigator)
self.assertTrue(True)
def test_send_ramp_up_approval_request_first_review_email(self):
send_ramp_up_approval_request_first_review_email(
self.sender, self.recipients, self.primary_investigator
)
self.assertTrue(True)
def test_send_ramp_up_approved_email(self):
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertTrue(True)
def test_send_ramp_up_denied_email(self):
send_ramp_up_denied_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
def test_send_send_ramp_up_denied_email_to_approver(self):
send_ramp_up_denied_email_to_approver(
self.sender, self.recipients, self.primary_investigator, self.approver_2
)
self.assertTrue(True)

View File

@ -24,7 +24,7 @@ class TestRequestApprovalScript(BaseTest):
binary_data=b'1234')
script = RequestApproval()
script.do_task(task, workflow.study_id, workflow.id, "study.approval1", "study.approval2")
self.assertEquals(2, db.session.query(ApprovalModel).count())
self.assertEqual(2, db.session.query(ApprovalModel).count())
def test_do_task_with_blank_second_approver(self):
self.load_example_data()
@ -39,7 +39,7 @@ class TestRequestApprovalScript(BaseTest):
binary_data=b'1234')
script = RequestApproval()
script.do_task(task, workflow.study_id, workflow.id, "study.approval1", "study.approval2")
self.assertEquals(1, db.session.query(ApprovalModel).count())
self.assertEqual(1, db.session.query(ApprovalModel).count())
def test_do_task_with_incorrect_argument(self):
@ -64,5 +64,5 @@ class TestRequestApprovalScript(BaseTest):
script = RequestApproval()
script.do_task_validate_only(task, workflow.study_id, workflow.id, "study.approval1")
self.assertEquals(0, db.session.query(ApprovalModel).count())
self.assertEqual(0, db.session.query(ApprovalModel).count())

View File

@ -87,10 +87,10 @@ class TestStudyApi(BaseTest):
headers=self.logged_in_headers(), content_type="application/json")
self.assert_success(api_response)
study = StudySchema().loads(api_response.get_data(as_text=True))
self.assertEquals(1, len(study.files))
self.assertEquals("UVA Compliance/PRC Approval", study.files[0]["category"])
self.assertEquals("Cancer Center's PRC Approval Form", study.files[0]["description"])
self.assertEquals("UVA Compliance/PRC Approval.png", study.files[0]["download_name"])
self.assertEqual(1, len(study.files))
self.assertEqual("UVA Compliance/PRC Approval", study.files[0]["category"])
self.assertEqual("Cancer Center's PRC Approval Form", study.files[0]["description"])
self.assertEqual("UVA Compliance/PRC Approval.png", study.files[0]["download_name"])
# TODO: WRITE A TEST FOR STUDY FILES
@ -180,10 +180,10 @@ class TestStudyApi(BaseTest):
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.assertEquals(num_abandoned, 1)
self.assertEquals(num_open, 1)
self.assertEquals(num_active, 1)
self.assertEquals(num_incomplete, 1)
self.assertEqual(num_abandoned, 1)
self.assertEqual(num_open, 1)
self.assertEqual(num_active, 1)
self.assertEqual(num_incomplete, 1)
self.assertEqual(len(json_data), num_db_studies_after)
self.assertEqual(num_open + num_active + num_incomplete + num_abandoned, num_db_studies_after)

View File

@ -153,7 +153,7 @@ class TestStudyService(BaseTest):
self.assertEqual(1, docs["UVACompl_PRCAppr"]['count'])
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0])
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0]['file_id'])
self.assertEquals(workflow.id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_id'])
self.assertEqual(workflow.id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_id'])
def test_get_all_studies(self):
user = self.create_user_with_study_and_workflow()
@ -174,8 +174,8 @@ class TestStudyService(BaseTest):
binary_data=b'1234', irb_doc_code="UVACompl_PRCAppr" )
studies = StudyService().get_all_studies_with_files()
self.assertEquals(1, len(studies))
self.assertEquals(3, len(studies[0].files))
self.assertEqual(1, len(studies))
self.assertEqual(3, len(studies[0].files))
@ -191,17 +191,17 @@ class TestStudyService(BaseTest):
workflow = self.create_workflow('docx') # The workflow really doesnt matter in this case.
investigators = StudyService().get_investigators(workflow.study_id)
self.assertEquals(9, len(investigators))
self.assertEqual(9, len(investigators))
# dhf8r is in the ldap mock data.
self.assertEquals("dhf8r", investigators['PI']['user_id'])
self.assertEquals("Dan Funk", investigators['PI']['display_name']) # Data from ldap
self.assertEquals("Primary Investigator", investigators['PI']['label']) # Data from xls file.
self.assertEquals("Always", investigators['PI']['display']) # Data from xls file.
self.assertEqual("dhf8r", investigators['PI']['user_id'])
self.assertEqual("Dan Funk", investigators['PI']['display_name']) # Data from ldap
self.assertEqual("Primary Investigator", investigators['PI']['label']) # Data from xls file.
self.assertEqual("Always", investigators['PI']['display']) # Data from xls file.
# asd3v is not in ldap, so an error should be returned.
self.assertEquals("asd3v", investigators['DC']['user_id'])
self.assertEquals("Unable to locate a user with id asd3v in LDAP", investigators['DC']['error']) # Data from ldap
self.assertEqual("asd3v", investigators['DC']['user_id'])
self.assertEqual("Unable to locate a user with id asd3v in LDAP", investigators['DC']['error']) # Data from ldap
# No value is provided for Department Chair
self.assertIsNone(investigators['DEPT_CH']['user_id'])

View File

@ -48,7 +48,7 @@ class TestTasksApi(BaseTest):
# The total number of tasks may change over time, as users move through gateways
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
self.assertIsNotNone(workflow.total_tasks)
self.assertEquals(prev_completed_task_count + 1, workflow.completed_tasks)
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
# Assure a record exists in the Task Events
task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \
@ -57,25 +57,25 @@ class TestTasksApi(BaseTest):
self.assertGreater(len(task_events), 0)
event = task_events[0]
self.assertIsNotNone(event.study_id)
self.assertEquals("dhf8r", event.user_uid)
self.assertEquals(workflow.id, event.workflow_id)
self.assertEquals(workflow.workflow_spec_id, event.workflow_spec_id)
self.assertEquals(workflow.spec_version, event.spec_version)
self.assertEquals(WorkflowService.TASK_ACTION_COMPLETE, event.action)
self.assertEquals(task_in.id, task_id)
self.assertEquals(task_in.name, event.task_name)
self.assertEquals(task_in.title, event.task_title)
self.assertEquals(task_in.type, event.task_type)
self.assertEquals("COMPLETED", event.task_state)
self.assertEqual("dhf8r", event.user_uid)
self.assertEqual(workflow.id, event.workflow_id)
self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id)
self.assertEqual(workflow.spec_version, event.spec_version)
self.assertEqual(WorkflowService.TASK_ACTION_COMPLETE, event.action)
self.assertEqual(task_in.id, task_id)
self.assertEqual(task_in.name, event.task_name)
self.assertEqual(task_in.title, event.task_title)
self.assertEqual(task_in.type, event.task_type)
self.assertEqual("COMPLETED", event.task_state)
# Not sure what vodoo is happening inside of marshmallow to get me in this state.
if isinstance(task_in.multi_instance_type, MultiInstanceType):
self.assertEquals(task_in.multi_instance_type.value, event.mi_type)
self.assertEqual(task_in.multi_instance_type.value, event.mi_type)
else:
self.assertEquals(task_in.multi_instance_type, event.mi_type)
self.assertEqual(task_in.multi_instance_type, event.mi_type)
self.assertEquals(task_in.multi_instance_count, event.mi_count)
self.assertEquals(task_in.multi_instance_index, event.mi_index)
self.assertEquals(task_in.process_name, event.process_name)
self.assertEqual(task_in.multi_instance_count, event.mi_count)
self.assertEqual(task_in.multi_instance_index, event.mi_index)
self.assertEqual(task_in.process_name, event.process_name)
self.assertIsNotNone(event.date)
@ -155,14 +155,14 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals("Do You Have Bananas", nav[0]['title'])
self.assertEquals("Bananas?", nav[1]['title'])
self.assertEquals("FUTURE", nav[1]['state'])
self.assertEquals("yes", nav[2]['title'])
self.assertEquals("NOOP", nav[2]['state'])
self.assertEquals("no", nav[3]['title'])
self.assertEquals("NOOP", nav[3]['state'])
self.assertEqual(5, len(nav))
self.assertEqual("Do You Have Bananas", nav[0]['title'])
self.assertEqual("Bananas?", nav[1]['title'])
self.assertEqual("FUTURE", nav[1]['state'])
self.assertEqual("yes", nav[2]['title'])
self.assertEqual("NOOP", nav[2]['state'])
self.assertEqual("no", nav[3]['title'])
self.assertEqual("NOOP", nav[3]['state'])
def test_navigation_with_exclusive_gateway(self):
self.load_example_data()
@ -172,14 +172,14 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow)
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEquals(7, len(nav))
self.assertEquals("Task 1", nav[0]['title'])
self.assertEquals("Which Branch?", nav[1]['title'])
self.assertEquals("a", nav[2]['title'])
self.assertEquals("Task 2a", nav[3]['title'])
self.assertEquals("b", nav[4]['title'])
self.assertEquals("Task 2b", nav[5]['title'])
self.assertEquals("Task 3", nav[6]['title'])
self.assertEqual(7, len(nav))
self.assertEqual("Task 1", nav[0]['title'])
self.assertEqual("Which Branch?", nav[1]['title'])
self.assertEqual("a", nav[2]['title'])
self.assertEqual("Task 2a", nav[3]['title'])
self.assertEqual("b", nav[4]['title'])
self.assertEqual("Task 2b", nav[5]['title'])
self.assertEqual("Task 3", nav[6]['title'])
def test_document_added_to_workflow_shows_up_in_file_list(self):
self.load_example_data()
@ -285,8 +285,8 @@ class TestTasksApi(BaseTest):
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
workflow = self.get_workflow_api(workflow)
self.assertEquals('Task_Manual_One', workflow.next_task.name)
self.assertEquals('ManualTask', workflow_api.next_task.type)
self.assertEqual('Task_Manual_One', workflow.next_task.name)
self.assertEqual('ManualTask', workflow_api.next_task.type)
self.assertTrue('Markdown' in workflow_api.next_task.documentation)
self.assertTrue('Dan' in workflow_api.next_task.documentation)
@ -296,7 +296,7 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
task = self.get_workflow_api(workflow).next_task
self.assertEquals("JustAValue", task.properties['JustAKey'])
self.assertEqual("JustAValue", task.properties['JustAKey'])
@patch('crc.services.protocol_builder.requests.get')
@ -316,13 +316,13 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
navigation = self.get_workflow_api(workflow).navigation
self.assertEquals(4, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEquals("UserTask", workflow.next_task.type)
self.assertEquals(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEquals(9, workflow.next_task.multi_instance_count)
self.assertEqual(4, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(9, workflow.next_task.multi_instance_count)
# Assure that the names for each task are properly updated, so they aren't all the same.
self.assertEquals("Primary Investigator", workflow.next_task.properties['display_name'])
self.assertEqual("Primary Investigator", workflow.next_task.properties['display_name'])
def test_lookup_endpoint_for_task_field_enumerations(self):
@ -364,18 +364,18 @@ class TestTasksApi(BaseTest):
navigation = workflow_api.navigation
task = workflow_api.next_task
self.assertEquals(2, len(navigation))
self.assertEquals("UserTask", task.type)
self.assertEquals("Activity_A", task.name)
self.assertEquals("My Sub Process", task.process_name)
self.assertEqual(2, len(navigation))
self.assertEqual("UserTask", task.type)
self.assertEqual("Activity_A", task.name)
self.assertEqual("My Sub Process", task.process_name)
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
task = workflow_api.next_task
self.assertIsNotNone(task)
self.assertEquals("Activity_B", task.name)
self.assertEquals("Sub Workflow Example", task.process_name)
self.assertEqual("Activity_B", task.name)
self.assertEqual("Sub Workflow Example", task.process_name)
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
self.assertEquals(WorkflowStatus.complete, workflow_api.status)
self.assertEqual(WorkflowStatus.complete, workflow_api.status)
def test_update_task_resets_token(self):
self.load_example_data()
@ -385,7 +385,7 @@ class TestTasksApi(BaseTest):
first_task = self.get_workflow_api(workflow).next_task
self.complete_form(workflow, first_task, {"has_bananas": True})
workflow = self.get_workflow_api(workflow)
self.assertEquals('Task_Num_Bananas', workflow.next_task.name)
self.assertEqual('Task_Num_Bananas', workflow.next_task.name)
# Trying to re-submit the initial task, and answer differently, should result in an error.
self.complete_form(workflow, first_task, {"has_bananas": False}, error_code="invalid_state")
@ -406,18 +406,18 @@ class TestTasksApi(BaseTest):
workflow = WorkflowApiSchema().load(json_data)
# Assure the Next Task is the one we just reset the token to be on.
self.assertEquals("Task_Has_Bananas", workflow.next_task.name)
self.assertEqual("Task_Has_Bananas", workflow.next_task.name)
# Go ahead and get that workflow one more time, it should still be right.
workflow = self.get_workflow_api(workflow)
# Assure the Next Task is the one we just reset the token to be on.
self.assertEquals("Task_Has_Bananas", workflow.next_task.name)
self.assertEqual("Task_Has_Bananas", workflow.next_task.name)
# The next task should be a different value.
self.complete_form(workflow, workflow.next_task, {"has_bananas": False})
workflow = self.get_workflow_api(workflow)
self.assertEquals('Task_Why_No_Bananas', workflow.next_task.name)
self.assertEqual('Task_Why_No_Bananas', workflow.next_task.name)
@patch('crc.services.protocol_builder.requests.get')
def test_parallel_multi_instance(self, mock_get):
@ -432,13 +432,13 @@ class TestTasksApi(BaseTest):
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEquals(12, len(workflow_api.navigation))
self.assertEqual(12, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"]
self.assertEquals(9, len(ready_items))
self.assertEqual(9, len(ready_items))
self.assertEquals("UserTask", workflow_api.next_task.type)
self.assertEquals("MutiInstanceTask",workflow_api.next_task.name)
self.assertEquals("more information", workflow_api.next_task.title)
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("MutiInstanceTask",workflow_api.next_task.name)
self.assertEqual("more information", workflow_api.next_task.title)
for i in random.sample(range(9), 9):
task = TaskSchema().load(ready_items[i]['task'])
@ -446,5 +446,5 @@ class TestTasksApi(BaseTest):
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)
self.assertEquals(WorkflowStatus.complete, workflow.status)
self.assertEqual(WorkflowStatus.complete, workflow.status)

View File

@ -28,7 +28,7 @@ class TestStudyApi(BaseTest):
content_type='multipart/form-data')
self.assert_success(rv)
self.assertIsNotNone(rv.data)
self.assertEquals('application/octet-stream', rv.content_type)
self.assertEqual('application/octet-stream', rv.content_type)
def test_list_scripts(self):
rv = self.app.get('/v1.0/list_scripts')

View File

@ -19,5 +19,5 @@ class TestUpdateStudyScript(BaseTest):
script = UpdateStudy()
script.do_task(task, workflow.study_id, workflow.id, "title:details.label", "pi:details.value")
self.assertEquals("My New Title", workflow.study.title)
self.assertEquals("dhf8r", workflow.study.primary_investigator_id)
self.assertEqual("My New Title", workflow.study.title)
self.assertEqual("dhf8r", workflow.study.primary_investigator_id)

View File

@ -57,13 +57,13 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task = next_user_tasks[0]
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
self.assertEquals("dhf8r", task.data["investigator"]["user_id"])
self.assertEqual("dhf8r", task.data["investigator"]["user_id"])
self.assertEqual("MutiInstanceTask", task.get_name())
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEquals(MultiInstanceType.sequential, api_task.multi_instance_type)
self.assertEquals(3, api_task.multi_instance_count)
self.assertEquals(1, api_task.multi_instance_index)
self.assertEqual(MultiInstanceType.sequential, api_task.multi_instance_type)
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(1, api_task.multi_instance_index)
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
@ -72,8 +72,8 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", api_task.name)
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
self.assertEquals(3, api_task.multi_instance_count)
self.assertEquals(2, api_task.multi_instance_index)
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(2, api_task.multi_instance_index)
processor.complete_task(task)
processor.do_engine_steps()
@ -81,8 +81,8 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", task.get_name())
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
self.assertEquals(3, api_task.multi_instance_count)
self.assertEquals(3, api_task.multi_instance_index)
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(3, api_task.multi_instance_index)
processor.complete_task(task)
processor.do_engine_steps()
task = processor.bpmn_workflow.last_task
@ -91,7 +91,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
expected['PI']['email'] = "asd3v@virginia.edu"
expected['SC_I']['email'] = "asdf32@virginia.edu"
expected['DC']['email'] = "dhf8r@virginia.edu"
self.assertEquals(expected,
self.assertEqual(expected,
task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())
@ -117,10 +117,10 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task = next_user_tasks[2]
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
self.assertEquals("asd3v", task.data["investigator"]["user_id"]) # The last of the tasks
self.assertEqual("asd3v", task.data["investigator"]["user_id"]) # The last of the tasks
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEquals(MultiInstanceType.parallel, api_task.multi_instance_type)
self.assertEqual(MultiInstanceType.parallel, api_task.multi_instance_type)
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
@ -144,7 +144,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
expected['PI']['email'] = "asd3v@virginia.edu"
expected['SC_I']['email'] = "asdf32@virginia.edu"
expected['DC']['email'] = "dhf8r@virginia.edu"
self.assertEquals(expected,
self.assertEqual(expected,
task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())

View File

@ -66,9 +66,9 @@ class TestWorkflowService(BaseTest):
task = processor.next_task()
WorkflowService.process_options(task, task.task_spec.form.fields[0])
options = task.task_spec.form.fields[0].options
self.assertEquals(28, len(options))
self.assertEquals('1000', options[0]['id'])
self.assertEquals("UVA - INTERNAL - GM USE ONLY", options[0]['name'])
self.assertEqual(28, len(options))
self.assertEqual('1000', options[0]['id'])
self.assertEqual("UVA - INTERNAL - GM USE ONLY", options[0]['name'])
def test_random_data_populate_form_on_auto_complete(self):
self.load_example_data()