Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vocab for sample issues #574

Merged
merged 14 commits into from
Jul 30, 2024
3 changes: 2 additions & 1 deletion microsetta_private_api/admin/tests/test_admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,8 @@ def test_scan_barcode_success(self):
scan_info = {
"sample_barcode": self.TEST_BARCODE,
"sample_status": "sample-is-valid",
"technician_notes": ""
"technician_notes": "",
"observations": []
}
input_json = json.dumps(scan_info)

Expand Down
87 changes: 84 additions & 3 deletions microsetta_private_api/admin/tests/test_admin_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import psycopg2.extras
from dateutil.relativedelta import relativedelta

from microsetta_private_api.exceptions import RepoException
import microsetta_private_api.model.project as p

from werkzeug.exceptions import Unauthorized, NotFound
Expand Down Expand Up @@ -323,15 +324,18 @@ def make_tz_datetime(y, m, d):
"barcode": test_barcode,
"scan_timestamp": make_tz_datetime(2017, 7, 16),
"sample_status": 'no-registered-account',
"technician_notes": "huh?"
"technician_notes": "huh?",
"observations": [None]
}

second_scan = {
"barcode_scan_id": second_scan_id,
"barcode": test_barcode,
"scan_timestamp": make_tz_datetime(2020, 12, 4),
"sample_status": 'sample-is-valid',
"technician_notes": None
"technician_notes": None,
"observations": [None]

}
try:
add_dummy_scan(first_scan)
Expand Down Expand Up @@ -779,6 +783,7 @@ def test_scan_barcode_success(self):
TEST_BARCODE = '000000001'
TEST_STATUS = "sample-has-inconsistencies"
TEST_NOTES = "THIS IS A UNIT TEST"
TEST_OBSERVATIONS = ["Tube is not intact"]
admin_repo = AdminRepo(t)

# check that before doing a scan, no scans are recorded for this
Expand All @@ -790,7 +795,8 @@ def test_scan_barcode_success(self):
TEST_BARCODE,
{
"sample_status": TEST_STATUS,
"technician_notes": TEST_NOTES
"technician_notes": TEST_NOTES,
"observations": TEST_OBSERVATIONS
}
)

Expand All @@ -800,6 +806,81 @@ def test_scan_barcode_success(self):
first_scan = diag['scans_info'][0]
self.assertEqual(first_scan['technician_notes'], TEST_NOTES)
self.assertEqual(first_scan['sample_status'], TEST_STATUS)
self.assertEqual(first_scan['observations'], TEST_OBSERVATIONS)

def test_scan_with_no_observations(self):
with Transaction() as t:

TEST_BARCODE = '000000001'
TEST_NOTES = "THIS IS A UNIT TEST"
TEST_STATUS = "sample-has-inconsistencies"
admin_repo = AdminRepo(t)

# check that before doing a scan, no scans are recorded for this
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
self.assertEqual(len(diag['scans_info']), 0)

admin_repo.scan_barcode(
TEST_BARCODE,
{
"sample_status": TEST_STATUS,
"technician_notes": TEST_NOTES,
"observations": None
}
)
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
first_scan = diag['scans_info'][0]
self.assertEqual(first_scan['observations'], [None])

def test_scan_with_multiple_observations(self):
with Transaction() as t:

TEST_BARCODE = '000000001'
TEST_NOTES = "THIS IS A UNIT TEST"
TEST_STATUS = "sample-has-inconsistencies"
TEST_OBSERVATIONS = ["Tube is not intact",
"Screw cap is loose",
"Insufficient ethanol"]
admin_repo = AdminRepo(t)

# check that before doing a scan, no scans are recorded for this
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
self.assertEqual(len(diag['scans_info']), 0)

admin_repo.scan_barcode(
TEST_BARCODE,
{
"sample_status": TEST_STATUS,
"technician_notes": TEST_NOTES,
"observations": TEST_OBSERVATIONS
}
)
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
first_scan = diag['scans_info'][0]
self.assertEqual(len(first_scan['observations']), 3)

def test_scan_with_wrong_observation(self):
with Transaction() as t:

TEST_BARCODE = '000000001'
TEST_NOTES = "THIS IS A UNIT TEST"
TEST_STATUS = "sample-has-inconsistencies"
TEST_OBSERVATIONS = ["Wrong"]
admin_repo = AdminRepo(t)

# check that before doing a scan, no scans are recorded for this
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
self.assertEqual(len(diag['scans_info']), 0)

with self.assertRaises(RepoException):
admin_repo.scan_barcode(
TEST_BARCODE,
{
"sample_status": TEST_STATUS,
"technician_notes": TEST_NOTES,
"observations": TEST_OBSERVATIONS
}
)

def test_scan_barcode_error_nonexistent(self):
with Transaction() as t:
Expand Down
5 changes: 5 additions & 0 deletions microsetta_private_api/api/microsetta_private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2578,6 +2578,11 @@ paths:
technician_notes:
type: string
example: "Sample Processing Complete!"
observations:
type: array
items:
type: string
example: ["Observation 1", "Observation 2"]
responses:
'201':
description: Successfully recorded new barcode scan
Expand Down
15 changes: 10 additions & 5 deletions microsetta_private_api/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2328,7 +2328,8 @@ def test_associate_sample_locked(self):
any_status = 'sample-has-inconsistencies'
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
json={'sample_status': any_status,
'technician_notes': "foobar"},
'technician_notes': "foobar",
'observations': []},
headers=make_headers(FAKE_TOKEN_ADMIN))
self.assertEqual(201, post_resp.status_code)

Expand Down Expand Up @@ -2383,7 +2384,8 @@ def test_edit_sample_locked(self):
bad_status = 'sample-has-inconsistencies'
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
json={'sample_status': bad_status,
'technician_notes': "foobar"},
'technician_notes': "foobar",
'observations': []},
headers=make_headers(FAKE_TOKEN_ADMIN))
self.assertEqual(201, post_resp.status_code)

Expand Down Expand Up @@ -2448,7 +2450,8 @@ def test_edit_sample_locked(self):
good_status = "sample-is-valid"
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
json={'sample_status': good_status,
'technician_notes': "foobar"},
'technician_notes': "foobar",
'observations': []},
headers=make_headers(FAKE_TOKEN_ADMIN))
self.assertEqual(201, post_resp.status_code)

Expand Down Expand Up @@ -2508,7 +2511,8 @@ def test_dissociate_sample_from_source_locked(self):
dummy_is_admin=True)
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
json={'sample_status': 'sample-is-valid',
'technician_notes': "foobar"},
'technician_notes': "foobar",
'observations': []},
headers=make_headers(FAKE_TOKEN_ADMIN))
self.assertEqual(201, post_resp.status_code)

Expand Down Expand Up @@ -2563,7 +2567,8 @@ def test_update_sample_association_locked(self):
dummy_is_admin=True)
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
json={'sample_status': 'sample-is-valid',
'technician_notes': "foobar"},
'technician_notes': "foobar",
'observations': []},
headers=make_headers(FAKE_TOKEN_ADMIN))
self.assertEqual(201, post_resp.status_code)

Expand Down
56 changes: 56 additions & 0 deletions microsetta_private_api/db/patches/0140.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- May 13, 2024
-- Create table to store observation categories
CREATE TABLE sample_observation_categories (
ayobi marked this conversation as resolved.
Show resolved Hide resolved
category VARCHAR(255) PRIMARY KEY
);

-- Insert predefined observation categories
INSERT INTO sample_observation_categories (category)
VALUES ('Sample'), ('Swab'), ('Tube');

-- Create table to store sample observations
CREATE TABLE sample_observations (
observation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category VARCHAR(255) NOT NULL,
observation VARCHAR(255) NOT NULL,
FOREIGN KEY (category) REFERENCES sample_observation_categories(category),
UNIQUE (category, observation)
);

-- Create table to store associations between observations and projects
CREATE TABLE sample_observation_project_associations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
observation_id UUID NOT NULL,
project_id INT NOT NULL,
ayobi marked this conversation as resolved.
Show resolved Hide resolved
FOREIGN KEY (observation_id) REFERENCES sample_observations(observation_id),
UNIQUE (observation_id)
ayobi marked this conversation as resolved.
Show resolved Hide resolved
);

-- Insert predefined observations and associate them with a project
WITH inserted_observations AS (
INSERT INTO sample_observations (category, observation)
VALUES
('Tube', 'Tube is not intact'),
('Tube', 'Screw cap is loose'),
('Tube', 'Insufficient ethanol'),
('Tube', 'No ethanol'),
('Swab', 'No swab in tube'),
('Swab', 'Multiple swabs in tube'),
('Swab', 'Incorrect swab type'),
('Sample', 'No visible sample'),
('Sample', 'Excess sample on swab')
RETURNING observation_id, category, observation
)
INSERT INTO sample_observation_project_associations (observation_id, project_id)
SELECT observation_id, 1
FROM inserted_observations;

-- Create table to store observation ids associated with barcode scans ids
CREATE TABLE sample_barcode_scan_observations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
barcode_scan_id UUID NOT NULL,
observation_id UUID NOT NULL,
FOREIGN KEY (barcode_scan_id) REFERENCES barcodes.barcode_scans(barcode_scan_id),
FOREIGN KEY (observation_id) REFERENCES sample_observations(observation_id),
UNIQUE (barcode_scan_id, observation_id)
);
77 changes: 67 additions & 10 deletions microsetta_private_api/repo/admin_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ def _rows_to_dicts_list(rows):
# get (partial) projects_info list for this barcode
query = f"""
SELECT {p.DB_PROJ_NAME_KEY}, {p.IS_MICROSETTA_KEY},
{p.BANK_SAMPLES_KEY}, {p.PLATING_START_DATE_KEY}
{p.BANK_SAMPLES_KEY}, {p.PLATING_START_DATE_KEY},
project_id
FROM barcodes.project
INNER JOIN barcodes.project_barcode
USING (project_id)
Expand All @@ -389,13 +390,34 @@ def _rows_to_dicts_list(rows):
# get scans_info list for this barcode
# NB: ORDER MATTERS here. Do not change the order unless you
# are positive you know what already depends on it.
cur.execute("SELECT barcode_scan_id, barcode, "
"scan_timestamp, sample_status, "
"technician_notes "
"FROM barcodes.barcode_scans "
"WHERE barcode=%s "
"ORDER BY scan_timestamp asc",
(sample_barcode,))
cur.execute("""
SELECT
bs.barcode_scan_id,
bs.barcode,
bs.scan_timestamp,
bs.sample_status,
bs.technician_notes,
array_agg(so.observation) AS observations
FROM
barcodes.barcode_scans bs
LEFT JOIN
sample_barcode_scan_observations bso ON bs.barcode_scan_id
= bso.barcode_scan_id
LEFT JOIN
sample_observations so ON bso.observation_id
= so.observation_id
LEFT JOIN
sample_observation_project_associations sopa
ON so.observation_id = sopa.observation_id
WHERE
bs.barcode = %s
GROUP BY
bs.barcode_scan_id, bs.barcode, bs.scan_timestamp,
bs.sample_status, bs.technician_notes
ORDER BY
bs.scan_timestamp ASC
""", (sample_barcode,))

# this can't be None; worst-case is an empty list
scans_info = _rows_to_dicts_list(cur.fetchall())

Expand Down Expand Up @@ -424,14 +446,23 @@ def _rows_to_dicts_list(rows):
and barcode_info is None:
return None

cur.execute("""
ayobi marked this conversation as resolved.
Show resolved Hide resolved
SELECT so.*, sopa.project_id
FROM ag.sample_observations so
JOIN ag.sample_observation_project_associations sopa
ON so.observation_id = sopa.observation_id
""")
observations = _rows_to_dicts_list(cur.fetchall())

diagnostic = {
"account": account,
"source": source,
"sample": sample,
"latest_scan": latest_scan,
"scans_info": scans_info,
"barcode_info": barcode_info,
"projects_info": projects_info
"projects_info": projects_info,
"observations": observations
}

if grab_kit:
Expand Down Expand Up @@ -1091,7 +1122,7 @@ def scan_barcode(self, sample_barcode, scan_info):
sample_barcode,
datetime.datetime.now(),
scan_info['sample_status'],
scan_info['technician_notes']
scan_info['technician_notes'],
ayobi marked this conversation as resolved.
Show resolved Hide resolved
)

cur.execute(
Expand All @@ -1102,6 +1133,32 @@ def scan_barcode(self, sample_barcode, scan_info):
scan_args
)

if scan_info['observations']:
for observation in scan_info['observations']:
cur.execute(
"SELECT observation_id FROM sample_observations "
"WHERE observation = %s",
(observation,)
)
ayobi marked this conversation as resolved.
Show resolved Hide resolved

result = cur.fetchone()
if result is None:
raise RepoException(
f"No observation_id found for "
f"observation: {observation}"
)

observation_id = result[0]

cur.execute(
ayobi marked this conversation as resolved.
Show resolved Hide resolved
"""
INSERT INTO sample_barcode_scan_observations
(barcode_scan_id, observation_id)
VALUES (%s, %s)
""",
(new_uuid, observation_id)
)

return new_uuid

def search_barcode(self, sql_cond, cond_params):
Expand Down
Loading