Merge branch 'feature/better_approval_status' into rrt/dev
This commit is contained in:
commit
213d3f3501
|
@ -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": [
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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()
|
||||||
|
|
12
crc/api.yml
12
crc/api.yml
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue