diff --git a/microsetta_private_api/admin/admin_impl.py b/microsetta_private_api/admin/admin_impl.py index b52975852..171bd2824 100644 --- a/microsetta_private_api/admin/admin_impl.py +++ b/microsetta_private_api/admin/admin_impl.py @@ -56,7 +56,7 @@ def search_kit_id(token_info, kit_id): admin_repo = AdminRepo(t) diag = admin_repo.retrieve_diagnostics_by_kit_id(kit_id) if diag is None: - return jsonify(code=404, message="Kit ID not found"), 404 + return jsonify(code=404, message=f'Kit ID {kit_id} not found'), 404 return jsonify(diag), 200 @@ -263,13 +263,17 @@ def create_kits(body, token_info): number_of_samples = body['number_of_samples'] kit_prefix = body.get('kit_id_prefix', None) project_ids = body['project_ids'] + user_barcodes = body.get('user_barcodes', []) with Transaction() as t: admin_repo = AdminRepo(t) try: - kits = admin_repo.create_kits(number_of_kits, number_of_samples, - kit_prefix, project_ids) + kits = admin_repo.create_kits(number_of_kits, + number_of_samples, + kit_prefix, + user_barcodes, + project_ids) except KeyError: return jsonify(code=422, message="Unable to create kits"), 422 else: @@ -278,6 +282,93 @@ def create_kits(body, token_info): return jsonify(kits), 201 +def handle_barcodes(body, token_info): + validate_admin_access(token_info) + + kit_ids = body['kit_ids'] + + if isinstance(kit_ids, str): + kit_ids = [kit_ids] + + with Transaction() as t: + kit_repo = KitRepo(t) + for kit_id in kit_ids: + diag = kit_repo.get_kit_all_samples(kit_id) + if diag is None: + return jsonify(f'Kit ID {kit_id} not found'), 404 + + action = body.get('action') + + if action == 'create': + if 'generate_barcode_single' in body: + if body['generate_barcode_single']: + number_of_kits = 1 + number_of_samples = 1 + elif 'generate_barcodes' in body: + number_of_kits = (body['num_kits']) + number_of_samples = (body['num_samples']) + else: + number_of_kits = len(body['kit_ids']) + number_of_samples = 1 + + with Transaction() as t: + admin_repo = AdminRepo(t) + + barcode = admin_repo._generate_novel_barcodes( + number_of_kits, number_of_samples, kit_names=kit_ids) + + t.commit() + + return jsonify(barcode[1]), 201 + + elif action == 'insert': + return insert_barcodes(body, token_info) + + else: + return jsonify(code=404, message='Invalid action'), 404 + + +def insert_barcodes(body, token_info): + validate_admin_access(token_info) + + kit_ids = body['kit_ids'] + barcodes = body['barcodes'] + + # Ensure kit_ids is a list, even if it's a single value + if isinstance(kit_ids, str): + kit_ids = [kit_ids] + + with Transaction() as t: + admin_repo = AdminRepo(t) + for barcode in barcodes: + diag = admin_repo.retrieve_diagnostics_by_barcode(barcode) + if diag is not None: + return jsonify(f'Barcode {barcode} already exists'), 404 + + # Check if the lengths match + if len(kit_ids) != len(barcodes): + return jsonify("The number of kit IDs must " + "match the number of barcodes"), 400 + + with Transaction() as t: + admin_repo = AdminRepo(t) + + insertion_data = [] + for kit_id, barcode in zip(kit_ids, barcodes): + diag = admin_repo.retrieve_diagnostics_by_kit_id(kit_id) + sample_info = diag['sample_diagnostic_info'][0] + if sample_info['projects_info']: + for project in sample_info['projects_info']: + project_id = str(project['project_id']) + insertion_data.append((kit_id, barcode, project_id)) + + admin_repo._insert_barcodes_to_existing_kit(insertion_data) + + t.commit() + + return '', 204 + + def get_account_events(account_id, token_info): validate_admin_access(token_info) diff --git a/microsetta_private_api/admin/sample_summary.py b/microsetta_private_api/admin/sample_summary.py index a64f00e2a..44b13194a 100644 --- a/microsetta_private_api/admin/sample_summary.py +++ b/microsetta_private_api/admin/sample_summary.py @@ -83,6 +83,7 @@ def per_sample(project, barcodes, strip_sampleid): ffq_complete, ffq_taken, _ = vs_repo.get_ffq_status_by_sample( sample.id ) + print("ffq complete", ffq_complete, "ffq taken", ffq_taken) summary = { "sampleid": None if strip_sampleid else barcode, diff --git a/microsetta_private_api/admin/tests/test_admin_repo.py b/microsetta_private_api/admin/tests/test_admin_repo.py index 40da36a7a..5c73a6117 100644 --- a/microsetta_private_api/admin/tests/test_admin_repo.py +++ b/microsetta_private_api/admin/tests/test_admin_repo.py @@ -352,7 +352,6 @@ def make_tz_datetime(y, m, d): self.assertGreater(len(diag['projects_info']), 0) self.assertEqual(len(diag['scans_info']), 2) # order matters in the returned vals, so test that - print(diag['scans_info'][0], first_scan) self.assertEqual(diag['scans_info'][0], first_scan) self.assertEqual(diag['scans_info'][1], second_scan) self.assertEqual(diag['latest_scan'], second_scan) @@ -454,7 +453,11 @@ def test_get_project_barcodes_by_id(self): output_id = admin_repo.create_project(input) # create some fake kits - created = admin_repo.create_kits(2, 3, 'foo', [output_id, ]) + created = admin_repo.create_kits(2, + 3, + 'foo', + None, + [output_id, ]) exp = [] for kit in created['created']: @@ -609,14 +612,14 @@ def test_create_kits_fail_nonexistent_project(self): admin_repo.create_kits(5, 3, '', + None, [10000, SurveyTemplateRepo.VIOSCREEN_ID]) def test_create_kits_success_not_microsetta(self): with Transaction() as t: admin_repo = AdminRepo(t) - non_tmi = admin_repo.create_kits(5, 3, '', - [33]) + non_tmi = admin_repo.create_kits(5, 3, '', None, [33]) self.assertEqual(['created', ], list(non_tmi.keys())) self.assertEqual(len(non_tmi['created']), 5) for obj in non_tmi['created']: @@ -643,8 +646,7 @@ def test_create_kits_success_not_microsetta(self): def test_create_kits_success_is_microsetta(self): with Transaction() as t: admin_repo = AdminRepo(t) - tmi = admin_repo.create_kits(4, 2, 'foo', - [1]) + tmi = admin_repo.create_kits(4, 2, 'foo', None, [1]) self.assertEqual(['created', ], list(tmi.keys())) self.assertEqual(len(tmi['created']), 4) for obj in tmi['created']: @@ -878,7 +880,6 @@ def test_scan_with_multiple_observations(self): scans = [scan['observations'] for scan in diag['scans_info']] scans_observation_ids = [obs['observation_id'] for scan in scans for obs in scan] - self.assertEqual(scans_observation_ids, observation_ids) def test_scan_with_wrong_observation(self): @@ -1487,3 +1488,108 @@ def test_update_perk_fulfillment_state(self): ) obs = cur.fetchone() self.assertFalse(obs[0]) + + def test_generate_novel_barcodes_admin_success(self): + number_of_kits = 1 + number_of_samples = 3 + + with Transaction() as t: + admin_repo = AdminRepo(t) + + kit_names = admin_repo._generate_novel_kit_names( + number_of_kits, kit_prefix=None) + + new_barcodes = admin_repo._generate_novel_barcodes( + number_of_kits, number_of_samples, kit_names) + + self.assertEqual(len(new_barcodes[1]), + number_of_kits * number_of_samples) + self.assertTrue(all(barcodes.startswith('X') + for barcodes in new_barcodes[1])) + + def test_generate_novel_barcodes_admin_failure(self): + number_of_kits = 0 + number_of_samples = 3 + + with Transaction() as t: + admin_repo = AdminRepo(t) + + kit_names = admin_repo._generate_novel_kit_names( + number_of_kits, kit_prefix=None) + + new_barcodes = admin_repo._generate_novel_barcodes( + number_of_kits, number_of_samples, kit_names) + + self.assertTrue(new_barcodes[1] == [], []) + + def test_insert_barcodes_admin_success(self): + kit_name = 'test' + barcode = 'X00332312' + project_id = '1' + kit_name_barcode_prj_id_tuple = [(kit_name, barcode, project_id)] + + with Transaction() as t: + admin_repo = AdminRepo(t) + admin_repo._insert_barcodes_to_existing_kit( + kit_name_barcode_prj_id_tuple + ) + + with t.cursor() as cur: + cur.execute( + "SELECT barcode " + "FROM barcodes.barcode " + "WHERE kit_id = %s AND barcode = %s", + (kit_name, barcode) + ) + obs = cur.fetchone() + self.assertIsNotNone(obs, + "Expected barcode not found in database") + + def test_insert_barcodes_admin_fail_nonexisting_kit(self): + # test that inserting barcodes to a non-existent kit fails + kit_name_barcode_prj_id_tuple = [['test1123', 'X00332312', '1']] + + with Transaction() as t: + admin_repo = AdminRepo(t) + with self.assertRaises(psycopg2.errors.ForeignKeyViolation): + admin_repo._insert_barcodes_to_existing_kit( + kit_name_barcode_prj_id_tuple) + + def test_insert_barcodes_admin_fail_dup_barcodes(self): + # test that inserting duplicate barcode fails + kit_name_barcode_prj_id_tuple = [['test', '000000001', '1']] + + with Transaction() as t: + admin_repo = AdminRepo(t) + with self.assertRaises(psycopg2.errors.UniqueViolation): + admin_repo._insert_barcodes_to_existing_kit( + kit_name_barcode_prj_id_tuple) + + def test_user_barcode_create_kit_success(self): + with Transaction() as t: + admin_repo = AdminRepo(t) + admin_repo.create_kits(1, + 1, + '', + [['X99887769']], + [1]) + with t.cursor() as cur: + cur.execute( + "SELECT barcode " + "FROM barcodes.barcode " + "WHERE barcode = %s", + ('X99887769',) + ) + obs = cur.fetchall() + self.assertEqual(obs[0][0], 'X99887769') + + def test_user_barcode_dup_create_kit_fail(self): + with Transaction() as t: + admin_repo = AdminRepo(t) + user_barcode = ['000000001'] + with self.assertRaises(psycopg2.errors.UniqueViolation): + admin_repo.create_kits(1, + 1, + '', + [user_barcode], + [1]) diff --git a/microsetta_private_api/api/microsetta_private_api.yaml b/microsetta_private_api/api/microsetta_private_api.yaml index 88c96e8dc..9bc596870 100644 --- a/microsetta_private_api/api/microsetta_private_api.yaml +++ b/microsetta_private_api/api/microsetta_private_api.yaml @@ -2805,6 +2805,47 @@ paths: '422': $ref: '#/components/responses/422UnprocessableEntity' + '/admin/add_barcodes': + post: + operationId: microsetta_private_api.admin.admin_impl.handle_barcodes + tags: + - Admin + summary: Create or insert barcodes + description: Create barcodes for an existing kit or insert barcodes into the database + requestBody: + content: + application/json: + schema: + type: "object" + properties: + action: + type: string + enum: [create, insert] + description: Specify 'create' to generate new barcodes or 'insert' to insert provided barcodes + barcodes: + type: array + items: + type: string + description: Barcodes to insert (required if action is 'insert') + kit_id: + type: string + description: Kit ID to associate the barcodes with (required if action is 'insert') + required: + - action + responses: + '201': + description: Barcodes were successfully created or inserted + content: + application/json: + schema: + type: array + '204': + description: No data was returned + '404': + description: Kit ID not found or barcode already exists + '500': + description: Duplicate barcodes found (only relevant for insert action) + '/admin/events/accounts/{account_id}': get: operationId: microsetta_private_api.admin.admin_impl.get_account_events diff --git a/microsetta_private_api/repo/admin_repo.py b/microsetta_private_api/repo/admin_repo.py index ab032ea19..b85a18071 100644 --- a/microsetta_private_api/repo/admin_repo.py +++ b/microsetta_private_api/repo/admin_repo.py @@ -787,7 +787,7 @@ def _generate_random_kit_name(self, name_length, prefix): return prefix + '_' + rand_name def create_kits(self, number_of_kits, number_of_samples, kit_prefix, - project_ids): + user_barcodes, project_ids): """Create multiple kits, each with the same number of samples Parameters @@ -798,18 +798,66 @@ def create_kits(self, number_of_kits, number_of_samples, kit_prefix, Number of samples that each kit will contain kit_prefix : str or None A prefix to put on to the kit IDs, this is optional. + user_barcodes : list of lists of str + User provided barcodes to use for the kits. If None, barcodes will + be generated. project_ids : list of int Project ids the samples are to be associated with """ kit_names = self._generate_novel_kit_names(number_of_kits, kit_prefix) - kit_name_and_barcode_tuples_list, new_barcodes = \ - self._generate_novel_barcodes( - number_of_kits, number_of_samples, kit_names) - return self._create_kits(kit_names, new_barcodes, - kit_name_and_barcode_tuples_list, - number_of_samples, project_ids) + if user_barcodes is None: + user_barcodes = [] + + total_required_barcodes = number_of_kits * number_of_samples + total_user_barcodes = sum(len(slot) for slot in user_barcodes) + + total_barcodes_to_generate = \ + total_required_barcodes - total_user_barcodes + generated_barcodes = self._generate_novel_barcodes( + 1, total_barcodes_to_generate, kit_names + )[1] if total_barcodes_to_generate > 0 else [] + + all_barcodes_per_slot = [] + + for i in range(number_of_samples): + slot_barcodes = [] + + # add user-provided barcodes if provided + if i < len(user_barcodes): + slot_barcodes.extend(user_barcodes[i]) + + # add generated barcodes if there slots need to fill + remaining_barcodes_needed = \ + number_of_kits - len(slot_barcodes) + slot_barcodes.extend( + generated_barcodes[:remaining_barcodes_needed]) + generated_barcodes = \ + generated_barcodes[remaining_barcodes_needed:] + + all_barcodes_per_slot.append(slot_barcodes) + + # make the final list of kit-barcode tuples + kit_name_and_barcode_tuples_list = [ + (kit_name, all_barcodes_per_slot[slot_index][kit_index]) + for kit_index, kit_name in enumerate(kit_names) + for slot_index in range(number_of_samples) + ] + + new_barcodes = [ + [all_barcodes_per_slot[slot_index][kit_index] + for slot_index in range(number_of_samples)] + for kit_index in range(number_of_kits) + ] + + return self._create_kits( + kit_names, + new_barcodes, + kit_name_and_barcode_tuples_list, + number_of_samples, + project_ids + ) def _are_any_projects_tmi(self, project_ids): """Return true if any input projects are part of microsetta""" @@ -889,7 +937,65 @@ def _generate_novel_barcodes(self, number_of_kits, number_of_samples, kit_name_and_barcode_tuples_list.append( (name, new_barcodes[offset + i])) - return kit_name_and_barcode_tuples_list, new_barcodes + return kit_name_and_barcode_tuples_list, new_barcodes + + def _insert_barcodes_to_existing_kit(self, kit_barcode_project_tuples): + """Insert barcodes into the database for an existing kit + + Parameters + ---------- + kit_barcode_project_tuples: list of tuple + Each tuple contains (kit_id, barcode, project_id) + """ + + # check for empty input + if not kit_barcode_project_tuples: + raise ValueError("kit_barcode_project_tuples cannot be empty") + + # Extract project IDs + project_ids = [int(t[2]) for t in kit_barcode_project_tuples] + + is_tmi = self._are_any_projects_tmi(project_ids) + + # Create a list to store unique (kit_id, barcode) pairs + unique_barcode_tuples = [] + seen_barcodes = set() + + # Populate the list with unique barcodes + for kit_id, barcode, _ in kit_barcode_project_tuples: + if barcode not in seen_barcodes: + unique_barcode_tuples.append((kit_id, barcode, 'unassigned')) + seen_barcodes.add(barcode) + + with self._transaction.cursor() as cur: + # Insert unique barcodes into the barcode table + if unique_barcode_tuples: + cur.executemany("INSERT INTO barcode (kit_id, barcode, " + "status) VALUES (%s, %s, %s)", + unique_barcode_tuples) + + # Create barcode project associations + barcode_projects = [(barcode, project_id) + for _, barcode, project_id + in kit_barcode_project_tuples] + cur.executemany("INSERT INTO project_barcode " + "(barcode, project_id) " + "VALUES (%s, %s)", barcode_projects) + + if is_tmi: + # Insert into ag_kit_barcodes table for TMI projects + kit_barcodes_insert = [(kit_id, barcode) + for kit_id, barcode, _ + in unique_barcode_tuples] + + cur.executemany("INSERT INTO ag_kit_barcodes " + "(ag_kit_id, barcode) " + "SELECT ag_kit_id, %s " + "FROM ag_kit " + "WHERE supplied_kit_id = %s", + [(barcode, kit_id) + for kit_id, barcode + in kit_barcodes_insert]) def _create_kits(self, kit_names, new_barcodes, kit_name_and_barcode_tuples_list, @@ -921,9 +1027,16 @@ def _create_kits(self, kit_names, new_barcodes, with self._transaction.cursor() as cur: # create barcode project associations barcode_projects = [] - for barcode in new_barcodes: - for prj_id in project_ids: - barcode_projects.append((barcode, prj_id)) + if isinstance(new_barcodes, list) and \ + all(isinstance(item, list) for item in new_barcodes): + for barcodes in new_barcodes: + for barcode in barcodes: + for prj_id in project_ids: + barcode_projects.append((barcode, prj_id)) + else: + for barcode in new_barcodes: + for prj_id in project_ids: + barcode_projects.append((barcode, prj_id)) # create kits in kit table new_kit_uuids = [str(uuid.uuid4()) for x in kit_names] @@ -1020,7 +1133,6 @@ def create_kit(self, kit_name, box_id, address_dict, Project ids that all barcodes in kit are to be associated with """ - kit_names = [kit_name] address = None if address_dict is None else json.dumps(address_dict) kit_details = [{KIT_BOX_ID_KEY: box_id, KIT_ADDRESS_KEY: address, @@ -1028,6 +1140,7 @@ def create_kit(self, kit_name, box_id, address_dict, KIT_INBOUND_KEY: inbound_fedex_code}] kit_name_and_barcode_tuples_list = \ [(kit_name, x) for x in barcodes_list] + kit_names = [kit_name] return self._create_kits(kit_names, barcodes_list, kit_name_and_barcode_tuples_list, diff --git a/microsetta_private_api/repo/tests/test_sample.py b/microsetta_private_api/repo/tests/test_sample.py index e917ed439..44b383d8a 100644 --- a/microsetta_private_api/repo/tests/test_sample.py +++ b/microsetta_private_api/repo/tests/test_sample.py @@ -152,6 +152,7 @@ def test_get_supplied_kit_id_by_sample(self): 1, 1, "UNITTEST", + None, [1] )