Addition of Equipment and SubmissionType creation.

This commit is contained in:
Landon Wark
2023-12-20 12:54:37 -06:00
parent 0dd51827a0
commit 0d64095e42
22 changed files with 755 additions and 123 deletions

View File

@@ -18,6 +18,8 @@ from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer
from .kit_creator import KitAdder
from .submission_type_creator import SubbmissionTypeAdder
logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger")
@@ -207,11 +209,13 @@ class AddSubForm(QWidget):
self.tab1 = QWidget()
self.tab2 = QWidget()
self.tab3 = QWidget()
self.tab4 = QWidget()
self.tabs.resize(300,200)
# Add tabs
self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Add Kit")
self.tabs.addTab(self.tab3, "Add SubmissionType")
self.tabs.addTab(self.tab4, "Add Kit")
# Create submission adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
@@ -238,10 +242,14 @@ class AddSubForm(QWidget):
self.tab2.layout.addWidget(self.controls_viewer)
self.tab2.setLayout(self.tab2.layout)
# create custom widget to add new tabs
adder = KitAdder(self)
ST_adder = SubbmissionTypeAdder(self)
self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(adder)
self.tab3.layout.addWidget(ST_adder)
self.tab3.setLayout(self.tab3.layout)
kit_adder = KitAdder(self)
self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(kit_adder)
self.tab4.setLayout(self.tab4.layout)
# add tabs to main widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)

View File

@@ -0,0 +1,89 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox)
from backend.db.models import SubmissionType
from backend.validators.pydant import PydEquipment, PydEquipmentPool
class EquipmentUsage(QDialog):
def __init__(self, parent, submission_type:SubmissionType|str) -> QDialog:
super().__init__(parent)
self.setWindowTitle("Equipment Checklist")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
# self.static_equipment = submission_type.get_equipment()
self.opt_equipment = submission_type.get_equipment()
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.populate_form()
def populate_form(self):
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
for eq in self.opt_equipment:
self.layout.addWidget(eq.toForm(parent=self))
self.layout.addWidget(self.buttonBox)
def parse_form(self):
output = []
for widget in self.findChildren(QWidget):
match widget:
case (EquipmentCheckBox()|PoolComboBox()) :
output.append(widget.parse_form())
case _:
pass
return [item for item in output if item != None]
class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
self.label = QLabel()
self.label.setMaximumWidth(125)
self.label.setMinimumWidth(125)
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
# self.check.setEnabled(False)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
text = equipment.name
self.setObjectName(equipment.name)
self.label.setText(text)
self.layout.addWidget(self.label)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str|None:
if self.check.isChecked():
return self.objectName()
else:
return None
class PoolComboBox(QWidget):
def __init__(self, parent, pool:PydEquipmentPool) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
# label = QLabel()
# label.setText(pool.name)
self.box = QComboBox()
self.box.setMaximumWidth(125)
self.box.setMinimumWidth(125)
self.box.addItems([item.name for item in pool.equipment])
self.check = QCheckBox()
# self.layout.addWidget(label)
self.layout.addWidget(self.box)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str:
if self.check.isChecked():
return self.box.currentText()
else:
return None

View File

@@ -82,7 +82,7 @@ class KitAdder(QWidget):
print(self.app)
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm()
reg_form = ReagentTypeForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
self.grid.addWidget(reg_form, maxrow,0,1,4)
@@ -139,8 +139,8 @@ class ReagentTypeForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self) -> None:
super().__init__()
def __init__(self, parent) -> None:
super().__init__(parent)
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Reagent Type Name"),0,0)

View File

@@ -53,7 +53,7 @@ class KitSelector(QDialog):
super().__init__()
self.setWindowTitle(title)
self.widget = QComboBox()
kits = [item.__str__() for item in KitType.query()]
kits = [item.name for item in KitType.query()]
self.widget.addItems(kits)
self.widget.setEditable(False)
# set yes/no buttons

View File

@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.models import BasicSubmission
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation
from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from xhtml2pdf import pisa
from .pop_ups import QuestionAsker
from .equipment_usage import EquipmentUsage
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker
@@ -159,22 +160,44 @@ class SubmissionsSheet(QTableView):
# barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self)
backupAction = QAction("Backup", self)
equipAction = QAction("Add Equipment", self)
# hitpickAction = QAction("Hitpicks", self)
renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details())
# barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment())
backupAction.triggered.connect(lambda: self.regenerate_submission_form())
equipAction.triggered.connect(lambda: self.add_equipment())
# hitpickAction.triggered.connect(lambda: self.hit_pick())
self.menu.addAction(detailsAction)
self.menu.addAction(renameAction)
# self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction)
self.menu.addAction(backupAction)
self.menu.addAction(equipAction)
# self.menu.addAction(hitpickAction)
# add other required actions
self.menu.popup(QCursor.pos())
def add_equipment(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, rsl_plate_id):
submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type)
if dlg.exec():
equipment = dlg.parse_form()
for equip in equipment:
e = Equipment.query(name=equip)
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
def delete_item(self, event):
"""
Confirms user deletion and sends id to backend for deletion.
@@ -193,65 +216,6 @@ class SubmissionsSheet(QTableView):
return
self.setData()
# def hit_pick(self):
# """
# Extract positive samples from submissions with PCR results and export to csv.
# NOTE: For this to work for arbitrary samples, positive samples must have 'positive' in their name
# """
# # Get all selected rows
# indices = self.selectionModel().selectedIndexes()
# # convert to id numbers
# indices = [index.sibling(index.row(), 0).data() for index in indices]
# # biomek can handle 4 plates maximum
# if len(indices) > 4:
# logger.error(f"Error: Had to truncate number of plates to 4.")
# indices = indices[:4]
# # lookup ids in the database
# # subs = [lookup_submissions(ctx=self.ctx, id=id) for id in indices]
# subs = [BasicSubmission.query(id=id) for id in indices]
# # full list of samples
# dicto = []
# # list to contain plate images
# images = []
# for iii, sub in enumerate(subs):
# # second check to make sure there aren't too many plates
# if iii > 3:
# logger.error(f"Error: Had to truncate number of plates to 4.")
# continue
# plate_dicto = sub.hitpick_plate(plate_number=iii+1)
# if plate_dicto == None:
# continue
# image = make_plate_map(plate_dicto)
# images.append(image)
# for item in plate_dicto:
# if len(dicto) < 94:
# dicto.append(item)
# else:
# logger.error(f"We had to truncate the number of samples to 94.")
# logger.debug(f"We found {len(dicto)} to hitpick")
# # convert all samples to dataframe
# df = make_hitpicks(dicto)
# df = df[df.positive != False]
# logger.debug(f"Size of the dataframe: {df.shape[0]}")
# msg = AlertPop(message=f"We found {df.shape[0]} samples to hitpick", status="INFORMATION")
# msg.exec()
# if df.size == 0:
# return
# date = datetime.strftime(datetime.today(), "%Y-%m-%d")
# # ask for filename and save as csv.
# home_dir = Path(self.ctx.directory_path).joinpath(f"Hitpicks_{date}.csv").resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
# if fname.__str__() == ".":
# logger.debug("Saving csv was cancelled.")
# return
# df.to_csv(fname.__str__(), index=False)
# # show plate maps
# for image in images:
# try:
# image.show()
# except Exception as e:
# logger.error(f"Could not show image: {e}.")
def link_extractions(self):
self.link_extractions_function()
self.app.report.add_result(self.report)

View File

@@ -0,0 +1,118 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
)
from sqlalchemy import FLOAT, INTEGER
from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentAssociation, BasicSubmission
from backend.validators import PydReagentType, PydKit
import logging
from pprint import pformat
from tools import Report
from typing import Tuple
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}")
class SubbmissionTypeAdder(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.report = Report()
self.app = parent.parent()
self.template_path = ""
main_box = QVBoxLayout(self)
scroll = QScrollArea(self)
main_box.addWidget(scroll)
scroll.setWidgetResizable(True)
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
scrollContent.setLayout(self.grid)
# insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Submission Type Name:"),2,0)
# widget to get kit name
self.st_name = QLineEdit()
self.st_name.setObjectName("submission_type_name")
self.grid.addWidget(self.st_name,2,1,1,2)
self.grid.addWidget(QLabel("Template File"),3,0)
template_selector = QPushButton("Select")
self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
for iii, key in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
# field_name = key
# self.grid.addWidget(QLabel(field_name),idx,0)
# print(self.columns[key].type)
# match self.columns[key].type:
# case FLOAT():
# add_widget = QDoubleSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case INTEGER():
# add_widget = QSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case _:
# add_widget = QLineEdit()
# add_widget.setObjectName(key)
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path)
def submit(self):
info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info)
with open(self.template_path, "rb") as f:
ST.template_file = f.read()
logger.debug(ST.__dict__)
def parse_form(self):
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return [{widget.objectName():widget.parse_form()} for widget in widgets]
def get_template_path(self):
self.template_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__())
class InfoWidget(QWidget):
def __init__(self, parent: QWidget, key) -> None:
super().__init__(parent)
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel(key.replace("_", " ").title()),0,0,1,4)
self.setObjectName(key)
grid.addWidget(QLabel("Sheet Names (comma seperated):"),1,0)
self.sheet = QLineEdit()
self.sheet.setObjectName("sheets")
grid.addWidget(self.sheet, 1,1,1,3)
grid.addWidget(QLabel("Row:"),2,0,alignment=Qt.AlignmentFlag.AlignRight)
self.row = QSpinBox()
self.row.setObjectName("row")
grid.addWidget(self.row,2,1)
grid.addWidget(QLabel("Column:"),2,2,alignment=Qt.AlignmentFlag.AlignRight)
self.column = QSpinBox()
self.column.setObjectName("column")
grid.addWidget(self.column,2,3)
def parse_form(self):
return dict(
sheets = self.sheet.text().split(","),
row = self.row.value(),
column = self.column.value()
)

View File

@@ -62,12 +62,13 @@ class SubmissionFormContainer(QWidget):
self.app.result_reporter()
def scrape_reagents(self, *args, **kwargs):
print(f"\n\n{inspect.stack()[1].function}\n\n")
self.scrape_reagents_function(args[0])
caller = inspect.stack()[1].function.__repr__().replace("'", "")
logger.debug(f"Args: {args}, kwargs: {kwargs}")
self.scrape_reagents_function(args[0], caller=caller)
self.kit_integrity_completion()
self.app.report.add_result(self.report)
self.report = Report()
match inspect.stack()[1].function:
match inspect.stack()[1].function.__repr__():
case "import_submission_function":
pass
case _:
@@ -83,7 +84,7 @@ class SubmissionFormContainer(QWidget):
self.kit_integrity_completion_function()
self.app.report.add_result(self.report)
self.report = Report()
match inspect.stack()[1].function:
match inspect.stack()[1].function.__repr__():
case "import_submission_function":
pass
case _:
@@ -161,7 +162,7 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Outgoing report: {self.report.results}")
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
def scrape_reagents_function(self, extraction_kit:str):
def scrape_reagents_function(self, extraction_kit:str, caller:str|None=None):
"""
Extracted scrape reagents function that will run when
form 'extraction_kit' widget is updated.
@@ -173,6 +174,9 @@ class SubmissionFormContainer(QWidget):
Returns:
Tuple[QMainWindow, dict]: Updated application and result
"""
self.form.reagents = []
logger.debug(f"\n\n{caller}\n\n")
# assert caller == "import_submission_function"
report = Report()
logger.debug(f"Extraction kit: {extraction_kit}")
# obj.reagents = []
@@ -195,7 +199,15 @@ class SubmissionFormContainer(QWidget):
# obj.reagents.append(reagent)
# else:
# obj.missing_reagents.append(reagent)
self.form.reagents = self.prsr.sub['reagents']
match caller:
case "import_submission_function":
self.form.reagents = self.prsr.sub['reagents']
case _:
already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing]
names = list(set([item.type for item in already_have]))
logger.debug(f"reagents: {already_have}")
reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names]
self.form.reagents = already_have + reagents
# logger.debug(f"Imported reagents: {obj.reagents}")
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
self.report.add_result(report)
@@ -221,6 +233,7 @@ class SubmissionFormContainer(QWidget):
self.ext_kit = kit_widget.currentText()
# for reagent in obj.pyd.reagents:
for reagent in self.form.reagents:
logger.debug(f"Creating widget for {reagent}")
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
# add_widget.setParent(sub_form_container.form)
self.form.layout().addWidget(add_widget)