Skip to content

Commit 233e89c

Browse files
Merge pull request #574 from ayobi/vocab_samples_issues
vocab for sample issues
2 parents 7fecc9e + e4259ee commit 233e89c

File tree

7 files changed

+314
-25
lines changed

7 files changed

+314
-25
lines changed

microsetta_private_api/admin/admin_impl.py

+10
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ def scan_barcode(token_info, sample_barcode, body):
153153
return response
154154

155155

156+
def get_observations(token_info, sample_barcode):
157+
validate_admin_access(token_info)
158+
159+
with Transaction() as t:
160+
admin_repo = AdminRepo(t)
161+
observations = admin_repo.\
162+
retrieve_observations_by_project(sample_barcode)
163+
return jsonify(observations), 200
164+
165+
156166
def sample_pulldown_single_survey(token_info,
157167
sample_barcode,
158168
survey_template_id):

microsetta_private_api/admin/tests/test_admin_api.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,8 @@ def test_scan_barcode_success(self):
695695
scan_info = {
696696
"sample_barcode": self.TEST_BARCODE,
697697
"sample_status": "sample-is-valid",
698-
"technician_notes": ""
698+
"technician_notes": "",
699+
"observations": []
699700
}
700701
input_json = json.dumps(scan_info)
701702

microsetta_private_api/admin/tests/test_admin_repo.py

+113-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import psycopg2.extras
66
from dateutil.relativedelta import relativedelta
77

8+
from microsetta_private_api.exceptions import RepoException
89
import microsetta_private_api.model.project as p
910

1011
from werkzeug.exceptions import Unauthorized, NotFound
@@ -323,15 +324,19 @@ def make_tz_datetime(y, m, d):
323324
"barcode": test_barcode,
324325
"scan_timestamp": make_tz_datetime(2017, 7, 16),
325326
"sample_status": 'no-registered-account',
326-
"technician_notes": "huh?"
327+
"technician_notes": "huh?",
328+
"observations": [{'observation_id': None, 'observation': None,
329+
'category': None}]
327330
}
328331

329332
second_scan = {
330333
"barcode_scan_id": second_scan_id,
331334
"barcode": test_barcode,
332335
"scan_timestamp": make_tz_datetime(2020, 12, 4),
333336
"sample_status": 'sample-is-valid',
334-
"technician_notes": None
337+
"technician_notes": None,
338+
"observations": [{'observation_id': None, 'observation': None,
339+
'category': None}]
335340
}
336341
try:
337342
add_dummy_scan(first_scan)
@@ -347,6 +352,7 @@ def make_tz_datetime(y, m, d):
347352
self.assertGreater(len(diag['projects_info']), 0)
348353
self.assertEqual(len(diag['scans_info']), 2)
349354
# order matters in the returned vals, so test that
355+
print(diag['scans_info'][0], first_scan)
350356
self.assertEqual(diag['scans_info'][0], first_scan)
351357
self.assertEqual(diag['scans_info'][1], second_scan)
352358
self.assertEqual(diag['latest_scan'], second_scan)
@@ -776,30 +782,127 @@ def test_scan_barcode_success(self):
776782
# TODO FIXME HACK: Need to build mock barcodes rather than using
777783
# these fixed ones
778784

779-
TEST_BARCODE = '000000001'
785+
TEST_BARCODE = '000010860'
780786
TEST_STATUS = "sample-has-inconsistencies"
781787
TEST_NOTES = "THIS IS A UNIT TEST"
782788
admin_repo = AdminRepo(t)
789+
with t.dict_cursor() as cur:
790+
cur.execute("SELECT observation_id "
791+
"FROM "
792+
"barcodes.sample_observation_project_associations "
793+
"WHERE project_id = 1")
794+
observation_id = cur.fetchone()
795+
796+
# check that before doing a scan,
797+
# no scans are recorded for this
798+
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
799+
self.assertEqual(len(diag['scans_info']), 0)
800+
801+
# do a scan
802+
admin_repo.scan_barcode(
803+
TEST_BARCODE,
804+
{
805+
"sample_status": TEST_STATUS,
806+
"technician_notes": TEST_NOTES,
807+
"observations": observation_id
808+
}
809+
)
810+
811+
# show that now a scan is recorded for this barcode
812+
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
813+
self.assertEqual(len(diag['scans_info']), 1)
814+
first_scan = diag['scans_info'][0]
815+
first_observation = first_scan['observations'][0]
816+
scan_observation_id = first_observation['observation_id']
817+
818+
self.assertEqual(first_scan['technician_notes'], TEST_NOTES)
819+
self.assertEqual(first_scan['sample_status'], TEST_STATUS)
820+
self.assertEqual(scan_observation_id, observation_id[0])
821+
822+
def test_scan_with_no_observations(self):
823+
with Transaction() as t:
824+
825+
TEST_BARCODE = '000010860'
826+
TEST_NOTES = "THIS IS A UNIT TEST"
827+
TEST_STATUS = "sample-has-inconsistencies"
828+
admin_repo = AdminRepo(t)
783829

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

788-
# do a scan
789834
admin_repo.scan_barcode(
790835
TEST_BARCODE,
791836
{
792837
"sample_status": TEST_STATUS,
793-
"technician_notes": TEST_NOTES
838+
"technician_notes": TEST_NOTES,
839+
"observations": None
794840
}
795841
)
796-
797-
# show that now a scan is recorded for this barcode
798842
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
799-
self.assertEqual(len(diag['scans_info']), 1)
800843
first_scan = diag['scans_info'][0]
801-
self.assertEqual(first_scan['technician_notes'], TEST_NOTES)
802-
self.assertEqual(first_scan['sample_status'], TEST_STATUS)
844+
first_observation = first_scan['observations'][0]
845+
scan_observation = first_observation['observation']
846+
self.assertEqual(scan_observation, None)
847+
848+
def test_scan_with_multiple_observations(self):
849+
with Transaction() as t:
850+
851+
TEST_BARCODE = '000010860'
852+
TEST_NOTES = "THIS IS A UNIT TEST"
853+
TEST_STATUS = "sample-has-inconsistencies"
854+
admin_repo = AdminRepo(t)
855+
856+
with t.dict_cursor() as cur:
857+
cur.execute("SELECT observation_id "
858+
"FROM "
859+
"barcodes.sample_observation_project_associations "
860+
"WHERE project_id = 1")
861+
rows = cur.fetchmany(2)
862+
observation_ids = [row['observation_id'] for row in rows]
863+
864+
# check that before doing a scan,
865+
# no scans are recorded for this
866+
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
867+
self.assertEqual(len(diag['scans_info']), 0)
868+
869+
admin_repo.scan_barcode(
870+
TEST_BARCODE,
871+
{
872+
"sample_status": TEST_STATUS,
873+
"technician_notes": TEST_NOTES,
874+
"observations": observation_ids
875+
}
876+
)
877+
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
878+
scans = [scan['observations'] for scan in diag['scans_info']]
879+
scans_observation_ids = [obs['observation_id'] for scan in
880+
scans for obs in scan]
881+
882+
self.assertEqual(scans_observation_ids, observation_ids)
883+
884+
def test_scan_with_wrong_observation(self):
885+
with Transaction() as t:
886+
887+
TEST_BARCODE = '000000001'
888+
TEST_NOTES = "THIS IS A UNIT TEST"
889+
TEST_STATUS = "sample-has-inconsistencies"
890+
TEST_OBSERVATIONS = ["ad374d60-466d-4db0-9a91-5e3e8aec7698"]
891+
admin_repo = AdminRepo(t)
892+
893+
# check that before doing a scan, no scans are recorded for this
894+
diag = admin_repo.retrieve_diagnostics_by_barcode(TEST_BARCODE)
895+
self.assertEqual(len(diag['scans_info']), 0)
896+
897+
with self.assertRaises(RepoException):
898+
admin_repo.scan_barcode(
899+
TEST_BARCODE,
900+
{
901+
"sample_status": TEST_STATUS,
902+
"technician_notes": TEST_NOTES,
903+
"observations": TEST_OBSERVATIONS
904+
}
905+
)
803906

804907
def test_scan_barcode_error_nonexistent(self):
805908
with Transaction() as t:

microsetta_private_api/api/microsetta_private_api.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -2546,6 +2546,25 @@ paths:
25462546
'401':
25472547
$ref: '#/components/responses/401Unauthorized'
25482548

2549+
'/admin/scan/observations/{sample_barcode}':
2550+
get:
2551+
operationId: microsetta_private_api.admin.admin_impl.get_observations
2552+
tags:
2553+
- Admin
2554+
parameters:
2555+
- $ref: '#/components/parameters/sample_barcode'
2556+
summary: Return a list of observations
2557+
description: Return a list of observations
2558+
responses:
2559+
'200':
2560+
description: Array of observations
2561+
content:
2562+
application/json:
2563+
schema:
2564+
type: array
2565+
'401':
2566+
$ref: '#/components/responses/401Unauthorized'
2567+
25492568
'/admin/scan/{sample_barcode}':
25502569
post:
25512570
# Note: We might want to be able to differentiate system administrator operations
@@ -2578,6 +2597,11 @@ paths:
25782597
technician_notes:
25792598
type: string
25802599
example: "Sample Processing Complete!"
2600+
observations:
2601+
type: array
2602+
items:
2603+
type: string
2604+
example: ["Observation 1", "Observation 2"]
25812605
responses:
25822606
'201':
25832607
description: Successfully recorded new barcode scan

microsetta_private_api/api/tests/test_api.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -2328,7 +2328,8 @@ def test_associate_sample_locked(self):
23282328
any_status = 'sample-has-inconsistencies'
23292329
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
23302330
json={'sample_status': any_status,
2331-
'technician_notes': "foobar"},
2331+
'technician_notes': "foobar",
2332+
'observations': []},
23322333
headers=make_headers(FAKE_TOKEN_ADMIN))
23332334
self.assertEqual(201, post_resp.status_code)
23342335

@@ -2383,7 +2384,8 @@ def test_edit_sample_locked(self):
23832384
bad_status = 'sample-has-inconsistencies'
23842385
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
23852386
json={'sample_status': bad_status,
2386-
'technician_notes': "foobar"},
2387+
'technician_notes': "foobar",
2388+
'observations': []},
23872389
headers=make_headers(FAKE_TOKEN_ADMIN))
23882390
self.assertEqual(201, post_resp.status_code)
23892391

@@ -2448,7 +2450,8 @@ def test_edit_sample_locked(self):
24482450
good_status = "sample-is-valid"
24492451
post_resp = self.client.post('/api/admin/scan/%s' % BARCODE,
24502452
json={'sample_status': good_status,
2451-
'technician_notes': "foobar"},
2453+
'technician_notes': "foobar",
2454+
'observations': []},
24522455
headers=make_headers(FAKE_TOKEN_ADMIN))
24532456
self.assertEqual(201, post_resp.status_code)
24542457

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

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

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
-- May 13, 2024
2+
-- Create table to store observation categories
3+
CREATE TABLE barcodes.sample_observation_categories (
4+
category VARCHAR(255) PRIMARY KEY
5+
);
6+
7+
-- Insert predefined observation categories
8+
INSERT INTO barcodes.sample_observation_categories (category)
9+
VALUES ('Sample'), ('Swab'), ('Tube');
10+
11+
-- Create table to store sample observations
12+
CREATE TABLE barcodes.sample_observations (
13+
observation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
14+
category VARCHAR(255) NOT NULL,
15+
observation VARCHAR(255) NOT NULL,
16+
FOREIGN KEY (category) REFERENCES barcodes.sample_observation_categories(category),
17+
UNIQUE (category, observation)
18+
);
19+
20+
-- Create table to store associations between observations and projects
21+
CREATE TABLE barcodes.sample_observation_project_associations (
22+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
23+
observation_id UUID NOT NULL,
24+
project_id INT NOT NULL,
25+
FOREIGN KEY (observation_id) REFERENCES barcodes.sample_observations(observation_id),
26+
FOREIGN KEY (project_id) REFERENCES barcodes.project(project_id),
27+
UNIQUE (observation_id, project_id)
28+
);
29+
30+
-- Insert predefined observations and associate them with a project
31+
WITH inserted_observations AS (
32+
INSERT INTO barcodes.sample_observations (category, observation)
33+
VALUES
34+
('Tube', 'Tube is not intact'),
35+
('Tube', 'Screw cap is loose'),
36+
('Tube', 'Insufficient ethanol'),
37+
('Tube', 'No ethanol'),
38+
('Swab', 'No swab in tube'),
39+
('Swab', 'Multiple swabs in tube'),
40+
('Swab', 'Incorrect swab type'),
41+
('Sample', 'No visible sample'),
42+
('Sample', 'Excess sample on swab')
43+
RETURNING observation_id, category, observation
44+
)
45+
INSERT INTO barcodes.sample_observation_project_associations (observation_id, project_id)
46+
SELECT observation_id, 1
47+
FROM inserted_observations;
48+
49+
-- Create table to store observation ids associated with barcode scans ids
50+
CREATE TABLE barcodes.sample_barcode_scan_observations (
51+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
52+
barcode_scan_id UUID NOT NULL,
53+
observation_id UUID NOT NULL,
54+
FOREIGN KEY (barcode_scan_id) REFERENCES barcodes.barcode_scans(barcode_scan_id),
55+
FOREIGN KEY (observation_id) REFERENCES barcodes.sample_observations(observation_id),
56+
UNIQUE (barcode_scan_id, observation_id)
57+
);

0 commit comments

Comments
 (0)