update to First Strand constructor

This commit is contained in:
Landon Wark
2023-10-12 13:09:12 -05:00
parent 1b6d415788
commit 957edb814a
13 changed files with 236 additions and 56 deletions

View File

@@ -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!")

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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