Merge branch 'feature/better_approval_status' into rrt/dev

This commit is contained in:
Dan Funk 2020-06-05 19:11:16 -04:00
commit 213d3f3501
10 changed files with 102 additions and 57 deletions

56
Pipfile.lock generated
View File

@ -351,11 +351,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==1.6.0" "version": "==1.6.1"
}, },
"inflection": { "inflection": {
"hashes": [ "hashes": [
@ -510,29 +510,29 @@
}, },
"numpy": { "numpy": {
"hashes": [ "hashes": [
"sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d", "sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233",
"sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897", "sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b",
"sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88", "sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7",
"sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6", "sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f",
"sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7", "sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5",
"sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26", "sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb",
"sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a", "sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583",
"sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d", "sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1",
"sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961", "sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a",
"sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5", "sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271",
"sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2", "sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824",
"sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032", "sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3",
"sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba", "sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc",
"sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085", "sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161",
"sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509", "sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f",
"sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170", "sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f",
"sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae", "sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf",
"sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d", "sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b",
"sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c", "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0",
"sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720", "sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675",
"sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec" "sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"
], ],
"version": "==1.18.4" "version": "==1.18.5"
}, },
"openapi-spec-validator": { "openapi-spec-validator": {
"hashes": [ "hashes": [
@ -924,11 +924,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==1.6.0" "version": "==1.6.1"
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [

View File

@ -42,7 +42,7 @@ 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") 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_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))
# Fallback emails # Fallback emails
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com'] FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']

View File

@ -87,3 +87,9 @@ def load_example_rrt_data():
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
ExampleDataLoader.clean_db() ExampleDataLoader.clean_db()
ExampleDataLoader().load_rrt() 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

@ -808,12 +808,18 @@ paths:
$ref: "#/components/schemas/Script" $ref: "#/components/schemas/Script"
/approval: /approval:
parameters: parameters:
- name: everything - name: status
in: query in: query
required: false required: false
description: If set to true, returns all the approvals known to the system. description: If set to true, returns all the approvals with any status.
schema: schema:
type: boolean 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: get:
operationId: crc.api.approval.get_approvals operationId: crc.api.approval.get_approvals
summary: Provides a list of workflows approvals summary: Provides a list of workflows approvals

View File

@ -13,11 +13,13 @@ from crc.services.approval_service import ApprovalService
from crc.services.ldap_service import LdapService from crc.services.ldap_service import LdapService
def get_approvals(everything=False): def get_approvals(status=None, as_user=None):
if everything: #status = ApprovalStatus.PENDING.value
approvals = ApprovalService.get_all_approvals(include_cancelled=True) user = g.user.uid
else: if as_user:
approvals = ApprovalService.get_approvals_per_user(g.user.uid, include_cancelled=False) user = as_user
approvals = ApprovalService.get_approvals_per_user(user, status,
include_cancelled=False)
results = ApprovalSchema(many=True).dump(approvals) results = ApprovalSchema(many=True).dump(approvals)
return results return results

View File

@ -20,6 +20,9 @@ class ApprovalStatus(enum.Enum):
DECLINED = "DECLINED" # rejected by the reviewer DECLINED = "DECLINED" # rejected by the reviewer
CANCELED = "CANCELED" # The document was replaced with a new version and this review is no longer needed. 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): class ApprovalFile(db.Model):
file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True) file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True)
@ -81,13 +84,13 @@ class Approval(object):
instance.associated_files = [] instance.associated_files = []
for approval_file in model.approval_files: for approval_file in model.approval_files:
try: 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] extra_info = doc_dictionary[approval_file.file_data.file_model.irb_doc_code]
except: except:
extra_info = None extra_info = None
associated_file = {} associated_file = {}
associated_file['id'] = approval_file.file_data.file_model.id associated_file['id'] = approval_file.file_data.file_model.id
if extra_info: if extra_info:
irb_doc_code = approval_file.file_data.file_model.irb_doc_code
associated_file['name'] = '_'.join((extra_info['category1'], associated_file['name'] = '_'.join((extra_info['category1'],
approval_file.file_data.file_model.name)) approval_file.file_data.file_model.name))
associated_file['description'] = extra_info['description'] associated_file['description'] = extra_info['description']

View File

@ -11,7 +11,8 @@ class RequestApproval(Script):
return """ return """
Creates an approval request on this workflow, by the given approver_uid(s)," 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 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: Example:
RequestApproval approver1 "dhf8r" RequestApproval approver1 "dhf8r"

View File

@ -19,44 +19,70 @@ from crc.services.mails import (
send_ramp_up_denied_email_to_approver send_ramp_up_denied_email_to_approver
) )
class ApprovalService(object): class ApprovalService(object):
"""Provides common tools for working with an Approval""" """Provides common tools for working with an Approval"""
@staticmethod @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', """Returns one approval, with all additional approvals as 'related_approvals',
the main approval can be pinned to an approver with an optional argument. the main approval can be pinned to an approver with an optional argument.
Will return null if no approvals exist on the study.""" Will return null if no approvals exist on the study."""
main_approval = None main_approval = None
related_approvals = [] related_approvals = []
query = db.session.query(ApprovalModel).\ query = db.session.query(ApprovalModel).filter(ApprovalModel.study_id == study.id)
filter(ApprovalModel.study_id == study.id)
if not include_cancelled: if not include_cancelled:
query=query.filter(ApprovalModel.status != ApprovalStatus.CANCELED.value) query=query.filter(ApprovalModel.status != ApprovalStatus.CANCELED.value)
approvals = query.all() # All non-cancelled approvals.
approvals = query.all()
for approval_model in approvals: for approval_model in approvals:
if approval_model.approver_uid == approver_uid: if approval_model.approver_uid == approver_uid:
main_approval = Approval.from_model(approval_model) main_approval = approval_model
else: 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: if not main_approval and len(related_approvals) > 0:
main_approval = related_approvals[0] main_approval = related_approvals[0]
related_approvals = related_approvals[1:] 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 return main_approval
@staticmethod @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 """Returns a list of approval objects (not db models) for the given
approver. """ approver. """
studies = db.session.query(StudyModel).join(ApprovalModel).\ studies = db.session.query(StudyModel).join(ApprovalModel).\
filter(ApprovalModel.approver_uid == approver_uid).all() filter(ApprovalModel.approver_uid == approver_uid).all()
approvals = [] approvals = []
for study in studies: 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: if approval:
approvals.append(approval) approvals.append(approval)
return approvals return approvals

View File

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

View File

@ -68,16 +68,15 @@ class TestApprovals(BaseTest):
approval = response[0] approval = response[0]
self.assertEqual(approval['approver']['uid'], approver_uid) self.assertEqual(approval['approver']['uid'], approver_uid)
def test_list_approvals_per_admin(self): def test_list_approvals_as_user(self):
"""All approvals will be returned""" """All approvals as different user"""
rv = self.app.get('/v1.0/approval?everything=true', headers=self.logged_in_headers()) rv = self.app.get('/v1.0/approval?as_user=lb3dp', headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
response = json.loads(rv.get_data(as_text=True)) response = json.loads(rv.get_data(as_text=True))
# Returned approvals should match what's in the db, we should get one approval back # Returned approvals should match what's in the db for user ld3dp, we should get one
# per study (2 studies), and that approval should have one related approval. # approval back per study (2 studies), and that approval should have one related approval.
approvals_count = ApprovalModel.query.count()
response_count = len(response) response_count = len(response)
self.assertEqual(2, response_count) self.assertEqual(2, response_count)