diff --git a/Pipfile.lock b/Pipfile.lock index 43163832..f8ab746b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -351,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": [ @@ -510,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": [ @@ -924,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": [ diff --git a/config/default.py b/config/default.py index bac744fd..b42680ac 100644 --- a/config/default.py +++ b/config/default.py @@ -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") 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 = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com'] diff --git a/crc/__init__.py b/crc/__init__.py index 66b91b63..a1dd95f6 100644 --- a/crc/__init__.py +++ b/crc/__init__.py @@ -87,3 +87,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() diff --git a/crc/api.yml b/crc/api.yml index 07e61251..638eb787 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -808,12 +808,18 @@ paths: $ref: "#/components/schemas/Script" /approval: parameters: - - name: everything + - name: status in: query 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: - 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: operationId: crc.api.approval.get_approvals summary: Provides a list of workflows approvals diff --git a/crc/api/approval.py b/crc/api/approval.py index 9c42c82e..b23315df 100644 --- a/crc/api/approval.py +++ b/crc/api/approval.py @@ -13,11 +13,13 @@ 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) +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 diff --git a/crc/models/approval.py b/crc/models/approval.py index ee19f4b7..0592fbd1 100644 --- a/crc/models/approval.py +++ b/crc/models/approval.py @@ -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'] diff --git a/crc/scripts/request_approval.py b/crc/scripts/request_approval.py index 1e5d2c6c..0a4c76ff 100644 --- a/crc/scripts/request_approval.py +++ b/crc/scripts/request_approval.py @@ -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" diff --git a/crc/services/approval_service.py b/crc/services/approval_service.py index 04b0db65..8f9d45dd 100644 --- a/crc/services/approval_service.py +++ b/crc/services/approval_service.py @@ -19,44 +19,70 @@ from crc.services.mails import ( 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 diff --git a/crc/services/ldap_service.py b/crc/services/ldap_service.py index f6fb6c55..ef3d25f4 100644 --- a/crc/services/ldap_service.py +++ b/crc/services/ldap_service.py @@ -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 diff --git a/tests/test_approvals_api.py b/tests/test_approvals_api.py index 6d95be39..b80eaef0 100644 --- a/tests/test_approvals_api.py +++ b/tests/test_approvals_api.py @@ -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)