From 7b9c28ce9dd660500d28a8a541e4082e103ff70f Mon Sep 17 00:00:00 2001 From: dhakim87 Date: Thu, 12 Nov 2020 19:00:03 -0800 Subject: [PATCH 1/5] Backend for looking up sample status information by email address --- microsetta_private_api/admin/admin_impl.py | 76 +++++++++++++++++++ .../api/microsetta_private_api.yaml | 29 +++++++ microsetta_private_api/repo/sample_repo.py | 15 ++++ 3 files changed, 120 insertions(+) diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index 0813bb56..34dc28ec 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -1,4 +1,6 @@ import uuid +from collections import defaultdict + from flask import jsonify, Response from microsetta_private_api.config_manager import SERVER_CONFIG @@ -7,6 +9,9 @@ from microsetta_private_api.exceptions import RepoException from microsetta_private_api.repo.account_repo import AccountRepo from microsetta_private_api.repo.event_log_repo import EventLogRepo +from microsetta_private_api.repo.kit_repo import KitRepo +from microsetta_private_api.repo.sample_repo import SampleRepo +from microsetta_private_api.repo.source_repo import SourceRepo from microsetta_private_api.repo.transaction import Transaction from microsetta_private_api.repo.admin_repo import AdminRepo from microsetta_private_api.repo.metadata_repo import (retrieve_metadata, @@ -297,3 +302,74 @@ def get_daklapack_articles(token_info): admin_repo = AdminRepo(t) dak_article_dicts = admin_repo.get_daklapack_articles() return jsonify(dak_article_dicts), 200 + + +def query_email_stats(body, token_info): + validate_admin_access(token_info) + + email_list = body.get("emails") + project = body.get("project") + + results = [] + with Transaction() as t: + account_repo = AccountRepo(t) + kit_repo = KitRepo(t) + source_repo = SourceRepo(t) + sample_repo = SampleRepo(t) + + for email in email_list: + result = {'email': email, 'project': project} + results.append(result) + # can use internal lookup by email, because we have admin access + account = account_repo._find_account_by_email(email) # noqa + if account is None: + result['summary'] = "No Account" + continue + else: + result['account_id'] = account.id + result['creation_time'] = account.creation_time + result['kit_name'] = account.created_with_kit_id + + if account.created_with_kit_id is not None: + unused = kit_repo.get_kit_unused_samples( + account.created_with_kit_id + ) + result['outstanding-kit-samples'] = len(unused.samples) + + sample_statuses = defaultdict(int) + sources = source_repo.get_sources_in_account(account.id) + + samples_in_project = 0 + for source in sources: + samples = sample_repo.get_samples_by_source(account.id, + source.id) + for sample in samples: + if project is not None and \ + project not in sample.sample_projects: + continue + samples_in_project += 1 + sample_status = sample_repo.get_sample_status( + sample.barcode, + sample._latest_scan_timestamp + ) + sample_statuses[sample_status] += 1 + result.update(sample_statuses) + + if result.get('outstanding-kit-samples', 0) > 0: + result['summary'] = 'Possible Unreturned Samples' + elif result.get('sample-is-valid') == samples_in_project: + result['summary'] = 'All Samples Valid' + else: + result['summary'] = 'May Require User Interaction' + + return jsonify(results), 200 + + + + + + + + + + diff --git a/microsetta_private_api/api/microsetta_private_api.yaml b/microsetta_private_api/api/microsetta_private_api.yaml index 1dbd09a4..07ccb53a 100644 --- a/microsetta_private_api/api/microsetta_private_api.yaml +++ b/microsetta_private_api/api/microsetta_private_api.yaml @@ -1350,6 +1350,8 @@ paths: operationId: microsetta_private_api.admin.admin_impl.get_daklapack_articles tags: - Admin + parameters: + - $ref: '#/components/parameters/account_id' responses: '200': description: Return list of dictionaries of full info on all daklapack articles @@ -1358,6 +1360,33 @@ paths: schema: type: array + '/admin/account_email_summary': + post: + operationId: microsetta_private_api.admin.admin_impl.query_email_stats + tags: + - Admin + requestBody: + content: + application/json: + schema: + type: object + properties: + # issue type defines what resolution_url the user should go to + emails: + type: array + items: + $ref: '#/components/schemas/email' + project: + type: string + nullable: true + responses: + '200': + description: Return list of dictionaries of sample status for requested accounts + content: + application/json: + schema: + type: array + components: parameters: # path parameters diff --git a/microsetta_private_api/repo/sample_repo.py b/microsetta_private_api/repo/sample_repo.py index fa4c6b01..4acda2db 100644 --- a/microsetta_private_api/repo/sample_repo.py +++ b/microsetta_private_api/repo/sample_repo.py @@ -228,3 +228,18 @@ def dissociate_sample(self, account_id, source_id, sample_id, # And detach the sample from the source self._update_sample_association(sample_id, None, override_locked=override_locked) + + def get_sample_status(self, sample_barcode, scan_timestamp): + with self._transaction.cursor() as cur: + cur.execute( + "SELECT " + "sample_status " + "FROM barcodes.barcode_scans " + "WHERE barcode=%s AND scan_timestamp = %s " + "LIMIT 1", + (sample_barcode, scan_timestamp) + ) + row = cur.fetchone() + if row is None: + return None + return row[0] From 7e4a9873389a467bad2103c9496e948006d7d3ea Mon Sep 17 00:00:00 2001 From: dhakim87 Date: Fri, 13 Nov 2020 14:54:40 -0800 Subject: [PATCH 2/5] cast known numeric columns and fill NaNs with 0s --- microsetta_private_api/admin/admin_impl.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index 34dc28ec..81f67068 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -334,7 +334,10 @@ def query_email_stats(body, token_info): unused = kit_repo.get_kit_unused_samples( account.created_with_kit_id ) - result['outstanding-kit-samples'] = len(unused.samples) + if unused is None: + result['unclaimed-samples-in-kit'] = 0 + else: + result['unclaimed-samples-in-kit'] = len(unused.samples) sample_statuses = defaultdict(int) sources = source_repo.get_sources_in_account(account.id) @@ -345,18 +348,23 @@ def query_email_stats(body, token_info): source.id) for sample in samples: if project is not None and \ + project != "" and \ project not in sample.sample_projects: continue samples_in_project += 1 sample_status = sample_repo.get_sample_status( sample.barcode, - sample._latest_scan_timestamp + sample._latest_scan_timestamp # noqa ) + if sample_status is None: + sample_status = "never-scanned" sample_statuses[sample_status] += 1 result.update(sample_statuses) - if result.get('outstanding-kit-samples', 0) > 0: + if result.get('unclaimed-samples-in-kit', 0) > 0: result['summary'] = 'Possible Unreturned Samples' + elif samples_in_project == 0: + result['summary'] = "No Samples In Specified Project" elif result.get('sample-is-valid') == samples_in_project: result['summary'] = 'All Samples Valid' else: From c6d1668ad20929d06c2ca21494069af4629f1a63 Mon Sep 17 00:00:00 2001 From: dhakim87 Date: Fri, 13 Nov 2020 14:55:42 -0800 Subject: [PATCH 3/5] Flake8 --- microsetta_private_api/admin/admin_impl.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index 81f67068..7f5255b5 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -371,13 +371,3 @@ def query_email_stats(body, token_info): result['summary'] = 'May Require User Interaction' return jsonify(results), 200 - - - - - - - - - - From bfcc93a30e1ba4ef2bf9989057b3acb781d7b0c6 Mon Sep 17 00:00:00 2001 From: dhakim87 Date: Fri, 13 Nov 2020 15:31:22 -0800 Subject: [PATCH 4/5] What the... why did I modify daklapack route... Unmodified --- microsetta_private_api/api/microsetta_private_api.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/microsetta_private_api/api/microsetta_private_api.yaml b/microsetta_private_api/api/microsetta_private_api.yaml index 07ccb53a..2e0e474c 100644 --- a/microsetta_private_api/api/microsetta_private_api.yaml +++ b/microsetta_private_api/api/microsetta_private_api.yaml @@ -1350,8 +1350,6 @@ paths: operationId: microsetta_private_api.admin.admin_impl.get_daklapack_articles tags: - Admin - parameters: - - $ref: '#/components/parameters/account_id' responses: '200': description: Return list of dictionaries of full info on all daklapack articles From c6e15534d913874f6e7d10fb38d78ebfbca1a5f1 Mon Sep 17 00:00:00 2001 From: dhakim87 Date: Fri, 13 Nov 2020 16:45:22 -0800 Subject: [PATCH 5/5] Defaulted emails to empty list rather than None, added unit test exercising new route with no project, a valid project, and an invalid project --- microsetta_private_api/admin/admin_impl.py | 2 +- .../admin/tests/test_admin_api.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index 7f5255b5..739258eb 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -307,7 +307,7 @@ def get_daklapack_articles(token_info): def query_email_stats(body, token_info): validate_admin_access(token_info) - email_list = body.get("emails") + email_list = body.get("emails", []) project = body.get("project") results = [] diff --git a/microsetta_private_api/admin/tests/test_admin_api.py b/microsetta_private_api/admin/tests/test_admin_api.py index 8a44237f..98ae90ef 100644 --- a/microsetta_private_api/admin/tests/test_admin_api.py +++ b/microsetta_private_api/admin/tests/test_admin_api.py @@ -716,6 +716,34 @@ def test_get_daklapack_articles(self): self.assertEqual(len(article_dicts_list), len(response_obj)) self.assertEqual(FIRST_DAKLAPACK_ARTICLE, response_obj[0]) + def test_email_stats(self): + with Transaction() as t: + accts = AccountRepo(t) + acct1 = accts.get_account("65dcd6c8-69fa-4de8-a33a-3de4957a0c79") + acct2 = accts.get_account("556f5dc4-8cf2-49ae-876c-32fbdfb005dd") + + # execute articles get + for project in [None, "American Gut Project", "NotAProj"]: + response = self.client.post( + "/api/admin/account_email_summary", + headers=MOCK_HEADERS, + content_type='application/json', + data=json.dumps({ + "emails": [acct1.email, acct2.email], + "project": project + }) + ) + self.assertEqual(200, response.status_code) + result = json.loads(response.data) + self.assertEqual(result[0]["account_id"], + "65dcd6c8-69fa-4de8-a33a-3de4957a0c79") + self.assertEqual(result[1]["account_id"], + "556f5dc4-8cf2-49ae-876c-32fbdfb005dd") + if project is None or project == "American Gut Project": + self.assertEqual(result[0]["sample-is-valid"], 1) + else: + self.assertEqual(result[0].get("sample-is-valid", 0), 0) + def test_metadata_qiita_compatible_invalid(self): data = json.dumps({'sample_barcodes': ['bad']}) response = self.client.post('/api/admin/metadata/qiita-compatible',