diff --git a/CHANGELOG.md b/CHANGELOG.md index 66bac9b..8426150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 202310.02 + +- Improvements to First strand constructor. +- Submission forms can now be dragged and dropped into the form widget. + ## 202310.01 - Controls linker is now depreciated. diff --git a/README.md b/README.md index 988ea4b..fb26a18 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## Startup: -1. Open the app using the shortcut in the Submissions folder. For example: 'L:\Robotics Laboratory Support\Submissions\submissions_v122b.exe - Shortcut.lnk' (Version may have changed). +1. Open the app using the shortcut in the Submissions folder. For example: L:\\Robotics Laboratory Support\\Submissions\\submissions_v122b.exe - Shortcut.lnk (Version may have changed). a. Ignore the large black window of fast scrolling text, it is there for debugging purposes. b. The 'Submissions' tab should be open by default. @@ -60,10 +60,6 @@ This is meant to import .xslx files created from the Design & Analysis Software c. Repeat 6b for the Lot and the Expiry row and columns. 7. Click the "Submit" button at the top. -## Linking Controls: - -1. Click "Monthly" -> "Link Controls". Entire process should be handled automatically. - ## Linking Extraction Logs: 1. Click "Monthly" -> "Link Extraction Logs". diff --git a/TODO.md b/TODO.md index b81a32e..c287f8d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ +- [x] Drag and drop files into submission form area? - [ ] Get info for controls into their sample hitpicks. - [x] Move submission-type specific parser functions into class methods in their respective models. -- [ ] Improve results reporting. +- [ ] Improve function results reporting. - Maybe make it a list until it gets to the reporter? - [x] Increase robustness of form parsers by adding custom procedures for each. - [x] Rerun Kit integrity if extraction kit changed in the form. diff --git a/alembic.ini b/alembic.ini index b0c1125..309811a 100644 --- a/alembic.ini +++ b/alembic.ini @@ -56,8 +56,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # output_encoding = utf-8 ; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db -; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db -sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db +sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db +; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db [post_write_hooks] diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index 807685c..8cf1891 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -4,7 +4,7 @@ from pathlib import Path # Version of the realpython-reader package __project__ = "submissions" -__version__ = "202310.1b" +__version__ = "202310.2b" __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __copyright__ = "2022-2023, Government of Canada" diff --git a/src/submissions/backend/db/functions/lookups.py b/src/submissions/backend/db/functions/lookups.py index d4b7c5a..6ac0e1c 100644 --- a/src/submissions/backend/db/functions/lookups.py +++ b/src/submissions/backend/db/functions/lookups.py @@ -417,7 +417,8 @@ def lookup_reagenttype_kittype_association(ctx:Settings, def lookup_submission_sample_association(ctx:Settings, submission:models.BasicSubmission|str|None=None, sample:models.BasicSample|str|None=None, - limit:int=0 + limit:int=0, + chronologic:bool=False ) -> models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]: query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionSampleAssociation) match submission: @@ -435,6 +436,8 @@ def lookup_submission_sample_association(ctx:Settings, case _: pass logger.debug(f"Query count: {query.count()}") + if chronologic: + query.join(models.BasicSubmission).order_by(models.BasicSubmission.submitted_date) if query.count() == 1: limit = 1 return query_return(query=query, limit=limit) diff --git a/src/submissions/backend/db/functions/misc.py b/src/submissions/backend/db/functions/misc.py index 8448626..1a68a24 100644 --- a/src/submissions/backend/db/functions/misc.py +++ b/src/submissions/backend/db/functions/misc.py @@ -247,6 +247,8 @@ def get_polymorphic_subclass(base:object, polymorphic_identity:str|None=None): Returns: _type_: Subclass, or parent class on """ + if isinstance(polymorphic_identity, dict): + polymorphic_identity = polymorphic_identity['value'] if polymorphic_identity == None: return base else: diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 58a4887..e6db1eb 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1,6 +1,7 @@ ''' Models for the main submission types. ''' +from getpass import getuser import math from . import Base from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT, case @@ -15,7 +16,8 @@ from pandas import Timestamp from dateutil.parser import parse import re import pandas as pd -from tools import row_map +from openpyxl import Workbook +from tools import check_not_nan, row_map logger = logging.getLogger(f"submissions.{__name__}") @@ -238,6 +240,20 @@ class BasicSubmission(Base): continue return output_list + @classmethod + def custom_platemap(cls, xl:pd.ExcelFile, plate_map:pd.DataFrame) -> pd.DataFrame: + """ + Stupid stopgap solution to there being an issue with the Bacterial Culture plate map + + Args: + xl (pd.ExcelFile): original xl workbook + plate_map (pd.DataFrame): original plate map + + Returns: + pd.DataFrame: updated plate map. + """ + return plate_map + @classmethod def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict: """ @@ -265,6 +281,19 @@ class BasicSubmission(Base): """ logger.debug(f"Called {cls.__name__} sample parser") return input_dict + + @classmethod + def custom_autofill(cls, input_excel:Workbook) -> Workbook: + """ + Adds custom autofill methods for submission + + Args: + input_excel (Workbook): input workbook + + Returns: + Workbook: updated workbook + """ + return input_excel # Below are the custom submission types @@ -286,6 +315,49 @@ class BacterialCulture(BasicSubmission): if full_data: output['controls'] = [item.to_sub_dict() for item in self.controls] return output + + @classmethod + def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame: + """ + Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent. + + Args: + xl (pd.ExcelFile): original xl workbook + plate_map (pd.DataFrame): original plate map + + Returns: + pd.DataFrame: updated plate map. + """ + plate_map = super().custom_platemap(xl, plate_map) + num1 = xl.parse("Sample List").iloc[40,1] + num2 = xl.parse("Sample List").iloc[41,1] + logger.debug(f"Broken: {plate_map.iloc[5,0]}, {plate_map.iloc[6,0]}") + logger.debug(f"Replace: {num1}, {num2}") + if not check_not_nan(plate_map.iloc[5,0]): + plate_map.iloc[5,0] = num1 + if not check_not_nan(plate_map.iloc[6,0]): + plate_map.iloc[6,0] = num2 + return plate_map + + @classmethod + def custom_autofill(cls, input_excel: Workbook) -> Workbook: + """ + Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent. + + Args: + input_excel (Workbook): Input openpyxl workbook + + Returns: + Workbook: Updated openpyxl workbook + """ + input_excel = super().custom_autofill(input_excel) + sheet = input_excel['Plate Map'] + if sheet.cell(12,2).value == None: + sheet.cell(row=12, column=2, value="=IF(ISBLANK('Sample List'!$B42),\"\",'Sample List'!$B42)") + if sheet.cell(13,2).value == None: + sheet.cell(row=13, column=2, value="=IF(ISBLANK('Sample List'!$B43),\"\",'Sample List'!$B43)") + input_excel["Sample List"].cell(row=15, column=2, value=getuser()[0:2].upper()) + return input_excel class Wastewater(BasicSubmission): """ @@ -326,8 +398,7 @@ class Wastewater(BasicSubmission): if xl != None: input_dict['csv'] = xl.parse("Copy to import file") return input_dict - - + class WastewaterArtic(BasicSubmission): """ derivative submission type for artic wastewater @@ -371,7 +442,6 @@ class WastewaterArtic(BasicSubmission): input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip() return input_dict - class BasicSample(Base): """ Base of basic sample which polymorphs into BCSample and WWSample @@ -457,7 +527,7 @@ class BasicSample(Base): Sample name: {self.submitter_id}
Well: {row_map[assoc.row]}{assoc.column} """ - return dict(name=self.submitter_id, positive=False, tooltip=tooltip_text) + return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text) class WastewaterSample(BasicSample): """ @@ -538,6 +608,16 @@ class WastewaterSample(BasicSample): except (TypeError, AttributeError) as e: logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.") return sample + + def get_recent_ww_submission(self): + results = [sub for sub in self.submissions if isinstance(sub, Wastewater)] + if len(results) > 1: + results = results.sort(key=lambda sub: sub.submitted_date) + try: + return results[0] + except IndexError: + return None + class BacterialCultureSample(BasicSample): """ diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index 172f571..39cfa4d 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -376,6 +376,8 @@ class SampleParser(object): df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']] df = pd.DataFrame(df.values[1:], columns=df.iloc[0]) df = df.set_index(df.columns[0]) + custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type) + df = custom_mapper.custom_platemap(self.xl, df) return df def construct_lookup_table(self, lookup_table_location:dict) -> pd.DataFrame: diff --git a/src/submissions/frontend/__init__.py b/src/submissions/frontend/__init__.py index 2dfd969..6aa2f0c 100644 --- a/src/submissions/frontend/__init__.py +++ b/src/submissions/frontend/__init__.py @@ -8,17 +8,15 @@ from PyQt6.QtWidgets import ( QMainWindow, QToolBar, QTabWidget, QWidget, QVBoxLayout, QComboBox, QHBoxLayout, - QScrollArea, QLineEdit, QDateEdit, - QSpinBox + QScrollArea, QLineEdit, QDateEdit ) -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QAction from PyQt6.QtWebEngineWidgets import QWebEngineView from pathlib import Path from backend.db import ( construct_reagent, store_object, lookup_control_types, lookup_modes ) -# from .all_window_functions import extract_form_info from tools import check_if_app, Settings from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent import logging @@ -55,7 +53,6 @@ class App(QMainWindow): self._createToolBar() self._connectActions() self._controls_getter() - # self.status_bar = self.statusBar() self.show() self.statusBar().showMessage('Ready', 5000) @@ -68,7 +65,6 @@ class App(QMainWindow): menuBar = self.menuBar() fileMenu = menuBar.addMenu("&File") # Creating menus using a title - # editMenu = menuBar.addMenu("&Edit") methodsMenu = menuBar.addMenu("&Methods") reportMenu = menuBar.addMenu("&Reports") maintenanceMenu = menuBar.addMenu("&Monthly") @@ -79,7 +75,6 @@ class App(QMainWindow): fileMenu.addAction(self.importPCRAction) methodsMenu.addAction(self.constructFS) reportMenu.addAction(self.generateReportAction) - # maintenanceMenu.addAction(self.joinControlsAction) maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinPCRAction) @@ -105,7 +100,6 @@ class App(QMainWindow): self.generateReportAction = QAction("Make Report", self) self.addKitAction = QAction("Import Kit", self) self.addOrgAction = QAction("Import Org", self) - # self.joinControlsAction = QAction("Link Controls") self.joinExtractionAction = QAction("Link Extraction Logs") self.joinPCRAction = QAction("Link PCR Logs") self.helpAction = QAction("&About", self) @@ -128,12 +122,12 @@ class App(QMainWindow): self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter) self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter) self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter) - # self.joinControlsAction.triggered.connect(self.linkControls) self.joinExtractionAction.triggered.connect(self.linkExtractions) self.joinPCRAction.triggered.connect(self.linkPCR) self.helpAction.triggered.connect(self.showAbout) self.docsAction.triggered.connect(self.openDocs) self.constructFS.triggered.connect(self.construct_first_strand) + self.table_widget.formwidget.import_drag.connect(self.importSubmission) def showAbout(self): """ @@ -168,12 +162,14 @@ class App(QMainWindow): else: self.statusBar().showMessage("Action completed sucessfully.", 5000) - def importSubmission(self): + def importSubmission(self, fname:Path|None=None): """ import submission from excel sheet into form """ from .main_window_functions import import_submission_function - self, result = import_submission_function(self) + self.raise_() + self.activateWindow() + self, result = import_submission_function(self, fname) logger.debug(f"Import result: {result}") self.result_reporter(result) @@ -395,12 +391,25 @@ class AddSubForm(QWidget): class SubmissionFormWidget(QWidget): + import_drag = pyqtSignal(Path) + def __init__(self, parent: QWidget) -> None: logger.debug(f"Setting form widget...") super().__init__(parent) self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer", "qt_scrollarea_vcontainer", "submit_btn" ] + self.setAcceptDrops(True) + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0]) + self.import_drag.emit(fname) def parse_form(self) -> Tuple[dict, list]: logger.debug(f"Hello from parser!") diff --git a/src/submissions/frontend/custom_widgets/misc.py b/src/submissions/frontend/custom_widgets/misc.py index 5b41139..5a8adba 100644 --- a/src/submissions/frontend/custom_widgets/misc.py +++ b/src/submissions/frontend/custom_widgets/misc.py @@ -3,17 +3,19 @@ Contains miscellaneous widgets for frontend functions ''' from datetime import date from pprint import pformat +from PyQt6 import QtCore from PyQt6.QtWidgets import ( QLabel, QVBoxLayout, QLineEdit, QComboBox, QDialog, QDialogButtonBox, QDateEdit, QSizePolicy, QWidget, QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox, - QHBoxLayout, QScrollArea + QHBoxLayout, QScrollArea, QFormLayout ) from PyQt6.QtCore import Qt, QDate, QSize from tools import check_not_nan, jinja_template_loading, Settings from backend.db.functions import construct_kit_from_yaml, \ - lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association + lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association, \ + lookup_submissions from backend.db.models import SubmissionTypeKitTypeAssociation from sqlalchemy import FLOAT, INTEGER, String import logging @@ -277,7 +279,6 @@ class KitAdder(QWidget): info[widget.objectName()] = widget.date().toPyDate() return info, reagents - class ReagentTypeForm(QWidget): """ custom widget to add information about a new reagenttype @@ -458,3 +459,67 @@ class ParsedQLabel(QLabel): else: self.setText(f"MISSING {output}") +class FirstStrandSalvage(QDialog): + + def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None: + super().__init__() + if rsl_plate_num == None: + rsl_plate_num = "" + self.setWindowTitle("Add Reagent") + + QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + self.submitter_id_input = QLineEdit() + self.submitter_id_input.setText(submitter_id) + self.rsl_plate_num = QLineEdit() + self.rsl_plate_num.setText(rsl_plate_num) + self.row_letter = QComboBox() + self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']) + self.row_letter.setEditable(False) + self.column_number = QComboBox() + self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']) + self.column_number.setEditable(False) + self.layout = QFormLayout() + self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input) + self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num) + self.layout.addRow(self.tr("&Source Row:"), self.row_letter) + self.layout.addRow(self.tr("&Source Column:"), self.column_number) + self.layout.addWidget(self.buttonBox) + self.setLayout(self.layout) + + def parse_form(self): + return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}") + +class FirstStrandPlateList(QDialog): + + def __init__(self, ctx:Settings) -> None: + super().__init__() + self.setWindowTitle("First Strand Plates") + + QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + ww = [item.rsl_plate_num for item in lookup_submissions(ctx=ctx, submission_type="Wastewater")] + self.plate1 = QComboBox() + self.plate2 = QComboBox() + self.plate3 = QComboBox() + self.layout = QFormLayout() + for ii, plate in enumerate([self.plate1, self.plate2, self.plate3]): + plate.addItems(ww) + self.layout.addRow(self.tr(f"&Plate {ii+1}:"), plate) + self.layout.addWidget(self.buttonBox) + self.setLayout(self.layout) + + def parse_form(self): + output = [] + for plate in [self.plate1, self.plate2, self.plate3]: + output.append(plate.currentText()) + return output + + + diff --git a/src/submissions/frontend/custom_widgets/sub_details.py b/src/submissions/frontend/custom_widgets/sub_details.py index afaa3af..1ff59e1 100644 --- a/src/submissions/frontend/custom_widgets/sub_details.py +++ b/src/submissions/frontend/custom_widgets/sub_details.py @@ -291,8 +291,6 @@ class SubmissionDetails(QDialog): btn.setParent(self) btn.setFixedWidth(900) btn.clicked.connect(self.export) - with open("test.html", "w") as f: - f.write(self.html) def export(self): """ diff --git a/src/submissions/frontend/main_window_functions.py b/src/submissions/frontend/main_window_functions.py index c6caaf1..4fefc1f 100644 --- a/src/submissions/frontend/main_window_functions.py +++ b/src/submissions/frontend/main_window_functions.py @@ -22,10 +22,12 @@ from PyQt6.QtWidgets import ( ) from .all_window_functions import select_open_file, select_save_file from PyQt6.QtCore import QSignalBlocker +from backend.db.models import BasicSubmission from backend.db.functions import ( construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes, update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types, - lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type + lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type, + get_polymorphic_subclass ) from backend.excel.parser import SheetParser, PCRParser, SampleParser from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df @@ -34,10 +36,12 @@ from .custom_widgets.pop_ups import AlertPop, QuestionAsker from .custom_widgets import ReportDatePicker from .custom_widgets.misc import ImportReagent, ParsedQLabel from .visualizations.control_charts import create_charts, construct_html +from pathlib import Path +from frontend.custom_widgets.misc import FirstStrandSalvage, FirstStrandPlateList logger = logging.getLogger(f"submissions.{__name__}") -def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]: +def import_submission_function(obj:QMainWindow, fname:Path|None=None) -> Tuple[QMainWindow, dict|None]: """ Import a new submission to the app window @@ -56,7 +60,8 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None] obj.missing_info = [] # set file dialog - fname = select_open_file(obj, file_extension="xlsx") + if isinstance(fname, bool) or fname == None: + fname = select_open_file(obj, file_extension="xlsx") logger.debug(f"Attempting to parse file: {fname}") if not fname.exists(): result = dict(message=f"File {fname.__str__()} not found.", status="critical") @@ -113,7 +118,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None] add_widget = QComboBox() # lookup existing kits by 'submission_type' decided on by sheetparser logger.debug(f"Looking up kits used for {pyd.submission_type['value']}") - # uses = [item.__str__() for item in lookup_kittype_by_use(ctx=obj.ctx, used_for=pyd.submission_type['value'])] uses = [item.__str__() for item in lookup_kit_types(ctx=obj.ctx, used_for=pyd.submission_type['value'])] logger.debug(f"Kits received for {pyd.submission_type['value']}: {uses}") if check_not_nan(value['value']): @@ -125,7 +129,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None] obj.ext_kit = uses[0] # Run reagent scraper whenever extraction kit is changed. add_widget.currentTextChanged.connect(obj.scrape_reagents) - # add_widget.addItems(uses) case 'submitted_date': # uses base calendar add_widget = QDateEdit(calendarPopup=True) @@ -221,7 +224,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic # get current kit being used obj.ext_kit = kit_widget.currentText() for item in obj.reagents: - obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False)) + obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False, label_name=f"lot_{item.type}")) reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name) add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit) obj.table_widget.formlayout.addWidget(add_widget) @@ -231,7 +234,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning") for item in obj.missing_reagents: # Add label that has parsed as False to show "MISSING" label. - obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False)) + obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False, label_name=f"missing_{item.type}")) # Set default parameters for the empty reagent. reagent = dict(type=item.type, lot=None, exp=date.today(), name=None) # create and add widget @@ -896,8 +899,8 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re logger.debug(f"Attempting: {item['type']}") worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value']) # Hacky way to pop in 'signed by' - if info['submission_type'] == "Bacterial Culture": - workbook["Sample List"].cell(row=14, column=2, value=getuser()[0:2].upper()) + custom_parser = get_polymorphic_subclass(BasicSubmission, info['submission_type']) + workbook = custom_parser.custom_autofill(workbook) fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx") workbook.save(filename=fname.__str__()) @@ -934,46 +937,63 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict] logger.debug(f"Samples: {pformat(samples)}") logger.debug("Called first strand sample parser") plates = sprsr.grab_plates() + # Fix no plates found in form. + if plates == []: + dlg = FirstStrandPlateList(ctx=obj.ctx) + if dlg.exec(): + plates = dlg.parse_form() + plates = list(set(plates)) logger.debug(f"Plates: {pformat(plates)}") output_samples = [] logger.debug(f"Samples: {pformat(samples)}") old_plate_number = 1 + old_plate = '' for item in samples: try: item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0] except AttributeError: - item['well'] = item + pass item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip() new_dict = {} new_dict['sample'] = item['submitter_id'] - if item['submitter_id'] == "NTC1": - new_dict['destination_row'] = 8 - new_dict['destination_column'] = 2 - new_dict['plate_number'] = 'control' - elif item['submitter_id'] == "NTC2": - new_dict['destination_row'] = 8 - new_dict['destination_column'] = 5 - new_dict['plate_number'] = 'control' - else: - new_dict['destination_row'] = item['row'] - new_dict['destination_column'] = item['column'] plate_num, plate = get_plates(input_sample_number=new_dict['sample'], plates=plates) if plate_num == None: plate_num = str(old_plate_number) + "*" else: old_plate_number = plate_num logger.debug(f"Got plate number: {plate_num}, plate: {plate}") + if item['submitter_id'] == "NTC1": + new_dict['destination_row'] = 8 + new_dict['destination_column'] = 2 + new_dict['plate_number'] = 'control' + new_dict['plate'] = None + output_samples.append(new_dict) + continue + elif item['submitter_id'] == "NTC2": + new_dict['destination_row'] = 8 + new_dict['destination_column'] = 5 + new_dict['plate_number'] = 'control' + new_dict['plate'] = None + output_samples.append(new_dict) + continue + else: + new_dict['destination_row'] = item['row'] + new_dict['destination_column'] = item['column'] + new_dict['plate_number'] = plate_num + # Fix plate association not found if plate == None: + dlg = FirstStrandSalvage(ctx=obj.ctx, submitter_id=item['submitter_id'], rsl_plate_num=old_plate) + if dlg.exec(): + item.update(dlg.parse_form()) try: new_dict['source_row'], new_dict['source_column'] = convert_well_to_row_column(item['well']) - new_dict['plate_number'] = plate_num except KeyError: pass else: - new_dict['plate_number'] = plate_num new_dict['plate'] = plate.submission.rsl_plate_num new_dict['source_row'] = plate.row new_dict['source_column'] = plate.column + old_plate = plate.submission.rsl_plate_num output_samples.append(new_dict) df = pd.DataFrame.from_records(output_samples) df.sort_values(by=['destination_column', 'destination_row'], ascending=True, inplace=True) @@ -1001,6 +1021,7 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d obj.missing_reagents = [] # Remove previous reagent widgets [item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")] + [item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QPushButton)] reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit) logger.debug(f"Got reagents: {reagents}") for reagent in obj.prsr.sub['reagents']: @@ -1013,5 +1034,3 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d logger.debug(f"Missing reagents: {obj.missing_reagents}") return obj, None - -