Large scale frontend refactor

This commit is contained in:
Landon Wark
2023-11-14 11:16:54 -06:00
parent bf4149b1b3
commit da714c355d
26 changed files with 2682 additions and 2447 deletions

View File

@@ -0,0 +1,13 @@
'''
Contains all custom generated PyQT6 derivative widgets.
'''
# from .app import App
from .functions import *
from .misc import *
from .pop_ups import *
from .submission_table import *
from .submission_widget import *
from .controls_chart import *
from .kit_creator import *
from .app import App

View File

@@ -0,0 +1,412 @@
'''
Constructs main application.
TODO: Complete.
'''
import sys
from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow,
QToolBar
)
from PyQt6.QtGui import QAction
from pathlib import Path
from backend.validators import PydReagent
# from frontend.functions import (
# add_kit_function, add_org_function, link_controls_function, export_csv_function
# )
from tools import check_if_app, Settings, Report
from .pop_ups import AlertPop
from .misc import AddReagentForm
import logging
from datetime import date
import webbrowser
from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer
from .kit_creator import KitAdder
import webbrowser
logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger")
class App(QMainWindow):
def __init__(self, ctx: Settings = None):
logger.debug(f"Initializing main window...")
super().__init__()
self.ctx = ctx
self.last_dir = ctx.directory_path
self.report = Report()
# indicate version and connected database in title bar
try:
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}"
except (AttributeError, KeyError):
self.title = f"Submissions App"
# set initial app position and size
self.left = 0
self.top = 0
self.width = 1300
self.height = 1000
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
# insert tabs into main app
self.table_widget = AddSubForm(self)
self.setCentralWidget(self.table_widget)
# run initial setups
self._createActions()
self._createMenuBar()
self._createToolBar()
self._connectActions()
# self._controls_getter()
self.show()
self.statusBar().showMessage('Ready', 5000)
def _createMenuBar(self):
"""
adds items to menu bar
"""
logger.debug(f"Creating menu bar...")
menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File")
# Creating menus using a title
# methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction)
helpMenu.addAction(self.docsAction)
fileMenu.addAction(self.importAction)
fileMenu.addAction(self.importPCRAction)
# methodsMenu.addAction(self.constructFS)
reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction)
def _createToolBar(self):
"""
adds items to toolbar
"""
logger.debug(f"Creating toolbar...")
toolbar = QToolBar("My main toolbar")
self.addToolBar(toolbar)
toolbar.addAction(self.addReagentAction)
toolbar.addAction(self.addKitAction)
toolbar.addAction(self.addOrgAction)
def _createActions(self):
"""
creates actions
"""
logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self)
self.importPCRAction = QAction("&Import PCR Results", self)
self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self)
self.joinExtractionAction = QAction("Link Extraction Logs")
self.joinPCRAction = QAction("Link PCR Logs")
self.helpAction = QAction("&About", self)
self.docsAction = QAction("&Docs", self)
# self.constructFS = QAction("Make First Strand", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
logger.debug(f"Connecting actions...")
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
# self.addKitAction.triggered.connect(self.add_kit)
# self.addOrgAction.triggered.connect(self.add_org)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
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):
"""
Show the 'about' message
"""
output = f"Version: {self.ctx.package.__version__}\n\nAuthor: {self.ctx.package.__author__['name']} - {self.ctx.package.__author__['email']}\n\nCopyright: {self.ctx.package.__copyright__}"
about = AlertPop(message=output, status="information")
about.exec()
def openDocs(self):
"""
Open the documentation html pages
"""
if check_if_app():
url = Path(sys._MEIPASS).joinpath("files", "docs", "index.html")
else:
url = Path("docs\\build\\index.html").absolute()
logger.debug(f"Attempting to open {url}")
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
def result_reporter(self):
# def result_reporter(self, result:TypedDict[]|None=None):
"""
Report any anomolous results - if any - to the user
Args:
result (dict | None, optional): The result from a function. Defaults to None.
"""
# logger.info(f"We got the result: {result}")
# if result != None:
# msg = AlertPop(message=result['message'], status=result['status'])
# msg.exec()
logger.debug(f"Running results reporter for: {self.report.results}")
if len(self.report.results) > 0:
logger.debug(f"We've got some results!")
for result in self.report.results:
logger.debug(f"Showing result: {result}")
if result != None:
alert = result.report()
if alert.exec():
pass
self.report = Report()
else:
self.statusBar().showMessage("Action completed sucessfully.", 5000)
# def importSubmission(self, fname:Path|None=None):
# """
# import submission from excel sheet into form
# """
# # from .main_window_functions import import_submission_function
# self.raise_()
# self.activateWindow()
# self = import_submission_function(self, fname)
# logger.debug(f"Result from result reporter: {self.report.results}")
# self.result_reporter()
# def kit_reload(self):
# """
# Removes all reagents from form before running kit integrity completion.
# """
# # from .main_window_functions import kit_reload_function
# self = kit_reload_function(self)
# self.result_reporter()
# def kit_integrity_completion(self):
# """
# Performs check of imported reagents
# NOTE: this will not change self.reagents which should be fine
# since it's only used when looking up
# """
# # from .main_window_functions import kit_integrity_completion_function
# self = kit_integrity_completion_function(self)
# self.result_reporter()
# def submit_new_sample(self):
# """
# Attempt to add sample to database when 'submit' button clicked
# """
# # from .main_window_functions import submit_new_sample_function
# self = submit_new_sample_function(self)
# self.result_reporter()
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, name:str|None=None):
"""
Action to create new reagent in DB.
Args:
reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None.
reagent_type (str | None, optional): Parsed reagent type from import form. Defaults to None.
Returns:
models.Reagent: the constructed reagent object to add to submission
"""
report = Report()
if isinstance(reagent_lot, bool):
reagent_lot = ""
# create form
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
if dlg.exec():
# extract form info
# info = extract_form_info(dlg)
info = dlg.parse_form()
logger.debug(f"Reagent info: {info}")
# create reagent object
# reagent = construct_reagent(ctx=self.ctx, info_dict=info)
reagent = PydReagent(ctx=self.ctx, **info)
# send reagent to db
# store_reagent(ctx=self.ctx, reagent=reagent)
sqlobj, result = reagent.toSQL()
sqlobj.save()
# result = store_object(ctx=self.ctx, object=reagent.toSQL()[0])
report.add_result(result)
self.result_reporter()
return reagent
# def generateReport(self):
# """
# Action to create a summary of sheet data per client
# """
# # from .main_window_functions import generate_report_function
# self, result = generate_report_function(self)
# self.result_reporter(result)
# def add_kit(self):
# """
# Constructs new kit from yaml and adds to DB.
# """
# # from .main_window_functions import add_kit_function
# self, result = add_kit_function(self)
# self.result_reporter(result)
# def add_org(self):
# """
# Constructs new kit from yaml and adds to DB.
# """
# # from .main_window_functions import add_org_function
# self, result = add_org_function(self)
# self.result_reporter(result)
# def _controls_getter(self):
# """
# Lookup controls from database and send to chartmaker
# """
# # from .main_window_functions import controls_getter_function
# self = controls_getter_function(self)
# self.result_reporter()
# def _chart_maker(self):
# """
# Creates plotly charts for webview
# """
# # from .main_window_functions import chart_maker_function
# self = chart_maker_function(self)
# self.result_reporter()
# def linkControls(self):
# """
# Adds controls pulled from irida to relevant submissions
# NOTE: Depreciated due to improvements in controls scraper.
# """
# # from .main_window_functions import link_controls_function
# self, result = link_controls_function(self)
# self.result_reporter(result)
# def linkExtractions(self):
# """
# Links extraction logs from .csv files to relevant submissions.
# """
# # from .main_window_functions import link_extractions_function
# self, result = link_extractions_function(self)
# self.result_reporter(result)
# def linkPCR(self):
# """
# Links PCR logs from .csv files to relevant submissions.
# """
# # from .main_window_functions import link_pcr_function
# self, result = link_pcr_function(self)
# self.result_reporter(result)
# def importPCRResults(self):
# """
# Imports results exported from Design and Analysis .eds files
# """
# # from .main_window_functions import import_pcr_results_function
# self, result = import_pcr_results_function(self)
# self.result_reporter(result)
# def construct_first_strand(self):
# """
# Converts first strand excel sheet to Biomek CSV
# """
# from .main_window_functions import construct_first_strand_function
# self, result = construct_first_strand_function(self)
# self.result_reporter(result)
# def scrape_reagents(self, *args, **kwargs):
# # from .main_window_functions import scrape_reagents
# logger.debug(f"Args: {args}")
# logger.debug(F"kwargs: {kwargs}")
# self = scrape_reagents(self, args[0])
# self.kit_integrity_completion()
# self.result_reporter()
# def export_csv(self, fname:Path|None=None):
# # from .main_window_functions import export_csv_function
# export_csv_function(self, fname)
class AddSubForm(QWidget):
def __init__(self, parent:QWidget):
logger.debug(f"Initializating subform...")
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
self.parent = parent
# Initialize tab screen
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tab2 = QWidget()
self.tab3 = 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")
# Create submission adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout)
self.formwidget.setFixedWidth(300)
# Make scrollable interior for form
self.interior = QScrollArea(self.tab1)
self.interior.setWidgetResizable(True)
self.interior.setFixedWidth(325)
self.interior.setWidget(self.formwidget)
# Create sheet to hold existing submissions
self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent=parent)
self.sheetlayout.addWidget(self.sub_wid)
# Create layout of first tab to hold form and sheet
self.tab1.layout = QHBoxLayout(self)
self.tab1.setLayout(self.tab1.layout)
self.tab1.layout.addWidget(self.interior)
self.tab1.layout.addWidget(self.sheetwidget)
# create widgets for tab 2
# self.datepicker = ControlsDatePicker()
# self.webengineview = QWebEngineView()
# set tab2 layout
self.tab2.layout = QVBoxLayout(self)
# self.control_typer = QComboBox()
# fetch types of controls
# con_types = get_all_Control_Types_names(ctx=parent.ctx)
# con_types = [item.name for item in lookup_control_types(ctx=parent.ctx)]
# con_types = [item.name for item in ControlType.query()]
# self.control_typer.addItems(con_types)
# create custom widget to get types of analysis
# self.mode_typer = QComboBox()
# mode_types = get_all_available_modes(ctx=parent.ctx)
# mode_types = lookup_modes(ctx=parent.ctx)
# mode_types = Control.get_modes()
# self.mode_typer.addItems(mode_types)
# create custom widget to get subtypes of analysis
# self.sub_typer = QComboBox()
# self.sub_typer.setEnabled(False)
# add widgets to tab2 layout
# self.tab2.layout.addWidget(self.datepicker)
# self.tab2.layout.addWidget(self.control_typer)
# self.tab2.layout.addWidget(self.mode_typer)
# self.tab2.layout.addWidget(self.sub_typer)
# self.tab2.layout.addWidget(self.webengineview)
self.controls_viewer = ControlsViewer(self)
self.tab2.layout.addWidget(self.controls_viewer)
self.tab2.setLayout(self.tab2.layout)
# create custom widget to add new tabs
adder = KitAdder(self)
self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(adder)
self.tab3.setLayout(self.tab3.layout)
# add tabs to main widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)

View File

@@ -0,0 +1,193 @@
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
QDateEdit, QLabel, QSizePolicy
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control, get_control_subtypes
from PyQt6.QtCore import QDate, QSize
import logging
from tools import Report, Result
from backend.excel.reports import convert_data_list_to_df
from frontend.visualizations.control_charts import create_charts, construct_html
logger = logging.getLogger(f"submissions.{__name__}")
class ControlsViewer(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
self.app = self.parent().parent
print(f"\n\n{self.app}\n\n")
self.report = Report()
self.datepicker = ControlsDatePicker()
self.webengineview = QWebEngineView()
# set tab2 layout
self.layout = QVBoxLayout(self)
self.control_typer = QComboBox()
# fetch types of controls
# con_types = get_all_Control_Types_names(ctx=parent.ctx)
# con_types = [item.name for item in lookup_control_types(ctx=parent.ctx)]
con_types = [item.name for item in ControlType.query()]
self.control_typer.addItems(con_types)
# create custom widget to get types of analysis
self.mode_typer = QComboBox()
# mode_types = get_all_available_modes(ctx=parent.ctx)
# mode_types = lookup_modes(ctx=parent.ctx)
mode_types = Control.get_modes()
self.mode_typer.addItems(mode_types)
# create custom widget to get subtypes of analysis
self.sub_typer = QComboBox()
self.sub_typer.setEnabled(False)
# add widgets to tab2 layout
self.layout.addWidget(self.datepicker)
self.layout.addWidget(self.control_typer)
self.layout.addWidget(self.mode_typer)
self.layout.addWidget(self.sub_typer)
self.layout.addWidget(self.webengineview)
self.setLayout(self.layout)
self.controls_getter()
self.control_typer.currentIndexChanged.connect(self.controls_getter)
self.mode_typer.currentIndexChanged.connect(self.controls_getter)
self.datepicker.start_date.dateChanged.connect(self.controls_getter)
self.datepicker.end_date.dateChanged.connect(self.controls_getter)
def controls_getter(self):
"""
Lookup controls from database and send to chartmaker
"""
# from .main_window_functions import controls_getter_function
self.controls_getter_function()
# self.result_reporter()
def chart_maker(self):
"""
Creates plotly charts for webview
"""
# from .main_window_functions import chart_maker_function
self.chart_maker_function()
# self.result_reporter()
def controls_getter_function(self):
"""
Get controls based on start/end dates
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
# subtype defaults to disabled
try:
self.sub_typer.disconnect()
except TypeError:
pass
# correct start date being more recent than end date and rerun
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!")
threemonthsago = self.datepicker.end_date.date().addDays(-60)
# block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(threemonthsago)
self.controls_getter()
self.report.add_result(report)
return
# convert to python useable date objects
self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate()
self.con_type = self.control_typer.currentText()
self.mode = self.mode_typer.currentText()
self.sub_typer.clear()
# lookup subtypes
sub_types = get_control_subtypes(type=self.con_type, mode=self.mode)
# sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type)
if sub_types != []:
# block signal that will rerun controls getter and update sub_typer
with QSignalBlocker(self.sub_typer) as blocker:
self.sub_typer.addItems(sub_types)
self.sub_typer.setEnabled(True)
self.sub_typer.currentTextChanged.connect(self.chart_maker)
else:
self.sub_typer.clear()
self.sub_typer.setEnabled(False)
self.chart_maker()
self.report.add_result(report)
def chart_maker_function(self):
"""
Create html chart for controls reporting
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}")
# set the subtype for kraken
if self.sub_typer.currentText() == "":
self.subtype = None
else:
self.subtype = self.sub_typer.currentText()
logger.debug(f"Subtype: {self.subtype}")
# query all controls using the type/start and end dates from the gui
# controls = get_all_controls_by_type(ctx=obj.ctx, con_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
# controls = lookup_controls(ctx=obj.ctx, control_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
# if no data found from query set fig to none for reporting in webview
if controls == None:
fig = None
else:
# change each control to list of dictionaries
data = [control.convert_by_mode(mode=self.mode) for control in controls]
# flatten data to one dimensional list
data = [item for sublist in data for item in sublist]
logger.debug(f"Control objects going into df conversion: {type(data)}")
if data == []:
self.report.add_result(Result(status="Critical", msg="No data found for controls in given date range."))
return
# send to dataframe creator
df = convert_data_list_to_df(input=data, subtype=self.subtype)
if self.subtype == None:
title = self.mode
else:
title = f"{self.mode} - {self.subtype}"
# send dataframe to chart maker
fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title)
logger.debug(f"Updating figure...")
# construct html for webview
html = construct_html(figure=fig)
logger.debug(f"The length of html code is: {len(html)}")
self.webengineview.setHtml(html)
self.webengineview.update()
logger.debug("Figure updated... I hope.")
self.report.add_result(report)
class ControlsDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# start date is two months prior to end date by default
twomonthsago = QDate.currentDate().addDays(-60)
self.start_date.setDate(twomonthsago)
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def sizeHint(self) -> QSize:
return QSize(80,20)

View File

@@ -0,0 +1,56 @@
'''
functions used by all windows in the application's frontend
NOTE: Depreciated. Moved to functions.__init__
'''
from pathlib import Path
import logging
from PyQt6.QtWidgets import QMainWindow, QFileDialog
logger = logging.getLogger(f"submissions.{__name__}")
def select_open_file(obj:QMainWindow, file_extension:str) -> Path:
"""
File dialog to select a file to read from
Args:
obj (QMainWindow): Original main app window to be parent
file_extension (str): file extension
Returns:
Path: Path of file to be opened
"""
try:
# home_dir = Path(obj.ctx.directory_path).resolve().__str__()
home_dir = obj.last_dir.resolve().__str__()
except FileNotFoundError:
home_dir = Path.home().resolve().__str__()
except AttributeError:
home_dir = obj.app.last_dir.resolve().__str__()
fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0])
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0])
obj.last_file = fname
return fname
def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
"""
File dialog to select a file to write to
Args:
obj (QMainWindow): Original main app window to be parent
default_name (str): default base file name
extension (str): file extension
Returns:
Path: Path of file to be opened
"""
try:
# home_dir = Path(obj.ctx.directory_path).joinpath(default_name).resolve().__str__()
home_dir = obj.last_dir.joinpath(default_name).resolve().__str__()
except FileNotFoundError:
home_dir = Path.home().joinpath(default_name).resolve().__str__()
except AttributeError:
home_dir = obj.app.last_dir.joinpath(default_name).resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter = f"{extension}(*.{extension})")[0])
# fname = Path(QFileDialog.getSaveFileName(obj, "Save File", filter = f"{extension}(*.{extension})")[0])
obj.last_dir = fname.parent
return fname

View File

@@ -0,0 +1,223 @@
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
)
from sqlalchemy import FLOAT, INTEGER
from backend.db import SubmissionTypeKitTypeAssociation, SubmissionType, ReagentType
from backend.validators import PydReagentType, PydKit
import logging
from pprint import pformat
from tools import Report, Result
from typing import Tuple
logger = logging.getLogger(f"submissions.{__name__}")
class KitAdder(QWidget):
"""
dialog to get information to add kit
"""
def __init__(self, parent) -> None:
super().__init__(parent)
# self.ctx = parent_ctx
self.report = Report()
self.app = parent.parent
main_box = QVBoxLayout(self)
scroll = QScrollArea(self)
main_box.addWidget(scroll)
scroll.setWidgetResizable(True)
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
# self.setLayout(self.grid)
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("Kit Name:"),2,0)
# widget to get kit name
kit_name = QLineEdit()
kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1)
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit
used_for = QComboBox()
used_for.setObjectName("used_for")
# Insert all existing sample types
# used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
used_for.addItems([item.name for item in SubmissionType.query()])
used_for.setEditable(True)
self.grid.addWidget(used_for,3,1)
# Get all fields in SubmissionTypeKitTypeAssociation
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
for iii, column in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
field_name = column.name.replace("_", " ").title()
self.grid.addWidget(QLabel(field_name),idx,0)
match column.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(column.name)
self.grid.addWidget(add_widget, idx,1)
self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit)
scroll.setWidget(scrollContent)
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
"qt_scrollarea_vcontainer", "submit_btn"
]
def add_RT(self) -> None:
"""
insert new reagent type row
"""
print(self.app)
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm()
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)
def submit(self) -> None:
"""
send kit to database
"""
report = Report()
# get form info
info, reagents = self.parse_form()
# info, reagents = extract_form_info(self)
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
logger.debug(f"kit info: {pformat(info)}")
logger.debug(f"kit reagents: {pformat(reagents)}")
info['reagent_types'] = reagents
logger.debug(pformat(info))
# send to kit constructor
kit = PydKit(name=info['kit_name'])
for reagent in info['reagent_types']:
uses = {
info['used_for']:
{'sheet':reagent['sheet'],
'name':reagent['name'],
'lot':reagent['lot'],
'expiry':reagent['expiry']
}}
kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
logger.debug(f"Output pyd object: {kit.__dict__}")
# result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
sqlobj, result = kit.toSQL(self.ctx)
report.add_result(result=result)
sqlobj.save()
self.__init__(self.parent())
def parse_form(self) -> Tuple[dict, list]:
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
reagents = []
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
for widget in widgets:
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
match widget:
case ReagentTypeForm():
reagents.append(widget.parse_form())
case QLineEdit():
info[widget.objectName()] = widget.text()
case QComboBox():
info[widget.objectName()] = widget.currentText()
case QDateEdit():
info[widget.objectName()] = widget.date().toPyDate()
return info, reagents
class ReagentTypeForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self) -> None:
super().__init__()
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Reagent Type Name"),0,0)
# Widget to get reagent info
self.reagent_getter = QComboBox()
self.reagent_getter.setObjectName("rtname")
# lookup all reagent type names from db
# lookup = lookup_reagent_types(ctx=ctx)
lookup = ReagentType.query()
logger.debug(f"Looked up ReagentType names: {lookup}")
self.reagent_getter.addItems([item.__str__() for item in lookup])
self.reagent_getter.setEditable(True)
grid.addWidget(self.reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# widget to get extension of life
self.eol = QSpinBox()
self.eol.setObjectName('eol')
self.eol.setMinimum(0)
grid.addWidget(self.eol, 0,3)
grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
self.location_sheet_name = QLineEdit()
self.location_sheet_name.setObjectName("sheet")
self.location_sheet_name.setText("e.g. 'Reagent Info'")
grid.addWidget(self.location_sheet_name, 1,1)
for iii, item in enumerate(["Name", "Lot", "Expiry"]):
idx = iii + 2
grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
row = QSpinBox()
row.setFixedWidth(50)
row.setObjectName(f'{item.lower()}_row')
row.setMinimum(0)
grid.addWidget(row, idx, 1)
grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
col = QSpinBox()
col.setFixedWidth(50)
col.setObjectName(f'{item.lower()}_column')
col.setMinimum(0)
grid.addWidget(col, idx, 3)
self.setFixedHeight(175)
max_row = grid.rowCount()
self.r_button = QPushButton("Remove")
self.r_button.clicked.connect(self.remove)
grid.addWidget(self.r_button,max_row,0,1,1)
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
"qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
]
def remove(self):
self.setParent(None)
self.destroy()
def parse_form(self) -> dict:
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
info['eol'] = self.eol.value()
info['sheet'] = self.location_sheet_name.text()
info['rtname'] = self.reagent_getter.currentText()
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
for widget in widgets:
logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
match widget:
case QLineEdit():
info[widget.objectName()] = widget.text()
case QComboBox():
info[widget.objectName()] = widget.currentText()
case QDateEdit():
info[widget.objectName()] = widget.date().toPyDate()
case QSpinBox() | QDoubleSpinBox():
if "_" in widget.objectName():
key, sub_key = widget.objectName().split("_")
if key not in info.keys():
info[key] = {}
logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
info[key][sub_key] = widget.value()
return info

View File

@@ -0,0 +1,749 @@
'''
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, QFormLayout
)
from PyQt6.QtCore import Qt, QDate, QSize, pyqtSignal
from tools import check_not_nan, jinja_template_loading, Settings, Result
from backend.db.models import *
from sqlalchemy import FLOAT, INTEGER
import logging
import numpy as np
from .pop_ups import AlertPop, QuestionAsker
from backend.validators import PydReagent, PydKit, PydReagentType, PydSubmission
from typing import Tuple, List
from pprint import pformat
import difflib
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class AddReagentForm(QDialog):
"""
dialog to add gather info about new reagent
"""
def __init__(self, ctx:dict, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, reagent_name:str|None=None) -> None:
super().__init__()
self.ctx = ctx
if reagent_lot == None:
reagent_lot = reagent_type
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)
# widget to get lot info
self.name_input = QComboBox()
self.name_input.setObjectName("name")
self.name_input.setEditable(True)
self.name_input.setCurrentText(reagent_name)
# self.name_input.setText(reagent_name)
self.lot_input = QLineEdit()
self.lot_input.setObjectName("lot")
self.lot_input.setText(reagent_lot)
# widget to get expiry info
self.exp_input = QDateEdit(calendarPopup=True)
self.exp_input.setObjectName('expiry')
# if expiry is not passed in from gui, use today
if expiry == None:
self.exp_input.setDate(QDate.currentDate())
else:
self.exp_input.setDate(expiry)
# widget to get reagent type info
self.type_input = QComboBox()
self.type_input.setObjectName('type')
# self.type_input.addItems([item.name for item in lookup_reagent_types(ctx=ctx)])
self.type_input.addItems([item.name for item in ReagentType.query()])
logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string?
try:
reagent_type = reagent_type.replace("_", " ").title()
except AttributeError:
reagent_type = None
# set parsed reagent type to top of list
index = self.type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
if index >= 0:
self.type_input.setCurrentIndex(index)
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Name:"))
self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Lot:"))
self.layout.addWidget(self.lot_input)
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
self.layout.addWidget(self.exp_input)
self.layout.addWidget(QLabel("Type:"))
self.layout.addWidget(self.type_input)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.type_input.currentTextChanged.connect(self.update_names)
def parse_form(self):
return dict(name=self.name_input.currentText(),
lot=self.lot_input.text(),
expiry=self.exp_input.date().toPyDate(),
type=self.type_input.currentText())
def update_names(self):
"""
Updates reagent names form field with examples from reagent type
"""
logger.debug(self.type_input.currentText())
self.name_input.clear()
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=self.type_input.currentText())
lookup = Reagent.query(reagent_type=self.type_input.currentText())
self.name_input.addItems(list(set([item.name for item in lookup])))
class ReportDatePicker(QDialog):
"""
custom dialog to ask for report start/stop dates
"""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates
self.start_date = QDateEdit(calendarPopup=True)
self.start_date.setObjectName("start_date")
self.start_date.setDate(QDate.currentDate())
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setObjectName("end_date")
self.end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
# class KitAdder(QWidget):
# """
# dialog to get information to add kit
# """
# def __init__(self) -> None:
# super().__init__()
# # self.ctx = parent_ctx
# main_box = QVBoxLayout(self)
# scroll = QScrollArea(self)
# main_box.addWidget(scroll)
# scroll.setWidgetResizable(True)
# scrollContent = QWidget(scroll)
# self.grid = QGridLayout()
# # self.setLayout(self.grid)
# 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("Kit Name:"),2,0)
# # widget to get kit name
# kit_name = QLineEdit()
# kit_name.setObjectName("kit_name")
# self.grid.addWidget(kit_name,2,1)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# # widget to get uses of kit
# used_for = QComboBox()
# used_for.setObjectName("used_for")
# # Insert all existing sample types
# # used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
# used_for.addItems([item.name for item in SubmissionType.query()])
# used_for.setEditable(True)
# self.grid.addWidget(used_for,3,1)
# # Get all fields in SubmissionTypeKitTypeAssociation
# self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
# for iii, column in enumerate(self.columns):
# idx = iii + 4
# # convert field name to human readable.
# field_name = column.name.replace("_", " ").title()
# self.grid.addWidget(QLabel(field_name),idx,0)
# match column.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(column.name)
# self.grid.addWidget(add_widget, idx,1)
# self.add_RT_btn = QPushButton("Add Reagent Type")
# self.grid.addWidget(self.add_RT_btn)
# self.add_RT_btn.clicked.connect(self.add_RT)
# self.submit_btn.clicked.connect(self.submit)
# scroll.setWidget(scrollContent)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn"
# ]
# def add_RT(self) -> None:
# """
# insert new reagent type row
# """
# # get bottommost row
# maxrow = self.grid.rowCount()
# reg_form = ReagentTypeForm()
# 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)
# def submit(self) -> None:
# """
# send kit to database
# """
# # get form info
# info, reagents = self.parse_form()
# # info, reagents = extract_form_info(self)
# info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
# logger.debug(f"kit info: {pformat(info)}")
# logger.debug(f"kit reagents: {pformat(reagents)}")
# info['reagent_types'] = reagents
# logger.debug(pformat(info))
# # send to kit constructor
# kit = PydKit(name=info['kit_name'])
# for reagent in info['reagent_types']:
# uses = {
# info['used_for']:
# {'sheet':reagent['sheet'],
# 'name':reagent['name'],
# 'lot':reagent['lot'],
# 'expiry':reagent['expiry']
# }}
# kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
# logger.debug(f"Output pyd object: {kit.__dict__}")
# # result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
# sqlobj, result = kit.toSQL(self.ctx)
# sqlobj.save()
# msg = AlertPop(message=result['message'], status=result['status'])
# msg.exec()
# self.__init__(self.ctx)
# def parse_form(self) -> Tuple[dict, list]:
# logger.debug(f"Hello from {self.__class__} parser!")
# info = {}
# reagents = []
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
# for widget in widgets:
# # logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
# match widget:
# case ReagentTypeForm():
# reagents.append(widget.parse_form())
# case QLineEdit():
# info[widget.objectName()] = widget.text()
# case QComboBox():
# info[widget.objectName()] = widget.currentText()
# case QDateEdit():
# info[widget.objectName()] = widget.date().toPyDate()
# return info, reagents
# class ReagentTypeForm(QWidget):
# """
# custom widget to add information about a new reagenttype
# """
# def __init__(self) -> None:
# super().__init__()
# grid = QGridLayout()
# self.setLayout(grid)
# grid.addWidget(QLabel("Reagent Type Name"),0,0)
# # Widget to get reagent info
# self.reagent_getter = QComboBox()
# self.reagent_getter.setObjectName("rtname")
# # lookup all reagent type names from db
# # lookup = lookup_reagent_types(ctx=ctx)
# lookup = ReagentType.query()
# logger.debug(f"Looked up ReagentType names: {lookup}")
# self.reagent_getter.addItems([item.__str__() for item in lookup])
# self.reagent_getter.setEditable(True)
# grid.addWidget(self.reagent_getter,0,1)
# grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# # widget to get extension of life
# self.eol = QSpinBox()
# self.eol.setObjectName('eol')
# self.eol.setMinimum(0)
# grid.addWidget(self.eol, 0,3)
# grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
# self.location_sheet_name = QLineEdit()
# self.location_sheet_name.setObjectName("sheet")
# self.location_sheet_name.setText("e.g. 'Reagent Info'")
# grid.addWidget(self.location_sheet_name, 1,1)
# for iii, item in enumerate(["Name", "Lot", "Expiry"]):
# idx = iii + 2
# grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
# row = QSpinBox()
# row.setFixedWidth(50)
# row.setObjectName(f'{item.lower()}_row')
# row.setMinimum(0)
# grid.addWidget(row, idx, 1)
# grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
# col = QSpinBox()
# col.setFixedWidth(50)
# col.setObjectName(f'{item.lower()}_column')
# col.setMinimum(0)
# grid.addWidget(col, idx, 3)
# self.setFixedHeight(175)
# max_row = grid.rowCount()
# self.r_button = QPushButton("Remove")
# self.r_button.clicked.connect(self.remove)
# grid.addWidget(self.r_button,max_row,0,1,1)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
# ]
# def remove(self):
# self.setParent(None)
# self.destroy()
# def parse_form(self) -> dict:
# logger.debug(f"Hello from {self.__class__} parser!")
# info = {}
# info['eol'] = self.eol.value()
# info['sheet'] = self.location_sheet_name.text()
# info['rtname'] = self.reagent_getter.currentText()
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
# for widget in widgets:
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
# match widget:
# case QLineEdit():
# info[widget.objectName()] = widget.text()
# case QComboBox():
# info[widget.objectName()] = widget.currentText()
# case QDateEdit():
# info[widget.objectName()] = widget.date().toPyDate()
# case QSpinBox() | QDoubleSpinBox():
# if "_" in widget.objectName():
# key, sub_key = widget.objectName().split("_")
# if key not in info.keys():
# info[key] = {}
# logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
# info[key][sub_key] = widget.value()
# return info
# class ControlsDatePicker(QWidget):
# """
# custom widget to pick start and end dates for controls graphs
# """
# def __init__(self) -> None:
# super().__init__()
# self.start_date = QDateEdit(calendarPopup=True)
# # start date is two months prior to end date by default
# twomonthsago = QDate.currentDate().addDays(-60)
# self.start_date.setDate(twomonthsago)
# self.end_date = QDateEdit(calendarPopup=True)
# self.end_date.setDate(QDate.currentDate())
# self.layout = QHBoxLayout()
# self.layout.addWidget(QLabel("Start Date"))
# self.layout.addWidget(self.start_date)
# self.layout.addWidget(QLabel("End Date"))
# self.layout.addWidget(self.end_date)
# self.setLayout(self.layout)
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# def sizeHint(self) -> QSize:
# return QSize(80,20)
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")]
ww = [item.rsl_plate_num for item in BasicSubmission.query(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
# class ReagentFormWidget(QWidget):
# def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
# super().__init__(parent)
# # self.setParent(parent)
# self.reagent = reagent
# self.extraction_kit = extraction_kit
# # self.ctx = reagent.ctx
# layout = QVBoxLayout()
# self.label = self.ReagentParsedLabel(reagent=reagent)
# layout.addWidget(self.label)
# self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
# layout.addWidget(self.lot)
# # Remove spacing between reagents
# layout.setContentsMargins(0,0,0,0)
# self.setLayout(layout)
# self.setObjectName(reagent.name)
# self.missing = reagent.missing
# # If changed set self.missing to True and update self.label
# self.lot.currentTextChanged.connect(self.updated)
# def parse_form(self) -> Tuple[PydReagent, dict]:
# lot = self.lot.currentText()
# # wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
# wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
# # if reagent doesn't exist in database, off to add it (uses App.add_reagent)
# if wanted_reagent == None:
# dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
# if dlg.exec():
# wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
# return wanted_reagent, None
# else:
# # In this case we will have an empty reagent and the submission will fail kit integrity check
# logger.debug("Will not add reagent.")
# return None, Result(msg="Failed integrity check", status="Critical")
# else:
# # Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
# # from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
# # rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
# rt = ReagentType.query(name=self.reagent.type)
# if rt == None:
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
# rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
# return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
# def updated(self):
# self.missing = True
# self.label.updated(self.reagent.type)
# class ReagentParsedLabel(QLabel):
# def __init__(self, reagent:PydReagent):
# super().__init__()
# try:
# check = not reagent.missing
# except:
# check = False
# self.setObjectName(f"{reagent.type}_label")
# if check:
# self.setText(f"Parsed {reagent.type}")
# else:
# self.setText(f"MISSING {reagent.type}")
# def updated(self, reagent_type:str):
# self.setText(f"UPDATED {reagent_type}")
# class ReagentLot(QComboBox):
# def __init__(self, reagent, extraction_kit:str) -> None:
# super().__init__()
# # self.ctx = reagent.ctx
# self.setEditable(True)
# # if reagent.parsed:
# # pass
# logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
# # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
# # lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
# lookup = Reagent.query(reagent_type=reagent.type)
# relevant_reagents = [item.__str__() for item in lookup]
# output_reg = []
# for rel_reagent in relevant_reagents:
# # extract strings from any sets.
# if isinstance(rel_reagent, set):
# for thing in rel_reagent:
# output_reg.append(thing)
# elif isinstance(rel_reagent, str):
# output_reg.append(rel_reagent)
# relevant_reagents = output_reg
# # if reagent in sheet is not found insert it into the front of relevant reagents so it shows
# logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
# if str(reagent.lot) not in relevant_reagents:
# if check_not_nan(reagent.lot):
# relevant_reagents.insert(0, str(reagent.lot))
# else:
# # TODO: look up the last used reagent of this type in the database
# # looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
# looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
# try:
# # looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
# looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
# except AttributeError:
# looked_up_reg = None
# logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
# if looked_up_reg != None:
# relevant_reagents.remove(str(looked_up_reg.lot))
# relevant_reagents.insert(0, str(looked_up_reg.lot))
# else:
# if len(relevant_reagents) > 1:
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
# idx = relevant_reagents.index(str(reagent.lot))
# logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
# moved_reag = relevant_reagents.pop(idx)
# relevant_reagents.insert(0, moved_reag)
# else:
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
# logger.debug(f"New relevant reagents: {relevant_reagents}")
# self.setObjectName(f"lot_{reagent.type}")
# self.addItems(relevant_reagents)
# class SubmissionFormWidget(QWidget):
# def __init__(self, parent: QWidget, **kwargs) -> None:
# super().__init__(parent)
# # self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# # "qt_scrollarea_vcontainer", "submit_btn"
# # ]
# self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
# layout = QVBoxLayout()
# for k, v in kwargs.items():
# if k not in self.ignore:
# add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
# if add_widget != None:
# layout.addWidget(add_widget)
# else:
# setattr(self, k, v)
# self.setLayout(layout)
# def create_widget(self, key:str, value:dict, submission_type:str|None=None):
# if key not in self.ignore:
# return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
# return None
# def clear_form(self):
# for item in self.findChildren(QWidget):
# item.setParent(None)
# def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
# query = self.findChildren(QWidget)
# if object_name != None:
# query = [widget for widget in query if widget.objectName()==object_name]
# return query
# def parse_form(self) -> PydSubmission:
# logger.debug(f"Hello from form parser!")
# info = {}
# reagents = []
# if hasattr(self, 'csv'):
# info['csv'] = self.csv
# for widget in self.findChildren(QWidget):
# # logger.debug(f"Parsed widget of type {type(widget)}")
# match widget:
# case ReagentFormWidget():
# reagent, _ = widget.parse_form()
# if reagent != None:
# reagents.append(reagent)
# case self.InfoItem():
# field, value = widget.parse_form()
# if field != None:
# info[field] = value
# logger.debug(f"Info: {pformat(info)}")
# logger.debug(f"Reagents: {pformat(reagents)}")
# # app = self.parent().parent().parent().parent().parent().parent().parent().parent
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
# return submission
# class InfoItem(QWidget):
# def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
# super().__init__(parent)
# layout = QVBoxLayout()
# self.label = self.ParsedQLabel(key=key, value=value)
# self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value'])
# self.setObjectName(key)
# try:
# self.missing:bool = value['missing']
# except (TypeError, KeyError):
# self.missing:bool = True
# if self.input != None:
# layout.addWidget(self.label)
# layout.addWidget(self.input)
# layout.setContentsMargins(0,0,0,0)
# self.setLayout(layout)
# match self.input:
# case QComboBox():
# self.input.currentTextChanged.connect(self.update_missing)
# case QDateEdit():
# self.input.dateChanged.connect(self.update_missing)
# case QLineEdit():
# self.input.textChanged.connect(self.update_missing)
# def parse_form(self):
# match self.input:
# case QLineEdit():
# value = self.input.text()
# case QComboBox():
# value = self.input.currentText()
# case QDateEdit():
# value = self.input.date().toPyDate()
# case _:
# return None, None
# return self.input.objectName(), dict(value=value, missing=self.missing)
# def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
# try:
# value = value['value']
# except (TypeError, KeyError):
# pass
# obj = parent.parent().parent()
# logger.debug(f"Creating widget for: {key}")
# match key:
# case 'submitting_lab':
# add_widget = QComboBox()
# # lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
# labs = [item.__str__() for item in Organization.query()]
# # try to set closest match to top of list
# try:
# labs = difflib.get_close_matches(value, labs, len(labs), 0)
# except (TypeError, ValueError):
# pass
# # set combobox values to lookedup values
# add_widget.addItems(labs)
# case 'extraction_kit':
# # if extraction kit not available, all other values fail
# if not check_not_nan(value):
# msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
# msg.exec()
# # create combobox to hold looked up kits
# add_widget = QComboBox()
# # lookup existing kits by 'submission_type' decided on by sheetparser
# logger.debug(f"Looking up kits used for {submission_type}")
# uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
# obj.uses = uses
# logger.debug(f"Kits received for {submission_type}: {uses}")
# if check_not_nan(value):
# logger.debug(f"The extraction kit in parser was: {value}")
# uses.insert(0, uses.pop(uses.index(value)))
# obj.ext_kit = value
# else:
# logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
# obj.ext_kit = uses[0]
# add_widget.addItems(uses)
# # Run reagent scraper whenever extraction kit is changed.
# # add_widget.currentTextChanged.connect(obj.scrape_reagents)
# case 'submitted_date':
# # uses base calendar
# add_widget = QDateEdit(calendarPopup=True)
# # sets submitted date based on date found in excel sheet
# try:
# add_widget.setDate(value)
# # if not found, use today
# except:
# add_widget.setDate(date.today())
# case 'submission_category':
# add_widget = QComboBox()
# cats = ['Diagnostic', "Surveillance", "Research"]
# # cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
# cats += [item.name for item in SubmissionType.query()]
# try:
# cats.insert(0, cats.pop(cats.index(value)))
# except ValueError:
# cats.insert(0, cats.pop(cats.index(submission_type)))
# add_widget.addItems(cats)
# case _:
# # anything else gets added in as a line edit
# add_widget = QLineEdit()
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
# add_widget.setText(str(value).replace("_", " "))
# if add_widget != None:
# add_widget.setObjectName(key)
# add_widget.setParent(parent)
# return add_widget
# def update_missing(self):
# self.missing = True
# self.label.updated(self.objectName())
# class ParsedQLabel(QLabel):
# def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
# super().__init__()
# try:
# check = not value['missing']
# except:
# check = True
# if label_name != None:
# self.setObjectName(label_name)
# else:
# self.setObjectName(f"{key}_label")
# if title:
# output = key.replace('_', ' ').title()
# else:
# output = key.replace('_', ' ')
# if check:
# self.setText(f"Parsed {output}")
# else:
# self.setText(f"MISSING {output}")
# def updated(self, key:str, title:bool=True):
# if title:
# output = key.replace('_', ' ').title()
# else:
# output = key.replace('_', ' ')
# self.setText(f"UPDATED {output}")

View File

@@ -0,0 +1,102 @@
'''
Contains dialogs for notification and prompting.
'''
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog,
QDialogButtonBox, QMessageBox, QComboBox
)
from tools import jinja_template_loading
import logging
from backend.db.models import KitType, SubmissionType
from typing import Literal
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class QuestionAsker(QDialog):
"""
dialog to ask yes/no questions
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
self.setWindowTitle(title)
# set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
# Text for the yes/no question
self.message = QLabel(message)
self.layout.addWidget(self.message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class AlertPop(QMessageBox):
"""
Dialog to show an alert.
"""
def __init__(self, message:str, status:Literal['Information', 'Question', 'Warning', 'Critical'], owner:str|None=None) -> QMessageBox:
super().__init__()
# select icon by string
icon = getattr(QMessageBox.Icon, status)
self.setIcon(icon)
self.setInformativeText(message)
self.setWindowTitle(f"{owner} - {status.title()}")
class KitSelector(QDialog):
"""
dialog to ask yes/no questions
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
self.setWindowTitle(title)
self.widget = QComboBox()
# kits = [item.__str__() for item in lookup_kit_types(ctx=ctx)]
kits = [item.__str__() for item in KitType.query()]
self.widget.addItems(kits)
self.widget.setEditable(False)
# set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
# Text for the yes/no question
message = QLabel(message)
self.layout.addWidget(message)
self.layout.addWidget(self.widget)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def getValues(self):
return self.widget.currentText()
class SubmissionTypeSelector(QDialog):
"""
dialog to ask yes/no questions
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
self.setWindowTitle(title)
self.widget = QComboBox()
# sub_type = [item.name for item in lookup_submission_type(ctx=ctx)]
sub_type = [item.name for item in SubmissionType.query()]
self.widget.addItems(sub_type)
self.widget.setEditable(False)
# set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
# Text for the yes/no question
message = QLabel(message)
self.layout.addWidget(message)
self.layout.addWidget(self.widget)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
return self.widget.currentText()

View File

@@ -0,0 +1,646 @@
'''
Contains widgets specific to the submission summary and submission details.
'''
import base64
from datetime import datetime
from io import BytesIO
import pprint
from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea,
QMessageBox, QFileDialog, QMenu, QLabel,
QDialogButtonBox, QToolBar
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.functions import submissions_to_df
from backend.db.models import BasicSubmission
from backend.excel import make_hitpicks, make_report_html, make_report_xlsx
from tools import check_if_app, Settings, Report, Result
from tools import jinja_template_loading
from xhtml2pdf import pisa
from pathlib import Path
import logging
from .pop_ups import QuestionAsker, AlertPop
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
import pandas as pd
from getpass import getuser
import json
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
NOTE: Copied from Stack Overflow. I have no idea how it actually works.
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0]
def columnCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str|None:
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
class SubmissionsSheet(QTableView):
"""
presents submission summary to user in tab1
"""
def __init__(self, parent) -> None:
"""
initialize
Args:
ctx (dict): settings passed from gui
"""
super().__init__(parent)
self.app = self.parent()
# self.ctx = ctx
self.report = Report()
self.setData()
self.resizeColumnsToContents()
self.resizeRowsToContents()
self.setSortingEnabled(True)
self.doubleClicked.connect(self.show_details)
def setData(self) -> None:
"""
sets data in model
"""
self.data = submissions_to_df()
try:
self.data['id'] = self.data['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(3)
except KeyError:
pass
proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
def show_details(self) -> None:
"""
creates detailed data to show in seperate window
"""
logger.debug(f"Sheet.app: {self.app}")
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
dlg = SubmissionDetails(parent=self, id=value)
if dlg.exec():
pass
def create_barcode(self) -> None:
"""
Generates a window for displaying barcode
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = BarcodeWindow(value)
if dlg.exec():
dlg.print_barcode()
def add_comment(self) -> None:
"""
Generates a text editor window.
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = SubmissionComment(ctx=self.ctx, rsl=value)
if dlg.exec():
dlg.add_comment()
def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
Args:
event (_type_): the item of interest
"""
self.menu = QMenu(self)
renameAction = QAction('Delete', self)
detailsAction = QAction('Details', self)
barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", 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())
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(hitpickAction)
# add other required actions
self.menu.popup(QCursor.pos())
def delete_item(self, event):
"""
Confirms user deletion and sends id to backend for deletion.
Args:
event (_type_): the item of interest
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
logger.debug(index)
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
if msg.exec():
# delete_submission(id=value)
BasicSubmission.query(id=value).delete()
else:
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)
self.report = Report()
self.app.result_reporter()
def link_extractions_function(self):
"""
Link extractions from runlogs to imported submissions
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
fname = select_open_file(self, file_extension="csv")
with open(fname.__str__(), 'r') as f:
# split csv on commas
runs = [col.strip().split(",") for col in f.readlines()]
count = 0
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_num=run[1].strip(),
sample_count=run[2].strip(),
status=run[3].strip(),
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# elution columns are item 6 in the comma split list to the end
for ii in range(6, len(run)):
new_run[f"column{str(ii-5)}_vol"] = run[ii]
# Lookup imported submissions
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
# If no such submission exists, move onto the next run
if sub == None:
continue
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
count += 1
except AttributeError:
continue
if sub.extraction_info != None:
existing = json.loads(sub.extraction_info)
else:
existing = None
# Check if the new info already exists in the imported submission
try:
if json.dumps(new_run) in sub.extraction_info:
logger.debug(f"Looks like we already have that info.")
continue
except TypeError:
pass
# Update or create the extraction info
if existing != None:
try:
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
existing.append(new_run)
logger.debug(f"Setting: {existing}")
sub.extraction_info = json.dumps(existing)
except TypeError:
logger.error(f"Error updating!")
sub.extraction_info = json.dumps([new_run])
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.extraction_info}")
else:
sub.extraction_info = json.dumps([new_run])
sub.save()
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def link_pcr(self):
self.link_pcr_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def link_pcr_function(self):
"""
Link PCR data from run logs to an imported submission
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
fname = select_open_file(self, file_extension="csv")
with open(fname.__str__(), 'r') as f:
# split csv rows on comma
runs = [col.strip().split(",") for col in f.readlines()]
count = 0
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_num=run[1].strip(),
biomek_status=run[2].strip(),
quant_status=run[3].strip(),
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# lookup imported submission
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
# if imported submission doesn't exist move on to next run
if sub == None:
continue
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
except AttributeError:
continue
# check if pcr_info already exists
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
existing = json.loads(sub.pcr_info)
else:
existing = None
# check if this entry already exists in imported submission
try:
if json.dumps(new_run) in sub.pcr_info:
logger.debug(f"Looks like we already have that info.")
continue
else:
count += 1
except TypeError:
logger.error(f"No json to dump")
if existing != None:
try:
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
existing.append(new_run)
logger.debug(f"Setting: {existing}")
sub.pcr_info = json.dumps(existing)
except TypeError:
logger.error(f"Error updating!")
sub.pcr_info = json.dumps([new_run])
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}")
else:
sub.pcr_info = json.dumps([new_run])
sub.save()
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def generate_report(self):
self.generate_report_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def generate_report_function(self):
"""
Generate a summary of activities for a time period
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
# ask for date ranges
dlg = ReportDatePicker()
if dlg.exec():
info = dlg.parse_form()
logger.debug(f"Report info: {info}")
# find submissions based on date range
subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date'])
# convert each object to dict
records = [item.report_dict() for item in subs]
# make dataframe from record dictionaries
detailed_df, summary_df = make_report_xlsx(records=records)
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
# get save location of report
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf", extension="pdf")
with open(fname, "w+b") as f:
pisa.CreatePDF(html, dest=f)
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
summary_df.to_excel(writer, sheet_name="Report")
detailed_df.to_excel(writer, sheet_name="Details", index=False)
worksheet = writer.sheets['Report']
for idx, col in enumerate(summary_df): # loop through all columns
series = summary_df[col]
max_len = max((
series.astype(str).map(len).max(), # len of largest item
len(str(series.name)) # len of column name/header
)) + 20 # adding a little extra space
try:
worksheet.column_dimensions[get_column_letter(idx)].width = max_len
except ValueError:
pass
for cell in worksheet['D']:
if cell.row > 1:
cell.style = 'Currency'
writer.close()
self.report.add_result(report)
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, parent, id:int) -> None:
super().__init__(parent)
# self.ctx = ctx
self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# get submision from db
# sub = lookup_submissions(ctx=ctx, id=id)
sub = BasicSubmission.query(id=id)
logger.debug(f"Submission details data:\n{pprint.pformat(sub.to_dict())}")
self.base_dict = sub.to_dict(full_data=True)
# don't want id
del self.base_dict['id']
logger.debug(f"Creating barcode.")
if not check_if_app():
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
logger.debug(f"Hitpicking plate...")
self.plate_dicto = sub.hitpick_plate()
logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
# logger.debug(f"Platemap: {self.base_dict['platemap']}")
# logger.debug(f"platemap: {platemap}")
# image_io = BytesIO()
# try:
# platemap.save(image_io, 'JPEG')
# except AttributeError:
# logger.error(f"No plate map found for {sub.rsl_plate_num}")
# self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
self.template = env.get_template("submission_details.html")
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
webview.setMaximumSize(900, 500)
webview.setHtml(self.html)
self.layout = QVBoxLayout()
interior.resize(900, 500)
interior.setWidget(webview)
self.setFixedSize(900, 500)
# button to export a pdf version
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(900)
btn.clicked.connect(self.export)
def export(self):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
# try:
# home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
# except FileNotFoundError:
# home_dir = Path.home().resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# if fname.__str__() == ".":
# logger.debug("Saving pdf was cancelled.")
# return
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
del self.base_dict['platemap']
export_map = make_plate_map(self.plate_dicto)
image_io = BytesIO()
try:
export_map.save(image_io, 'JPEG')
except AttributeError:
logger.error(f"No plate map found")
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
self.html2 = self.template.render(sub=self.base_dict)
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()
class BarcodeWindow(QDialog):
def __init__(self, rsl_num:str):
super().__init__()
# set the title
self.setWindowTitle("Image")
self.layout = QVBoxLayout()
# setting the geometry of window
self.setGeometry(0, 0, 400, 300)
# creating label
self.label = QLabel()
self.img = make_plate_barcode(rsl_num)
self.pixmap = QPixmap()
self.pixmap.loadFromData(self.img)
# adding image to label
self.label.setPixmap(self.pixmap)
# show all the widgets]
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.label)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self._createActions()
self._createToolBar()
self._connectActions()
def _createToolBar(self):
"""
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
toolbar.addAction(self.printAction)
def _createActions(self):
"""
creates actions
"""
self.printAction = QAction("&Print", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.printAction.triggered.connect(self.print_barcode)
def print_barcode(self):
"""
Sends barcode image to printer.
"""
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage())
def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
logger.debug(f"Hello from print handler.")
painter = QPainter(printer)
image = QPixmap.fromImage(im)
painter.drawPixmap(120, -20, image)
painter.end()
class SubmissionComment(QDialog):
"""
a window for adding comment text to a submission
"""
def __init__(self, ctx:Settings, rsl:str) -> None:
super().__init__()
self.ctx = ctx
self.rsl = rsl
self.setWindowTitle(f"{self.rsl} Submission Comment")
# create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.setFixedSize(400, 300)
self.layout.addWidget(self.txt_editor)
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def add_comment(self):
"""
Adds comment to submission object.
"""
commenter = getuser()
comment = self.txt_editor.toPlainText()
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
full_comment = {"name":commenter, "time": dt, "text": comment}
logger.debug(f"Full comment: {full_comment}")
# sub = lookup_submission_by_rsl_num(ctx = self.ctx, rsl_num=self.rsl)
# sub = lookup_submissions(ctx = self.ctx, rsl_number=self.rsl)
sub = BasicSubmission.query(rsl_number=self.rsl)
try:
sub.comment.append(full_comment)
except AttributeError:
sub.comment = [full_comment]
logger.debug(sub.__dict__)
self.ctx.database_session.add(sub)
self.ctx.database_session.commit()

View File

@@ -0,0 +1,747 @@
from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel
)
from PyQt6.QtCore import pyqtSignal
from pathlib import Path
from . import select_open_file, select_save_file
import logging
from pathlib import Path
from tools import Report, Result, check_not_nan
from backend.excel.parser import SheetParser, PCRParser
from backend.validators import PydSubmission, PydReagent
from backend.db import (
check_kit_integrity, update_last_used, KitType, Organization, SubmissionType, Reagent,
ReagentType, KitTypeReagentTypeAssociation, BasicSubmission, update_subsampassoc_with_pcr
)
from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop
# from .misc import ReagentFormWidget
from typing import List, Tuple
import difflib
from datetime import date
import inspect
import json
logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionFormContainer(QWidget):
import_drag = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None:
logger.debug(f"Setting form widget...")
super().__init__(parent)
self.app = self.parent().parent#().parent().parent().parent().parent().parent
self.report = Report()
# self.parent = parent
self.setAcceptDrops(True)
self.import_drag.connect(self.importSubmission)
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])
logger.debug(f"App: {self.app}")
self.app.last_dir = fname.parent
self.import_drag.emit(fname)
def importSubmission(self, fname:Path|None=None):
"""
import submission from excel sheet into form
"""
# from .main_window_functions import import_submission_function
self.app.raise_()
self.app.activateWindow()
self.import_submission_function(fname)
logger.debug(f"Result from result reporter: {self.report.results}")
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def scrape_reagents(self, *args, **kwargs):
# from .main_window_functions import scrape_reagents
# logger.debug(f"Args: {args}")
# logger.debug(F"kwargs: {kwargs}")
print(f"\n\n{inspect.stack()[1].function}\n\n")
self.scrape_reagents_function(args[0])
self.kit_integrity_completion()
self.app.report.add_result(self.report)
self.report = Report()
match inspect.stack()[1].function:
case "import_submission_function":
pass
case _:
self.app.result_reporter()
# def kit_reload_function(self):
# """
# Reload the fields in the form
# Args:
# obj (QMainWindow): original app window
# Returns:
# Tuple[QMainWindow, dict]: Collection of new main app window and result dict
# """
# report = Report()
# # for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
# logger.debug(f"Attempting to clear {obj.form.find_widgets()}")
# for item in self.form.find_widgets():
# if isinstance(item, ReagentFormWidget):
# item.setParent(None)
# self.kit_integrity_completion_function()
# self.report.add_result(report)
def kit_integrity_completion(self):
"""
Performs check of imported reagents
NOTE: this will not change self.reagents which should be fine
since it's only used when looking up
"""
# from .main_window_functions import kit_integrity_completion_function
self.kit_integrity_completion_function()
self.app.report.add_result(self.report)
self.report = Report()
match inspect.stack()[1].function:
case "import_submission_function":
pass
case _:
self.app.result_reporter()
def submit_new_sample(self):
"""
Attempt to add sample to database when 'submit' button clicked
"""
# from .main_window_functions import submit_new_sample_function
self.submit_new_sample_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def export_csv(self, fname:Path|None=None):
# from .main_window_functions import export_csv_function
self.export_csv_function(fname)
def import_submission_function(self, fname:Path|None=None):
"""
Import a new submission to the app window
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict|None]: Collection of new main app window and result dict
"""
logger.debug(f"\n\nStarting Import...\n\n")
report = Report()
# logger.debug(obj.ctx)
# initialize samples
try:
self.form.setParent(None)
except AttributeError:
pass
self.samples = []
self.missing_info = []
# set file dialog
if isinstance(fname, bool) or fname == None:
fname = select_open_file(self, 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")
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
self.report.add_result(report)
return
# create sheetparser using excel sheet and context from gui
try:
self.prsr = SheetParser(ctx=self.ctx, filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.prsr = SheetParser(ctx=self.app.ctx, filepath=fname)
try:
logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}")
self.pyd = self.prsr.to_pydantic()
logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
except Exception as e:
report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="Critical"))
self.report.add_result(report)
return
self.form = self.pyd.toForm(parent=self)
self.layout().addWidget(self.form)
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
logger.debug(f"Kitwidget {kit_widget}")
self.scrape_reagents(kit_widget.currentText())
kit_widget.currentTextChanged.connect(self.scrape_reagents)
# compare obj.reagents with expected reagents in kit
if self.prsr.sample_result != None:
report.add_result(msg=self.prsr.sample_result, status="Warning")
self.report.add_result(report)
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):
"""
Extracted scrape reagents function that will run when
form 'extraction_kit' widget is updated.
Args:
obj (QMainWindow): updated main application
extraction_kit (str): name of extraction kit (in 'extraction_kit' widget)
Returns:
Tuple[QMainWindow, dict]: Updated application and result
"""
report = Report()
logger.debug(f"Extraction kit: {extraction_kit}")
# obj.reagents = []
# obj.missing_reagents = []
# Remove previous reagent widgets
try:
old_reagents = self.form.find_widgets()
except AttributeError:
logger.error(f"Couldn't find old reagents.")
old_reagents = []
# logger.debug(f"\n\nAttempting to clear: {old_reagents}\n\n")
for reagent in old_reagents:
if isinstance(reagent, ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None)
# reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
# logger.debug(f"Got reagents: {reagents}")
# for reagent in obj.prsr.sub['reagents']:
# # create label
# if reagent.parsed:
# obj.reagents.append(reagent)
# else:
# obj.missing_reagents.append(reagent)
self.form.reagents = self.prsr.sub['reagents']
# logger.debug(f"Imported reagents: {obj.reagents}")
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
self.report.add_result(report)
logger.debug(f"Outgoing report: {self.report.results}")
def kit_integrity_completion_function(self):
"""
Compare kit contents to parsed contents
Args:
obj (QMainWindow): The original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
missing_reagents = []
# logger.debug(inspect.currentframe().f_back.f_code.co_name)
# find the widget that contains kit info
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
logger.debug(f"Kit selector: {kit_widget}")
# get current kit being used
self.ext_kit = kit_widget.currentText()
# for reagent in obj.pyd.reagents:
for reagent in self.form.reagents:
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)
if reagent.missing:
missing_reagents.append(reagent)
logger.debug(f"Checking integrity of {self.ext_kit}")
# TODO: put check_kit_integrity here instead of what's here?
# see if there are any missing reagents
if len(missing_reagents) > 0:
result = Result(msg=f"""The submission you are importing is missing some reagents expected by the kit.\n\n
It looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\n
Alternatively, 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!""".replace(" ", ""), status="Warning")
report.add_result(result)
if hasattr(self.pyd, "csv"):
export_csv_btn = QPushButton("Export CSV")
export_csv_btn.setObjectName("export_csv_btn")
self.form.layout().addWidget(export_csv_btn)
export_csv_btn.clicked.connect(self.export_csv)
submit_btn = QPushButton("Submit")
submit_btn.setObjectName("submit_btn")
self.form.layout().addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample)
self.report.add_result(report)
logger.debug(f"Outgoing report: {self.report.results}")
def submit_new_sample_function(self) -> QWidget:
"""
Parse forms and add sample to the database.
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
logger.debug(f"\n\nBeginning Submission\n\n")
report = Report()
self.pyd: PydSubmission = self.form.parse_form()
logger.debug(f"Submission: {pformat(self.pyd)}")
logger.debug("Checking kit integrity...")
result = check_kit_integrity(sub=self.pyd)
report.add_result(result)
if len(result.results) > 0:
self.report.add_result(report)
return
base_submission, result = self.pyd.toSQL()
# check output message for issues
match result.code:
# code 0: everything is fine.
case 0:
self.report.add_result(None)
# code 1: ask for overwrite
case 1:
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result['message'])
if dlg.exec():
# Do not add duplicate reagents.
# base_submission.reagents = []
result = None
else:
self.app.ctx.database_session.rollback()
self.report.add_result(Result(msg="Overwrite cancelled", status="Information"))
return
# code 2: No RSL plate number given
case 2:
self.report.add_result(result)
return
case _:
pass
# add reagents to submission object
for reagent in base_submission.reagents:
update_last_used(reagent=reagent, kit=base_submission.extraction_kit)
logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
base_submission.save()
# update summary sheet
self.app.table_widget.sub_wid.setData()
# reset form
self.form.setParent(None)
logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
wkb = self.pyd.autofill_excel()
if wkb != None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
try:
wkb.save(filename=fname.__str__())
except PermissionError:
logger.error("Hit a permission error when saving workbook. Cancelled?")
if hasattr(self.pyd, 'csv'):
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
if dlg.exec():
fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
try:
self.pyd.csv.to_csv(fname.__str__(), index=False)
except PermissionError:
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
self.report.add_result(report)
def export_csv_function(self, fname:Path|None=None):
if isinstance(fname, bool) or fname == None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
try:
self.pyd.csv.to_csv(fname.__str__(), index=False)
except PermissionError:
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
def import_pcr_results(self):
self.import_pcr_results_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def import_pcr_results_function(self):
"""
Import Quant-studio PCR data to an imported submission
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
fname = select_open_file(self, file_extension="xlsx")
parser = PCRParser(filepath=fname)
logger.debug(f"Attempting lookup for {parser.plate_num}")
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
sub = BasicSubmission.query(rsl_number=parser.plate_num)
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
except AttributeError:
# If no plate is found, may be because this is a repeat. Lop off the '-1' or '-2' and repeat
logger.error(f"Submission of number {parser.plate_num} not found. Attempting rescue of plate repeat.")
parser.plate_num = "-".join(parser.plate_num.split("-")[:-1])
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=parser.plate_num)
sub = BasicSubmission.query(rsl_number=parser.plate_num)
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
except AttributeError:
logger.error(f"Rescue of {parser.plate_num} failed.")
# return obj, dict(message="Couldn't find a submission with that RSL number.", status="warning")
self.report.add_result(Result(msg="Couldn't find a submission with that RSL number.", status="Warning"))
return
# Check if PCR info already exists
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
existing = json.loads(sub.pcr_info)
else:
existing = None
if existing != None:
# update pcr_info
try:
logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}")
if json.dumps(parser.pcr) not in sub.pcr_info:
existing.append(parser.pcr)
logger.debug(f"Setting: {existing}")
sub.pcr_info = json.dumps(existing)
except TypeError:
logger.error(f"Error updating!")
sub.pcr_info = json.dumps([parser.pcr])
logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}")
else:
sub.pcr_info = json.dumps([parser.pcr])
# obj.ctx.database_session.add(sub)
logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}")
logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
sub.save()
logger.debug(f"Got {len(parser.samples)} samples to update!")
logger.debug(f"Parser samples: {parser.samples}")
for sample in sub.samples:
logger.debug(f"Running update on: {sample}")
try:
sample_dict = [item for item in parser.samples if item['sample']==sample.rsl_number][0]
except IndexError:
continue
update_subsampassoc_with_pcr(submission=sub, sample=sample, input_dict=sample_dict)
self.report.add_result(Result(msg=f"We added PCR info to {sub.rsl_plate_num}.", status='Information'))
# return obj, result
class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, **kwargs) -> None:
super().__init__(parent)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn"
# ]
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
layout = QVBoxLayout()
for k, v in kwargs.items():
if k not in self.ignore:
add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
if add_widget != None:
layout.addWidget(add_widget)
else:
setattr(self, k, v)
self.setLayout(layout)
def create_widget(self, key:str, value:dict, submission_type:str|None=None):
if key not in self.ignore:
return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
return None
def clear_form(self):
for item in self.findChildren(QWidget):
item.setParent(None)
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
query = self.findChildren(QWidget)
if object_name != None:
query = [widget for widget in query if widget.objectName()==object_name]
return query
def parse_form(self) -> PydSubmission:
logger.debug(f"Hello from form parser!")
info = {}
reagents = []
if hasattr(self, 'csv'):
info['csv'] = self.csv
for widget in self.findChildren(QWidget):
# logger.debug(f"Parsed widget of type {type(widget)}")
match widget:
case ReagentFormWidget():
reagent, _ = widget.parse_form()
if reagent != None:
reagents.append(reagent)
case self.InfoItem():
field, value = widget.parse_form()
if field != None:
info[field] = value
logger.debug(f"Info: {pformat(info)}")
logger.debug(f"Reagents: {pformat(reagents)}")
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
return submission
class InfoItem(QWidget):
def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
super().__init__(parent)
layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value'])
self.setObjectName(key)
try:
self.missing:bool = value['missing']
except (TypeError, KeyError):
self.missing:bool = True
if self.input != None:
layout.addWidget(self.label)
layout.addWidget(self.input)
layout.setContentsMargins(0,0,0,0)
self.setLayout(layout)
match self.input:
case QComboBox():
self.input.currentTextChanged.connect(self.update_missing)
case QDateEdit():
self.input.dateChanged.connect(self.update_missing)
case QLineEdit():
self.input.textChanged.connect(self.update_missing)
def parse_form(self):
match self.input:
case QLineEdit():
value = self.input.text()
case QComboBox():
value = self.input.currentText()
case QDateEdit():
value = self.input.date().toPyDate()
case _:
return None, None
return self.input.objectName(), dict(value=value, missing=self.missing)
def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
try:
value = value['value']
except (TypeError, KeyError):
pass
obj = parent.parent().parent()
logger.debug(f"Creating widget for: {key}")
match key:
case 'submitting_lab':
add_widget = QComboBox()
# lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
labs = [item.__str__() for item in Organization.query()]
# try to set closest match to top of list
try:
labs = difflib.get_close_matches(value, labs, len(labs), 0)
except (TypeError, ValueError):
pass
# set combobox values to lookedup values
add_widget.addItems(labs)
case 'extraction_kit':
# if extraction kit not available, all other values fail
if not check_not_nan(value):
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
msg.exec()
# create combobox to hold looked up kits
add_widget = QComboBox()
# lookup existing kits by 'submission_type' decided on by sheetparser
logger.debug(f"Looking up kits used for {submission_type}")
uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
obj.uses = uses
logger.debug(f"Kits received for {submission_type}: {uses}")
if check_not_nan(value):
logger.debug(f"The extraction kit in parser was: {value}")
uses.insert(0, uses.pop(uses.index(value)))
obj.ext_kit = value
else:
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
obj.ext_kit = uses[0]
add_widget.addItems(uses)
# Run reagent scraper whenever extraction kit is changed.
# add_widget.currentTextChanged.connect(obj.scrape_reagents)
case 'submitted_date':
# uses base calendar
add_widget = QDateEdit(calendarPopup=True)
# sets submitted date based on date found in excel sheet
try:
add_widget.setDate(value)
# if not found, use today
except:
add_widget.setDate(date.today())
case 'submission_category':
add_widget = QComboBox()
cats = ['Diagnostic', "Surveillance", "Research"]
# cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
cats += [item.name for item in SubmissionType.query()]
try:
cats.insert(0, cats.pop(cats.index(value)))
except ValueError:
cats.insert(0, cats.pop(cats.index(submission_type)))
add_widget.addItems(cats)
case _:
# anything else gets added in as a line edit
add_widget = QLineEdit()
logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
add_widget.setText(str(value).replace("_", " "))
if add_widget != None:
add_widget.setObjectName(key)
add_widget.setParent(parent)
return add_widget
def update_missing(self):
self.missing = True
self.label.updated(self.objectName())
class ParsedQLabel(QLabel):
def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
super().__init__()
try:
check = not value['missing']
except:
check = True
if label_name != None:
self.setObjectName(label_name)
else:
self.setObjectName(f"{key}_label")
if title:
output = key.replace('_', ' ').title()
else:
output = key.replace('_', ' ')
if check:
self.setText(f"Parsed {output}")
else:
self.setText(f"MISSING {output}")
def updated(self, key:str, title:bool=True):
if title:
output = key.replace('_', ' ').title()
else:
output = key.replace('_', ' ')
self.setText(f"UPDATED {output}")
class ReagentFormWidget(QWidget):
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
super().__init__(parent)
# self.setParent(parent)
self.reagent = reagent
self.extraction_kit = extraction_kit
# self.ctx = reagent.ctx
layout = QVBoxLayout()
self.label = self.ReagentParsedLabel(reagent=reagent)
layout.addWidget(self.label)
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
layout.addWidget(self.lot)
# Remove spacing between reagents
layout.setContentsMargins(0,0,0,0)
self.setLayout(layout)
self.setObjectName(reagent.name)
self.missing = reagent.missing
# If changed set self.missing to True and update self.label
self.lot.currentTextChanged.connect(self.updated)
def parse_form(self) -> Tuple[PydReagent, dict]:
lot = self.lot.currentText()
# wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
# if reagent doesn't exist in database, off to add it (uses App.add_reagent)
if wanted_reagent == None:
dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec():
wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
return wanted_reagent, None
else:
# In this case we will have an empty reagent and the submission will fail kit integrity check
logger.debug("Will not add reagent.")
return None, Result(msg="Failed integrity check", status="Critical")
else:
# Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
# rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
rt = ReagentType.query(name=self.reagent.type)
if rt == None:
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
def updated(self):
self.missing = True
self.label.updated(self.reagent.type)
class ReagentParsedLabel(QLabel):
def __init__(self, reagent:PydReagent):
super().__init__()
try:
check = not reagent.missing
except:
check = False
self.setObjectName(f"{reagent.type}_label")
if check:
self.setText(f"Parsed {reagent.type}")
else:
self.setText(f"MISSING {reagent.type}")
def updated(self, reagent_type:str):
self.setText(f"UPDATED {reagent_type}")
class ReagentLot(QComboBox):
def __init__(self, reagent, extraction_kit:str) -> None:
super().__init__()
# self.ctx = reagent.ctx
self.setEditable(True)
# if reagent.parsed:
# pass
logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
lookup = Reagent.query(reagent_type=reagent.type)
relevant_reagents = [item.__str__() for item in lookup]
output_reg = []
for rel_reagent in relevant_reagents:
# extract strings from any sets.
if isinstance(rel_reagent, set):
for thing in rel_reagent:
output_reg.append(thing)
elif isinstance(rel_reagent, str):
output_reg.append(rel_reagent)
relevant_reagents = output_reg
# if reagent in sheet is not found insert it into the front of relevant reagents so it shows
logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
if str(reagent.lot) not in relevant_reagents:
if check_not_nan(reagent.lot):
relevant_reagents.insert(0, str(reagent.lot))
else:
# TODO: look up the last used reagent of this type in the database
# looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
try:
# looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
except AttributeError:
looked_up_reg = None
logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
if looked_up_reg != None:
relevant_reagents.remove(str(looked_up_reg.lot))
relevant_reagents.insert(0, str(looked_up_reg.lot))
else:
if len(relevant_reagents) > 1:
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
idx = relevant_reagents.index(str(reagent.lot))
logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
moved_reag = relevant_reagents.pop(idx)
relevant_reagents.insert(0, moved_reag)
else:
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
logger.debug(f"New relevant reagents: {relevant_reagents}")
self.setObjectName(f"lot_{reagent.type}")
self.addItems(relevant_reagents)